diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c777bfc876..24c5e8d45d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -38,6 +38,14 @@ jobs: - name: Install Foundry uses: onbjerg/foundry-toolchain@v1 + - name: Free disk space + run: | + # Based on https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + - name: rust cache uses: Swatinem/rust-cache@v2 with: @@ -52,7 +60,6 @@ jobs: **/node_modules .yarn/cache key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - name: build test run: cargo build --release --bin run-locally - name: run test diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f329182d6e..1b4a49c044 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -40,6 +40,13 @@ jobs: shared-key: "test" workspaces: | ./rust + - name: Free disk space + run: | + # Based on https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Run tests run: cargo test @@ -61,6 +68,13 @@ jobs: shared-key: "lint" workspaces: | ./rust + - name: Free disk space + run: | + # Based on https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Check run: cargo check --all-features --all-targets - name: Rustfmt diff --git a/.gitignore b/.gitignore index 98b688d0eb..4989ff4f48 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ test_deploy.env **/*.swp **/*.swo -rust/vendor/ rust/tmp_db rust/tmp.env tmp.env diff --git a/.prettierignore b/.prettierignore index f130e76564..a37cd3e514 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,4 +7,3 @@ *.Dockerfile Dockerfile - diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 30c86ef126..d24082f19a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -23,11 +23,27 @@ dependencies = [ "which", ] +[[package]] +name = "access-control" +version = "0.1.0" +dependencies = [ + "solana-program", +] + +[[package]] +name = "account-utils" +version = "0.1.0" +dependencies = [ + "borsh 0.9.3", + "solana-program", + "spl-type-length-value", +] + [[package]] name = "addr2line" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" dependencies = [ "gimli", ] @@ -38,6 +54,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.7", +] + [[package]] name = "aes" version = "0.7.5" @@ -52,22 +78,37 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher 0.4.4", "cpufeatures", ] +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes 0.8.3", + "cipher 0.4.4", + "ctr 0.9.2", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.10", "once_cell", "version_check", ] @@ -85,9 +126,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -98,6 +139,33 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -107,17 +175,81 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "ascii" @@ -125,6 +257,74 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.22", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-compression" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0122885821398cc923ece939e24d1056a2384ee719432397fa9db87230ff11" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -142,8 +342,8 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] @@ -153,8 +353,8 @@ version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] @@ -187,6 +387,17 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "auto_impl" version = "0.5.0" @@ -194,8 +405,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7862e21c893d65a1650125d157eaeec691439379a1cee17ee49031b79236ada4" dependencies = [ "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -206,8 +417,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" dependencies = [ "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -219,9 +430,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" dependencies = [ "addr2line", "cc", @@ -248,8 +459,8 @@ checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a" dependencies = [ "heck 0.3.3", "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -289,9 +500,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -313,11 +524,11 @@ checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" [[package]] name = "bigdecimal" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" dependencies = [ - "num-bigint", + "num-bigint 0.4.3", "num-integer", "num-traits", ] @@ -337,14 +548,14 @@ version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", "lazycell", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "regex", "rustc-hash", "shlex", @@ -357,6 +568,21 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bitvec" version = "0.17.4" @@ -385,7 +611,21 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", +] + +[[package]] +name = "blake3" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", ] [[package]] @@ -440,26 +680,60 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "008b57b368e638ed60664350ea4f2f3647a0192173478df2736cc255a025a796" +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive 0.9.3", + "hashbrown 0.11.2", +] + [[package]] name = "borsh" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ - "borsh-derive", + "borsh-derive 0.10.3", "hashbrown 0.13.2", ] +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", + "proc-macro-crate 0.1.5", + "proc-macro2 1.0.58", + "syn 1.0.109", +] + [[package]] name = "borsh-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", + "borsh-derive-internal 0.10.3", + "borsh-schema-derive-internal 0.10.3", "proc-macro-crate 0.1.5", - "proc-macro2", + "proc-macro2 1.0.58", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -469,8 +743,19 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -480,11 +765,32 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bs58" version = "0.4.0" @@ -493,9 +799,19 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bv" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] [[package]] name = "byte-slice-cast" @@ -511,9 +827,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytecheck" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -522,15 +838,35 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -546,6 +882,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + [[package]] name = "bzip2-sys" version = "0.1.11+1.0.8" @@ -566,6 +912,16 @@ dependencies = [ "serde", ] +[[package]] +name = "caps" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" +dependencies = [ + "libc", + "thiserror", +] + [[package]] name = "cargo-platform" version = "0.1.2" @@ -615,13 +971,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", "time 0.1.45", @@ -629,6 +985,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-humanize" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32dce1ea1988dbdf9f9815ff11425828523bd2a134ec0805d2ac8af26ee6096e" +dependencies = [ + "chrono", +] + [[package]] name = "cipher" version = "0.3.0" @@ -661,16 +1026,58 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.25" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "bitflags", - "clap_derive", - "clap_lex", - "indexmap", + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_derive 3.2.25", + "clap_lex 0.2.4", + "indexmap", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap 0.16.0", +] + +[[package]] +name = "clap" +version = "4.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba77a07e4489fb41bd90e8d4201c3eb246b3c2c9ea2ba0bddd6c1d1df87db7d" +dependencies = [ + "clap_builder", + "clap_derive 4.3.2", "once_cell", - "textwrap", +] + +[[package]] +name = "clap_builder" +version = "4.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9b4a88bb4bc35d3d6f65a21b0f0bafe9c894fa00978de242c555ec28bea1c0" +dependencies = [ + "anstream", + "anstyle", + "bitflags 1.3.2", + "clap_lex 0.5.0", + "strsim 0.10.0", ] [[package]] @@ -681,11 +1088,23 @@ checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ "heck 0.4.1", "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -696,20 +1115,16 @@ dependencies = [ ] [[package]] -name = "cobs" -version = "0.2.3" +name = "clap_lex" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "cobs" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] name = "coins-bip32" @@ -720,13 +1135,13 @@ dependencies = [ "bincode", "bs58", "coins-core", - "digest 0.10.6", - "getrandom", + "digest 0.10.7", + "getrandom 0.2.10", "hmac 0.12.1", "k256", "lazy_static", "serde", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", ] @@ -738,12 +1153,12 @@ checksum = "2a11892bcac83b4c6e95ab84b5b06c76d9d70ad73548dd07418269c5c7977171" dependencies = [ "bitvec 0.17.4", "coins-bip32", - "getrandom", + "getrandom 0.2.10", "hex 0.4.3", "hmac 0.12.1", "pbkdf2 0.11.0", - "rand", - "sha2 0.10.6", + "rand 0.8.5", + "sha2 0.10.7", "thiserror", ] @@ -757,14 +1172,14 @@ dependencies = [ "base64 0.12.3", "bech32 0.7.3", "blake2", - "digest 0.10.6", + "digest 0.10.7", "generic-array 0.14.7", "hex 0.4.3", "ripemd", "serde", "serde_derive", - "sha2 0.10.6", - "sha3 0.10.7", + "sha2 0.10.7", + "sha3 0.10.8", "thiserror", ] @@ -795,6 +1210,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "combine" version = "3.8.1" @@ -827,6 +1248,39 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.45.0", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "const-oid" version = "0.7.1" @@ -835,9 +1289,15 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" + +[[package]] +name = "constant_time_eq" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" [[package]] name = "convert_case" @@ -861,23 +1321,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.3.20", + "time 0.3.22", "version_check", ] [[package]] name = "cookie_store" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e4b6aa369f41f5faa04bb80c9b1f4216ea81646ed6124d76ba5c49a7aafd9cd" +checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" dependencies = [ "cookie", "idna 0.2.3", "log", "publicsuffix", "serde", + "serde_derive", "serde_json", - "time 0.3.20", + "time 0.3.22", "url", ] @@ -908,9 +1369,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -940,6 +1401,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.9.0", + "scopeguard", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -952,9 +1437,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -972,7 +1457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ "generic-array 0.14.7", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -984,7 +1469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array 0.14.7", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -996,14 +1481,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.7", + "rand_core 0.6.4", "typenum", ] [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.7", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ "generic-array 0.14.7", "subtle", @@ -1038,56 +1534,25 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" -dependencies = [ - "nix", - "windows-sys 0.45.0", -] - -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.15", + "nix 0.26.2", + "windows-sys 0.48.0", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +name = "curve25519-dalek" +version = "3.2.2" +source = "git+https://github.com/Eclipse-Laboratories-Inc/curve25519-dalek?branch=v3.2.2-relax-zeroize#5154e5d02be0d9a7486dde86d67ff0327511c717" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "serde", + "subtle", + "zeroize", ] [[package]] @@ -1114,9 +1579,9 @@ dependencies = [ "darling 0.13.4", "graphql-parser", "once_cell", - "proc-macro2", - "quote", - "strsim", + "proc-macro2 1.0.58", + "quote 1.0.26", + "strsim 0.10.0", "syn 1.0.109", ] @@ -1158,9 +1623,9 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", - "strsim", + "proc-macro2 1.0.58", + "quote 1.0.26", + "strsim 0.10.0", "syn 1.0.109", ] @@ -1172,9 +1637,9 @@ checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", - "strsim", + "proc-macro2 1.0.58", + "quote 1.0.26", + "strsim 0.10.0", "syn 1.0.109", ] @@ -1185,7 +1650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core 0.13.4", - "quote", + "quote 1.0.26", "syn 1.0.109", ] @@ -1196,10 +1661,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core 0.14.4", - "quote", + "quote 1.0.26", "syn 1.0.109", ] +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if", + "num_cpus", + "rayon", +] + [[package]] name = "dashmap" version = "5.4.0" @@ -1210,9 +1686,15 @@ dependencies = [ "hashbrown 0.12.3", "lock_api", "once_cell", - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.8", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "der" version = "0.5.1" @@ -1228,18 +1710,38 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ - "const-oid 0.9.2", + "const-oid 0.9.3", "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.3", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derivative" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -1249,8 +1751,8 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -1270,8 +1772,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ "darling 0.14.4", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -1292,12 +1794,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case 0.4.0", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "rustc_version", "syn 1.0.109", ] +[[package]] +name = "dialoguer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "difflib" version = "0.4.0" @@ -1324,15 +1838,24 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", "subtle", ] +[[package]] +name = "dir-diff" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2860407d7d7e2e004bb2128510ad9e8d669e76fa005ccf567977b5d71b8b4a0b" +dependencies = [ + "walkdir", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1374,6 +1897,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", +] + +[[package]] +name = "dlopen" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -1398,6 +1955,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "eager" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe71d579d1812060163dff96056261deb5bf6729b100fa2e36a68b9649ba3d3" + [[package]] name = "ecdsa" version = "0.14.8" @@ -1410,6 +1973,61 @@ dependencies = [ "signature", ] +[[package]] +name = "ecdsa-signature" +version = "0.1.0" +dependencies = [ + "hyperlane-core", + "solana-program", + "thiserror", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "git+https://github.com/Eclipse-Laboratories-Inc/ed25519-dalek?branch=main#7529d65506147b6cb24ca6d8f4fc062cac33b395" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek-bip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +dependencies = [ + "derivation-path", + "ed25519-dalek", + "hmac 0.12.1", + "sha2 0.10.7", +] + +[[package]] +name = "educe" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079044df30bb07de7d846d41a184c4b00e66ebdac93ee459253474f3a47e50ae" +dependencies = [ + "enum-ordinalize", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.8.1" @@ -1426,7 +2044,7 @@ dependencies = [ "crypto-bigint 0.3.2", "der 0.5.1", "generic-array 0.14.7", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1440,17 +2058,23 @@ dependencies = [ "base16ct", "crypto-bigint 0.4.9", "der 0.6.1", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array 0.14.7", "group", - "pkcs8", - "rand_core", + "pkcs8 0.9.0", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -1461,26 +2085,85 @@ dependencies = [ ] [[package]] -name = "enum_dispatch" -version = "0.3.11" +name = "enum-iterator" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" +checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9" dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", + "enum-iterator-derive", ] [[package]] -name = "errno" -version = "0.3.1" +name = "enum-iterator-derive" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f76552f53cefc9a7f64987c3701b99d982f7690606fd67de1d09712fbf52f1" +dependencies = [ + "num-bigint 0.4.3", + "num-traits", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" +dependencies = [ + "once_cell", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime 2.1.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", ] [[package]] @@ -1505,7 +2188,7 @@ dependencies = [ "hex 0.4.3", "hmac 0.11.0", "pbkdf2 0.8.0", - "rand", + "rand 0.8.5", "scrypt 0.7.0", "serde", "serde_json", @@ -1521,18 +2204,18 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" dependencies = [ - "aes 0.8.2", + "aes 0.8.3", "ctr 0.9.2", - "digest 0.10.6", + "digest 0.10.7", "hex 0.4.3", "hmac 0.12.1", "pbkdf2 0.11.0", - "rand", + "rand 0.8.5", "scrypt 0.10.0", "serde", "serde_json", - "sha2 0.10.6", - "sha3 0.10.7", + "sha2 0.10.7", + "sha3 0.10.8", "thiserror", "uuid 0.8.2", ] @@ -1549,7 +2232,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha3 0.10.7", + "sha3 0.10.8", "thiserror", "uint", ] @@ -1561,10 +2244,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" dependencies = [ "crunchy", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", + "fixed-hash 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "impl-codec 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "impl-rlp 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "impl-serde 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "scale-info", "tiny-keccak", ] @@ -1576,10 +2259,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" dependencies = [ "ethbloom", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", + "fixed-hash 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "impl-codec 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "impl-rlp 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "impl-serde 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "primitive-types", "scale-info", "uint", @@ -1638,10 +2321,10 @@ dependencies = [ "dunce", "ethers-core", "eyre", - "getrandom", + "getrandom 0.2.10", "hex 0.4.3", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "regex", "reqwest", "serde", @@ -1660,8 +2343,8 @@ dependencies = [ "ethers-contract-abigen", "ethers-core", "hex 0.4.3", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "serde_json", "syn 1.0.109", ] @@ -1683,8 +2366,8 @@ dependencies = [ "k256", "once_cell", "open-fastrlp", - "proc-macro2", - "rand", + "proc-macro2 1.0.58", + "rand 0.8.5", "rlp", "rlp-derive", "serde", @@ -1693,7 +2376,7 @@ dependencies = [ "syn 1.0.109", "thiserror", "tiny-keccak", - "unicode-xid", + "unicode-xid 0.2.4", ] [[package]] @@ -1702,7 +2385,7 @@ version = "1.0.2" source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2023-06-01#6c26645cc1707a3d9c987a603a513fb38a5edb3f" dependencies = [ "ethers-core", - "getrandom", + "getrandom 0.2.10", "reqwest", "semver", "serde", @@ -1771,7 +2454,7 @@ dependencies = [ "futures-core", "futures-timer", "futures-util", - "getrandom", + "getrandom 0.2.10", "hashers", "hex 0.4.3", "http", @@ -1806,11 +2489,11 @@ dependencies = [ "eth-keystore 0.5.0", "ethers-core", "hex 0.4.3", - "rand", + "rand 0.8.5", "rusoto_core", "rusoto_kms", - "sha2 0.10.6", - "spki", + "sha2 0.10.7", + "spki 0.6.0", "thiserror", "tracing", ] @@ -1861,16 +2544,34 @@ dependencies = [ "instant", ] +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + [[package]] name = "ff" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -1878,11 +2579,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane#3c2a89084ccfc27b82fda29007b4e27215a75cb1" +dependencies = [ + "byteorder", + "rand 0.8.5", "rustc-hex", "static_assertions", ] +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -1915,9 +2637,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1952,9 +2674,9 @@ dependencies = [ [[package]] name = "fuel-core-chain-config" -version = "0.17.11" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c858f8848e1f1a3d2dc060679983b75bd3160e62a5dfcf7afc2e5eabfdfa227" +checksum = "cff4779c1649be51e1244aca7322c625064d052f9f6f529feb26f538cc839bd6" dependencies = [ "anyhow", "bech32 0.9.1", @@ -1963,7 +2685,7 @@ dependencies = [ "hex 0.4.3", "itertools", "postcard", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_with", @@ -1972,9 +2694,9 @@ dependencies = [ [[package]] name = "fuel-core-client" -version = "0.17.11" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808f7273c7ea1ad10766794701f4dfc234426aa5a8b7e69720af007cd551889d" +checksum = "39b386680fb36dfee949c2977c1c682cf459c6b7862d243fb4615193b769d2b9" dependencies = [ "anyhow", "cynic", @@ -1995,9 +2717,9 @@ dependencies = [ [[package]] name = "fuel-core-storage" -version = "0.17.11" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0430e82e41e840c954645450113bae55ab5b31d8f0e77154c94fa6eb6c01a04" +checksum = "240b31746485e24215a1159b0887f2013545c78252377d55601b07f9f25367ae" dependencies = [ "anyhow", "fuel-core-types", @@ -2007,9 +2729,9 @@ dependencies = [ [[package]] name = "fuel-core-types" -version = "0.17.11" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b7da3d01bd0cc89a8e74f4b40bd5bf160fcec3b506d8503bdb0703846fc567" +checksum = "c75b20301e07c8dfd793c8a5385d3b2ee0e80c36f7323957260174819d8a25fe" dependencies = [ "anyhow", "derive_more", @@ -2031,12 +2753,12 @@ dependencies = [ "coins-bip32", "coins-bip39", "fuel-types", - "getrandom", + "getrandom 0.2.10", "lazy_static", - "rand", + "rand 0.8.5", "secp256k1", "serde", - "sha2 0.10.6", + "sha2 0.10.7", "zeroize", ] @@ -2046,11 +2768,11 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b13103bf12f62930dd26f75f90d6a95d952fdcd677a356f57d8ef8df7ae02b84" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "fuel-storage", "hashbrown 0.13.2", "hex 0.4.3", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", ] @@ -2073,7 +2795,7 @@ dependencies = [ "fuel-types", "itertools", "num-integer", - "rand", + "rand 0.8.5", "serde", "serde_json", ] @@ -2085,7 +2807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89fc99a9878b98135c4b05c71fe63b82f4cb3a00abac278935f8be7282f8e468" dependencies = [ "hex 0.4.3", - "rand", + "rand 0.8.5", "serde", ] @@ -2095,7 +2817,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b36aac727729b94c620265da76112e1d36a1af0c067745491c376f084f5b7b38" dependencies = [ - "bitflags", + "bitflags 1.3.2", "derivative", "fuel-asm", "fuel-crypto", @@ -2104,9 +2826,9 @@ dependencies = [ "fuel-tx", "fuel-types", "itertools", - "rand", + "rand 0.8.5", "serde", - "sha3 0.10.7", + "sha3 0.10.8", "tai64", "thiserror", ] @@ -2137,8 +2859,8 @@ dependencies = [ "fuel-abi-types", "itertools", "lazy_static", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "regex", "serde_json", "syn 1.0.109", @@ -2170,9 +2892,9 @@ dependencies = [ "fuels-code-gen", "itertools", "lazy_static", - "proc-macro2", - "quote", - "rand", + "proc-macro2 1.0.58", + "quote 1.0.26", + "rand 0.8.5", "regex", "serde_json", "syn 1.0.109", @@ -2195,8 +2917,8 @@ dependencies = [ "futures", "hex 0.4.3", "itertools", - "proc-macro2", - "rand", + "proc-macro2 1.0.58", + "rand 0.8.5", "regex", "serde", "serde_json", @@ -2227,7 +2949,7 @@ dependencies = [ "fuels-types", "hex 0.4.3", "itertools", - "rand", + "rand 0.8.5", "serde", "sha2 0.9.9", "tai64", @@ -2252,7 +2974,7 @@ dependencies = [ "futures", "hex 0.4.3", "portpicker", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_with", @@ -2279,7 +3001,7 @@ dependencies = [ "hex 0.4.3", "itertools", "lazy_static", - "proc-macro2", + "proc-macro2 1.0.58", "regex", "serde", "serde_json", @@ -2369,8 +3091,8 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] @@ -2434,15 +3156,39 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ + "serde", "typenum", "version_check", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" -version = "0.2.9" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "js-sys", @@ -2453,9 +3199,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "glob" @@ -2463,6 +3209,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "goblin" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7666983ed0dd8d21a6f6576ee00053ca0926fb281a5522577a4dbd0f1b54143" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "graphql-parser" version = "0.4.0" @@ -2480,15 +3237,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] [[package]] name = "h2" -version = "0.3.18" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -2499,7 +3256,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.8", "tracing", ] @@ -2512,6 +3269,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2530,6 +3296,16 @@ dependencies = [ "ahash 0.8.3", ] +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + [[package]] name = "hashers" version = "1.0.1" @@ -2541,11 +3317,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" dependencies = [ - "hashbrown 0.12.3", + "hashbrown 0.14.0", ] [[package]] @@ -2555,7 +3331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "bytes", "headers-core", "http", @@ -2607,9 +3383,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -2636,6 +3412,12 @@ dependencies = [ "serde", ] +[[package]] +name = "histogram" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" + [[package]] name = "hkdf" version = "0.12.3" @@ -2645,13 +3427,23 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.0", "digest 0.9.0", ] @@ -2661,7 +3453,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array 0.14.7", + "hmac 0.8.1", ] [[package]] @@ -2698,11 +3501,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -2733,7 +3551,7 @@ dependencies = [ "hyper", "log", "rustls 0.19.1", - "rustls-native-certs", + "rustls-native-certs 0.5.0", "tokio", "tokio-rustls 0.22.0", "webpki 0.21.4", @@ -2742,15 +3560,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" dependencies = [ "http", "hyper", - "rustls 0.20.8", + "rustls 0.21.2", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", ] [[package]] @@ -2785,9 +3603,11 @@ dependencies = [ "async-trait", "backtrace", "backtrace-oneline", + "bs58", "color-eyre", "config", "derive-new", + "ed25519-dalek", "ethers", "ethers-prometheus", "eyre", @@ -2796,12 +3616,12 @@ dependencies = [ "hyperlane-core", "hyperlane-ethereum", "hyperlane-fuel", + "hyperlane-sealevel", "hyperlane-test", "itertools", - "lazy_static", "mockall", "once_cell", - "opentelemetry", + "opentelemetry 0.18.0", "opentelemetry-jaeger", "opentelemetry-zipkin", "paste", @@ -2820,8 +3640,9 @@ dependencies = [ "tracing", "tracing-error", "tracing-futures", - "tracing-opentelemetry", + "tracing-opentelemetry 0.18.0", "tracing-subscriber", + "walkdir", "warp", ] @@ -2831,30 +3652,34 @@ version = "0.1.0" dependencies = [ "async-trait", "auto_impl 1.1.0", + "borsh 0.9.3", + "bs58", "bytes", "config", "convert_case 0.6.0", "derive-new", "derive_more", + "ethers", "ethers-contract", "ethers-core", "ethers-providers", "eyre", + "fixed-hash 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom 0.2.10", "hex 0.4.3", - "hyperlane-base", "itertools", - "lazy_static", - "num", + "num 0.4.0", "num-derive", "num-traits", "primitive-types", "serde", "serde_json", - "sha3 0.10.7", + "sha3 0.10.8", "strum 0.24.1", "thiserror", + "tiny-keccak", "tokio", - "walkdir", + "uint", ] [[package]] @@ -2872,7 +3697,7 @@ dependencies = [ "futures-util", "hex 0.4.3", "hyperlane-core", - "num", + "num 0.4.0", "num-traits", "reqwest", "serde", @@ -2901,100 +3726,483 @@ dependencies = [ ] [[package]] -name = "hyperlane-test" +name = "hyperlane-sealevel" version = "0.1.0" dependencies = [ + "account-utils", + "anyhow", "async-trait", + "base64 0.13.1", + "borsh 0.9.3", "hyperlane-core", - "mockall", + "hyperlane-sealevel-interchain-security-module-interface", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-multisig-ism-message-id", + "hyperlane-sealevel-validator-announce", + "jsonrpc-core", + "multisig-ism", + "num-traits", + "serde", + "serializable-account-meta", + "solana-account-decoder", + "solana-client", + "solana-sdk", + "solana-transaction-status", + "thiserror", + "tracing", + "tracing-futures", + "url", ] [[package]] -name = "iana-time-zone" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +name = "hyperlane-sealevel-client" +version = "0.1.0" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", + "account-utils", + "borsh 0.9.3", + "clap 4.3.9", + "hex 0.4.3", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-multisig-ism-message-id", + "hyperlane-sealevel-token", + "hyperlane-sealevel-token-collateral", + "hyperlane-sealevel-token-lib", + "hyperlane-sealevel-token-native", + "hyperlane-sealevel-validator-announce", + "pretty_env_logger", + "serde", + "serde_json", + "solana-clap-utils", + "solana-cli-config", + "solana-client", + "solana-program", + "solana-sdk", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +name = "hyperlane-sealevel-connection-client" +version = "0.1.0" dependencies = [ - "cxx", - "cxx-build", + "access-control", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-mailbox", + "solana-program", ] [[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +name = "hyperlane-sealevel-interchain-security-module-interface" +version = "0.1.0" +dependencies = [ + "borsh 0.9.3", + "solana-program", + "spl-type-length-value", +] [[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +name = "hyperlane-sealevel-mailbox" +version = "0.1.0" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "access-control", + "account-utils", + "base64 0.13.1", + "borsh 0.9.3", + "getrandom 0.2.10", + "hyperlane-core", + "hyperlane-sealevel-interchain-security-module-interface", + "hyperlane-sealevel-message-recipient-interface", + "itertools", + "log", + "num-derive", + "num-traits", + "serializable-account-meta", + "solana-program", + "spl-noop", + "thiserror", ] [[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +name = "hyperlane-sealevel-mailbox-test" +version = "0.1.0" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "access-control", + "account-utils", + "base64 0.13.1", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-interchain-security-module-interface", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-sealevel-test-send-receiver", + "hyperlane-test-utils", + "itertools", + "log", + "num-derive", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-noop", + "thiserror", ] [[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +name = "hyperlane-sealevel-message-recipient-interface" +version = "0.1.0" dependencies = [ - "parity-scale-codec", + "borsh 0.9.3", + "hyperlane-core", + "solana-program", + "spl-type-length-value", ] [[package]] -name = "impl-rlp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +name = "hyperlane-sealevel-multisig-ism-message-id" +version = "0.1.0" dependencies = [ - "rlp", + "access-control", + "account-utils", + "borsh 0.9.3", + "ecdsa-signature", + "hex 0.4.3", + "hyperlane-core", + "hyperlane-sealevel-interchain-security-module-interface", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-multisig-ism-message-id", + "multisig-ism", + "num-derive", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "thiserror", ] [[package]] -name = "impl-serde" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +name = "hyperlane-sealevel-test-ism" +version = "0.1.0" dependencies = [ - "serde", + "account-utils", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-interchain-security-module-interface", + "hyperlane-sealevel-mailbox", + "hyperlane-test-transaction-utils", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", ] [[package]] -name = "impl-trait-for-tuples" -version = "0.2.2" +name = "hyperlane-sealevel-test-send-receiver" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh 0.9.3", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-test-utils", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-noop", +] + +[[package]] +name = "hyperlane-sealevel-token" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-sealevel-token-lib", + "hyperlane-test-utils", + "num-derive", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-associated-token-account", + "spl-noop", + "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "hyperlane-sealevel-token-collateral" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-sealevel-token-lib", + "hyperlane-test-utils", + "num-derive", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-associated-token-account", + "spl-noop", + "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "hyperlane-sealevel-token-lib" +version = "0.1.0" +dependencies = [ + "access-control", + "account-utils", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "num-derive", + "num-traits", + "serializable-account-meta", + "solana-program", + "spl-associated-token-account", + "spl-noop", + "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "hyperlane-sealevel-token-native" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-connection-client", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-sealevel-token-lib", + "hyperlane-test-utils", + "num-derive", + "num-traits", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-noop", + "tarpc", + "thiserror", +] + +[[package]] +name = "hyperlane-sealevel-validator-announce" +version = "0.1.0" +dependencies = [ + "account-utils", + "borsh 0.9.3", + "ecdsa-signature", + "hex 0.4.3", + "hyperlane-core", + "hyperlane-sealevel-mailbox", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "hyperlane-test" +version = "0.1.0" +dependencies = [ + "async-trait", + "hyperlane-core", + "mockall", +] + +[[package]] +name = "hyperlane-test-transaction-utils" +version = "0.1.0" +dependencies = [ + "solana-program", + "solana-program-test", + "solana-sdk", +] + +[[package]] +name = "hyperlane-test-utils" +version = "0.1.0" +dependencies = [ + "borsh 0.9.3", + "hyperlane-core", + "hyperlane-sealevel-interchain-security-module-interface", + "hyperlane-sealevel-mailbox", + "hyperlane-sealevel-message-recipient-interface", + "hyperlane-sealevel-test-ism", + "hyperlane-test-transaction-utils", + "serializable-account-meta", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-noop", + "spl-token-2022", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "rayon", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane#3c2a89084ccfc27b82fda29007b4e27215a75cb1" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane#3c2a89084ccfc27b82fda29007b4e27215a75cb1" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane#3c2a89084ccfc27b82fda29007b4e27215a75cb1" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -3004,6 +4212,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "index_list" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9d968042a4902e08810946fc7cd5851eb75e80301342305af755ca06cb82ce" + [[package]] name = "indexmap" version = "1.9.3" @@ -3014,6 +4228,18 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indicatif" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + [[package]] name = "inout" version = "0.1.3" @@ -3043,9 +4269,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", @@ -3054,9 +4280,20 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.7.2" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" +dependencies = [ + "hermit-abi 0.3.1", + "rustix 0.38.1", + "windows-sys 0.48.0", +] [[package]] name = "itertools" @@ -3084,9 +4321,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -3102,6 +4339,21 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "k256" version = "0.11.6" @@ -3111,15 +4363,15 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve 0.12.3", - "sha2 0.10.6", - "sha3 0.10.7", + "sha2 0.10.7", + "sha3 0.10.8", ] [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] @@ -3138,9 +4390,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libloading" @@ -3169,58 +4421,119 @@ dependencies = [ ] [[package]] -name = "libz-sys" -version = "1.1.9" +name = "libsecp256k1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" dependencies = [ - "cc", - "pkg-config", - "vcpkg", + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "typenum", ] [[package]] -name = "link-cplusplus" -version = "1.0.8" +name = "libsecp256k1-core" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" dependencies = [ - "cc", + "crunchy", + "digest 0.9.0", + "subtle", ] [[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" - -[[package]] -name = "lock_api" -version = "0.4.9" +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" dependencies = [ - "autocfg", - "scopeguard", + "libsecp256k1-core", ] [[package]] -name = "log" -version = "0.4.17" +name = "libsecp256k1-gen-genmult" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" dependencies = [ - "cfg-if", + "libsecp256k1-core", ] [[package]] -name = "lz4-sys" +name = "libz-sys" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lz4" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1" +dependencies = [ + "libc", + "lz4-sys", +] + +[[package]] +name = "lz4-sys" version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" @@ -3267,7 +4580,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3276,15 +4589,53 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "migration" version = "0.1.0" dependencies = [ - "hyperlane-core", "sea-orm", "sea-orm-migration", "serde", - "time 0.3.20", + "time 0.3.22", "tokio", "tracing", "tracing-subscriber", @@ -3314,23 +4665,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -3355,8 +4705,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" dependencies = [ "cfg-if", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -3378,6 +4749,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "multisig-ism" +version = "0.1.0" +dependencies = [ + "borsh 0.9.3", + "ecdsa-signature", + "hex 0.4.3", + "hyperlane-core", + "solana-program", + "spl-type-length-value", + "thiserror", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -3396,13 +4780,25 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", "static_assertions", @@ -3434,17 +4830,42 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex 0.2.4", + "num-integer", + "num-iter", + "num-rational 0.2.4", + "num-traits", +] + [[package]] name = "num" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" dependencies = [ - "num-bigint", - "num-complex", + "num-bigint 0.4.3", + "num-complex 0.4.3", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.1", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", "num-traits", ] @@ -3460,6 +4881,16 @@ dependencies = [ "serde", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.3" @@ -3476,8 +4907,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -3502,6 +4933,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -3509,7 +4952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", - "num-bigint", + "num-bigint 0.4.3", "num-integer", "num-traits", "serde", @@ -3526,28 +4969,85 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi 0.3.1", "libc", ] +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive 0.5.7", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" -version = "0.30.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -3581,18 +5081,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" dependencies = [ "bytes", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -3607,8 +5107,8 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] @@ -3620,9 +5120,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ "cc", "libc", @@ -3630,6 +5130,25 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "js-sys", + "lazy_static", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "thiserror", +] + [[package]] name = "opentelemetry" version = "0.18.0" @@ -3665,7 +5184,7 @@ dependencies = [ "headers", "http", "once_cell", - "opentelemetry", + "opentelemetry 0.18.0", "opentelemetry-http", "opentelemetry-semantic-conventions", "reqwest", @@ -3680,7 +5199,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b02e0230abb0ab6636d18e2ba8fa02903ea63772281340ccac18e0af3ec9eeb" dependencies = [ - "opentelemetry", + "opentelemetry 0.18.0", ] [[package]] @@ -3693,7 +5212,7 @@ dependencies = [ "futures-core", "http", "once_cell", - "opentelemetry", + "opentelemetry 0.18.0", "opentelemetry-http", "opentelemetry-semantic-conventions", "reqwest", @@ -3727,7 +5246,7 @@ checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113" dependencies = [ "async-trait", "crossbeam-channel", - "dashmap", + "dashmap 5.4.0", "fnv", "futures-channel", "futures-executor", @@ -3735,7 +5254,7 @@ dependencies = [ "once_cell", "opentelemetry_api", "percent-encoding", - "rand", + "rand 0.8.5", "thiserror", "tokio", "tokio-stream", @@ -3762,9 +5281,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "ouroboros" @@ -3784,8 +5303,8 @@ checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" dependencies = [ "Inflector", "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -3821,9 +5340,9 @@ version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -3845,7 +5364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.8", ] [[package]] @@ -3864,25 +5383,25 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.1", ] [[package]] name = "password-hash" -version = "0.2.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" +checksum = "c1a5d4e9c205d2c1ae73b84aab6240e98218c0e72e63b50422cfb2d1ca952282" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3893,7 +5412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -3909,6 +5428,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac 0.8.0", +] + [[package]] name = "pbkdf2" version = "0.8.0" @@ -3916,9 +5444,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" dependencies = [ "base64ct", - "crypto-mac", + "crypto-mac 0.11.0", "hmac 0.11.0", - "password-hash 0.2.3", + "password-hash 0.2.1", "sha2 0.9.9", ] @@ -3928,10 +5456,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "hmac 0.12.1", "password-hash 0.4.2", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -3940,17 +5468,35 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "percentage" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "2fd23b938276f14057220b707937bcb42fa76dda7560e57a2da30cb52d557937" +dependencies = [ + "num 0.2.1", +] [[package]] name = "pest" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" +checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" dependencies = [ "thiserror", "ucd-trie", @@ -3958,9 +5504,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" +checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" dependencies = [ "pest", "pest_generator", @@ -3968,26 +5514,26 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" +checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" dependencies = [ "pest", "pest_meta", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] [[package]] name = "pest_meta" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" +checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" dependencies = [ "once_cell", "pest", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -4015,8 +5561,8 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -4032,6 +5578,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der 0.5.1", + "spki 0.5.4", + "zeroize", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -4039,22 +5596,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ "der 0.6.1", - "spki", + "spki 0.6.0", ] [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] -name = "portpicker" -version = "0.1.1" +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug 0.3.0", + "universal-hash", +] + +[[package]] +name = "portpicker" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -4104,16 +5679,25 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger 0.7.1", + "log", +] + [[package]] name = "primitive-types" version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +source = "git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane#3c2a89084ccfc27b82fda29007b4e27215a75cb1" dependencies = [ - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", + "fixed-hash 0.8.0 (git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane)", + "impl-codec 0.6.0 (git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane)", + "impl-rlp 0.3.0 (git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane)", + "impl-serde 0.4.0 (git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane)", "scale-info", "uint", ] @@ -4129,12 +5713,13 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ "once_cell", - "toml_edit", + "thiserror", + "toml", ] [[package]] @@ -4144,8 +5729,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", "version_check", ] @@ -4156,16 +5741,25 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.56" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" dependencies = [ "unicode-ident", ] @@ -4212,8 +5806,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -4227,13 +5821,90 @@ dependencies = [ "psl-types", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b435e71d9bfa0d8889927231970c51fb89c58fa63bffcab117c9c7a41e5ef8f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "fxhash", + "quinn-proto", + "quinn-udp", + "rustls 0.20.8", + "thiserror", + "tokio", + "tracing", + "webpki 0.22.0", +] + +[[package]] +name = "quinn-proto" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce546b9688f767a57530652488420d419a8b1f44a478b451c3d1ab6d992a55" +dependencies = [ + "bytes", + "fxhash", + "rand 0.8.5", + "ring", + "rustls 0.20.8", + "rustls-native-certs 0.6.3", + "rustls-pemfile 0.2.1", + "slab", + "thiserror", + "tinyvec", + "tracing", + "webpki 0.22.0", +] + +[[package]] +name = "quinn-udp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07946277141531aea269befd949ed16b2c85a780ba1043244eda0969e538e54" +dependencies = [ + "futures-util", + "libc", + "quinn-proto", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + [[package]] name = "quote" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.58", ] [[package]] @@ -4248,6 +5919,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -4255,8 +5939,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -4266,7 +5960,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -4275,7 +5978,59 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.10", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rcgen" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" +dependencies = [ + "pem", + "ring", + "time 0.3.22", + "yasna", ] [[package]] @@ -4284,7 +6039,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -4293,7 +6048,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -4302,20 +6057,20 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.10", "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -4335,9 +6090,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "relayer" @@ -4384,11 +6139,12 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.0", + "async-compression", + "base64 0.21.2", "bytes", "cookie", "cookie_store", @@ -4399,7 +6155,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls 0.23.2", + "hyper-rustls 0.24.0", "hyper-tls", "ipnet", "js-sys", @@ -4409,14 +6165,15 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.20.8", - "rustls-pemfile", + "rustls 0.21.2", + "rustls-pemfile 1.0.3", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", + "tokio-util 0.7.8", "tower-service", "url", "wasm-bindgen", @@ -4458,39 +6215,41 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] name = "rkyv" -version = "0.7.41" +version = "0.7.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21499ed91807f07ae081880aabb2ccc0235e9d88011867d984525e9a4c3cfa3e" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" dependencies = [ + "bitvec 1.0.1", "bytecheck", "hashbrown 0.12.3", "ptr_meta", "rend", "rkyv_derive", "seahash", + "tinyvec", + "uuid 1.4.0", ] [[package]] name = "rkyv_derive" -version = "0.7.41" +version = "0.7.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1c672430eb41556291981f45ca900a0239ad007242d1cb4b4167af842db666" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] [[package]] name = "rlp" version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +source = "git+https://github.com/hyperlane-xyz/parity-common.git?branch=hyperlane#3c2a89084ccfc27b82fda29007b4e27215a75cb1" dependencies = [ "bytes", "rustc-hex", @@ -4502,8 +6261,8 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -4524,8 +6283,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" dependencies = [ "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "rpassword" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956" +dependencies = [ + "libc", "serde", + "serde_json", + "winapi", ] [[package]] @@ -4535,7 +6306,7 @@ dependencies = [ "ctrlc", "eyre", "maplit", - "nix", + "nix 0.26.2", "tempfile", "ureq", "which", @@ -4664,17 +6435,17 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.29.1" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bd36b60561ee1fb5ec2817f198b6fd09fa571c897a5e86d1487cfc2b096dfc" +checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042" dependencies = [ "arrayvec", - "borsh", + "borsh 0.10.3", "bytecheck", "byteorder", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -4707,17 +6478,39 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" -version = "0.37.18" +version = "0.37.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" +checksum = "62f25693a73057a1b4cb56179dd3c7ea21a7c6c5ee7d85781f5749b46f34b79c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc6396159432b5c8490d4e301d8c705f61860b8b6c863bf79942ce5401968f3" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] @@ -4746,6 +6539,18 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "rustls" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct 0.7.0", +] + [[package]] name = "rustls-native-certs" version = "0.5.0" @@ -4758,13 +6563,44 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.3", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" dependencies = [ - "base64 0.21.0", + "ring", + "untrusted", ] [[package]] @@ -4808,9 +6644,9 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfdef77228a4c05dc94211441595746732131ad7f6530c6c18f045da7b7ab937" +checksum = "ad560913365790f17cbf12479491169f01b9d46d29cfc7422bf8c64bdc61b731" dependencies = [ "cfg-if", "derive_more", @@ -4820,13 +6656,13 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53012eae69e5aa5c14671942a5dd47de59d4cdcff8532a6dd0e081faf1119482" +checksum = "19df9bd9ace6cc2fe19387c96ce677e823e07d017ceed253e7bb3d1d1bd9c73b" dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -4866,13 +6702,13 @@ dependencies = [ "hyperlane-test", "itertools", "migration", - "num-bigint", + "num-bigint 0.4.3", "prometheus", "sea-orm", "serde", "serde_json", "thiserror", - "time 0.3.20", + "time 0.3.22", "tokio", "tokio-test", "tracing", @@ -4881,10 +6717,24 @@ dependencies = [ ] [[package]] -name = "scratch" -version = "1.0.5" +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", +] [[package]] name = "scrypt" @@ -4894,7 +6744,7 @@ checksum = "879588d8f90906e73302547e20fffefdd240eb3e0e744e142321f5d49dea0518" dependencies = [ "base64ct", "hmac 0.11.0", - "password-hash 0.2.3", + "password-hash 0.2.1", "pbkdf2 0.8.0", "salsa20 0.8.1", "sha2 0.9.9", @@ -4909,7 +6759,7 @@ dependencies = [ "hmac 0.12.1", "pbkdf2 0.11.0", "salsa20 0.10.2", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -4954,10 +6804,10 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.20", + "time 0.3.22", "tracing", "url", - "uuid 1.3.2", + "uuid 1.4.0", ] [[package]] @@ -4967,7 +6817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efbf34a2caf70c2e3be9bb1e674e9540f6dfd7c8f40f6f05daf3b9740e476005" dependencies = [ "chrono", - "clap", + "clap 3.2.25", "dotenvy", "regex", "sea-schema", @@ -4984,8 +6834,8 @@ checksum = "28936f26d62234ff0be16f80115dbdeb3237fe9c25cf18fbcd1e3b3592360f20" dependencies = [ "bae", "heck 0.3.3", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -4996,7 +6846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "278d3adfd0832b6ffc17d3cfbc574d3695a5c1b38814e0bc8ac238d33f3d87cf" dependencies = [ "async-trait", - "clap", + "clap 3.2.25", "dotenvy", "futures", "sea-orm", @@ -5008,24 +6858,24 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.28.4" +version = "0.28.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd34be05fdde9ec79231414bdd44ba1aa9c57349190076699e90721cb5eb59b" +checksum = "bbab99b8cd878ab7786157b7eb8df96333a6807cc6e45e8888c85b51534b401a" dependencies = [ "bigdecimal", "chrono", "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.20", - "uuid 1.3.2", + "time 0.3.22", + "uuid 1.4.0", ] [[package]] name = "sea-query-binder" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03548c63aec07afd4fd190923e0160d2f2fc92def27470b54154cf232da6203b" +checksum = "4cea85029985b40dfbf18318d85fe985c04db7c1b4e5e8e0a0a0cdff5f1e30f9" dependencies = [ "bigdecimal", "chrono", @@ -5033,8 +6883,8 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.20", - "uuid 1.3.2", + "time 0.3.22", + "uuid 1.4.0", ] [[package]] @@ -5044,8 +6894,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63f62030c60f3a691f5fe251713b4e220b306e50a71e1d6f9cce1f24bb781978" dependencies = [ "heck 0.4.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", "thiserror", ] @@ -5068,8 +6918,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56821b7076f5096b8f726e2791ad255a99c82498e08ec477a65a96c461ff1927" dependencies = [ "heck 0.3.3", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -5089,8 +6939,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21" dependencies = [ "heck 0.3.3", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "rustversion", "syn 1.0.109", ] @@ -5110,7 +6960,7 @@ dependencies = [ "base16ct", "der 0.6.1", "generic-array 0.14.7", - "pkcs8", + "pkcs8 0.9.0", "subtle", "zeroize", ] @@ -5121,7 +6971,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" dependencies = [ - "rand", + "rand 0.8.5", "secp256k1-sys", ] @@ -5145,11 +6995,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -5158,9 +7008,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -5183,9 +7033,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] @@ -5200,22 +7050,31 @@ dependencies = [ "serde_json", ] +[[package]] +name = "serde_bytes" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -5252,11 +7111,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling 0.13.4", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "serializable-account-meta" +version = "0.1.0" +dependencies = [ + "borsh 0.9.3", + "solana-program", +] + [[package]] name = "sha-1" version = "0.10.1" @@ -5265,7 +7144,7 @@ checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -5276,7 +7155,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -5306,13 +7185,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -5329,11 +7208,11 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c2bb1a323307527314a36bfb73f24febb08ce2b8a554bf4ffd6f51ad15198c" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] @@ -5347,13 +7226,19 @@ dependencies = [ ] [[package]] -name = "shlex" +name = "shell-words" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] -name = "signal-hook-registry" +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" @@ -5367,8 +7252,8 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.6", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -5377,6 +7262,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.8" @@ -5402,6 +7297,816 @@ dependencies = [ "winapi", ] +[[package]] +name = "solana-account-decoder" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "Inflector", + "base64 0.13.1", + "bincode", + "bs58", + "bv", + "lazy_static", + "serde", + "serde_derive", + "serde_json", + "solana-address-lookup-table-program", + "solana-config-program", + "solana-sdk", + "solana-vote-program", + "spl-token", + "spl-token-2022", + "thiserror", + "zstd", +] + +[[package]] +name = "solana-address-lookup-table-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "bytemuck", + "log", + "num-derive", + "num-traits", + "rustc_version", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-program", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-banks-client" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "borsh 0.9.3", + "futures", + "solana-banks-interface", + "solana-program", + "solana-sdk", + "tarpc", + "thiserror", + "tokio", + "tokio-serde", +] + +[[package]] +name = "solana-banks-interface" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "serde", + "solana-sdk", + "tarpc", +] + +[[package]] +name = "solana-banks-server" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "crossbeam-channel", + "futures", + "solana-banks-interface", + "solana-client", + "solana-runtime", + "solana-sdk", + "solana-send-transaction-service", + "tarpc", + "tokio", + "tokio-serde", + "tokio-stream", +] + +[[package]] +name = "solana-bpf-loader-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "byteorder", + "libsecp256k1", + "log", + "solana-measure", + "solana-metrics", + "solana-program-runtime", + "solana-sdk", + "solana-zk-token-sdk", + "solana_rbpf", + "thiserror", +] + +[[package]] +name = "solana-bucket-map" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "log", + "memmap2", + "modular-bitfield", + "rand 0.7.3", + "solana-measure", + "solana-sdk", + "tempfile", +] + +[[package]] +name = "solana-clap-utils" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "chrono", + "clap 2.34.0", + "rpassword", + "solana-perf", + "solana-remote-wallet", + "solana-sdk", + "thiserror", + "tiny-bip39", + "uriparse", + "url", +] + +[[package]] +name = "solana-cli-config" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "dirs-next", + "lazy_static", + "serde", + "serde_derive", + "serde_yaml", + "solana-clap-utils", + "solana-sdk", + "url", +] + +[[package]] +name = "solana-client" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "async-mutex", + "async-trait", + "base64 0.13.1", + "bincode", + "bs58", + "bytes", + "clap 2.34.0", + "crossbeam-channel", + "enum_dispatch", + "futures", + "futures-util", + "indexmap", + "indicatif", + "itertools", + "jsonrpc-core", + "lazy_static", + "log", + "quinn", + "quinn-proto", + "rand 0.7.3", + "rand_chacha 0.2.2", + "rayon", + "reqwest", + "rustls 0.20.8", + "semver", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-clap-utils", + "solana-faucet", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-sdk", + "solana-streamer", + "solana-transaction-status", + "solana-version", + "solana-vote-program", + "spl-token-2022", + "thiserror", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.17.2", + "tungstenite 0.17.3", + "url", +] + +[[package]] +name = "solana-compute-budget-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-config-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "chrono", + "serde", + "serde_derive", + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-faucet" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "byteorder", + "clap 2.34.0", + "crossbeam-channel", + "log", + "serde", + "serde_derive", + "solana-clap-utils", + "solana-cli-config", + "solana-logger", + "solana-metrics", + "solana-sdk", + "solana-version", + "spl-memo 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-frozen-abi" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "ahash 0.7.6", + "blake3", + "block-buffer 0.9.0", + "bs58", + "bv", + "byteorder", + "cc", + "either", + "generic-array 0.14.7", + "getrandom 0.1.16", + "hashbrown 0.12.3", + "im", + "lazy_static", + "log", + "memmap2", + "once_cell", + "rand_core 0.6.4", + "rustc_version", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.7", + "solana-frozen-abi-macro", + "subtle", + "thiserror", +] + +[[package]] +name = "solana-frozen-abi-macro" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "solana-logger" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "env_logger 0.9.3", + "lazy_static", + "log", +] + +[[package]] +name = "solana-measure" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "log", + "solana-sdk", +] + +[[package]] +name = "solana-metrics" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "crossbeam-channel", + "gethostname", + "lazy_static", + "log", + "reqwest", + "solana-sdk", +] + +[[package]] +name = "solana-net-utils" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "clap 3.2.25", + "crossbeam-channel", + "log", + "nix 0.24.3", + "rand 0.7.3", + "serde", + "serde_derive", + "socket2", + "solana-logger", + "solana-sdk", + "solana-version", + "tokio", + "url", +] + +[[package]] +name = "solana-perf" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "ahash 0.7.6", + "bincode", + "bv", + "caps", + "curve25519-dalek", + "dlopen", + "dlopen_derive", + "fnv", + "lazy_static", + "libc", + "log", + "nix 0.24.3", + "rand 0.7.3", + "rayon", + "serde", + "solana-metrics", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-vote-program", +] + +[[package]] +name = "solana-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "base64 0.13.1", + "bincode", + "bitflags 1.3.2", + "blake3", + "borsh 0.9.3", + "borsh-derive 0.9.3", + "bs58", + "bv", + "bytemuck", + "cc", + "console_error_panic_hook", + "console_log", + "curve25519-dalek", + "getrandom 0.2.10", + "itertools", + "js-sys", + "lazy_static", + "libc", + "libsecp256k1", + "log", + "memoffset 0.6.5", + "num-derive", + "num-traits", + "parking_lot 0.12.1", + "rand 0.7.3", + "rand_chacha 0.2.2", + "rustc_version", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.7", + "sha3 0.10.8", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk-macro", + "thiserror", + "tiny-bip39", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "solana-program-runtime" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "base64 0.13.1", + "bincode", + "eager", + "enum-iterator", + "itertools", + "libc", + "libloading", + "log", + "num-derive", + "num-traits", + "rand 0.7.3", + "rustc_version", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-measure", + "solana-metrics", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-program-test" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "assert_matches", + "async-trait", + "base64 0.13.1", + "bincode", + "chrono-humanize", + "log", + "serde", + "solana-banks-client", + "solana-banks-server", + "solana-bpf-loader-program", + "solana-logger", + "solana-program-runtime", + "solana-runtime", + "solana-sdk", + "solana-vote-program", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-rayon-threadlimit" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "lazy_static", + "num_cpus", +] + +[[package]] +name = "solana-remote-wallet" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "console", + "dialoguer", + "log", + "num-derive", + "num-traits", + "parking_lot 0.12.1", + "qstring", + "semver", + "solana-sdk", + "thiserror", + "uriparse", +] + +[[package]] +name = "solana-runtime" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "arrayref", + "bincode", + "blake3", + "bv", + "bytemuck", + "byteorder", + "bzip2", + "crossbeam-channel", + "dashmap 4.0.2", + "dir-diff", + "flate2", + "fnv", + "im", + "index_list", + "itertools", + "lazy_static", + "log", + "lru", + "lz4", + "memmap2", + "num-derive", + "num-traits", + "num_cpus", + "once_cell", + "ouroboros", + "rand 0.7.3", + "rayon", + "regex", + "rustc_version", + "serde", + "serde_derive", + "solana-address-lookup-table-program", + "solana-bucket-map", + "solana-compute-budget-program", + "solana-config-program", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-measure", + "solana-metrics", + "solana-program-runtime", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-stake-program", + "solana-vote-program", + "solana-zk-token-proof-program", + "solana-zk-token-sdk", + "strum 0.24.1", + "strum_macros 0.24.3", + "symlink", + "tar", + "tempfile", + "thiserror", + "zstd", +] + +[[package]] +name = "solana-sdk" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "assert_matches", + "base64 0.13.1", + "bincode", + "bitflags 1.3.2", + "borsh 0.9.3", + "bs58", + "bytemuck", + "byteorder", + "chrono", + "derivation-path", + "digest 0.10.7", + "ed25519-dalek", + "ed25519-dalek-bip32", + "generic-array 0.14.7", + "hmac 0.12.1", + "itertools", + "js-sys", + "lazy_static", + "libsecp256k1", + "log", + "memmap2", + "num-derive", + "num-traits", + "pbkdf2 0.11.0", + "qstring", + "rand 0.7.3", + "rand_chacha 0.2.2", + "rustc_version", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.7", + "sha3 0.10.8", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-program", + "solana-sdk-macro", + "thiserror", + "uriparse", + "wasm-bindgen", +] + +[[package]] +name = "solana-sdk-macro" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bs58", + "proc-macro2 1.0.58", + "quote 1.0.26", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "solana-send-transaction-service" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "crossbeam-channel", + "log", + "solana-client", + "solana-measure", + "solana-metrics", + "solana-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-stake-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "log", + "num-derive", + "num-traits", + "rustc_version", + "serde", + "serde_derive", + "solana-config-program", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-metrics", + "solana-program-runtime", + "solana-sdk", + "solana-vote-program", + "thiserror", +] + +[[package]] +name = "solana-streamer" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "crossbeam-channel", + "futures-util", + "histogram", + "indexmap", + "itertools", + "libc", + "log", + "nix 0.24.3", + "pem", + "percentage", + "pkcs8 0.8.0", + "quinn", + "rand 0.7.3", + "rcgen", + "rustls 0.20.8", + "solana-metrics", + "solana-perf", + "solana-sdk", + "thiserror", + "tokio", + "x509-parser", +] + +[[package]] +name = "solana-transaction-status" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "Inflector", + "base64 0.13.1", + "bincode", + "borsh 0.9.3", + "bs58", + "lazy_static", + "log", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-address-lookup-table-program", + "solana-measure", + "solana-metrics", + "solana-sdk", + "solana-vote-program", + "spl-associated-token-account", + "spl-memo 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "solana-version" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "log", + "rustc_version", + "semver", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk", +] + +[[package]] +name = "solana-vote-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bincode", + "log", + "num-derive", + "num-traits", + "rustc_version", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-metrics", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-zk-token-proof-program" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "bytemuck", + "getrandom 0.1.16", + "num-derive", + "num-traits", + "solana-program-runtime", + "solana-sdk", + "solana-zk-token-sdk", +] + +[[package]] +name = "solana-zk-token-sdk" +version = "1.14.13" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +dependencies = [ + "aes-gcm-siv", + "arrayref", + "base64 0.13.1", + "bincode", + "bytemuck", + "byteorder", + "cipher 0.4.4", + "curve25519-dalek", + "getrandom 0.1.16", + "itertools", + "lazy_static", + "merlin", + "num-derive", + "num-traits", + "rand 0.7.3", + "serde", + "serde_json", + "sha3 0.9.1", + "solana-program", + "solana-sdk", + "subtle", + "thiserror", + "zeroize", +] + +[[package]] +name = "solana_rbpf" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a28c5dfe7e8af38daa39d6561c8e8b9ed7a2f900951ebe7362ad6348d36c73" +dependencies = [ + "byteorder", + "combine", + "goblin", + "hash32", + "libc", + "log", + "rand 0.8.5", + "rustc-demangle", + "scroll", + "thiserror", +] + [[package]] name = "spin" version = "0.5.2" @@ -5409,22 +8114,117 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der 0.5.1", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + +[[package]] +name = "spl-associated-token-account" +version = "1.1.2" +source = "git+https://github.com/hyperlane-xyz/solana-program-library.git?branch=hyperlane#5de3c060c276afb4e767111b3b42fbbf4a81d83f" +dependencies = [ + "assert_matches", + "borsh 0.9.3", + "num-derive", + "num-traits", + "solana-program", + "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "spl-memo" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-memo" +version = "3.0.1" +source = "git+https://github.com/hyperlane-xyz/solana-program-library.git?branch=hyperlane#5de3c060c276afb4e767111b3b42fbbf4a81d83f" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-noop" +version = "0.1.3" +source = "git+https://github.com/hyperlane-xyz/solana-program-library.git?branch=hyperlane#5de3c060c276afb4e767111b3b42fbbf4a81d83f" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-token" +version = "3.5.0" +source = "git+https://github.com/hyperlane-xyz/solana-program-library.git?branch=hyperlane#5de3c060c276afb4e767111b3b42fbbf4a81d83f" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum 0.5.7", + "solana-program", + "thiserror", +] + +[[package]] +name = "spl-token-2022" +version = "0.5.0" +source = "git+https://github.com/hyperlane-xyz/solana-program-library.git?branch=hyperlane#5de3c060c276afb4e767111b3b42fbbf4a81d83f" dependencies = [ - "lock_api", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum 0.5.7", + "solana-program", + "solana-zk-token-sdk", + "spl-memo 3.0.1 (git+https://github.com/hyperlane-xyz/solana-program-library.git?branch=hyperlane)", + "spl-token", + "thiserror", ] [[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +name = "spl-type-length-value" +version = "0.1.0" +source = "git+https://github.com/hyperlane-xyz/solana-program-library.git?branch=hyperlane#5de3c060c276afb4e767111b3b42fbbf4a81d83f" dependencies = [ - "base64ct", - "der 0.6.1", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum 0.6.1", + "solana-program", + "thiserror", ] [[package]] @@ -5458,7 +8258,7 @@ dependencies = [ "atoi", "base64 0.13.1", "bigdecimal", - "bitflags", + "bitflags 1.3.2", "byteorder", "bytes", "chrono", @@ -5481,25 +8281,25 @@ dependencies = [ "log", "md-5 0.10.5", "memchr", - "num-bigint", + "num-bigint 0.4.3", "once_cell", "paste", "percent-encoding", - "rand", + "rand 0.8.5", "rust_decimal", "serde", "serde_json", "sha1", - "sha2 0.10.6", + "sha2 0.10.7", "smallvec", "sqlformat", "sqlx-rt", "stringprep", "thiserror", - "time 0.3.20", + "time 0.3.22", "tokio-stream", "url", - "uuid 1.3.2", + "uuid 1.4.0", "whoami", ] @@ -5513,8 +8313,8 @@ dependencies = [ "either", "heck 0.4.1", "once_cell", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "serde_json", "sqlx-core", "sqlx-rt", @@ -5556,6 +8356,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.10.0" @@ -5584,8 +8390,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" dependencies = [ "heck 0.3.3", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -5596,17 +8402,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "rustversion", "syn 1.0.109", ] [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] [[package]] name = "syn" @@ -5614,8 +8437,8 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "unicode-ident", ] @@ -5625,11 +8448,23 @@ version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", + "unicode-xid 0.2.4", +] + [[package]] name = "tai64" version = "4.0.0" @@ -5645,17 +8480,64 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tarpc" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38a012bed6fb9681d3bf71ffaa4f88f3b4b9ed3198cda6e4c8462d24d4bb80" +dependencies = [ + "anyhow", + "fnv", + "futures", + "humantime 2.1.0", + "opentelemetry 0.17.0", + "pin-project", + "rand 0.8.5", + "serde", + "static_assertions", + "tarpc-plugins", + "thiserror", + "tokio", + "tokio-serde", + "tokio-util 0.6.10", + "tracing", + "tracing-opentelemetry 0.17.4", +] + +[[package]] +name = "tarpc-plugins" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee42b4e559f17bce0385ebf511a7beb67d5cc33c12c96b7f4e9789919d9c10f" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", + "rustix 0.37.21", + "windows-sys 0.48.0", ] [[package]] @@ -5673,6 +8555,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "textwrap" version = "0.16.0" @@ -5694,8 +8585,8 @@ version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] @@ -5744,9 +8635,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "serde", @@ -5756,19 +8647,38 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2 0.4.0", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -5795,11 +8705,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -5828,8 +8739,8 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] @@ -5865,6 +8776,32 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.2", + "tokio", +] + +[[package]] +name = "tokio-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" +dependencies = [ + "bincode", + "bytes", + "educe", + "futures-core", + "futures-sink", + "pin-project", + "serde", + "serde_json", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -5917,6 +8854,21 @@ dependencies = [ "tungstenite 0.18.0", ] +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "slab", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -5940,23 +8892,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" - -[[package]] -name = "toml_edit" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow", -] - [[package]] name = "tower-service" version = "0.3.2" @@ -5978,20 +8913,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -6028,6 +8963,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" +dependencies = [ + "once_cell", + "opentelemetry 0.17.0", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-opentelemetry" version = "0.18.0" @@ -6035,7 +8983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de" dependencies = [ "once_cell", - "opentelemetry", + "opentelemetry 0.18.0", "tracing", "tracing-core", "tracing-log", @@ -6089,13 +9037,14 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.8.5", "rustls 0.20.8", "sha-1", "thiserror", "url", "utf-8", "webpki 0.22.0", + "webpki-roots 0.22.6", ] [[package]] @@ -6110,7 +9059,7 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror", "url", @@ -6123,8 +9072,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a46ee5bd706ff79131be9c94e7edcb82b703c487766a114434e5790361cf08c5" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 1.0.109", ] @@ -6169,9 +9118,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -6194,6 +9143,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -6206,6 +9161,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unreachable" version = "1.0.0" @@ -6223,24 +9188,38 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.6.2" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ - "base64 0.13.1", + "base64 0.21.2", + "flate2", "log", "once_cell", + "rustls 0.21.2", + "rustls-webpki", "url", + "webpki-roots 0.23.1", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", ] [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna 0.4.0", "percent-encoding", ] @@ -6250,21 +9229,27 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom", + "getrandom 0.2.10", "serde", ] [[package]] name = "uuid" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" dependencies = [ "serde", ] @@ -6305,6 +9290,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.4" @@ -6329,11 +9320,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -6355,7 +9345,7 @@ dependencies = [ "multer", "percent-encoding", "pin-project", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "scoped-tls", "serde", "serde_json", @@ -6363,11 +9353,17 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite 0.18.0", - "tokio-util", + "tokio-util 0.7.8", "tower-service", "tracing", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -6382,9 +9378,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6392,24 +9388,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -6419,32 +9415,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote", + "quote 1.0.26", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "proc-macro2 1.0.58", + "quote 1.0.26", + "syn 2.0.15", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-timer" @@ -6463,9 +9459,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -6509,6 +9505,15 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "webpki-roots" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" +dependencies = [ + "rustls-webpki", +] + [[package]] name = "which" version = "4.4.0" @@ -6522,9 +9527,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" dependencies = [ "wasm-bindgen", "web-sys", @@ -6567,7 +9572,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -6600,7 +9605,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -6620,9 +9625,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", @@ -6717,15 +9722,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" -[[package]] -name = "winnow" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.10.1" @@ -6763,11 +9759,38 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time 0.3.22", +] + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + [[package]] name = "xml-rs" -version = "0.8.4" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "52839dc911083a8ef63efa4d039d1f58b5e409f923e44c80828f206f66e5541c" [[package]] name = "yaml-rust" @@ -6778,6 +9801,15 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time 0.3.22", +] + [[package]] name = "zeroize" version = "1.6.0" @@ -6793,11 +9825,30 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.58", + "quote 1.0.26", "syn 2.0.15", ] +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.8+zstd.1.5.5" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2238488c06..87d8cc60e5 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,17 +1,36 @@ -cargo-features = ["edition2021"] - [workspace] - members = [ "agents/relayer", "agents/scraper", "agents/validator", "chains/hyperlane-ethereum", "chains/hyperlane-fuel", + "chains/hyperlane-sealevel", "ethers-prometheus", "hyperlane-base", "hyperlane-core", "hyperlane-test", + "sealevel/client", + "sealevel/libraries/access-control", + "sealevel/libraries/account-utils", + "sealevel/libraries/ecdsa-signature", + "sealevel/libraries/hyperlane-sealevel-connection-client", + "sealevel/libraries/hyperlane-sealevel-token", + "sealevel/libraries/interchain-security-module-interface", + "sealevel/libraries/message-recipient-interface", + "sealevel/libraries/multisig-ism", + "sealevel/libraries/serializable-account-meta", + "sealevel/libraries/test-transaction-utils", + "sealevel/libraries/test-utils", + "sealevel/programs/hyperlane-sealevel-token", + "sealevel/programs/hyperlane-sealevel-token-collateral", + "sealevel/programs/hyperlane-sealevel-token-native", + "sealevel/programs/ism/multisig-ism-message-id", + "sealevel/programs/ism/test-ism", + "sealevel/programs/mailbox", + "sealevel/programs/mailbox-test", + "sealevel/programs/test-send-receiver", + "sealevel/programs/validator-announce", "utils/abigen", "utils/backtrace-oneline", "utils/hex", @@ -27,38 +46,241 @@ publish = false version = "0.1.0" [workspace.dependencies] -async-trait = { version = "0.1" } -color-eyre = { version = "0.6" } +Inflector = "0.11.4" +anyhow = "1.0" +async-trait = "0.1" +base64 = "0.13" +bincode = "1.3" +blake3 = "1.3" +borsh = "0.9" +bs58 = "0.4.0" +clap = "4" +color-eyre = "0.6" config = "~0.13.3" derive-new = "0.5" +derive_builder = "0.12" derive_more = "0.99" enum_dispatch = "0.3" -ethers = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-06-01" } -ethers-contract = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-06-01", features = ["legacy"] } -ethers-core = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-06-01" } -ethers-providers = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-06-01" } -ethers-signers = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-06-01", features = ["aws"] } eyre = "0.6" fuels = "0.38" fuels-code-gen = "0.38" futures = "0.3" futures-util = "0.3" +hex = "0.4" itertools = "0.10" -num = {version = "0.4"} +jsonrpc-core = "18.0" +log = "0.4" +maplit = "1.0" +num = "0.4" num-derive = "0.3" num-traits = "0.2" +parking_lot = "0.12" paste = "1.0" +pretty_env_logger = "0.4" +primitive-types = "=0.12.1" prometheus = "0.13" reqwest = "0.11" +rlp = "=0.5.2" rocksdb = "0.20" -serde = { version = "1.0", features = ["derive"] } +semver = "1.0" +serde_bytes = "0.11" +serde_derive = "1.0" serde_json = "1.0" +sha2 = "0.10" +solana-account-decoder = "=1.14.13" +solana-banks-client = "=1.14.13" +solana-banks-interface = "=1.14.13" +solana-banks-server = "=1.14.13" +solana-clap-utils = "=1.14.13" +solana-cli-config = "=1.14.13" +solana-client = "=1.14.13" +solana-program = "=1.14.13" +solana-program-test = "=1.14.13" +solana-sdk = "=1.14.13" +solana-transaction-status = "=1.14.13" +solana-zk-token-sdk = "=1.14.13" +spl-associated-token-account = { version = "=1.1.2", features = ["no-entrypoint"] } +spl-noop = { version = "=0.1.3", features = ["no-entrypoint"] } +spl-token = { version = "=3.5.0", features = ["no-entrypoint"] } +spl-token-2022 = { version = "=0.5.0", features = ["no-entrypoint"] } +spl-type-length-value = "=0.1.0" static_assertions = "1.1" strum = "0.24" strum_macros = "0.24" thiserror = "1.0" -tokio = { version = "1", features = ["parking_lot"] } -tracing = { version = "0.1", features = ["release_max_level_debug"] } +tracing-error = "0.2" tracing-futures = "0.2" -tracing-subscriber = { version = "0.3", default-features = false } +ureq = "2.4" url = "2.3" +which = "4.3" + +# Required for WASM support https://docs.rs/getrandom/latest/getrandom/#webassembly-support +getrandom = { version = "0.2", features = ["js"] } + +[workspace.dependencies.curve25519-dalek] +version = "~3.2" +features = ["serde"] + +[workspace.dependencies.ed25519-dalek] +version = "~1.0" +features = [] + +[workspace.dependencies.ethers] +git = "https://github.com/hyperlane-xyz/ethers-rs" +tag = "2023-06-01" +features = [] + +[workspace.dependencies.ethers-contract] +git = "https://github.com/hyperlane-xyz/ethers-rs" +tag = "2023-06-01" +features = ["legacy"] + +[workspace.dependencies.ethers-core] +git = "https://github.com/hyperlane-xyz/ethers-rs" +tag = "2023-06-01" +features = [] + +[workspace.dependencies.ethers-providers] +git = "https://github.com/hyperlane-xyz/ethers-rs" +tag = "2023-06-01" +features = [] + +[workspace.dependencies.ethers-signers] +git = "https://github.com/hyperlane-xyz/ethers-rs" +tag = "2023-06-01" +features = ["aws"] + +[workspace.dependencies.generic-array] +version = "0.14" +features = [ + "serde", + "more_lengths", +] +default-features = false + +[workspace.dependencies.serde] +version = "1.0" +features = ["derive"] + +[workspace.dependencies.solana] +path = "patches/solana-1.14.13" +features = [] + +[workspace.dependencies.tokio] +version = "1" +features = ["parking_lot"] + +[workspace.dependencies.tracing] +version = "0.1" +features = ["release_max_level_debug"] + +[workspace.dependencies.tracing-subscriber] +version = "0.3" +features = [] +default-features = false + +[patch.crates-io.curve25519-dalek] +version = "3.2.2" +git = "https://github.com/Eclipse-Laboratories-Inc/curve25519-dalek" +branch = "v3.2.2-relax-zeroize" + +[patch.crates-io.ed25519-dalek] +version = "1.0.1" +git = "https://github.com/Eclipse-Laboratories-Inc/ed25519-dalek" +branch = "main" + +[patch.crates-io.primitive-types] +version = "=0.12.1" +git = "https://github.com/hyperlane-xyz/parity-common.git" +branch = "hyperlane" + +[patch.crates-io.rlp] +version = "=0.5.2" +git = "https://github.com/hyperlane-xyz/parity-common.git" +branch = "hyperlane" + +[patch.crates-io.solana-account-decoder] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-banks-client] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-banks-interface] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-banks-server] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-clap-utils] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-cli-config] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-client] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-program] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-program-test] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-sdk] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-transaction-status] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.solana-zk-token-sdk] +version = "=1.14.13" +git = "https://github.com/hyperlane-xyz/solana.git" +tag = "hyperlane-1.14.13-2023-07-04" + +[patch.crates-io.spl-associated-token-account] +version = "=1.1.2" +git = "https://github.com/hyperlane-xyz/solana-program-library.git" +branch = "hyperlane" + +[patch.crates-io.spl-noop] +version = "=0.1.3" +git = "https://github.com/hyperlane-xyz/solana-program-library.git" +branch = "hyperlane" + +[patch.crates-io.spl-token] +version = "=3.5.0" +git = "https://github.com/hyperlane-xyz/solana-program-library.git" +branch = "hyperlane" + +[patch.crates-io.spl-token-2022] +version = "=0.5.0" +git = "https://github.com/hyperlane-xyz/solana-program-library.git" +branch = "hyperlane" + +[patch.crates-io.spl-type-length-value] +version = "=0.1.0" + +git = "https://github.com/hyperlane-xyz/solana-program-library.git" +branch = "hyperlane" diff --git a/rust/Dockerfile b/rust/Dockerfile index a20cad05a2..cd9f79b8da 100644 --- a/rust/Dockerfile +++ b/rust/Dockerfile @@ -17,6 +17,7 @@ COPY hyperlane-core ./hyperlane-core COPY hyperlane-test ./hyperlane-test COPY ethers-prometheus ./ethers-prometheus COPY utils ./utils +COPY sealevel ./sealevel COPY Cargo.toml . COPY Cargo.lock . diff --git a/rust/agents/relayer/Cargo.toml b/rust/agents/relayer/Cargo.toml index 06d801d369..e6a349b70d 100644 --- a/rust/agents/relayer/Cargo.toml +++ b/rust/agents/relayer/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "relayer" documentation.workspace = true @@ -29,7 +31,7 @@ tracing-subscriber.workspace = true tracing.workspace = true regex = "1.5" -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["agent"] } hyperlane-base = { path = "../../hyperlane-base" } hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" } num-derive.workspace = true diff --git a/rust/agents/relayer/src/msg/processor.rs b/rust/agents/relayer/src/msg/processor.rs index aaaac49b1a..d978afccce 100644 --- a/rust/agents/relayer/src/msg/processor.rs +++ b/rust/agents/relayer/src/msg/processor.rs @@ -323,7 +323,7 @@ mod test { } fn add_db_entry(db: &HyperlaneRocksDB, msg: &HyperlaneMessage, retry_count: u32) { - db.store_message(&msg, Default::default()).unwrap(); + db.store_message(msg, Default::default()).unwrap(); if retry_count > 0 { db.store_pending_message_retry_count_by_message_id(&msg.id(), &retry_count) .unwrap(); @@ -343,14 +343,14 @@ mod test { /// Only adds database entries to the pending message prefix if the message's /// retry count is greater than zero fn persist_retried_messages( - retries: &Vec, + retries: &[u32], db: &HyperlaneRocksDB, destination_domain: &HyperlaneDomain, ) { let mut nonce = 0; retries.iter().for_each(|num_retries| { - let message = dummy_hyperlane_message(&destination_domain, nonce); - add_db_entry(&db, &message, *num_retries); + let message = dummy_hyperlane_message(destination_domain, nonce); + add_db_entry(db, &message, *num_retries); nonce += 1; }); } @@ -365,7 +365,7 @@ mod test { num_operations: usize, ) -> Vec> { let (message_processor, mut receive_channel) = - dummy_message_processor(&origin_domain, &destination_domain, &db); + dummy_message_processor(origin_domain, destination_domain, db); let process_fut = message_processor.spawn(); let mut pending_messages = vec![]; diff --git a/rust/agents/relayer/src/prover.rs b/rust/agents/relayer/src/prover.rs index e87680d6af..81d1a89604 100644 --- a/rust/agents/relayer/src/prover.rs +++ b/rust/agents/relayer/src/prover.rs @@ -157,7 +157,7 @@ mod test { // insert the leaves for leaf in test_case.leaves.iter() { let hashed_leaf = hash_message(leaf); - tree.ingest(hashed_leaf).unwrap(); + tree.ingest(hashed_leaf.into()).unwrap(); } // assert the tree has the proper leaf count diff --git a/rust/agents/relayer/src/relayer.rs b/rust/agents/relayer/src/relayer.rs index 03750398c9..fc1dc889a6 100644 --- a/rust/agents/relayer/src/relayer.rs +++ b/rust/agents/relayer/src/relayer.rs @@ -269,7 +269,7 @@ impl Relayer { let index_settings = self.as_ref().settings.chains[origin.name()].index.clone(); let contract_sync = self.message_syncs.get(origin).unwrap().clone(); let cursor = contract_sync - .forward_backward_message_sync_cursor(index_settings.chunk_size) + .forward_backward_message_sync_cursor(index_settings) .await; tokio::spawn(async move { contract_sync diff --git a/rust/agents/scraper/Cargo.toml b/rust/agents/scraper/Cargo.toml index fbbe2bfbbd..e3dd22d362 100644 --- a/rust/agents/scraper/Cargo.toml +++ b/rust/agents/scraper/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "scraper" documentation.workspace = true @@ -28,7 +30,7 @@ tracing.workspace = true hex = { path = "../../utils/hex" } hyperlane-base = { path = "../../hyperlane-base" } -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["agent"] } migration = { path = "migration" } [dev-dependencies] diff --git a/rust/agents/scraper/migration/Cargo.toml b/rust/agents/scraper/migration/Cargo.toml index 84be0f752d..110677f7de 100644 --- a/rust/agents/scraper/migration/Cargo.toml +++ b/rust/agents/scraper/migration/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "migration" documentation.workspace = true @@ -18,8 +20,6 @@ serde.workspace = true time = "0.3" tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] } -hyperlane-core = { path = "../../../hyperlane-core" } - # bin-only deps tracing-subscriber.workspace = true tracing.workspace = true diff --git a/rust/agents/validator/Cargo.toml b/rust/agents/validator/Cargo.toml index f1b636d81f..32da7c6794 100644 --- a/rust/agents/validator/Cargo.toml +++ b/rust/agents/validator/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "validator" documentation.workspace = true @@ -22,7 +24,7 @@ tracing-futures.workspace = true tracing-subscriber.workspace = true tracing.workspace = true -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["agent"] } hyperlane-base = { path = "../../hyperlane-base" } hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" } diff --git a/rust/agents/validator/src/validator.rs b/rust/agents/validator/src/validator.rs index 59e0897d20..108922e13c 100644 --- a/rust/agents/validator/src/validator.rs +++ b/rust/agents/validator/src/validator.rs @@ -148,7 +148,7 @@ impl Validator { .clone(); let contract_sync = self.message_sync.clone(); let cursor = contract_sync - .forward_backward_message_sync_cursor(index_settings.chunk_size) + .forward_backward_message_sync_cursor(index_settings) .await; tokio::spawn(async move { contract_sync @@ -256,7 +256,10 @@ impl Validator { info!("Validator has announced signature storage location"); break; } - info!("Validator has not announced signature storage location"); + info!( + announced_locations=?locations, + "Validator has not announced signature storage location" + ); let balance_delta = self .validator_announce .announce_tokens_needed(signed_announcement.clone()) diff --git a/rust/chains/hyperlane-ethereum/Cargo.toml b/rust/chains/hyperlane-ethereum/Cargo.toml index dac732e5e7..d13a54e84d 100644 --- a/rust/chains/hyperlane-ethereum/Cargo.toml +++ b/rust/chains/hyperlane-ethereum/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "hyperlane-ethereum" documentation.workspace = true @@ -16,8 +18,8 @@ ethers-core.workspace = true ethers-signers.workspace = true ethers.workspace = true futures-util.workspace = true -hex = "0.4.3" -num = "0.4" +hex.workspace = true +num.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs index 90a02c0ead..7ac5ee87b7 100644 --- a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs +++ b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs @@ -9,9 +9,9 @@ use ethers::prelude::Middleware; use tracing::instrument; use hyperlane_core::{ - ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain, - HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexer, InterchainGasPaymaster, - InterchainGasPayment, LogMeta, H160, H256, + BlockRange, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, + HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, IndexRange, Indexer, + InterchainGasPaymaster, InterchainGasPayment, LogMeta, H160, H256, }; use crate::contracts::i_interchain_gas_paymaster::{ @@ -87,14 +87,19 @@ where #[instrument(err, skip(self))] async fn fetch_logs( &self, - from_block: u32, - to_block: u32, + range: IndexRange, ) -> ChainResult> { + let BlockRange(range) = range else { + return Err(ChainCommunicationError::from_other_str( + "EthereumInterchainGasPaymasterIndexer only supports block-based indexing", + )); + }; + let events = self .contract .gas_payment_filter() - .from_block(from_block) - .to_block(to_block) + .from_block(*range.start()) + .to_block(*range.end()) .query_with_meta() .await?; @@ -104,8 +109,8 @@ where ( InterchainGasPayment { message_id: H256::from(log.message_id), - payment: log.payment, - gas_amount: log.gas_amount, + payment: log.payment.into(), + gas_amount: log.gas_amount.into(), }, log_meta.into(), ) diff --git a/rust/chains/hyperlane-ethereum/src/interchain_security_module.rs b/rust/chains/hyperlane-ethereum/src/interchain_security_module.rs index accc0ef292..5e30e889c9 100644 --- a/rust/chains/hyperlane-ethereum/src/interchain_security_module.rs +++ b/rust/chains/hyperlane-ethereum/src/interchain_security_module.rs @@ -121,7 +121,7 @@ where ); let (verifies, gas_estimate) = try_join(tx.call(), tx.estimate_gas()).await?; if verifies { - Ok(Some(gas_estimate)) + Ok(Some(gas_estimate.into())) } else { Ok(None) } diff --git a/rust/chains/hyperlane-ethereum/src/mailbox.rs b/rust/chains/hyperlane-ethereum/src/mailbox.rs index 980f890d93..7da575629b 100644 --- a/rust/chains/hyperlane-ethereum/src/mailbox.rs +++ b/rust/chains/hyperlane-ethereum/src/mailbox.rs @@ -9,15 +9,15 @@ use async_trait::async_trait; use ethers::abi::AbiEncode; use ethers::prelude::Middleware; use ethers_contract::builders::ContractCall; -use hyperlane_core::accumulator::incremental::IncrementalMerkle; -use hyperlane_core::accumulator::TREE_DEPTH; use tracing::instrument; +use hyperlane_core::accumulator::incremental::IncrementalMerkle; +use hyperlane_core::accumulator::TREE_DEPTH; use hyperlane_core::{ - utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator, - HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage, - HyperlaneProtocolError, HyperlaneProvider, Indexer, LogMeta, Mailbox, MessageIndexer, - RawHyperlaneMessage, TxCostEstimate, TxOutcome, H160, H256, U256, + utils::fmt_bytes, BlockRange, ChainCommunicationError, ChainResult, Checkpoint, + ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + HyperlaneMessage, HyperlaneProtocolError, HyperlaneProvider, IndexRange, Indexer, LogMeta, + Mailbox, MessageIndexer, RawHyperlaneMessage, TxCostEstimate, TxOutcome, H160, H256, U256, }; use crate::contracts::arbitrum_node_interface::ArbitrumNodeInterface; @@ -130,16 +130,18 @@ where } #[instrument(err, skip(self))] - async fn fetch_logs( - &self, - from: u32, - to: u32, - ) -> ChainResult> { + async fn fetch_logs(&self, range: IndexRange) -> ChainResult> { + let BlockRange(range) = range else { + return Err(ChainCommunicationError::from_other_str( + "EthereumMailboxIndexer only supports block-based indexing", + )) + }; + let mut events: Vec<(HyperlaneMessage, LogMeta)> = self .contract .dispatch_filter() - .from_block(from) - .to_block(to) + .from_block(*range.start()) + .to_block(*range.end()) .query_with_meta() .await? .into_iter() @@ -176,12 +178,18 @@ where } #[instrument(err, skip(self))] - async fn fetch_logs(&self, from: u32, to: u32) -> ChainResult> { + async fn fetch_logs(&self, range: IndexRange) -> ChainResult> { + let BlockRange(range) = range else { + return Err(ChainCommunicationError::from_other_str( + "EthereumMailboxIndexer only supports block-based indexing", + )) + }; + Ok(self .contract .process_id_filter() - .from_block(from) - .to_block(to) + .from_block(*range.start()) + .to_block(*range.end()) .query_with_meta() .await? .into_iter() @@ -380,6 +388,7 @@ where Some(fixed_block_number), ) .await + .map(Into::into) .map_err(ChainCommunicationError::from_other)?; } @@ -446,13 +455,13 @@ where Some( arbitrum_node_interface .estimate_retryable_ticket( - H160::zero(), + H160::zero().into(), // Give the sender a deposit, otherwise it reverts - U256::MAX, + U256::MAX.into(), self.contract.address(), - U256::zero(), - H160::zero(), - H160::zero(), + U256::zero().into(), + H160::zero().into(), + H160::zero().into(), contract_call.calldata().unwrap_or_default(), ) .estimate_gas() @@ -469,9 +478,9 @@ where .map_err(ChainCommunicationError::from_other)?; Ok(TxCostEstimate { - gas_limit, - gas_price, - l2_gas_limit, + gas_limit: gas_limit.into(), + gas_price: gas_price.into(), + l2_gas_limit: l2_gas_limit.map(|v| v.into()), }) } @@ -501,8 +510,9 @@ mod test { use ethers::{ providers::{MockProvider, Provider}, - types::{Block, Transaction}, + types::{Block, Transaction, U256 as EthersU256}, }; + use hyperlane_core::{ ContractLocator, HyperlaneDomain, HyperlaneMessage, KnownHyperlaneDomain, Mailbox, TxCostEstimate, H160, H256, U256, @@ -534,7 +544,7 @@ mod test { assert!(mailbox.arbitrum_node_interface.is_some()); // Confirm `H160::from_low_u64_ne(0xC8)` does what's expected assert_eq!( - mailbox.arbitrum_node_interface.as_ref().unwrap().address(), + H160::from(mailbox.arbitrum_node_interface.as_ref().unwrap().address()), H160::from_str("0x00000000000000000000000000000000000000C8").unwrap(), ); @@ -544,7 +554,8 @@ mod test { // RPC 4: eth_gasPrice by process_estimate_costs // Return 15 gwei - let gas_price: U256 = ethers::utils::parse_units("15", "gwei").unwrap().into(); + let gas_price: U256 = + EthersU256::from(ethers::utils::parse_units("15", "gwei").unwrap()).into(); mock_provider.push(gas_price).unwrap(); // RPC 3: eth_estimateGas to the ArbitrumNodeInterface's estimateRetryableTicket function by process_estimate_costs diff --git a/rust/chains/hyperlane-ethereum/src/provider.rs b/rust/chains/hyperlane-ethereum/src/provider.rs index 9540e706e2..6ea06433d6 100644 --- a/rust/chains/hyperlane-ethereum/src/provider.rs +++ b/rust/chains/hyperlane-ethereum/src/provider.rs @@ -6,13 +6,13 @@ use std::time::Duration; use async_trait::async_trait; use derive_new::new; use ethers::prelude::Middleware; +use hyperlane_core::ethers_core_types; use tokio::time::sleep; use tracing::instrument; use hyperlane_core::{ BlockInfo, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, - HyperlaneDomain, HyperlaneProvider, HyperlaneProviderError, TxnInfo, TxnReceiptInfo, H160, - H256, + HyperlaneDomain, HyperlaneProvider, HyperlaneProviderError, TxnInfo, TxnReceiptInfo, H256, }; use crate::BuildableWithProvider; @@ -51,7 +51,11 @@ where { #[instrument(err, skip(self))] async fn get_block_by_hash(&self, hash: &H256) -> ChainResult { - let block = get_with_retry_on_none(hash, |h| self.provider.get_block(*h)).await?; + let block = get_with_retry_on_none(hash, |h| { + let eth_h256: ethers_core_types::H256 = h.into(); + self.provider.get_block(eth_h256) + }) + .await?; Ok(BlockInfo { hash: *hash, timestamp: block.timestamp.as_u64(), @@ -72,19 +76,19 @@ where .map_err(ChainCommunicationError::from_other)? .map(|r| -> Result<_, HyperlaneProviderError> { Ok(TxnReceiptInfo { - gas_used: r.gas_used.ok_or(HyperlaneProviderError::NoGasUsed)?, - cumulative_gas_used: r.cumulative_gas_used, - effective_gas_price: r.effective_gas_price, + gas_used: r.gas_used.ok_or(HyperlaneProviderError::NoGasUsed)?.into(), + cumulative_gas_used: r.cumulative_gas_used.into(), + effective_gas_price: r.effective_gas_price.map(Into::into), }) }) .transpose()?; Ok(TxnInfo { hash: *hash, - max_fee_per_gas: txn.max_fee_per_gas, - max_priority_fee_per_gas: txn.max_priority_fee_per_gas, - gas_price: txn.gas_price, - gas_limit: txn.gas, + max_fee_per_gas: txn.max_fee_per_gas.map(Into::into), + max_priority_fee_per_gas: txn.max_priority_fee_per_gas.map(Into::into), + gas_price: txn.gas_price.map(Into::into), + gas_limit: txn.gas.into(), nonce: txn.nonce.as_u64(), sender: txn.from.into(), recipient: txn.to.map(Into::into), @@ -96,7 +100,7 @@ where async fn is_contract(&self, address: &H256) -> ChainResult { let code = self .provider - .get_code(H160::from(*address), None) + .get_code(ethers_core_types::H160::from(*address), None) .await .map_err(ChainCommunicationError::from_other)?; Ok(!code.is_empty()) @@ -111,10 +115,14 @@ where async fn get_storage_at(&self, address: H256, location: H256) -> ChainResult { let storage = self .provider - .get_storage_at(H160::from(address), location, None) + .get_storage_at( + ethers_core_types::H160::from(address), + location.into(), + None, + ) .await .map_err(ChainCommunicationError::from_other)?; - Ok(storage) + Ok(storage.into()) } } diff --git a/rust/chains/hyperlane-ethereum/src/signers.rs b/rust/chains/hyperlane-ethereum/src/signers.rs index b2b29a3bf7..c7abaa2411 100644 --- a/rust/chains/hyperlane-ethereum/src/signers.rs +++ b/rust/chains/hyperlane-ethereum/src/signers.rs @@ -4,7 +4,9 @@ use ethers::types::transaction::eip2718::TypedTransaction; use ethers::types::transaction::eip712::Eip712; use ethers_signers::{AwsSigner, AwsSignerError, LocalWallet, Signer, WalletError}; -use hyperlane_core::{HyperlaneSigner, HyperlaneSignerError, H160, H256}; +use hyperlane_core::{ + HyperlaneSigner, HyperlaneSignerError, Signature as HyperlaneSignature, H160, H256, +}; /// Ethereum-supported signer types #[derive(Debug, Clone)] @@ -83,15 +85,15 @@ impl Signer for Signers { #[async_trait] impl HyperlaneSigner for Signers { fn eth_address(&self) -> H160 { - Signer::address(self) + Signer::address(self).into() } - async fn sign_hash(&self, hash: &H256) -> Result { + async fn sign_hash(&self, hash: &H256) -> Result { let mut signature = Signer::sign_message(self, hash) .await .map_err(|err| HyperlaneSignerError::from(Box::new(err) as Box<_>))?; signature.v = 28 - (signature.v % 2); - Ok(signature) + Ok(signature.into()) } } diff --git a/rust/chains/hyperlane-ethereum/src/singleton_signer.rs b/rust/chains/hyperlane-ethereum/src/singleton_signer.rs index e325fc69ae..790c8e73a7 100644 --- a/rust/chains/hyperlane-ethereum/src/singleton_signer.rs +++ b/rust/chains/hyperlane-ethereum/src/singleton_signer.rs @@ -6,7 +6,9 @@ use thiserror::Error; use tokio::sync::{mpsc, oneshot}; use tracing::warn; -use hyperlane_core::{HyperlaneSigner, HyperlaneSignerError, H160, H256}; +use hyperlane_core::{ + HyperlaneSigner, HyperlaneSignerError, Signature as HyperlaneSignature, H160, H256, +}; use crate::Signers; @@ -50,11 +52,14 @@ impl HyperlaneSigner for SingletonSignerHandle { self.address } - async fn sign_hash(&self, hash: &H256) -> Result { + async fn sign_hash(&self, hash: &H256) -> Result { let (tx, rx) = oneshot::channel(); let task = (*hash, tx); self.tx.send(task).map_err(SingletonSignerError::from)?; - rx.await.map_err(SingletonSignerError::from)? + match rx.await { + Ok(res) => res.map(Into::into), + Err(err) => Err(SingletonSignerError::from(err).into()), + } } } @@ -94,7 +99,7 @@ impl SingletonSigner { } } }; - if tx.send(res).is_err() { + if tx.send(res.map(Into::into)).is_err() { warn!( "Failed to send signature back to the signer handle because the channel was closed" ); diff --git a/rust/chains/hyperlane-ethereum/src/tx.rs b/rust/chains/hyperlane-ethereum/src/tx.rs index 636b764128..dbfb47a09b 100644 --- a/rust/chains/hyperlane-ethereum/src/tx.rs +++ b/rust/chains/hyperlane-ethereum/src/tx.rs @@ -38,7 +38,7 @@ where let dispatch_fut = tx.send(); let dispatched = dispatch_fut.await?; - let tx_hash: H256 = *dispatched; + let tx_hash: H256 = (*dispatched).into(); info!(?to, %data, ?tx_hash, "Dispatched tx"); @@ -80,7 +80,8 @@ where } else { tx.estimate_gas() .await? - .saturating_add(U256::from(GAS_ESTIMATE_BUFFER)) + .saturating_add(U256::from(GAS_ESTIMATE_BUFFER).into()) + .into() }; let Ok((max_fee, max_priority_fee)) = provider.estimate_eip1559_fees(None).await else { // Is not EIP 1559 chain @@ -92,7 +93,7 @@ where ) { // Polygon needs a max priority fee >= 30 gwei let min_polygon_fee = U256::from(30_000_000_000u64); - max_priority_fee.max(min_polygon_fee) + max_priority_fee.max(min_polygon_fee.into()) } else { max_priority_fee }; diff --git a/rust/chains/hyperlane-ethereum/src/validator_announce.rs b/rust/chains/hyperlane-ethereum/src/validator_announce.rs index 3b50d93372..2d455fc061 100644 --- a/rust/chains/hyperlane-ethereum/src/validator_announce.rs +++ b/rust/chains/hyperlane-ethereum/src/validator_announce.rs @@ -83,7 +83,7 @@ where ) -> ChainResult> { let serialized_signature: [u8; 65] = announcement.signature.into(); let tx = self.contract.announce( - announcement.value.validator, + announcement.value.validator.into(), announcement.value.storage_location, serialized_signature.into(), ); @@ -127,7 +127,9 @@ where ) -> ChainResult>> { let storage_locations = self .contract - .get_announced_storage_locations(validators.iter().map(|v| H160::from(*v)).collect()) + .get_announced_storage_locations( + validators.iter().map(|v| H160::from(*v).into()).collect(), + ) .call() .await?; Ok(storage_locations) @@ -136,6 +138,8 @@ where #[instrument(ret, skip(self))] async fn announce_tokens_needed(&self, announcement: SignedType) -> Option { let validator = announcement.value.validator; + let eth_h160: ethers::types::H160 = validator.into(); + let Ok(contract_call) = self .announce_contract_call(announcement, None) .await @@ -144,7 +148,7 @@ where return None; }; - let Ok(balance) = self.provider.get_balance(validator, None).await + let Ok(balance) = self.provider.get_balance(eth_h160, None).await else { trace!("Unable to query balance"); return None; @@ -155,7 +159,7 @@ where trace!("Unable to get announce max cost"); return None; }; - Some(max_cost.saturating_sub(balance)) + Some(max_cost.saturating_sub(balance).into()) } #[instrument(err, ret, skip(self))] diff --git a/rust/chains/hyperlane-fuel/Cargo.toml b/rust/chains/hyperlane-fuel/Cargo.toml index 7dec1d4ec9..7dabcdd514 100644 --- a/rust/chains/hyperlane-fuel/Cargo.toml +++ b/rust/chains/hyperlane-fuel/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "hyperlane-fuel" documentation.workspace = true @@ -8,7 +10,7 @@ publish.workspace = true version.workspace = true [dependencies] -anyhow = "1.0" +anyhow.workspace = true async-trait.workspace = true fuels.workspace = true serde.workspace = true diff --git a/rust/chains/hyperlane-fuel/src/interchain_gas.rs b/rust/chains/hyperlane-fuel/src/interchain_gas.rs index 4d57d7ea2c..ee15bd63b6 100644 --- a/rust/chains/hyperlane-fuel/src/interchain_gas.rs +++ b/rust/chains/hyperlane-fuel/src/interchain_gas.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use hyperlane_core::{ - ChainResult, HyperlaneChain, HyperlaneContract, Indexer, InterchainGasPaymaster, + ChainResult, HyperlaneChain, HyperlaneContract, IndexRange, Indexer, InterchainGasPaymaster, }; use hyperlane_core::{HyperlaneDomain, HyperlaneProvider, InterchainGasPayment, LogMeta, H256}; @@ -35,8 +35,7 @@ pub struct FuelInterchainGasPaymasterIndexer {} impl Indexer for FuelInterchainGasPaymasterIndexer { async fn fetch_logs( &self, - from_block: u32, - to_block: u32, + range: IndexRange, ) -> ChainResult> { todo!() } diff --git a/rust/chains/hyperlane-fuel/src/mailbox.rs b/rust/chains/hyperlane-fuel/src/mailbox.rs index 40882f30e5..b989631505 100644 --- a/rust/chains/hyperlane-fuel/src/mailbox.rs +++ b/rust/chains/hyperlane-fuel/src/mailbox.rs @@ -4,13 +4,14 @@ use std::num::NonZeroU64; use async_trait::async_trait; use fuels::prelude::{Bech32ContractId, WalletUnlocked}; +use hyperlane_core::accumulator::incremental::IncrementalMerkle; use tracing::instrument; use hyperlane_core::{ - accumulator::incremental::IncrementalMerkle, utils::fmt_bytes, ChainCommunicationError, - ChainResult, Checkpoint, ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract, - HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexer, LogMeta, Mailbox, - TxCostEstimate, TxOutcome, H256, U256, + utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator, + HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage, + HyperlaneProvider, IndexRange, Indexer, LogMeta, Mailbox, TxCostEstimate, TxOutcome, H256, + U256, }; use crate::{ @@ -153,11 +154,7 @@ pub struct FuelMailboxIndexer {} #[async_trait] impl Indexer for FuelMailboxIndexer { - async fn fetch_logs( - &self, - from: u32, - to: u32, - ) -> ChainResult> { + async fn fetch_logs(&self, range: IndexRange) -> ChainResult> { todo!() } @@ -168,7 +165,7 @@ impl Indexer for FuelMailboxIndexer { #[async_trait] impl Indexer for FuelMailboxIndexer { - async fn fetch_logs(&self, from: u32, to: u32) -> ChainResult> { + async fn fetch_logs(&self, range: IndexRange) -> ChainResult> { todo!() } diff --git a/rust/chains/hyperlane-sealevel/Cargo.toml b/rust/chains/hyperlane-sealevel/Cargo.toml new file mode 100644 index 0000000000..82dbf2446f --- /dev/null +++ b/rust/chains/hyperlane-sealevel/Cargo.toml @@ -0,0 +1,33 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +base64.workspace = true +borsh.workspace = true +jsonrpc-core.workspace = true +num-traits.workspace = true +serde.workspace = true +solana-account-decoder.workspace = true +solana-client.workspace = true +solana-sdk.workspace = true +solana-transaction-status.workspace = true +thiserror.workspace = true +tracing-futures.workspace = true +tracing.workspace = true +url.workspace = true + +hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-sealevel-mailbox = { path = "../../sealevel/programs/mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-interchain-security-module-interface = { path = "../../sealevel/libraries/interchain-security-module-interface" } +hyperlane-sealevel-message-recipient-interface = { path = "../../sealevel/libraries/message-recipient-interface" } +serializable-account-meta = { path = "../../sealevel/libraries/serializable-account-meta" } +account-utils = { path = "../../sealevel/libraries/account-utils" } +multisig-ism = { path = "../../sealevel/libraries/multisig-ism" } +hyperlane-sealevel-multisig-ism-message-id = { path = "../../sealevel/programs/ism/multisig-ism-message-id", features = ["no-entrypoint"] } +hyperlane-sealevel-validator-announce = { path = "../../sealevel/programs/validator-announce", features = ["no-entrypoint"] } diff --git a/rust/chains/hyperlane-sealevel/src/client.rs b/rust/chains/hyperlane-sealevel/src/client.rs new file mode 100644 index 0000000000..0ecc300817 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/client.rs @@ -0,0 +1,24 @@ +use solana_client::nonblocking::rpc_client::RpcClient; + +/// Kludge to implement Debug for RpcClient. +pub(crate) struct RpcClientWithDebug(RpcClient); + +impl RpcClientWithDebug { + pub fn new(rpc_endpoint: String) -> Self { + Self(RpcClient::new(rpc_endpoint)) + } +} + +impl std::fmt::Debug for RpcClientWithDebug { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("RpcClient { ... }") + } +} + +impl std::ops::Deref for RpcClientWithDebug { + type Target = RpcClient; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/rust/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs new file mode 100644 index 0000000000..92731fe42c --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -0,0 +1,76 @@ +use async_trait::async_trait; +use hyperlane_core::{ + ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + HyperlaneProvider, IndexRange, Indexer, InterchainGasPaymaster, InterchainGasPayment, LogMeta, + H256, +}; +use tracing::{info, instrument}; + +use crate::{ConnectionConf, SealevelProvider}; +use solana_sdk::pubkey::Pubkey; + +/// A reference to an IGP contract on some Sealevel chain +#[derive(Debug)] +pub struct SealevelInterchainGasPaymaster { + program_id: Pubkey, + domain: HyperlaneDomain, +} + +impl SealevelInterchainGasPaymaster { + /// Create a new Sealevel IGP. + pub fn new(_conf: &ConnectionConf, locator: ContractLocator) -> Self { + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + Self { + program_id, + domain: locator.domain.clone(), + } + } +} + +impl HyperlaneContract for SealevelInterchainGasPaymaster { + fn address(&self) -> H256 { + self.program_id.to_bytes().into() + } +} + +impl HyperlaneChain for SealevelInterchainGasPaymaster { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(SealevelProvider::new(self.domain.clone())) + } +} + +impl InterchainGasPaymaster for SealevelInterchainGasPaymaster {} + +/// Struct that retrieves event data for a Sealevel IGP contract +#[derive(Debug)] +pub struct SealevelInterchainGasPaymasterIndexer {} + +impl SealevelInterchainGasPaymasterIndexer { + /// Create a new Sealevel IGP indexer. + pub fn new(_conf: &ConnectionConf, _locator: ContractLocator) -> Self { + Self {} + } +} + +#[async_trait] +impl Indexer for SealevelInterchainGasPaymasterIndexer { + #[instrument(err, skip(self))] + async fn fetch_logs( + &self, + _range: IndexRange, + ) -> ChainResult> { + info!("Gas payment indexing not implemented for Sealevel"); + Ok(vec![]) + } + + #[instrument(level = "debug", err, ret, skip(self))] + async fn get_finalized_block_number(&self) -> ChainResult { + // As a workaround to avoid gas payment indexing on Sealevel, + // we pretend the block number is 1. + Ok(1) + } +} diff --git a/rust/chains/hyperlane-sealevel/src/interchain_security_module.rs b/rust/chains/hyperlane-sealevel/src/interchain_security_module.rs new file mode 100644 index 0000000000..953b2eac5b --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/interchain_security_module.rs @@ -0,0 +1,94 @@ +use async_trait::async_trait; +use num_traits::cast::FromPrimitive; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Keypair}; +use tracing::warn; + +use hyperlane_core::{ + ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneMessage, InterchainSecurityModule, ModuleType, H256, U256, +}; +use hyperlane_sealevel_interchain_security_module_interface::InterchainSecurityModuleInstruction; +use serializable_account_meta::SimulationReturnData; + +use crate::{utils::simulate_instruction, ConnectionConf, RpcClientWithDebug}; + +/// A reference to an InterchainSecurityModule contract on some Sealevel chain +#[derive(Debug)] +pub struct SealevelInterchainSecurityModule { + rpc_client: RpcClientWithDebug, + payer: Option, + program_id: Pubkey, + domain: HyperlaneDomain, +} + +impl SealevelInterchainSecurityModule { + /// Create a new sealevel InterchainSecurityModule + pub fn new(conf: &ConnectionConf, locator: ContractLocator, payer: Option) -> Self { + let rpc_client = RpcClientWithDebug::new(conf.url.to_string()); + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + Self { + rpc_client, + payer, + program_id, + domain: locator.domain.clone(), + } + } +} + +impl HyperlaneContract for SealevelInterchainSecurityModule { + fn address(&self) -> H256 { + self.program_id.to_bytes().into() + } +} + +impl HyperlaneChain for SealevelInterchainSecurityModule { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(crate::SealevelProvider::new(self.domain.clone())) + } +} + +#[async_trait] +impl InterchainSecurityModule for SealevelInterchainSecurityModule { + async fn module_type(&self) -> ChainResult { + let instruction = Instruction::new_with_bytes( + self.program_id, + &InterchainSecurityModuleInstruction::Type + .encode() + .map_err(ChainCommunicationError::from_other)?[..], + vec![], + ); + + let module = simulate_instruction::>( + &self.rpc_client, + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await? + .ok_or_else(|| { + ChainCommunicationError::from_other_str("No return data was returned from the ISM") + })? + .return_data; + + if let Some(module_type) = ModuleType::from_u32(module) { + Ok(module_type) + } else { + warn!(%module, "Unknown module type"); + Ok(ModuleType::Unused) + } + } + + async fn dry_run_verify( + &self, + _message: &HyperlaneMessage, + _metadata: &[u8], + ) -> ChainResult> { + // TODO: Implement this once we have aggregation ISM support in Sealevel + Ok(Some(U256::zero())) + } +} diff --git a/rust/chains/hyperlane-sealevel/src/lib.rs b/rust/chains/hyperlane-sealevel/src/lib.rs new file mode 100644 index 0000000000..0546c11a11 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/lib.rs @@ -0,0 +1,26 @@ +//! Implementation of hyperlane for Sealevel. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![deny(warnings)] + +pub use crate::multisig_ism::*; +pub(crate) use client::RpcClientWithDebug; +pub use interchain_gas::*; +pub use interchain_security_module::*; +pub use mailbox::*; +pub use provider::*; +pub use solana_sdk::signer::keypair::Keypair; +pub use trait_builder::*; +pub use validator_announce::*; + +mod interchain_gas; +mod interchain_security_module; +mod mailbox; +mod multisig_ism; +mod provider; +mod trait_builder; +mod utils; + +mod client; +mod validator_announce; diff --git a/rust/chains/hyperlane-sealevel/src/mailbox.rs b/rust/chains/hyperlane-sealevel/src/mailbox.rs new file mode 100644 index 0000000000..cd55a0bbed --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/mailbox.rs @@ -0,0 +1,748 @@ +#![allow(warnings)] // FIXME remove + +use std::{collections::HashMap, num::NonZeroU64, str::FromStr as _}; + +use async_trait::async_trait; +use borsh::{BorshDeserialize, BorshSerialize}; +use jsonrpc_core::futures_util::TryFutureExt; +use tracing::{debug, info, instrument, warn}; + +use hyperlane_core::{ + accumulator::incremental::IncrementalMerkle, ChainCommunicationError, ChainResult, Checkpoint, + ContractLocator, Decode as _, Encode as _, HyperlaneAbi, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, IndexRange, Indexer, LogMeta, Mailbox, + MessageIndexer, SequenceRange, TxCostEstimate, TxOutcome, H256, U256, +}; +use hyperlane_sealevel_interchain_security_module_interface::{ + InterchainSecurityModuleInstruction, VerifyInstruction, +}; +use hyperlane_sealevel_mailbox::{ + accounts::{DispatchedMessageAccount, InboxAccount, OutboxAccount}, + instruction::InboxProcess, + mailbox_dispatched_message_pda_seeds, mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds, + mailbox_process_authority_pda_seeds, mailbox_processed_message_pda_seeds, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use serializable_account_meta::SimulationReturnData; +use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; +use solana_client::{ + nonblocking::rpc_client::RpcClient, + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcSendTransactionConfig}, + rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, +}; +use solana_sdk::{ + account::Account, + commitment_config::CommitmentConfig, + compute_budget::ComputeBudgetInstruction, + hash::Hash, + instruction::AccountMeta, + instruction::Instruction, + message::Message, + pubkey::Pubkey, + signature::Signature, + signer::{keypair::Keypair, Signer as _}, + transaction::{Transaction, VersionedTransaction}, +}; +use solana_transaction_status::{ + EncodedConfirmedBlock, EncodedTransaction, EncodedTransactionWithStatusMeta, + UiInnerInstructions, UiInstruction, UiMessage, UiParsedInstruction, UiReturnDataEncoding, + UiTransaction, UiTransactionReturnData, UiTransactionStatusMeta, +}; + +use crate::RpcClientWithDebug; +use crate::{ + utils::{get_account_metas, simulate_instruction}, + ConnectionConf, SealevelProvider, +}; + +const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111"; +const SPL_NOOP: &str = "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"; + +// FIXME solana uses the first 64 byte signature of a transaction to uniquely identify the +// transaction rather than a 32 byte transaction hash like ethereum. Hash it here to reduce +// size - requires more thought to ensure this makes sense to do... +fn signature_to_txn_hash(signature: &Signature) -> H256 { + H256::from(solana_sdk::hash::hash(signature.as_ref()).to_bytes()) +} + +// The max amount of compute units for a transaction. +// TODO: consider a more sane value and/or use IGP gas payments instead. +const PROCESS_COMPUTE_UNITS: u32 = 1_400_000; + +/// A reference to a Mailbox contract on some Sealevel chain +pub struct SealevelMailbox { + program_id: Pubkey, + inbox: (Pubkey, u8), + outbox: (Pubkey, u8), + rpc_client: RpcClient, + domain: HyperlaneDomain, + payer: Option, +} + +impl SealevelMailbox { + /// Create a new sealevel mailbox + pub fn new( + conf: &ConnectionConf, + locator: ContractLocator, + payer: Option, + ) -> ChainResult { + // Set the `processed` commitment at rpc level + let rpc_client = + RpcClient::new_with_commitment(conf.url.to_string(), CommitmentConfig::processed()); + + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + let domain = locator.domain.id(); + let inbox = Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), &program_id); + let outbox = Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &program_id); + + debug!( + "domain={}\nmailbox={}\ninbox=({}, {})\noutbox=({}, {})", + domain, program_id, inbox.0, inbox.1, outbox.0, outbox.1, + ); + + Ok(SealevelMailbox { + program_id, + inbox, + outbox, + rpc_client, + domain: locator.domain.clone(), + payer, + }) + } + + pub fn inbox(&self) -> (Pubkey, u8) { + self.inbox + } + pub fn outbox(&self) -> (Pubkey, u8) { + self.outbox + } + + /// Simulates an instruction, and attempts to deserialize it into a T. + /// If no return data at all was returned, returns Ok(None). + /// If some return data was returned but deserialization was unsuccesful, + /// an Err is returned. + pub async fn simulate_instruction( + &self, + instruction: Instruction, + ) -> ChainResult> { + simulate_instruction( + &self.rpc_client, + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await + } + + /// Simulates an Instruction that will return a list of AccountMetas. + pub async fn get_account_metas( + &self, + instruction: Instruction, + ) -> ChainResult> { + get_account_metas( + &self.rpc_client, + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await + } + + /// Gets the recipient ISM given a recipient program id and the ISM getter account metas. + pub async fn get_recipient_ism( + &self, + recipient_program_id: Pubkey, + ism_getter_account_metas: Vec, + ) -> ChainResult { + let mut accounts = vec![ + // Inbox PDA + AccountMeta::new_readonly(self.inbox.0, false), + // The recipient program. + AccountMeta::new_readonly(recipient_program_id, false), + ]; + accounts.extend(ism_getter_account_metas); + + let instruction = Instruction::new_with_borsh( + self.program_id, + &hyperlane_sealevel_mailbox::instruction::Instruction::InboxGetRecipientIsm( + recipient_program_id, + ), + accounts, + ); + let ism = self + .simulate_instruction::>(instruction) + .await? + .ok_or(ChainCommunicationError::from_other_str( + "No return data from InboxGetRecipientIsm instruction", + ))? + .return_data; + Ok(ism) + } + + /// Gets the account metas required for the recipient's + /// `MessageRecipientInstruction::InterchainSecurityModule` instruction. + pub async fn get_ism_getter_account_metas( + &self, + recipient_program_id: Pubkey, + ) -> ChainResult> { + let instruction = + hyperlane_sealevel_message_recipient_interface::MessageRecipientInstruction::InterchainSecurityModuleAccountMetas; + self.get_account_metas_with_instruction_bytes( + recipient_program_id, + &instruction + .encode() + .map_err(ChainCommunicationError::from_other)?, + hyperlane_sealevel_message_recipient_interface::INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_PDA_SEEDS, + ).await + } + + /// Gets the account metas required for the ISM's `Verify` instruction. + pub async fn get_ism_verify_account_metas( + &self, + ism: Pubkey, + metadata: Vec, + message: Vec, + ) -> ChainResult> { + let instruction = + InterchainSecurityModuleInstruction::VerifyAccountMetas(VerifyInstruction { + metadata, + message, + }); + self.get_account_metas_with_instruction_bytes( + ism, + &instruction + .encode() + .map_err(ChainCommunicationError::from_other)?, + hyperlane_sealevel_interchain_security_module_interface::VERIFY_ACCOUNT_METAS_PDA_SEEDS, + ) + .await + } + + /// Gets the account metas required for the recipient's `MessageRecipientInstruction::Handle` instruction. + pub async fn get_handle_account_metas( + &self, + message: &HyperlaneMessage, + ) -> ChainResult> { + let recipient_program_id = Pubkey::new_from_array(message.recipient.into()); + let instruction = MessageRecipientInstruction::HandleAccountMetas(HandleInstruction { + sender: message.sender, + origin: message.origin, + message: message.body.clone(), + }); + + self.get_account_metas_with_instruction_bytes( + recipient_program_id, + &instruction + .encode() + .map_err(ChainCommunicationError::from_other)?, + hyperlane_sealevel_message_recipient_interface::HANDLE_ACCOUNT_METAS_PDA_SEEDS, + ) + .await + } + + async fn get_account_metas_with_instruction_bytes( + &self, + program_id: Pubkey, + instruction_data: &[u8], + account_metas_pda_seeds: &[&[u8]], + ) -> ChainResult> { + let (account_metas_pda_key, _) = + Pubkey::find_program_address(account_metas_pda_seeds, &program_id); + let instruction = Instruction::new_with_bytes( + program_id, + instruction_data, + vec![AccountMeta::new(account_metas_pda_key, false)], + ); + + self.get_account_metas(instruction).await + } +} + +impl HyperlaneContract for SealevelMailbox { + fn address(&self) -> H256 { + self.program_id.to_bytes().into() + } +} + +impl HyperlaneChain for SealevelMailbox { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(SealevelProvider::new(self.domain.clone())) + } +} + +impl std::fmt::Debug for SealevelMailbox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self as &dyn HyperlaneContract) + } +} + +// TODO refactor the sealevel client into a lib and bin, pull in and use the lib here rather than +// duplicating. +#[async_trait] +impl Mailbox for SealevelMailbox { + #[instrument(err, ret, skip(self))] + async fn count(&self, _maybe_lag: Option) -> ChainResult { + let tree = self.tree(_maybe_lag).await?; + + tree.count() + .try_into() + .map_err(ChainCommunicationError::from_other) + } + + #[instrument(err, ret, skip(self))] + async fn delivered(&self, id: H256) -> ChainResult { + let (processed_message_account_key, _processed_message_account_bump) = + Pubkey::find_program_address( + mailbox_processed_message_pda_seeds!(id), + &self.program_id, + ); + + let account = self + .rpc_client + .get_account_with_commitment( + &processed_message_account_key, + CommitmentConfig::finalized(), + ) + .await + .map_err(ChainCommunicationError::from_other)?; + + Ok(account.value.is_some()) + } + + #[instrument(err, ret, skip(self))] + async fn tree(&self, lag: Option) -> ChainResult { + assert!( + lag.is_none(), + "Sealevel does not support querying point-in-time" + ); + + let outbox_account = self + .rpc_client + .get_account_with_commitment(&self.outbox.0, CommitmentConfig::finalized()) + .await + .map_err(ChainCommunicationError::from_other)? + .value + .ok_or_else(|| { + ChainCommunicationError::from_other_str("Could not find account data") + })?; + let outbox = OutboxAccount::fetch(&mut outbox_account.data.as_ref()) + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + + Ok(outbox.tree) + } + + #[instrument(err, ret, skip(self))] + async fn latest_checkpoint(&self, lag: Option) -> ChainResult { + assert!( + lag.is_none(), + "Sealevel does not support querying point-in-time" + ); + + let tree = self.tree(lag).await?; + + let root = tree.root(); + let count: u32 = tree + .count() + .try_into() + .map_err(ChainCommunicationError::from_other)?; + let index = count.checked_sub(1).ok_or_else(|| { + ChainCommunicationError::from_contract_error_str( + "Outbox is empty, cannot compute checkpoint", + ) + })?; + let checkpoint = Checkpoint { + mailbox_address: self.program_id.to_bytes().into(), + mailbox_domain: self.domain.id(), + root, + index, + }; + Ok(checkpoint) + } + + #[instrument(err, ret, skip(self))] + async fn default_ism(&self) -> ChainResult { + let inbox_account = self + .rpc_client + .get_account(&self.inbox.0) + .await + .map_err(ChainCommunicationError::from_other)?; + let inbox = InboxAccount::fetch(&mut inbox_account.data.as_ref()) + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + + Ok(inbox.default_ism.to_bytes().into()) + } + + #[instrument(err, ret, skip(self))] + async fn recipient_ism(&self, recipient: H256) -> ChainResult { + let recipient_program_id = Pubkey::new_from_array(recipient.0); + + // Get the account metas required for the recipient.InterchainSecurityModule instruction. + let ism_getter_account_metas = self + .get_ism_getter_account_metas(recipient_program_id) + .await?; + + // Get the ISM to use. + let ism_pubkey = self + .get_recipient_ism(recipient_program_id, ism_getter_account_metas) + .await?; + + Ok(ism_pubkey.to_bytes().into()) + } + + #[instrument(err, ret, skip(self))] + async fn process( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + _tx_gas_limit: Option, + ) -> ChainResult { + let recipient: Pubkey = message.recipient.0.into(); + let mut encoded_message = vec![]; + message.write_to(&mut encoded_message).unwrap(); + + let payer = self + .payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?; + + let mut instructions = Vec::with_capacity(2); + // Set the compute unit limit. + instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( + PROCESS_COMPUTE_UNITS, + )); + + // "processed" level commitment does not guarantee finality. + // roughly 5% of blocks end up on a dropped fork. + // However we don't want this function to be a bottleneck and there already + // is retry logic in the agents. + let commitment = CommitmentConfig::processed(); + + let (process_authority_key, _process_authority_bump) = Pubkey::try_find_program_address( + mailbox_process_authority_pda_seeds!(&recipient), + &self.program_id, + ) + .ok_or_else(|| { + ChainCommunicationError::from_other_str( + "Could not find program address for process authority", + ) + })?; + let (processed_message_account_key, _processed_message_account_bump) = + Pubkey::try_find_program_address( + mailbox_processed_message_pda_seeds!(message.id()), + &self.program_id, + ) + .ok_or_else(|| { + ChainCommunicationError::from_other_str( + "Could not find program address for processed message account", + ) + })?; + + // Get the account metas required for the recipient.InterchainSecurityModule instruction. + let ism_getter_account_metas = self.get_ism_getter_account_metas(recipient).await?; + + // Get the recipient ISM. + let ism = self + .get_recipient_ism(recipient, ism_getter_account_metas.clone()) + .await?; + + let ixn = + hyperlane_sealevel_mailbox::instruction::Instruction::InboxProcess(InboxProcess { + metadata: metadata.to_vec(), + message: encoded_message.clone(), + }); + let ixn_data = ixn + .into_instruction_data() + .map_err(ChainCommunicationError::from_other)?; + + // Craft the accounts for the transaction. + let mut accounts: Vec = vec![ + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(Pubkey::from_str(SYSTEM_PROGRAM).unwrap(), false), + AccountMeta::new(self.inbox.0, false), + AccountMeta::new_readonly(process_authority_key, false), + AccountMeta::new(processed_message_account_key, false), + ]; + accounts.extend(ism_getter_account_metas); + accounts.extend([ + AccountMeta::new_readonly(Pubkey::from_str(SPL_NOOP).unwrap(), false), + AccountMeta::new_readonly(ism, false), + ]); + + // Get the account metas required for the ISM.Verify instruction. + let ism_verify_account_metas = self + .get_ism_verify_account_metas(ism, metadata.into(), encoded_message) + .await?; + accounts.extend(ism_verify_account_metas); + + // The recipient. + accounts.extend([AccountMeta::new_readonly(recipient, false)]); + + // Get account metas required for the Handle instruction + let handle_account_metas = self.get_handle_account_metas(message).await?; + accounts.extend(handle_account_metas); + + let inbox_instruction = Instruction { + program_id: self.program_id, + data: ixn_data, + accounts, + }; + tracing::info!("accounts={:#?}", inbox_instruction.accounts); + instructions.push(inbox_instruction); + let (recent_blockhash, _) = self + .rpc_client + .get_latest_blockhash_with_commitment(commitment) + .await + .map_err(ChainCommunicationError::from_other)?; + let txn = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + let signature = self + .rpc_client + .send_and_confirm_transaction(&txn) + .await + .map_err(ChainCommunicationError::from_other)?; + tracing::info!("signature={}", signature); + tracing::info!("txn={:?}", txn); + let executed = self + .rpc_client + .confirm_transaction_with_commitment(&signature, commitment) + .await + .map_err(|err| warn!("Failed to confirm inbox process transaction: {}", err)) + .map(|ctx| ctx.value) + .unwrap_or(false); + let txid = signature_to_txn_hash(&signature); + + Ok(TxOutcome { + txid, + executed, + // TODO use correct data upon integrating IGP support + gas_price: U256::zero(), + gas_used: U256::zero(), + }) + } + + #[instrument(err, ret, skip(self))] + async fn process_estimate_costs( + &self, + _message: &HyperlaneMessage, + _metadata: &[u8], + ) -> ChainResult { + // TODO use correct data upon integrating IGP support + Ok(TxCostEstimate { + gas_limit: U256::zero(), + gas_price: U256::zero(), + l2_gas_limit: None, + }) + } + + fn process_calldata(&self, _message: &HyperlaneMessage, _metadata: &[u8]) -> Vec { + todo!() + } +} + +/// Struct that retrieves event data for a Sealevel Mailbox contract +#[derive(Debug)] +pub struct SealevelMailboxIndexer { + rpc_client: RpcClientWithDebug, + mailbox: SealevelMailbox, + program_id: Pubkey, +} + +impl SealevelMailboxIndexer { + pub fn new(conf: &ConnectionConf, locator: ContractLocator) -> ChainResult { + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + let rpc_client = RpcClientWithDebug::new(conf.url.to_string()); + let mailbox = SealevelMailbox::new(conf, locator, None)?; + Ok(Self { + program_id, + rpc_client, + mailbox, + }) + } + + async fn get_finalized_block_number(&self) -> ChainResult { + let height = self + .rpc_client + .get_block_height() + .await + .map_err(ChainCommunicationError::from_other)? + .try_into() + // FIXME solana block height is u64... + .expect("sealevel block height exceeds u32::MAX"); + Ok(height) + } + + async fn get_message_with_nonce(&self, nonce: u32) -> ChainResult<(HyperlaneMessage, LogMeta)> { + let target_message_account_bytes = &[ + &hyperlane_sealevel_mailbox::accounts::DISPATCHED_MESSAGE_DISCRIMINATOR[..], + &nonce.to_le_bytes()[..], + ] + .concat(); + let target_message_account_bytes = base64::encode(target_message_account_bytes); + + // First, find all accounts with the matching account data. + // To keep responses small in case there is ever more than 1 + // match, we don't request the full account data, and just request + // the `unique_message_pubkey` field. + let memcmp = RpcFilterType::Memcmp(Memcmp { + // Ignore the first byte, which is the `initialized` bool flag. + offset: 1, + bytes: MemcmpEncodedBytes::Base64(target_message_account_bytes), + encoding: None, + }); + let config = RpcProgramAccountsConfig { + filters: Some(vec![memcmp]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + // Don't return any data + data_slice: Some(UiDataSliceConfig { + offset: 1 + 8 + 4 + 8, // the offset to get the `unique_message_pubkey` field + length: 32, // the length of the `unique_message_pubkey` field + }), + commitment: Some(CommitmentConfig::finalized()), + min_context_slot: None, + }, + with_context: Some(false), + }; + let accounts = self + .rpc_client + .get_program_accounts_with_config(&self.mailbox.program_id, config) + .await + .map_err(ChainCommunicationError::from_other)?; + + // Now loop through matching accounts and find the one with a valid account pubkey + // that proves it's an actual message storage PDA. + let mut valid_message_storage_pda_pubkey = Option::::None; + + for (pubkey, account) in accounts.iter() { + let unique_message_pubkey = Pubkey::new(&account.data); + let (expected_pubkey, _bump) = Pubkey::try_find_program_address( + mailbox_dispatched_message_pda_seeds!(unique_message_pubkey), + &self.mailbox.program_id, + ) + .ok_or_else(|| { + ChainCommunicationError::from_other_str( + "Could not find program address for unique_message_pubkey", + ) + })?; + if expected_pubkey == *pubkey { + valid_message_storage_pda_pubkey = Some(*pubkey); + break; + } + } + + let valid_message_storage_pda_pubkey = + valid_message_storage_pda_pubkey.ok_or_else(|| { + ChainCommunicationError::from_other_str( + "Could not find valid message storage PDA pubkey", + ) + })?; + + // Now that we have the valid message storage PDA pubkey, we can get the full account data. + let account = self + .rpc_client + .get_account_with_commitment( + &valid_message_storage_pda_pubkey, + CommitmentConfig::finalized(), + ) + .await + .map_err(ChainCommunicationError::from_other)? + .value + .ok_or_else(|| { + ChainCommunicationError::from_other_str("Could not find account data") + })?; + let dispatched_message_account = + DispatchedMessageAccount::fetch(&mut account.data.as_ref()) + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + let hyperlane_message = + HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?; + + Ok(( + hyperlane_message, + LogMeta { + address: self.mailbox.program_id.to_bytes().into(), + block_number: dispatched_message_account.slot, + // TODO: get these when building out scraper support. + // It's inconvenient to get these :| + block_hash: H256::zero(), + transaction_hash: H256::zero(), + transaction_index: 0, + log_index: U256::zero(), + }, + )) + } +} + +#[async_trait] +impl MessageIndexer for SealevelMailboxIndexer { + #[instrument(err, skip(self))] + async fn fetch_count_at_tip(&self) -> ChainResult<(u32, u32)> { + let tip = Indexer::::get_finalized_block_number(self as _).await?; + // TODO: need to make sure the call and tip are at the same height? + let count = self.mailbox.count(None).await?; + Ok((count, tip)) + } +} + +#[async_trait] +impl Indexer for SealevelMailboxIndexer { + async fn fetch_logs(&self, range: IndexRange) -> ChainResult> { + let SequenceRange(range) = range else { + return Err(ChainCommunicationError::from_other_str( + "SealevelMailboxIndexer only supports sequence-based indexing", + )) + }; + + info!( + ?range, + "Fetching SealevelMailboxIndexer HyperlaneMessage logs" + ); + + let mut messages = Vec::with_capacity((range.end() - range.start()) as usize); + for nonce in range { + messages.push(self.get_message_with_nonce(nonce).await?); + } + Ok(messages) + } + + async fn get_finalized_block_number(&self) -> ChainResult { + self.get_finalized_block_number().await + } +} + +#[async_trait] +impl Indexer for SealevelMailboxIndexer { + async fn fetch_logs(&self, _range: IndexRange) -> ChainResult> { + todo!() + } + + async fn get_finalized_block_number(&self) -> ChainResult { + self.get_finalized_block_number().await + } +} + +struct SealevelMailboxAbi; + +// TODO figure out how this is used and if we can support it for sealevel. +impl HyperlaneAbi for SealevelMailboxAbi { + const SELECTOR_SIZE_BYTES: usize = 8; + + fn fn_map() -> HashMap, &'static str> { + todo!() + } +} diff --git a/rust/chains/hyperlane-sealevel/src/multisig_ism.rs b/rust/chains/hyperlane-sealevel/src/multisig_ism.rs new file mode 100644 index 0000000000..72c4d02f90 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/multisig_ism.rs @@ -0,0 +1,140 @@ +use async_trait::async_trait; + +use hyperlane_core::{ + ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, MultisigIsm, RawHyperlaneMessage, H256, +}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, +}; + +use crate::{ + utils::{get_account_metas, simulate_instruction}, + ConnectionConf, RpcClientWithDebug, SealevelProvider, +}; + +use hyperlane_sealevel_multisig_ism_message_id::instruction::ValidatorsAndThreshold; +use multisig_ism::interface::{ + MultisigIsmInstruction, VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS, +}; + +/// A reference to a MultisigIsm contract on some Sealevel chain +#[derive(Debug)] +pub struct SealevelMultisigIsm { + rpc_client: RpcClientWithDebug, + payer: Option, + program_id: Pubkey, + domain: HyperlaneDomain, +} + +impl SealevelMultisigIsm { + /// Create a new Sealevel MultisigIsm. + pub fn new(conf: &ConnectionConf, locator: ContractLocator, payer: Option) -> Self { + let rpc_client = RpcClientWithDebug::new(conf.url.to_string()); + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + + Self { + rpc_client, + payer, + program_id, + domain: locator.domain.clone(), + } + } +} + +impl HyperlaneContract for SealevelMultisigIsm { + fn address(&self) -> H256 { + self.program_id.to_bytes().into() + } +} + +impl HyperlaneChain for SealevelMultisigIsm { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(SealevelProvider::new(self.domain.clone())) + } +} + +#[async_trait] +impl MultisigIsm for SealevelMultisigIsm { + /// Returns the validator and threshold needed to verify message + async fn validators_and_threshold( + &self, + message: &HyperlaneMessage, + ) -> ChainResult<(Vec, u8)> { + let message_bytes = RawHyperlaneMessage::from(message).to_vec(); + + let account_metas = self + .get_validators_and_threshold_account_metas(message_bytes.clone()) + .await?; + + let instruction = Instruction::new_with_bytes( + self.program_id, + &MultisigIsmInstruction::ValidatorsAndThreshold(message_bytes) + .encode() + .map_err(ChainCommunicationError::from_other)?[..], + account_metas, + ); + + let validators_and_threshold = simulate_instruction::( + &self.rpc_client, + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await? + .ok_or_else(|| { + ChainCommunicationError::from_other_str( + "No return data was returned from the multisig ism", + ) + })?; + + let validators = validators_and_threshold + .validators + .into_iter() + .map(|validator| validator.into()) + .collect(); + + Ok((validators, validators_and_threshold.threshold)) + } +} + +impl SealevelMultisigIsm { + async fn get_validators_and_threshold_account_metas( + &self, + message_bytes: Vec, + ) -> ChainResult> { + let (account_metas_pda_key, _account_metas_pda_bump) = Pubkey::try_find_program_address( + VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS, + &self.program_id, + ) + .ok_or_else(|| { + ChainCommunicationError::from_other_str( + "Could not find program address for domain data", + ) + })?; + + let instruction = Instruction::new_with_bytes( + self.program_id, + &MultisigIsmInstruction::ValidatorsAndThresholdAccountMetas(message_bytes) + .encode() + .map_err(ChainCommunicationError::from_other)?[..], + vec![AccountMeta::new_readonly(account_metas_pda_key, false)], + ); + + get_account_metas( + &self.rpc_client, + self.payer + .as_ref() + .ok_or_else(|| ChainCommunicationError::SignerUnavailable)?, + instruction, + ) + .await + } +} diff --git a/rust/chains/hyperlane-sealevel/src/provider.rs b/rust/chains/hyperlane-sealevel/src/provider.rs new file mode 100644 index 0000000000..b853e30e4b --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/provider.rs @@ -0,0 +1,46 @@ +use async_trait::async_trait; + +use hyperlane_core::{ + BlockInfo, ChainResult, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, TxnInfo, H256, +}; + +/// A wrapper around a Sealevel provider to get generic blockchain information. +#[derive(Debug)] +pub struct SealevelProvider { + domain: HyperlaneDomain, +} + +impl SealevelProvider { + /// Create a new Sealevel provider. + pub fn new(domain: HyperlaneDomain) -> Self { + SealevelProvider { domain } + } +} + +impl HyperlaneChain for SealevelProvider { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(SealevelProvider { + domain: self.domain.clone(), + }) + } +} + +#[async_trait] +impl HyperlaneProvider for SealevelProvider { + async fn get_block_by_hash(&self, _hash: &H256) -> ChainResult { + todo!() // FIXME + } + + async fn get_txn_by_hash(&self, _hash: &H256) -> ChainResult { + todo!() // FIXME + } + + async fn is_contract(&self, _address: &H256) -> ChainResult { + // FIXME + Ok(true) + } +} diff --git a/rust/chains/hyperlane-sealevel/src/solana/ed25519_program.rs b/rust/chains/hyperlane-sealevel/src/solana/ed25519_program.rs new file mode 100644 index 0000000000..e1b1663079 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/ed25519_program.rs @@ -0,0 +1,7 @@ +//! The [ed25519 native program][np]. +//! +//! [np]: https://docs.solana.com/developing/runtime-facilities/programs#ed25519-program +use crate::solana::pubkey::Pubkey; +use solana_sdk_macro::declare_id; + +declare_id!("Ed25519SigVerify111111111111111111111111111"); diff --git a/rust/chains/hyperlane-sealevel/src/solana/fee_calculator.rs b/rust/chains/hyperlane-sealevel/src/solana/fee_calculator.rs new file mode 100644 index 0000000000..dce48a0d89 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/fee_calculator.rs @@ -0,0 +1,360 @@ +//! Calculation of transaction fees. + +#![allow(clippy::integer_arithmetic)] + +use serde_derive::{Deserialize, Serialize}; + +use super::{ed25519_program, message::Message, secp256k1_program}; +// use super:: +use log::*; + +#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FeeCalculator { + /// The current cost of a signature. + /// + /// This amount may increase/decrease over time based on cluster processing + /// load. + pub lamports_per_signature: u64, +} + +impl FeeCalculator { + pub fn new(lamports_per_signature: u64) -> Self { + Self { + lamports_per_signature, + } + } + + #[deprecated( + since = "1.9.0", + note = "Please do not use, will no longer be available in the future" + )] + pub fn calculate_fee(&self, message: &Message) -> u64 { + let mut num_signatures: u64 = 0; + for instruction in &message.instructions { + let program_index = instruction.program_id_index as usize; + // Message may not be sanitized here + if program_index < message.account_keys.len() { + let id = message.account_keys[program_index]; + if (secp256k1_program::check_id(&id) || ed25519_program::check_id(&id)) + && !instruction.data.is_empty() + { + num_signatures += instruction.data[0] as u64; + } + } + } + + self.lamports_per_signature + * (u64::from(message.header.num_required_signatures) + num_signatures) + } +} + +/* +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, AbiExample)] +#[serde(rename_all = "camelCase")] +pub struct FeeRateGovernor { + // The current cost of a signature This amount may increase/decrease over time based on + // cluster processing load. + #[serde(skip)] + pub lamports_per_signature: u64, + + // The target cost of a signature when the cluster is operating around target_signatures_per_slot + // signatures + pub target_lamports_per_signature: u64, + + // Used to estimate the desired processing capacity of the cluster. As the signatures for + // recent slots are fewer/greater than this value, lamports_per_signature will decrease/increase + // for the next slot. A value of 0 disables lamports_per_signature fee adjustments + pub target_signatures_per_slot: u64, + + pub min_lamports_per_signature: u64, + pub max_lamports_per_signature: u64, + + // What portion of collected fees are to be destroyed, as a fraction of std::u8::MAX + pub burn_percent: u8, +} + +pub const DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 10_000; +pub const DEFAULT_TARGET_SIGNATURES_PER_SLOT: u64 = 50 * DEFAULT_MS_PER_SLOT; + +// Percentage of tx fees to burn +pub const DEFAULT_BURN_PERCENT: u8 = 50; + +impl Default for FeeRateGovernor { + fn default() -> Self { + Self { + lamports_per_signature: 0, + target_lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, + target_signatures_per_slot: DEFAULT_TARGET_SIGNATURES_PER_SLOT, + min_lamports_per_signature: 0, + max_lamports_per_signature: 0, + burn_percent: DEFAULT_BURN_PERCENT, + } + } +} + +impl FeeRateGovernor { + pub fn new(target_lamports_per_signature: u64, target_signatures_per_slot: u64) -> Self { + let base_fee_rate_governor = Self { + target_lamports_per_signature, + lamports_per_signature: target_lamports_per_signature, + target_signatures_per_slot, + ..FeeRateGovernor::default() + }; + + Self::new_derived(&base_fee_rate_governor, 0) + } + + pub fn new_derived( + base_fee_rate_governor: &FeeRateGovernor, + latest_signatures_per_slot: u64, + ) -> Self { + let mut me = base_fee_rate_governor.clone(); + + if me.target_signatures_per_slot > 0 { + // lamports_per_signature can range from 50% to 1000% of + // target_lamports_per_signature + me.min_lamports_per_signature = std::cmp::max(1, me.target_lamports_per_signature / 2); + me.max_lamports_per_signature = me.target_lamports_per_signature * 10; + + // What the cluster should charge at `latest_signatures_per_slot` + let desired_lamports_per_signature = + me.max_lamports_per_signature + .min(me.min_lamports_per_signature.max( + me.target_lamports_per_signature + * std::cmp::min(latest_signatures_per_slot, std::u32::MAX as u64) + as u64 + / me.target_signatures_per_slot as u64, + )); + + trace!( + "desired_lamports_per_signature: {}", + desired_lamports_per_signature + ); + + let gap = desired_lamports_per_signature as i64 + - base_fee_rate_governor.lamports_per_signature as i64; + + if gap == 0 { + me.lamports_per_signature = desired_lamports_per_signature; + } else { + // Adjust fee by 5% of target_lamports_per_signature to produce a smooth + // increase/decrease in fees over time. + let gap_adjust = + std::cmp::max(1, me.target_lamports_per_signature / 20) as i64 * gap.signum(); + + trace!( + "lamports_per_signature gap is {}, adjusting by {}", + gap, + gap_adjust + ); + + me.lamports_per_signature = + me.max_lamports_per_signature + .min(me.min_lamports_per_signature.max( + (base_fee_rate_governor.lamports_per_signature as i64 + gap_adjust) + as u64, + )); + } + } else { + me.lamports_per_signature = base_fee_rate_governor.target_lamports_per_signature; + me.min_lamports_per_signature = me.target_lamports_per_signature; + me.max_lamports_per_signature = me.target_lamports_per_signature; + } + debug!( + "new_derived(): lamports_per_signature: {}", + me.lamports_per_signature + ); + me + } + + pub fn clone_with_lamports_per_signature(&self, lamports_per_signature: u64) -> Self { + Self { + lamports_per_signature, + ..*self + } + } + + /// calculate unburned fee from a fee total, returns (unburned, burned) + pub fn burn(&self, fees: u64) -> (u64, u64) { + let burned = fees * u64::from(self.burn_percent) / 100; + (fees - burned, burned) + } + + /// create a FeeCalculator based on current cluster signature throughput + pub fn create_fee_calculator(&self) -> FeeCalculator { + FeeCalculator::new(self.lamports_per_signature) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{pubkey::Pubkey, system_instruction}, + }; + + #[test] + fn test_fee_rate_governor_burn() { + let mut fee_rate_governor = FeeRateGovernor::default(); + assert_eq!(fee_rate_governor.burn(2), (1, 1)); + + fee_rate_governor.burn_percent = 0; + assert_eq!(fee_rate_governor.burn(2), (2, 0)); + + fee_rate_governor.burn_percent = 100; + assert_eq!(fee_rate_governor.burn(2), (0, 2)); + } + + #[test] + #[allow(deprecated)] + fn test_fee_calculator_calculate_fee() { + // Default: no fee. + let message = Message::default(); + assert_eq!(FeeCalculator::default().calculate_fee(&message), 0); + + // No signature, no fee. + assert_eq!(FeeCalculator::new(1).calculate_fee(&message), 0); + + // One signature, a fee. + let pubkey0 = Pubkey::new(&[0; 32]); + let pubkey1 = Pubkey::new(&[1; 32]); + let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); + let message = Message::new(&[ix0], Some(&pubkey0)); + assert_eq!(FeeCalculator::new(2).calculate_fee(&message), 2); + + // Two signatures, double the fee. + let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); + let ix1 = system_instruction::transfer(&pubkey1, &pubkey0, 1); + let message = Message::new(&[ix0, ix1], Some(&pubkey0)); + assert_eq!(FeeCalculator::new(2).calculate_fee(&message), 4); + } + + #[test] + #[allow(deprecated)] + fn test_fee_calculator_calculate_fee_secp256k1() { + use crate::instruction::Instruction; + let pubkey0 = Pubkey::new(&[0; 32]); + let pubkey1 = Pubkey::new(&[1; 32]); + let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); + let mut secp_instruction = Instruction { + program_id: crate::secp256k1_program::id(), + accounts: vec![], + data: vec![], + }; + let mut secp_instruction2 = Instruction { + program_id: crate::secp256k1_program::id(), + accounts: vec![], + data: vec![1], + }; + + let message = Message::new( + &[ + ix0.clone(), + secp_instruction.clone(), + secp_instruction2.clone(), + ], + Some(&pubkey0), + ); + assert_eq!(FeeCalculator::new(1).calculate_fee(&message), 2); + + secp_instruction.data = vec![0]; + secp_instruction2.data = vec![10]; + let message = Message::new(&[ix0, secp_instruction, secp_instruction2], Some(&pubkey0)); + assert_eq!(FeeCalculator::new(1).calculate_fee(&message), 11); + } + + #[test] + fn test_fee_rate_governor_derived_default() { + solana_logger::setup(); + + let f0 = FeeRateGovernor::default(); + assert_eq!( + f0.target_signatures_per_slot, + DEFAULT_TARGET_SIGNATURES_PER_SLOT + ); + assert_eq!( + f0.target_lamports_per_signature, + DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE + ); + assert_eq!(f0.lamports_per_signature, 0); + + let f1 = FeeRateGovernor::new_derived(&f0, DEFAULT_TARGET_SIGNATURES_PER_SLOT); + assert_eq!( + f1.target_signatures_per_slot, + DEFAULT_TARGET_SIGNATURES_PER_SLOT + ); + assert_eq!( + f1.target_lamports_per_signature, + DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE + ); + assert_eq!( + f1.lamports_per_signature, + DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2 + ); // min + } + + #[test] + fn test_fee_rate_governor_derived_adjust() { + solana_logger::setup(); + + let mut f = FeeRateGovernor { + target_lamports_per_signature: 100, + target_signatures_per_slot: 100, + ..FeeRateGovernor::default() + }; + f = FeeRateGovernor::new_derived(&f, 0); + + // Ramp fees up + let mut count = 0; + loop { + let last_lamports_per_signature = f.lamports_per_signature; + + f = FeeRateGovernor::new_derived(&f, std::u64::MAX); + info!("[up] f.lamports_per_signature={}", f.lamports_per_signature); + + // some maximum target reached + if f.lamports_per_signature == last_lamports_per_signature { + break; + } + // shouldn't take more than 1000 steps to get to minimum + assert!(count < 1000); + count += 1; + } + + // Ramp fees down + let mut count = 0; + loop { + let last_lamports_per_signature = f.lamports_per_signature; + f = FeeRateGovernor::new_derived(&f, 0); + + info!( + "[down] f.lamports_per_signature={}", + f.lamports_per_signature + ); + + // some minimum target reached + if f.lamports_per_signature == last_lamports_per_signature { + break; + } + + // shouldn't take more than 1000 steps to get to minimum + assert!(count < 1000); + count += 1; + } + + // Arrive at target rate + let mut count = 0; + while f.lamports_per_signature != f.target_lamports_per_signature { + f = FeeRateGovernor::new_derived(&f, f.target_signatures_per_slot); + info!( + "[target] f.lamports_per_signature={}", + f.lamports_per_signature + ); + // shouldn't take more than 100 steps to get to target + assert!(count < 100); + count += 1; + } + } +} +*/ diff --git a/rust/chains/hyperlane-sealevel/src/solana/sdk/Cargo.toml b/rust/chains/hyperlane-sealevel/src/solana/sdk/Cargo.toml new file mode 100644 index 0000000000..4369afa77d --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/sdk/Cargo.toml @@ -0,0 +1,101 @@ +[package] +name = "solana-sdk" +version = "1.14.13" +description = "Solana SDK" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +homepage = "https://solana.com/" +documentation = "https://docs.rs/solana-sdk" +readme = "README.md" +license = "Apache-2.0" +edition = "2021" + +[features] +# "program" feature is a legacy feature retained to support v1.3 and older +# programs. New development should not use this feature. Instead use the +# solana-program crate +program = [] + +default = [ + "full" # functionality that is not compatible or needed for on-chain programs +] +full = [ + "assert_matches", + "byteorder", + "chrono", + "generic-array", + "memmap2", + "rand", + "rand_chacha", + "serde_json", + # "ed25519-dalek", + "ed25519-dalek-bip32", + # "solana-logger", + "libsecp256k1", + "sha3", + "digest", +] + +[dependencies] +assert_matches = { version = "1.5.0", optional = true } +base64 = "0.13" +bincode = "1.3.3" +bitflags = "1.3.1" +borsh = "0.9.3" +bs58 = "0.4.0" +bytemuck = { version = "1.11.0", features = ["derive"] } +byteorder = { version = "1.4.3", optional = true } +chrono = { default-features = false, features = ["alloc"], version = "0.4", optional = true } +derivation-path = { version = "0.2.0", default-features = false } +digest = { version = "0.10.1", optional = true } +ed25519-dalek-bip32 = { version = "0.2.0", optional = true } +ed25519-dalek = { version = "=1.0.1", git = "https://github.com/Eclipse-Laboratories-Inc/ed25519-dalek", branch = "steven/fix-deps" } +generic-array = { version = "0.14.5", default-features = false, features = ["serde", "more_lengths"], optional = true } +hmac = "0.12.1" +itertools = "0.10.3" +lazy_static = "1.4.0" +libsecp256k1 = { version = "0.6.0", optional = true } +log = "0.4.17" +memmap2 = { version = "0.5.3", optional = true } +num-derive = "0.3" +num-traits = "0.2" +pbkdf2 = { version = "0.11.0", default-features = false } +qstring = "0.7.2" +rand = { version = "0.7.0", optional = true } +rand_chacha = { version = "0.2.2", optional = true } +rustversion = "1.0.7" +serde = "1.0.138" +serde_bytes = "0.11" +serde_derive = "1.0.103" +serde_json = { version = "1.0.81", optional = true } +sha2 = "0.10.2" +sha3 = { version = "0.10.1", optional = true } +# solana-frozen-abi = { path = "../frozen-abi", version = "=1.14.13" } +# solana-frozen-abi-macro = { path = "../frozen-abi/macro", version = "=1.14.13" } +# solana-logger = { path = "../logger", version = "=1.14.13", optional = true } +# solana-program = { path = "program", version = "=1.14.13" } +solana-sdk-macro = { path = "macro", version = "=1.14.13" } +thiserror = "1.0" +uriparse = "0.6.4" +wasm-bindgen = "0.2" + +[dependencies.curve25519-dalek] +version = "3.2.1" +features = ["serde"] +git = "https://github.com/Eclipse-Laboratories-Inc/curve25519-dalek" +branch = "steven/fix-deps" + +[dev-dependencies] +anyhow = "1.0.58" +hex = "0.4.3" +static_assertions = "1.1.0" +tiny-bip39 = "0.8.2" + +[build-dependencies] +rustc_version = "0.4" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lib] +crate-type = ["cdylib", "rlib"] diff --git a/rust/chains/hyperlane-sealevel/src/solana/sdk/macro/Cargo.toml b/rust/chains/hyperlane-sealevel/src/solana/sdk/macro/Cargo.toml new file mode 100644 index 0000000000..826e451846 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/sdk/macro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "solana-sdk-macro" +version = "1.14.13" +description = "Solana SDK Macro" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +homepage = "https://solana.com/" +documentation = "https://docs.rs/solana-sdk-macro" +license = "Apache-2.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +bs58 = "0.4.0" +proc-macro2 = "1.0.19" +quote = "1.0" +syn = { version = "1.0", features = ["full", "extra-traits"] } +rustversion = "1.0.7" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/rust/chains/hyperlane-sealevel/src/solana/sdk/macro/src/lib.rs b/rust/chains/hyperlane-sealevel/src/solana/sdk/macro/src/lib.rs new file mode 100644 index 0000000000..feccca6255 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/sdk/macro/src/lib.rs @@ -0,0 +1,405 @@ +//! Convenience macro to declare a static public key and functions to interact with it +//! +//! Input: a single literal base58 string representation of a program's id +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::{Delimiter, Span, TokenTree}; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream, Result}; +use syn::{parse_macro_input, Expr, LitByte, LitStr}; + +fn parse_id( + input: ParseStream, + pubkey_type: proc_macro2::TokenStream, +) -> Result { + let id = if input.peek(syn::LitStr) { + let id_literal: LitStr = input.parse()?; + parse_pubkey(&id_literal, &pubkey_type)? + } else { + let expr: Expr = input.parse()?; + quote! { #expr } + }; + + if !input.is_empty() { + let stream: proc_macro2::TokenStream = input.parse()?; + return Err(syn::Error::new_spanned(stream, "unexpected token")); + } + Ok(id) +} + +fn id_to_tokens( + id: &proc_macro2::TokenStream, + pubkey_type: proc_macro2::TokenStream, + tokens: &mut proc_macro2::TokenStream, +) { + tokens.extend(quote! { + /// The static program ID + pub static ID: #pubkey_type = #id; + + /// Confirms that a given pubkey is equivalent to the program ID + pub fn check_id(id: &#pubkey_type) -> bool { + id == &ID + } + + /// Returns the program ID + pub fn id() -> #pubkey_type { + ID + } + + #[cfg(test)] + #[test] + fn test_id() { + assert!(check_id(&id())); + } + }); +} + +/* +fn deprecated_id_to_tokens( + id: &proc_macro2::TokenStream, + pubkey_type: proc_macro2::TokenStream, + tokens: &mut proc_macro2::TokenStream, +) { + tokens.extend(quote! { + /// The static program ID + pub static ID: #pubkey_type = #id; + + /// Confirms that a given pubkey is equivalent to the program ID + #[deprecated()] + pub fn check_id(id: &#pubkey_type) -> bool { + id == &ID + } + + /// Returns the program ID + #[deprecated()] + pub fn id() -> #pubkey_type { + ID + } + + #[cfg(test)] + #[test] + fn test_id() { + #[allow(deprecated)] + assert!(check_id(&id())); + } + }); +} + +struct SdkPubkey(proc_macro2::TokenStream); + +impl Parse for SdkPubkey { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_sdk::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for SdkPubkey { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let id = &self.0; + tokens.extend(quote! {#id}) + } +} + +struct ProgramSdkPubkey(proc_macro2::TokenStream); + +impl Parse for ProgramSdkPubkey { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_program::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for ProgramSdkPubkey { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let id = &self.0; + tokens.extend(quote! {#id}) + } +} +*/ + +struct Id(proc_macro2::TokenStream); + +impl Parse for Id { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { Pubkey }).map(Self) + } +} + +impl ToTokens for Id { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + id_to_tokens(&self.0, quote! { Pubkey }, tokens) + } +} + +/* +struct IdDeprecated(proc_macro2::TokenStream); + +impl Parse for IdDeprecated { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_sdk::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for IdDeprecated { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + deprecated_id_to_tokens(&self.0, quote! { ::solana_sdk::pubkey::Pubkey }, tokens) + } +} + +struct ProgramSdkId(proc_macro2::TokenStream); +impl Parse for ProgramSdkId { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_program::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for ProgramSdkId { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + id_to_tokens(&self.0, quote! { ::solana_program::pubkey::Pubkey }, tokens) + } +} + +struct ProgramSdkIdDeprecated(proc_macro2::TokenStream); +impl Parse for ProgramSdkIdDeprecated { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_program::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for ProgramSdkIdDeprecated { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + deprecated_id_to_tokens(&self.0, quote! { ::solana_program::pubkey::Pubkey }, tokens) + } +} + +#[allow(dead_code)] // `respan` may be compiled out +struct RespanInput { + to_respan: Path, + respan_using: Span, +} + +impl Parse for RespanInput { + fn parse(input: ParseStream) -> Result { + let to_respan: Path = input.parse()?; + let _comma: Token![,] = input.parse()?; + let respan_tree: TokenTree = input.parse()?; + match respan_tree { + TokenTree::Group(g) if g.delimiter() == Delimiter::None => { + let ident: Ident = syn::parse2(g.stream())?; + Ok(RespanInput { + to_respan, + respan_using: ident.span(), + }) + } + TokenTree::Ident(i) => Ok(RespanInput { + to_respan, + respan_using: i.span(), + }), + val => Err(syn::Error::new_spanned( + val, + "expected None-delimited group", + )), + } + } +} + +/// A proc-macro which respans the tokens in its first argument (a `Path`) +/// to be resolved at the tokens of its second argument. +/// For internal use only. +/// +/// There must be exactly one comma in the input, +/// which is used to separate the two arguments. +/// The second argument should be exactly one token. +/// +/// For example, `respan!($crate::foo, with_span)` +/// produces the tokens `$crate::foo`, but resolved +/// at the span of `with_span`. +/// +/// The input to this function should be very short - +/// its only purpose is to override the span of a token +/// sequence containing `$crate`. For all other purposes, +/// a more general proc-macro should be used. +#[rustversion::since(1.46.0)] // `Span::resolved_at` is stable in 1.46.0 and above +#[proc_macro] +pub fn respan(input: TokenStream) -> TokenStream { + // Obtain the `Path` we are going to respan, and the ident + // whose span we will be using. + let RespanInput { + to_respan, + respan_using, + } = parse_macro_input!(input as RespanInput); + // Respan all of the tokens in the `Path` + let to_respan: proc_macro2::TokenStream = to_respan + .into_token_stream() + .into_iter() + .map(|mut t| { + // Combine the location of the token with the resolution behavior of `respan_using` + let new_span: Span = t.span().resolved_at(respan_using); + t.set_span(new_span); + t + }) + .collect(); + TokenStream::from(to_respan) +} + +#[proc_macro] +pub fn pubkey(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as SdkPubkey); + TokenStream::from(quote! {#id}) +} + +#[proc_macro] +pub fn program_pubkey(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as ProgramSdkPubkey); + TokenStream::from(quote! {#id}) +} +*/ + +#[proc_macro] +pub fn declare_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as Id); + TokenStream::from(quote! {#id}) +} + +/* +#[proc_macro] +pub fn declare_deprecated_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as IdDeprecated); + TokenStream::from(quote! {#id}) +} + +#[proc_macro] +pub fn program_declare_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as ProgramSdkId); + TokenStream::from(quote! {#id}) +} + +#[proc_macro] +pub fn program_declare_deprecated_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as ProgramSdkIdDeprecated); + TokenStream::from(quote! {#id}) +} +*/ + +fn parse_pubkey( + id_literal: &LitStr, + pubkey_type: &proc_macro2::TokenStream, +) -> Result { + let id_vec = bs58::decode(id_literal.value()) + .into_vec() + .map_err(|_| syn::Error::new_spanned(id_literal, "failed to decode base58 string"))?; + let id_array = <[u8; 32]>::try_from(<&[u8]>::clone(&&id_vec[..])).map_err(|_| { + syn::Error::new_spanned( + id_literal, + format!("pubkey array is not 32 bytes long: len={}", id_vec.len()), + ) + })?; + let bytes = id_array.iter().map(|b| LitByte::new(*b, Span::call_site())); + Ok(quote! { + #pubkey_type::new_from_array( + [#(#bytes,)*] + ) + }) +} + +/* +struct Pubkeys { + method: Ident, + num: usize, + pubkeys: proc_macro2::TokenStream, +} +impl Parse for Pubkeys { + fn parse(input: ParseStream) -> Result { + let pubkey_type = quote! { + ::solana_sdk::pubkey::Pubkey + }; + + let method = input.parse()?; + let _comma: Token![,] = input.parse()?; + let (num, pubkeys) = if input.peek(syn::LitStr) { + let id_literal: LitStr = input.parse()?; + (1, parse_pubkey(&id_literal, &pubkey_type)?) + } else if input.peek(Bracket) { + let pubkey_strings; + bracketed!(pubkey_strings in input); + let punctuated: Punctuated = + Punctuated::parse_terminated(&pubkey_strings)?; + let mut pubkeys: Punctuated = Punctuated::new(); + for string in punctuated.iter() { + pubkeys.push(parse_pubkey(string, &pubkey_type)?); + } + (pubkeys.len(), quote! {#pubkeys}) + } else { + let stream: proc_macro2::TokenStream = input.parse()?; + return Err(syn::Error::new_spanned(stream, "unexpected token")); + }; + + Ok(Pubkeys { + method, + num, + pubkeys, + }) + } +} + +impl ToTokens for Pubkeys { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Pubkeys { + method, + num, + pubkeys, + } = self; + + let pubkey_type = quote! { + ::solana_sdk::pubkey::Pubkey + }; + if *num == 1 { + tokens.extend(quote! { + pub fn #method() -> #pubkey_type { + #pubkeys + } + }); + } else { + tokens.extend(quote! { + pub fn #method() -> ::std::vec::Vec<#pubkey_type> { + vec![#pubkeys] + } + }); + } + } +} + +#[proc_macro] +pub fn pubkeys(input: TokenStream) -> TokenStream { + let pubkeys = parse_macro_input!(input as Pubkeys); + TokenStream::from(quote! {#pubkeys}) +} + +// The normal `wasm_bindgen` macro generates a .bss section which causes the resulting +// BPF program to fail to load, so for now this stub should be used when building for BPF +#[proc_macro_attribute] +pub fn wasm_bindgen_stub(_attr: TokenStream, item: TokenStream) -> TokenStream { + match parse_macro_input!(item as syn::Item) { + syn::Item::Struct(mut item_struct) => { + if let syn::Fields::Named(fields) = &mut item_struct.fields { + // Strip out any `#[wasm_bindgen]` added to struct fields. This is custom + // syntax supplied by the normal `wasm_bindgen` macro. + for field in fields.named.iter_mut() { + field.attrs.retain(|attr| { + !attr + .path + .segments + .iter() + .any(|segment| segment.ident == "wasm_bindgen") + }); + } + } + quote! { #item_struct } + } + item => { + quote!(#item) + } + } + .into() +} +*/ diff --git a/rust/chains/hyperlane-sealevel/src/solana/sdk/src/lib.rs b/rust/chains/hyperlane-sealevel/src/solana/sdk/src/lib.rs new file mode 100644 index 0000000000..9106cce526 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/sdk/src/lib.rs @@ -0,0 +1 @@ +pub use solana_sdk_macro::declare_id; diff --git a/rust/chains/hyperlane-sealevel/src/solana/secp256k1_program.rs b/rust/chains/hyperlane-sealevel/src/solana/secp256k1_program.rs new file mode 100644 index 0000000000..529fd7aad3 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/secp256k1_program.rs @@ -0,0 +1,12 @@ +//! The [secp256k1 native program][np]. +//! +//! [np]: https://docs.solana.com/developing/runtime-facilities/programs#secp256k1-program +//! +//! Constructors for secp256k1 program instructions, and documentation on the +//! program's usage can be found in [`solana_sdk::secp256k1_instruction`]. +//! +//! [`solana_sdk::secp256k1_instruction`]: https://docs.rs/solana-sdk/latest/solana_sdk/secp256k1_instruction/index.html +use crate::solana::pubkey::Pubkey; +use solana_sdk_macro::declare_id; + +declare_id!("KeccakSecp256k11111111111111111111111111111"); diff --git a/rust/chains/hyperlane-sealevel/src/solana/solana_sdk/mod.rs b/rust/chains/hyperlane-sealevel/src/solana/solana_sdk/mod.rs new file mode 100644 index 0000000000..9106cce526 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/solana_sdk/mod.rs @@ -0,0 +1 @@ +pub use solana_sdk_macro::declare_id; diff --git a/rust/chains/hyperlane-sealevel/src/solana/solana_sdk/solana_sdk_macro/mod.rs b/rust/chains/hyperlane-sealevel/src/solana/solana_sdk/solana_sdk_macro/mod.rs new file mode 100644 index 0000000000..15e138ca4a --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/solana/solana_sdk/solana_sdk_macro/mod.rs @@ -0,0 +1,423 @@ +//! Convenience macro to declare a static public key and functions to interact with it +//! +//! Input: a single literal base58 string representation of a program's id +extern crate proc_macro; + +use proc_macro::{TokenStream}; +use syn::{parse_macro_input, LitStr, Expr, LitByte}; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream, Result}; +use proc_macro2::{Delimiter, Span, TokenTree}; + + +use { + // proc_macro::TokenStream, + // proc_macro2::{Delimiter, Span, TokenTree}, + // quote::{quote, ToTokens}, + // std::convert::TryFrom, + // syn::{ + // bracketed, + // parse::{Parse, ParseStream, Result}, + // parse_macro_input, + // punctuated::Punctuated, + // token::Bracket, + // Expr, Ident, LitByte, LitStr, Path, Token, + // }, +}; + + +fn parse_id( + input: ParseStream, + pubkey_type: proc_macro2::TokenStream, +) -> Result { + let id = if input.peek(syn::LitStr) { + let id_literal: LitStr = input.parse()?; + parse_pubkey(&id_literal, &pubkey_type)? + } else { + let expr: Expr = input.parse()?; + quote! { #expr } + }; + + if !input.is_empty() { + let stream: proc_macro2::TokenStream = input.parse()?; + return Err(syn::Error::new_spanned(stream, "unexpected token")); + } + Ok(id) +} + +fn id_to_tokens( + id: &proc_macro2::TokenStream, + pubkey_type: proc_macro2::TokenStream, + tokens: &mut proc_macro2::TokenStream, +) { + tokens.extend(quote! { + /// The static program ID + pub static ID: #pubkey_type = #id; + + /// Confirms that a given pubkey is equivalent to the program ID + pub fn check_id(id: &#pubkey_type) -> bool { + id == &ID + } + + /// Returns the program ID + pub fn id() -> #pubkey_type { + ID + } + + #[cfg(test)] + #[test] + fn test_id() { + assert!(check_id(&id())); + } + }); +} + +/* +fn deprecated_id_to_tokens( + id: &proc_macro2::TokenStream, + pubkey_type: proc_macro2::TokenStream, + tokens: &mut proc_macro2::TokenStream, +) { + tokens.extend(quote! { + /// The static program ID + pub static ID: #pubkey_type = #id; + + /// Confirms that a given pubkey is equivalent to the program ID + #[deprecated()] + pub fn check_id(id: &#pubkey_type) -> bool { + id == &ID + } + + /// Returns the program ID + #[deprecated()] + pub fn id() -> #pubkey_type { + ID + } + + #[cfg(test)] + #[test] + fn test_id() { + #[allow(deprecated)] + assert!(check_id(&id())); + } + }); +} + +struct SdkPubkey(proc_macro2::TokenStream); + +impl Parse for SdkPubkey { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_sdk::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for SdkPubkey { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let id = &self.0; + tokens.extend(quote! {#id}) + } +} + +struct ProgramSdkPubkey(proc_macro2::TokenStream); + +impl Parse for ProgramSdkPubkey { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_program::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for ProgramSdkPubkey { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let id = &self.0; + tokens.extend(quote! {#id}) + } +} +*/ + +struct Id(proc_macro2::TokenStream); + + +impl Parse for Id { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_sdk::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for Id { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + id_to_tokens(&self.0, quote! { ::solana_sdk::pubkey::Pubkey }, tokens) + } +} + +/* +struct IdDeprecated(proc_macro2::TokenStream); + +impl Parse for IdDeprecated { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_sdk::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for IdDeprecated { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + deprecated_id_to_tokens(&self.0, quote! { ::solana_sdk::pubkey::Pubkey }, tokens) + } +} + +struct ProgramSdkId(proc_macro2::TokenStream); +impl Parse for ProgramSdkId { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_program::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for ProgramSdkId { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + id_to_tokens(&self.0, quote! { ::solana_program::pubkey::Pubkey }, tokens) + } +} + +struct ProgramSdkIdDeprecated(proc_macro2::TokenStream); +impl Parse for ProgramSdkIdDeprecated { + fn parse(input: ParseStream) -> Result { + parse_id(input, quote! { ::solana_program::pubkey::Pubkey }).map(Self) + } +} + +impl ToTokens for ProgramSdkIdDeprecated { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + deprecated_id_to_tokens(&self.0, quote! { ::solana_program::pubkey::Pubkey }, tokens) + } +} + +#[allow(dead_code)] // `respan` may be compiled out +struct RespanInput { + to_respan: Path, + respan_using: Span, +} + +impl Parse for RespanInput { + fn parse(input: ParseStream) -> Result { + let to_respan: Path = input.parse()?; + let _comma: Token![,] = input.parse()?; + let respan_tree: TokenTree = input.parse()?; + match respan_tree { + TokenTree::Group(g) if g.delimiter() == Delimiter::None => { + let ident: Ident = syn::parse2(g.stream())?; + Ok(RespanInput { + to_respan, + respan_using: ident.span(), + }) + } + TokenTree::Ident(i) => Ok(RespanInput { + to_respan, + respan_using: i.span(), + }), + val => Err(syn::Error::new_spanned( + val, + "expected None-delimited group", + )), + } + } +} + +/// A proc-macro which respans the tokens in its first argument (a `Path`) +/// to be resolved at the tokens of its second argument. +/// For internal use only. +/// +/// There must be exactly one comma in the input, +/// which is used to separate the two arguments. +/// The second argument should be exactly one token. +/// +/// For example, `respan!($crate::foo, with_span)` +/// produces the tokens `$crate::foo`, but resolved +/// at the span of `with_span`. +/// +/// The input to this function should be very short - +/// its only purpose is to override the span of a token +/// sequence containing `$crate`. For all other purposes, +/// a more general proc-macro should be used. +#[rustversion::since(1.46.0)] // `Span::resolved_at` is stable in 1.46.0 and above +#[proc_macro] +pub fn respan(input: TokenStream) -> TokenStream { + // Obtain the `Path` we are going to respan, and the ident + // whose span we will be using. + let RespanInput { + to_respan, + respan_using, + } = parse_macro_input!(input as RespanInput); + // Respan all of the tokens in the `Path` + let to_respan: proc_macro2::TokenStream = to_respan + .into_token_stream() + .into_iter() + .map(|mut t| { + // Combine the location of the token with the resolution behavior of `respan_using` + let new_span: Span = t.span().resolved_at(respan_using); + t.set_span(new_span); + t + }) + .collect(); + TokenStream::from(to_respan) +} + +#[proc_macro] +pub fn pubkey(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as SdkPubkey); + TokenStream::from(quote! {#id}) +} + +#[proc_macro] +pub fn program_pubkey(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as ProgramSdkPubkey); + TokenStream::from(quote! {#id}) +} +*/ + +#[proc_macro] +pub fn declare_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as Id); + TokenStream::from(quote! {#id}) +} + +/* +#[proc_macro] +pub fn declare_deprecated_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as IdDeprecated); + TokenStream::from(quote! {#id}) +} + +#[proc_macro] +pub fn program_declare_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as ProgramSdkId); + TokenStream::from(quote! {#id}) +} + +#[proc_macro] +pub fn program_declare_deprecated_id(input: TokenStream) -> TokenStream { + let id = parse_macro_input!(input as ProgramSdkIdDeprecated); + TokenStream::from(quote! {#id}) +} +*/ + +fn parse_pubkey( + id_literal: &LitStr, + pubkey_type: &proc_macro2::TokenStream, +) -> Result { + let id_vec = bs58::decode(id_literal.value()) + .into_vec() + .map_err(|_| syn::Error::new_spanned(id_literal, "failed to decode base58 string"))?; + let id_array = <[u8; 32]>::try_from(<&[u8]>::clone(&&id_vec[..])).map_err(|_| { + syn::Error::new_spanned( + id_literal, + format!("pubkey array is not 32 bytes long: len={}", id_vec.len()), + ) + })?; + let bytes = id_array.iter().map(|b| LitByte::new(*b, Span::call_site())); + Ok(quote! { + #pubkey_type::new_from_array( + [#(#bytes,)*] + ) + }) +} + +/* +struct Pubkeys { + method: Ident, + num: usize, + pubkeys: proc_macro2::TokenStream, +} +impl Parse for Pubkeys { + fn parse(input: ParseStream) -> Result { + let pubkey_type = quote! { + ::solana_sdk::pubkey::Pubkey + }; + + let method = input.parse()?; + let _comma: Token![,] = input.parse()?; + let (num, pubkeys) = if input.peek(syn::LitStr) { + let id_literal: LitStr = input.parse()?; + (1, parse_pubkey(&id_literal, &pubkey_type)?) + } else if input.peek(Bracket) { + let pubkey_strings; + bracketed!(pubkey_strings in input); + let punctuated: Punctuated = + Punctuated::parse_terminated(&pubkey_strings)?; + let mut pubkeys: Punctuated = Punctuated::new(); + for string in punctuated.iter() { + pubkeys.push(parse_pubkey(string, &pubkey_type)?); + } + (pubkeys.len(), quote! {#pubkeys}) + } else { + let stream: proc_macro2::TokenStream = input.parse()?; + return Err(syn::Error::new_spanned(stream, "unexpected token")); + }; + + Ok(Pubkeys { + method, + num, + pubkeys, + }) + } +} + +impl ToTokens for Pubkeys { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Pubkeys { + method, + num, + pubkeys, + } = self; + + let pubkey_type = quote! { + ::solana_sdk::pubkey::Pubkey + }; + if *num == 1 { + tokens.extend(quote! { + pub fn #method() -> #pubkey_type { + #pubkeys + } + }); + } else { + tokens.extend(quote! { + pub fn #method() -> ::std::vec::Vec<#pubkey_type> { + vec![#pubkeys] + } + }); + } + } +} + +#[proc_macro] +pub fn pubkeys(input: TokenStream) -> TokenStream { + let pubkeys = parse_macro_input!(input as Pubkeys); + TokenStream::from(quote! {#pubkeys}) +} + +// The normal `wasm_bindgen` macro generates a .bss section which causes the resulting +// BPF program to fail to load, so for now this stub should be used when building for BPF +#[proc_macro_attribute] +pub fn wasm_bindgen_stub(_attr: TokenStream, item: TokenStream) -> TokenStream { + match parse_macro_input!(item as syn::Item) { + syn::Item::Struct(mut item_struct) => { + if let syn::Fields::Named(fields) = &mut item_struct.fields { + // Strip out any `#[wasm_bindgen]` added to struct fields. This is custom + // syntax supplied by the normal `wasm_bindgen` macro. + for field in fields.named.iter_mut() { + field.attrs.retain(|attr| { + !attr + .path + .segments + .iter() + .any(|segment| segment.ident == "wasm_bindgen") + }); + } + } + quote! { #item_struct } + } + item => { + quote!(#item) + } + } + .into() +} +*/ diff --git a/rust/chains/hyperlane-sealevel/src/trait_builder.rs b/rust/chains/hyperlane-sealevel/src/trait_builder.rs new file mode 100644 index 0000000000..6d4385cd6b --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/trait_builder.rs @@ -0,0 +1,61 @@ +use url::Url; + +use hyperlane_core::{ + config::{ConfigErrResultExt, ConfigPath, ConfigResult, FromRawConf}, + ChainCommunicationError, +}; + +/// Sealevel connection configuration +#[derive(Debug, Clone)] +pub struct ConnectionConf { + /// Fully qualified string to connect to + pub url: Url, +} + +/// Raw Sealevel connection configuration used for better deserialization errors. +#[derive(Debug, serde::Deserialize)] +pub struct RawConnectionConf { + url: Option, +} + +/// An error type when parsing a connection configuration. +#[derive(thiserror::Error, Debug)] +pub enum ConnectionConfError { + /// Missing `url` for connection configuration + #[error("Missing `url` for connection configuration")] + MissingConnectionUrl, + /// Invalid `url` for connection configuration + #[error("Invalid `url` for connection configuration: `{0}` ({1})")] + InvalidConnectionUrl(String, url::ParseError), +} + +impl FromRawConf<'_, RawConnectionConf> for ConnectionConf { + fn from_config_filtered( + raw: RawConnectionConf, + cwp: &ConfigPath, + _filter: (), + ) -> ConfigResult { + use ConnectionConfError::*; + match raw { + RawConnectionConf { url: Some(url) } => Ok(Self { + url: url + .parse() + .map_err(|e| InvalidConnectionUrl(url, e)) + .into_config_result(|| cwp.join("url"))?, + }), + RawConnectionConf { url: None } => { + Err(MissingConnectionUrl).into_config_result(|| cwp.join("url")) + } + } + } +} + +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +struct SealevelNewConnectionError(#[from] anyhow::Error); + +impl From for ChainCommunicationError { + fn from(err: SealevelNewConnectionError) -> Self { + ChainCommunicationError::from_other(err) + } +} diff --git a/rust/chains/hyperlane-sealevel/src/utils.rs b/rust/chains/hyperlane-sealevel/src/utils.rs new file mode 100644 index 0000000000..01cd669db1 --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/utils.rs @@ -0,0 +1,79 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{ChainCommunicationError, ChainResult}; + +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::{ + commitment_config::CommitmentConfig, + instruction::{AccountMeta, Instruction}, + message::Message, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_transaction_status::UiReturnDataEncoding; + +/// Simulates an instruction, and attempts to deserialize it into a T. +/// If no return data at all was returned, returns Ok(None). +/// If some return data was returned but deserialization was unsuccessful, +/// an Err is returned. +pub async fn simulate_instruction( + rpc_client: &RpcClient, + payer: &Keypair, + instruction: Instruction, +) -> ChainResult> { + let commitment = CommitmentConfig::finalized(); + let (recent_blockhash, _) = rpc_client + .get_latest_blockhash_with_commitment(commitment) + .await + .map_err(ChainCommunicationError::from_other)?; + let return_data = rpc_client + .simulate_transaction(&Transaction::new_unsigned(Message::new_with_blockhash( + &[instruction], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await + .map_err(ChainCommunicationError::from_other)? + .value + .return_data; + + if let Some(return_data) = return_data { + let bytes = match return_data.data.1 { + UiReturnDataEncoding::Base64 => { + base64::decode(return_data.data.0).map_err(ChainCommunicationError::from_other)? + } + }; + + let decoded_data = + T::try_from_slice(bytes.as_slice()).map_err(ChainCommunicationError::from_other)?; + + return Ok(Some(decoded_data)); + } + + Ok(None) +} + +/// Simulates an Instruction that will return a list of AccountMetas. +pub async fn get_account_metas( + rpc_client: &RpcClient, + payer: &Keypair, + instruction: Instruction, +) -> ChainResult> { + // If there's no data at all, default to an empty vec. + let account_metas = simulate_instruction::>>( + rpc_client, + payer, + instruction, + ) + .await? + .map(|serializable_account_metas| { + serializable_account_metas + .return_data + .into_iter() + .map(|serializable_account_meta| serializable_account_meta.into()) + .collect() + }) + .unwrap_or_else(Vec::new); + + Ok(account_metas) +} diff --git a/rust/chains/hyperlane-sealevel/src/validator_announce.rs b/rust/chains/hyperlane-sealevel/src/validator_announce.rs new file mode 100644 index 0000000000..c7f44623ef --- /dev/null +++ b/rust/chains/hyperlane-sealevel/src/validator_announce.rs @@ -0,0 +1,129 @@ +use async_trait::async_trait; +use tracing::{info, instrument, warn}; + +use hyperlane_core::{ + Announcement, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, + HyperlaneContract, HyperlaneDomain, SignedType, TxOutcome, ValidatorAnnounce, H160, H256, U256, +}; +use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; + +use crate::{ConnectionConf, RpcClientWithDebug}; +use hyperlane_sealevel_validator_announce::{ + accounts::ValidatorStorageLocationsAccount, validator_storage_locations_pda_seeds, +}; + +/// A reference to a ValidatorAnnounce contract on some Sealevel chain +#[derive(Debug)] +pub struct SealevelValidatorAnnounce { + program_id: Pubkey, + rpc_client: RpcClientWithDebug, + domain: HyperlaneDomain, +} + +impl SealevelValidatorAnnounce { + /// Create a new Sealevel ValidatorAnnounce + pub fn new(conf: &ConnectionConf, locator: ContractLocator) -> Self { + let rpc_client = RpcClientWithDebug::new(conf.url.to_string()); + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + Self { + program_id, + rpc_client, + domain: locator.domain.clone(), + } + } +} + +impl HyperlaneContract for SealevelValidatorAnnounce { + fn address(&self) -> H256 { + self.program_id.to_bytes().into() + } +} + +impl HyperlaneChain for SealevelValidatorAnnounce { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(crate::SealevelProvider::new(self.domain.clone())) + } +} + +#[async_trait] +impl ValidatorAnnounce for SealevelValidatorAnnounce { + async fn get_announced_storage_locations( + &self, + validators: &[H256], + ) -> ChainResult>> { + info!(program_id=?self.program_id, validators=?validators, "Getting validator storage locations"); + + // Get the validator storage location PDAs for each validator. + let account_pubkeys: Vec = validators + .iter() + .map(|v| { + let (key, _bump) = Pubkey::find_program_address( + // The seed is based off the H160 representation of the validator address. + validator_storage_locations_pda_seeds!(H160::from_slice(&v.as_bytes()[12..])), + &self.program_id, + ); + key + }) + .collect(); + + // Get all validator storage location accounts. + // If an account doesn't exist, it will be returned as None. + let accounts = self + .rpc_client + .get_multiple_accounts_with_commitment(&account_pubkeys, CommitmentConfig::finalized()) + .await + .map_err(ChainCommunicationError::from_other)? + .value; + + // Parse the storage locations from each account. + // If a validator's account doesn't exist, its storage locations will + // be returned as an empty list. + let storage_locations: Vec> = accounts + .into_iter() + .map(|account| { + account + .map(|account| { + match ValidatorStorageLocationsAccount::fetch(&mut &account.data[..]) { + Ok(v) => v.into_inner().storage_locations, + Err(err) => { + // If there's an error parsing the account, gracefully return an empty list + info!(?account, ?err, "Unable to parse validator announce account"); + vec![] + } + } + }) + .unwrap_or_default() + }) + .collect(); + + Ok(storage_locations) + } + + async fn announce_tokens_needed( + &self, + _announcement: SignedType, + ) -> Option { + Some(U256::zero()) + } + + #[instrument(err, ret, skip(self))] + async fn announce( + &self, + _announcement: SignedType, + _tx_gas_limit: Option, + ) -> ChainResult { + warn!( + "Announcing validator storage locations within the agents is not supported on Sealevel" + ); + Ok(TxOutcome { + txid: H256::zero(), + executed: false, + gas_used: U256::zero(), + gas_price: U256::zero(), + }) + } +} diff --git a/rust/config/sealevel/relayer.env b/rust/config/sealevel/relayer.env new file mode 100644 index 0000000000..bba715da2e --- /dev/null +++ b/rust/config/sealevel/relayer.env @@ -0,0 +1,15 @@ +export BASE_CONFIG="sealevel.json" +export RUN_ENV="sealevel" +export HYP_BASE_DB="/tmp/SEALEVEL_DB/relayer" +export HYP_RELAYER_RELAYCHAINS="sealeveltest1,sealeveltest2" +export HYP_BASE_METRICS=9091 +export HYP_BASE_ALLOWLOCALCHECKPOINTSYNCERS=true + +# The first 32 bytes of test-keys/test_deployer-keypair.json as hexadecimal, +# which is the secret key. +export HYP_BASE_CHAINS_SEALEVELTEST1_SIGNER_KEY=892bf6949af4233e62f854cb3618bc1a3ee3341dc71ada08c4d5deca239acf4f +export HYP_BASE_CHAINS_SEALEVELTEST1_SIGNER_TYPE="hexKey" +export HYP_BASE_CHAINS_SEALEVELTEST2_SIGNER_KEY=892bf6949af4233e62f854cb3618bc1a3ee3341dc71ada08c4d5deca239acf4f +export HYP_BASE_CHAINS_SEALEVELTEST2_SIGNER_TYPE="hexKey" + +export HYP_BASE_TRACING_LEVEL="debug" diff --git a/rust/config/sealevel/sealevel.json b/rust/config/sealevel/sealevel.json new file mode 100644 index 0000000000..f3629849d5 --- /dev/null +++ b/rust/config/sealevel/sealevel.json @@ -0,0 +1,49 @@ +{ + "environment": "sealevel", + "chains": { + "sealeveltest1": { + "name": "SealevelTest1", + "domain": "13375", + "addresses": { + "mailbox": "692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1", + "interchainGasPaymaster": "FixmeFixmeFixmeFixmeFixmeFixmeFixmeFixmeFixm", + "validatorAnnounce": "DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn" + }, + "signer": null, + "protocol": "sealevel", + "finalityBlocks": "0", + "connection": { + "type": "http", + "url": "http://localhost:8899" + }, + "index": { + "from": "1", + "mode": "sequence" + } + }, + "sealeveltest2": { + "name": "SealevelTest2", + "domain": "13376", + "addresses": { + "mailbox": "9tCUWNjpqcf3NUSrtp7vquYVCwbEByvLjZUrhG5dgvhj", + "interchainGasPaymaster": "FixmeFixmeFixmeFixmeFixmeFixmeFixmeFixmeFixm", + "validatorAnnounce": "3Uo5j2Bti9aZtrDqJmAyuwiFaJFPFoNL5yxTpVCNcUhb" + }, + "signer": null, + "protocol": "sealevel", + "finalityBlocks": "0", + "connection": { + "type": "http", + "url": "http://localhost:8899" + }, + "index": { + "from": "1", + "mode": "sequence" + } + } + }, + "tracing": { + "level": "info", + "fmt": "pretty" + } +} diff --git a/rust/config/sealevel/test-keys/test_deployer-account.json b/rust/config/sealevel/test-keys/test_deployer-account.json new file mode 100644 index 0000000000..1541f52385 --- /dev/null +++ b/rust/config/sealevel/test-keys/test_deployer-account.json @@ -0,0 +1,13 @@ +{ + "pubkey": "E9VrvAdGRvCguN2XgXsgu9PNmMM3vZsU8LSUrM68j8ty", + "account": { + "lamports": 500000000000000000, + "data": [ + "", + "base64" + ], + "owner": "11111111111111111111111111111111", + "executable": false, + "rentEpoch": 0 + } +} \ No newline at end of file diff --git a/rust/config/sealevel/test-keys/test_deployer-keypair.json b/rust/config/sealevel/test-keys/test_deployer-keypair.json new file mode 100644 index 0000000000..36e1ec6786 --- /dev/null +++ b/rust/config/sealevel/test-keys/test_deployer-keypair.json @@ -0,0 +1 @@ +[137,43,246,148,154,244,35,62,98,248,84,203,54,24,188,26,62,227,52,29,199,26,218,8,196,213,222,202,35,154,207,79,195,85,53,151,7,182,83,94,59,5,131,252,40,75,87,11,243,118,71,59,195,222,212,148,179,233,253,121,97,210,114,98] \ No newline at end of file diff --git a/rust/config/sealevel/validator.env b/rust/config/sealevel/validator.env new file mode 100644 index 0000000000..95b038de04 --- /dev/null +++ b/rust/config/sealevel/validator.env @@ -0,0 +1,10 @@ +export BASE_CONFIG="sealevel.json" +export RUN_ENV="sealevel" +export HYP_BASE_DB="/tmp/SEALEVEL_DB/validator" +export HYP_VALIDATOR_ORIGINCHAINNAME="sealeveltest1" +export HYP_VALIDATOR_VALIDATOR_KEY="59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +export HYP_VALIDATOR_VALIDATOR_TYPE="hexKey" +export HYP_VALIDATOR_REORGPERIOD="0" +export HYP_VALIDATOR_INTERVAL="1" +export HYP_VALIDATOR_CHECKPOINTSYNCER_TYPE="localStorage" +export HYP_VALIDATOR_CHECKPOINTSYNCER_PATH="/tmp/test_sealevel_checkpoints_0x70997970c51812dc3a010c7d01b50e0d17dc79c8" diff --git a/rust/config/testnet_config.json b/rust/config/testnet_config.json index 7ff7b984be..7b0d0dd935 100644 --- a/rust/config/testnet_config.json +++ b/rust/config/testnet_config.json @@ -125,6 +125,44 @@ "index": { "from": 1941997 } + }, + "solanadevnet": { + "name": "solanadevnet", + "domain": 1399811151, + "addresses": { + "mailbox": "4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn", + "interchainGasPaymaster": "FixmeFixmeFixmeFixmeFixmeFixmeFixmeFixmeFixm", + "validatorAnnounce": "CMHKvdq4CopDf7qXnDCaTybS15QekQeRt4oUB219yxsp" + }, + "protocol": "sealevel", + "finalityBlocks": 0, + "connection": { + "type": "http", + "url": "https://api.devnet.solana.com" + }, + "index": { + "from": 1, + "mode": "sequence" + } + }, + "zbctestnet": { + "name": "zbctestnet", + "domain": 2053254516, + "addresses": { + "mailbox": "4hW22NXtJ2AXrEVbeAmxjhvxWPSNvfTfAphKXdRBZUco", + "interchainGasPaymaster": "FixmeFixmeFixmeFixmeFixmeFixmeFixmeFixmeFixm", + "validatorAnnounce": "Ar1WiYNhN6F33pj4pcVo5jRMV3V8iJqKiMRSbaDEeqkq" + }, + "protocol": "sealevel", + "finalityBlocks": 0, + "connection": { + "type": "http", + "url": "https://api.zebec.eclipsenetwork.xyz:8899" + }, + "index": { + "from": 1, + "mode": "sequence" + } } } } diff --git a/rust/ethers-prometheus/Cargo.toml b/rust/ethers-prometheus/Cargo.toml index 3e45a53f3e..fadd3c5bf1 100644 --- a/rust/ethers-prometheus/Cargo.toml +++ b/rust/ethers-prometheus/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "ethers-prometheus" documentation.workspace = true @@ -8,20 +10,19 @@ publish.workspace = true version.workspace = true [dependencies] -prometheus = "0.13" -ethers = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-06-01" } -derive_builder = "0.12" -derive-new = "0.5" -async-trait = { version = "0.1", default-features = false } -futures = "0.3" -parking_lot = { version = "0.12" } -maplit = "1.0" -log = "0.4" -tokio = { workspace = true, features = ["time", "sync", "parking_lot"] } +async-trait.workspace = true +derive-new.workspace = true +derive_builder.workspace = true +ethers.workspace = true +futures.workspace = true +log.workspace = true +maplit.workspace = true +parking_lot.workspace = true +prometheus.workspace = true +serde = { workspace = true, features = ["derive"], optional = true } +serde_json = { workspace = true } static_assertions.workspace = true - -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", default-features = false } +tokio = { workspace = true, features = ["time", "sync", "parking_lot"] } # enable feature for this crate that is imported by ethers-rs primitive-types = { version = "*", features = ["fp-conversion"] } diff --git a/rust/hyperlane-base/Cargo.toml b/rust/hyperlane-base/Cargo.toml index 0fb62fcd98..ef639b1d86 100644 --- a/rust/hyperlane-base/Cargo.toml +++ b/rust/hyperlane-base/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "hyperlane-base" documentation.workspace = true @@ -10,15 +12,18 @@ version.workspace = true [dependencies] # Main block async-trait.workspace = true +bs58.workspace = true color-eyre = { workspace = true, optional = true } config.workspace = true derive-new.workspace = true +ed25519-dalek.workspace = true ethers.workspace = true eyre.workspace = true fuels.workspace = true futures-util.workspace = true itertools.workspace = true mockall = "0.11" +once_cell = "1.16" paste.workspace = true prometheus.workspace = true rocksdb.workspace = true @@ -28,7 +33,7 @@ static_assertions.workspace = true tempfile = { version = "3.3", optional = true } thiserror.workspace = true tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] } -tracing-error = "0.2" +tracing-error.workspace = true tracing-futures.workspace = true tracing-subscriber = { workspace = true, features = ["json", "ansi"] } tracing.workspace = true @@ -38,9 +43,10 @@ backtrace = { version = "0.3", optional = true } backtrace-oneline = { path = "../utils/backtrace-oneline", optional = true } ethers-prometheus = { path = "../ethers-prometheus", features = ["serde"] } -hyperlane-core = { path = "../hyperlane-core" } +hyperlane-core = { path = "../hyperlane-core", features = ["agent"] } hyperlane-ethereum = { path = "../chains/hyperlane-ethereum" } hyperlane-fuel = { path = "../chains/hyperlane-fuel" } +hyperlane-sealevel = { path = "../chains/hyperlane-sealevel" } hyperlane-test = { path = "../hyperlane-test" } # these versions are important! @@ -60,12 +66,10 @@ rusoto_kms = "*" rusoto_s3 = "*" rusoto_sts = "*" -lazy_static = "1.4" -once_cell = "1.16" - [dev-dependencies] -color-eyre = "0.6" +color-eyre.workspace = true tempfile = "3.3" +walkdir = { version = "2" } [features] default = ["oneline-errors", "color-eyre"] diff --git a/rust/hyperlane-base/src/contract_sync/cursor.rs b/rust/hyperlane-base/src/contract_sync/cursor.rs index e0f2bb8166..78fdea4af0 100644 --- a/rust/hyperlane-base/src/contract_sync/cursor.rs +++ b/rust/hyperlane-base/src/contract_sync/cursor.rs @@ -12,8 +12,9 @@ use tokio::time::sleep; use tracing::{debug, warn}; use hyperlane_core::{ - ChainResult, ContractSyncCursor, CursorAction, HyperlaneMessage, HyperlaneMessageStore, - HyperlaneWatermarkedLogStore, Indexer, LogMeta, MessageIndexer, + BlockRange, ChainResult, ContractSyncCursor, CursorAction, HyperlaneMessage, + HyperlaneMessageStore, HyperlaneWatermarkedLogStore, IndexMode, IndexRange, Indexer, LogMeta, + MessageIndexer, SequenceRange, }; use crate::contract_sync::eta_calculator::SyncerEtaCalculator; @@ -21,6 +22,8 @@ use crate::contract_sync::eta_calculator::SyncerEtaCalculator; /// Time window for the moving average used in the eta calculator in seconds. const ETA_TIME_WINDOW: f64 = 2. * 60.; +const MAX_SEQUENCE_RANGE: u32 = 100; + /// A struct that holds the data needed for forwards and backwards /// message sync cursors. #[derive(Debug, new)] @@ -57,7 +60,7 @@ impl MessageSyncCursor { &mut self, logs: Vec<(HyperlaneMessage, LogMeta)>, prev_nonce: u32, - ) -> eyre::Result<()> { + ) -> Result<()> { // If we found messages, but did *not* find the message we were looking for, // we need to rewind to the block at which we found the last message. if !logs.is_empty() && !logs.iter().any(|m| m.0.nonce == self.next_nonce) { @@ -79,51 +82,62 @@ impl MessageSyncCursor { /// A MessageSyncCursor that syncs forwards in perpetuity. #[derive(new)] -pub(crate) struct ForwardMessageSyncCursor(MessageSyncCursor); +pub(crate) struct ForwardMessageSyncCursor { + cursor: MessageSyncCursor, + mode: IndexMode, +} impl ForwardMessageSyncCursor { - async fn get_next_range(&mut self) -> ChainResult> { + async fn get_next_range(&mut self) -> ChainResult> { // Check if any new messages have been inserted into the DB, // and update the cursor accordingly. while self - .0 - .retrieve_message_by_nonce(self.0.next_nonce) + .cursor + .retrieve_message_by_nonce(self.cursor.next_nonce) .await .is_some() { if let Some(block_number) = self - .0 - .retrieve_dispatched_block_number(self.0.next_nonce) + .cursor + .retrieve_dispatched_block_number(self.cursor.next_nonce) .await { debug!(next_block = block_number, "Fast forwarding next block"); // It's possible that eth_getLogs dropped logs from this block, therefore we cannot do block_number + 1. - self.0.next_block = block_number; + self.cursor.next_block = block_number; } debug!( - next_nonce = self.0.next_nonce + 1, + next_nonce = self.cursor.next_nonce + 1, "Fast forwarding next nonce" ); - self.0.next_nonce += 1; + self.cursor.next_nonce += 1; } - let (mailbox_count, tip) = self.0.indexer.fetch_count_at_tip().await?; - let cursor_count = self.0.next_nonce; + let (mailbox_count, tip) = self.cursor.indexer.fetch_count_at_tip().await?; + let cursor_count = self.cursor.next_nonce; let cmp = cursor_count.cmp(&mailbox_count); match cmp { Ordering::Equal => { // We are synced up to the latest nonce so we don't need to index anything. // We update our next block number accordingly. - self.0.next_block = tip; + self.cursor.next_block = tip; Ok(None) } Ordering::Less => { // The cursor is behind the mailbox, so we need to index some blocks. // We attempt to index a range of blocks that is as large as possible. - let from = self.0.next_block; - let to = u32::min(tip, from + self.0.chunk_size); - self.0.next_block = to + 1; - Ok(Some((from, to))) + let from = self.cursor.next_block; + let to = u32::min(tip, from + self.cursor.chunk_size); + self.cursor.next_block = to + 1; + + let range = match self.mode { + IndexMode::Block => BlockRange(from..=to), + IndexMode::Sequence => SequenceRange( + cursor_count..=u32::min(mailbox_count, cursor_count + MAX_SEQUENCE_RANGE), + ), + }; + + Ok(Some(range)) } Ordering::Greater => { // Providers may be internally inconsistent, e.g. RPC request A could hit a node @@ -149,21 +163,21 @@ impl ContractSyncCursor for ForwardMessageSyncCursor { } fn latest_block(&self) -> u32 { - self.0.next_block.saturating_sub(1) + self.cursor.next_block.saturating_sub(1) } /// If the previous block has been synced, rewind to the block number /// at which it was dispatched. /// Otherwise, rewind all the way back to the start block. - async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> eyre::Result<()> { - let prev_nonce = self.0.next_nonce.saturating_sub(1); + async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> Result<()> { + let prev_nonce = self.cursor.next_nonce.saturating_sub(1); // We may wind up having re-indexed messages that are previous to the nonce that we are looking for. // We should not consider these messages when checking for continuity errors. let filtered_logs = logs .into_iter() - .filter(|m| m.0.nonce >= self.0.next_nonce) + .filter(|m| m.0.nonce >= self.cursor.next_nonce) .collect(); - self.0.update(filtered_logs, prev_nonce).await + self.cursor.update(filtered_logs, prev_nonce).await } } @@ -172,10 +186,11 @@ impl ContractSyncCursor for ForwardMessageSyncCursor { pub(crate) struct BackwardMessageSyncCursor { cursor: MessageSyncCursor, synced: bool, + mode: IndexMode, } impl BackwardMessageSyncCursor { - async fn get_next_range(&mut self) -> Option<(u32, u32)> { + async fn get_next_range(&mut self) -> Option { // Check if any new messages have been inserted into the DB, // and update the cursor accordingly. while !self.synced { @@ -212,14 +227,23 @@ impl BackwardMessageSyncCursor { let to = self.cursor.next_block; let from = to.saturating_sub(self.cursor.chunk_size); self.cursor.next_block = from.saturating_sub(1); - // TODO: Consider returning a proper ETA for the backwards pass - Some((from, to)) + + let next_nonce = self.cursor.next_nonce; + + let range = match self.mode { + IndexMode::Block => BlockRange(from..=to), + IndexMode::Sequence => { + SequenceRange(next_nonce.saturating_sub(MAX_SEQUENCE_RANGE)..=next_nonce) + } + }; + + Some(range) } /// If the previous block has been synced, rewind to the block number /// at which it was dispatched. /// Otherwise, rewind all the way back to the start block. - async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> eyre::Result<()> { + async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> Result<()> { let prev_nonce = self.cursor.next_nonce.saturating_add(1); // We may wind up having re-indexed messages that are previous to the nonce that we are looking for. // We should not consider these messages when checking for continuity errors. @@ -249,16 +273,13 @@ impl ForwardBackwardMessageSyncCursor { indexer: Arc, db: Arc, chunk_size: u32, + mode: IndexMode, ) -> Result { let (count, tip) = indexer.fetch_count_at_tip().await?; - let forward_cursor = ForwardMessageSyncCursor::new(MessageSyncCursor::new( - indexer.clone(), - db.clone(), - chunk_size, - tip, - tip, - count, - )); + let forward_cursor = ForwardMessageSyncCursor::new( + MessageSyncCursor::new(indexer.clone(), db.clone(), chunk_size, tip, tip, count), + mode, + ); let backward_cursor = BackwardMessageSyncCursor::new( MessageSyncCursor::new( @@ -270,6 +291,7 @@ impl ForwardBackwardMessageSyncCursor { count.saturating_sub(1), ), count == 0, + mode, ); Ok(Self { forward: forward_cursor, @@ -299,10 +321,10 @@ impl ContractSyncCursor for ForwardBackwardMessageSyncCursor { } fn latest_block(&self) -> u32 { - self.forward.0.next_block.saturating_sub(1) + self.forward.cursor.next_block.saturating_sub(1) } - async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> eyre::Result<()> { + async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> Result<()> { match self.direction { SyncDirection::Forward => self.forward.update(logs).await, SyncDirection::Backward => self.backward.update(logs).await, @@ -392,19 +414,24 @@ where }; let rate_limit = self.get_rate_limit().await?; - if let Some(rate_limit) = rate_limit { - return Ok((CursorAction::Sleep(rate_limit), eta)); + let action = if let Some(rate_limit) = rate_limit { + CursorAction::Sleep(rate_limit) } else { self.from = to + 1; - return Ok((CursorAction::Query((from, to)), eta)); - } + // TODO: note at the moment IndexModes are not considered here, and + // block-based indexing is always used. + // This should be changed when Sealevel IGP indexing is implemented, + // along with a refactor to better accommodate indexing modes. + CursorAction::Query(BlockRange(from..=to)) + }; + Ok((action, eta)) } fn latest_block(&self) -> u32 { self.from.saturating_sub(1) } - async fn update(&mut self, _: Vec<(T, LogMeta)>) -> eyre::Result<()> { + async fn update(&mut self, _: Vec<(T, LogMeta)>) -> Result<()> { // Store a relatively conservative view of the high watermark, which should allow a single watermark to be // safely shared across multiple cursors, so long as they are running sufficiently in sync self.db diff --git a/rust/hyperlane-base/src/contract_sync/mod.rs b/rust/hyperlane-base/src/contract_sync/mod.rs index 34d11ec3c6..46c169fe18 100644 --- a/rust/hyperlane-base/src/contract_sync/mod.rs +++ b/rust/hyperlane-base/src/contract_sync/mod.rs @@ -62,17 +62,16 @@ where indexed_height.set(cursor.latest_block() as i64); let Ok((action, eta)) = cursor.next_action().await else { continue }; match action { - CursorAction::Query((from, to)) => { - debug!(from, to, "Looking for for events in block range"); + CursorAction::Query(range) => { + debug!(?range, "Looking for for events in index range"); - let logs = self.indexer.fetch_logs(from, to).await?; + let logs = self.indexer.fetch_logs(range.clone()).await?; info!( - from, - to, + ?range, num_logs = logs.len(), estimated_time_to_sync = fmt_sync_time(eta), - "Found log(s) in block range" + "Found log(s) in index range" ); // Store deliveries let stored = self.db.store_logs(&logs).await?; @@ -105,6 +104,7 @@ where let index_settings = IndexSettings { from: watermark.unwrap_or(index_settings.from), chunk_size: index_settings.chunk_size, + mode: index_settings.mode, }; Box::new( RateLimitedContractSyncCursor::new( @@ -137,19 +137,23 @@ impl MessageContractSync { index_settings.from, next_nonce, ); - Box::new(ForwardMessageSyncCursor::new(forward_data)) + Box::new(ForwardMessageSyncCursor::new( + forward_data, + index_settings.mode, + )) } /// Returns a new cursor to be used for syncing dispatched messages from the indexer pub async fn forward_backward_message_sync_cursor( &self, - chunk_size: u32, + index_settings: IndexSettings, ) -> Box> { Box::new( ForwardBackwardMessageSyncCursor::new( self.indexer.clone(), self.db.clone(), - chunk_size, + index_settings.chunk_size, + index_settings.mode, ) .await .unwrap(), diff --git a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs index 6ba50861b3..0f4f1a8aa5 100644 --- a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs +++ b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use eyre::Result; use paste::paste; use tokio::time::sleep; -use tracing::{debug, trace}; +use tracing::{debug, instrument, trace}; use hyperlane_core::{ HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneMessageStore, @@ -215,6 +215,7 @@ impl HyperlaneRocksDB { #[async_trait] impl HyperlaneLogStore for HyperlaneRocksDB { /// Store a list of dispatched messages and their associated metadata. + #[instrument(skip_all)] async fn store_logs(&self, messages: &[(HyperlaneMessage, LogMeta)]) -> Result { let mut stored = 0; for (message, meta) in messages { @@ -233,6 +234,7 @@ impl HyperlaneLogStore for HyperlaneRocksDB { #[async_trait] impl HyperlaneLogStore for HyperlaneRocksDB { /// Store a list of interchain gas payments and their associated metadata. + #[instrument(skip_all)] async fn store_logs(&self, payments: &[(InterchainGasPayment, LogMeta)]) -> Result { let mut new = 0; for (payment, meta) in payments { diff --git a/rust/hyperlane-base/src/settings/chains.rs b/rust/hyperlane-base/src/settings/chains.rs index c476a9583a..4bd7fa5723 100644 --- a/rust/hyperlane-base/src/settings/chains.rs +++ b/rust/hyperlane-base/src/settings/chains.rs @@ -8,16 +8,17 @@ use ethers_prometheus::middleware::{ ChainInfo, ContractInfo, PrometheusMiddlewareConf, WalletInfo, }; use hyperlane_core::{ - config::*, AggregationIsm, CcipReadIsm, ContractLocator, HyperlaneAbi, HyperlaneDomain, - HyperlaneDomainProtocol, HyperlaneProvider, HyperlaneSigner, Indexer, InterchainGasPaymaster, - InterchainGasPayment, InterchainSecurityModule, Mailbox, MessageIndexer, MultisigIsm, - RoutingIsm, ValidatorAnnounce, H160, H256, + config::*, utils::hex_or_base58_to_h256, AggregationIsm, CcipReadIsm, ContractLocator, + HyperlaneAbi, HyperlaneDomain, HyperlaneDomainProtocol, HyperlaneProvider, HyperlaneSigner, + IndexMode, Indexer, InterchainGasPaymaster, InterchainGasPayment, InterchainSecurityModule, + Mailbox, MessageIndexer, MultisigIsm, RoutingIsm, ValidatorAnnounce, H256, }; use hyperlane_ethereum::{ self as h_eth, BuildableWithProvider, EthereumInterchainGasPaymasterAbi, EthereumMailboxAbi, EthereumValidatorAnnounceAbi, }; use hyperlane_fuel as h_fuel; +use hyperlane_sealevel as h_sealevel; use crate::{ settings::signers::{BuildableWithSignerConf, RawSignerConf}, @@ -31,6 +32,8 @@ pub enum ChainConnectionConf { Ethereum(h_eth::ConnectionConf), /// Fuel configuration Fuel(h_fuel::ConnectionConf), + /// Sealevel configuration. + Sealevel(h_sealevel::ConnectionConf), } /// Specify the chain name (enum variant) under the `chain` key @@ -39,6 +42,7 @@ pub enum ChainConnectionConf { enum RawChainConnectionConf { Ethereum(h_eth::RawConnectionConf), Fuel(h_fuel::RawConnectionConf), + Sealevel(h_sealevel::RawConnectionConf), #[serde(other)] Unknown, } @@ -53,6 +57,7 @@ impl FromRawConf<'_, RawChainConnectionConf> for ChainConnectionConf { match raw { Ethereum(r) => Ok(Self::Ethereum(r.parse_config(&cwp.join("connection"))?)), Fuel(r) => Ok(Self::Fuel(r.parse_config(&cwp.join("connection"))?)), + Sealevel(r) => Ok(Self::Sealevel(r.parse_config(&cwp.join("connection"))?)), Unknown => { Err(eyre!("Unknown chain protocol")).into_config_result(|| cwp.join("protocol")) } @@ -65,6 +70,7 @@ impl ChainConnectionConf { match self { Self::Ethereum(_) => HyperlaneDomainProtocol::Ethereum, Self::Fuel(_) => HyperlaneDomainProtocol::Fuel, + Self::Sealevel(_) => HyperlaneDomainProtocol::Sealevel, } } } @@ -107,13 +113,7 @@ impl FromRawConf<'_, RawCoreContractAddresses> for CoreContractAddresses { ) }) .take_err(&mut err, path) - .and_then(|v| { - if v.len() <= 42 { - v.parse::().take_err(&mut err, path).map(Into::into) - } else { - v.parse().take_err(&mut err, path) - } - }) + .and_then(|v| hex_or_base58_to_h256(&v).take_err(&mut err, path)) }}; } @@ -137,6 +137,8 @@ pub struct IndexSettings { pub from: u32, /// The number of blocks to query at once when indexing contracts. pub chunk_size: u32, + /// The indexing mode. + pub mode: IndexMode, } #[derive(Debug, Deserialize)] @@ -144,6 +146,7 @@ pub struct IndexSettings { struct RawIndexSettings { from: Option, chunk: Option, + mode: Option, } impl FromRawConf<'_, RawIndexSettings> for IndexSettings { @@ -165,7 +168,11 @@ impl FromRawConf<'_, RawIndexSettings> for IndexSettings { .unwrap_or(1999); err.into_result()?; - Ok(Self { from, chunk_size }) + Ok(Self { + from, + chunk_size, + mode: raw.mode.unwrap_or_default(), + }) } } @@ -312,8 +319,8 @@ impl ChainConf { self.build_ethereum(conf, &locator, metrics, h_eth::HyperlaneProviderBuilder {}) .await } - ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(_) => todo!(), } .context(ctx) } @@ -335,6 +342,12 @@ impl ChainConf { .map(|m| Box::new(m) as Box) .map_err(Into::into) } + ChainConnectionConf::Sealevel(conf) => { + let keypair = self.sealevel_signer().await.context(ctx)?; + h_sealevel::SealevelMailbox::new(conf, locator, keypair) + .map(|m| Box::new(m) as Box) + .map_err(Into::into) + } } .context(ctx) } @@ -361,6 +374,10 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(conf) => { + let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new(conf, locator)?); + Ok(indexer as Box) + } } .context(ctx) } @@ -387,6 +404,10 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(conf) => { + let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new(conf, locator)?); + Ok(indexer as Box>) + } } .context(ctx) } @@ -412,6 +433,12 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(conf) => { + let paymaster = Box::new(h_sealevel::SealevelInterchainGasPaymaster::new( + conf, locator, + )); + Ok(paymaster as Box) + } } .context(ctx) } @@ -439,6 +466,12 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(conf) => { + let indexer = Box::new(h_sealevel::SealevelInterchainGasPaymasterIndexer::new( + conf, locator, + )); + Ok(indexer as Box>) + } } .context(ctx) } @@ -456,6 +489,10 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(conf) => { + let va = Box::new(h_sealevel::SealevelValidatorAnnounce::new(conf, locator)); + Ok(va as Box) + } } .context("Building ValidatorAnnounce") } @@ -482,6 +519,13 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(conf) => { + let keypair = self.sealevel_signer().await.context(ctx)?; + let ism = Box::new(h_sealevel::SealevelInterchainSecurityModule::new( + conf, locator, keypair, + )); + Ok(ism as Box) + } } .context(ctx) } @@ -502,6 +546,11 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(conf) => { + let keypair = self.sealevel_signer().await.context(ctx)?; + let ism = Box::new(h_sealevel::SealevelMultisigIsm::new(conf, locator, keypair)); + Ok(ism as Box) + } } .context(ctx) } @@ -525,6 +574,9 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(_) => { + Err(eyre!("Sealevel does not support routing ISM yet")).context(ctx) + } } .context(ctx) } @@ -548,6 +600,9 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(_) => { + Err(eyre!("Sealevel does not support aggregation ISM yet")).context(ctx) + } } .context(ctx) } @@ -571,6 +626,9 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), + ChainConnectionConf::Sealevel(_) => { + Err(eyre!("Sealevel does not support CCIP read ISM yet")).context(ctx) + } } .context(ctx) } @@ -593,6 +651,10 @@ impl ChainConf { }) } + async fn sealevel_signer(&self) -> Result> { + self.signer().await + } + /// Get a clone of the ethereum metrics conf with correctly configured /// contract information. fn metrics_conf( @@ -610,7 +672,7 @@ impl ChainConf { if let Some(signer) = signer { cfg.wallets - .entry(signer.eth_address()) + .entry(signer.eth_address().into()) .or_insert_with(|| WalletInfo { name: Some(agent_name.into()), }); diff --git a/rust/hyperlane-base/src/settings/signers.rs b/rust/hyperlane-base/src/settings/signers.rs index 4fe16aeb35..9daeaa3759 100644 --- a/rust/hyperlane-base/src/settings/signers.rs +++ b/rust/hyperlane-base/src/settings/signers.rs @@ -8,6 +8,9 @@ use rusoto_kms::KmsClient; use serde::Deserialize; use tracing::instrument; +use ed25519_dalek::SecretKey; +use hyperlane_sealevel::Keypair; + use super::aws_credentials::AwsChainCredentialsProvider; use hyperlane_core::{config::*, H256}; @@ -139,3 +142,19 @@ impl BuildableWithSignerConf for fuels::prelude::WalletUnlocked { }) } } + +#[async_trait] +impl BuildableWithSignerConf for Keypair { + async fn build(conf: &SignerConf) -> Result { + Ok(match conf { + SignerConf::HexKey { key } => { + let secret = SecretKey::from_bytes(key.as_bytes()) + .context("Invalid sealevel ed25519 secret key")?; + Keypair::from_bytes(&ed25519_dalek::Keypair::from(secret).to_bytes()) + .context("Unable to create Keypair")? + } + SignerConf::Aws { .. } => bail!("Aws signer is not supported by fuel"), + SignerConf::Node => bail!("Node signer is not supported by fuel"), + }) + } +} diff --git a/rust/hyperlane-base/src/types/checkpoint_syncer.rs b/rust/hyperlane-base/src/types/checkpoint_syncer.rs index 3a40665ea0..fdfd9190aa 100644 --- a/rust/hyperlane-base/src/types/checkpoint_syncer.rs +++ b/rust/hyperlane-base/src/types/checkpoint_syncer.rs @@ -2,13 +2,12 @@ use core::str::FromStr; use std::collections::HashMap; use std::path::PathBuf; -use ethers::types::Address; use eyre::{eyre, Context, Report, Result}; use prometheus::{IntGauge, IntGaugeVec}; use rusoto_core::Region; use serde::Deserialize; -use hyperlane_core::config::*; +use hyperlane_core::{config::*, H160}; use crate::{CheckpointSyncer, LocalStorage, MultisigCheckpointSyncer, S3Storage}; @@ -165,7 +164,7 @@ impl MultisigCheckpointSyncerConf { let gauge = validator_checkpoint_index.with_label_values(&[origin, &key.to_lowercase()]); if let Ok(conf) = value.build(Some(gauge)) { - checkpoint_syncers.insert(Address::from_str(key)?, conf.into()); + checkpoint_syncers.insert(H160::from_str(key)?, conf.into()); } else { continue; } diff --git a/rust/hyperlane-base/src/types/multisig.rs b/rust/hyperlane-base/src/types/multisig.rs index ac31f0eb5a..820c13febb 100644 --- a/rust/hyperlane-base/src/types/multisig.rs +++ b/rust/hyperlane-base/src/types/multisig.rs @@ -2,7 +2,6 @@ use std::collections::{hash_map::Entry, HashMap}; use std::sync::Arc; use derive_new::new; -use ethers::prelude::Address; use eyre::Result; use tracing::{debug, instrument, trace}; @@ -18,7 +17,7 @@ use crate::CheckpointSyncer; #[derive(Clone, Debug, new)] pub struct MultisigCheckpointSyncer { /// The checkpoint syncer for each valid validator signer address - checkpoint_syncers: HashMap>, + checkpoint_syncers: HashMap>, } impl MultisigCheckpointSyncer { diff --git a/rust/hyperlane-core/tests/chain_config.rs b/rust/hyperlane-base/tests/chain_config.rs similarity index 88% rename from rust/hyperlane-core/tests/chain_config.rs rename to rust/hyperlane-base/tests/chain_config.rs index 1c64d60a0b..3a02542680 100644 --- a/rust/hyperlane-core/tests/chain_config.rs +++ b/rust/hyperlane-base/tests/chain_config.rs @@ -7,8 +7,7 @@ use eyre::Context; use walkdir::WalkDir; use hyperlane_base::{RawSettings, Settings}; -use hyperlane_core::config::*; -use hyperlane_core::KnownHyperlaneDomain; +use hyperlane_core::{config::*, KnownHyperlaneDomain}; /// Relative path to the `hyperlane-monorepo/rust/config/` /// directory, which is where the agent's config files @@ -60,7 +59,11 @@ fn config_paths(root: &Path) -> Vec { /// of a test env. This test simply tries to do some sanity checks /// against the integrity of that data. fn hyperlane_settings() -> Vec { - let root = Path::new(AGENT_CONFIG_PATH_ROOT); + // Determine the config path based on the crate root so that + // the debugger can also find the config file. + let crate_root = env!("CARGO_MANIFEST_DIR"); + let config_path = format!("{}/{}", crate_root, AGENT_CONFIG_PATH_ROOT); + let root = Path::new(config_path.as_str()); let paths = config_paths(root); let files: Vec = paths .iter() @@ -69,18 +72,19 @@ fn hyperlane_settings() -> Vec { paths .iter() .zip(files.iter()) - .map(|(p, f)| { + // Filter out config files that can't be parsed as json (e.g. env files) + .filter_map(|(p, f)| { let raw: RawSettings = Config::builder() .add_source(config::File::from_str(f.as_str(), FileFormat::Json)) .build() - .unwrap() + .ok()? .try_deserialize::() .unwrap_or_else(|e| { panic!("!cfg({}): {:?}: {}", p, e, f); }); Settings::from_config(raw, &ConfigPath::default()) .context("Config parsing error, please check the config reference (https://docs.hyperlane.xyz/docs/operators/agent-configuration/configuration-reference)") - .unwrap() + .ok() }) .collect() } diff --git a/rust/hyperlane-core/Cargo.toml b/rust/hyperlane-core/Cargo.toml index 454feec8cb..7827dfda61 100644 --- a/rust/hyperlane-core/Cargo.toml +++ b/rust/hyperlane-core/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "hyperlane-core" documentation.workspace = true @@ -7,19 +9,18 @@ license-file.workspace = true publish.workspace = true version.workspace = true -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] async-trait.workspace = true auto_impl = "1.0" +borsh.workspace = true +bs58.workspace = true bytes = { version = "1", features = ["serde"] } convert_case = "0.6" derive-new.workspace = true -ethers-providers.workspace = true -ethers-core.workspace = true -ethers-contract.workspace = true +derive_more.workspace = true eyre.workspace = true -hex = "0.4.3" +getrandom.workspace = true +hex.workspace = true itertools.workspace = true num = { workspace = true, features = ["serde"] } num-derive.workspace = true @@ -27,20 +28,25 @@ num-traits.workspace = true serde.workspace = true serde_json.workspace = true sha3 = "0.10" -strum.workspace = true thiserror.workspace = true +uint = "0.9.5" +fixed-hash = "0.8.0" +tiny-keccak = { version = "2.0.2", features = ["keccak"]} -# version determined by ethers-rs -primitive-types = "*" -lazy_static = "*" -derive_more.workspace = true +config = { workspace = true, optional = true } +ethers = { workspace = true, optional = true } +ethers-core = { workspace = true, optional = true } +ethers-contract = { workspace = true, optional = true } +ethers-providers = { workspace = true, optional = true } +strum = { workspace = true, optional = true } +primitive-types = { workspace = true, optional = true } [dev-dependencies] -config.workspace = true -hyperlane-base = { path = "../hyperlane-base" } tokio = { workspace = true, features = ["rt", "time"] } -walkdir = { version = "2" } [features] default = [] -test-utils = [] +test-utils = ["dep:config"] +agent = ["ethers", "strum"] +strum = ["dep:strum"] +ethers = ["dep:ethers-core", "dep:ethers-contract", "dep:ethers-providers", "dep:primitive-types", "dep:ethers"] \ No newline at end of file diff --git a/rust/hyperlane-core/src/accumulator/incremental.rs b/rust/hyperlane-core/src/accumulator/incremental.rs index 439a8e67e2..9e01c79bbc 100644 --- a/rust/hyperlane-core/src/accumulator/incremental.rs +++ b/rust/hyperlane-core/src/accumulator/incremental.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use derive_new::new; use crate::accumulator::{ @@ -6,7 +7,7 @@ use crate::accumulator::{ H256, TREE_DEPTH, ZERO_HASHES, }; -#[derive(Debug, Clone, Copy, new)] +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone, Copy, new, PartialEq, Eq)] /// An incremental merkle tree, modeled on the eth2 deposit contract pub struct IncrementalMerkle { branch: [H256; TREE_DEPTH], @@ -104,7 +105,7 @@ mod test { // insert the leaves for leaf in test_case.leaves.iter() { let hashed_leaf = hash_message(leaf); - tree.ingest(hashed_leaf); + tree.ingest(hashed_leaf.into()); } // assert the tree has the proper leaf count diff --git a/rust/hyperlane-core/src/accumulator/merkle.rs b/rust/hyperlane-core/src/accumulator/merkle.rs index ff8a772a74..a9df396823 100644 --- a/rust/hyperlane-core/src/accumulator/merkle.rs +++ b/rust/hyperlane-core/src/accumulator/merkle.rs @@ -1,4 +1,3 @@ -use lazy_static::lazy_static; use thiserror::Error; use crate::{ @@ -15,12 +14,46 @@ use crate::{ // - remove ring dependency // In accordance with its license terms, the apache2 license is reproduced below -lazy_static! { - /// Zero nodes to act as "synthetic" left and right subtrees of other zero nodes. - pub static ref ZERO_NODES: Vec = { - (0..=TREE_DEPTH).map(MerkleTree::Zero).collect() - }; -} +// Can't initialize this using `lazy_static` because of a constaint in Solana: static variables cannot be writable. +// See the following links for more info: +// https://stackoverflow.com/questions/70630344/failed-to-deploy-my-solana-smart-contract +// https://docs.solana.com/developing/on-chain-programs/limitations#static-writable-data +/// Zero nodes to act as "synthetic" left and right subtrees of other zero nodes. +pub const ZERO_NODES: [MerkleTree; TREE_DEPTH + 1] = [ + MerkleTree::Zero(0), + MerkleTree::Zero(1), + MerkleTree::Zero(2), + MerkleTree::Zero(3), + MerkleTree::Zero(4), + MerkleTree::Zero(5), + MerkleTree::Zero(6), + MerkleTree::Zero(7), + MerkleTree::Zero(8), + MerkleTree::Zero(9), + MerkleTree::Zero(10), + MerkleTree::Zero(11), + MerkleTree::Zero(12), + MerkleTree::Zero(13), + MerkleTree::Zero(14), + MerkleTree::Zero(15), + MerkleTree::Zero(16), + MerkleTree::Zero(17), + MerkleTree::Zero(18), + MerkleTree::Zero(19), + MerkleTree::Zero(20), + MerkleTree::Zero(21), + MerkleTree::Zero(22), + MerkleTree::Zero(23), + MerkleTree::Zero(24), + MerkleTree::Zero(25), + MerkleTree::Zero(26), + MerkleTree::Zero(27), + MerkleTree::Zero(28), + MerkleTree::Zero(29), + MerkleTree::Zero(30), + MerkleTree::Zero(31), + MerkleTree::Zero(32), +]; /// Right-sparse Merkle tree. /// @@ -491,6 +524,12 @@ mod tests { assert_eq!(second.hash(), incr.root()); assert_eq!(full.hash(), incr.root()); } + + #[test] + fn it_sets_zero_nodes_correctly() { + let expected_zero_nodes: Vec<_> = (0..=TREE_DEPTH).map(MerkleTree::Zero).collect(); + assert_eq!(expected_zero_nodes.as_slice(), ZERO_NODES.as_slice()); + } } /* diff --git a/rust/hyperlane-core/src/accumulator/mod.rs b/rust/hyperlane-core/src/accumulator/mod.rs index ef418e66bb..10c6034beb 100644 --- a/rust/hyperlane-core/src/accumulator/mod.rs +++ b/rust/hyperlane-core/src/accumulator/mod.rs @@ -1,4 +1,3 @@ -use lazy_static::lazy_static; use sha3::{digest::Update, Digest, Keccak256}; use crate::H256; @@ -11,8 +10,9 @@ pub mod merkle; /// Utilities for manipulating proofs to reflect sparse merkle trees. pub mod sparse; -/// Tree depth -pub const TREE_DEPTH: usize = 32; +mod zero_hashes; +pub use zero_hashes::{TREE_DEPTH, ZERO_HASHES}; + const EMPTY_SLICE: &[H256] = &[]; pub(super) fn hash_concat(left: impl AsRef<[u8]>, right: impl AsRef<[u8]>) -> H256 { @@ -25,31 +25,45 @@ pub(super) fn hash_concat(left: impl AsRef<[u8]>, right: impl AsRef<[u8]>) -> H2 ) } -lazy_static! { - /// A cache of the zero hashes for each layer of the tree. - pub static ref ZERO_HASHES: [H256; TREE_DEPTH + 1] = { +/// The root of an empty tree +pub const INITIAL_ROOT: H256 = H256([ + 39, 174, 91, 160, 141, 114, 145, 201, 108, 140, 189, 220, 193, 72, 191, 72, 166, 214, 140, 121, + 116, 185, 67, 86, 245, 55, 84, 239, 97, 113, 215, 87, +]); + +#[cfg(test)] +mod test { + use super::*; + + fn compute_zero_hashes() -> [H256; TREE_DEPTH + 1] { + // Implementation previously used in the `lazy_static!` macro for `ZERO_HASHES` let mut hashes = [H256::zero(); TREE_DEPTH + 1]; for i in 0..TREE_DEPTH { hashes[i + 1] = hash_concat(hashes[i], hashes[i]); } hashes - }; - - /// The root of an empty tree - pub static ref INITIAL_ROOT: H256 = incremental::IncrementalMerkle::default().root(); -} - -#[cfg(test)] -mod test { - use super::*; + } #[test] fn it_calculates_the_initial_root() { assert_eq!( - *INITIAL_ROOT, + INITIAL_ROOT, "0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757" .parse() .unwrap() ); } + + #[test] + fn it_prints_zero_hashes_items() { + assert_eq!(zero_hashes::ZERO_HASHES, compute_zero_hashes()); + } + + #[test] + fn it_computes_initial_root() { + assert_eq!( + incremental::IncrementalMerkle::default().root(), + INITIAL_ROOT + ); + } } diff --git a/rust/hyperlane-core/src/accumulator/sparse.rs b/rust/hyperlane-core/src/accumulator/sparse.rs index 1543de32b0..1cff678ac7 100644 --- a/rust/hyperlane-core/src/accumulator/sparse.rs +++ b/rust/hyperlane-core/src/accumulator/sparse.rs @@ -140,10 +140,7 @@ mod tests { fn tree_and_roots() -> (MerkleTree, Vec) { const LEAF_COUNT: usize = 47; - let all_leaves: Vec = (0..LEAF_COUNT) - .into_iter() - .map(|_| H256::from([0xAA; 32])) - .collect(); + let all_leaves: Vec = (0..LEAF_COUNT).map(|_| H256::from([0xAA; 32])).collect(); let mut roots = [H256::zero(); LEAF_COUNT]; let mut tree = MerkleTree::create(&[], TREE_DEPTH); for i in 0..LEAF_COUNT { @@ -157,7 +154,7 @@ mod tests { fn as_latest() { let (tree, roots) = tree_and_roots(); - for i in 0..roots.len() { + for (i, root) in roots.iter().enumerate() { let current_proof_i = tree.prove_against_current(i); let latest_proof_i = current_proof_i.as_latest(); assert!(verify_merkle_proof( @@ -165,7 +162,7 @@ mod tests { &latest_proof_i.path, TREE_DEPTH, i, - roots[i] + *root, )); } } @@ -174,7 +171,7 @@ mod tests { fn prove_against_previous() { let (tree, roots) = tree_and_roots(); for i in 0..roots.len() { - for j in i..roots.len() { + for (j, root) in roots.iter().enumerate().skip(i) { let proof = tree.prove_against_previous(i, j); assert_eq!(proof.root(), roots[j]); assert!(verify_merkle_proof( @@ -182,7 +179,7 @@ mod tests { &proof.path, TREE_DEPTH, i, - roots[j] + *root, )); } } diff --git a/rust/hyperlane-core/src/accumulator/zero_hashes.rs b/rust/hyperlane-core/src/accumulator/zero_hashes.rs new file mode 100644 index 0000000000..1304f456e2 --- /dev/null +++ b/rust/hyperlane-core/src/accumulator/zero_hashes.rs @@ -0,0 +1,143 @@ +use crate::H256; + +/// Tree depth +pub const TREE_DEPTH: usize = 32; +// keccak256 zero hashes +const Z_0: H256 = H256([ + 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, 0, 0, 0, +]); +const Z_1: H256 = H256([ + 173, 50, 40, 182, 118, 247, 211, 205, 66, 132, 165, 68, 63, 23, 241, 150, 43, 54, 228, 145, + 179, 10, 64, 178, 64, 88, 73, 229, 151, 186, 95, 181, +]); +const Z_2: H256 = H256([ + 180, 193, 25, 81, 149, 124, 111, 143, 100, 44, 74, 246, 28, 214, 178, 70, 64, 254, 198, 220, + 127, 198, 7, 238, 130, 6, 169, 158, 146, 65, 13, 48, +]); +const Z_3: H256 = H256([ + 33, 221, 185, 163, 86, 129, 92, 63, 172, 16, 38, 182, 222, 197, 223, 49, 36, 175, 186, 219, 72, + 92, 155, 165, 163, 227, 57, 138, 4, 183, 186, 133, +]); +const Z_4: H256 = H256([ + 229, 135, 105, 179, 42, 27, 234, 241, 234, 39, 55, 90, 68, 9, 90, 13, 31, 182, 100, 206, 45, + 211, 88, 231, 252, 191, 183, 140, 38, 161, 147, 68, +]); +const Z_5: H256 = H256([ + 14, 176, 30, 191, 201, 237, 39, 80, 12, 212, 223, 201, 121, 39, 45, 31, 9, 19, 204, 159, 102, + 84, 13, 126, 128, 5, 129, 17, 9, 225, 207, 45, +]); +const Z_6: H256 = H256([ + 136, 124, 34, 189, 135, 80, 211, 64, 22, 172, 60, 102, 181, 255, 16, 45, 172, 221, 115, 246, + 176, 20, 231, 16, 181, 30, 128, 34, 175, 154, 25, 104, +]); +const Z_7: H256 = H256([ + 255, 215, 1, 87, 228, 128, 99, 252, 51, 201, 122, 5, 15, 127, 100, 2, 51, 191, 100, 108, 201, + 141, 149, 36, 198, 185, 43, 207, 58, 181, 111, 131, +]); +const Z_8: H256 = H256([ + 152, 103, 204, 95, 127, 25, 107, 147, 186, 225, 226, 126, 99, 32, 116, 36, 69, 210, 144, 242, + 38, 56, 39, 73, 139, 84, 254, 197, 57, 247, 86, 175, +]); +const Z_9: H256 = H256([ + 206, 250, 212, 229, 8, 192, 152, 185, 167, 225, 216, 254, 177, 153, 85, 251, 2, 186, 150, 117, + 88, 80, 120, 113, 9, 105, 211, 68, 15, 80, 84, 224, +]); +const Z_10: H256 = H256([ + 249, 220, 62, 127, 224, 22, 224, 80, 239, 242, 96, 51, 79, 24, 165, 212, 254, 57, 29, 130, 9, + 35, 25, 245, 150, 79, 46, 46, 183, 193, 195, 165, +]); +const Z_11: H256 = H256([ + 248, 177, 58, 73, 226, 130, 246, 9, 195, 23, 168, 51, 251, 141, 151, 109, 17, 81, 124, 87, 29, + 18, 33, 162, 101, 210, 90, 247, 120, 236, 248, 146, +]); +const Z_12: H256 = H256([ + 52, 144, 198, 206, 235, 69, 10, 236, 220, 130, 226, 130, 147, 3, 29, 16, 199, 215, 59, 248, 94, + 87, 191, 4, 26, 151, 54, 10, 162, 197, 217, 156, +]); +const Z_13: H256 = H256([ + 193, 223, 130, 217, 196, 184, 116, 19, 234, 226, 239, 4, 143, 148, 180, 211, 85, 76, 234, 115, + 217, 43, 15, 122, 249, 110, 2, 113, 198, 145, 226, 187, +]); +const Z_14: H256 = H256([ + 92, 103, 173, 215, 198, 202, 243, 2, 37, 106, 222, 223, 122, 177, 20, 218, 10, 207, 232, 112, + 212, 73, 163, 164, 137, 247, 129, 214, 89, 232, 190, 204, +]); +const Z_15: H256 = H256([ + 218, 123, 206, 159, 78, 134, 24, 182, 189, 47, 65, 50, 206, 121, 140, 220, 122, 96, 231, 225, + 70, 10, 114, 153, 227, 198, 52, 42, 87, 150, 38, 210, +]); +const Z_16: H256 = H256([ + 39, 51, 229, 15, 82, 110, 194, 250, 25, 162, 43, 49, 232, 237, 80, 242, 60, 209, 253, 249, 76, + 145, 84, 237, 58, 118, 9, 162, 241, 255, 152, 31, +]); +const Z_17: H256 = H256([ + 225, 211, 181, 200, 7, 178, 129, 228, 104, 60, 198, 214, 49, 92, 249, 91, 154, 222, 134, 65, + 222, 252, 179, 35, 114, 241, 193, 38, 227, 152, 239, 122, +]); +const Z_18: H256 = H256([ + 90, 45, 206, 10, 138, 127, 104, 187, 116, 86, 15, 143, 113, 131, 124, 44, 46, 187, 203, 247, + 255, 251, 66, 174, 24, 150, 241, 63, 124, 116, 121, 160, +]); +const Z_19: H256 = H256([ + 180, 106, 40, 182, 245, 85, 64, 248, 148, 68, 246, 61, 224, 55, 142, 61, 18, 27, 224, 158, 6, + 204, 157, 237, 28, 32, 230, 88, 118, 211, 106, 160, +]); +const Z_20: H256 = H256([ + 198, 94, 150, 69, 100, 71, 134, 182, 32, 226, 221, 42, 214, 72, 221, 252, 191, 74, 126, 91, 26, + 58, 78, 207, 231, 246, 70, 103, 163, 240, 183, 226, +]); +const Z_21: H256 = H256([ + 244, 65, 133, 136, 237, 53, 162, 69, 140, 255, 235, 57, 185, 61, 38, 241, 141, 42, 177, 59, + 220, 230, 174, 229, 142, 123, 153, 53, 158, 194, 223, 217, +]); +const Z_22: H256 = H256([ + 90, 156, 22, 220, 0, 214, 239, 24, 183, 147, 58, 111, 141, 198, 92, 203, 85, 102, 113, 56, 119, + 111, 125, 234, 16, 16, 112, 220, 135, 150, 227, 119, +]); +const Z_23: H256 = H256([ + 77, 248, 79, 64, 174, 12, 130, 41, 208, 214, 6, 158, 92, 143, 57, 167, 194, 153, 103, 122, 9, + 211, 103, 252, 123, 5, 227, 188, 56, 14, 230, 82, +]); +const Z_24: H256 = H256([ + 205, 199, 37, 149, 247, 76, 123, 16, 67, 208, 225, 255, 186, 183, 52, 100, 140, 131, 141, 251, + 5, 39, 217, 113, 182, 2, 188, 33, 108, 150, 25, 239, +]); +const Z_25: H256 = H256([ + 10, 191, 90, 201, 116, 161, 237, 87, 244, 5, 10, 165, 16, 221, 156, 116, 245, 8, 39, 123, 57, + 215, 151, 59, 178, 223, 204, 197, 238, 176, 97, 141, +]); +const Z_26: H256 = H256([ + 184, 205, 116, 4, 111, 243, 55, 240, 167, 191, 44, 142, 3, 225, 15, 100, 44, 24, 134, 121, 141, + 113, 128, 106, 177, 232, 136, 217, 229, 238, 135, 208, +]); +const Z_27: H256 = H256([ + 131, 140, 86, 85, 203, 33, 198, 203, 131, 49, 59, 90, 99, 17, 117, 223, 244, 150, 55, 114, 204, + 233, 16, 129, 136, 179, 74, 200, 124, 129, 196, 30, +]); +const Z_28: H256 = H256([ + 102, 46, 228, 221, 45, 215, 178, 188, 112, 121, 97, 177, 230, 70, 196, 4, 118, 105, 220, 182, + 88, 79, 13, 141, 119, 13, 175, 93, 126, 125, 235, 46, +]); +const Z_29: H256 = H256([ + 56, 138, 178, 14, 37, 115, 209, 113, 168, 129, 8, 231, 157, 130, 14, 152, 242, 108, 11, 132, + 170, 139, 47, 74, 164, 150, 141, 187, 129, 142, 163, 34, +]); +const Z_30: H256 = H256([ + 147, 35, 124, 80, 186, 117, 238, 72, 95, 76, 34, 173, 242, 247, 65, 64, 11, 223, 141, 106, 156, + 199, 223, 126, 202, 229, 118, 34, 22, 101, 215, 53, +]); +const Z_31: H256 = H256([ + 132, 72, 129, 139, 180, 174, 69, 98, 132, 158, 148, 158, 23, 172, 22, 224, 190, 22, 104, 142, + 21, 107, 92, 241, 94, 9, 140, 98, 124, 0, 86, 169, +]); +const Z_32: H256 = H256([ + 39, 174, 91, 160, 141, 114, 145, 201, 108, 140, 189, 220, 193, 72, 191, 72, 166, 214, 140, 121, + 116, 185, 67, 86, 245, 55, 84, 239, 97, 113, 215, 87, +]); + +/// Precomputed zero hashes for building the merkle tree +/// A cache of the zero hashes for each layer of the tree. +pub const ZERO_HASHES: [H256; TREE_DEPTH + 1] = [ + Z_0, Z_1, Z_2, Z_3, Z_4, Z_5, Z_6, Z_7, Z_8, Z_9, Z_10, Z_11, Z_12, Z_13, Z_14, Z_15, Z_16, + Z_17, Z_18, Z_19, Z_20, Z_21, Z_22, Z_23, Z_24, Z_25, Z_26, Z_27, Z_28, Z_29, Z_30, Z_31, Z_32, +]; diff --git a/rust/hyperlane-core/src/chain.rs b/rust/hyperlane-core/src/chain.rs index 7c87350dcd..f1f3a1208b 100644 --- a/rust/hyperlane-core/src/chain.rs +++ b/rust/hyperlane-core/src/chain.rs @@ -1,10 +1,11 @@ #![allow(missing_docs)] -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Debug, Formatter}; use std::hash::{Hash, Hasher}; use num_derive::FromPrimitive; use num_traits::FromPrimitive; +#[cfg(feature = "strum")] use strum::{EnumIter, EnumString, IntoStaticStr}; use crate::utils::many_to_one; @@ -21,7 +22,9 @@ pub struct ContractLocator<'a> { pub domain: &'a HyperlaneDomain, pub address: H256, } -impl<'a> Display for ContractLocator<'a> { + +#[cfg(feature = "strum")] +impl<'a> std::fmt::Display for ContractLocator<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -58,20 +61,15 @@ impl From<&'_ Address> for H160 { } /// All domains supported by Hyperlane. -#[derive( - FromPrimitive, - EnumString, - IntoStaticStr, - strum::Display, - EnumIter, - PartialEq, - Eq, - Debug, - Clone, - Copy, - Hash, +#[derive(FromPrimitive, PartialEq, Eq, Debug, Clone, Copy, Hash)] +#[cfg_attr( + feature = "strum", + derive(strum::Display, EnumString, IntoStaticStr, EnumIter) +)] +#[cfg_attr( + feature = "strum", + strum(serialize_all = "lowercase", ascii_case_insensitive) )] -#[strum(serialize_all = "lowercase", ascii_case_insensitive)] pub enum KnownHyperlaneDomain { Ethereum = 1, Goerli = 5, @@ -89,9 +87,9 @@ pub enum KnownHyperlaneDomain { Optimism = 10, OptimismGoerli = 420, - #[strum(serialize = "bsc")] + #[cfg_attr(feature = "strum", strum(serialize = "bsc"))] BinanceSmartChain = 56, - #[strum(serialize = "bsctestnet")] + #[cfg_attr(feature = "strum", strum(serialize = "bsctestnet"))] BinanceSmartChainTestnet = 97, Celo = 42220, @@ -114,6 +112,11 @@ pub enum KnownHyperlaneDomain { /// Fuel1 local chain FuelTest1 = 13374, + + /// Sealevel local chain 1 + SealevelTest1 = 13375, + /// Sealevel local chain 1 + SealevelTest2 = 13376, } #[derive(Clone)] @@ -151,10 +154,15 @@ impl HyperlaneDomain { } /// Types of Hyperlane domains. -#[derive( - FromPrimitive, EnumString, IntoStaticStr, strum::Display, Copy, Clone, Eq, PartialEq, Debug, +#[derive(FromPrimitive, Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr( + feature = "strum", + derive(strum::Display, EnumString, IntoStaticStr, EnumIter) +)] +#[cfg_attr( + feature = "strum", + strum(serialize_all = "lowercase", ascii_case_insensitive) )] -#[strum(serialize_all = "lowercase", ascii_case_insensitive)] pub enum HyperlaneDomainType { /// A mainnet. Mainnet, @@ -167,15 +175,22 @@ pub enum HyperlaneDomainType { } /// A selector for which base library should handle this domain. -#[derive( - FromPrimitive, EnumString, IntoStaticStr, strum::Display, Copy, Clone, Eq, PartialEq, Debug, +#[derive(FromPrimitive, Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr( + feature = "strum", + derive(strum::Display, EnumString, IntoStaticStr, EnumIter) +)] +#[cfg_attr( + feature = "strum", + strum(serialize_all = "lowercase", ascii_case_insensitive) )] -#[strum(serialize_all = "lowercase", ascii_case_insensitive)] pub enum HyperlaneDomainProtocol { /// An EVM-based chain type which uses hyperlane-ethereum. Ethereum, /// A Fuel-based chain type which uses hyperlane-fuel. Fuel, + /// A Sealevel-based chain type which uses hyperlane-sealevel. + Sealevel, } impl HyperlaneDomainProtocol { @@ -184,11 +199,13 @@ impl HyperlaneDomainProtocol { match self { Ethereum => format!("{:?}", H160::from(addr)), Fuel => format!("{:?}", addr), + Sealevel => format!("{:?}", addr), } } } impl KnownHyperlaneDomain { + #[cfg(feature = "strum")] pub fn as_str(self) -> &'static str { self.into() } @@ -206,7 +223,7 @@ impl KnownHyperlaneDomain { Goerli, Mumbai, Fuji, ArbitrumGoerli, OptimismGoerli, BinanceSmartChainTestnet, Alfajores, MoonbaseAlpha, Zksync2Testnet, Sepolia ], - LocalTestChain: [Test1, Test2, Test3, FuelTest1], + LocalTestChain: [Test1, Test2, Test3, FuelTest1, SealevelTest1, SealevelTest2], }) } @@ -220,6 +237,7 @@ impl KnownHyperlaneDomain { Alfajores, Moonbeam, MoonbaseAlpha, Zksync2Testnet, Test1, Test2, Test3 ], HyperlaneDomainProtocol::Fuel: [FuelTest1], + HyperlaneDomainProtocol::Sealevel: [SealevelTest1, SealevelTest2], }) } } @@ -238,6 +256,7 @@ impl Hash for HyperlaneDomain { } } +#[cfg(feature = "strum")] impl AsRef for HyperlaneDomain { fn as_ref(&self) -> &str { self.name() @@ -270,7 +289,8 @@ impl From<&HyperlaneDomain> for HyperlaneDomainProtocol { } } -impl Display for HyperlaneDomain { +#[cfg(feature = "strum")] +impl std::fmt::Display for HyperlaneDomain { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name()) } @@ -278,7 +298,14 @@ impl Display for HyperlaneDomain { impl Debug for HyperlaneDomain { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "HyperlaneDomain({} ({}))", self.name(), self.id()) + #[cfg(feature = "strum")] + { + write!(f, "HyperlaneDomain({} ({}))", self.name(), self.id()) + } + #[cfg(not(feature = "strum"))] + { + write!(f, "HyperlaneDomain({})", self.id()) + } } } @@ -291,6 +318,7 @@ pub enum HyperlaneDomainConfigError { } impl HyperlaneDomain { + #[cfg(feature = "strum")] pub fn from_config( domain_id: u32, name: &str, @@ -319,6 +347,7 @@ impl HyperlaneDomain { } /// The chain name + #[cfg(feature = "strum")] pub fn name(&self) -> &str { match self { HyperlaneDomain::Known(domain) => domain.as_str(), @@ -357,6 +386,7 @@ impl HyperlaneDomain { } #[cfg(test)] +#[cfg(feature = "strum")] mod tests { use crate::KnownHyperlaneDomain; use std::str::FromStr; diff --git a/rust/hyperlane-core/src/config/str_or_int.rs b/rust/hyperlane-core/src/config/str_or_int.rs index 661c1b918e..f888cedfe4 100644 --- a/rust/hyperlane-core/src/config/str_or_int.rs +++ b/rust/hyperlane-core/src/config/str_or_int.rs @@ -1,4 +1,4 @@ -use primitive_types::U256; +use crate::U256; use serde::Deserialize; use std::fmt::{Debug, Formatter}; use std::num::{ParseIntError, TryFromIntError}; diff --git a/rust/hyperlane-core/src/error.rs b/rust/hyperlane-core/src/error.rs index 493a5a23c6..968f158988 100644 --- a/rust/hyperlane-core/src/error.rs +++ b/rust/hyperlane-core/src/error.rs @@ -3,10 +3,6 @@ use std::error::Error as StdError; use std::fmt::{Debug, Display, Formatter}; use std::ops::Deref; -use ethers_contract::ContractError; -use ethers_core::types::SignatureError; -use ethers_providers::{Middleware, ProviderError}; - use crate::HyperlaneProviderError; use crate::H256; @@ -65,9 +61,6 @@ pub enum ChainCommunicationError { /// An error with a contract call #[error(transparent)] ContractError(HyperlaneCustomErrorWrapper), - /// Provider Error - #[error(transparent)] - ProviderError(#[from] ProviderError), /// A transaction was dropped from the mempool #[error("Transaction dropped from mempool {0:?}")] TransactionDropped(H256), @@ -78,6 +71,9 @@ pub enum ChainCommunicationError { /// A transaction submission timed out #[error("Transaction submission timed out")] TransactionTimeout(), + /// No signer is available and was required for the operation + #[error("Signer unavailable")] + SignerUnavailable, } impl ChainCommunicationError { @@ -90,14 +86,53 @@ impl ChainCommunicationError { pub fn from_other_boxed(err: Box) -> Self { Self::Other(HyperlaneCustomErrorWrapper(err)) } -} -impl From> for ChainCommunicationError -where - M: Middleware + 'static, -{ - fn from(e: ContractError) -> Self { - Self::ContractError(HyperlaneCustomErrorWrapper(Box::new(e))) + /// Creates a chain communication error of the other error variant from a static string + pub fn from_other_str(err: &'static str) -> Self { + #[derive(Debug)] + #[repr(transparent)] + struct StringError(&'static str); + impl Display for StringError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0) + } + } + impl StdError for StringError {} + + Self::from_contract_error(StringError(err)) + } + + /// Creates a chain communication error of the contract error variant from any other existing + /// error + pub fn from_contract_error(err: E) -> Self + where + E: HyperlaneCustomError, + { + Self::ContractError(HyperlaneCustomErrorWrapper(Box::new(err))) + } + + /// Creates a chain communication error of the contract error variant from any other existing + /// error + pub fn from_contract_error_boxed(err: Box) -> Self + where + E: HyperlaneCustomError, + { + Self::ContractError(HyperlaneCustomErrorWrapper(err)) + } + + /// Creates a chain communication error of the contract error variant from a static string + pub fn from_contract_error_str(err: &'static str) -> Self { + #[derive(Debug)] + #[repr(transparent)] + struct StringError(&'static str); + impl Display for StringError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0) + } + } + impl StdError for StringError {} + + Self::from_contract_error(StringError(err)) } } @@ -107,12 +142,29 @@ impl From for ChainCommunicationError { } } +#[cfg(feature = "ethers")] +impl From> + for ChainCommunicationError +{ + fn from(err: ethers_contract::ContractError) -> Self { + Self::ContractError(HyperlaneCustomErrorWrapper(Box::new(err))) + } +} + +#[cfg(feature = "ethers")] +impl From for ChainCommunicationError { + fn from(err: ethers::providers::ProviderError) -> Self { + Self::ContractError(HyperlaneCustomErrorWrapper(Box::new(err))) + } +} + /// Error types for the Hyperlane protocol #[derive(Debug, thiserror::Error)] pub enum HyperlaneProtocolError { /// Signature Error pasthrough + #[cfg(feature = "ethers")] #[error(transparent)] - SignatureError(#[from] SignatureError), + SignatureError(#[from] ethers_core::types::SignatureError), /// IO error from Read/Write usage #[error(transparent)] IoError(#[from] std::io::Error), diff --git a/rust/hyperlane-core/src/lib.rs b/rust/hyperlane-core/src/lib.rs index 841a3bd732..0e8349186b 100644 --- a/rust/hyperlane-core/src/lib.rs +++ b/rust/hyperlane-core/src/lib.rs @@ -2,7 +2,7 @@ //! implementations. #![warn(missing_docs)] -#![forbid(unsafe_code)] +#![deny(unsafe_code)] #![forbid(where_clauses_object_safety)] extern crate core; diff --git a/rust/hyperlane-core/src/test_utils.rs b/rust/hyperlane-core/src/test_utils.rs index 2bce83e1cf..c24d704191 100644 --- a/rust/hyperlane-core/src/test_utils.rs +++ b/rust/hyperlane-core/src/test_utils.rs @@ -2,9 +2,8 @@ use std::fs::File; use std::io::Read; use std::path::PathBuf; -use primitive_types::H256; - use crate::accumulator::merkle::Proof; +use crate::H256; /// Struct representing a single merkle test case #[derive(serde::Deserialize, serde::Serialize)] diff --git a/rust/hyperlane-core/src/traits/cursor.rs b/rust/hyperlane-core/src/traits/cursor.rs index 33dcf1ea4b..0beb238686 100644 --- a/rust/hyperlane-core/src/traits/cursor.rs +++ b/rust/hyperlane-core/src/traits/cursor.rs @@ -3,15 +3,7 @@ use std::time::Duration; use async_trait::async_trait; use auto_impl::auto_impl; -use crate::{ChainResult, LogMeta}; - -/// The action that should be taken by the contract sync loop -pub enum CursorAction { - /// Direct the contract_sync task to query a block range - Query((u32, u32)), - /// Direct the contract_sync task to sleep for a duration - Sleep(Duration), -} +use crate::{ChainResult, IndexRange, LogMeta}; /// A cursor governs event indexing for a contract. #[async_trait] @@ -27,3 +19,11 @@ pub trait ContractSyncCursor: Send + Sync + 'static { /// accordingly. async fn update(&mut self, logs: Vec<(T, LogMeta)>) -> eyre::Result<()>; } + +/// The action that should be taken by the contract sync loop +pub enum CursorAction { + /// Direct the contract_sync task to query a block range (inclusive) + Query(IndexRange), + /// Direct the contract_sync task to sleep for a duration + Sleep(Duration), +} diff --git a/rust/hyperlane-core/src/traits/deployed.rs b/rust/hyperlane-core/src/traits/deployed.rs index 9e98f2e43e..ecd3d63efb 100644 --- a/rust/hyperlane-core/src/traits/deployed.rs +++ b/rust/hyperlane-core/src/traits/deployed.rs @@ -42,19 +42,37 @@ pub trait HyperlaneAbi { impl fmt::Debug for dyn HyperlaneChain { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let domain = self.domain(); - write!(f, "HyperlaneChain({} ({}))", domain, domain.id()) + #[cfg(feature = "strum")] + { + write!(f, "HyperlaneChain({domain} ({}))", domain.id()) + } + #[cfg(not(feature = "strum"))] + { + write!(f, "HyperlaneChain({})", domain.id()) + } } } impl fmt::Debug for dyn HyperlaneContract { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let domain = self.domain(); - write!( - f, - "HyperlaneContract({:?} @ {} ({}))", - self.address(), - domain, - domain.id(), - ) + #[cfg(feature = "strum")] + { + write!( + f, + "HyperlaneContract({:?} @ {domain} ({}))", + self.address(), + domain.id(), + ) + } + #[cfg(not(feature = "strum"))] + { + write!( + f, + "HyperlaneContract({:?} @ {})", + self.address(), + domain.id(), + ) + } } } diff --git a/rust/hyperlane-core/src/traits/encode.rs b/rust/hyperlane-core/src/traits/encode.rs index 2d61b36f0e..bf1287c7d1 100644 --- a/rust/hyperlane-core/src/traits/encode.rs +++ b/rust/hyperlane-core/src/traits/encode.rs @@ -1,5 +1,3 @@ -use ethers_core::types::{Signature, SignatureError}; -use std::convert::TryFrom; use std::io::{Error, ErrorKind}; use crate::{HyperlaneProtocolError, H256, U256}; @@ -28,7 +26,8 @@ pub trait Decode { Self: Sized; } -impl Encode for Signature { +#[cfg(feature = "ethers")] +impl Encode for ethers_core::types::Signature { fn write_to(&self, writer: &mut W) -> std::io::Result where W: std::io::Write, @@ -38,7 +37,8 @@ impl Encode for Signature { } } -impl Decode for Signature { +#[cfg(feature = "ethers")] +impl Decode for ethers_core::types::Signature { fn read_from(reader: &mut R) -> Result where R: std::io::Read, @@ -46,7 +46,7 @@ impl Decode for Signature { let mut buf = [0u8; 65]; let len = reader.read(&mut buf)?; if len != 65 { - Err(SignatureError::InvalidLength(len).into()) + Err(ethers_core::types::SignatureError::InvalidLength(len).into()) } else { Ok(Self::try_from(buf.as_ref())?) } diff --git a/rust/hyperlane-core/src/traits/indexer.rs b/rust/hyperlane-core/src/traits/indexer.rs index a90af25dd6..fd4fd48700 100644 --- a/rust/hyperlane-core/src/traits/indexer.rs +++ b/rust/hyperlane-core/src/traits/indexer.rs @@ -5,18 +5,42 @@ //! a chain-specific library and provider (e.g. ethers::provider). use std::fmt::Debug; +use std::ops::RangeInclusive; use async_trait::async_trait; use auto_impl::auto_impl; +use serde::Deserialize; use crate::{ChainResult, HyperlaneMessage, LogMeta}; +/// Indexing mode. +#[derive(Copy, Debug, Default, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum IndexMode { + /// Block based indexing. + #[default] + Block, + /// Sequence based indexing. + Sequence, +} + +/// An indexing range. +#[derive(Debug, Clone)] +pub enum IndexRange { + /// For block-based indexers + BlockRange(RangeInclusive), + /// For indexers that look for specific sequences, e.g. message nonces. + SequenceRange(RangeInclusive), +} + +pub use IndexRange::*; + /// Interface for an indexer. #[async_trait] #[auto_impl(&, Box, Arc,)] pub trait Indexer: Send + Sync + Debug { /// Fetch list of logs between blocks `from` and `to`, inclusive. - async fn fetch_logs(&self, from: u32, to: u32) -> ChainResult>; + async fn fetch_logs(&self, range: IndexRange) -> ChainResult>; /// Get the chain's latest block number that has reached finality async fn get_finalized_block_number(&self) -> ChainResult; diff --git a/rust/hyperlane-core/src/traits/interchain_security_module.rs b/rust/hyperlane-core/src/traits/interchain_security_module.rs index 6ecd300286..1d6907fad9 100644 --- a/rust/hyperlane-core/src/traits/interchain_security_module.rs +++ b/rust/hyperlane-core/src/traits/interchain_security_module.rs @@ -2,14 +2,16 @@ use std::fmt::Debug; use async_trait::async_trait; use auto_impl::auto_impl; +use borsh::{BorshDeserialize, BorshSerialize}; use num_derive::FromPrimitive; -use primitive_types::U256; -use strum::Display; -use crate::{ChainResult, HyperlaneContract, HyperlaneMessage}; +use crate::{ChainResult, HyperlaneContract, HyperlaneMessage, U256}; /// Enumeration of all known module types -#[derive(FromPrimitive, Clone, Debug, Default, Display, Copy, PartialEq, Eq)] +#[derive( + FromPrimitive, Clone, Debug, Default, Copy, PartialEq, Eq, BorshDeserialize, BorshSerialize, +)] +#[cfg_attr(feature = "strum", derive(strum::Display))] pub enum ModuleType { /// INVALID ISM #[default] diff --git a/rust/hyperlane-core/src/traits/mod.rs b/rust/hyperlane-core/src/traits/mod.rs index 61bcf6d02c..48b8430cea 100644 --- a/rust/hyperlane-core/src/traits/mod.rs +++ b/rust/hyperlane-core/src/traits/mod.rs @@ -44,13 +44,17 @@ pub struct TxOutcome { // TODO: more? What can be abstracted across all chains? } +#[cfg(feature = "ethers")] impl From for TxOutcome { fn from(t: ethers_core::types::TransactionReceipt) -> Self { Self { - txid: t.transaction_hash, + txid: t.transaction_hash.into(), executed: t.status.unwrap().low_u32() == 1, - gas_used: t.gas_used.unwrap_or(crate::U256::zero()), - gas_price: t.effective_gas_price.unwrap_or(crate::U256::zero()), + gas_used: t.gas_used.map(Into::into).unwrap_or(crate::U256::zero()), + gas_price: t + .effective_gas_price + .map(Into::into) + .unwrap_or(crate::U256::zero()), } } } diff --git a/rust/hyperlane-core/src/traits/signing.rs b/rust/hyperlane-core/src/traits/signing.rs index 8f0ee4b25f..5257279192 100644 --- a/rust/hyperlane-core/src/traits/signing.rs +++ b/rust/hyperlane-core/src/traits/signing.rs @@ -2,18 +2,13 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use auto_impl::auto_impl; -use ethers_core::{ - types::{Address, Signature}, - utils::hash_message, -}; - use serde::{ ser::{SerializeStruct, Serializer}, Deserialize, Serialize, }; use crate::utils::fmt_bytes; -use crate::{HyperlaneProtocolError, H160, H256}; +use crate::{Signature, H160, H256}; /// An error incurred by a signer #[derive(thiserror::Error, Debug)] @@ -43,7 +38,11 @@ pub trait HyperlaneSignerExt { ) -> Result, HyperlaneSignerError>; /// Check whether a message was signed by a specific address. - fn verify(&self, signed: &SignedType) -> Result<(), HyperlaneProtocolError>; + #[cfg(feature = "ethers")] + fn verify( + &self, + signed: &SignedType, + ) -> Result<(), crate::HyperlaneProtocolError>; } #[async_trait] @@ -57,7 +56,11 @@ impl HyperlaneSignerExt for S { Ok(SignedType { value, signature }) } - fn verify(&self, signed: &SignedType) -> Result<(), HyperlaneProtocolError> { + #[cfg(feature = "ethers")] + fn verify( + &self, + signed: &SignedType, + ) -> Result<(), crate::HyperlaneProtocolError> { signed.verify(self.eth_address()) } } @@ -72,7 +75,7 @@ pub trait Signable: Sized { /// EIP-191 compliant hash of the signing hash. fn eth_signed_message_hash(&self) -> H256 { - hash_message(self.signing_hash()) + hashes::hash_message(self.signing_hash()) } } @@ -103,17 +106,20 @@ impl Serialize for SignedType { impl SignedType { /// Recover the Ethereum address of the signer - pub fn recover(&self) -> Result { - Ok(self - .signature - .recover(self.value.eth_signed_message_hash())?) + #[cfg(feature = "ethers")] + pub fn recover(&self) -> Result { + let hash = ethers_core::types::H256::from(self.value.eth_signed_message_hash()); + let sig = ethers_core::types::Signature::from(self.signature); + Ok(sig.recover(hash)?.into()) } /// Check whether a message was signed by a specific address - pub fn verify(&self, signer: Address) -> Result<(), HyperlaneProtocolError> { - Ok(self - .signature - .verify(self.value.eth_signed_message_hash(), signer)?) + #[cfg(feature = "ethers")] + pub fn verify(&self, signer: H160) -> Result<(), crate::HyperlaneProtocolError> { + let hash = ethers_core::types::H256::from(self.value.eth_signed_message_hash()); + let sig = ethers_core::types::Signature::from(self.signature); + let signer = ethers_core::types::H160::from(signer); + Ok(sig.verify(hash, signer)?) } } @@ -126,3 +132,54 @@ impl Debug for SignedType { ) } } + +// Copied from https://github.com/hyperlane-xyz/ethers-rs/blob/hyperlane/ethers-core/src/utils/hash.rs +// so that we can get EIP-191 hashing without the `ethers` feature +mod hashes { + const PREFIX: &str = "\x19Ethereum Signed Message:\n"; + use crate::H256; + use tiny_keccak::{Hasher, Keccak}; + + /// Hash a message according to EIP-191. + /// + /// The data is a UTF-8 encoded string and will enveloped as follows: + /// `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed + /// using keccak256. + pub fn hash_message(message: S) -> H256 + where + S: AsRef<[u8]>, + { + let message = message.as_ref(); + + let mut eth_message = format!("{PREFIX}{}", message.len()).into_bytes(); + eth_message.extend_from_slice(message); + + keccak256(ð_message).into() + } + + /// Compute the Keccak-256 hash of input bytes. + // TODO: Add Solidity Keccak256 packing support + pub fn keccak256(bytes: S) -> [u8; 32] + where + S: AsRef<[u8]>, + { + let mut output = [0u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(bytes.as_ref()); + hasher.finalize(&mut output); + output + } + + #[test] + #[cfg(feature = "ethers")] + fn ensure_signed_hashes_match() { + assert_eq!( + ethers_core::utils::hash_message(b"gm crypto!"), + hash_message(b"gm crypto!").into() + ); + assert_eq!( + ethers_core::utils::hash_message(b"hyperlane"), + hash_message(b"hyperlane").into() + ); + } +} diff --git a/rust/hyperlane-core/src/traits/validator_announce.rs b/rust/hyperlane-core/src/traits/validator_announce.rs index 3ee4b4b35f..91ad55abfb 100644 --- a/rust/hyperlane-core/src/traits/validator_announce.rs +++ b/rust/hyperlane-core/src/traits/validator_announce.rs @@ -8,7 +8,7 @@ use crate::{Announcement, ChainResult, HyperlaneContract, SignedType, TxOutcome, /// Interface for the ValidatorAnnounce chain contract. Allows abstraction over /// different chains #[async_trait] -#[auto_impl(Box, Arc)] +#[auto_impl(&, Box, Arc)] pub trait ValidatorAnnounce: HyperlaneContract + Send + Sync + Debug { /// Returns the announced storage locations for the provided validators. async fn get_announced_storage_locations( diff --git a/rust/hyperlane-core/src/types/checkpoint.rs b/rust/hyperlane-core/src/types/checkpoint.rs index 73e0d9d1ba..e5cf613202 100644 --- a/rust/hyperlane-core/src/types/checkpoint.rs +++ b/rust/hyperlane-core/src/types/checkpoint.rs @@ -1,10 +1,10 @@ +use std::fmt::Debug; + use derive_more::Deref; -use ethers_core::types::{Address, Signature}; use serde::{Deserialize, Serialize}; use sha3::{digest::Update, Digest, Keccak256}; -use std::fmt::Debug; -use crate::{utils::domain_hash, Signable, SignedType, H256}; +use crate::{utils::domain_hash, Signable, Signature, SignedType, H160, H256}; /// An Hyperlane checkpoint #[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug)] @@ -73,7 +73,7 @@ pub type SignedCheckpointWithMessageId = SignedType; #[derive(Clone, Debug)] pub struct SignedCheckpointWithSigner { /// The recovered signer - pub signer: Address, + pub signer: H160, /// The signed checkpoint pub signed_checkpoint: SignedType, } @@ -84,7 +84,7 @@ pub struct SignatureWithSigner { /// The signature pub signature: Signature, /// The signer of the signature - pub signer: Address, + pub signer: H160, } /// A checkpoint and multiple signatures diff --git a/rust/hyperlane-core/src/types/log_metadata.rs b/rust/hyperlane-core/src/types/log_metadata.rs index ec05cf874c..fdd33e0274 100644 --- a/rust/hyperlane-core/src/types/log_metadata.rs +++ b/rust/hyperlane-core/src/types/log_metadata.rs @@ -1,8 +1,10 @@ use std::cmp::Ordering; -use ethers_contract::LogMeta as EthersLogMeta; use serde::{Deserialize, Serialize}; +#[cfg(feature = "ethers")] +use ethers_contract::LogMeta as EthersLogMeta; + use crate::{H256, U256}; /// A close clone of the Ethereum `LogMeta`, this is designed to be a more @@ -29,28 +31,23 @@ pub struct LogMeta { pub log_index: U256, } +#[cfg(feature = "ethers")] impl From for LogMeta { fn from(v: EthersLogMeta) -> Self { - Self { - address: v.address.into(), - block_number: v.block_number.as_u64(), - block_hash: v.block_hash, - transaction_hash: v.transaction_hash, - transaction_index: v.transaction_index.as_u64(), - log_index: v.log_index, - } + Self::from(&v) } } +#[cfg(feature = "ethers")] impl From<&EthersLogMeta> for LogMeta { fn from(v: &EthersLogMeta) -> Self { Self { - address: v.address.into(), + address: crate::H160::from(v.address).into(), block_number: v.block_number.as_u64(), - block_hash: v.block_hash, - transaction_hash: v.transaction_hash, + block_hash: v.block_hash.into(), + transaction_hash: v.transaction_hash.into(), transaction_index: v.transaction_index.as_u64(), - log_index: v.log_index, + log_index: v.log_index.into(), } } } diff --git a/rust/hyperlane-core/src/types/mod.rs b/rust/hyperlane-core/src/types/mod.rs index 280572427b..b502a4b50b 100644 --- a/rust/hyperlane-core/src/types/mod.rs +++ b/rust/hyperlane-core/src/types/mod.rs @@ -1,7 +1,11 @@ -pub use primitive_types::{H128, H160, H256, H512, U128, U256, U512}; +use serde::{Deserialize, Serialize}; +use std::fmt; use std::io::{Read, Write}; use std::ops::Add; +pub use self::primitive_types::*; +#[cfg(feature = "ethers")] +pub use ::primitive_types as ethers_core_types; pub use announcement::*; pub use chain_data::*; pub use checkpoint::*; @@ -15,10 +19,89 @@ mod chain_data; mod checkpoint; mod log_metadata; mod message; +mod serialize; /// Unified 32-byte identifier with convenience tooling for handling /// 20-byte ids (e.g ethereum addresses) pub mod identifiers; +mod primitive_types; + +// Copied from https://github.com/hyperlane-xyz/ethers-rs/blob/hyperlane/ethers-core/src/types/signature.rs#L54 +// To avoid depending on the `ethers` type +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy, Hash)] +/// An ECDSA signature +pub struct Signature { + /// R value + pub r: U256, + /// S Value + pub s: U256, + /// V value + pub v: u64, +} + +impl Signature { + /// Copies and serializes `self` into a new `Vec` with the recovery id included + pub fn to_vec(&self) -> Vec { + self.into() + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let sig = <[u8; 65]>::from(self); + write!(f, "{}", hex::encode(&sig[..])) + } +} + +impl From<&Signature> for [u8; 65] { + fn from(src: &Signature) -> [u8; 65] { + let mut sig = [0u8; 65]; + src.r.to_big_endian(&mut sig[0..32]); + src.s.to_big_endian(&mut sig[32..64]); + sig[64] = src.v as u8; + sig + } +} + +impl From for [u8; 65] { + fn from(src: Signature) -> [u8; 65] { + <[u8; 65]>::from(&src) + } +} + +impl From<&Signature> for Vec { + fn from(src: &Signature) -> Vec { + <[u8; 65]>::from(src).to_vec() + } +} + +impl From for Vec { + fn from(src: Signature) -> Vec { + <[u8; 65]>::from(&src).to_vec() + } +} + +#[cfg(feature = "ethers")] +impl From for Signature { + fn from(value: ethers_core::types::Signature) -> Self { + Self { + r: value.r.into(), + s: value.s.into(), + v: value.v, + } + } +} + +#[cfg(feature = "ethers")] +impl From for ethers_core::types::Signature { + fn from(value: Signature) -> Self { + Self { + r: value.r.into(), + s: value.s.into(), + v: value.v, + } + } +} /// A payment of a message's gas costs. #[derive(Debug, Copy, Clone)] diff --git a/rust/hyperlane-core/src/types/primitive_types.rs b/rust/hyperlane-core/src/types/primitive_types.rs new file mode 100644 index 0000000000..be47c824fe --- /dev/null +++ b/rust/hyperlane-core/src/types/primitive_types.rs @@ -0,0 +1,310 @@ +// Based on https://github.com/paritytech/parity-common/blob/a5ef7308d6986e62431e35d3156fed0a7a585d39/primitive-types/src/lib.rs + +#![allow(clippy::assign_op_pattern)] +#![allow(clippy::reversed_empty_ranges)] + +use std::fmt::Formatter; + +use crate::types::serialize; +use borsh::{BorshDeserialize, BorshSerialize}; +use fixed_hash::{construct_fixed_hash, impl_fixed_hash_conversions}; +use serde::de::Visitor; +use uint::construct_uint; + +/// Error type for conversion. +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + /// Overflow encountered. + Overflow, +} + +construct_uint! { + /// 128-bit unsigned integer. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct U128(2); +} +construct_uint! { + /// 256-bit unsigned integer. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct U256(4); +} + +construct_uint! { + /// 512-bit unsigned integer. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct U512(8); +} + +construct_fixed_hash! { + /// 128-bit hash type. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct H128(16); +} + +construct_fixed_hash! { + /// 160-bit hash type. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct H160(20); +} + +construct_fixed_hash! { + /// 256-bit hash type. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct H256(32); +} + +construct_fixed_hash! { + /// 512-bit hash type. + #[derive(BorshSerialize, BorshDeserialize)] + pub struct H512(64); +} + +struct H512Visitor; +impl<'de> Visitor<'de> for H512Visitor { + type Value = H512; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a 512-bit hash") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + v.try_into() + .map_err(|_| E::invalid_length(v.len(), &self)) + .map(H512) + } +} + +#[cfg(feature = "ethers")] +type EthersH160 = ethers_core::types::H160; +#[cfg(feature = "ethers")] +type EthersH256 = ethers_core::types::H256; +#[cfg(feature = "ethers")] +impl_fixed_hash_conversions!(H256, EthersH160); +#[cfg(feature = "ethers")] +impl_fixed_hash_conversions!(EthersH256, H160); + +impl_fixed_hash_conversions!(H256, H160); +impl_fixed_hash_conversions!(H512, H256); +impl_fixed_hash_conversions!(H512, H160); + +macro_rules! impl_fixed_uint_conversions { + ($larger:ty, $smaller:ty) => { + impl From<$smaller> for $larger { + impl_fixed_uint_conversions!(@from_smaller $larger, $smaller); + } + + impl<'a> From<&'a $smaller> for $larger { + impl_fixed_uint_conversions!(@from_smaller $larger, &'a $smaller); + } + + impl TryFrom<$larger> for $smaller { + type Error = Error; + impl_fixed_uint_conversions!(@try_from_larger $larger, $smaller); + } + + impl<'a> TryFrom<&'a $larger> for $smaller { + type Error = Error; + impl_fixed_uint_conversions!(@try_from_larger &'a $larger, $smaller); + } + }; + (@from_smaller $larger:ty, $smaller:ty) => { + fn from(val: $smaller) -> $larger { + let mut ret = <$larger>::zero(); + for i in 0..val.0.len() { + ret.0[i] = val.0[i]; + } + ret + } + }; + (@try_from_larger $larger:ty, $smaller:ty) => { + fn try_from(val: $larger) -> Result<$smaller, Error> { + let mut ret = <$smaller>::zero(); + for i in 0..ret.0.len() { + ret.0[i] = val.0[i]; + } + + let mut ov = 0; + for i in ret.0.len()..val.0.len() { + ov |= val.0[i]; + } + if ov == 0 { + Ok(ret) + } else { + Err(Error::Overflow) + } + } + }; +} + +#[cfg(feature = "ethers")] +impl_fixed_uint_conversions!(U256, ethers_core::types::U128); +impl_fixed_uint_conversions!(U256, U128); +impl_fixed_uint_conversions!(U512, U128); +impl_fixed_uint_conversions!(U512, U256); + +macro_rules! impl_f64_conversions { + ($ty:ty) => { + impl $ty { + /// Lossy saturating conversion from a `f64` to a `$ty`. Like for floating point to + /// primitive integer type conversions, this truncates fractional parts. + /// + /// The conversion follows the same rules as converting `f64` to other + /// primitive integer types. Namely, the conversion of `value: f64` behaves as + /// follows: + /// - `NaN` => `0` + /// - `(-∞, 0]` => `0` + /// - `(0, $ty::MAX]` => `value as $ty` + /// - `($ty::MAX, +∞)` => `$ty::MAX` + pub fn from_f64_lossy(val: f64) -> $ty { + const TY_BITS: u64 = <$ty>::zero().0.len() as u64 * <$ty>::WORD_BITS as u64; + if val >= 1.0 { + let bits = val.to_bits(); + // NOTE: Don't consider the sign or check that the subtraction will + // underflow since we already checked that the value is greater + // than 1.0. + let exponent = ((bits >> 52) & 0x7ff) - 1023; + let mantissa = (bits & 0x0f_ffff_ffff_ffff) | 0x10_0000_0000_0000; + + if exponent <= 52 { + <$ty>::from(mantissa >> (52 - exponent)) + } else if exponent < TY_BITS { + <$ty>::from(mantissa) << <$ty>::from(exponent - 52) + } else { + <$ty>::MAX + } + } else { + <$ty>::zero() + } + } + + /// Lossy conversion of `$ty` to `f64`. + pub fn to_f64_lossy(self) -> f64 { + let mut acc = 0.0; + for i in (0..self.0.len()).rev() { + acc += self.0[i] as f64 * 2.0f64.powi((i * <$ty>::WORD_BITS) as i32); + } + acc + } + } + }; +} + +impl_f64_conversions!(U128); +impl_f64_conversions!(U256); +impl_f64_conversions!(U512); + +#[cfg(feature = "ethers")] +macro_rules! impl_inner_conversion { + ($a:ty, $b:ty) => { + impl From<$a> for $b { + fn from(val: $a) -> Self { + Self(val.0) + } + } + + impl<'a> From<&'a $a> for $b { + fn from(val: &'a $a) -> Self { + Self(val.0) + } + } + + impl From<$b> for $a { + fn from(val: $b) -> Self { + Self(val.0) + } + } + + impl<'a> From<&'a $b> for $a { + fn from(val: &'a $b) -> Self { + Self(val.0) + } + } + }; +} + +#[cfg(feature = "ethers")] +impl_inner_conversion!(H128, ethers_core::types::H128); +#[cfg(feature = "ethers")] +impl_inner_conversion!(H160, ethers_core::types::H160); +#[cfg(feature = "ethers")] +impl_inner_conversion!(H256, ethers_core::types::H256); +#[cfg(feature = "ethers")] +impl_inner_conversion!(H512, ethers_core::types::H512); +#[cfg(feature = "ethers")] +impl_inner_conversion!(U128, ethers_core::types::U128); +#[cfg(feature = "ethers")] +impl_inner_conversion!(U256, ethers_core::types::U256); +#[cfg(feature = "ethers")] +impl_inner_conversion!(U512, ethers_core::types::U512); + +/// Add Serde serialization support to an integer created by `construct_uint!`. +macro_rules! impl_uint_serde { + ($name: ident, $len: expr) => { + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut slice = [0u8; 2 + 2 * $len * 8]; + let mut bytes = [0u8; $len * 8]; + self.to_big_endian(&mut bytes); + serialize::serialize_uint(&mut slice, &bytes, serializer) + } + } + + impl<'de> serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut bytes = [0u8; $len * 8]; + let wrote = serialize::deserialize_check_len( + deserializer, + serialize::ExpectedLen::Between(0, &mut bytes), + )?; + Ok(bytes[0..wrote].into()) + } + } + }; +} + +/// Add Serde serialization support to a fixed-sized hash type created by `construct_fixed_hash!`. +macro_rules! impl_fixed_hash_serde { + ($name: ident, $len: expr) => { + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut slice = [0u8; 2 + 2 * $len]; + serialize::serialize_raw(&mut slice, &self.0, serializer) + } + } + + impl<'de> serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut bytes = [0u8; $len]; + serialize::deserialize_check_len( + deserializer, + serialize::ExpectedLen::Exact(&mut bytes), + )?; + Ok($name(bytes)) + } + } + }; +} + +impl_uint_serde!(U128, 2); +impl_uint_serde!(U256, 4); +impl_uint_serde!(U512, 8); + +impl_fixed_hash_serde!(H128, 16); +impl_fixed_hash_serde!(H160, 20); +impl_fixed_hash_serde!(H256, 32); +impl_fixed_hash_serde!(H512, 64); diff --git a/rust/hyperlane-core/src/types/serialize.rs b/rust/hyperlane-core/src/types/serialize.rs new file mode 100644 index 0000000000..9ffb69994e --- /dev/null +++ b/rust/hyperlane-core/src/types/serialize.rs @@ -0,0 +1,349 @@ +#![allow(unused)] +// Based on https://github.com/paritytech/parity-common/blob/7194def73feb7d97644303f1a6ddbab29bbb799f/primitive-types/impls/serde/src/serialize.rs + +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use core::{fmt, result::Result}; +use serde::{de, Deserializer, Serializer}; + +static CHARS: &[u8] = b"0123456789abcdef"; + +/// Serialize given bytes to a 0x-prefixed hex string. +/// +/// If `skip_leading_zero` initial 0s will not be printed out, +/// unless the byte string is empty, in which case `0x0` will be returned. +/// The results are consistent with `serialize_uint` output if the flag is +/// on and `serialize_raw` if the flag is off. +pub fn to_hex(bytes: &[u8], skip_leading_zero: bool) -> String { + let bytes = if skip_leading_zero { + let non_zero = bytes.iter().take_while(|b| **b == 0).count(); + let bytes = &bytes[non_zero..]; + if bytes.is_empty() { + return "0x0".into(); + } else { + bytes + } + } else if bytes.is_empty() { + return "0x".into(); + } else { + bytes + }; + + let mut slice = vec![0u8; (bytes.len() + 1) * 2]; + to_hex_raw(&mut slice, bytes, skip_leading_zero).into() +} + +fn to_hex_raw<'a>(v: &'a mut [u8], bytes: &[u8], skip_leading_zero: bool) -> &'a str { + assert!(v.len() > 1 + bytes.len() * 2); + + v[0] = b'0'; + v[1] = b'x'; + + let mut idx = 2; + let first_nibble = bytes[0] >> 4; + if first_nibble != 0 || !skip_leading_zero { + v[idx] = CHARS[first_nibble as usize]; + idx += 1; + } + v[idx] = CHARS[(bytes[0] & 0xf) as usize]; + idx += 1; + + for &byte in bytes.iter().skip(1) { + v[idx] = CHARS[(byte >> 4) as usize]; + v[idx + 1] = CHARS[(byte & 0xf) as usize]; + idx += 2; + } + + // SAFETY: all characters come either from CHARS or "0x", therefore valid UTF8 + #[allow(unsafe_code)] + unsafe { + core::str::from_utf8_unchecked(&v[0..idx]) + } +} + +/// Decoding bytes from hex string error. +#[derive(Debug, PartialEq, Eq)] +pub enum FromHexError { + /// The `0x` prefix is missing. + #[deprecated(since = "0.3.2", note = "We support non 0x-prefixed hex strings")] + MissingPrefix, + /// Invalid (non-hex) character encountered. + InvalidHex { + /// The unexpected character. + character: char, + /// Index of that occurrence. + index: usize, + }, +} + +#[cfg(feature = "std")] +impl std::error::Error for FromHexError {} + +impl fmt::Display for FromHexError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match *self { + #[allow(deprecated)] + Self::MissingPrefix => write!(fmt, "0x prefix is missing"), + Self::InvalidHex { character, index } => { + write!(fmt, "invalid hex character: {}, at {}", character, index) + } + } + } +} + +/// Decode given (both 0x-prefixed or not) hex string into a vector of bytes. +/// +/// Returns an error if non-hex characters are present. +pub fn from_hex(v: &str) -> Result, FromHexError> { + let (v, stripped) = v.strip_prefix("0x").map_or((v, false), |v| (v, true)); + + let mut bytes = vec![0u8; (v.len() + 1) / 2]; + from_hex_raw(v, &mut bytes, stripped)?; + Ok(bytes) +} + +/// Decode given 0x-prefix-stripped hex string into provided slice. +/// Used internally by `from_hex` and `deserialize_check_len`. +/// +/// The method will panic if `bytes` have incorrect length (make sure to allocate enough beforehand). +fn from_hex_raw(v: &str, bytes: &mut [u8], stripped: bool) -> Result { + let bytes_len = v.len(); + let mut modulus = bytes_len % 2; + let mut buf = 0; + let mut pos = 0; + for (index, byte) in v.bytes().enumerate() { + buf <<= 4; + + match byte { + b'A'..=b'F' => buf |= byte - b'A' + 10, + b'a'..=b'f' => buf |= byte - b'a' + 10, + b'0'..=b'9' => buf |= byte - b'0', + b' ' | b'\r' | b'\n' | b'\t' => { + buf >>= 4; + continue; + } + b => { + let character = char::from(b); + return Err(FromHexError::InvalidHex { + character, + index: index + if stripped { 2 } else { 0 }, + }); + } + } + + modulus += 1; + if modulus == 2 { + modulus = 0; + bytes[pos] = buf; + pos += 1; + } + } + + Ok(pos) +} + +/// Serializes a slice of bytes. +pub fn serialize_raw(slice: &mut [u8], bytes: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + if bytes.is_empty() { + serializer.serialize_str("0x") + } else { + serializer.serialize_str(to_hex_raw(slice, bytes, false)) + } +} + +/// Serializes a slice of bytes. +pub fn serialize(bytes: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + let mut slice = vec![0u8; (bytes.len() + 1) * 2]; + serialize_raw(&mut slice, bytes, serializer) +} + +/// Serialize a slice of bytes as uint. +/// +/// The representation will have all leading zeros trimmed. +pub fn serialize_uint(slice: &mut [u8], bytes: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + let non_zero = bytes.iter().take_while(|b| **b == 0).count(); + let bytes = &bytes[non_zero..]; + if bytes.is_empty() { + serializer.serialize_str("0x0") + } else { + serializer.serialize_str(to_hex_raw(slice, bytes, true)) + } +} + +/// Expected length of bytes vector. +#[derive(Debug, PartialEq, Eq)] +pub enum ExpectedLen<'a> { + /// Exact length in bytes. + Exact(&'a mut [u8]), + /// A bytes length between (min; slice.len()]. + Between(usize, &'a mut [u8]), +} + +impl<'a> fmt::Display for ExpectedLen<'a> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match *self { + ExpectedLen::Exact(ref v) => write!(fmt, "{} bytes", v.len()), + ExpectedLen::Between(min, ref v) => write!(fmt, "between ({}; {}] bytes", min, v.len()), + } + } +} + +/// Deserialize into vector of bytes. This will allocate an O(n) intermediate +/// string. +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct Visitor; + + impl<'b> de::Visitor<'b> for Visitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "a (both 0x-prefixed or not) hex string or byte array" + ) + } + + fn visit_str(self, v: &str) -> Result { + from_hex(v).map_err(E::custom) + } + + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut bytes = vec![]; + while let Some(n) = seq.next_element::()? { + bytes.push(n); + } + Ok(bytes) + } + + fn visit_newtype_struct>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_bytes(self) + } + } + + deserializer.deserialize_str(Visitor) +} + +/// Deserialize into vector of bytes with additional size check. +/// Returns number of bytes written. +pub fn deserialize_check_len<'a, 'de, D>( + deserializer: D, + len: ExpectedLen<'a>, +) -> Result +where + D: Deserializer<'de>, +{ + struct Visitor<'a> { + len: ExpectedLen<'a>, + } + + impl<'a, 'b> de::Visitor<'b> for Visitor<'a> { + type Value = usize; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "a (both 0x-prefixed or not) hex string or byte array containing {}", + self.len + ) + } + + fn visit_str(self, v: &str) -> Result { + let (v, stripped) = v.strip_prefix("0x").map_or((v, false), |v| (v, true)); + + let len = v.len(); + let is_len_valid = match self.len { + ExpectedLen::Exact(ref slice) => len == 2 * slice.len(), + ExpectedLen::Between(min, ref slice) => len <= 2 * slice.len() && len > 2 * min, + }; + + if !is_len_valid { + return Err(E::invalid_length(v.len(), &self)); + } + + let bytes = match self.len { + ExpectedLen::Exact(slice) => slice, + ExpectedLen::Between(_, slice) => slice, + }; + + from_hex_raw(v, bytes, stripped).map_err(E::custom) + } + + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + let len = v.len(); + let is_len_valid = match self.len { + ExpectedLen::Exact(ref slice) => len == slice.len(), + ExpectedLen::Between(min, ref slice) => len <= slice.len() && len > min, + }; + + if !is_len_valid { + return Err(E::invalid_length(v.len(), &self)); + } + + let bytes = match self.len { + ExpectedLen::Exact(slice) => slice, + ExpectedLen::Between(_, slice) => slice, + }; + + bytes[..len].copy_from_slice(v); + Ok(len) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + self.visit_bytes(&v) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let mut v = vec![]; + while let Some(n) = seq.next_element::()? { + v.push(n); + } + self.visit_byte_buf(v) + } + + fn visit_newtype_struct>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_bytes(self) + } + } + + deserializer.deserialize_str(Visitor { len }) +} diff --git a/rust/hyperlane-core/src/utils.rs b/rust/hyperlane-core/src/utils.rs index e57d99675c..9072364a91 100644 --- a/rust/hyperlane-core/src/utils.rs +++ b/rust/hyperlane-core/src/utils.rs @@ -1,8 +1,9 @@ -use std::time::Duration; +use std::{str::FromStr, time::Duration}; +use eyre::Result; use sha3::{digest::Update, Digest, Keccak256}; -use crate::{KnownHyperlaneDomain, H256}; +use crate::{KnownHyperlaneDomain, H160, H256}; /// Strips the '0x' prefix off of hex string so it can be deserialized. /// @@ -17,6 +18,25 @@ pub fn strip_0x_prefix(s: &str) -> &str { } } +/// Converts a hex or base58 string to an H256. +pub fn hex_or_base58_to_h256(string: &str) -> Result { + let h256 = if string.starts_with("0x") { + match string.len() { + 66 => H256::from_str(string)?, + 42 => H160::from_str(string)?.into(), + _ => eyre::bail!("Invalid hex string"), + } + } else { + let bytes = bs58::decode(string).into_vec()?; + if bytes.len() != 32 { + eyre::bail!("Invalid length of base58 string") + } + H256::from_slice(bytes.as_slice()) + }; + + Ok(h256) +} + /// Computes hash of domain concatenated with "HYPERLANE" pub fn domain_hash(address: H256, domain: impl Into) -> H256 { H256::from_slice( @@ -55,9 +75,16 @@ pub fn fmt_bytes(bytes: &[u8]) -> String { /// Format a domain id as a name if it is known or just the number if not. pub fn fmt_domain(domain: u32) -> String { - KnownHyperlaneDomain::try_from(domain) - .map(|d| d.to_string()) - .unwrap_or_else(|_| domain.to_string()) + #[cfg(feature = "strum")] + { + KnownHyperlaneDomain::try_from(domain) + .map(|d| d.to_string()) + .unwrap_or_else(|_| domain.to_string()) + } + #[cfg(not(feature = "strum"))] + { + domain.to_string() + } } /// Formats the duration in the most appropriate time units. diff --git a/rust/hyperlane-test/Cargo.toml b/rust/hyperlane-test/Cargo.toml index cdd7e8933d..f096b593a6 100644 --- a/rust/hyperlane-test/Cargo.toml +++ b/rust/hyperlane-test/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "hyperlane-test" documentation.workspace = true diff --git a/rust/sealevel/.gitignore b/rust/sealevel/.gitignore new file mode 100644 index 0000000000..b21c3b0132 --- /dev/null +++ b/rust/sealevel/.gitignore @@ -0,0 +1,2 @@ +/target +environments/**/deploy-logs.txt \ No newline at end of file diff --git a/rust/sealevel/README.md b/rust/sealevel/README.md new file mode 100644 index 0000000000..b0feeb0aca --- /dev/null +++ b/rust/sealevel/README.md @@ -0,0 +1,127 @@ +# Hyperlane Sealevel (Solana VM) Integration + +# Running local end to end test + +A local end to end test has been written that will: + +1. Run a local Solana network +2. Deploy two sets of core contracts (i.e. Mailbox / Multisig ISM / ValidatorAnnounce) onto this chain, one with domain 13375 and the other 13376. +3. Deploy a "native" warp route on domain 13375 and a "synthetic" warp route on domain 13376 +4. Send native lamports from domain 13375 to 13376 +5. A validator & relayer can then be spun up to deliver the message + +### Build and run solana-test-validator + +This only needs to be done once when initially setting things up. + +1. Clone the `solar-eclipse` repo, which is the Eclipse fork of the Solana repo. This is needed to run the local Solana network. Check out the `steven/hyperlane-fix-deps` branch: + +``` +git clone git@github.com:Eclipse-Laboratories-Inc/solar-eclipse --branch steven/hyperlane-fix-deps +``` + +2. `cd` into the repo and build the `solana-test-validator` using the local `cargo` script (which ensures the correct version is used): + +``` +./cargo build -p solana-test-validator +``` + +### Check out `eclipse-program-library` + +This is a fork (with some dependency fixes) of the eclipse fork of the `solana-program-library`. This contains "SPL" programs that are commonly used programs - stuff like the token program, etc. + +Note these instructions previously required a different remote and branch - make sure to move to this remote & branch if you ahven't already! + +1. Check out the branch `trevor/steven/eclipse-1.14.13/with-tlv-lib`: + +``` +git clone git@github.com:tkporter/eclipse-program-library.git --branch trevor/steven/eclipse-1.14.13/with-tlv-lib +``` + +### Build the required SPL programs and Hyperlane programs + +This command will build all the required SPL programs (e.g. the token program, token 2022 program, SPL noop, etc...) found in the local repo of `eclipse-program-library`, +and will build all the required Hyperlane programs (e.g. the Mailbox program, Validator Announce, etc...). + +You need to run this if any changes are made to programs that you want to be used in future runs of the end to end test. + +Change the paths to your local `solar-eclipse` repo and `eclipse-program-library` as necessary, and run this from the `rust` directory of hyperlane-monorepo. + +``` +SOLAR_ECLIPSE_DIR=~/solar-eclipse ECLIPSE_PROGRAM_LIBRARY_DIR=~/eclipse-program-library ./utils/sealevel-test.bash build-only +``` + +### Run the local Solana network + +This will run the `solana-test-validator` with a funded test account `E9VrvAdGRvCguN2XgXsgu9PNmMM3vZsU8LSUrM68j8ty` that will later be used for deploying contracts. It will also create some of the required SPL programs at the specified program IDs - these program IDs are consistent across Solana networks and are required by our Hyperlane programs. Change paths as necessary - the \*.so files should have been created by the prior command. The `--ledger` directory is arbitrary and is just the data dir for the Solana validator. + +``` +mkdir -p /tmp/eclipse/ledger-dir && target/debug/solana-test-validator --reset --ledger /tmp/eclipse/ledger-dir --account E9VrvAdGRvCguN2XgXsgu9PNmMM3vZsU8LSUrM68j8ty ~/abacus-monorepo/rust/config/sealevel/test-keys/test_deployer-account.json --bpf-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA ~/eclipse-program-library/target/deploy/spl_token.so --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb ~/eclipse-program-library/target/deploy/spl_token_2022.so --bpf-program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL ~/eclipse-program-library/target/deploy/spl_associated_token_account.so --bpf-program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV ~/eclipse-program-library/account-compression/target/deploy/spl_noop.so +``` + +By now you should have an output like this - keep it running and move to another terminal: + +``` +Ledger location: /tmp/eclipse/ledger-dir +Log: /tmp/eclipse/ledger-dir/validator.log +⠒ Initializing... +⠄ Initializing... +Identity: 4P5rtWdphhehU32myNQcTSMgrCRz7kdvZEnasX6fahJQ +Genesis Hash: G7CY7wEzbdjh8RwqTszxrpYTqiHKvqwpaw3JbmKJjJhU +Version: 1.14.13 +Shred Version: 419 +Gossip Address: 127.0.0.1:1024 +TPU Address: 127.0.0.1:1027 +JSON RPC URL: http://127.0.0.1:8899 +⠒ 00:05:35 | Processed Slot: 668 | Confirmed Slot: 668 | Finalized Slot: 6 +``` + +### Run the local end to end script + +Run the script found at `rust/utils/sealevel-test.bash`. This will build all required programs, deploy contracts, and test sending a warp route message. You need to supply the paths to your local `solar-eclipse` and `eclipse-program-library` repos: + +``` +SOLAR_ECLIPSE_DIR=~/solar-eclipse ECLIPSE_PROGRAM_LIBRARY_DIR=~/eclipse-program-library ./utils/sealevel-test.bash +``` + +Note: this won't rebuild any of the programs. If you want to rebuild them, you can either cd into them individually and run `cargo build-sbf --arch sbf`, or you can run the above bash script with `force-build-programs` as the first argument. + +You'll see a bunch of output here showing programs being built and deployed. Eventually you should see some logs saying `grep -q 'Message not delivered'`. At this point, the contracts have all been deployed and a native warp route transfer has been made. You can move on to running the validator and relayer. + +### Running the validator + +In a separate terminal, cd to `hyperlane-monorepo/rust`. + +1. Source the env vars: + +``` +source ./config/sealevel/validator.env +``` + +2. Run the validator (this clears the DB / checkpoints if present): + +``` +mkdir /tmp/SEALEVEL_DB ; rm -rf /tmp/SEALEVEL_DB/validator /tmp/test_sealevel_checkpoints_0x70997970c51812dc3a010c7d01b50e0d17dc79c8/* ; CONFIG_FILES=./config/sealevel/sealevel.json cargo run --bin validator +``` + +You should see some INFO logs about checkpoint at index 0. + +You can confirm things are working correctly by looking at `/tmp/CHECKPOINTS_DIR`, where the validator posts its signatures. + +### Running the relayer + +In a separate terminal, again in `hyperlane-monorepo/rust`: + +1. Source the env vars: + +``` +source ./config/sealevel/relayer.env +``` + +2. Run the relayer (the rm is to make sure the relayer's DB is cleared): + +``` +rm -rf /tmp/SEALEVEL_DB/relayer ; RUST_BACKTRACE=full CONFIG_FILES=./config/sealevel/sealevel.json cargo run --bin relayer +``` + +When the original `sealevel-test.bash` exits with a 0 exit code and some logs about Hyperlane Token Storage, the message has been successfully delivered! diff --git a/rust/sealevel/client/Cargo.toml b/rust/sealevel/client/Cargo.toml new file mode 100644 index 0000000000..2d862cb130 --- /dev/null +++ b/rust/sealevel/client/Cargo.toml @@ -0,0 +1,30 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +borsh.workspace = true +clap = { workspace = true, features = ["derive"] } +hex.workspace = true +pretty_env_logger.workspace = true +serde.workspace = true +serde_json.workspace = true +solana-clap-utils.workspace = true +solana-cli-config.workspace = true +solana-client.workspace = true +solana-program.workspace = true +solana-sdk.workspace = true + +account-utils = { path = "../libraries/account-utils" } +hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../programs/mailbox" } +hyperlane-sealevel-multisig-ism-message-id = { path = "../programs/ism/multisig-ism-message-id" } +hyperlane-sealevel-token = { path = "../programs/hyperlane-sealevel-token" } +hyperlane-sealevel-token-collateral = { path = "../programs/hyperlane-sealevel-token-collateral" } +hyperlane-sealevel-token-lib = { path = "../libraries/hyperlane-sealevel-token" } +hyperlane-sealevel-token-native = { path = "../programs/hyperlane-sealevel-token-native" } +hyperlane-sealevel-validator-announce = { path = "../programs/validator-announce" } \ No newline at end of file diff --git a/rust/sealevel/client/src/cmd_utils.rs b/rust/sealevel/client/src/cmd_utils.rs new file mode 100644 index 0000000000..9fa908ea21 --- /dev/null +++ b/rust/sealevel/client/src/cmd_utils.rs @@ -0,0 +1,160 @@ +use std::{ + collections::HashMap, + fs::File, + io::Write, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use solana_client::{client_error::ClientError, rpc_client::RpcClient}; +use solana_sdk::{ + commitment_config::CommitmentConfig, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +/// Open a file in append mode, or create it if it does not exist. +fn append_to(p: impl AsRef) -> File { + File::options() + .create(true) + .append(true) + .open(p) + .expect("Failed to open file") +} + +pub fn build_cmd( + cmd: &[&str], + log: impl AsRef, + log_all: bool, + wd: Option<&str>, + env: Option<&HashMap<&str, &str>>, + assert_success: bool, +) { + assert!(!cmd.is_empty(), "Must specify a command!"); + let mut c = Command::new(cmd[0]); + c.args(&cmd[1..]); + if log_all { + c.stdout(Stdio::inherit()); + } else { + c.stdout(append_to(log)); + } + if let Some(wd) = wd { + c.current_dir(wd); + } + if let Some(env) = env { + c.envs(env); + } + let status = c.status().expect("Failed to run command"); + if assert_success { + assert!( + status.success(), + "Command returned non-zero exit code: {}", + cmd.join(" ") + ); + } +} + +pub(crate) fn account_exists(client: &RpcClient, account: &Pubkey) -> Result { + // Using `get_account_with_commitment` instead of `get_account` so we get Ok(None) when the account + // doesn't exist, rather than an error + let exists = client + .get_account_with_commitment(account, CommitmentConfig::processed())? + .value + .is_some(); + Ok(exists) +} + +pub(crate) fn deploy_program_idempotent( + payer_path: &str, + program_keypair: &Keypair, + program_keypair_path: &str, + program_path: &str, + url: &str, + log_file: impl AsRef, +) -> Result<(), ClientError> { + let client = RpcClient::new(url.to_string()); + if !account_exists(&client, &program_keypair.pubkey())? { + deploy_program( + payer_path, + program_keypair_path, + program_path, + url, + log_file, + ); + } else { + println!("Program {} already deployed", program_keypair.pubkey()); + } + + Ok(()) +} + +pub(crate) fn deploy_program( + payer_path: &str, + program_keypair_path: &str, + program_path: &str, + url: &str, + log_file: impl AsRef, +) { + build_cmd( + &[ + "solana", + "--url", + url, + "-k", + payer_path, + "program", + "deploy", + program_path, + "--upgrade-authority", + payer_path, + "--program-id", + program_keypair_path, + ], + log_file, + true, + None, + None, + true, + ); +} + +pub(crate) fn create_new_file(parent_dir: &Path, name: &str) -> PathBuf { + let path = parent_dir.join(name); + let _file = File::create(path.clone()) + .unwrap_or_else(|_| panic!("Failed to create file {}", path.display())); + path +} + +pub(crate) fn create_new_directory(parent_dir: &Path, name: &str) -> PathBuf { + let path = parent_dir.join(name); + std::fs::create_dir_all(path.clone()) + .unwrap_or_else(|_| panic!("Failed to create directory {}", path.display())); + path +} + +pub(crate) fn create_and_write_keypair( + key_dir: &Path, + key_name: &str, + use_existing_key: bool, +) -> (Keypair, PathBuf) { + let path = key_dir.join(key_name); + + if use_existing_key { + if let Ok(file) = File::open(path.clone()) { + println!("Using existing key at path {}", path.display()); + let keypair_bytes: Vec = serde_json::from_reader(file).unwrap(); + let keypair = Keypair::from_bytes(&keypair_bytes[..]).unwrap(); + return (keypair, path); + } + } + + let keypair = Keypair::new(); + let keypair_json = serde_json::to_string(&keypair.to_bytes()[..]).unwrap(); + + let mut file = File::create(path.clone()).expect("Failed to create keypair file"); + file.write_all(keypair_json.as_bytes()) + .expect("Failed to write keypair to file"); + println!("Wrote keypair {} to {}", keypair.pubkey(), path.display()); + + (keypair, path) +} diff --git a/rust/sealevel/client/src/core.rs b/rust/sealevel/client/src/core.rs new file mode 100644 index 0000000000..990f79d349 --- /dev/null +++ b/rust/sealevel/client/src/core.rs @@ -0,0 +1,264 @@ +use serde::{Deserialize, Serialize}; + +use solana_program::pubkey::Pubkey; +use solana_sdk::signature::Signer; + +use std::{fs::File, io::Write, path::Path, str::FromStr}; + +use crate::{ + cmd_utils::{create_and_write_keypair, create_new_directory, create_new_file, deploy_program}, + Context, CoreCmd, CoreSubCmd, +}; + +pub(crate) fn process_core_cmd(mut ctx: Context, cmd: CoreCmd) { + match cmd.cmd { + CoreSubCmd::Deploy(core) => { + let environments_dir = create_new_directory(&core.environments_dir, &core.environment); + let chain_dir = create_new_directory(&environments_dir, &core.chain); + let core_dir = create_new_directory(&chain_dir, "core"); + let key_dir = create_new_directory(&core_dir, "keys"); + let log_file = create_new_file(&core_dir, "deploy-logs.txt"); + + let ism_program_id = deploy_multisig_ism_message_id( + &mut ctx, + core.use_existing_keys, + &key_dir, + &core.built_so_dir, + &log_file, + ); + + let mailbox_program_id = deploy_mailbox( + &mut ctx, + core.use_existing_keys, + &key_dir, + &core.built_so_dir, + &log_file, + core.local_domain, + ism_program_id, + ); + + let validator_announce_program_id = deploy_validator_announce( + &mut ctx, + core.use_existing_keys, + &key_dir, + &core.built_so_dir, + &log_file, + mailbox_program_id, + core.local_domain, + ); + + let program_ids = CoreProgramIds { + mailbox: mailbox_program_id, + validator_announce: validator_announce_program_id, + multisig_ism_message_id: ism_program_id, + }; + write_program_ids(&core_dir, program_ids); + } + } +} + +fn deploy_multisig_ism_message_id( + ctx: &mut Context, + use_existing_key: bool, + key_dir: &Path, + built_so_dir: &Path, + log_file: impl AsRef, +) -> Pubkey { + let (keypair, keypair_path) = create_and_write_keypair( + key_dir, + "hyperlane_sealevel_multisig_ism_message_id-keypair.json", + use_existing_key, + ); + let program_id = keypair.pubkey(); + + deploy_program( + &ctx.payer_path, + keypair_path.to_str().unwrap(), + built_so_dir + .join("hyperlane_sealevel_multisig_ism_message_id.so") + .to_str() + .unwrap(), + &ctx.client.url(), + log_file, + ); + + println!( + "Deployed Multisig ISM Message ID at program ID {}", + program_id + ); + + // Initialize + let instruction = hyperlane_sealevel_multisig_ism_message_id::instruction::init_instruction( + program_id, + ctx.payer.pubkey(), + ) + .unwrap(); + + ctx.instructions.push(instruction); + ctx.send_transaction(&[&ctx.payer]); + ctx.instructions.clear(); + + println!("Initialized Multisig ISM Message ID "); + + program_id +} + +fn deploy_mailbox( + ctx: &mut Context, + use_existing_key: bool, + key_dir: &Path, + built_so_dir: &Path, + log_file: impl AsRef, + local_domain: u32, + default_ism: Pubkey, +) -> Pubkey { + let (keypair, keypair_path) = create_and_write_keypair( + key_dir, + "hyperlane_sealevel_mailbox-keypair.json", + use_existing_key, + ); + let program_id = keypair.pubkey(); + + deploy_program( + &ctx.payer_path, + keypair_path.to_str().unwrap(), + built_so_dir + .join("hyperlane_sealevel_mailbox.so") + .to_str() + .unwrap(), + &ctx.client.url(), + log_file, + ); + + println!("Deployed Mailbox at program ID {}", program_id); + + // Initialize + let instruction = hyperlane_sealevel_mailbox::instruction::init_instruction( + program_id, + local_domain, + default_ism, + ctx.payer.pubkey(), + ) + .unwrap(); + + ctx.instructions.push(instruction); + ctx.send_transaction(&[&ctx.payer]); + ctx.instructions.clear(); + + println!("Initialized Mailbox"); + + program_id +} + +fn deploy_validator_announce( + ctx: &mut Context, + use_existing_key: bool, + key_dir: &Path, + built_so_dir: &Path, + log_file: impl AsRef, + mailbox_program_id: Pubkey, + local_domain: u32, +) -> Pubkey { + let (keypair, keypair_path) = create_and_write_keypair( + key_dir, + "hyperlane_sealevel_validator_announce-keypair.json", + use_existing_key, + ); + let program_id = keypair.pubkey(); + + deploy_program( + &ctx.payer_path, + keypair_path.to_str().unwrap(), + built_so_dir + .join("hyperlane_sealevel_validator_announce.so") + .to_str() + .unwrap(), + &ctx.client.url(), + log_file, + ); + + println!("Deployed ValidatorAnnounce at program ID {}", program_id); + + // Initialize + let instruction = hyperlane_sealevel_validator_announce::instruction::init_instruction( + program_id, + ctx.payer.pubkey(), + mailbox_program_id, + local_domain, + ) + .unwrap(); + + ctx.instructions.push(instruction); + ctx.send_transaction(&[&ctx.payer]); + ctx.instructions.clear(); + + println!("Initialized ValidatorAnnounce"); + + program_id +} + +#[derive(Debug)] +pub(crate) struct CoreProgramIds { + pub mailbox: Pubkey, + pub validator_announce: Pubkey, + pub multisig_ism_message_id: Pubkey, +} + +impl From for CoreProgramIds { + fn from(program_ids: PrettyCoreProgramIds) -> Self { + Self { + mailbox: Pubkey::from_str(program_ids.mailbox.as_str()).unwrap(), + validator_announce: Pubkey::from_str(program_ids.validator_announce.as_str()).unwrap(), + multisig_ism_message_id: Pubkey::from_str(program_ids.multisig_ism_message_id.as_str()) + .unwrap(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct PrettyCoreProgramIds { + mailbox: String, + validator_announce: String, + multisig_ism_message_id: String, +} + +impl From for PrettyCoreProgramIds { + fn from(program_ids: CoreProgramIds) -> Self { + Self { + mailbox: program_ids.mailbox.to_string(), + validator_announce: program_ids.validator_announce.to_string(), + multisig_ism_message_id: program_ids.multisig_ism_message_id.to_string(), + } + } +} + +fn write_program_ids(core_dir: &Path, program_ids: CoreProgramIds) { + let pretty_program_ids = PrettyCoreProgramIds::from(program_ids); + + let json = serde_json::to_string_pretty(&pretty_program_ids).unwrap(); + let path = core_dir.join("program-ids.json"); + + println!("Writing program IDs to {}:\n{}", path.display(), json); + + let mut file = File::create(path).expect("Failed to create keypair file"); + file.write_all(json.as_bytes()) + .expect("Failed to write program IDs to file"); +} + +pub(crate) fn read_core_program_ids( + environments_dir: &Path, + environment: &str, + chain: &str, +) -> CoreProgramIds { + let path = environments_dir + .join(environment) + .join(chain) + .join("core") + .join("program-ids.json"); + let file = File::open(path).expect("Failed to open program IDs file"); + + let pretty_program_ids: PrettyCoreProgramIds = + serde_json::from_reader(file).expect("Failed to read program IDs file"); + + CoreProgramIds::from(pretty_program_ids) +} diff --git a/rust/sealevel/client/src/main.rs b/rust/sealevel/client/src/main.rs new file mode 100644 index 0000000000..525ce71240 --- /dev/null +++ b/rust/sealevel/client/src/main.rs @@ -0,0 +1,1239 @@ +//! Test client for Hyperlane Sealevel Mailbox contract. + +// #![deny(missing_docs)] // FIXME +#![deny(unsafe_code)] + +use std::{path::PathBuf, str::FromStr}; + +use account_utils::DiscriminatorEncode; +use clap::{Args, Parser, Subcommand, ValueEnum}; +use hyperlane_core::{Encode, HyperlaneMessage, H160, H256}; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_mailbox::{ + accounts::{InboxAccount, OutboxAccount}, + instruction::{InboxProcess, Instruction as MailboxInstruction, OutboxDispatch, VERSION}, + mailbox_dispatched_message_pda_seeds, mailbox_inbox_pda_seeds, + mailbox_message_dispatch_authority_pda_seeds, mailbox_outbox_pda_seeds, + mailbox_processed_message_pda_seeds, spl_noop, +}; +use hyperlane_sealevel_multisig_ism_message_id::{ + access_control_pda_seeds as multisig_ism_message_id_access_control_pda_seeds, + accounts::AccessControlAccount, + domain_data_pda_seeds as multisig_ism_message_id_domain_data_pda_seeds, + instruction::{ + Domained, Instruction as MultisigIsmMessageIdInstruction, ValidatorsAndThreshold, + }, +}; +use hyperlane_sealevel_token::{ + hyperlane_token_ata_payer_pda_seeds, hyperlane_token_mint_pda_seeds, plugin::SyntheticPlugin, + spl_associated_token_account::get_associated_token_address_with_program_id, spl_token_2022, +}; +use hyperlane_sealevel_token_collateral::{ + hyperlane_token_escrow_pda_seeds, plugin::CollateralPlugin, +}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneTokenAccount, + hyperlane_token_pda_seeds, + instruction::{ + Init as HtInit, Instruction as HtInstruction, TransferRemote as HtTransferRemote, + }, +}; +use hyperlane_sealevel_token_native::{ + hyperlane_token_native_collateral_pda_seeds, plugin::NativePlugin, +}; +use hyperlane_sealevel_validator_announce::{ + accounts::ValidatorStorageLocationsAccount, + instruction::{ + AnnounceInstruction as ValidatorAnnounceAnnounceInstruction, + Instruction as ValidatorAnnounceInstruction, + }, + replay_protection_pda_seeds, validator_announce_pda_seeds, + validator_storage_locations_pda_seeds, +}; +use solana_clap_utils::input_validators::{is_keypair, is_url, normalize_to_url_if_moniker}; +use solana_cli_config::{Config, CONFIG_FILE}; +use solana_client::{rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig}; +use solana_program::pubkey; +use solana_sdk::{ + commitment_config::CommitmentConfig, + compute_budget::ComputeBudgetInstruction, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{read_keypair_file, Keypair, Signer as _}, + signer::signers::Signers, + system_program, + transaction::Transaction, +}; + +pub(crate) use crate::core::*; +use crate::warp_route::process_warp_route_cmd; + +mod cmd_utils; +mod r#core; +mod warp_route; + +// Note: from solana_program_runtime::compute_budget +const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000; +const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; +const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; + +const ECLIPSE_DOMAIN: u32 = 13375; // TODO import from hyperlane + +#[derive(Parser)] +#[command(version, about)] +struct Cli { + #[command(subcommand)] + cmd: HyperlaneSealevelCmd, + #[arg(long, short)] + url: Option, + #[arg(long, short)] + keypair: Option, + #[arg(long, short = 'b', default_value_t = MAX_COMPUTE_UNIT_LIMIT)] + compute_budget: u32, + #[arg(long, short = 'a')] + heap_size: Option, +} + +#[derive(Subcommand)] +enum HyperlaneSealevelCmd { + Core(CoreCmd), + Mailbox(MailboxCmd), + Token(TokenCmd), + ValidatorAnnounce(ValidatorAnnounceCmd), + MultisigIsmMessageId(MultisigIsmMessageIdCmd), + WarpRoute(WarpRouteCmd), +} + +#[derive(Args)] +pub(crate) struct WarpRouteCmd { + #[command(subcommand)] + cmd: WarpRouteSubCmd, +} + +#[derive(Subcommand)] +pub(crate) enum WarpRouteSubCmd { + Deploy(WarpRouteDeploy), +} + +#[derive(Args)] +pub(crate) struct WarpRouteDeploy { + #[arg(long)] + environment: String, + #[arg(long)] + environments_dir: PathBuf, + #[arg(long)] + built_so_dir: PathBuf, + #[arg(long)] + warp_route_name: String, + #[arg(long)] + token_config_file: PathBuf, + #[arg(long)] + chain_config_file: PathBuf, + #[arg(long)] + ata_payer_funding_amount: Option, +} + +#[derive(Args)] +struct CoreCmd { + #[command(subcommand)] + cmd: CoreSubCmd, +} + +#[derive(Subcommand)] +enum CoreSubCmd { + Deploy(CoreDeploy), +} + +#[derive(Args)] +struct CoreDeploy { + #[arg(long)] + local_domain: u32, + #[arg(long)] + environment: String, + #[arg(long)] + chain: String, + #[arg(long)] + use_existing_keys: bool, + #[arg(long)] + environments_dir: PathBuf, + #[arg(long)] + built_so_dir: PathBuf, +} + +#[derive(Args)] +struct MailboxCmd { + #[command(subcommand)] + cmd: MailboxSubCmd, +} + +#[derive(Subcommand)] +enum MailboxSubCmd { + Init(Init), + Query(Query), + Send(Outbox), + Receive(Inbox), + Delivered(Delivered), +} + +const MAILBOX_PROG_ID: Pubkey = pubkey!("692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1"); +const HYPERLANE_TOKEN_PROG_ID: Pubkey = pubkey!("3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH"); +const MULTISIG_ISM_MESSAGE_ID_PROG_ID: Pubkey = + pubkey!("2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw"); +const VALIDATOR_ANNOUNCE_PROG_ID: Pubkey = pubkey!("DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn"); + +#[derive(Args)] +struct Init { + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + program_id: Pubkey, + #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] + local_domain: u32, + #[arg(long, short, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] + default_ism: Pubkey, +} + +#[derive(Args)] +struct Query { + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + program_id: Pubkey, +} + +#[derive(Args)] +struct Outbox { + #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] + destination: u32, + #[arg(long, short)] + recipient: Pubkey, + #[arg(long, short, default_value = "Hello, World!")] + message: String, + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + program_id: Pubkey, + // #[arg(long, short, default_value_t = MAX_MESSAGE_BODY_BYTES)] + // message_len: usize, +} + +#[derive(Args)] +struct Inbox { + #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] + local_domain: u32, + #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] + origin: u32, + #[arg(long, short)] + recipient: Pubkey, + #[arg(long, short, default_value = "Hello, World!")] + message: String, + #[arg(long, short, default_value_t = 1)] + nonce: u32, + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + program_id: Pubkey, + #[arg(long, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] + ism: Pubkey, +} + +#[derive(Args)] +struct Delivered { + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + program_id: Pubkey, + #[arg(long, short)] + message_id: H256, +} + +// Actual content depends on which ISM is used. +struct ExampleMetadata { + pub root: H256, + pub index: u32, + pub leaf_index: u32, + // pub proof: [H256; 32], + pub signatures: Vec, +} +impl Encode for ExampleMetadata { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: std::io::Write, + { + writer.write_all(self.root.as_ref())?; + writer.write_all(&self.index.to_be_bytes())?; + writer.write_all(&self.leaf_index.to_be_bytes())?; + // for hash in self.proof { + // writer.write_all(hash.as_ref())?; + // } + for signature in &self.signatures { + writer.write_all(signature.as_ref())?; + } + Ok(32 + 4 + 4 + (32 * 32) + (self.signatures.len() * 32)) + } +} + +#[derive(Args)] +struct TokenCmd { + #[command(subcommand)] + cmd: TokenSubCmd, +} + +#[derive(Subcommand)] +enum TokenSubCmd { + Init(TokenInit), + Query(TokenQuery), + TransferRemote(TokenTransferRemote), + EnrollRemoteRouter(TokenEnrollRemoteRouter), +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum TokenType { + Native, + Synthetic, + Collateral, +} + +#[derive(Args)] +struct TokenInit { + #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] + program_id: Pubkey, + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + mailbox: Pubkey, + #[arg(value_enum)] + token_type: TokenType, + #[arg(long, short)] + interchain_security_module: Option, + #[arg(long, short, default_value_t = 9)] + decimals: u8, + #[arg(long, short, default_value_t = 18)] + remote_decimals: u8, +} + +#[derive(Args)] +struct TokenQuery { + #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] + program_id: Pubkey, + #[arg(value_enum)] + token_type: TokenType, +} + +#[derive(Args)] +struct TokenTransferRemote { + #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] + program_id: Pubkey, + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + mailbox: Pubkey, + // Note this is the keypair for normal account not the derived associated token account or delegate. + sender: String, + amount: u64, + // #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] + destination_domain: u32, + #[arg(long, short = 't', default_value_t = HYPERLANE_TOKEN_PROG_ID)] + destination_token_program_id: Pubkey, + recipient: String, + #[arg(value_enum)] + token_type: TokenType, +} + +#[derive(Args)] +struct TokenEnrollRemoteRouter { + #[arg(long, short, default_value_t = HYPERLANE_TOKEN_PROG_ID)] + program_id: Pubkey, + domain: u32, + router: H256, +} + +#[derive(Args)] +struct ValidatorAnnounceCmd { + #[command(subcommand)] + cmd: ValidatorAnnounceSubCmd, +} + +#[derive(Subcommand)] +enum ValidatorAnnounceSubCmd { + Init(ValidatorAnnounceInit), + Announce(ValidatorAnnounceAnnounce), + Query(ValidatorAnnounceQuery), +} + +#[derive(Args)] +struct ValidatorAnnounceInit { + #[arg(long, short, default_value_t = VALIDATOR_ANNOUNCE_PROG_ID)] + program_id: Pubkey, + #[arg(long, short, default_value_t = MAILBOX_PROG_ID)] + mailbox_id: Pubkey, + #[arg(long, short, default_value_t = ECLIPSE_DOMAIN)] + local_domain: u32, +} + +#[derive(Args)] +struct ValidatorAnnounceAnnounce { + #[arg(long, short, default_value_t = VALIDATOR_ANNOUNCE_PROG_ID)] + program_id: Pubkey, + #[arg(long)] + validator: H160, + #[arg(long)] + storage_location: String, + #[arg(long)] + signature: String, +} + +#[derive(Args)] +struct ValidatorAnnounceQuery { + #[arg(long, short, default_value_t = VALIDATOR_ANNOUNCE_PROG_ID)] + program_id: Pubkey, + validator: H160, +} + +#[derive(Args)] +struct MultisigIsmMessageIdCmd { + #[command(subcommand)] + cmd: MultisigIsmMessageIdSubCmd, +} + +#[derive(Subcommand)] +enum MultisigIsmMessageIdSubCmd { + Init(MultisigIsmMessageIdInit), + SetValidatorsAndThreshold(MultisigIsmMessageIdSetValidatorsAndThreshold), + Query(MultisigIsmMessageIdInit), +} + +#[derive(Args)] +struct MultisigIsmMessageIdInit { + #[arg(long, short, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] + program_id: Pubkey, +} + +#[derive(Args)] +struct MultisigIsmMessageIdSetValidatorsAndThreshold { + #[arg(long, short, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)] + program_id: Pubkey, + #[arg(long)] + domain: u32, + #[arg(long, value_delimiter = ',')] + validators: Vec, + #[arg(long)] + threshold: u8, +} + +pub(crate) struct Context { + client: RpcClient, + payer: Keypair, + payer_path: String, + commitment: CommitmentConfig, + instructions: Vec, +} + +impl Context { + fn send_transaction(&self, signers: &T) { + let recent_blockhash = self.client.get_latest_blockhash().unwrap(); + let txn = Transaction::new_signed_with_payer( + &self.instructions, + Some(&self.payer.pubkey()), + signers, + recent_blockhash, + ); + + let _signature = self + .client + .send_and_confirm_transaction_with_spinner_and_config( + &txn, + self.commitment, + RpcSendTransactionConfig { + preflight_commitment: Some(self.commitment.commitment), + ..RpcSendTransactionConfig::default() + }, + ) + .map_err(|err| { + eprintln!("{:#?}", err); + err + }) + .unwrap(); + } + + fn send_transaction_with_client(&self, client: &RpcClient, signers: &T) { + let recent_blockhash = client.get_latest_blockhash().unwrap(); + let txn = Transaction::new_signed_with_payer( + &self.instructions, + Some(&self.payer.pubkey()), + signers, + recent_blockhash, + ); + + let _signature = client + .send_and_confirm_transaction_with_spinner_and_config( + &txn, + self.commitment, + RpcSendTransactionConfig { + preflight_commitment: Some(self.commitment.commitment), + ..RpcSendTransactionConfig::default() + }, + ) + .map_err(|err| { + eprintln!("{:#?}", err); + err + }) + .unwrap(); + } +} + +fn main() { + pretty_env_logger::init(); + + let cli = Cli::parse(); + let config = match CONFIG_FILE.as_ref() { + Some(config_file) => Config::load(config_file).unwrap(), + None => Config::default(), + }; + let url = normalize_to_url_if_moniker(cli.url.unwrap_or(config.json_rpc_url)); + is_url(&url).unwrap(); + let keypair_path = cli.keypair.unwrap_or(config.keypair_path); + is_keypair(&keypair_path).unwrap(); + + let client = RpcClient::new(url); + let payer = read_keypair_file(keypair_path.clone()).unwrap(); + let commitment = CommitmentConfig::processed(); + + let mut instructions = vec![]; + if cli.compute_budget != DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT { + assert!(cli.compute_budget <= MAX_COMPUTE_UNIT_LIMIT); + instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( + cli.compute_budget, + )); + } + if let Some(heap_size) = cli.heap_size { + assert!(heap_size <= MAX_HEAP_FRAME_BYTES); + instructions.push(ComputeBudgetInstruction::request_heap_frame(heap_size)); + } + + let ctx = Context { + client, + payer, + payer_path: keypair_path, + commitment, + instructions, + }; + match cli.cmd { + HyperlaneSealevelCmd::Mailbox(cmd) => process_mailbox_cmd(ctx, cmd), + HyperlaneSealevelCmd::Token(cmd) => process_token_cmd(ctx, cmd), + HyperlaneSealevelCmd::ValidatorAnnounce(cmd) => process_validator_announce_cmd(ctx, cmd), + HyperlaneSealevelCmd::MultisigIsmMessageId(cmd) => { + process_multisig_ism_message_id_cmd(ctx, cmd) + } + HyperlaneSealevelCmd::Core(cmd) => process_core_cmd(ctx, cmd), + HyperlaneSealevelCmd::WarpRoute(cmd) => process_warp_route_cmd(ctx, cmd), + } +} + +fn process_mailbox_cmd(mut ctx: Context, cmd: MailboxCmd) { + match cmd.cmd { + MailboxSubCmd::Init(init) => { + let instruction = hyperlane_sealevel_mailbox::instruction::init_instruction( + init.program_id, + init.local_domain, + init.default_ism, + ctx.payer.pubkey(), + ) + .unwrap(); + + ctx.instructions.push(instruction); + ctx.send_transaction(&[&ctx.payer]); + } + MailboxSubCmd::Query(query) => { + let (inbox_account, inbox_bump) = + Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), &query.program_id); + let (outbox_account, outbox_bump) = + Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &query.program_id); + + let accounts = ctx + .client + .get_multiple_accounts_with_commitment( + &[inbox_account, outbox_account], + ctx.commitment, + ) + .unwrap() + .value; + println!("mailbox={}", query.program_id); + println!("--------------------------------"); + println!("Inbox: {}, bump={}", inbox_account, inbox_bump); + if let Some(info) = &accounts[0] { + println!("{:#?}", info); + match InboxAccount::fetch(&mut info.data.as_ref()) { + Ok(inbox) => println!("{:#?}", inbox.into_inner()), + Err(err) => println!("Failed to deserialize account data: {}", err), + } + } else { + println!("Not yet created?"); + } + println!("--------------------------------"); + println!("Outbox: {}, bump={}", outbox_account, outbox_bump); + if let Some(info) = &accounts[1] { + println!("{:#?}", info); + match OutboxAccount::fetch(&mut info.data.as_ref()) { + Ok(outbox) => println!("{:#?}", outbox.into_inner()), + Err(err) => println!("Failed to deserialize account data: {}", err), + } + } else { + println!("Not yet created?"); + } + } + MailboxSubCmd::Send(outbox) => { + let (outbox_account, _outbox_bump) = + Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &outbox.program_id); + let ixn = MailboxInstruction::OutboxDispatch(OutboxDispatch { + sender: ctx.payer.pubkey(), + destination_domain: outbox.destination, + recipient: H256(outbox.recipient.to_bytes()), + message_body: outbox.message.into(), + // message_body: std::iter::repeat(0x41).take(outbox.message_len).collect(), + }); + let outbox_instruction = Instruction { + program_id: outbox.program_id, + data: ixn.into_instruction_data().unwrap(), + accounts: vec![ + AccountMeta::new(outbox_account, false), + AccountMeta::new_readonly(ctx.payer.pubkey(), true), + AccountMeta::new_readonly(spl_noop::id(), false), + ], + }; + ctx.instructions.push(outbox_instruction); + ctx.send_transaction(&[&ctx.payer]); + } + MailboxSubCmd::Receive(inbox) => { + // TODO this probably needs some love + + let (inbox_account, _inbox_bump) = + Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), &inbox.program_id); + let hyperlane_message = HyperlaneMessage { + version: VERSION, + nonce: inbox.nonce, + origin: inbox.origin, + sender: H256::repeat_byte(123), + destination: inbox.local_domain, + recipient: H256::from(inbox.recipient.to_bytes()), + body: inbox.message.bytes().collect(), + }; + let mut encoded_message = vec![]; + hyperlane_message.write_to(&mut encoded_message).unwrap(); + let metadata = ExampleMetadata { + root: Default::default(), + index: 1, + leaf_index: 0, + // proof: Default::default(), + signatures: vec![], + }; + let mut encoded_metadata = vec![]; + metadata.write_to(&mut encoded_metadata).unwrap(); + + let ixn = MailboxInstruction::InboxProcess(InboxProcess { + metadata: encoded_metadata, + message: encoded_message, + }); + let inbox_instruction = Instruction { + program_id: inbox.program_id, + data: ixn.into_instruction_data().unwrap(), + accounts: vec![ + AccountMeta::new(inbox_account, false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(inbox.ism, false), + AccountMeta::new_readonly(inbox.recipient, false), + // Note: we would have to provide ism accounts and recipient accounts here if + // they were to use other accounts. + ], + }; + ctx.instructions.push(inbox_instruction); + ctx.send_transaction(&[&ctx.payer]); + } + MailboxSubCmd::Delivered(delivered) => { + let (processed_message_account_key, _processed_message_account_bump) = + Pubkey::find_program_address( + mailbox_processed_message_pda_seeds!(delivered.message_id), + &delivered.program_id, + ); + let account = ctx + .client + .get_account_with_commitment(&processed_message_account_key, ctx.commitment) + .unwrap() + .value; + if account.is_none() { + println!("Message not delivered"); + } else { + println!("Message delivered"); + } + } + }; +} + +fn process_token_cmd(mut ctx: Context, cmd: TokenCmd) { + match cmd.cmd { + TokenSubCmd::Init(init) => { + let (token_account, token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &init.program_id); + let (dispatch_authority_account, _dispatch_authority_bump) = + Pubkey::find_program_address( + mailbox_message_dispatch_authority_pda_seeds!(), + &init.program_id, + ); + + let ixn = HtInstruction::Init(HtInit { + mailbox: init.mailbox, + interchain_security_module: init.interchain_security_module, + decimals: init.decimals, + remote_decimals: init.remote_decimals, + }); + + // Accounts: + // 0. [executable] The system program. + // 1. [writable] The token PDA account. + // 2. [writable] The dispatch authority. + // 3. [signer] The payer. + // 4..N [??..??] Plugin-specific accounts. + let mut accounts = vec![ + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(token_account, false), + AccountMeta::new(dispatch_authority_account, false), + AccountMeta::new(ctx.payer.pubkey(), true), + ]; + + match init.token_type { + TokenType::Native => { + let (native_collateral_account, native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + &init.program_id, + ); + accounts.push(AccountMeta::new(native_collateral_account, false)); + + println!( + "native_collateral_account (key, bump)=({}, {})", + native_collateral_account, native_collateral_bump, + ); + } + TokenType::Synthetic => { + let (mint_account, mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds!(), + &init.program_id, + ); + accounts.push(AccountMeta::new(mint_account, false)); + println!("mint_account (key, bump)=({}, {})", mint_account, mint_bump,); + + let (ata_payer_account, ata_payer_bump) = Pubkey::find_program_address( + hyperlane_token_ata_payer_pda_seeds!(), + &init.program_id, + ); + accounts.push(AccountMeta::new(ata_payer_account, false)); + println!( + "ata_payer_account (key, bump)=({}, {})", + ata_payer_account, ata_payer_bump, + ); + } + TokenType::Collateral => { + // TODO implement this - for now, the warp route deployment tooling is sufficient + unimplemented!() + } + } + + println!("init.program_id {}", init.program_id); + + let init_instruction = Instruction { + program_id: init.program_id, + data: ixn.encode().unwrap(), + accounts, + }; + ctx.instructions.push(init_instruction); + + if init.token_type == TokenType::Synthetic { + let (mint_account, _mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds!(), + &init.program_id, + ); + ctx.instructions.push( + spl_token_2022::instruction::initialize_mint2( + &spl_token_2022::id(), + &mint_account, + &mint_account, + None, + init.decimals, + ) + .unwrap(), + ); + + let (ata_payer_account, _ata_payer_bump) = Pubkey::find_program_address( + hyperlane_token_ata_payer_pda_seeds!(), + &init.program_id, + ); + ctx.instructions + .push(solana_program::system_instruction::transfer( + &ctx.payer.pubkey(), + &ata_payer_account, + 1000000000, + )); + } + + ctx.send_transaction(&[&ctx.payer]); + + println!( + "hyperlane_token (key, bump) =({}, {})", + token_account, token_bump + ); + } + TokenSubCmd::Query(query) => { + let (token_account, token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &query.program_id); + + let mut accounts_to_query = vec![token_account]; + + match query.token_type { + TokenType::Native => { + let (native_collateral_account, _native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + &query.program_id, + ); + accounts_to_query.push(native_collateral_account); + } + TokenType::Synthetic => { + let (mint_account, _mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds!(), + &query.program_id, + ); + let (ata_payer_account, _ata_payer_bump) = Pubkey::find_program_address( + hyperlane_token_ata_payer_pda_seeds!(), + &query.program_id, + ); + accounts_to_query.push(mint_account); + accounts_to_query.push(ata_payer_account); + } + TokenType::Collateral => { + let (escrow_account, _escrow_bump) = Pubkey::find_program_address( + hyperlane_token_escrow_pda_seeds!(), + &query.program_id, + ); + accounts_to_query.push(escrow_account); + } + } + + let accounts = ctx + .client + .get_multiple_accounts_with_commitment(&accounts_to_query, ctx.commitment) + .unwrap() + .value; + println!("hyperlane-sealevel-token={}", query.program_id); + println!("--------------------------------"); + println!( + "Hyperlane Token Storage: {}, bump={}", + token_account, token_bump + ); + if let Some(info) = &accounts[0] { + println!("{:#?}", info); + + match query.token_type { + TokenType::Native => { + match HyperlaneTokenAccount::::fetch(&mut info.data.as_ref()) + { + Ok(token) => println!("{:#?}", token.into_inner()), + Err(err) => println!("Failed to deserialize account data: {}", err), + } + } + TokenType::Synthetic => { + match HyperlaneTokenAccount::::fetch( + &mut info.data.as_ref(), + ) { + Ok(token) => println!("{:#?}", token.into_inner()), + Err(err) => println!("Failed to deserialize account data: {}", err), + } + } + TokenType::Collateral => { + match HyperlaneTokenAccount::::fetch( + &mut info.data.as_ref(), + ) { + Ok(token) => println!("{:#?}", token.into_inner()), + Err(err) => println!("Failed to deserialize account data: {}", err), + } + } + } + } else { + println!("Not yet created?"); + } + println!("--------------------------------"); + + match query.token_type { + TokenType::Native => { + let (native_collateral_account, native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + &query.program_id, + ); + println!( + "Native Token Collateral: {}, bump={}", + native_collateral_account, native_collateral_bump + ); + if let Some(info) = &accounts[1] { + println!("{:#?}", info); + } else { + println!("Not yet created?"); + } + println!("--------------------------------"); + } + TokenType::Synthetic => { + let (mint_account, mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds!(), + &query.program_id, + ); + println!( + "Mint / Mint Authority: {}, bump={}", + mint_account, mint_bump + ); + if let Some(info) = &accounts[1] { + println!("{:#?}", info); + use solana_program::program_pack::Pack as _; + match spl_token_2022::state::Mint::unpack_from_slice(info.data.as_ref()) { + Ok(mint) => println!("{:#?}", mint), + Err(err) => println!("Failed to deserialize account data: {}", err), + } + } else { + println!("Not yet created?"); + } + + let (ata_payer_account, ata_payer_bump) = Pubkey::find_program_address( + hyperlane_token_ata_payer_pda_seeds!(), + &query.program_id, + ); + println!( + "ATA payer account: {}, bump={}", + ata_payer_account, ata_payer_bump, + ); + } + TokenType::Collateral => { + let (escrow_account, escrow_bump) = Pubkey::find_program_address( + hyperlane_token_escrow_pda_seeds!(), + &query.program_id, + ); + + println!( + "escrow_account (key, bump)=({}, {})", + escrow_account, escrow_bump, + ); + } + } + } + TokenSubCmd::TransferRemote(xfer) => { + is_keypair(&xfer.sender).unwrap(); + let sender = read_keypair_file(xfer.sender).unwrap(); + + let recipient = if xfer.recipient.starts_with("0x") { + H256::from_str(&xfer.recipient).unwrap() + } else { + let pubkey = Pubkey::from_str(&xfer.recipient).unwrap(); + H256::from_slice(&pubkey.to_bytes()[..]) + }; + + let (token_account, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &xfer.program_id); + let (dispatch_authority_account, _dispatch_authority_bump) = + Pubkey::find_program_address( + mailbox_message_dispatch_authority_pda_seeds!(), + &xfer.program_id, + ); + + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_account, _dispatched_message_bump) = + Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &xfer.mailbox, + ); + + let (mailbox_outbox_account, _mailbox_outbox_bump) = + Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), &xfer.mailbox); + + let ixn = HtInstruction::TransferRemote(HtTransferRemote { + destination_domain: xfer.destination_domain, + recipient, + amount_or_id: xfer.amount.into(), + }); + + // Transfers tokens to a remote. + // Burns the tokens from the sender's associated token account and + // then dispatches a message to the remote recipient. + // + // 0. [executable] The system program. + // 0. [executable] The spl_noop program. + // 1. [] The token PDA account. + // 2. [executable] The mailbox program. + // 3. [writeable] The mailbox outbox account. + // 4. [] Message dispatch authority. + // 5. [writeable,signer] The token sender and mailbox payer. + // 6. [signer] Unique message account. + // 7. [writeable] Message storage PDA. + let mut accounts = vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(token_account, false), + AccountMeta::new_readonly(xfer.mailbox, false), + AccountMeta::new(mailbox_outbox_account, false), + AccountMeta::new_readonly(dispatch_authority_account, false), + AccountMeta::new(sender.pubkey(), true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_account, false), + ]; + + match xfer.token_type { + TokenType::Native => { + // 5. [executable] The system program. + // 6. [writeable] The native token collateral PDA account. + let (native_collateral_account, _native_collateral_bump) = + Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + &xfer.program_id, + ); + accounts.extend([ + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(native_collateral_account, false), + ]); + } + TokenType::Synthetic => { + // 5. [executable] The spl_token_2022 program. + // 6. [writeable] The mint / mint authority PDA account. + // 7. [writeable] The token sender's associated token account, from which tokens will be burned. + let (mint_account, _mint_bump) = Pubkey::find_program_address( + hyperlane_token_mint_pda_seeds!(), + &xfer.program_id, + ); + let sender_associated_token_account = + get_associated_token_address_with_program_id( + &sender.pubkey(), + &mint_account, + &spl_token_2022::id(), + ); + accounts.extend([ + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(mint_account, false), + AccountMeta::new(sender_associated_token_account, false), + ]); + } + TokenType::Collateral => { + // 5. [executable] The SPL token program for the mint. + // 6. [writeable] The mint. + // 7. [writeable] The token sender's associated token account, from which tokens will be sent. + // 8. [writeable] The escrow PDA account. + let fetched_token_account = ctx + .client + .get_account_with_commitment(&token_account, ctx.commitment) + .unwrap() + .value + .unwrap(); + let token = HyperlaneTokenAccount::::fetch( + &mut &fetched_token_account.data[..], + ) + .unwrap() + .into_inner(); + + let sender_associated_token_account = + get_associated_token_address_with_program_id( + &sender.pubkey(), + &token.plugin_data.mint, + &token.plugin_data.spl_token_program, + ); + accounts.extend([ + AccountMeta::new_readonly(token.plugin_data.spl_token_program, false), + AccountMeta::new(token.plugin_data.mint, false), + AccountMeta::new(sender_associated_token_account, false), + AccountMeta::new(token.plugin_data.escrow, false), + ]); + } + } + + eprintln!("accounts={:#?}", accounts); // FIXME remove + let xfer_instruction = Instruction { + program_id: xfer.program_id, + data: ixn.encode().unwrap(), + accounts, + }; + ctx.instructions.push(xfer_instruction); + + ctx.send_transaction(&[&ctx.payer, &sender, &unique_message_account_keypair]); + } + TokenSubCmd::EnrollRemoteRouter(enroll) => { + let enroll_instruction = HtInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain: enroll.domain, + router: enroll.router.into(), + }); + let (token_account, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &enroll.program_id); + + let instruction = Instruction { + program_id: enroll.program_id, + data: enroll_instruction.encode().unwrap(), + accounts: vec![ + AccountMeta::new(token_account, false), + AccountMeta::new_readonly(ctx.payer.pubkey(), true), + ], + }; + ctx.instructions.push(instruction); + + ctx.send_transaction(&[&ctx.payer]); + } + } +} + +fn process_validator_announce_cmd(mut ctx: Context, cmd: ValidatorAnnounceCmd) { + match cmd.cmd { + ValidatorAnnounceSubCmd::Init(init) => { + let init_instruction = + hyperlane_sealevel_validator_announce::instruction::init_instruction( + init.program_id, + ctx.payer.pubkey(), + init.mailbox_id, + init.local_domain, + ) + .unwrap(); + ctx.instructions.push(init_instruction); + + ctx.send_transaction(&[&ctx.payer]); + } + ValidatorAnnounceSubCmd::Announce(announce) => { + let signature = hex::decode(if announce.signature.starts_with("0x") { + &announce.signature[2..] + } else { + &announce.signature + }) + .unwrap(); + + let announce_instruction = ValidatorAnnounceAnnounceInstruction { + validator: announce.validator, + storage_location: announce.storage_location, + signature, + }; + + let (validator_announce_account, _validator_announce_bump) = + Pubkey::find_program_address(validator_announce_pda_seeds!(), &announce.program_id); + + let (validator_storage_locations_key, _validator_storage_locations_bump_seed) = + Pubkey::find_program_address( + validator_storage_locations_pda_seeds!(announce.validator), + &announce.program_id, + ); + + let replay_id = announce_instruction.replay_id(); + let (replay_protection_pda_key, _replay_protection_bump_seed) = + Pubkey::find_program_address( + replay_protection_pda_seeds!(replay_id), + &announce.program_id, + ); + + let ixn = ValidatorAnnounceInstruction::Announce(announce_instruction); + + // Accounts: + // 0. [signer] The payer. + // 1. [executable] The system program. + // 2. [] The ValidatorAnnounce PDA account. + // 3. [writeable] The validator-specific ValidatorStorageLocationsAccount PDA account. + // 4. [writeable] The ReplayProtection PDA account specific to the announcement being made. + let accounts = vec![ + AccountMeta::new_readonly(ctx.payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(validator_announce_account, false), + AccountMeta::new(validator_storage_locations_key, false), + AccountMeta::new(replay_protection_pda_key, false), + ]; + + let announce_instruction = Instruction { + program_id: announce.program_id, + data: ixn.into_instruction_data().unwrap(), + accounts, + }; + ctx.instructions.push(announce_instruction); + + ctx.send_transaction(&[&ctx.payer]); + } + ValidatorAnnounceSubCmd::Query(query) => { + let (validator_storage_locations_key, _validator_storage_locations_bump_seed) = + Pubkey::find_program_address( + validator_storage_locations_pda_seeds!(query.validator), + &query.program_id, + ); + + let account = ctx + .client + .get_account_with_commitment(&validator_storage_locations_key, ctx.commitment) + .unwrap() + .value; + if let Some(account) = account { + let validator_storage_locations = + ValidatorStorageLocationsAccount::fetch(&mut &account.data[..]) + .unwrap() + .into_inner(); + println!( + "Validator {} storage locations:\n{:#?}", + query.validator, validator_storage_locations + ); + } else { + println!("Validator not yet announced"); + } + } + } +} + +fn process_multisig_ism_message_id_cmd(mut ctx: Context, cmd: MultisigIsmMessageIdCmd) { + match cmd.cmd { + MultisigIsmMessageIdSubCmd::Init(init) => { + let init_instruction = + hyperlane_sealevel_multisig_ism_message_id::instruction::init_instruction( + init.program_id, + ctx.payer.pubkey(), + ) + .unwrap(); + ctx.instructions.push(init_instruction); + + ctx.send_transaction(&[&ctx.payer]); + } + MultisigIsmMessageIdSubCmd::SetValidatorsAndThreshold(set_config) => { + let (access_control_pda_key, _access_control_pda_bump) = Pubkey::find_program_address( + multisig_ism_message_id_access_control_pda_seeds!(), + &set_config.program_id, + ); + + let (domain_data_pda_key, _domain_data_pda_bump) = Pubkey::find_program_address( + multisig_ism_message_id_domain_data_pda_seeds!(set_config.domain), + &set_config.program_id, + ); + + let ixn = MultisigIsmMessageIdInstruction::SetValidatorsAndThreshold(Domained { + domain: set_config.domain, + data: ValidatorsAndThreshold { + validators: set_config.validators, + threshold: set_config.threshold, + }, + }); + + // Accounts: + // 0. `[signer]` The access control owner and payer of the domain PDA. + // 1. `[]` The access control PDA account. + // 2. `[writable]` The PDA relating to the provided domain. + // 3. `[executable]` OPTIONAL - The system program account. Required if creating the domain PDA. + let accounts = vec![ + AccountMeta::new(ctx.payer.pubkey(), true), + AccountMeta::new_readonly(access_control_pda_key, false), + AccountMeta::new(domain_data_pda_key, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let set_instruction = Instruction { + program_id: set_config.program_id, + data: ixn.encode().unwrap(), + accounts, + }; + ctx.instructions.push(set_instruction); + + ctx.send_transaction(&[&ctx.payer]); + } + MultisigIsmMessageIdSubCmd::Query(query) => { + let (access_control_pda_key, _access_control_pda_bump) = Pubkey::find_program_address( + multisig_ism_message_id_access_control_pda_seeds!(), + &query.program_id, + ); + + let accounts = ctx + .client + .get_multiple_accounts_with_commitment(&[access_control_pda_key], ctx.commitment) + .unwrap() + .value; + let access_control = + AccessControlAccount::fetch(&mut &accounts[0].as_ref().unwrap().data[..]) + .unwrap() + .into_inner(); + println!("Access control: {:#?}", access_control); + } + } +} diff --git a/rust/sealevel/client/src/warp_route.rs b/rust/sealevel/client/src/warp_route.rs new file mode 100644 index 0000000000..1d7d1926f7 --- /dev/null +++ b/rust/sealevel/client/src/warp_route.rs @@ -0,0 +1,566 @@ +use hyperlane_core::{utils::hex_or_base58_to_h256, H256}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fs::File, path::Path, str::FromStr}; + +use solana_client::{client_error::ClientError, rpc_client::RpcClient}; +use solana_program::program_error::ProgramError; +use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Signer}; + +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; + +use hyperlane_sealevel_token::{hyperlane_token_mint_pda_seeds, spl_token, spl_token_2022}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneTokenAccount, + hyperlane_token_pda_seeds, + instruction::{enroll_remote_routers_instruction, Init}, +}; + +use crate::{ + cmd_utils::{ + account_exists, create_and_write_keypair, create_new_directory, deploy_program_idempotent, + }, + core::{read_core_program_ids, CoreProgramIds}, + Context, WarpRouteCmd, WarpRouteSubCmd, +}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct DecimalMetadata { + decimals: u8, + remote_decimals: Option, +} + +impl DecimalMetadata { + fn remote_decimals(&self) -> u8 { + self.remote_decimals.unwrap_or(self.decimals) + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +enum TokenType { + Native, + Synthetic(TokenMetadata), + Collateral(CollateralInfo), +} + +impl TokenType { + fn program_name(&self) -> &str { + match self { + TokenType::Native => "hyperlane_sealevel_token_native", + TokenType::Synthetic(_) => "hyperlane_sealevel_token", + TokenType::Collateral(_) => "hyperlane_sealevel_token_collateral", + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct TokenMetadata { + name: String, + symbol: String, + total_supply: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +enum SplTokenProgramType { + Token, + Token2022, +} + +impl SplTokenProgramType { + fn program_id(&self) -> Pubkey { + match &self { + SplTokenProgramType::Token => spl_token::id(), + SplTokenProgramType::Token2022 => spl_token_2022::id(), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct CollateralInfo { + #[serde(rename = "token")] + mint: String, + spl_token_program: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct OptionalConnectionClientConfig { + mailbox: Option, + interchain_gas_paymaster: Option, + interchain_security_module: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct OptionalOwnableConfig { + owner: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct TokenConfig { + #[serde(flatten)] + token_type: TokenType, + foreign_deployment: Option, + #[serde(flatten)] + decimal_metadata: DecimalMetadata, + #[serde(flatten)] + ownable: OptionalOwnableConfig, + #[serde(flatten)] + connection_client: OptionalConnectionClientConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RpcUrlConfig { + pub http: String, +} + +/// An abridged version of the Typescript ChainMetadata +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ChainMetadata { + chain_id: u32, + /// Hyperlane domain, only required if differs from id above + domain_id: Option, + name: String, + /// Collection of RPC endpoints + public_rpc_urls: Vec, +} + +impl ChainMetadata { + fn client(&self) -> RpcClient { + RpcClient::new_with_commitment( + self.public_rpc_urls[0].http.clone(), + CommitmentConfig::confirmed(), + ) + } + + fn domain_id(&self) -> u32 { + self.domain_id.unwrap_or(self.chain_id) + } +} + +pub(crate) fn process_warp_route_cmd(mut ctx: Context, cmd: WarpRouteCmd) { + match cmd.cmd { + WarpRouteSubCmd::Deploy(deploy) => { + let token_config_file = File::open(deploy.token_config_file).unwrap(); + let token_configs: HashMap = + serde_json::from_reader(token_config_file).unwrap(); + + let chain_config_file = File::open(deploy.chain_config_file).unwrap(); + let chain_configs: HashMap = + serde_json::from_reader(chain_config_file).unwrap(); + + let environments_dir = + create_new_directory(&deploy.environments_dir, &deploy.environment); + + let artifacts_dir = create_new_directory(&environments_dir, "warp-routes"); + let warp_route_dir = create_new_directory(&artifacts_dir, &deploy.warp_route_name); + let keys_dir = create_new_directory(&warp_route_dir, "keys"); + + let foreign_deployments = token_configs + .iter() + .filter(|(_, token_config)| token_config.foreign_deployment.is_some()) + .map(|(chain_name, token_config)| { + let chain_config = chain_configs.get(chain_name).unwrap(); + ( + chain_config.domain_id(), + hex_or_base58_to_h256(token_config.foreign_deployment.as_ref().unwrap()) + .unwrap(), + ) + }) + .collect::>(); + + let mut routers: HashMap = foreign_deployments; + + let token_configs_to_deploy = token_configs + .into_iter() + .filter(|(_, token_config)| token_config.foreign_deployment.is_none()) + .collect::>(); + + // Deploy to chains that don't have a foreign deployment + for (chain_name, token_config) in token_configs_to_deploy.iter() { + let chain_config = chain_configs + .get(chain_name) + .unwrap_or_else(|| panic!("Chain config not found for chain: {}", chain_name)); + + if token_config.ownable.owner.is_some() { + println!("WARNING: Ownership transfer is not yet supported in this deploy tooling, ownership is granted to the payer account"); + } + + let program_id = deploy_warp_route( + &mut ctx, + &keys_dir, + &deploy.environments_dir, + &deploy.environment, + &deploy.built_so_dir, + chain_config, + token_config, + deploy.ata_payer_funding_amount, + ); + + routers.insert( + chain_config.domain_id(), + H256::from_slice(&program_id.to_bytes()[..]), + ); + } + + // Now enroll routers + for (chain_name, _) in token_configs_to_deploy { + let chain_config = chain_configs + .get(&chain_name) + .unwrap_or_else(|| panic!("Chain config not found for chain: {}", chain_name)); + + let domain_id = chain_config.domain_id(); + let program_id: Pubkey = + Pubkey::new_from_array(*routers.get(&domain_id).unwrap().as_fixed_bytes()); + + let enrolled_routers = get_routers(&chain_config.client(), &program_id).unwrap(); + + let expected_routers = routers + .iter() + .filter(|(router_domain_id, _)| *router_domain_id != &domain_id) + .map(|(domain, router)| { + ( + *domain, + RemoteRouterConfig { + domain: *domain, + router: Some(*router), + }, + ) + }) + .collect::>(); + + // Routers to enroll (or update to a Some value) + let routers_to_enroll = expected_routers + .iter() + .filter(|(domain, router_config)| { + enrolled_routers.get(domain) != router_config.router.as_ref() + }) + .map(|(_, router_config)| router_config.clone()); + + // Routers to remove + let routers_to_unenroll = enrolled_routers + .iter() + .filter(|(domain, _)| !expected_routers.contains_key(domain)) + .map(|(domain, _)| RemoteRouterConfig { + domain: *domain, + router: None, + }); + + // All router config changes + let router_configs = routers_to_enroll + .chain(routers_to_unenroll) + .collect::>(); + + if !router_configs.is_empty() { + println!( + "Enrolling routers for chain: {}, program_id {}, routers: {:?}", + chain_name, program_id, router_configs, + ); + + ctx.instructions.push( + enroll_remote_routers_instruction( + program_id, + ctx.payer.pubkey(), + router_configs, + ) + .unwrap(), + ); + ctx.send_transaction_with_client(&chain_config.client(), &[&ctx.payer]); + ctx.instructions.clear(); + } else { + println!( + "No router changes for chain: {}, program_id {}", + chain_name, program_id + ); + } + } + + let routers_by_name: HashMap = routers + .iter() + .map(|(domain_id, router)| { + ( + chain_configs + .iter() + .find(|(_, chain_config)| chain_config.domain_id() == *domain_id) + .unwrap() + .0 + .clone(), + *router, + ) + }) + .collect::>(); + write_program_ids(&warp_route_dir, &routers_by_name); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn deploy_warp_route( + ctx: &mut Context, + key_dir: &Path, + environments_dir: &Path, + environment: &str, + built_so_dir: &Path, + chain_config: &ChainMetadata, + token_config: &TokenConfig, + ata_payer_funding_amount: Option, +) -> Pubkey { + println!( + "Attempting deploy on chain: {}\nToken config: {:?}", + chain_config.name, token_config + ); + + let (keypair, keypair_path) = create_and_write_keypair( + key_dir, + format!( + "{}-{}.json", + token_config.token_type.program_name(), + chain_config.name + ) + .as_str(), + true, + ); + let program_id = keypair.pubkey(); + + deploy_program_idempotent( + &ctx.payer_path, + &keypair, + keypair_path.to_str().unwrap(), + built_so_dir + .join(format!("{}.so", token_config.token_type.program_name())) + .to_str() + .unwrap(), + &chain_config.public_rpc_urls[0].http, + // Not used + "/", + ) + .unwrap(); + + let core_program_ids = read_core_program_ids(environments_dir, environment, &chain_config.name); + init_warp_route_idempotent( + ctx, + &chain_config.client(), + &core_program_ids, + chain_config, + token_config, + program_id, + ata_payer_funding_amount, + ) + .unwrap(); + + match &token_config.token_type { + TokenType::Native => { + println!("Deploying native token"); + } + TokenType::Synthetic(_token_metadata) => { + println!("Deploying synthetic token"); + } + TokenType::Collateral(_collateral_info) => { + println!("Deploying collateral token"); + } + } + + program_id +} + +fn init_warp_route_idempotent( + ctx: &mut Context, + client: &RpcClient, + core_program_ids: &CoreProgramIds, + _chain_config: &ChainMetadata, + token_config: &TokenConfig, + program_id: Pubkey, + ata_payer_funding_amount: Option, +) -> Result<(), ProgramError> { + let (token_pda, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), &program_id); + + if let Some(ata_payer_funding_amount) = ata_payer_funding_amount { + if matches!( + token_config.token_type, + TokenType::Collateral(_) | TokenType::Synthetic(_) + ) { + fund_ata_payer_up_to(ctx, client, program_id, ata_payer_funding_amount); + } + } + + if account_exists(client, &token_pda).unwrap() { + println!("Token PDA already exists, skipping init"); + return Ok(()); + } + + init_warp_route( + ctx, + client, + core_program_ids, + _chain_config, + token_config, + program_id, + ) +} + +fn fund_ata_payer_up_to( + ctx: &mut Context, + client: &RpcClient, + program_id: Pubkey, + ata_payer_funding_amount: u64, +) { + let (ata_payer_account, _ata_payer_bump) = Pubkey::find_program_address( + hyperlane_sealevel_token::hyperlane_token_ata_payer_pda_seeds!(), + &program_id, + ); + + let current_balance = client.get_balance(&ata_payer_account).unwrap(); + + let funding_amount = ata_payer_funding_amount.saturating_sub(current_balance); + + if funding_amount == 0 { + println!("ATA payer fully funded with balance of {}", current_balance); + return; + } + + println!( + "Funding ATA payer {} with funding_amount {} to reach total balance of {}", + ata_payer_account, funding_amount, ata_payer_funding_amount + ); + ctx.instructions + .push(solana_program::system_instruction::transfer( + &ctx.payer.pubkey(), + &ata_payer_account, + funding_amount, + )); + ctx.send_transaction_with_client(client, &[&ctx.payer]); + ctx.instructions.clear(); +} + +fn init_warp_route( + ctx: &mut Context, + client: &RpcClient, + core_program_ids: &CoreProgramIds, + _chain_config: &ChainMetadata, + token_config: &TokenConfig, + program_id: Pubkey, +) -> Result<(), ProgramError> { + // If the Mailbox was provided as configuration, use that. Otherwise, default to + // the Mailbox found in the core program ids. + let mailbox = token_config + .connection_client + .mailbox + .as_ref() + .map(|s| Pubkey::from_str(s).unwrap()) + .unwrap_or(core_program_ids.mailbox); + + let init = Init { + mailbox, + interchain_security_module: token_config + .connection_client + .interchain_security_module + .as_ref() + .map(|s| Pubkey::from_str(s).unwrap()), + decimals: token_config.decimal_metadata.decimals, + remote_decimals: token_config.decimal_metadata.remote_decimals(), + }; + + let mut init_instructions = match &token_config.token_type { + TokenType::Native => vec![ + hyperlane_sealevel_token_native::instruction::init_instruction( + program_id, + ctx.payer.pubkey(), + init, + )?, + ], + TokenType::Synthetic(_token_metadata) => { + let decimals = init.decimals; + + let mut instructions = vec![hyperlane_sealevel_token::instruction::init_instruction( + program_id, + ctx.payer.pubkey(), + init, + )?]; + + let (mint_account, _mint_bump) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), &program_id); + // TODO: Also set Metaplex metadata? + instructions.push( + spl_token_2022::instruction::initialize_mint2( + &spl_token_2022::id(), + &mint_account, + &mint_account, + None, + decimals, + ) + .unwrap(), + ); + + instructions + } + TokenType::Collateral(collateral_info) => { + vec![ + hyperlane_sealevel_token_collateral::instruction::init_instruction( + program_id, + ctx.payer.pubkey(), + init, + collateral_info + .spl_token_program + .as_ref() + .expect("Cannot initalize collateral warp route without SPL token program") + .program_id(), + collateral_info.mint.parse().expect("Invalid mint address"), + )?, + ] + } + }; + + ctx.instructions.append(&mut init_instructions); + ctx.send_transaction_with_client(client, &[&ctx.payer]); + ctx.instructions.clear(); + + Ok(()) +} + +fn get_routers( + client: &RpcClient, + token_program_id: &Pubkey, +) -> Result, ClientError> { + let (token_pda, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), token_program_id); + + let account = client.get_account(&token_pda)?; + let token_data = HyperlaneTokenAccount::<()>::fetch(&mut &account.data[..]) + .unwrap() + .into_inner(); + + Ok(token_data.remote_routers) +} + +#[derive(Serialize, Deserialize)] +struct SerializedProgramId { + hex: String, + base58: String, +} + +fn write_program_ids(warp_route_dir: &Path, routers: &HashMap) { + let serialized_program_ids = routers + .iter() + .map(|(chain_name, router)| { + ( + chain_name.clone(), + SerializedProgramId { + hex: format!("0x{}", hex::encode(router)), + base58: Pubkey::new_from_array(router.to_fixed_bytes()).to_string(), + }, + ) + }) + .collect::>(); + + let program_ids_file = warp_route_dir.join("program-ids.json"); + let program_ids_file = File::create(program_ids_file).unwrap(); + serde_json::to_writer_pretty(program_ids_file, &serialized_program_ids).unwrap(); +} diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 0000000000..484e2fb182 --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[113,244,152,170,85,122,42,51,10,74,244,18,91,8,135,77,156,19,172,122,139,50,248,3,186,184,186,140,110,165,78,161,76,88,146,213,185,127,121,92,132,2,249,73,19,192,73,170,105,85,247,241,48,175,67,28,165,29,224,252,173,165,38,140] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 0000000000..243fcbd9ad --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[135,153,145,193,50,88,169,205,206,171,48,1,17,242,3,43,225,72,101,163,93,126,105,165,159,44,243,196,182,240,4,87,22,253,47,198,217,75,23,60,181,129,251,103,140,170,111,35,152,97,16,23,64,17,198,239,79,225,120,141,55,38,60,86] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 0000000000..3428c9c497 --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[252,76,67,201,250,68,86,32,216,136,163,46,192,20,249,175,209,94,101,235,24,240,204,4,246,159,180,138,253,20,48,146,182,104,250,124,231,168,239,248,95,199,219,250,126,156,57,113,83,209,232,171,10,90,153,238,72,138,186,34,77,87,172,211] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet/core/program-ids.json b/rust/sealevel/environments/devnet/solanadevnet/core/program-ids.json new file mode 100644 index 0000000000..a9acf6dfd7 --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet/core/program-ids.json @@ -0,0 +1,5 @@ +{ + "mailbox": "692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1", + "validator_announce": "DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn", + "multisig_ism_message_id": "2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw" +} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 0000000000..31a9bdeb37 --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[6,132,236,247,134,194,48,96,56,63,48,146,121,215,228,1,80,199,79,128,232,145,31,24,170,246,162,253,52,12,244,198,141,84,17,12,114,138,14,65,37,185,65,155,156,209,188,73,159,63,157,69,158,103,155,16,217,78,19,53,6,226,115,117] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 0000000000..4e8c405ce3 --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[75,122,156,10,143,146,130,96,41,203,245,228,178,140,170,105,167,226,18,171,187,4,70,210,1,234,232,194,206,26,65,248,243,199,245,54,127,196,31,152,114,133,16,172,1,103,105,249,111,240,129,216,26,184,14,131,242,197,189,46,163,142,2,120] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 0000000000..153a856cec --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet1/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[60,166,246,212,217,15,197,101,188,59,172,187,217,44,158,58,65,180,5,179,193,73,206,199,134,54,56,70,26,169,141,82,49,9,182,63,146,255,211,243,158,55,120,3,60,23,151,134,195,85,195,50,62,205,7,162,107,106,40,106,220,117,82,91] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/solanadevnet1/core/program-ids.json b/rust/sealevel/environments/devnet/solanadevnet1/core/program-ids.json new file mode 100644 index 0000000000..105a160e39 --- /dev/null +++ b/rust/sealevel/environments/devnet/solanadevnet1/core/program-ids.json @@ -0,0 +1,5 @@ +{ + "mailbox": "AWgqPcY1vjHRoFLHNgs15fdvy4bqEakHmYXW78B8GgYk", + "validator_announce": "4JRZrYJnXJn6KPSCG4tA6GBomP2zwQv8bD65anWnHmNz", + "multisig_ism_message_id": "HQcv2ibNRuJdHU8Lt9t655YUjXj4Rp9nW8mbcA26cYqM" +} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/chain-config.json b/rust/sealevel/environments/devnet/warp-routes/chain-config.json new file mode 100644 index 0000000000..96691fdff5 --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/chain-config.json @@ -0,0 +1,29 @@ +{ + "solanadevnet": { + "chainId": 13375, + "name": "solanadevnet", + "publicRpcUrls": [ + { + "http": "https://api.devnet.solana.com" + } + ] + }, + "solanadevnet1": { + "chainId": 13376, + "name": "solanadevnet1", + "publicRpcUrls": [ + { + "http": "https://api.devnet.solana.com" + } + ] + }, + "fuji": { + "chainId": 43113, + "name": "fuji", + "publicRpcUrls": [ + { + "http": "https://api.avax-test.network/ext/bc/C/rpc" + } + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token-solanadevnet1.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token-solanadevnet1.json new file mode 100644 index 0000000000..7ef8d00db3 --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token-solanadevnet1.json @@ -0,0 +1 @@ +[234,24,62,69,201,85,23,105,162,73,39,96,54,24,252,131,65,61,204,71,240,230,98,153,79,12,102,57,135,254,59,159,71,62,216,28,221,183,176,75,40,167,248,151,145,3,242,74,196,153,147,167,98,202,124,87,70,27,115,81,78,50,199,68] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json new file mode 100644 index 0000000000..5bd77b5616 --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json @@ -0,0 +1 @@ +[39,54,74,37,67,213,195,70,204,38,146,230,111,25,95,162,197,128,223,145,57,112,78,217,51,236,68,252,254,70,26,37,135,224,112,242,167,101,9,162,147,37,98,70,138,147,6,126,136,247,145,107,228,139,68,251,82,120,107,18,4,102,190,221] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/program-ids.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/program-ids.json new file mode 100644 index 0000000000..0a7a1eab0a --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/collateraltest/program-ids.json @@ -0,0 +1,14 @@ +{ + "solanadevnet1": { + "hex": "0x473ed81cddb7b04b28a7f8979103f24ac49993a762ca7c57461b73514e32c744", + "base58": "5o7XXLy8N67cgCjSt4zKNnzAbDRpkVruu8BbpPNehBrK" + }, + "fuji": { + "hex": "0x000000000000000000000000b3af04fc8b461138eca4f5fc1d5955bbe6d20fca", + "base58": "1111111111113WC5zqqJzmcsiZZcai6ZxbvW84do" + }, + "solanadevnet": { + "hex": "0x87e070f2a76509a2932562468a93067e88f7916be48b44fb52786b120466bedd", + "base58": "A9QY6ZQ3t1T3Pk58gTx1vsSeH2B2AywwK2V7SpH2w2cC" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/collateraltest/token-config.json b/rust/sealevel/environments/devnet/warp-routes/collateraltest/token-config.json new file mode 100644 index 0000000000..d63f7a11e2 --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/collateraltest/token-config.json @@ -0,0 +1,23 @@ +{ + "solanadevnet": { + "type": "collateral", + "token": "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr", + "splTokenProgram": "token", + "decimals": 6, + "name": "USD Coin Dev", + "symbol": "USDC" + }, + "solanadevnet1": { + "type": "synthetic", + "decimals": 6, + "name": "USD Coin Dev", + "symbol": "USDC" + }, + "fuji": { + "type": "synthetic", + "decimals": 6, + "name": "USD Coin Dev", + "symbol": "USDC", + "foreignDeployment": "0xb3AF04Fc8b461138eCA4F5fC1D5955Bbe6D20Fca" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token-solanadevnet1.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token-solanadevnet1.json new file mode 100644 index 0000000000..85b4ecd615 --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token-solanadevnet1.json @@ -0,0 +1 @@ +[108,3,96,252,94,52,97,146,193,61,252,4,209,156,21,178,42,234,170,70,97,252,167,156,146,209,57,48,52,224,211,72,40,108,141,225,165,106,19,22,48,134,115,13,111,173,228,116,229,5,197,167,246,245,139,19,60,75,183,152,49,124,190,33] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json new file mode 100644 index 0000000000..b9d79c759d --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json @@ -0,0 +1 @@ +[169,25,77,166,171,9,74,84,180,104,209,80,36,170,223,85,56,255,50,104,185,250,53,188,65,168,235,7,176,81,99,182,113,69,170,191,248,224,191,67,181,13,107,166,133,126,157,101,165,157,24,202,25,96,195,132,107,100,86,78,48,232,7,142] \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/program-ids.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/program-ids.json new file mode 100644 index 0000000000..811f6d7ecd --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/nativetest/program-ids.json @@ -0,0 +1,14 @@ +{ + "solanadevnet": { + "hex": "0x7145aabff8e0bf43b50d6ba6857e9d65a59d18ca1960c3846b64564e30e8078e", + "base58": "8dAgevgBAnhvxoB5mWNUfmXi6H8WLC3ZaP8poaRHkzaR" + }, + "solanadevnet1": { + "hex": "0x286c8de1a56a13163086730d6fade474e505c5a7f6f58b133c4bb798317cbe21", + "base58": "3ioKCgR4pyrtkJvsB3zketopnR3mqjjBszSgtiQXQz7i" + }, + "fuji": { + "hex": "0x00000000000000000000000011cf63c916263d6bbd710f43816ee6703e1c5da3", + "base58": "111111111111FPh8wLMbV6vLtqUcvxKR496PAXL" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/devnet/warp-routes/nativetest/token-config.json b/rust/sealevel/environments/devnet/warp-routes/nativetest/token-config.json new file mode 100644 index 0000000000..712b1f31a8 --- /dev/null +++ b/rust/sealevel/environments/devnet/warp-routes/nativetest/token-config.json @@ -0,0 +1,19 @@ +{ + "solanadevnet": { + "type": "native", + "decimals": 9 + }, + "solanadevnet1": { + "type": "synthetic", + "decimals": 9, + "name": "Solana (solanadevnet)", + "symbol": "SOL" + }, + "fuji": { + "type": "synthetic", + "decimals": 9, + "name": "Solana (solanadevnet)", + "symbol": "SOL", + "foreignDeployment": "0x11CF63c916263d6BBD710F43816ee6703e1C5da3" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 0000000000..484e2fb182 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[113,244,152,170,85,122,42,51,10,74,244,18,91,8,135,77,156,19,172,122,139,50,248,3,186,184,186,140,110,165,78,161,76,88,146,213,185,127,121,92,132,2,249,73,19,192,73,170,105,85,247,241,48,175,67,28,165,29,224,252,173,165,38,140] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 0000000000..243fcbd9ad --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[135,153,145,193,50,88,169,205,206,171,48,1,17,242,3,43,225,72,101,163,93,126,105,165,159,44,243,196,182,240,4,87,22,253,47,198,217,75,23,60,181,129,251,103,140,170,111,35,152,97,16,23,64,17,198,239,79,225,120,141,55,38,60,86] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 0000000000..3428c9c497 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest1/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[252,76,67,201,250,68,86,32,216,136,163,46,192,20,249,175,209,94,101,235,24,240,204,4,246,159,180,138,253,20,48,146,182,104,250,124,231,168,239,248,95,199,219,250,126,156,57,113,83,209,232,171,10,90,153,238,72,138,186,34,77,87,172,211] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest1/core/program-ids.json b/rust/sealevel/environments/local-e2e/sealeveltest1/core/program-ids.json new file mode 100644 index 0000000000..a9acf6dfd7 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest1/core/program-ids.json @@ -0,0 +1,5 @@ +{ + "mailbox": "692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1", + "validator_announce": "DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn", + "multisig_ism_message_id": "2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw" +} \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 0000000000..5d17fc1124 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[222,255,22,134,94,105,56,122,208,160,175,127,249,7,9,61,12,110,163,100,255,114,133,62,171,49,222,193,39,231,136,249,131,251,23,52,193,139,17,122,124,153,164,193,162,233,9,87,24,40,187,244,129,39,39,8,62,191,198,138,8,53,251,70] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 0000000000..ee3021ce31 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[119,156,99,147,184,17,168,119,174,254,129,203,223,83,51,35,99,182,15,15,58,156,141,241,245,169,72,101,143,147,15,8,50,213,208,201,12,135,40,197,122,87,89,157,235,179,146,95,204,46,89,252,205,83,58,61,205,33,83,138,54,25,23,176] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 0000000000..e15794f271 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest2/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[189,70,127,148,71,67,60,187,176,28,129,66,59,243,228,106,194,143,14,238,72,50,253,210,16,179,234,160,154,50,131,9,36,214,41,0,180,6,245,233,254,165,69,146,82,0,244,49,220,233,60,78,63,235,177,253,34,149,232,214,43,170,124,38] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/sealeveltest2/core/program-ids.json b/rust/sealevel/environments/local-e2e/sealeveltest2/core/program-ids.json new file mode 100644 index 0000000000..6ab1ce88ca --- /dev/null +++ b/rust/sealevel/environments/local-e2e/sealeveltest2/core/program-ids.json @@ -0,0 +1,5 @@ +{ + "mailbox": "9tCUWNjpqcf3NUSrtp7vquYVCwbEByvLjZUrhG5dgvhj", + "validator_announce": "3Uo5j2Bti9aZtrDqJmAyuwiFaJFPFoNL5yxTpVCNcUhb", + "multisig_ism_message_id": "4RSV6iyqW9X66Xq3RDCVsKJ7hMba5uv6XP8ttgxjVUB1" +} \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/warp-routes/chain-config.json b/rust/sealevel/environments/local-e2e/warp-routes/chain-config.json new file mode 100644 index 0000000000..5914c12d6a --- /dev/null +++ b/rust/sealevel/environments/local-e2e/warp-routes/chain-config.json @@ -0,0 +1,20 @@ +{ + "sealeveltest1": { + "chainId": 13375, + "name": "sealeveltest1", + "publicRpcUrls": [ + { + "http": "http://localhost:8899" + } + ] + }, + "sealeveltest2": { + "chainId": 13376, + "name": "sealeveltest2", + "publicRpcUrls": [ + { + "http": "http://localhost:8899" + } + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2.json b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2.json new file mode 100644 index 0000000000..5bd74ba372 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token-sealeveltest2.json @@ -0,0 +1 @@ +[150,184,67,120,68,103,211,31,174,166,80,126,38,201,166,221,187,186,138,146,47,236,182,38,17,22,202,64,143,124,229,151,35,23,249,97,93,78,188,36,25,173,75,136,88,14,42,128,160,59,44,122,96,188,150,13,231,214,147,77,188,55,168,126] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1.json b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1.json new file mode 100644 index 0000000000..676bed53f7 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/keys/hyperlane_sealevel_token_native-sealeveltest1.json @@ -0,0 +1 @@ +[245,11,246,177,71,74,101,17,124,83,118,145,35,212,169,255,215,255,1,94,185,33,54,171,79,86,221,83,104,234,22,42,167,123,78,46,210,49,137,76,200,203,142,238,33,173,204,112,93,132,137,188,204,107,47,207,64,163,88,222,35,230,11,123] \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json new file mode 100644 index 0000000000..ba62748efe --- /dev/null +++ b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json @@ -0,0 +1,10 @@ +{ + "sealeveltest1": { + "hex": "0xa77b4e2ed231894cc8cb8eee21adcc705d8489bccc6b2fcf40a358de23e60b7b", + "base58": "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga" + }, + "sealeveltest2": { + "hex": "0x2317f9615d4ebc2419ad4b88580e2a80a03b2c7a60bc960de7d6934dbc37a87e", + "base58": "3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/token-config.json b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/token-config.json new file mode 100644 index 0000000000..551fbd05a5 --- /dev/null +++ b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/token-config.json @@ -0,0 +1,12 @@ +{ + "sealeveltest1": { + "type": "native", + "decimals": 9 + }, + "sealeveltest2": { + "type": "synthetic", + "decimals": 9, + "name": "Solana", + "symbol": "SOL" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 0000000000..1d037e6a7a --- /dev/null +++ b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[41,66,119,0,251,5,86,239,146,22,239,71,92,242,131,74,187,171,216,223,119,184,174,19,60,191,221,113,245,239,17,122,58,40,14,132,102,210,107,196,225,165,211,209,126,115,247,179,7,192,130,21,109,208,255,191,140,95,154,231,85,6,214,241] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 0000000000..69fd3baad8 --- /dev/null +++ b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[63,21,4,198,48,27,204,153,114,92,118,116,234,163,49,14,128,10,0,19,56,226,121,151,6,205,21,108,169,125,212,113,29,16,150,112,133,212,123,146,110,230,188,148,124,117,183,159,93,85,69,97,122,78,86,187,44,166,129,154,160,73,41,186] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 0000000000..5ddd8b30e0 --- /dev/null +++ b/rust/sealevel/environments/testnet3/solanadevnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[68,92,27,29,32,175,172,214,16,253,88,245,2,84,255,5,186,178,191,163,136,96,18,168,23,83,232,216,205,114,154,143,168,162,161,239,196,33,75,35,20,61,227,247,44,133,46,222,78,227,191,3,46,2,248,246,206,141,64,183,75,184,121,191] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/solanadevnet/core/program-ids.json b/rust/sealevel/environments/testnet3/solanadevnet/core/program-ids.json new file mode 100644 index 0000000000..b09c77c09a --- /dev/null +++ b/rust/sealevel/environments/testnet3/solanadevnet/core/program-ids.json @@ -0,0 +1,5 @@ +{ + "mailbox": "4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn", + "validator_announce": "CMHKvdq4CopDf7qXnDCaTybS15QekQeRt4oUB219yxsp", + "multisig_ism_message_id": "2xTVcwDWZgBu69aawCdYHXqH7xQP36iBQ7rN2px1g7ms" +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/chain-config.json b/rust/sealevel/environments/testnet3/warp-routes/chain-config.json new file mode 100644 index 0000000000..97ce82e352 --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/chain-config.json @@ -0,0 +1,29 @@ +{ + "solanadevnet": { + "chainId": 1399811151, + "name": "solanadevnet", + "publicRpcUrls": [ + { + "http": "https://api.devnet.solana.com" + } + ] + }, + "zbctestnet": { + "chainId": 2053254516, + "name": "zbctestnet", + "publicRpcUrls": [ + { + "http": "https://api.zebec.eclipsenetwork.xyz:8899" + } + ] + }, + "fuji": { + "chainId": 43113, + "name": "fuji", + "publicRpcUrls": [ + { + "http": "https://api.avax-test.network/ext/bc/C/rpc" + } + ] + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/collateraltest/keys/hyperlane_sealevel_token-zbctestnet.json b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/keys/hyperlane_sealevel_token-zbctestnet.json new file mode 100644 index 0000000000..545ce2949f --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/keys/hyperlane_sealevel_token-zbctestnet.json @@ -0,0 +1 @@ +[149,181,67,100,124,202,96,21,72,171,60,101,2,224,110,102,98,101,151,12,223,22,133,37,187,219,151,44,209,142,19,52,167,183,94,157,104,211,168,249,168,71,32,224,57,203,177,249,239,28,81,95,43,58,169,150,25,236,182,240,119,146,189,228] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json new file mode 100644 index 0000000000..dbfcba9d81 --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/keys/hyperlane_sealevel_token_collateral-solanadevnet.json @@ -0,0 +1 @@ +[98,129,191,177,102,138,233,60,156,241,236,62,216,72,254,103,183,93,70,101,75,216,137,204,55,192,74,81,77,235,129,248,250,176,128,150,198,17,155,33,251,185,183,201,212,28,44,194,220,95,98,92,146,8,192,17,20,32,3,58,184,37,56,85] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/collateraltest/program-ids.json b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/program-ids.json new file mode 100644 index 0000000000..2f72559aa9 --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/program-ids.json @@ -0,0 +1,14 @@ +{ + "zbctestnet": { + "hex": "0xa7b75e9d68d3a8f9a84720e039cbb1f9ef1c515f2b3aa99619ecb6f07792bde4", + "base58": "CHhFzfTi46rzdocEpvbkSwdiqkYmfWrJo6Ru7haFTr2w" + }, + "fuji": { + "hex": "0x000000000000000000000000a97f4eacbc363f82d25a540440afc6f78920299b", + "base58": "1111111111113Mxh1B6fskiQrE2FY7RuvZDF7PfQ" + }, + "solanadevnet": { + "hex": "0xfab08096c6119b21fbb9b7c9d41c2cc2dc5f625c9208c0111420033ab8253855", + "base58": "Hsb2PdnUvd7VvZJ1svS8TrVLfsRDdDTWoHK5r2RwGZBS" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/collateraltest/token-config.json b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/token-config.json new file mode 100644 index 0000000000..a56de6c9ac --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/collateraltest/token-config.json @@ -0,0 +1,23 @@ +{ + "solanadevnet": { + "type": "collateral", + "token": "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr", + "splTokenProgram": "token", + "decimals": 6, + "name": "USD Coin Dev", + "symbol": "USDC" + }, + "zbctestnet": { + "type": "synthetic", + "decimals": 6, + "name": "USD Coin Dev", + "symbol": "USDC" + }, + "fuji": { + "type": "synthetic", + "decimals": 6, + "name": "USD Coin Dev", + "symbol": "USDC", + "foreignDeployment": "0xa97F4eACbc363f82D25a540440AFC6F78920299b" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/nativetest/keys/hyperlane_sealevel_token-zbctestnet.json b/rust/sealevel/environments/testnet3/warp-routes/nativetest/keys/hyperlane_sealevel_token-zbctestnet.json new file mode 100644 index 0000000000..dd84f3e3ed --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/nativetest/keys/hyperlane_sealevel_token-zbctestnet.json @@ -0,0 +1 @@ +[174,140,99,187,147,203,245,154,172,106,6,32,102,224,204,109,51,6,225,225,13,138,211,93,223,224,106,244,224,235,167,4,208,173,130,114,194,96,99,199,203,96,255,117,14,85,58,238,80,242,33,149,210,117,64,206,126,127,178,117,201,117,224,151] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json b/rust/sealevel/environments/testnet3/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json new file mode 100644 index 0000000000..0d2df675e3 --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/nativetest/keys/hyperlane_sealevel_token_native-solanadevnet.json @@ -0,0 +1 @@ +[211,130,122,136,147,203,98,25,35,161,148,152,75,119,132,211,228,27,148,224,200,130,204,186,108,89,180,204,61,134,31,201,42,140,185,95,186,218,14,12,41,76,133,231,57,34,111,23,173,172,165,221,240,26,185,158,14,250,248,167,59,227,33,3] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/nativetest/program-ids.json b/rust/sealevel/environments/testnet3/warp-routes/nativetest/program-ids.json new file mode 100644 index 0000000000..0deadb3b04 --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/nativetest/program-ids.json @@ -0,0 +1,14 @@ +{ + "fuji": { + "hex": "0x000000000000000000000000ddd2e6d7cc3fa4599de681376690c8ba538dab51", + "base58": "11111111111146F2zy7DPHvybUST2mVP2A8mFRDi" + }, + "solanadevnet": { + "hex": "0x2a8cb95fbada0e0c294c85e739226f17adaca5ddf01ab99e0efaf8a73be32103", + "base58": "3s6afZYk3EmjsZQ33N9yPTdSk4cY5CKeQ5wtoBcWjFUn" + }, + "zbctestnet": { + "hex": "0xd0ad8272c26063c7cb60ff750e553aee50f22195d27540ce7e7fb275c975e097", + "base58": "F3bFj2U3nFTiPZxjnPeBqf9JPAHm7K49JbqUQAcEpfx2" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/warp-routes/nativetest/token-config.json b/rust/sealevel/environments/testnet3/warp-routes/nativetest/token-config.json new file mode 100644 index 0000000000..d841a2131a --- /dev/null +++ b/rust/sealevel/environments/testnet3/warp-routes/nativetest/token-config.json @@ -0,0 +1,19 @@ +{ + "solanadevnet": { + "type": "native", + "decimals": 9 + }, + "zbctestnet": { + "type": "synthetic", + "decimals": 9, + "name": "Solana (solanadevnet)", + "symbol": "SOL" + }, + "fuji": { + "type": "synthetic", + "decimals": 9, + "name": "Solana (solanadevnet)", + "symbol": "SOL", + "foreignDeployment": "0xDDD2E6d7cC3Fa4599dE681376690c8ba538DaB51" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_mailbox-keypair.json b/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_mailbox-keypair.json new file mode 100644 index 0000000000..bc1c1fa027 --- /dev/null +++ b/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_mailbox-keypair.json @@ -0,0 +1 @@ +[185,208,10,5,131,252,243,100,236,109,92,209,28,101,30,190,97,63,180,206,194,54,50,182,70,214,48,168,186,27,215,103,54,243,26,127,33,239,42,202,242,203,131,19,227,124,11,216,221,90,211,84,144,105,195,194,170,196,103,243,124,47,91,136] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json b/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json new file mode 100644 index 0000000000..87ef4b03a2 --- /dev/null +++ b/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_multisig_ism_message_id-keypair.json @@ -0,0 +1 @@ +[156,209,97,206,171,195,1,92,35,14,13,208,107,21,178,73,30,226,228,192,137,132,70,46,136,42,225,95,139,108,242,29,91,221,243,235,11,196,152,185,28,191,141,3,163,148,184,3,107,18,26,77,67,171,75,157,29,169,63,130,172,206,247,243] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json b/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json new file mode 100644 index 0000000000..c68755ed52 --- /dev/null +++ b/rust/sealevel/environments/testnet3/zbctestnet/core/keys/hyperlane_sealevel_validator_announce-keypair.json @@ -0,0 +1 @@ +[2,238,224,202,95,38,240,140,116,21,132,235,60,247,223,25,175,228,101,232,146,210,22,212,11,75,186,71,11,183,87,123,146,71,56,166,224,120,225,8,79,238,239,26,216,240,153,165,100,17,33,163,100,154,92,233,58,244,31,219,17,233,202,6] \ No newline at end of file diff --git a/rust/sealevel/environments/testnet3/zbctestnet/core/program-ids.json b/rust/sealevel/environments/testnet3/zbctestnet/core/program-ids.json new file mode 100644 index 0000000000..2fbc365407 --- /dev/null +++ b/rust/sealevel/environments/testnet3/zbctestnet/core/program-ids.json @@ -0,0 +1,5 @@ +{ + "mailbox": "4hW22NXtJ2AXrEVbeAmxjhvxWPSNvfTfAphKXdRBZUco", + "validator_announce": "Ar1WiYNhN6F33pj4pcVo5jRMV3V8iJqKiMRSbaDEeqkq", + "multisig_ism_message_id": "7BcQ1SVxd4GPG6oPqHinv5F2q8ATPRQYq27tkLQoiNvN" +} \ No newline at end of file diff --git a/rust/sealevel/libraries/access-control/Cargo.toml b/rust/sealevel/libraries/access-control/Cargo.toml new file mode 100644 index 0000000000..ec6f79f70f --- /dev/null +++ b/rust/sealevel/libraries/access-control/Cargo.toml @@ -0,0 +1,17 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "access-control" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +solana-program.workspace = true + +[dev-dependencies] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/access-control/src/lib.rs b/rust/sealevel/libraries/access-control/src/lib.rs new file mode 100644 index 0000000000..a4b354cbf8 --- /dev/null +++ b/rust/sealevel/libraries/access-control/src/lib.rs @@ -0,0 +1,192 @@ +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; + +pub trait AccessControl { + fn owner(&self) -> Option<&Pubkey>; + + /// Returns Ok(()) if `maybe_owner` is the owner and is a signer. + fn ensure_owner_signer(&self, maybe_owner: &AccountInfo) -> Result<(), ProgramError> { + // Owner cannot be None. + let owner = self.owner().ok_or(ProgramError::InvalidArgument)?; + + if !maybe_owner.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if owner != maybe_owner.key { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } + + /// Note this does not check that the existing owner is a signer, + /// nor does it serialize the change to the account. + fn set_owner(&mut self, new_owner: Option) -> Result<(), ProgramError>; + + /// Sets ownership if the `existing_owner` is the current owner and is a signer. + /// Errors if `existing_owner` is not a signer or is not the current owner. + /// Note this does not serialize the change to the account. + fn transfer_ownership( + &mut self, + maybe_existing_owner: &AccountInfo, + new_owner: Option, + ) -> Result<(), ProgramError> { + self.ensure_owner_signer(maybe_existing_owner)?; + self.set_owner(new_owner)?; + msg!("Ownership transferred to {:?}", new_owner); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + struct TestAccessControl { + owner: Option, + } + + impl AccessControl for TestAccessControl { + fn owner(&self) -> Option<&Pubkey> { + self.owner.as_ref() + } + + fn set_owner(&mut self, new_owner: Option) -> Result<(), ProgramError> { + self.owner = new_owner; + Ok(()) + } + } + + #[test] + fn test_ensure_owner_signer() { + let owner = Pubkey::new_unique(); + let access_control = TestAccessControl { owner: Some(owner) }; + + let mut owner_account_lamports = 0; + let mut owner_account_data = vec![0; 0]; + // Is a signer and the owner + let owner_account_info = AccountInfo::new( + &owner, + true, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &owner, + false, + 0, + ); + assert_eq!( + access_control.ensure_owner_signer(&owner_account_info), + Ok(()) + ); + + // Not a signer, is the owner + let owner_account_info = AccountInfo::new( + &owner, + false, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &owner, + false, + 0, + ); + assert_eq!( + access_control.ensure_owner_signer(&owner_account_info), + Err(ProgramError::MissingRequiredSignature), + ); + + // Is a signer, not the owner + let non_owner = Pubkey::new_unique(); + let owner_account_info = AccountInfo::new( + &non_owner, + true, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &owner, + false, + 0, + ); + assert_eq!( + access_control.ensure_owner_signer(&owner_account_info), + Err(ProgramError::InvalidArgument), + ); + } + + #[test] + fn test_transfer_ownership() { + let owner = Pubkey::new_unique(); + let mut access_control = TestAccessControl { owner: Some(owner) }; + + let mut owner_account_lamports = 0; + let mut owner_account_data = vec![0; 0]; + // Is a signer and the owner + let owner_account_info = AccountInfo::new( + &owner, + true, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &owner, + false, + 0, + ); + + let new_owner = Pubkey::new_unique(); + // Transfer ownership to new_owner + assert_eq!( + access_control.transfer_ownership(&owner_account_info, Some(new_owner)), + Ok(()) + ); + assert_eq!(access_control.owner, Some(new_owner)); + + // Now the old owner shouldn't be able to transfer ownership anymore + assert_eq!( + access_control.transfer_ownership(&owner_account_info, Some(new_owner)), + Err(ProgramError::InvalidArgument), + ); + + // The new owner now, but not a signer + let owner_account_info = AccountInfo::new( + &new_owner, + false, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &owner, + false, + 0, + ); + + // Ensure it can't transfer ownership because it's not a signer + assert_eq!( + access_control.transfer_ownership(&owner_account_info, None), + Err(ProgramError::MissingRequiredSignature), + ); + + // The new owner now, but a signer + let owner_account_info = AccountInfo::new( + &new_owner, + true, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &owner, + false, + 0, + ); + + // Transfer ownership to None + assert_eq!( + access_control.transfer_ownership(&owner_account_info, None), + Ok(()) + ); + assert_eq!(access_control.owner, None); + + // Now the "new owner" shouldn't be able to transfer ownership anymore + // because the owner is None + assert_eq!( + access_control.transfer_ownership(&owner_account_info, None), + Err(ProgramError::InvalidArgument), + ); + } +} diff --git a/rust/sealevel/libraries/account-utils/Cargo.toml b/rust/sealevel/libraries/account-utils/Cargo.toml new file mode 100644 index 0000000000..45ac5b892e --- /dev/null +++ b/rust/sealevel/libraries/account-utils/Cargo.toml @@ -0,0 +1,19 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "account-utils" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +solana-program.workspace = true +spl-type-length-value.workspace = true + +[dev-dependencies] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/account-utils/src/lib.rs b/rust/sealevel/libraries/account-utils/src/lib.rs new file mode 100644 index 0000000000..780faa7d0e --- /dev/null +++ b/rust/sealevel/libraries/account-utils/src/lib.rs @@ -0,0 +1,256 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, + program::{invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_instruction, system_program, +}; +use spl_type_length_value::discriminator::Discriminator; + +/// Data that has a predictable size when serialized. +pub trait SizedData { + /// Returns the size of the data when serialized. + fn size(&self) -> usize; +} + +/// Serializable data intended to be used by `AccountData`. +/// Consider removing the `Default` binding in the future. +pub trait Data: BorshDeserialize + BorshSerialize + Default {} + +impl Data for T where T: BorshDeserialize + BorshSerialize + Default {} + +/// Account data structure wrapper type that handles initialization and (de)serialization. +/// +/// (De)serialization is done with borsh and the "on-disk" format is as follows: +/// { +/// initialized: bool, +/// data: T, +/// } +#[derive(Debug, Default)] +pub struct AccountData { + data: Box, +} + +impl From for AccountData { + fn from(data: T) -> Self { + Self { + data: Box::new(data), + } + } +} + +impl From> for AccountData { + fn from(data: Box) -> Self { + Self { data } + } +} + +impl SizedData for AccountData +where + T: SizedData, +{ + fn size(&self) -> usize { + // Add an extra byte for the initialized flag. + 1 + self.data.size() + } +} + +impl AccountData +where + T: Data, +{ + pub fn into_inner(self) -> Box { + self.data + } + + /// Deserializes the account data from the given slice. + pub fn fetch_data(buf: &mut &[u8]) -> Result>, ProgramError> { + if buf.is_empty() { + return Ok(None); + } + // Account data is zero initialized. + let initialized = bool::deserialize(buf)?; + let data = if initialized { + Some(T::deserialize(buf).map(Box::new)?) + } else { + None + }; + Ok(data) + } + + /// Deserializes the account data from the given slice and wraps it in an `AccountData`. + pub fn fetch(buf: &mut &[u8]) -> Result { + Ok(Self::from(Self::fetch_data(buf)?.unwrap_or_default())) + } + + // Optimisically write then realloc on failure. + // If we serialize and calculate len before realloc we will waste heap space as there is no + // free(). Tradeoff between heap usage and compute budget. + pub fn store( + &self, + account: &AccountInfo<'_>, + allow_realloc: bool, + ) -> Result<(), ProgramError> { + if !account.is_writable || account.executable { + return Err(ProgramError::InvalidAccountData); + } + let realloc_increment = 1024; + loop { + // Create new scope to ensure `guard` is dropped before + // potential reallocation. + let data_len = { + let mut guard = account.try_borrow_mut_data()?; + let data = &mut *guard; + let data_len = data.len(); + + match self.store_in_slice(data) { + Ok(_) => break, + Err(err) => match err.kind() { + std::io::ErrorKind::WriteZero => { + if !allow_realloc { + return Err(ProgramError::BorshIoError(err.to_string())); + } + } + _ => return Err(ProgramError::BorshIoError(err.to_string())), + }, + }; + + data_len + }; + + if cfg!(target_os = "solana") { + account.realloc(data_len + realloc_increment, false)?; + } else { + panic!("realloc() is only supported on the SVM"); + } + } + Ok(()) + } + + pub fn store_in_slice(&self, target: &mut [u8]) -> Result<(), Box> { + // Create a new slice so that this new slice + // is updated to point to the unwritten data during serialization, and not `target` itself. + + let mut writable_target: &mut [u8] = &mut *target; + true.serialize(&mut writable_target) + .and_then(|_| self.data.serialize(&mut writable_target))?; + Ok(()) + } +} + +/// Creates associated token account using Program Derived Address for the given seeds. +/// Required to allow PDAs to be created even if they already have a lamport balance. +/// +/// Borrowed from https://github.com/solana-labs/solana-program-library/blob/cf77ed0c187d1becd0db56edff4491c28f18dfc8/associated-token-account/program/src/tools/account.rs#L18 +pub fn create_pda_account<'a>( + payer: &AccountInfo<'a>, + rent: &Rent, + space: usize, + owner: &Pubkey, + system_program: &AccountInfo<'a>, + new_pda_account: &AccountInfo<'a>, + new_pda_signer_seeds: &[&[u8]], +) -> Result<(), ProgramError> { + if new_pda_account.lamports() > 0 { + let required_lamports = rent + .minimum_balance(space) + .max(1) + .saturating_sub(new_pda_account.lamports()); + + if required_lamports > 0 { + invoke( + &system_instruction::transfer(payer.key, new_pda_account.key, required_lamports), + &[ + payer.clone(), + new_pda_account.clone(), + system_program.clone(), + ], + )?; + } + + invoke_signed( + &system_instruction::allocate(new_pda_account.key, space as u64), + &[new_pda_account.clone(), system_program.clone()], + &[new_pda_signer_seeds], + )?; + + invoke_signed( + &system_instruction::assign(new_pda_account.key, owner), + &[new_pda_account.clone(), system_program.clone()], + &[new_pda_signer_seeds], + ) + } else { + invoke_signed( + &system_instruction::create_account( + payer.key, + new_pda_account.key, + rent.minimum_balance(space).max(1), + space as u64, + owner, + ), + &[ + payer.clone(), + new_pda_account.clone(), + system_program.clone(), + ], + &[new_pda_signer_seeds], + ) + } +} + +/// Returns Ok() if the account is rent exempt, Err() otherwise. +pub fn verify_rent_exempt(account: &AccountInfo<'_>, rent: &Rent) -> Result<(), ProgramError> { + if !rent.is_exempt(account.lamports(), account.data_len()) { + return Err(ProgramError::AccountNotRentExempt); + } + Ok(()) +} + +/// Returns Ok if the account data is empty and the owner is the system program. +/// Returns Err otherwise. +pub fn verify_account_uninitialized(account: &AccountInfo) -> Result<(), ProgramError> { + if account.data_is_empty() && account.owner == &system_program::id() { + return Ok(()); + } + Err(ProgramError::AccountAlreadyInitialized) +} + +pub const PROGRAM_INSTRUCTION_DISCRIMINATOR: [u8; Discriminator::LENGTH] = [1, 1, 1, 1, 1, 1, 1, 1]; + +pub trait DiscriminatorData: Sized { + const DISCRIMINATOR_LENGTH: usize = Discriminator::LENGTH; + + const DISCRIMINATOR: [u8; Discriminator::LENGTH]; + const DISCRIMINATOR_SLICE: &'static [u8] = &Self::DISCRIMINATOR; +} + +pub trait DiscriminatorEncode: DiscriminatorData + borsh::BorshSerialize { + fn encode(self) -> Result, ProgramError> { + let mut buf = vec![]; + buf.extend_from_slice(Self::DISCRIMINATOR_SLICE); + buf.extend_from_slice( + &self + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + Ok(buf) + } +} + +// Auto-implement +impl DiscriminatorEncode for T where T: DiscriminatorData + borsh::BorshSerialize {} + +pub trait DiscriminatorDecode: DiscriminatorData + borsh::BorshDeserialize { + fn decode(data: &[u8]) -> Result { + let (discriminator, rest) = data.split_at(Discriminator::LENGTH); + if discriminator != Self::DISCRIMINATOR_SLICE { + return Err(ProgramError::InvalidInstructionData); + } + Self::try_from_slice(rest).map_err(|_| ProgramError::InvalidInstructionData) + } +} + +// Auto-implement +impl DiscriminatorDecode for T where T: DiscriminatorData + borsh::BorshDeserialize {} diff --git a/rust/sealevel/libraries/ecdsa-signature/Cargo.toml b/rust/sealevel/libraries/ecdsa-signature/Cargo.toml new file mode 100644 index 0000000000..13df708f81 --- /dev/null +++ b/rust/sealevel/libraries/ecdsa-signature/Cargo.toml @@ -0,0 +1,18 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "ecdsa-signature" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +solana-program.workspace = true +thiserror.workspace = true + +hyperlane-core = { path = "../../../hyperlane-core" } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/ecdsa-signature/src/lib.rs b/rust/sealevel/libraries/ecdsa-signature/src/lib.rs new file mode 100644 index 0000000000..feb7a5aa70 --- /dev/null +++ b/rust/sealevel/libraries/ecdsa-signature/src/lib.rs @@ -0,0 +1,150 @@ +use hyperlane_core::H160; +use solana_program::{ + keccak, + secp256k1_recover::{secp256k1_recover, Secp256k1RecoverError}, +}; + +/// Errors relating to an EcdsaSignature +#[derive(Copy, Clone, Debug, Eq, thiserror::Error, PartialEq)] +pub enum EcdsaSignatureError { + #[error("Invalid signature length")] + InvalidLength, + #[error("Invalid signature recovery ID")] + InvalidRecoveryId, +} + +/// An ECDSA signature with a recovery ID. +/// Signature recovery functions expect a 64 byte serialized r & s value and a 1 byte recovery ID +/// that is either 0 or 1. +/// This type is used to deserialize a 65 byte signature & allow easy recovery of the Ethereum +/// address signer. +#[derive(Debug, Eq, PartialEq)] +pub struct EcdsaSignature { + pub serialized_rs: [u8; 64], + pub recovery_id: u8, +} + +impl EcdsaSignature { + /// Deserializes a 65 byte signature into an EcdsaSignature. + /// The recovery ID, i.e. the `v` value, must be 0, 1, 27, or 28. + /// If it is 27 or 28, it's normalized to 0 or 1, which is what's required + /// by the secp256k1_recover function. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 65 { + return Err(EcdsaSignatureError::InvalidLength); + } + + let mut serialized_rs = [0u8; 64]; + serialized_rs.copy_from_slice(&bytes[..64]); + + let mut recovery_id = bytes[64]; + if recovery_id == 27 || recovery_id == 28 { + recovery_id -= 27; + } + + // Recovery ID must be 0 or 1 + if recovery_id > 1 { + return Err(EcdsaSignatureError::InvalidRecoveryId); + } + + Ok(Self { + serialized_rs, + recovery_id, + }) + } + + /// Serializes the signature into a 65 byte array. + #[allow(dead_code)] + pub fn as_fixed_bytes(&self) -> [u8; 65] { + let mut bytes = [0u8; 65]; + bytes[..64].copy_from_slice(&self.serialized_rs[..]); + bytes[64] = self.recovery_id; + bytes + } + + /// Recovers the Ethereum address of the signer of the signed message hash. + pub fn secp256k1_recover_ethereum_address( + &self, + hash: &[u8], + ) -> Result { + let public_key = secp256k1_recover(hash, self.recovery_id, self.serialized_rs.as_slice())?; + + let public_key_hash = { + let mut hasher = keccak::Hasher::default(); + hasher.hash(&public_key.to_bytes()[..]); + &hasher.result().to_bytes()[12..] + }; + + Ok(H160::from_slice(public_key_hash)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use hyperlane_core::H256; + + #[test] + fn test_decode_invalid_length() { + let bytes = [0u8; 64]; + assert!( + EcdsaSignature::from_bytes(&bytes[..]).unwrap_err() + == EcdsaSignatureError::InvalidLength + ); + + let bytes = [0u8; 66]; + assert!( + EcdsaSignature::from_bytes(&bytes[..]).unwrap_err() + == EcdsaSignatureError::InvalidLength + ); + } + + #[test] + fn test_decode_ecdsa_signature() { + // Various recovery ids. (encoded, decoded - Some is valid, None means err is expected) + let valid_recovery_ids = vec![ + // Valid ones + (0, Some(0)), + (1, Some(1)), + (27, Some(0)), + (28, Some(1)), + // Invalid ones + (2, None), + (3, None), + (26, None), + (29, None), + ]; + + for (encoded_recovery_id, decoded_recovery_id) in valid_recovery_ids { + let mut rs = [0u8; 64]; + rs[..32].copy_from_slice(&H256::random()[..]); + rs[32..].copy_from_slice(&H256::random()[..]); + + let mut bytes = [0u8; 65]; + bytes[..64].copy_from_slice(&rs[..]); + bytes[64] = encoded_recovery_id; + + let signature_result = EcdsaSignature::from_bytes(&bytes); + + match decoded_recovery_id { + Some(decoded_recovery_id) => { + let signature = signature_result.unwrap(); + assert_eq!( + signature, + EcdsaSignature { + serialized_rs: rs, + recovery_id: decoded_recovery_id, + } + ); + } + None => { + assert!(signature_result.is_err()); + assert!( + signature_result.unwrap_err() == EcdsaSignatureError::InvalidRecoveryId + ); + } + } + } + } +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-connection-client/Cargo.toml b/rust/sealevel/libraries/hyperlane-sealevel-connection-client/Cargo.toml new file mode 100644 index 0000000000..5fdbe21145 --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-connection-client/Cargo.toml @@ -0,0 +1,22 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-connection-client" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +solana-program.workspace = true + +access-control = { path = "../access-control" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-mailbox = { path = "../../programs/mailbox", features = ["no-entrypoint"] } + +[dev-dependencies] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/hyperlane-sealevel-connection-client/src/lib.rs b/rust/sealevel/libraries/hyperlane-sealevel-connection-client/src/lib.rs new file mode 100644 index 0000000000..7d296bfbe6 --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-connection-client/src/lib.rs @@ -0,0 +1,89 @@ +use access_control::AccessControl; +use borsh::BorshSerialize; +use solana_program::{ + account_info::AccountInfo, program::set_return_data, program_error::ProgramError, + pubkey::Pubkey, +}; + +pub mod router; + +/// Getters for the HyperlaneConnectionClient. +pub trait HyperlaneConnectionClient { + fn mailbox(&self) -> &Pubkey; + + fn interchain_gas_paymaster(&self) -> Option<&Pubkey>; + + fn interchain_security_module(&self) -> Option<&Pubkey>; + + fn set_interchain_security_module_return_data(&self) { + let ism: Option = self.interchain_security_module().cloned(); + set_return_data( + &ism.try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string())) + .unwrap()[..], + ); + } +} + +pub trait HyperlaneConnectionClientSetter { + fn set_mailbox(&mut self, new_mailbox: Pubkey); + + fn set_interchain_gas_paymaster(&mut self, new_igp: Option); + + fn set_interchain_security_module(&mut self, new_ism: Option); +} + +pub trait HyperlaneConnectionClientSetterAccessControl: + HyperlaneConnectionClientSetter + AccessControl +{ + fn set_mailbox_only_owner( + &mut self, + maybe_owner: &AccountInfo, + new_mailbox: Pubkey, + ) -> Result<(), ProgramError> { + self.ensure_owner_signer(maybe_owner)?; + self.set_mailbox(new_mailbox); + + Ok(()) + } + + fn set_interchain_gas_paymaster_only_owner( + &mut self, + maybe_owner: &AccountInfo, + new_igp: Option, + ) -> Result<(), ProgramError> { + self.ensure_owner_signer(maybe_owner)?; + self.set_interchain_gas_paymaster(new_igp); + + Ok(()) + } + + fn set_interchain_security_module_only_owner( + &mut self, + maybe_owner: &AccountInfo, + new_ism: Option, + ) -> Result<(), ProgramError> { + self.ensure_owner_signer(maybe_owner)?; + self.set_interchain_security_module(new_ism); + + Ok(()) + } +} + +/// A recipient of Hyperlane messages. +pub trait HyperlaneConnectionClientRecipient { + fn mailbox_process_authority(&self) -> &Pubkey; + + fn ensure_mailbox_process_authority_signer( + &self, + maybe_authority: &AccountInfo, + ) -> Result<(), ProgramError> { + if self.mailbox_process_authority() != maybe_authority.key { + return Err(ProgramError::InvalidArgument); + } + if !maybe_authority.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + Ok(()) + } +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-connection-client/src/router.rs b/rust/sealevel/libraries/hyperlane-sealevel-connection-client/src/router.rs new file mode 100644 index 0000000000..607ab089df --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-connection-client/src/router.rs @@ -0,0 +1,175 @@ +use access_control::AccessControl; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::H256; +use hyperlane_sealevel_mailbox::instruction::{ + Instruction as MailboxInstruction, OutboxDispatch as MailboxOutboxDispatch, +}; +use solana_program::{ + account_info::AccountInfo, + instruction::{AccountMeta, Instruction}, + msg, + program::invoke_signed, + program_error::ProgramError, + pubkey::Pubkey, +}; +use std::collections::HashMap; + +use crate::{HyperlaneConnectionClient, HyperlaneConnectionClientRecipient}; + +/// Configuration for a remote router. +#[derive(Debug, Clone, PartialEq, BorshDeserialize, BorshSerialize)] +pub struct RemoteRouterConfig { + /// The domain of the remote router. + pub domain: u32, + /// The remote router. + pub router: Option, +} + +/// The Hyperlane router pattern. +pub trait HyperlaneRouter { + /// Returns the router for the provided origin, or None if no router is enrolled. + fn router(&self, origin: u32) -> Option<&H256>; + + /// Returns Err if `maybe_router` is not the remote router for the provided origin. + fn only_remote_router(&self, origin: u32, maybe_router: &H256) -> Result<(), ProgramError> { + if !self.is_remote_router(origin, maybe_router) { + return Err(ProgramError::InvalidInstructionData); + } + Ok(()) + } + + /// Enrolls a remote router. + fn enroll_remote_router(&mut self, config: RemoteRouterConfig); + + /// Enrolls multiple remote routers. + fn enroll_remote_routers(&mut self, configs: Vec) { + for config in configs { + self.enroll_remote_router(config); + } + } + + /// Returns true if `maybe_router` is the remote router for the provided origin. + fn is_remote_router(&self, origin: u32, maybe_router: &H256) -> bool { + self.router(origin) == Some(maybe_router) + } +} + +/// The Hyperlane router pattern with setters restricted to the access control owner. +pub trait HyperlaneRouterAccessControl: HyperlaneRouter + AccessControl { + /// Enrolls a remote router if the provided `maybe_owner` is a signer and is the access control owner. + /// Otherwise, returns an error. + fn enroll_remote_router_only_owner( + &mut self, + maybe_owner: &AccountInfo, + config: RemoteRouterConfig, + ) -> Result<(), ProgramError> { + self.ensure_owner_signer(maybe_owner)?; + self.enroll_remote_router(config); + Ok(()) + } + + /// Enrolls multiple remote routers if the provided `maybe_owner` is a signer and is the access control owner. + /// Otherwise, returns an error. + fn enroll_remote_routers_only_owner( + &mut self, + maybe_owner: &AccountInfo, + configs: Vec, + ) -> Result<(), ProgramError> { + self.ensure_owner_signer(maybe_owner)?; + self.enroll_remote_routers(configs); + Ok(()) + } +} + +// Auto-implement +impl HyperlaneRouterAccessControl for T where T: HyperlaneRouter + AccessControl {} + +/// The Hyperlane router pattern with a helper function to dispatch messages +/// to remote routers. +pub trait HyperlaneRouterDispatch: HyperlaneRouter + HyperlaneConnectionClient { + /// Dispatches a message to the remote router for the provided destination domain. + fn dispatch( + &self, + program_id: &Pubkey, + dispatch_authority_seeds: &[&[u8]], + destination_domain: u32, + message_body: Vec, + account_metas: Vec, + account_infos: &[AccountInfo], + ) -> Result<(), ProgramError> { + // The recipient is the remote router, which must be enrolled. + let recipient = *self + .router(destination_domain) + .ok_or(ProgramError::InvalidArgument)?; + + let dispatch_instruction = MailboxInstruction::OutboxDispatch(MailboxOutboxDispatch { + sender: *program_id, + destination_domain, + recipient, + message_body, + }); + let mailbox_ixn = Instruction { + program_id: *self.mailbox(), + data: dispatch_instruction.into_instruction_data()?, + accounts: account_metas, + }; + // Call the Mailbox program to dispatch the message. + invoke_signed(&mailbox_ixn, account_infos, &[dispatch_authority_seeds]) + } +} + +// Auto-implement +impl HyperlaneRouterDispatch for T where T: HyperlaneRouter + HyperlaneConnectionClient {} + +/// The Hyperlane router pattern with a helper function to ensure messages +/// come only via the Mailbox & from an enrolled remote router. +pub trait HyperlaneRouterMessageRecipient: + HyperlaneRouter + HyperlaneConnectionClientRecipient +{ + /// Returns Err if `maybe_mailbox_process_authority` is not a signer or is not the + /// Mailbox's process authority for this recipient, or if the sender is not the + /// remote router for the provided origin. + fn ensure_valid_router_message( + &self, + maybe_mailbox_process_authority: &AccountInfo, + origin: u32, + sender: &H256, + ) -> Result<(), ProgramError> { + // First ensure that the Mailbox's process authority for this recipient + // is a signer. + self.ensure_mailbox_process_authority_signer(maybe_mailbox_process_authority)?; + + // Now make sure the sender is really a remote router. + self.only_remote_router(origin, sender) + } +} + +// Auto-implement +impl HyperlaneRouterMessageRecipient for T where + T: HyperlaneRouter + HyperlaneConnectionClientRecipient +{ +} + +/// A default implementation of `HyperlaneRouter` for `HashMap`. +impl HyperlaneRouter for HashMap { + fn router(&self, origin: u32) -> Option<&H256> { + self.get(&origin) + } + + fn enroll_remote_router(&mut self, config: RemoteRouterConfig) { + match config.router { + Some(router) => { + self.insert(config.domain, router); + } + None => { + self.remove(&config.domain); + } + } + + msg!( + "Set domain {} remote router to {:?}", + config.domain, + config.router + ); + } +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/Cargo.toml b/rust/sealevel/libraries/hyperlane-sealevel-token/Cargo.toml new file mode 100644 index 0000000000..ec3ae855ce --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/Cargo.toml @@ -0,0 +1,33 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-token-lib" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +thiserror.workspace = true +spl-associated-token-account.workspace = true +spl-noop.workspace = true +spl-token.workspace = true +spl-token-2022.workspace = true # FIXME Should we actually use 2022 here or try normal token program? + +access-control = { path = "../access-control" } +account-utils = { path = "../account-utils" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../../programs/mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-message-recipient-interface = { path = "../message-recipient-interface" } +serializable-account-meta = { path = "../serializable-account-meta" } + +[dev-dependencies] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/accounts.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/accounts.rs new file mode 100644 index 0000000000..a58d59816a --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/accounts.rs @@ -0,0 +1,258 @@ +//! Accounts for the Hyperlane token program. + +use access_control::AccessControl; +use account_utils::AccountData; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{H256, U256}; +use hyperlane_sealevel_connection_client::{ + router::{HyperlaneRouter, RemoteRouterConfig}, + HyperlaneConnectionClient, HyperlaneConnectionClientRecipient, HyperlaneConnectionClientSetter, + HyperlaneConnectionClientSetterAccessControl, +}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; +use std::{cmp::Ordering, collections::HashMap, fmt::Debug}; + +use crate::hyperlane_token_pda_seeds; + +/// HyperlaneToken account data. +pub type HyperlaneTokenAccount = AccountData>; + +/// A PDA account containing the data for a Hyperlane token +/// and any plugin-specific data. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default)] +pub struct HyperlaneToken { + /// The bump seed for this PDA. + pub bump: u8, + /// The address of the mailbox contract. + pub mailbox: Pubkey, + /// The Mailbox process authority specific to this program as the recipient. + pub mailbox_process_authority: Pubkey, + /// The dispatch authority PDA's bump seed. + pub dispatch_authority_bump: u8, + /// The decimals of the local token. + pub decimals: u8, + /// The decimals of the remote token. + pub remote_decimals: u8, + /// Access control owner. + pub owner: Option, + /// The interchain security module. + pub interchain_security_module: Option, + /// Remote routers. + pub remote_routers: HashMap, + /// Plugin-specific data. + pub plugin_data: T, +} + +impl HyperlaneToken +where + T: BorshSerialize + BorshDeserialize + Default + Debug, +{ + /// Deserializes the data from the provided `token_account_info` and returns it. + /// Returns an Err if the provided `token_account_info` is not the canonical HyperlaneToken PDA for this program. + pub fn verify_account_and_fetch_inner( + program_id: &Pubkey, + token_account_info: &AccountInfo<'_>, + ) -> Result { + let token = + HyperlaneTokenAccount::fetch(&mut &token_account_info.data.borrow()[..])?.into_inner(); + let token_seeds: &[&[u8]] = hyperlane_token_pda_seeds!(token.bump); + let expected_token_key = Pubkey::create_program_address(token_seeds, program_id)?; + if token_account_info.key != &expected_token_key { + return Err(ProgramError::InvalidArgument); + } + if token_account_info.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + Ok(*token) + } + + /// Converts a local token amount to a remote token amount, accounting for decimals and types. + pub fn local_amount_to_remote_amount(&self, amount: u64) -> Result { + convert_decimals(amount.into(), self.decimals, self.remote_decimals) + .ok_or(ProgramError::InvalidArgument) + } + + /// Converts a remote token amount to a local token amount, accounting for decimals and types. + pub fn remote_amount_to_local_amount(&self, amount: U256) -> Result { + let amount = convert_decimals(amount, self.remote_decimals, self.decimals) + .ok_or(ProgramError::InvalidArgument)? + .as_u64(); + Ok(amount) + } +} + +impl AccessControl for HyperlaneToken { + fn owner(&self) -> Option<&Pubkey> { + self.owner.as_ref() + } + + fn set_owner(&mut self, new_owner: Option) -> Result<(), ProgramError> { + self.owner = new_owner; + Ok(()) + } +} + +impl HyperlaneConnectionClient for HyperlaneToken { + fn mailbox(&self) -> &Pubkey { + &self.mailbox + } + + // Not yet supported + fn interchain_gas_paymaster(&self) -> Option<&Pubkey> { + None + } + + fn interchain_security_module(&self) -> Option<&Pubkey> { + self.interchain_security_module.as_ref() + } +} + +impl HyperlaneConnectionClientSetter for HyperlaneToken { + fn set_mailbox(&mut self, new_mailbox: Pubkey) { + self.mailbox = new_mailbox; + } + + fn set_interchain_gas_paymaster(&mut self, _new_igp: Option) { + // Not yet supported + } + + fn set_interchain_security_module(&mut self, new_ism: Option) { + self.interchain_security_module = new_ism; + } +} + +impl HyperlaneConnectionClientSetterAccessControl for HyperlaneToken {} + +impl HyperlaneConnectionClientRecipient for HyperlaneToken { + fn mailbox_process_authority(&self) -> &Pubkey { + &self.mailbox_process_authority + } +} + +impl HyperlaneRouter for HyperlaneToken { + fn router(&self, origin: u32) -> Option<&H256> { + self.remote_routers.router(origin) + } + + fn enroll_remote_router(&mut self, config: RemoteRouterConfig) { + self.remote_routers.enroll_remote_router(config); + } +} + +/// Converts an amount from one decimal representation to another. +pub fn convert_decimals(amount: U256, from_decimals: u8, to_decimals: u8) -> Option { + match from_decimals.cmp(&to_decimals) { + Ordering::Greater => { + let divisor = U256::from(10u64).checked_pow(U256::from(from_decimals - to_decimals)); + divisor.and_then(|d| amount.checked_div(d)) + } + Ordering::Less => { + let multiplier = U256::from(10u64).checked_pow(U256::from(to_decimals - from_decimals)); + multiplier.and_then(|m| amount.checked_mul(m)) + } + Ordering::Equal => Some(amount), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_convert_decimals() { + // No decimal difference + assert_eq!( + convert_decimals(U256::from(100), 2, 2), + Some(U256::from(100)) + ); + + // Low decimals -> High decimals + assert_eq!( + convert_decimals(U256::from(100), 2, 5), + Some(U256::from(100000)) + ); + + // High decimals -> Low decimals + assert_eq!( + convert_decimals(U256::from(100000), 5, 2), + Some(U256::from(100)) + ); + + // High decimals -> Low decimals, with loss of precision + assert_eq!( + convert_decimals(U256::from(100001), 5, 2), + Some(U256::from(100)) + ); + } + + #[test] + fn test_local_amount_to_remote_amount() { + let token: HyperlaneToken<()> = HyperlaneToken { + decimals: 9, + remote_decimals: 18, + ..HyperlaneToken::<()>::default() + }; + + assert_eq!( + token.local_amount_to_remote_amount(1_000_000_000), + Ok(U256::from(10).pow(U256::from(18))) + ); + + // Try an overflow + let token: HyperlaneToken<()> = HyperlaneToken { + decimals: 9, + remote_decimals: 200, + ..HyperlaneToken::<()>::default() + }; + + assert_eq!( + token.local_amount_to_remote_amount(1_000_000_000), + Err(ProgramError::InvalidArgument) + ); + + // Try a loss of precision + let token: HyperlaneToken<()> = HyperlaneToken { + decimals: 9, + remote_decimals: 5, + ..HyperlaneToken::<()>::default() + }; + + assert_eq!(token.local_amount_to_remote_amount(100), Ok(U256::zero())); + } + + #[test] + fn test_remote_amount_to_local_amount() { + let token: HyperlaneToken<()> = HyperlaneToken { + decimals: 9, + remote_decimals: 18, + ..HyperlaneToken::<()>::default() + }; + + assert_eq!( + token.remote_amount_to_local_amount(U256::from(10u64).pow(U256::from(18u64))), + Ok(10u64.pow(9u32)) + ); + + // Try an overflow + let token: HyperlaneToken<()> = HyperlaneToken { + decimals: 200, + remote_decimals: 9, + ..HyperlaneToken::<()>::default() + }; + + assert_eq!( + token.remote_amount_to_local_amount(1_000_000_000u64.into()), + Err(ProgramError::InvalidArgument) + ); + + // Try a loss of precision + let token: HyperlaneToken<()> = HyperlaneToken { + decimals: 5, + remote_decimals: 9, + ..HyperlaneToken::<()>::default() + }; + + assert_eq!(token.remote_amount_to_local_amount(100u64.into()), Ok(0)); + } +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/error.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/error.rs new file mode 100644 index 0000000000..d5242f3838 --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/error.rs @@ -0,0 +1,26 @@ +//! Errors for the Hyperlane Sealevel Token programs. + +use solana_program::program_error::ProgramError; + +/// Custom errors that may be returned by the Hyperlane Sealevel Token programs. +#[derive(Copy, Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] +#[repr(u32)] +pub enum Error { + /// An extra account was provided that was not required. + #[error("Unused account(s) provided")] + ExtraneousAccount = 1, + + /// An integer overflow occurred. + #[error("Integer overflow")] + IntegerOverflow = 2, + + /// A message decoding error occurred. + #[error("Message decoding error")] + MessageDecodeError = 3, +} + +impl From for ProgramError { + fn from(err: Error) -> Self { + ProgramError::Custom(err as u32) + } +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs new file mode 100644 index 0000000000..5657c7f9ab --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/instruction.rs @@ -0,0 +1,131 @@ +//! Instructions shared by all Hyperlane Sealevel Token programs. + +use account_utils::{DiscriminatorData, DiscriminatorEncode, PROGRAM_INSTRUCTION_DISCRIMINATOR}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{H256, U256}; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use hyperlane_sealevel_mailbox::mailbox_message_dispatch_authority_pda_seeds; + +use crate::hyperlane_token_pda_seeds; + +/// Instructions shared by all Hyperlane Sealevel Token programs. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub enum Instruction { + /// Initialize the program. + Init(Init), + /// Transfer tokens to a remote recipient. + TransferRemote(TransferRemote), + /// Enroll a remote router. Only owner. + EnrollRemoteRouter(RemoteRouterConfig), + /// Enroll multiple remote routers. Only owner. + EnrollRemoteRouters(Vec), + /// Set the interchain security module. Only owner. + SetInterchainSecurityModule(Option), + /// Transfer ownership of the program. Only owner. + TransferOwnership(Option), +} + +impl DiscriminatorData for Instruction { + const DISCRIMINATOR: [u8; Self::DISCRIMINATOR_LENGTH] = PROGRAM_INSTRUCTION_DISCRIMINATOR; +} + +/// Instruction data for initializing the program. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct Init { + /// The address of the mailbox contract. + pub mailbox: Pubkey, + /// The interchain security module. + pub interchain_security_module: Option, + /// The local decimals. + pub decimals: u8, + /// The remote decimals. + pub remote_decimals: u8, +} + +/// Instruction data for transferring `amount_or_id` token to `recipient` on `destination` domain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct TransferRemote { + /// The destination domain. + pub destination_domain: u32, + /// The remote recipient. + pub recipient: H256, + /// The amount or ID of the token to transfer. + pub amount_or_id: U256, +} + +/// Gets an instruction to initialize the program. This provides only the +/// account metas required by the library, and consuming programs are expected +/// to add the accounts for their own use. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + init: Init, +) -> Result { + let (token_key, _token_bump) = + Pubkey::try_find_program_address(hyperlane_token_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + let (dispatch_authority_key, _dispatch_authority_bump) = Pubkey::try_find_program_address( + mailbox_message_dispatch_authority_pda_seeds!(), + &program_id, + ) + .ok_or(ProgramError::InvalidSeeds)?; + + let ixn = Instruction::Init(init); + + // Accounts: + // 0. [executable] The system program. + // 1. [writable] The token PDA account. + // 2. [writable] The dispatch authority PDA account. + // 3. [signer] The payer and access control owner. + // 4..N [??..??] Plugin-specific accounts. + let accounts = vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(token_key, false), + AccountMeta::new(dispatch_authority_key, false), + AccountMeta::new(payer, true), + ]; + + let instruction = SolanaInstruction { + program_id, + data: ixn.encode()?, + accounts, + }; + + Ok(instruction) +} + +/// Enrolls remote routers. +pub fn enroll_remote_routers_instruction( + program_id: Pubkey, + owner_payer: Pubkey, + configs: Vec, +) -> Result { + let (token_key, _token_bump) = + Pubkey::try_find_program_address(hyperlane_token_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + let ixn = Instruction::EnrollRemoteRouters(configs); + + // Accounts: + // 0. [writeable] The token PDA account. + // 1. [signer] The owner. + let accounts = vec![ + AccountMeta::new(token_key, false), + AccountMeta::new(owner_payer, true), + ]; + + let instruction = SolanaInstruction { + program_id, + data: ixn.encode()?, + accounts, + }; + + Ok(instruction) +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/lib.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/lib.rs new file mode 100644 index 0000000000..ee07e60b29 --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/lib.rs @@ -0,0 +1,16 @@ +//! Shared logic for all Hyperlane Sealevel Token programs. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod accounts; +pub mod error; +pub mod instruction; +pub mod message; +pub mod processor; + +pub use spl_associated_token_account; +pub use spl_noop; +pub use spl_token; +pub use spl_token_2022; diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/message.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/message.rs new file mode 100644 index 0000000000..a9633dd787 --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/message.rs @@ -0,0 +1,78 @@ +//! The Hyperlane Token message format. + +use hyperlane_core::{Decode, Encode, HyperlaneProtocolError, H256, U256}; + +/// Message contents sent or received by a Hyperlane Token program to indicate +/// a remote token transfer. +#[derive(Debug)] +pub struct TokenMessage { + recipient: H256, + amount_or_id: U256, + metadata: Vec, +} + +impl Encode for TokenMessage { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: std::io::Write, + { + writer.write_all(self.recipient.as_ref())?; + + let mut amount_or_id = [0_u8; 32]; + self.amount_or_id.to_big_endian(&mut amount_or_id); + writer.write_all(&amount_or_id)?; + + writer.write_all(&self.metadata)?; + + Ok(32 + 32 + self.metadata.len()) + } +} + +impl Decode for TokenMessage { + fn read_from(reader: &mut R) -> Result + where + R: std::io::Read, + { + let mut recipient = H256::zero(); + reader.read_exact(recipient.as_mut())?; + + let mut amount_or_id = [0_u8; 32]; + reader.read_exact(&mut amount_or_id)?; + let amount_or_id = U256::from_big_endian(&amount_or_id); + + let mut metadata = vec![]; + reader.read_to_end(&mut metadata)?; + + Ok(Self { + recipient, + amount_or_id, + metadata, + }) + } +} + +impl TokenMessage { + /// Creates a new token message. + pub fn new(recipient: H256, amount_or_id: U256, metadata: Vec) -> Self { + Self { + recipient, + amount_or_id, + metadata, + } + } + + /// The recipient of the token transfer. + pub fn recipient(&self) -> H256 { + self.recipient + } + + /// The amount or ID of the token transfer. + pub fn amount(&self) -> U256 { + self.amount_or_id + } + + /// The metadata of the token transfer. + pub fn metadata(&self) -> &[u8] { + &self.metadata + } +} diff --git a/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs b/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs new file mode 100644 index 0000000000..45622f69a8 --- /dev/null +++ b/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs @@ -0,0 +1,690 @@ +//! Processor logic shared by all Hyperlane Sealevel Token programs. + +use access_control::AccessControl; +use account_utils::create_pda_account; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{Decode, Encode}; +use hyperlane_sealevel_connection_client::{ + router::{ + HyperlaneRouterAccessControl, HyperlaneRouterDispatch, HyperlaneRouterMessageRecipient, + RemoteRouterConfig, + }, + HyperlaneConnectionClient, HyperlaneConnectionClientSetterAccessControl, +}; +use hyperlane_sealevel_mailbox::{ + mailbox_message_dispatch_authority_pda_seeds, mailbox_process_authority_pda_seeds, +}; +use hyperlane_sealevel_message_recipient_interface::HandleInstruction; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + instruction::AccountMeta, + msg, + program::set_return_data, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; +use std::collections::HashMap; + +use crate::{ + accounts::{HyperlaneToken, HyperlaneTokenAccount}, + error::Error, + instruction::{Init, TransferRemote}, + message::TokenMessage, +}; + +/// Seeds relating to the PDA account with information about this warp route. +/// For convenience in getting the account metas required for handling messages, +/// this is the same as the `HANDLE_ACCOUNT_METAS_PDA_SEEDS` in the message +/// recipient interface. +#[macro_export] +macro_rules! hyperlane_token_pda_seeds { + () => {{ + &[ + b"hyperlane_message_recipient", + b"-", + b"handle", + b"-", + b"account_metas", + ] + }}; + + ($bump_seed:expr) => {{ + &[ + b"hyperlane_message_recipient", + b"-", + b"handle", + b"-", + b"account_metas", + &[$bump_seed], + ] + }}; +} + +/// A plugin that handles token transfers for a Hyperlane Sealevel Token program. +pub trait HyperlaneSealevelTokenPlugin +where + Self: + BorshSerialize + BorshDeserialize + std::cmp::PartialEq + std::fmt::Debug + Default + Sized, +{ + /// Initializes the plugin. + fn initialize<'a, 'b>( + program_id: &Pubkey, + system_program: &'a AccountInfo<'b>, + token_account: &'a AccountInfo<'b>, + payer_account: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + ) -> Result; + + /// Transfers tokens into the program. + fn transfer_in<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + sender_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError>; + + /// Transfers tokens out of the program. + fn transfer_out<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + system_program: &'a AccountInfo<'b>, + recipient_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError>; + + /// Gets the AccountMetas required by the `transfer_out` function. + /// Returns (AccountMetas, whether recipient wallet must be writeable) + fn transfer_out_account_metas( + program_id: &Pubkey, + token: &HyperlaneToken, + token_message: &TokenMessage, + ) -> Result<(Vec, bool), ProgramError>; +} + +/// Core functionality of a Hyperlane Sealevel Token program that uses +/// a plugin to handle token transfers. +pub struct HyperlaneSealevelToken< + T: HyperlaneSealevelTokenPlugin + + BorshDeserialize + + BorshSerialize + + std::cmp::PartialEq + + std::fmt::Debug, +> { + _plugin: std::marker::PhantomData, +} + +impl HyperlaneSealevelToken +where + T: HyperlaneSealevelTokenPlugin + + BorshSerialize + + BorshDeserialize + + std::cmp::PartialEq + + std::fmt::Debug + + Default, +{ + /// Initializes the program. + /// + /// Accounts: + /// 0. [executable] The system program. + /// 1. [writable] The token PDA account. + /// 2. [writable] The dispatch authority PDA account. + /// 3. [signer] The payer and access control owner. + /// 4..N [??..??] Plugin-specific accounts. + pub fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + // On chain create appears to use realloc which is limited to 1024 byte increments. + let token_account_size = 2048; + + let accounts_iter = &mut accounts.iter(); + + // Account 0: System program + let system_program_id = solana_program::system_program::id(); + let system_program = next_account_info(accounts_iter)?; + if system_program.key != &system_program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 1: Token storage account + let token_account = next_account_info(accounts_iter)?; + let (token_key, token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + if &token_key != token_account.key { + return Err(ProgramError::IncorrectProgramId); + } + if !token_account.data_is_empty() || token_account.owner != &system_program_id { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Account 2: Dispatch authority PDA. + let dispatch_authority_account = next_account_info(accounts_iter)?; + let (dispatch_authority_key, dispatch_authority_bump) = Pubkey::find_program_address( + mailbox_message_dispatch_authority_pda_seeds!(), + program_id, + ); + if *dispatch_authority_account.key != dispatch_authority_key { + return Err(ProgramError::IncorrectProgramId); + } + if !dispatch_authority_account.data_is_empty() + || dispatch_authority_account.owner != &system_program_id + { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Account 3: Payer + let payer_account = next_account_info(accounts_iter)?; + if !payer_account.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Get the Mailbox's process authority that is specific to this program + // as a recipient. + let (mailbox_process_authority, _mailbox_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &init.mailbox, + ); + + let plugin_data = T::initialize( + program_id, + system_program, + token_account, + payer_account, + accounts_iter, + )?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + let rent = Rent::get()?; + + // Create token account PDA + create_pda_account( + payer_account, + &rent, + token_account_size, + program_id, + system_program, + token_account, + hyperlane_token_pda_seeds!(token_bump), + )?; + + // Create dispatch authority PDA + create_pda_account( + payer_account, + &rent, + 0, + program_id, + system_program, + dispatch_authority_account, + mailbox_message_dispatch_authority_pda_seeds!(dispatch_authority_bump), + )?; + + let token: HyperlaneToken = HyperlaneToken { + bump: token_bump, + mailbox: init.mailbox, + mailbox_process_authority, + dispatch_authority_bump, + owner: Some(*payer_account.key), + interchain_security_module: init.interchain_security_module, + decimals: init.decimals, + remote_decimals: init.remote_decimals, + remote_routers: HashMap::new(), + plugin_data, + }; + HyperlaneTokenAccount::::from(token).store(token_account, true)?; + + Ok(()) + } + + /// Transfers tokens to a remote. + /// Burns the tokens from the sender's associated token account and + /// then dispatches a message to the remote recipient. + /// + /// Accounts: + /// 0. [executable] The system program. + /// 1. [executable] The spl_noop program. + /// 2. [] The token PDA account. + /// 3. [executable] The mailbox program. + /// 4. [writeable] The mailbox outbox account. + /// 5. [] Message dispatch authority. + /// 6. [signer] The token sender and mailbox payer. + /// 7. [signer] Unique message account. + /// 8. [writeable] Message storage PDA. + /// 9..N [??..??] Plugin-specific accounts. + pub fn transfer_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + xfer: TransferRemote, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: System program. + let system_program_account = next_account_info(accounts_iter)?; + if system_program_account.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: SPL Noop. + let spl_noop = next_account_info(accounts_iter)?; + if spl_noop.key != &spl_noop::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 2: Token storage account + let token_account = next_account_info(accounts_iter)?; + let token = + HyperlaneTokenAccount::fetch(&mut &token_account.data.borrow()[..])?.into_inner(); + let token_seeds: &[&[u8]] = hyperlane_token_pda_seeds!(token.bump); + let expected_token_key = Pubkey::create_program_address(token_seeds, program_id)?; + if token_account.key != &expected_token_key { + return Err(ProgramError::InvalidArgument); + } + if token_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: Mailbox program + let mailbox_info = next_account_info(accounts_iter)?; + if mailbox_info.key != &token.mailbox { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 4: Mailbox Outbox data account. + // No verification is performed here, the Mailbox will do that. + let mailbox_outbox_account = next_account_info(accounts_iter)?; + + // Account 5: Message dispatch authority + let dispatch_authority_account = next_account_info(accounts_iter)?; + let dispatch_authority_seeds: &[&[u8]] = + mailbox_message_dispatch_authority_pda_seeds!(token.dispatch_authority_bump); + let dispatch_authority_key = + Pubkey::create_program_address(dispatch_authority_seeds, program_id)?; + if *dispatch_authority_account.key != dispatch_authority_key { + return Err(ProgramError::InvalidArgument); + } + + // Account 6: Sender account / mailbox payer + let sender_wallet = next_account_info(accounts_iter)?; + if !sender_wallet.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 7: Unique message account + // Defer to the checks in the Mailbox, no need to verify anything here. + let unique_message_account = next_account_info(accounts_iter)?; + + // Account 8: Message storage PDA. + // Similarly defer to the checks in the Mailbox to ensure account validity. + let dispatched_message_pda = next_account_info(accounts_iter)?; + + // The amount denominated in the local decimals. + let local_amount: u64 = xfer + .amount_or_id + .try_into() + .map_err(|_| Error::IntegerOverflow)?; + // Convert to the remote number of decimals, which is universally understood + // by the remote routers as the number of decimals used by the message amount. + let remote_amount = token.local_amount_to_remote_amount(local_amount)?; + + // Transfer `local_amount` of tokens in... + T::transfer_in( + program_id, + &*token, + sender_wallet, + accounts_iter, + local_amount, + )?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + // The token message body, which specifies the remote_amount. + let token_transfer_message = + TokenMessage::new(xfer.recipient, remote_amount, vec![]).to_vec(); + + // Dispatch the message. + token.dispatch( + program_id, + dispatch_authority_seeds, + xfer.destination_domain, + token_transfer_message, + vec![ + AccountMeta::new(*mailbox_outbox_account.key, false), + AccountMeta::new_readonly(*dispatch_authority_account.key, true), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new(*sender_wallet.key, true), + AccountMeta::new_readonly(*unique_message_account.key, true), + AccountMeta::new(*dispatched_message_pda.key, false), + ], + &[ + mailbox_outbox_account.clone(), + dispatch_authority_account.clone(), + system_program_account.clone(), + spl_noop.clone(), + sender_wallet.clone(), + unique_message_account.clone(), + dispatched_message_pda.clone(), + ], + )?; + + msg!( + "Warp route transfer completed to destination: {}, recipient: {}, remote_amount: {}", + xfer.destination_domain, + xfer.recipient, + remote_amount + ); + + Ok(()) + } + + /// Accounts: + /// 0. [signer] Mailbox processor authority specific to this program. + /// 1. [executable] system_program + /// 2. [] hyperlane_token storage + /// 3. [depends on plugin] recipient wallet address + /// 4..N [??..??] Plugin-specific accounts. + pub fn transfer_from_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + xfer: HandleInstruction, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let mut message_reader = std::io::Cursor::new(xfer.message); + let message = TokenMessage::read_from(&mut message_reader) + .map_err(|_err| ProgramError::from(Error::MessageDecodeError))?; + + // Account 0: Mailbox authority + // This is verified further below. + let process_authority_account = next_account_info(accounts_iter)?; + + // Account 1: System program + let system_program = next_account_info(accounts_iter)?; + if system_program.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 2: Token account + let token_account = next_account_info(accounts_iter)?; + let token = + HyperlaneTokenAccount::fetch(&mut &token_account.data.borrow()[..])?.into_inner(); + let token_seeds: &[&[u8]] = hyperlane_token_pda_seeds!(token.bump); + let expected_token_key = Pubkey::create_program_address(token_seeds, program_id)?; + if token_account.key != &expected_token_key { + return Err(ProgramError::InvalidArgument); + } + if token_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: Recipient wallet + let recipient_wallet = next_account_info(accounts_iter)?; + let expected_recipient = Pubkey::new_from_array(message.recipient().into()); + if recipient_wallet.key != &expected_recipient { + return Err(ProgramError::InvalidArgument); + } + + // Verify the authenticity of the message. + // This ensures the `process_authority_account` is valid and a signer, + // and that the sender is the remote router for the origin. + token.ensure_valid_router_message(process_authority_account, xfer.origin, &xfer.sender)?; + + // The amount denominated in the remote decimals. + let remote_amount = message.amount(); + // Convert to the local number of decimals. + let local_amount: u64 = token.remote_amount_to_local_amount(remote_amount)?; + + // Transfer the `local_amount` of tokens out. + T::transfer_out( + program_id, + &*token, + system_program, + recipient_wallet, + accounts_iter, + local_amount, + )?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + msg!( + "Warp route transfer completed from origin: {}, recipient: {}, remote_amount: {}", + xfer.origin, + recipient_wallet.key, + remote_amount + ); + + Ok(()) + } + + /// Gets the account metas required by the `HandleInstruction` instruction, + /// serializes them, and sets them as return data. + /// + /// Accounts: + /// 0. [] The token PDA, which is the PDA with the seeds `HANDLE_ACCOUNT_METAS_PDA_SEEDS`. + pub fn transfer_from_remote_account_metas( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let mut message_reader = std::io::Cursor::new(transfer.message); + let message = TokenMessage::read_from(&mut message_reader) + .map_err(|_err| ProgramError::from(Error::MessageDecodeError))?; + + // Account 0: Token account. + let token_account_info = next_account_info(accounts_iter)?; + let token = HyperlaneToken::verify_account_and_fetch_inner(program_id, token_account_info)?; + + let (transfer_out_account_metas, writeable_recipient) = + T::transfer_out_account_metas(program_id, &token, &message)?; + + let mut accounts: Vec = vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false).into(), + AccountMeta::new_readonly(*token_account_info.key, false).into(), + AccountMeta { + pubkey: Pubkey::new_from_array(message.recipient().into()), + is_signer: false, + is_writable: writeable_recipient, + } + .into(), + ]; + accounts.extend(transfer_out_account_metas); + + // Wrap it in the SimulationReturnData because serialized account_metas + // may end with zero byte(s), which are incorrectly truncated as + // simulated transaction return data. + // See `SimulationReturnData` for details. + let bytes = SimulationReturnData::new(accounts) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + set_return_data(&bytes[..]); + + Ok(()) + } + + /// Enrolls a remote router. + /// + /// Accounts: + /// 0. [writeable] The token PDA account. + /// 1. [signer] The owner. + pub fn enroll_remote_router( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: RemoteRouterConfig, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Token account + let token_account = next_account_info(accounts_iter)?; + let mut token = + HyperlaneTokenAccount::fetch(&mut &token_account.data.borrow()[..])?.into_inner(); + let token_seeds: &[&[u8]] = hyperlane_token_pda_seeds!(token.bump); + let expected_token_key = Pubkey::create_program_address(token_seeds, program_id)?; + if token_account.key != &expected_token_key { + return Err(ProgramError::InvalidArgument); + } + if token_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 1: Owner + let owner_account = next_account_info(accounts_iter)?; + + // This errors if owner_account is not really the owner. + token.enroll_remote_router_only_owner(owner_account, config)?; + + // Store the updated token account. + HyperlaneTokenAccount::::from(token).store(token_account, true)?; + + Ok(()) + } + + /// Enrolls remote routers. + /// + /// Accounts: + /// 0. [writeable] The token PDA account. + /// 1. [signer] The owner. + pub fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Token account + let token_account = next_account_info(accounts_iter)?; + let mut token = + HyperlaneTokenAccount::fetch(&mut &token_account.data.borrow()[..])?.into_inner(); + let token_seeds: &[&[u8]] = hyperlane_token_pda_seeds!(token.bump); + let expected_token_key = Pubkey::create_program_address(token_seeds, program_id)?; + if token_account.key != &expected_token_key { + return Err(ProgramError::InvalidArgument); + } + if token_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 1: Owner + let owner_account = next_account_info(accounts_iter)?; + + // This errors if owner_account is not really the owner. + token.enroll_remote_routers_only_owner(owner_account, configs)?; + + // Store the updated token account. + HyperlaneTokenAccount::::from(token).store(token_account, true)?; + + Ok(()) + } + + /// Transfers ownership. + /// + /// Accounts: + /// 0. [writeable] The token PDA account. + /// 1. [signer] The current owner. + pub fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Token account + let token_account = next_account_info(accounts_iter)?; + let mut token = HyperlaneToken::verify_account_and_fetch_inner(program_id, token_account)?; + + // Account 1: Owner + let owner_account = next_account_info(accounts_iter)?; + + // This errors if owner_account is not really the owner. + token.transfer_ownership(owner_account, new_owner)?; + + // Store the updated token account. + HyperlaneTokenAccount::::from(token).store(token_account, true)?; + + Ok(()) + } + + /// Gets the interchain security module. + /// + /// Accounts: + /// 0. [] The token PDA account. + pub fn interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Token account + let token_account = next_account_info(accounts_iter)?; + let token = HyperlaneToken::::verify_account_and_fetch_inner(program_id, token_account)?; + + // Set the return data to the serialized Option representing + // the ISM. + token.set_interchain_security_module_return_data(); + + Ok(()) + } + + /// Gets the account metas required to get the ISM, serializes them, + /// and sets them as return data. + /// + /// Accounts: + /// None + pub fn interchain_security_module_account_metas(program_id: &Pubkey) -> ProgramResult { + let (token_key, _token_bump) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let account_metas: Vec = + vec![AccountMeta::new_readonly(token_key, false).into()]; + + // Wrap it in the SimulationReturnData because serialized account_metas + // may end with zero byte(s), which are incorrectly truncated as + // simulated transaction return data. + // See `SimulationReturnData` for details. + let bytes = SimulationReturnData::new(account_metas) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + set_return_data(&bytes[..]); + + Ok(()) + } + + /// Lets the owner set the interchain security module. + /// + /// Accounts: + /// 0. [writeable] The token PDA account. + /// 1. [signer] The access control owner. + pub fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + ism: Option, + ) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Token account + let token_account = next_account_info(accounts_iter)?; + let mut token = HyperlaneToken::verify_account_and_fetch_inner(program_id, token_account)?; + + // Account 1: Owner + let owner_account = next_account_info(accounts_iter)?; + + // This errors if owner_account is not really the owner. + token.set_interchain_security_module_only_owner(owner_account, ism)?; + + // Store the updated token account. + HyperlaneTokenAccount::::from(token).store(token_account, true)?; + + Ok(()) + } +} diff --git a/rust/sealevel/libraries/interchain-security-module-interface/Cargo.toml b/rust/sealevel/libraries/interchain-security-module-interface/Cargo.toml new file mode 100644 index 0000000000..e289bfd35b --- /dev/null +++ b/rust/sealevel/libraries/interchain-security-module-interface/Cargo.toml @@ -0,0 +1,17 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-interchain-security-module-interface" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +solana-program.workspace = true +spl-type-length-value.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/interchain-security-module-interface/src/lib.rs b/rust/sealevel/libraries/interchain-security-module-interface/src/lib.rs new file mode 100644 index 0000000000..7f297f15a8 --- /dev/null +++ b/rust/sealevel/libraries/interchain-security-module-interface/src/lib.rs @@ -0,0 +1,169 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::program_error::ProgramError; +use spl_type_length_value::discriminator::Discriminator; + +/// Instructions that a Hyperlane interchain security module is expected to process. +/// The first 8 bytes of the encoded instruction is a discriminator that +/// allows programs to implement the required interface. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum InterchainSecurityModuleInstruction { + /// Gets the type of ISM. + Type, + /// Verifies a message. + Verify(VerifyInstruction), + /// Gets the list of AccountMetas required for the `Verify` instruction. + /// The only account expected to be passed into this instruction is the + /// read-only PDA relating to the program ID and the seeds `VERIFY_ACCOUNT_METAS_PDA_SEEDS` + VerifyAccountMetas(VerifyInstruction), +} + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-interchain-security-module:type"])` +const TYPE_DISCRIMINATOR: [u8; Discriminator::LENGTH] = [105, 97, 97, 88, 63, 124, 106, 18]; +const TYPE_DISCRIMINATOR_SLICE: &[u8] = &TYPE_DISCRIMINATOR; + +#[derive(Eq, PartialEq, BorshSerialize, BorshDeserialize, Debug, Clone)] +pub struct VerifyInstruction { + pub metadata: Vec, + pub message: Vec, +} + +impl VerifyInstruction { + pub fn new(metadata: Vec, message: Vec) -> Self { + Self { metadata, message } + } +} + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-interchain-security-module:verify"])` +const VERIFY_DISCRIMINATOR: [u8; Discriminator::LENGTH] = [243, 53, 214, 0, 208, 18, 231, 67]; +const VERIFY_DISCRIMINATOR_SLICE: &[u8] = &VERIFY_DISCRIMINATOR; + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-interchain-security-module:verify-account-metas"])` +const VERIFY_ACCOUNT_METAS_DISCRIMINATOR: [u8; Discriminator::LENGTH] = + [200, 65, 157, 12, 89, 255, 131, 216]; +const VERIFY_ACCOUNT_METAS_DISCRIMINATOR_SLICE: &[u8] = &VERIFY_ACCOUNT_METAS_DISCRIMINATOR; + +/// Seeds for the PDA that's expected to be passed into the `VerifyAccountMetas` +/// instruction. +pub const VERIFY_ACCOUNT_METAS_PDA_SEEDS: &[&[u8]] = + &[b"hyperlane_ism", b"-", b"verify", b"-", b"account_metas"]; + +impl InterchainSecurityModuleInstruction { + pub fn encode(&self) -> Result, ProgramError> { + let mut buf = vec![]; + match self { + InterchainSecurityModuleInstruction::Type => { + buf.extend_from_slice(TYPE_DISCRIMINATOR_SLICE); + } + InterchainSecurityModuleInstruction::Verify(instruction) => { + buf.extend_from_slice(VERIFY_DISCRIMINATOR_SLICE); + buf.extend_from_slice( + &instruction + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + } + InterchainSecurityModuleInstruction::VerifyAccountMetas(instruction) => { + buf.extend_from_slice(VERIFY_ACCOUNT_METAS_DISCRIMINATOR_SLICE); + buf.extend_from_slice( + &instruction + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + } + } + + Ok(buf) + } + + pub fn decode(buf: &[u8]) -> Result { + if buf.len() < Discriminator::LENGTH { + return Err(ProgramError::InvalidInstructionData); + } + let (discriminator, rest) = buf.split_at(Discriminator::LENGTH); + match discriminator { + TYPE_DISCRIMINATOR_SLICE => Ok(Self::Type), + VERIFY_DISCRIMINATOR_SLICE => { + let instruction = VerifyInstruction::try_from_slice(rest) + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + Ok(Self::Verify(instruction)) + } + VERIFY_ACCOUNT_METAS_DISCRIMINATOR_SLICE => { + let instruction = VerifyInstruction::try_from_slice(rest) + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + Ok(Self::VerifyAccountMetas(instruction)) + } + _ => Err(ProgramError::InvalidInstructionData), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_program::hash::hashv; + + #[test] + fn test_discriminator_slices() { + assert_eq!( + &hashv(&[b"hyperlane-interchain-security-module:type"]).to_bytes() + [..Discriminator::LENGTH], + TYPE_DISCRIMINATOR_SLICE, + ); + + assert_eq!( + &hashv(&[b"hyperlane-interchain-security-module:verify"]).to_bytes() + [..Discriminator::LENGTH], + VERIFY_DISCRIMINATOR_SLICE, + ); + + assert_eq!( + &hashv(&[b"hyperlane-interchain-security-module:verify-account-metas"]).to_bytes() + [..Discriminator::LENGTH], + VERIFY_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + } + + #[test] + fn test_encode_decode_type_instruction() { + let instruction = InterchainSecurityModuleInstruction::Type; + + let encoded = instruction.encode().unwrap(); + assert_eq!(&encoded[..Discriminator::LENGTH], TYPE_DISCRIMINATOR_SLICE,); + + let decoded = InterchainSecurityModuleInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } + + #[test] + fn test_encode_decode_verify_instruction() { + let instruction = InterchainSecurityModuleInstruction::Verify(VerifyInstruction::new( + vec![5, 4, 3, 2, 1], + vec![1, 2, 3, 4, 5], + )); + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + VERIFY_DISCRIMINATOR_SLICE, + ); + + let decoded = InterchainSecurityModuleInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } + + #[test] + fn test_encode_decode_verify_account_metas_instruction() { + let instruction = InterchainSecurityModuleInstruction::VerifyAccountMetas( + VerifyInstruction::new(vec![5, 4, 3, 2, 1], vec![1, 2, 3, 4, 5]), + ); + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + VERIFY_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + + let decoded = InterchainSecurityModuleInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } +} diff --git a/rust/sealevel/libraries/message-recipient-interface/Cargo.toml b/rust/sealevel/libraries/message-recipient-interface/Cargo.toml new file mode 100644 index 0000000000..e6a6a41049 --- /dev/null +++ b/rust/sealevel/libraries/message-recipient-interface/Cargo.toml @@ -0,0 +1,19 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-message-recipient-interface" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +solana-program.workspace = true +spl-type-length-value.workspace = true + +hyperlane-core = { path = "../../../hyperlane-core" } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/message-recipient-interface/src/lib.rs b/rust/sealevel/libraries/message-recipient-interface/src/lib.rs new file mode 100644 index 0000000000..f1fba4f7f1 --- /dev/null +++ b/rust/sealevel/libraries/message-recipient-interface/src/lib.rs @@ -0,0 +1,237 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::H256; +use solana_program::program_error::ProgramError; +use spl_type_length_value::discriminator::Discriminator; + +/// Instructions that a Hyperlane message recipient is expected to process. +/// The first 8 bytes of the encoded instruction is a discriminator that +/// allows programs to implement the required interface. +#[derive(Eq, PartialEq, Debug)] +pub enum MessageRecipientInstruction { + /// Gets the ISM that should verify the message. + InterchainSecurityModule, + /// Gets the account metas required for the `InterchainSecurityModule` instruction. + /// Intended to be simulated by an off-chain client. + /// The only account passed into this instruction is expected to be + /// the read-only PDA relating to the program ID and the seeds + /// `INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_PDA_SEEDS` + InterchainSecurityModuleAccountMetas, + /// Handles a message from the Mailbox. + Handle(HandleInstruction), + /// Gets the account metas required for the `Handle` instruction. + /// Intended to be simulated by an off-chain client. + /// The only account passed into this instruction is expected to be + /// the read-only PDA relating to the program ID and the seeds + /// `HANDLE_ACCOUNT_METAS_PDA_SEEDS` + HandleAccountMetas(HandleInstruction), +} + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-message-recipient:interchain-security-module"])` +const INTERCHAIN_SECURITY_MODULE_DISCRIMINATOR: [u8; Discriminator::LENGTH] = + [45, 18, 245, 87, 234, 46, 246, 15]; +const INTERCHAIN_SECURITY_MODULE_DISCRIMINATOR_SLICE: &[u8] = + &INTERCHAIN_SECURITY_MODULE_DISCRIMINATOR; + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-message-recipient:interchain-security-module-account-metas"])` +const INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_DISCRIMINATOR: [u8; Discriminator::LENGTH] = + [190, 214, 218, 129, 67, 97, 4, 76]; +const INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_DISCRIMINATOR_SLICE: &[u8] = + &INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_DISCRIMINATOR; + +/// Seeds for the PDA that's expected to be passed into the `InterchainSecurityModuleAccountMetas` +/// instruction. +pub const INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_PDA_SEEDS: &[&[u8]] = &[ + b"hyperlane_message_recipient", + b"-", + b"interchain_security_module", + b"-", + b"account_metas", +]; + +#[derive(Eq, PartialEq, BorshSerialize, BorshDeserialize, Debug)] +pub struct HandleInstruction { + pub origin: u32, + pub sender: H256, + pub message: Vec, +} + +impl HandleInstruction { + pub fn new(origin: u32, sender: H256, message: Vec) -> Self { + Self { + origin, + sender, + message, + } + } +} + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-message-recipient:handle"])` +const HANDLE_DISCRIMINATOR: [u8; Discriminator::LENGTH] = [33, 210, 5, 66, 196, 212, 239, 142]; +const HANDLE_DISCRIMINATOR_SLICE: &[u8] = &HANDLE_DISCRIMINATOR; + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-message-recipient:handle-account-metas"])` +const HANDLE_ACCOUNT_METAS_DISCRIMINATOR: [u8; Discriminator::LENGTH] = + [194, 141, 30, 82, 241, 41, 169, 52]; +const HANDLE_ACCOUNT_METAS_DISCRIMINATOR_SLICE: &[u8] = &HANDLE_ACCOUNT_METAS_DISCRIMINATOR; + +/// Seeds for the PDA that's expected to be passed into the `HandleAccountMetas` +/// instruction. +pub const HANDLE_ACCOUNT_METAS_PDA_SEEDS: &[&[u8]] = &[ + b"hyperlane_message_recipient", + b"-", + b"handle", + b"-", + b"account_metas", +]; + +impl MessageRecipientInstruction { + pub fn encode(&self) -> Result, ProgramError> { + let mut buf = vec![]; + match self { + MessageRecipientInstruction::InterchainSecurityModule => { + buf.extend_from_slice(INTERCHAIN_SECURITY_MODULE_DISCRIMINATOR_SLICE); + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + buf.extend_from_slice(INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_DISCRIMINATOR_SLICE); + } + MessageRecipientInstruction::Handle(instruction) => { + buf.extend_from_slice(HANDLE_DISCRIMINATOR_SLICE); + buf.extend_from_slice( + &instruction + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + } + MessageRecipientInstruction::HandleAccountMetas(instruction) => { + buf.extend_from_slice(HANDLE_ACCOUNT_METAS_DISCRIMINATOR_SLICE); + buf.extend_from_slice( + &instruction + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + } + } + + Ok(buf) + } + + pub fn decode(buf: &[u8]) -> Result { + if buf.len() < Discriminator::LENGTH { + return Err(ProgramError::InvalidInstructionData); + } + let (discriminator, rest) = buf.split_at(Discriminator::LENGTH); + match discriminator { + INTERCHAIN_SECURITY_MODULE_DISCRIMINATOR_SLICE => Ok(Self::InterchainSecurityModule), + INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_DISCRIMINATOR_SLICE => { + Ok(Self::InterchainSecurityModuleAccountMetas) + } + HANDLE_DISCRIMINATOR_SLICE => { + let instruction = HandleInstruction::try_from_slice(rest) + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + Ok(Self::Handle(instruction)) + } + HANDLE_ACCOUNT_METAS_DISCRIMINATOR_SLICE => { + let instruction = HandleInstruction::try_from_slice(rest) + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + Ok(Self::HandleAccountMetas(instruction)) + } + _ => Err(ProgramError::InvalidInstructionData), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_program::hash::hashv; + + #[test] + fn test_discriminator_slices() { + assert_eq!( + &hashv(&[b"hyperlane-message-recipient:interchain-security-module"]).to_bytes() + [..Discriminator::LENGTH], + INTERCHAIN_SECURITY_MODULE_DISCRIMINATOR_SLICE, + ); + + assert_eq!( + &hashv(&[b"hyperlane-message-recipient:interchain-security-module-account-metas"]) + .to_bytes()[..Discriminator::LENGTH], + INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + + assert_eq!( + &hashv(&[b"hyperlane-message-recipient:handle"]).to_bytes()[..Discriminator::LENGTH], + HANDLE_DISCRIMINATOR_SLICE, + ); + + assert_eq!( + &hashv(&[b"hyperlane-message-recipient:handle-account-metas"]).to_bytes() + [..Discriminator::LENGTH], + HANDLE_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + } + + #[test] + fn test_encode_decode_interchain_security_module_instruction() { + let instruction = MessageRecipientInstruction::InterchainSecurityModule; + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + INTERCHAIN_SECURITY_MODULE_DISCRIMINATOR_SLICE, + ); + + let decoded = MessageRecipientInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } + + #[test] + fn test_encode_decode_interchain_security_module_account_metas_instruction() { + let instruction = MessageRecipientInstruction::InterchainSecurityModuleAccountMetas; + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + + let decoded = MessageRecipientInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } + + #[test] + fn test_encode_decode_handle_instruction() { + let instruction = MessageRecipientInstruction::Handle(HandleInstruction::new( + 69, + H256::random(), + vec![1, 2, 3, 4, 5], + )); + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + HANDLE_DISCRIMINATOR_SLICE, + ); + + let decoded = MessageRecipientInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } + + #[test] + fn test_encode_decode_handle_account_metas_instruction() { + let instruction = MessageRecipientInstruction::HandleAccountMetas(HandleInstruction::new( + 69, + H256::random(), + vec![1, 2, 3, 4, 5], + )); + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + HANDLE_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + + let decoded = MessageRecipientInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } +} diff --git a/rust/sealevel/libraries/multisig-ism/Cargo.toml b/rust/sealevel/libraries/multisig-ism/Cargo.toml new file mode 100644 index 0000000000..118159c16d --- /dev/null +++ b/rust/sealevel/libraries/multisig-ism/Cargo.toml @@ -0,0 +1,26 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "multisig-ism" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-data = ["dep:hex"] + +[dependencies] +borsh.workspace = true +hex = { workspace = true, optional = true } +solana-program.workspace = true +spl-type-length-value.workspace = true +thiserror.workspace = true + +hyperlane-core = { path = "../../../hyperlane-core" } +ecdsa-signature = { path = "../ecdsa-signature" } + +[dev-dependencies] +hex.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/multisig-ism/src/error.rs b/rust/sealevel/libraries/multisig-ism/src/error.rs new file mode 100644 index 0000000000..90717cf31a --- /dev/null +++ b/rust/sealevel/libraries/multisig-ism/src/error.rs @@ -0,0 +1,8 @@ +/// Errors relating to a MultisigIsm +#[derive(Copy, Clone, Debug, Eq, thiserror::Error, PartialEq)] +pub enum MultisigIsmError { + #[error("Invalid signature")] + InvalidSignature, + #[error("Threshold not met")] + ThresholdNotMet, +} diff --git a/rust/sealevel/libraries/multisig-ism/src/interface.rs b/rust/sealevel/libraries/multisig-ism/src/interface.rs new file mode 100644 index 0000000000..0584221a3d --- /dev/null +++ b/rust/sealevel/libraries/multisig-ism/src/interface.rs @@ -0,0 +1,124 @@ +use solana_program::program_error::ProgramError; +use spl_type_length_value::discriminator::Discriminator; + +/// Instructions that a Hyperlane Multisig ISM is expected to process. +/// The first 8 bytes of the encoded instruction is a discriminator that +/// allows programs to implement the required interface. +#[derive(Eq, PartialEq, Debug)] +pub enum MultisigIsmInstruction { + /// Gets the validators and threshold for the provided message. + ValidatorsAndThreshold(Vec), + /// Gets the account metas required for an instruction to the + /// `ValidatorsAndThreshold` program. + /// Intended to be simulated by an off-chain client. + /// The only account passed into this instruction is expected to be + /// the read-only PDA relating to the program ID and the seeds + /// `VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS` + ValidatorsAndThresholdAccountMetas(Vec), +} + +/// First 8 bytes of `hash::hashv(&[b"hyperlane-multisig-ism:validators-and-threshold"])` +const VALIDATORS_AND_THRESHOLD_DISCRIMINATOR: [u8; Discriminator::LENGTH] = + [82, 96, 5, 220, 241, 173, 13, 50]; +const VALIDATORS_AND_THRESHOLD_DISCRIMINATOR_SLICE: &[u8] = &VALIDATORS_AND_THRESHOLD_DISCRIMINATOR; + +const VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_DISCRIMINATOR: [u8; Discriminator::LENGTH] = + [113, 7, 132, 85, 239, 247, 157, 204]; +const VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_DISCRIMINATOR_SLICE: &[u8] = + &VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_DISCRIMINATOR; + +/// Seeds for the PDA that's expected to be passed into the `ValidatorsAndThresholdAccountMetas` +/// instruction. +pub const VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS: &[&[u8]] = &[ + b"hyperlane_multisig_ism", + b"-", + b"validators_and_threshold", + b"-", + b"account_metas", +]; + +impl MultisigIsmInstruction { + pub fn encode(&self) -> Result, ProgramError> { + let mut buf = vec![]; + match self { + MultisigIsmInstruction::ValidatorsAndThreshold(message) => { + buf.extend_from_slice(VALIDATORS_AND_THRESHOLD_DISCRIMINATOR_SLICE); + buf.extend_from_slice(&message[..]); + } + MultisigIsmInstruction::ValidatorsAndThresholdAccountMetas(message) => { + buf.extend_from_slice(VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_DISCRIMINATOR_SLICE); + buf.extend_from_slice(&message[..]); + } + } + + Ok(buf) + } + + pub fn decode(buf: &[u8]) -> Result { + if buf.len() < Discriminator::LENGTH { + return Err(ProgramError::InvalidInstructionData); + } + let (discriminator, rest) = buf.split_at(Discriminator::LENGTH); + match discriminator { + VALIDATORS_AND_THRESHOLD_DISCRIMINATOR_SLICE => { + let message = rest.to_vec(); + Ok(Self::ValidatorsAndThreshold(message)) + } + VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_DISCRIMINATOR_SLICE => { + let message = rest.to_vec(); + Ok(Self::ValidatorsAndThresholdAccountMetas(message)) + } + _ => Err(ProgramError::InvalidInstructionData), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_program::hash::hashv; + + #[test] + fn test_discriminator_slices() { + assert_eq!( + &hashv(&[b"hyperlane-multisig-ism:validators-and-threshold"]).to_bytes() + [..Discriminator::LENGTH], + VALIDATORS_AND_THRESHOLD_DISCRIMINATOR_SLICE, + ); + + assert_eq!( + &hashv(&[b"hyperlane-multisig-ism:validators-and-threshold-account-metas"]).to_bytes() + [..Discriminator::LENGTH], + VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + } + + #[test] + fn test_encode_decode_validators_and_threshold_instruction() { + let instruction = MultisigIsmInstruction::ValidatorsAndThreshold(vec![1, 2, 3, 4, 5]); + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + VALIDATORS_AND_THRESHOLD_DISCRIMINATOR_SLICE, + ); + + let decoded = MultisigIsmInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } + + #[test] + fn test_encode_decode_validators_and_threshold_account_metas_instruction() { + let instruction = + MultisigIsmInstruction::ValidatorsAndThresholdAccountMetas(vec![1, 2, 3, 4, 5]); + + let encoded = instruction.encode().unwrap(); + assert_eq!( + &encoded[..Discriminator::LENGTH], + VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_DISCRIMINATOR_SLICE, + ); + + let decoded = MultisigIsmInstruction::decode(&encoded).unwrap(); + assert_eq!(instruction, decoded); + } +} diff --git a/rust/sealevel/libraries/multisig-ism/src/lib.rs b/rust/sealevel/libraries/multisig-ism/src/lib.rs new file mode 100644 index 0000000000..e8f1007635 --- /dev/null +++ b/rust/sealevel/libraries/multisig-ism/src/lib.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod interface; +pub mod multisig; +#[cfg(feature = "test-data")] +pub mod test_data; + +pub use crate::multisig::MultisigIsm; diff --git a/rust/sealevel/libraries/multisig-ism/src/multisig.rs b/rust/sealevel/libraries/multisig-ism/src/multisig.rs new file mode 100644 index 0000000000..d744bd55d5 --- /dev/null +++ b/rust/sealevel/libraries/multisig-ism/src/multisig.rs @@ -0,0 +1,189 @@ +use crate::error::MultisigIsmError; +use ecdsa_signature::EcdsaSignature; +use hyperlane_core::{Signable, H160}; + +/// A type for verifying a quorum of ECDSA signatures from a validator set +/// over a signable data type. +pub struct MultisigIsm { + signed_data: T, + signatures: Vec, + validators: Vec, + threshold: u8, +} + +impl MultisigIsm { + pub fn new( + signed_data: T, + signatures: Vec, + validators: Vec, + threshold: u8, + ) -> Self { + Self { + signed_data, + signatures, + validators, + threshold, + } + } + + /// Returns Ok(()) if there is a quorum of validator signatures over the + /// signed data. + /// Requires the signatures over the signed data to be ordered by the `this.validators` + /// ordering. + /// Returns an error if the threshold is not met or if any of the signatures are invalid. + pub fn verify(&self) -> Result<(), MultisigIsmError> { + let signed_digest = self.signed_data.eth_signed_message_hash(); + let signed_digest_bytes = signed_digest.as_bytes(); + + let validator_count = self.validators.len(); + let mut validator_index = 0; + + // Assumes that signatures are ordered by validator + for i in 0..self.threshold { + let signer = self.signatures[i as usize] + .secp256k1_recover_ethereum_address(signed_digest_bytes) + .map_err(|_| MultisigIsmError::InvalidSignature)?; + + while validator_index < validator_count && signer != self.validators[validator_index] { + validator_index += 1; + } + + if validator_index >= validator_count { + return Err(MultisigIsmError::ThresholdNotMet); + } + + validator_index += 1; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use ecdsa_signature::EcdsaSignature; + + use std::str::FromStr; + + use hyperlane_core::H256; + + struct TestSignedPayload(); + + impl Signable for TestSignedPayload { + fn signing_hash(&self) -> H256 { + H256::from_str("0xf00000000000000000000000000000000000000000000000000000000000000f") + .unwrap() + } + } + + #[test] + fn test_secp256k1_recover_ethereum_address() { + // A test signature from this Ethereum address: + // Address: 0xfdB65576568b99A8a00a292577b8fc51abB115bD + // Private Key: 0x87368bfca2e509afbb87838a64a68bc34b8f7962a0496d12df6200e3401be691 + // The signature was generated using ethers-js: + // wallet = new ethers.Wallet('0x87368bfca2e509afbb87838a64a68bc34b8f7962a0496d12df6200e3401be691') + // await wallet.signMessage(ethers.utils.arrayify('0xf00000000000000000000000000000000000000000000000000000000000000f')) + + let signature = EcdsaSignature::from_bytes( + &hex::decode("4e561dcd350b7a271c7247843f7731a8a9810037c13784f5b3a9616788ca536976c5ff70b1865c4568e273a375851a5304dc7a1ac54f0783f3dde38d345313a91c").unwrap()[..] + ).unwrap(); + + let signed_hash = TestSignedPayload().eth_signed_message_hash(); + + let recovered_signer = signature + .secp256k1_recover_ethereum_address(signed_hash.as_fixed_bytes()) + .unwrap(); + assert_eq!( + recovered_signer, + H160::from_str("0xfdB65576568b99A8a00a292577b8fc51abB115bD").unwrap() + ); + } + + #[test] + fn test_multisig_ism_verify_success() { + // A test signature from this Ethereum address: + // Address: 0xfdB65576568b99A8a00a292577b8fc51abB115bD + // Private Key: 0x87368bfca2e509afbb87838a64a68bc34b8f7962a0496d12df6200e3401be691 + // The signature was generated using ethers-js: + // wallet = new ethers.Wallet('0x87368bfca2e509afbb87838a64a68bc34b8f7962a0496d12df6200e3401be691') + // await wallet.signMessage(ethers.utils.arrayify('0xf00000000000000000000000000000000000000000000000000000000000000f')) + + let validator_0 = H160::from_str("0xfdB65576568b99A8a00a292577b8fc51abB115bD").unwrap(); + let signature_0 = EcdsaSignature::from_bytes( + &hex::decode("4e561dcd350b7a271c7247843f7731a8a9810037c13784f5b3a9616788ca536976c5ff70b1865c4568e273a375851a5304dc7a1ac54f0783f3dde38d345313a91c").unwrap()[..] + ).unwrap(); + + // Address: 0x5090cEd8BC5A7D3c2FbE2b2702eE4a8e7b227181 + // Private Key: 0xe2dc693322e2b96b4405cb635cb3fb8aa35f65cca9c9171d54dd6f6dfe23dd14 + + let validator_1 = H160::from_str("0x5090cEd8BC5A7D3c2FbE2b2702eE4a8e7b227181").unwrap(); + let signature_1 = EcdsaSignature::from_bytes( + &hex::decode("9d510e0d988e44cf05a4e29d7b1ecec6e3277a8be137164f89d6cf52325190f058101ef9aa57d118f9452a38c156efbdb1b69d4022ac2c35370c433ca5b61aeb1c").unwrap()[..] + ).unwrap(); + + let multisig_ism = MultisigIsm::new( + TestSignedPayload(), + vec![signature_0, signature_1], + vec![validator_0, validator_1], + 2, + ); + + let result = multisig_ism.verify(); + assert!(result.is_ok()); + } + + #[test] + fn test_multisig_ism_verify_threshold_not_met() { + let validator_0 = H160::from_str("0xfdB65576568b99A8a00a292577b8fc51abB115bD").unwrap(); + let signature_0 = EcdsaSignature::from_bytes( + &hex::decode("4e561dcd350b7a271c7247843f7731a8a9810037c13784f5b3a9616788ca536976c5ff70b1865c4568e273a375851a5304dc7a1ac54f0783f3dde38d345313a91c").unwrap()[..] + ).unwrap(); + + let validator_1 = H160::from_str("0x5090cEd8BC5A7D3c2FbE2b2702eE4a8e7b227181").unwrap(); + // This signature corresponds to validator_0 + let signature_1 = EcdsaSignature::from_bytes( + &hex::decode("4e561dcd350b7a271c7247843f7731a8a9810037c13784f5b3a9616788ca536976c5ff70b1865c4568e273a375851a5304dc7a1ac54f0783f3dde38d345313a91c").unwrap()[..] + ).unwrap(); + + let multisig_ism = MultisigIsm::new( + TestSignedPayload(), + vec![signature_0, signature_1], + vec![validator_0, validator_1], + 2, + ); + + assert_eq!( + multisig_ism.verify().unwrap_err(), + MultisigIsmError::ThresholdNotMet + ); + } + + #[test] + fn test_multisig_ism_validators_out_of_order() { + let validator_0 = H160::from_str("0xfdB65576568b99A8a00a292577b8fc51abB115bD").unwrap(); + let signature_0 = EcdsaSignature::from_bytes( + &hex::decode("4e561dcd350b7a271c7247843f7731a8a9810037c13784f5b3a9616788ca536976c5ff70b1865c4568e273a375851a5304dc7a1ac54f0783f3dde38d345313a91c").unwrap()[..] + ).unwrap(); + + let validator_1 = H160::from_str("0x5090cEd8BC5A7D3c2FbE2b2702eE4a8e7b227181").unwrap(); + let signature_1 = EcdsaSignature::from_bytes( + &hex::decode("9d510e0d988e44cf05a4e29d7b1ecec6e3277a8be137164f89d6cf52325190f058101ef9aa57d118f9452a38c156efbdb1b69d4022ac2c35370c433ca5b61aeb1c").unwrap()[..] + ).unwrap(); + + let multisig_ism = MultisigIsm::new( + TestSignedPayload(), + // Sigs out of order + vec![signature_1, signature_0], + vec![validator_0, validator_1], + 2, + ); + + assert_eq!( + multisig_ism.verify().unwrap_err(), + MultisigIsmError::ThresholdNotMet + ); + } +} diff --git a/rust/sealevel/libraries/multisig-ism/src/test_data.rs b/rust/sealevel/libraries/multisig-ism/src/test_data.rs new file mode 100644 index 0000000000..df6c89d8e6 --- /dev/null +++ b/rust/sealevel/libraries/multisig-ism/src/test_data.rs @@ -0,0 +1,84 @@ +//! Gated on the "test-data" feature. +//! Useful for use in unit & integration tests, which can't import from +//! each other. + +use hyperlane_core::{Checkpoint, CheckpointWithMessageId, HyperlaneMessage, H160, H256}; +use std::str::FromStr; + +pub struct MultisigIsmTestData { + pub message: HyperlaneMessage, + pub checkpoint: CheckpointWithMessageId, + pub validators: Vec, + pub signatures: Vec>, +} + +const ORIGIN_DOMAIN: u32 = 1234u32; +const DESTINATION_DOMAIN: u32 = 4321u32; + +pub fn get_multisig_ism_test_data() -> MultisigIsmTestData { + let message = HyperlaneMessage { + version: 0, + nonce: 69, + origin: ORIGIN_DOMAIN, + sender: H256::from_str( + "0xafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafaf", + ) + .unwrap(), + destination: DESTINATION_DOMAIN, + recipient: H256::from_str( + "0xbebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebe", + ) + .unwrap(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }; + + let checkpoint = CheckpointWithMessageId { + checkpoint: Checkpoint { + mailbox_address: H256::from_str( + "0xabababababababababababababababababababababababababababababababab", + ) + .unwrap(), + mailbox_domain: ORIGIN_DOMAIN, + root: H256::from_str( + "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + ) + .unwrap(), + index: 69, + }, + message_id: message.id(), + }; + + // checkpoint.signing_hash() is equal to: + // 0x4fc33ff33d5e9305a2d87f7824d1b943ba219cff4c153ae8fd39b0d8620fc332 + + // Validator 0: + // Address: 0xE3DCDBbc248cE191bDc271f3FCcd0d95911BFC5D + // Private Key: 0x788aa7213bd92ff92017d767fde0d75601425818c8e4b21e87314c2a4dcd6091 + let validator_0 = H160::from_str("0xE3DCDBbc248cE191bDc271f3FCcd0d95911BFC5D").unwrap(); + // > await (new ethers.Wallet('0x788aa7213bd92ff92017d767fde0d75601425818c8e4b21e87314c2a4dcd6091')).signMessage(ethers.utils.arrayify('0x4fc33ff33d5e9305a2d87f7824d1b943ba219cff4c153ae8fd39b0d8620fc332')) + // '0x3a06cc01fef07025ee5ae9e29ae783338fe11f5c21af383fb8cc5878a2ea3616125c230ec07b059eaebb842af0a51040ad3214f9050cccef36b5c21c9c9cc4ba1b' + let signature_0 = hex::decode("3a06cc01fef07025ee5ae9e29ae783338fe11f5c21af383fb8cc5878a2ea3616125c230ec07b059eaebb842af0a51040ad3214f9050cccef36b5c21c9c9cc4ba1b").unwrap(); + + // Validator 1: + // Address: 0xb25206874C24733F05CC0dD11924724A8E7175bd + // Private Key: 0x4a599de3915f404d84a2ebe522bfe7032ebb1ca76a65b55d6eb212b129043a0e + let validator_1 = H160::from_str("0xb25206874C24733F05CC0dD11924724A8E7175bd").unwrap(); + // > await (new ethers.Wallet('0x4a599de3915f404d84a2ebe522bfe7032ebb1ca76a65b55d6eb212b129043a0e')).signMessage(ethers.utils.arrayify('0x4fc33ff33d5e9305a2d87f7824d1b943ba219cff4c153ae8fd39b0d8620fc332')) + // '0xfd34aac152ec85a79211c990f308c7e719145e2e67e48f2d10db4347d3a9102131254eccbcd0fe389afad96b88d368192b33649336893dfe1bbad43901d1bef71b' + let signature_1 = hex::decode("fd34aac152ec85a79211c990f308c7e719145e2e67e48f2d10db4347d3a9102131254eccbcd0fe389afad96b88d368192b33649336893dfe1bbad43901d1bef71b").unwrap(); + + // Validator 2: + // Address: 0x28b8d0E2bBfeDe9071F8Ff3DaC9CcE3d3176DBd3 + // Private Key: 0x2cc76d56db9924ddc3388164454dfea9edd2d5f5da81102fd3594fc7c5281515 + let validator_2 = H160::from_str("0x28b8d0E2bBfeDe9071F8Ff3DaC9CcE3d3176DBd3").unwrap(); + // > await (new ethers.Wallet('0x2cc76d56db9924ddc3388164454dfea9edd2d5f5da81102fd3594fc7c5281515')).signMessage(ethers.utils.arrayify('0x4fc33ff33d5e9305a2d87f7824d1b943ba219cff4c153ae8fd39b0d8620fc332')) + // '0x85992e471002c40730d2b91831ba40cd8ffcebf4905646c25b7b6abb7575f25d19395045466e833b7700e233bfa5836f0a459da05bf817efd6cb4f55bcaec4b51c' + let signature_2 = hex::decode("85992e471002c40730d2b91831ba40cd8ffcebf4905646c25b7b6abb7575f25d19395045466e833b7700e233bfa5836f0a459da05bf817efd6cb4f55bcaec4b51c").unwrap(); + + MultisigIsmTestData { + message, + checkpoint, + validators: vec![validator_0, validator_1, validator_2], + signatures: vec![signature_0, signature_1, signature_2], + } +} diff --git a/rust/sealevel/libraries/serializable-account-meta/Cargo.toml b/rust/sealevel/libraries/serializable-account-meta/Cargo.toml new file mode 100644 index 0000000000..5e0323b36d --- /dev/null +++ b/rust/sealevel/libraries/serializable-account-meta/Cargo.toml @@ -0,0 +1,16 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "serializable-account-meta" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +solana-program.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/serializable-account-meta/src/lib.rs b/rust/sealevel/libraries/serializable-account-meta/src/lib.rs new file mode 100644 index 0000000000..1153f9975f --- /dev/null +++ b/rust/sealevel/libraries/serializable-account-meta/src/lib.rs @@ -0,0 +1,55 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; + +/// A borsh-serializable version of `AccountMeta`. +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct SerializableAccountMeta { + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, +} + +impl From for SerializableAccountMeta { + fn from(account_meta: AccountMeta) -> Self { + Self { + pubkey: account_meta.pubkey, + is_signer: account_meta.is_signer, + is_writable: account_meta.is_writable, + } + } +} + +impl From for AccountMeta { + fn from(serializable_account_meta: SerializableAccountMeta) -> Self { + Self { + pubkey: serializable_account_meta.pubkey, + is_signer: serializable_account_meta.is_signer, + is_writable: serializable_account_meta.is_writable, + } + } +} + +/// A ridiculous workaround for https://github.com/solana-labs/solana/issues/31391, +/// which is a bug where if a simulated transaction's return data ends with zero byte(s), +/// they end up being incorrectly truncated. +/// As a workaround, we can (de)serialize data with a trailing non-zero byte. +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct SimulationReturnData +where + T: BorshSerialize + BorshDeserialize, +{ + pub return_data: T, + trailing_byte: u8, +} + +impl SimulationReturnData +where + T: BorshSerialize + BorshDeserialize, +{ + pub fn new(return_data: T) -> Self { + Self { + return_data, + trailing_byte: u8::MAX, + } + } +} diff --git a/rust/sealevel/libraries/test-transaction-utils/Cargo.toml b/rust/sealevel/libraries/test-transaction-utils/Cargo.toml new file mode 100644 index 0000000000..418c7c38fd --- /dev/null +++ b/rust/sealevel/libraries/test-transaction-utils/Cargo.toml @@ -0,0 +1,14 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-test-transaction-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +solana-program.workspace = true +solana-program-test.workspace = true +solana-sdk.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/test-transaction-utils/src/lib.rs b/rust/sealevel/libraries/test-transaction-utils/src/lib.rs new file mode 100644 index 0000000000..7aa1221aae --- /dev/null +++ b/rust/sealevel/libraries/test-transaction-utils/src/lib.rs @@ -0,0 +1,27 @@ +use solana_program::instruction::Instruction; +use solana_program_test::*; +use solana_sdk::{ + signature::{Signature, Signer}, + signer::keypair::Keypair, + signers::Signers, + transaction::Transaction, +}; + +pub async fn process_instruction( + banks_client: &mut BanksClient, + instruction: Instruction, + payer: &Keypair, + signers: &T, +) -> Result { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + signers, + recent_blockhash, + ); + let signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await?; + + Ok(signature) +} diff --git a/rust/sealevel/libraries/test-utils/Cargo.toml b/rust/sealevel/libraries/test-utils/Cargo.toml new file mode 100644 index 0000000000..77c18ece94 --- /dev/null +++ b/rust/sealevel/libraries/test-utils/Cargo.toml @@ -0,0 +1,28 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-test-utils" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +solana-program-test.workspace = true +solana-program.workspace = true +solana-sdk.workspace = true +spl-noop.workspace = true +spl-token-2022.workspace = true + +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-interchain-security-module-interface = { path = "../interchain-security-module-interface" } +hyperlane-sealevel-mailbox = { path = "../../programs/mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-message-recipient-interface = { path = "../message-recipient-interface" } +hyperlane-sealevel-test-ism = { path = "../../programs/ism/test-ism", features = ["test-client"] } +hyperlane-test-transaction-utils = { path = "../test-transaction-utils" } +serializable-account-meta = { path = "../serializable-account-meta" } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/libraries/test-utils/src/lib.rs b/rust/sealevel/libraries/test-utils/src/lib.rs new file mode 100644 index 0000000000..f7b67ce3fa --- /dev/null +++ b/rust/sealevel/libraries/test-utils/src/lib.rs @@ -0,0 +1,512 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{Encode, HyperlaneMessage}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey, + pubkey::Pubkey, + system_program, +}; +use solana_program_test::*; +use solana_sdk::{ + message::Message, + signature::{Signature, Signer}, + signer::keypair::Keypair, + signers::Signers, + transaction::{Transaction, TransactionError}, +}; + +use spl_token_2022::{extension::StateWithExtensions, state::Account}; + +use hyperlane_sealevel_interchain_security_module_interface::{ + InterchainSecurityModuleInstruction, VerifyInstruction, VERIFY_ACCOUNT_METAS_PDA_SEEDS, +}; +use hyperlane_sealevel_mailbox::{ + instruction::{InboxProcess, Init as InitMailbox, Instruction as MailboxInstruction}, + mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds, mailbox_process_authority_pda_seeds, + mailbox_processed_message_pda_seeds, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, HANDLE_ACCOUNT_METAS_PDA_SEEDS, + INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_PDA_SEEDS, +}; +use hyperlane_sealevel_test_ism::test_client::TestIsmTestClient; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; + +// ========= Mailbox ========= + +pub fn mailbox_id() -> Pubkey { + pubkey!("692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1") +} + +pub struct MailboxAccounts { + pub program: Pubkey, + pub inbox: Pubkey, + pub inbox_bump_seed: u8, + pub outbox: Pubkey, + pub outbox_bump_seed: u8, + pub default_ism: Pubkey, +} + +pub async fn initialize_mailbox( + banks_client: &mut BanksClient, + mailbox_program_id: &Pubkey, + payer: &Keypair, + local_domain: u32, +) -> Result { + let (inbox_account, inbox_bump) = + Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), mailbox_program_id); + let (outbox_account, outbox_bump) = + Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), mailbox_program_id); + + let default_ism = hyperlane_sealevel_test_ism::id(); + + let ixn = MailboxInstruction::Init(InitMailbox { + local_domain, + default_ism, + }); + let init_instruction = Instruction { + program_id: *mailbox_program_id, + data: ixn.into_instruction_data().unwrap(), + accounts: vec![ + AccountMeta::new(system_program::id(), false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(inbox_account, false), + AccountMeta::new(outbox_account, false), + ], + }; + + process_instruction(banks_client, init_instruction, payer, &[payer]).await?; + + // And initialize the default ISM + initialize_test_ism(banks_client, payer).await?; + + Ok(MailboxAccounts { + program: *mailbox_program_id, + inbox: inbox_account, + inbox_bump_seed: inbox_bump, + outbox: outbox_account, + outbox_bump_seed: outbox_bump, + default_ism, + }) +} + +async fn initialize_test_ism( + banks_client: &mut BanksClient, + payer: &Keypair, +) -> Result<(), BanksClientError> { + let mut test_ism = TestIsmTestClient::new(banks_client.clone(), clone_keypair(payer)); + test_ism.init().await?; + + Ok(()) +} + +/// Simulates an instruction, and attempts to deserialize it into a T. +/// If no return data at all was returned, returns Ok(None). +/// If some return data was returned but deserialization was unsuccesful, +/// an Err is returned. +pub async fn simulate_instruction( + banks_client: &mut BanksClient, + payer: &Keypair, + instruction: Instruction, +) -> Result, BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await?; + let simulation = banks_client + .simulate_transaction(Transaction::new_unsigned(Message::new_with_blockhash( + &[instruction], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await?; + // If the result is an err, return an err + if let Some(Err(err)) = simulation.result { + return Err(BanksClientError::TransactionError(err)); + } + let decoded_data = simulation + .simulation_details + .unwrap() + .return_data + .map(|return_data| T::try_from_slice(return_data.data.as_slice()).unwrap()); + + Ok(decoded_data) +} + +/// Simulates an Instruction that will return a list of AccountMetas. +pub async fn get_account_metas( + banks_client: &mut BanksClient, + payer: &Keypair, + instruction: Instruction, +) -> Result, BanksClientError> { + // If there's no data at all, default to an empty vec. + let account_metas = simulate_instruction::>>( + banks_client, + payer, + instruction, + ) + .await? + .map(|serializable_account_metas| { + serializable_account_metas + .return_data + .into_iter() + .map(|serializable_account_meta| serializable_account_meta.into()) + .collect() + }) + .unwrap_or_else(std::vec::Vec::new); + + Ok(account_metas) +} + +/// Gets the recipient ISM given a recipient program id and the ISM getter account metas. +pub async fn get_recipient_ism_with_account_metas( + banks_client: &mut BanksClient, + payer: &Keypair, + mailbox_accounts: &MailboxAccounts, + recipient_program_id: Pubkey, + ism_getter_account_metas: Vec, +) -> Result { + let mut accounts = vec![ + // Inbox PDA + AccountMeta::new_readonly(mailbox_accounts.inbox, false), + // The recipient program. + AccountMeta::new_readonly(recipient_program_id, false), + ]; + accounts.extend(ism_getter_account_metas); + + let instruction = Instruction::new_with_borsh( + mailbox_accounts.program, + &MailboxInstruction::InboxGetRecipientIsm(recipient_program_id), + accounts, + ); + let ism = + simulate_instruction::>(banks_client, payer, instruction) + .await? + .unwrap() + .return_data; + Ok(ism) +} + +/// Gets the account metas required for the recipient's +/// `MessageRecipientInstruction::InterchainSecurityModule` instruction. +pub async fn get_ism_getter_account_metas( + banks_client: &mut BanksClient, + payer: &Keypair, + recipient_program_id: Pubkey, +) -> Result, BanksClientError> { + let instruction = MessageRecipientInstruction::InterchainSecurityModuleAccountMetas; + + get_account_metas_with_instruction_bytes( + banks_client, + payer, + recipient_program_id, + &instruction.encode().unwrap(), + INTERCHAIN_SECURITY_MODULE_ACCOUNT_METAS_PDA_SEEDS, + ) + .await +} + +pub async fn get_recipient_ism( + banks_client: &mut BanksClient, + payer: &Keypair, + mailbox_accounts: &MailboxAccounts, + recipient: Pubkey, +) -> Result { + let account_metas = get_ism_getter_account_metas(banks_client, payer, recipient).await?; + + get_recipient_ism_with_account_metas( + banks_client, + payer, + mailbox_accounts, + recipient, + account_metas, + ) + .await +} + +/// Gets the account metas required for the ISM's `Verify` instruction. +pub async fn get_ism_verify_account_metas( + banks_client: &mut BanksClient, + payer: &Keypair, + ism: Pubkey, + metadata: Vec, + message: Vec, +) -> Result, BanksClientError> { + let instruction = InterchainSecurityModuleInstruction::VerifyAccountMetas(VerifyInstruction { + metadata, + message, + }); + + get_account_metas_with_instruction_bytes( + banks_client, + payer, + ism, + &instruction.encode().unwrap(), + VERIFY_ACCOUNT_METAS_PDA_SEEDS, + ) + .await +} + +/// Gets the account metas required for the recipient's `MessageRecipientInstruction::Handle` instruction. +pub async fn get_handle_account_metas( + banks_client: &mut BanksClient, + payer: &Keypair, + message: &HyperlaneMessage, +) -> Result, BanksClientError> { + let recipient_program_id = Pubkey::new_from_array(message.recipient.into()); + let instruction = MessageRecipientInstruction::HandleAccountMetas(HandleInstruction { + sender: message.sender, + origin: message.origin, + message: message.body.clone(), + }); + + get_account_metas_with_instruction_bytes( + banks_client, + payer, + recipient_program_id, + &instruction.encode().unwrap(), + HANDLE_ACCOUNT_METAS_PDA_SEEDS, + ) + .await +} + +async fn get_account_metas_with_instruction_bytes( + banks_client: &mut BanksClient, + payer: &Keypair, + program_id: Pubkey, + instruction_data: &[u8], + account_metas_pda_seeds: &[&[u8]], +) -> Result, BanksClientError> { + let (account_metas_pda_key, _) = + Pubkey::find_program_address(account_metas_pda_seeds, &program_id); + let instruction = Instruction::new_with_bytes( + program_id, + instruction_data, + vec![AccountMeta::new(account_metas_pda_key, false)], + ); + + get_account_metas(banks_client, payer, instruction).await +} + +pub async fn process( + banks_client: &mut BanksClient, + payer: &Keypair, + mailbox_accounts: &MailboxAccounts, + metadata: Vec, + message: &HyperlaneMessage, +) -> Result<(Signature, Pubkey), BanksClientError> { + let accounts = get_process_account_metas( + banks_client, + payer, + mailbox_accounts, + metadata.clone(), + message, + ) + .await?; + + process_with_accounts( + banks_client, + payer, + mailbox_accounts, + metadata, + message, + accounts, + ) + .await +} + +pub async fn process_with_accounts( + banks_client: &mut BanksClient, + payer: &Keypair, + mailbox_accounts: &MailboxAccounts, + metadata: Vec, + message: &HyperlaneMessage, + accounts: Vec, +) -> Result<(Signature, Pubkey), BanksClientError> { + let mut encoded_message = vec![]; + message.write_to(&mut encoded_message).unwrap(); + + let ixn = MailboxInstruction::InboxProcess(InboxProcess { + metadata: metadata.to_vec(), + message: encoded_message, + }); + let ixn_data = ixn.into_instruction_data().unwrap(); + + let inbox_instruction = Instruction { + program_id: mailbox_accounts.program, + data: ixn_data, + accounts, + }; + let recent_blockhash = banks_client.get_latest_blockhash().await?; + let txn = Transaction::new_signed_with_payer( + &[inbox_instruction], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + let tx_signature = txn.signatures[0]; + + banks_client.process_transaction(txn).await?; + + Ok(( + tx_signature, + Pubkey::find_program_address( + mailbox_processed_message_pda_seeds!(message.id()), + &mailbox_accounts.program, + ) + .0, + )) +} + +pub async fn get_process_account_metas( + banks_client: &mut BanksClient, + payer: &Keypair, + mailbox_accounts: &MailboxAccounts, + metadata: Vec, + message: &HyperlaneMessage, +) -> Result, BanksClientError> { + let mut encoded_message = vec![]; + message.write_to(&mut encoded_message).unwrap(); + + let recipient: Pubkey = message.recipient.0.into(); + + let (process_authority_key, _process_authority_bump) = Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(&recipient), + &mailbox_accounts.program, + ); + let (processed_message_account_key, _processed_message_account_bump) = + Pubkey::find_program_address( + mailbox_processed_message_pda_seeds!(message.id()), + &mailbox_accounts.program, + ); + + // Get the account metas required for the recipient.InterchainSecurityModule instruction. + let ism_getter_account_metas = + get_ism_getter_account_metas(banks_client, payer, recipient).await?; + + // Get the recipient ISM. + let ism = get_recipient_ism_with_account_metas( + banks_client, + payer, + mailbox_accounts, + recipient, + ism_getter_account_metas.clone(), + ) + .await?; + + // Craft the accounts for the transaction. + let mut accounts: Vec = vec![ + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(mailbox_accounts.inbox, false), + AccountMeta::new_readonly(process_authority_key, false), + AccountMeta::new(processed_message_account_key, false), + ]; + accounts.extend(ism_getter_account_metas); + accounts.extend([ + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(ism, false), + ]); + + // Get the account metas required for the ISM.Verify instruction. + let ism_verify_account_metas = + get_ism_verify_account_metas(banks_client, payer, ism, metadata, encoded_message).await?; + accounts.extend(ism_verify_account_metas); + + // The recipient. + accounts.extend([AccountMeta::new_readonly(recipient, false)]); + + // Get account metas required for the Handle instruction + let handle_account_metas = get_handle_account_metas(banks_client, payer, message).await?; + accounts.extend(handle_account_metas); + + Ok(accounts) +} + +// ========= Balance utils ========= + +pub async fn assert_lamports( + banks_client: &mut BanksClient, + account: &Pubkey, + expected_lamports: u64, +) { + let account = banks_client.get_account(*account).await.unwrap().unwrap(); + assert_eq!(account.lamports, expected_lamports); +} + +pub async fn assert_token_balance( + banks_client: &mut BanksClient, + account: &Pubkey, + expected_balance: u64, +) { + let data = banks_client + .get_account(*account) + .await + .unwrap() + .unwrap() + .data; + let state = StateWithExtensions::::unpack(&data).unwrap(); + assert_eq!(state.base.amount, expected_balance); +} + +// ========= General purpose utils ========= + +pub async fn new_funded_keypair( + banks_client: &mut BanksClient, + payer: &Keypair, + lamports: u64, +) -> Keypair { + let keypair = Keypair::new(); + transfer_lamports(banks_client, payer, &keypair.pubkey(), lamports).await; + keypair +} + +pub async fn transfer_lamports( + banks_client: &mut BanksClient, + payer: &Keypair, + to: &Pubkey, + lamports: u64, +) { + process_instruction( + banks_client, + solana_sdk::system_instruction::transfer(&payer.pubkey(), to, lamports), + payer, + &[payer], + ) + .await + .unwrap(); +} + +pub fn assert_transaction_error( + result: Result, + expected_error: TransactionError, +) { + // BanksClientError doesn't implement Eq, but TransactionError does + if let BanksClientError::TransactionError(tx_err) = result.err().unwrap() { + assert_eq!(tx_err, expected_error); + } else { + panic!("expected TransactionError"); + } +} + +// Hack to get around the absence of a Clone implementation in solana-sdk 1.14.13. +pub fn clone_keypair(keypair: &Keypair) -> Keypair { + let serialized = keypair.to_bytes(); + Keypair::from_bytes(&serialized).unwrap() +} + +pub async fn process_instruction( + banks_client: &mut BanksClient, + instruction: Instruction, + payer: &Keypair, + signers: &T, +) -> Result { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + signers, + recent_blockhash, + ); + let signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await?; + + Ok(signature) +} diff --git a/rust/sealevel/programs/config/sealevel/test-keys/hyperlane_sealevel_test_ism-keypair.json b/rust/sealevel/programs/config/sealevel/test-keys/hyperlane_sealevel_test_ism-keypair.json new file mode 100644 index 0000000000..9d945c77da --- /dev/null +++ b/rust/sealevel/programs/config/sealevel/test-keys/hyperlane_sealevel_test_ism-keypair.json @@ -0,0 +1 @@ +[26,63,174,236,18,56,41,93,178,209,127,72,173,217,235,237,221,42,147,19,37,178,79,98,67,215,157,241,99,188,21,80,170,254,173,10,96,168,249,90,38,77,134,84,142,101,80,250,142,207,45,195,201,15,142,157,205,1,63,8,72,28,103,40] \ No newline at end of file diff --git a/rust/sealevel/programs/config/sealevel/test-keys/hyperlane_sealevel_token_collateral-keypair.json b/rust/sealevel/programs/config/sealevel/test-keys/hyperlane_sealevel_token_collateral-keypair.json new file mode 100644 index 0000000000..822701b171 --- /dev/null +++ b/rust/sealevel/programs/config/sealevel/test-keys/hyperlane_sealevel_token_collateral-keypair.json @@ -0,0 +1 @@ +[53,211,164,100,198,163,41,106,143,30,19,111,44,44,76,141,247,205,14,168,31,233,17,198,149,12,250,24,92,62,157,75,224,228,13,66,45,75,76,193,119,78,73,189,12,55,247,124,100,160,113,38,128,159,41,25,52,137,207,199,85,92,193,255] \ No newline at end of file diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral/Cargo.toml b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/Cargo.toml new file mode 100644 index 0000000000..8fd2ab6a15 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/Cargo.toml @@ -0,0 +1,38 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-token-collateral" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +spl-associated-token-account.workspace = true +spl-noop.workspace = true +spl-token-2022.workspace = true # FIXME Should we actually use 2022 here or try normal token program? +spl-token.workspace = true +thiserror.workspace = true + +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-sealevel-token-lib = { path = "../../libraries/hyperlane-sealevel-token" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[dev-dependencies] +solana-program-test.workspace = true +solana-sdk.workspace = true + +hyperlane-test-utils = { path = "../../libraries/test-utils" } +hyperlane-sealevel-test-ism = { path = "../ism/test-ism", features = ["no-entrypoint"] } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/instruction.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/instruction.rs new file mode 100644 index 0000000000..df0d434fc8 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/instruction.rs @@ -0,0 +1,47 @@ +//! Instructions for the program. + +use hyperlane_sealevel_token_lib::instruction::{init_instruction as lib_init_instruction, Init}; + +use crate::{hyperlane_token_ata_payer_pda_seeds, hyperlane_token_escrow_pda_seeds}; + +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::SysvarId, +}; + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + init: Init, + spl_program: Pubkey, + mint: Pubkey, +) -> Result { + let mut instruction = lib_init_instruction(program_id, payer, init)?; + + // Add additional account metas: + // 0. [executable] The SPL token program for the mint, i.e. either SPL token program or the 2022 version. + // 1. [] The mint. + // 2. [executable] The Rent sysvar program. + // 3. [writable] The escrow PDA account. + // 4. [writable] The ATA payer PDA account. + + let (escrow_key, _escrow_bump) = + Pubkey::find_program_address(hyperlane_token_escrow_pda_seeds!(), &program_id); + + let (ata_payer_key, _ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), &program_id); + + instruction.accounts.append(&mut vec![ + AccountMeta::new_readonly(spl_program, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(Rent::id(), false), + AccountMeta::new(escrow_key, false), + AccountMeta::new(ata_payer_key, false), + ]); + + Ok(instruction) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/lib.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/lib.rs new file mode 100644 index 0000000000..ee25eb133e --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/lib.rs @@ -0,0 +1,14 @@ +//! The hyperlane-sealevel-token-collateral program. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod instruction; +pub mod plugin; +pub mod processor; + +pub use spl_associated_token_account; +pub use spl_noop; +pub use spl_token; +pub use spl_token_2022; diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/plugin.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/plugin.rs new file mode 100644 index 0000000000..5e2f62900c --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/plugin.rs @@ -0,0 +1,441 @@ +//! A plugin for the Hyperlane token program that escrows SPL tokens as collateral. + +use account_utils::{create_pda_account, verify_rent_exempt}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneToken, message::TokenMessage, processor::HyperlaneSealevelTokenPlugin, +}; +use serializable_account_meta::SerializableAccountMeta; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + instruction::AccountMeta, + program::{get_return_data, invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::{self, Sysvar}, +}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, + instruction::create_associated_token_account_idempotent, +}; +use spl_token_2022::instruction::{get_account_data_size, initialize_account, transfer_checked}; + +/// Seeds relating to the PDA account that acts both as the mint +/// *and* the mint authority. +#[macro_export] +macro_rules! hyperlane_token_escrow_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"escrow"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"escrow", &[$bump_seed]] + }}; +} + +/// Seeds relating to the PDA account that acts as the payer for +/// ATA creation. +#[macro_export] +macro_rules! hyperlane_token_ata_payer_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"ata_payer"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"ata_payer", &[$bump_seed]] + }}; +} + +/// A plugin for the Hyperlane token program that escrows SPL +/// tokens when transferring out to a remote chain, and pays them +/// out when transferring in from a remote chain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default)] +pub struct CollateralPlugin { + /// The SPL token program, i.e. either SPL token program or the 2022 version. + pub spl_token_program: Pubkey, + /// The mint. + pub mint: Pubkey, + /// The escrow PDA account. + pub escrow: Pubkey, + /// The escrow PDA bump seed. + pub escrow_bump: u8, + /// The ATA payer PDA bump seed. + pub ata_payer_bump: u8, +} + +impl CollateralPlugin { + fn verify_ata_payer_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + ata_payer_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let ata_payer_seeds: &[&[u8]] = + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump); + let expected_ata_payer_account = + Pubkey::create_program_address(ata_payer_seeds, program_id)?; + if ata_payer_account_info.key != &expected_ata_payer_account { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } +} + +impl HyperlaneSealevelTokenPlugin for CollateralPlugin { + /// Initializes the plugin. + /// + /// Accounts: + /// 0. [executable] The SPL token program for the mint, i.e. either SPL token program or the 2022 version. + /// 1. [] The mint. + /// 2. [executable] The Rent sysvar program. + /// 3. [writable] The escrow PDA account. + /// 4. [writable] The ATA payer PDA account. + fn initialize<'a, 'b>( + program_id: &Pubkey, + system_program: &'a AccountInfo<'b>, + _token_account_info: &'a AccountInfo<'b>, + payer_account_info: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + ) -> Result { + // Account 0: The SPL token program. + // This can either be the original SPL token program or the 2022 version. + // This is saved in the HyperlaneToken plugin data so that future interactions + // are done with the correct SPL token program. + let spl_token_account_info = next_account_info(accounts_iter)?; + if spl_token_account_info.key != &spl_token_2022::id() + && spl_token_account_info.key != &spl_token::id() + { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 1: The mint. + let mint_account_info = next_account_info(accounts_iter)?; + if mint_account_info.owner != spl_token_account_info.key { + return Err(ProgramError::IllegalOwner); + } + + // Account 2: The Rent sysvar program. + let rent_account_info = next_account_info(accounts_iter)?; + if rent_account_info.key != &sysvar::rent::id() { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: Escrow PDA account. + let escrow_account_info = next_account_info(accounts_iter)?; + let (escrow_key, escrow_bump) = + Pubkey::find_program_address(hyperlane_token_escrow_pda_seeds!(), program_id); + if &escrow_key != escrow_account_info.key { + return Err(ProgramError::IncorrectProgramId); + } + + // Get the required account size for the escrow PDA. + invoke( + &get_account_data_size( + spl_token_account_info.key, + mint_account_info.key, + // No additional extensions + &[], + )?, + &[mint_account_info.clone()], + )?; + let account_data_size: u64 = get_return_data() + .ok_or(ProgramError::InvalidArgument) + .and_then(|(returning_pubkey, data)| { + if &returning_pubkey != spl_token_account_info.key { + return Err(ProgramError::InvalidArgument); + } + let data: [u8; 8] = data + .as_slice() + .try_into() + .map_err(|_| ProgramError::InvalidArgument)?; + Ok(u64::from_le_bytes(data)) + })?; + + let rent = Rent::get()?; + + // Create escrow PDA owned by the SPL token program. + create_pda_account( + payer_account_info, + &rent, + account_data_size.try_into().unwrap(), + spl_token_account_info.key, + system_program, + escrow_account_info, + hyperlane_token_escrow_pda_seeds!(escrow_bump), + )?; + + // And initialize the escrow account. + invoke( + &initialize_account( + spl_token_account_info.key, + escrow_account_info.key, + mint_account_info.key, + escrow_account_info.key, + )?, + &[ + escrow_account_info.clone(), + mint_account_info.clone(), + escrow_account_info.clone(), + rent_account_info.clone(), + ], + )?; + + // Account 4: ATA payer. + let ata_payer_account_info = next_account_info(accounts_iter)?; + let (ata_payer_key, ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + if &ata_payer_key != ata_payer_account_info.key { + return Err(ProgramError::IncorrectProgramId); + } + + // Create the ATA payer. + // This is a separate PDA because the ATA program requires + // the payer to have no data in it. + create_pda_account( + payer_account_info, + &rent, + 0, + // Grant ownership to the system program so that the ATA program + // can call into the system program with the ATA payer as the + // payer. + &solana_program::system_program::id(), + system_program, + ata_payer_account_info, + hyperlane_token_ata_payer_pda_seeds!(ata_payer_bump), + )?; + + Ok(Self { + spl_token_program: *spl_token_account_info.key, + mint: *mint_account_info.key, + escrow: escrow_key, + escrow_bump, + ata_payer_bump, + }) + } + + /// Transfers tokens to the escrow account so they can be sent to a remote chain. + /// Burns the tokens from the sender's associated token account. + /// + /// Accounts: + /// 0. [executable] The SPL token program for the mint. + /// 1. [writeable] The mint. + /// 2. [writeable] The token sender's associated token account, from which tokens will be sent. + /// 3. [writeable] The escrow PDA account. + fn transfer_in<'a, 'b>( + _program_id: &Pubkey, + token: &HyperlaneToken, + sender_wallet_account_info: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: SPL token program. + let spl_token_account_info = next_account_info(accounts_iter)?; + if spl_token_account_info.key != &token.plugin_data.spl_token_program { + return Err(ProgramError::IncorrectProgramId); + } + if !spl_token_account_info.executable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 1: The mint. + let mint_account_info = next_account_info(accounts_iter)?; + if mint_account_info.key != &token.plugin_data.mint { + return Err(ProgramError::IncorrectProgramId); + } + if mint_account_info.owner != spl_token_account_info.key { + return Err(ProgramError::InvalidAccountData); + } + + // Account 2: The sender's associated token account. + let sender_ata_account_info = next_account_info(accounts_iter)?; + let expected_sender_associated_token_key = get_associated_token_address_with_program_id( + sender_wallet_account_info.key, + mint_account_info.key, + spl_token_account_info.key, + ); + if sender_ata_account_info.key != &expected_sender_associated_token_key { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: The escrow PDA account. + let escrow_account_info = next_account_info(accounts_iter)?; + if escrow_account_info.key != &token.plugin_data.escrow { + return Err(ProgramError::IncorrectProgramId); + } + + let transfer_instruction = transfer_checked( + spl_token_account_info.key, + sender_ata_account_info.key, + mint_account_info.key, + escrow_account_info.key, + sender_wallet_account_info.key, + // Multisignatures not supported at the moment. + &[], + amount, + token.decimals, + )?; + + // Sender wallet is expected to have signed this transaction. + invoke( + &transfer_instruction, + &[ + sender_ata_account_info.clone(), + mint_account_info.clone(), + escrow_account_info.clone(), + sender_wallet_account_info.clone(), + ], + )?; + + Ok(()) + } + + /// Transfers tokens out to a recipient's associated token account as a + /// result of a transfer to this chain from a remote chain. + /// + /// Accounts: + /// 0. [executable] SPL token for the mint. + /// 1. [executable] SPL associated token account. + /// 2. [writeable] Mint account. + /// 3. [writeable] Recipient associated token account. + /// 4. [writeable] ATA payer PDA account. + /// 5. [writeable] Escrow account. + fn transfer_out<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + system_program_account_info: &'a AccountInfo<'b>, + recipient_wallet_account_info: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: SPL token program. + let spl_token_account_info = next_account_info(accounts_iter)?; + if spl_token_account_info.key != &token.plugin_data.spl_token_program { + return Err(ProgramError::IncorrectProgramId); + } + if !spl_token_account_info.executable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 1: SPL associated token account + let spl_ata_account_info = next_account_info(accounts_iter)?; + if spl_ata_account_info.key != &spl_associated_token_account::id() { + return Err(ProgramError::IncorrectProgramId); + } + if !spl_ata_account_info.executable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 2: Mint account + let mint_account_info = next_account_info(accounts_iter)?; + if mint_account_info.key != &token.plugin_data.mint { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 3: Recipient associated token account + let recipient_ata_account_info = next_account_info(accounts_iter)?; + let expected_recipient_associated_token_account_key = + get_associated_token_address_with_program_id( + recipient_wallet_account_info.key, + mint_account_info.key, + spl_token_account_info.key, + ); + if recipient_ata_account_info.key != &expected_recipient_associated_token_account_key { + return Err(ProgramError::IncorrectProgramId); + } + if !recipient_ata_account_info.is_writable { + return Err(ProgramError::InvalidAccountData); + } + + // Account 4: ATA payer PDA account + let ata_payer_account_info = next_account_info(accounts_iter)?; + Self::verify_ata_payer_account_info(program_id, token, ata_payer_account_info)?; + + // Account 5: Escrow account. + let escrow_account_info = next_account_info(accounts_iter)?; + if escrow_account_info.key != &token.plugin_data.escrow { + return Err(ProgramError::IncorrectProgramId); + } + + // Create and init (this does both) associated token account if necessary. + invoke_signed( + &create_associated_token_account_idempotent( + ata_payer_account_info.key, + recipient_wallet_account_info.key, + mint_account_info.key, + spl_token_account_info.key, + ), + &[ + ata_payer_account_info.clone(), + recipient_ata_account_info.clone(), + recipient_wallet_account_info.clone(), + mint_account_info.clone(), + system_program_account_info.clone(), + spl_token_account_info.clone(), + ], + &[hyperlane_token_ata_payer_pda_seeds!( + token.plugin_data.ata_payer_bump + )], + )?; + + // After potentially paying for the ATA creation, we need to make sure + // the ATA payer still meets the rent-exemption requirements! + verify_rent_exempt(ata_payer_account_info, &Rent::get()?)?; + + let transfer_instruction = transfer_checked( + spl_token_account_info.key, + escrow_account_info.key, + mint_account_info.key, + recipient_ata_account_info.key, + escrow_account_info.key, + &[], + amount, + token.decimals, + )?; + + invoke_signed( + &transfer_instruction, + &[ + escrow_account_info.clone(), + mint_account_info.clone(), + recipient_ata_account_info.clone(), + escrow_account_info.clone(), + ], + &[hyperlane_token_escrow_pda_seeds!( + token.plugin_data.escrow_bump + )], + )?; + + Ok(()) + } + + /// Returns the accounts required for `transfer_out`. + fn transfer_out_account_metas( + program_id: &Pubkey, + token: &HyperlaneToken, + token_message: &TokenMessage, + ) -> Result<(Vec, bool), ProgramError> { + let ata_payer_account_key = Pubkey::create_program_address( + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump), + program_id, + )?; + + let recipient_associated_token_account = get_associated_token_address_with_program_id( + &Pubkey::new_from_array(token_message.recipient().into()), + &token.plugin_data.mint, + &token.plugin_data.spl_token_program, + ); + + Ok(( + vec![ + AccountMeta::new_readonly(token.plugin_data.spl_token_program, false).into(), + AccountMeta::new_readonly(spl_associated_token_account::id(), false).into(), + AccountMeta::new(token.plugin_data.mint, false).into(), + AccountMeta::new(recipient_associated_token_account, false).into(), + AccountMeta::new(ata_payer_account_key, false).into(), + AccountMeta::new(token.plugin_data.escrow, false).into(), + ], + // The recipient does not need to be writeable + false, + )) + } +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/processor.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/processor.rs new file mode 100644 index 0000000000..ceb6231345 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/src/processor.rs @@ -0,0 +1,223 @@ +//! Program processor. + +use account_utils::DiscriminatorDecode; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + instruction::{Init, Instruction as TokenIxn, TransferRemote}, + processor::HyperlaneSealevelToken, +}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; + +use crate::plugin::CollateralPlugin; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// Processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // First, check if the instruction has a discriminant relating to + // the message recipient interface. + if let Ok(message_recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) + { + return match message_recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + interchain_security_module_account_metas(program_id) + } + MessageRecipientInstruction::Handle(handle) => transfer_from_remote( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ), + MessageRecipientInstruction::HandleAccountMetas(handle) => { + transfer_from_remote_account_metas( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ) + } + }; + } + + // Otherwise, try decoding a "normal" token instruction + match TokenIxn::decode(instruction_data)? { + TokenIxn::Init(init) => initialize(program_id, accounts, init), + TokenIxn::TransferRemote(xfer) => transfer_remote(program_id, accounts, xfer), + TokenIxn::EnrollRemoteRouter(config) => enroll_remote_router(program_id, accounts, config), + TokenIxn::EnrollRemoteRouters(configs) => { + enroll_remote_routers(program_id, accounts, configs) + } + TokenIxn::SetInterchainSecurityModule(new_ism) => { + set_interchain_security_module(program_id, accounts, new_ism) + } + TokenIxn::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + } + .map_err(|err| { + msg!("{}", err); + err + }) +} + +/// Initializes the program. +/// +/// Accounts: +/// 0. [executable] The system program. +/// 1. [writable] The token PDA account. +/// 2. [writable] The dispatch authority PDA account. +/// 3. [signer] The payer and access control owner of the program. +/// 4. [executable] The SPL token program for the mint, i.e. either SPL token program or the 2022 version. +/// 5. [] The mint. +/// 6. [executable] The Rent sysvar program. +/// 7. [writable] The escrow PDA account. +/// 8. [writable] The ATA payer PDA account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + HyperlaneSealevelToken::::initialize(program_id, accounts, init) +} + +/// Transfers tokens to a remote. +/// Burns the tokens from the sender's associated token account and +/// then dispatches a message to the remote recipient. +/// +/// Accounts: +/// 0. [executable] The system program. +/// 1. [executable] The spl_noop program. +/// 2. [] The token PDA account. +/// 3. [executable] The mailbox program. +/// 4. [writeable] The mailbox outbox account. +/// 5. [] Message dispatch authority. +/// 6. [signer] The token sender and mailbox payer. +/// 7. [signer] Unique message account. +/// 8. [writeable] Message storage PDA. +/// 9. [executable] The SPL token program for the mint. +/// 10. [writeable] The mint. +/// 11. [writeable] The token sender's associated token account, from which tokens will be sent. +/// 12. [writeable] The escrow PDA account. +fn transfer_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemote, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_remote(program_id, accounts, transfer) +} + +// Accounts: +// 0. [signer] Mailbox process authority specific to this program. +// 1. [executable] system_program +// 2. [] hyperlane_token storage +// 3. [] recipient wallet address +// 4. [executable] SPL token 2022 program. +// 5. [executable] SPL associated token account. +// 6. [writeable] Mint account. +// 7. [writeable] Recipient associated token account. +// 8. [writeable] ATA payer PDA account. +// 9. [writeable] Escrow account. +fn transfer_from_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote(program_id, accounts, transfer) +} + +/// Gets the account metas for a `transfer_from_remote` instruction. +/// +/// Accounts: +/// None +fn transfer_from_remote_account_metas( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote_account_metas( + program_id, accounts, transfer, + ) +} + +/// Enrolls a remote router. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The owner. +fn enroll_remote_router( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: RemoteRouterConfig, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_router(program_id, accounts, config) +} + +/// Enrolls remote routers. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The owner. +fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_routers(program_id, accounts, configs) +} + +/// Transfers ownership. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The current owner. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_ownership(program_id, accounts, new_owner) +} + +/// Gets the interchain security module, returning it as a serialized Option. +/// +/// Accounts: +/// 0. [] The token PDA account. +fn interchain_security_module(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module(program_id, accounts) +} + +/// Gets the account metas for getting the interchain security module. +/// +/// Accounts: +/// None +fn interchain_security_module_account_metas(program_id: &Pubkey) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module_account_metas(program_id) +} + +/// Lets the owner set the interchain security module. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The access control owner. +fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_ism: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_security_module( + program_id, accounts, new_ism, + ) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-collateral/tests/functional.rs b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/tests/functional.rs new file mode 100644 index 0000000000..342110354b --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-collateral/tests/functional.rs @@ -0,0 +1,1336 @@ +//! Contains functional tests for things that cannot be done +//! strictly in unit tests. This includes CPIs, like creating +//! new PDA accounts. + +use account_utils::DiscriminatorEncode; +use hyperlane_core::{Encode, HyperlaneMessage, H256, U256}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + program_pack::Pack, + pubkey, + pubkey::Pubkey, + rent::Rent, + system_instruction, +}; +use std::collections::HashMap; + +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_mailbox::{ + accounts::{DispatchedMessage, DispatchedMessageAccount}, + mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds, + mailbox_process_authority_pda_seeds, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_collateral::{ + hyperlane_token_ata_payer_pda_seeds, hyperlane_token_escrow_pda_seeds, + plugin::CollateralPlugin, processor::process_instruction, +}; +use hyperlane_sealevel_token_lib::{ + accounts::{convert_decimals, HyperlaneToken, HyperlaneTokenAccount}, + hyperlane_token_pda_seeds, + instruction::{Init, Instruction as HyperlaneTokenInstruction, TransferRemote}, + message::TokenMessage, +}; +use hyperlane_test_utils::{ + assert_token_balance, assert_transaction_error, initialize_mailbox, mailbox_id, + new_funded_keypair, process, transfer_lamports, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; +use spl_associated_token_account::instruction::create_associated_token_account_idempotent; +use spl_token_2022::instruction::initialize_mint2; + +/// There are 1e9 lamports in one SOL. +const ONE_SOL_IN_LAMPORTS: u64 = 1000000000; +const LOCAL_DOMAIN: u32 = 1234; +const LOCAL_DECIMALS: u8 = 8; +const LOCAL_DECIMALS_U32: u32 = LOCAL_DECIMALS as u32; +const REMOTE_DOMAIN: u32 = 4321; +const REMOTE_DECIMALS: u8 = 18; +// Same for spl_token_2022 and spl_token +const MINT_ACCOUNT_LEN: usize = spl_token_2022::state::Mint::LEN; + +fn hyperlane_sealevel_token_collateral_id() -> Pubkey { + pubkey!("G8t1qe3YnYvhi1zS9ioUXuVFkwhBgvfHaLJt5X6PF18z") +} + +async fn setup_client() -> (BanksClient, Keypair) { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mut program_test = ProgramTest::new( + "hyperlane_sealevel_token_collateral", + program_id, + processor!(process_instruction), + ); + + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(spl_token_2022::processor::Processor::process), + ); + + program_test.add_program( + "spl_token", + spl_token::id(), + processor!(spl_token::processor::Processor::process), + ); + + program_test.add_program( + "spl_associated_token_account", + spl_associated_token_account::id(), + processor!(spl_associated_token_account::processor::process_instruction), + ); + + program_test.add_program("spl_noop", spl_noop::id(), processor!(spl_noop::noop)); + + let mailbox_program_id = mailbox_id(); + program_test.add_program( + "hyperlane_sealevel_mailbox", + mailbox_program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + // This serves as the default ISM on the Mailbox + program_test.add_program( + "hyperlane_sealevel_test_ism", + hyperlane_sealevel_test_ism::id(), + processor!(hyperlane_sealevel_test_ism::program::process_instruction), + ); + + let (banks_client, payer, _recent_blockhash) = program_test.start().await; + + (banks_client, payer) +} + +async fn initialize_mint( + banks_client: &mut BanksClient, + payer: &Keypair, + decimals: u8, + spl_token_program: &Pubkey, +) -> (Pubkey, Keypair) { + let mint = Keypair::new(); + let mint_authority = new_funded_keypair(banks_client, payer, ONE_SOL_IN_LAMPORTS).await; + + let payer_pubkey = payer.pubkey(); + let mint_pubkey = mint.pubkey(); + let mint_authority_pubkey = mint_authority.pubkey(); + + let init_mint_instruction = initialize_mint2( + spl_token_program, + &mint_pubkey, + &mint_authority_pubkey, + // No freeze authority + None, + decimals, + ) + .unwrap(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &payer_pubkey, + &mint_pubkey, + Rent::default().minimum_balance(MINT_ACCOUNT_LEN), + MINT_ACCOUNT_LEN.try_into().unwrap(), + spl_token_program, + ), + init_mint_instruction, + ], + Some(&payer_pubkey), + &[payer, &mint], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + (mint_pubkey, mint_authority) +} + +async fn mint_to( + banks_client: &mut BanksClient, + spl_token_program_id: &Pubkey, + mint: &Pubkey, + mint_authority: &Keypair, + recipient_account: &Pubkey, + amount: u64, +) { + let mint_instruction = spl_token_2022::instruction::mint_to( + spl_token_program_id, + mint, + recipient_account, + &mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[mint_instruction], + Some(&mint_authority.pubkey()), + &[mint_authority], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); +} + +async fn create_and_mint_to_ata( + banks_client: &mut BanksClient, + spl_token_program_id: &Pubkey, + mint: &Pubkey, + mint_authority: &Keypair, + payer: &Keypair, + recipient_wallet: &Pubkey, + amount: u64, +) -> Pubkey { + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + recipient_wallet, + mint, + spl_token_program_id, + ); + + // Create and init (this does both) associated token account if necessary. + let create_ata_instruction = create_associated_token_account_idempotent( + &payer.pubkey(), + recipient_wallet, + mint, + spl_token_program_id, + ); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[create_ata_instruction], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Mint tokens to the associated token account. + if amount > 0 { + mint_to( + banks_client, + spl_token_program_id, + mint, + mint_authority, + &recipient_associated_token_account, + amount, + ) + .await; + } + + recipient_associated_token_account +} + +struct HyperlaneTokenAccounts { + token: Pubkey, + token_bump: u8, + mailbox_process_authority: Pubkey, + dispatch_authority: Pubkey, + dispatch_authority_bump: u8, + escrow: Pubkey, + escrow_bump: u8, + ata_payer: Pubkey, + ata_payer_bump: u8, +} + +async fn initialize_hyperlane_token( + program_id: &Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, + mint: &Pubkey, + spl_token_program: &Pubkey, +) -> Result { + let (mailbox_process_authority_key, _mailbox_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &mailbox_id(), + ); + + let (token_account_key, token_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let (dispatch_authority_key, dispatch_authority_seed) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + + let (escrow_account_key, escrow_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_escrow_pda_seeds!(), program_id); + + let (ata_payer_account_key, ata_payer_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::Init(Init { + mailbox: mailbox_id(), + interchain_security_module: None, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + }) + .encode() + .unwrap(), + vec![ + // 0. [executable] The system program. + // 1. [writable] The token PDA account. + // 2. [writable] The dispatch authority PDA account. + // 3. [signer] The payer. + // 4. [executable] The SPL token program for the mint, i.e. either SPL token program or the 2022 version. + // 5. [] The mint. + // 6. [executable] The Rent sysvar program. + // 7. [writable] The escrow PDA account. + // 8. [writable] The ATA payer PDA account. + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(token_account_key, false), + AccountMeta::new(dispatch_authority_key, false), + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(*spl_token_program, false), + AccountMeta::new(*mint, false), + AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), + AccountMeta::new(escrow_account_key, false), + AccountMeta::new(ata_payer_account_key, false), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(HyperlaneTokenAccounts { + token: token_account_key, + token_bump: token_account_bump_seed, + mailbox_process_authority: mailbox_process_authority_key, + dispatch_authority: dispatch_authority_key, + dispatch_authority_bump: dispatch_authority_seed, + escrow: escrow_account_key, + escrow_bump: escrow_account_bump_seed, + ata_payer: ata_payer_account_key, + ata_payer_bump: ata_payer_account_bump_seed, + }) +} + +async fn enroll_remote_router( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + router: H256, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain, + router: Some(router), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Get the token account. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token, + Box::new(HyperlaneToken { + bump: hyperlane_token_accounts.token_bump, + mailbox: mailbox_accounts.program, + mailbox_process_authority: hyperlane_token_accounts.mailbox_process_authority, + dispatch_authority_bump: hyperlane_token_accounts.dispatch_authority_bump, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + owner: Some(payer.pubkey()), + interchain_security_module: None, + remote_routers: HashMap::new(), + plugin_data: CollateralPlugin { + spl_token_program: spl_token_2022::id(), + mint, + escrow: hyperlane_token_accounts.escrow, + escrow_bump: hyperlane_token_accounts.escrow_bump, + ata_payer_bump: hyperlane_token_accounts.ata_payer_bump, + }, + }), + ); + + // Verify the escrow account was created. + let escrow_account = banks_client + .get_account(hyperlane_token_accounts.escrow) + .await + .unwrap() + .unwrap(); + assert_eq!(escrow_account.owner, spl_token_2022::id()); + assert!(!escrow_account.data.is_empty()); + + // Verify the ATA payer account was created. + let ata_payer_account = banks_client + .get_account(hyperlane_token_accounts.ata_payer) + .await + .unwrap() + .unwrap(); + assert!(ata_payer_account.lamports > 0); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // To ensure a different signature is used, we'll use a different payer + let init_result = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &mint_authority, + &mint, + &spl_token_program_id, + ) + .await; + + assert_transaction_error( + init_result, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); +} + +async fn test_transfer_remote(spl_token_program_id: Pubkey) { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let token_sender = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + let token_sender_pubkey = token_sender.pubkey(); + // Mint 100 tokens to the token sender's ATA + let token_sender_ata = create_and_mint_to_ata( + &mut banks_client, + &spl_token_program_id, + &mint, + &mint_authority, + &payer, + &token_sender_pubkey, + 100 * 10u64.pow(LOCAL_DECIMALS_U32), + ) + .await; + + // Call transfer_remote + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_program_id, + ); + + let remote_token_recipient = H256::random(); + // Transfer 69 tokens. + let transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = + convert_decimals(transfer_amount.into(), LOCAL_DECIMALS, REMOTE_DECIMALS).unwrap(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferRemote(TransferRemote { + destination_domain: REMOTE_DOMAIN, + recipient: remote_token_recipient, + amount_or_id: transfer_amount.into(), + }) + .encode() + .unwrap(), + // 0. [executable] The system program. + // 1. [executable] The spl_noop program. + // 2. [] The token PDA account. + // 3. [executable] The mailbox program. + // 4. [writeable] The mailbox outbox account. + // 5. [] Message dispatch authority. + // 6. [signer] The token sender and mailbox payer. + // 7. [signer] Unique message account. + // 8. [writeable] Message storage PDA. + // 9. [executable] The spl_token_2022 program. + // 10. [writeable] The mint. + // 11. [writeable] The token sender's associated token account, from which tokens will be sent. + // 12. [writeable] The escrow PDA account. + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mailbox_accounts.program, false), + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(hyperlane_token_accounts.dispatch_authority, false), + AccountMeta::new_readonly(token_sender_pubkey, true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_key, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(mint, false), + AccountMeta::new(token_sender_ata, false), + AccountMeta::new(hyperlane_token_accounts.escrow, false), + ], + )], + Some(&token_sender_pubkey), + &[&token_sender, &unique_message_account_keypair], + recent_blockhash, + ); + let tx_signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the token sender's ATA balance is 31 full tokens. + assert_token_balance( + &mut banks_client, + &token_sender_ata, + 31 * 10u64.pow(LOCAL_DECIMALS_U32), + ) + .await; + + // And that the escrow's balance is 69 tokens. + assert_token_balance( + &mut banks_client, + &hyperlane_token_accounts.escrow, + 69 * 10u64.pow(LOCAL_DECIMALS_U32), + ) + .await; + + // And let's take a look at the dispatched message account data to verify the message looks right. + let dispatched_message_account_data = banks_client + .get_account(dispatched_message_key) + .await + .unwrap() + .unwrap() + .data; + let dispatched_message = + DispatchedMessageAccount::fetch(&mut &dispatched_message_account_data[..]) + .unwrap() + .into_inner(); + + let transfer_remote_tx_status = banks_client + .get_transaction_status(tx_signature) + .await + .unwrap() + .unwrap(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: program_id.to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient: remote_router, + // Expect the remote_transfer_amount to be in the message. + body: TokenMessage::new(remote_token_recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + assert_eq!( + dispatched_message, + Box::new(DispatchedMessage::new( + message.nonce, + transfer_remote_tx_status.slot, + unique_message_account_keypair.pubkey(), + message.to_vec(), + )), + ); +} + +// Test transfer_remote with spl_token +#[tokio::test] +async fn test_transfer_remote_spl_token() { + test_transfer_remote(spl_token_2022::id()).await; +} + +// Test transfer_remote with spl_token_2022 +#[tokio::test] +async fn test_transfer_remote_spl_token_2022() { + test_transfer_remote(spl_token_2022::id()).await; +} + +async fn transfer_from_remote( + initial_escrow_balance: u64, + remote_transfer_amount: U256, + sender_override: Option, + origin_override: Option, + spl_token_program_id: Pubkey, +) -> Result<(BanksClient, HyperlaneTokenAccounts, Pubkey), BanksClientError> { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + // ATA payer must have a balance to create new ATAs + transfer_lamports( + &mut banks_client, + &payer, + &hyperlane_token_accounts.ata_payer, + ONE_SOL_IN_LAMPORTS, + ) + .await; + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Give an initial balance to the escrow account which will be used by the + // transfer_from_remote. + mint_to( + &mut banks_client, + &spl_token_program_id, + &mint, + &mint_authority, + &hyperlane_token_accounts.escrow, + initial_escrow_balance, + ) + .await; + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &mint, + &spl_token_program_id, + ); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: origin_override.unwrap_or(REMOTE_DOMAIN), + // Default to the remote router as the sender + sender: sender_override.unwrap_or(remote_router), + destination: LOCAL_DOMAIN, + recipient: program_id.to_bytes().into(), + body: TokenMessage::new(recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await?; + + Ok(( + banks_client, + hyperlane_token_accounts, + recipient_associated_token_account, + )) +} + +// Tests when the SPL token is the non-2022 version +#[tokio::test] +async fn test_transfer_from_remote_spl_token() { + let initial_escrow_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let (mut banks_client, hyperlane_token_accounts, recipient_associated_token_account) = + transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + None, + None, + spl_token::id(), + ) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_token_balance( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; + + // And that the escrow's balance is lower because it was spent in the transfer. + assert_token_balance( + &mut banks_client, + &hyperlane_token_accounts.escrow, + initial_escrow_balance - local_transfer_amount, + ) + .await; +} + +// Tests when the SPL token is the 2022 version +#[tokio::test] +async fn test_transfer_from_remote_spl_token_2022() { + let initial_escrow_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let (mut banks_client, hyperlane_token_accounts, recipient_associated_token_account) = + transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + None, + None, + spl_token_2022::id(), + ) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_token_balance( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; + + // And that the escrow's balance is lower because it was spent in the transfer. + assert_token_balance( + &mut banks_client, + &hyperlane_token_accounts.escrow, + initial_escrow_balance - local_transfer_amount, + ) + .await; +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_sender_not_router() { + let initial_escrow_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + // Same remote domain origin, but wrong sender. + let result = transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + Some(H256::random()), + None, + spl_token_2022::id(), + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); + + // Wrong remote domain origin, but correct sender. + let result = transfer_from_remote( + initial_escrow_balance, + remote_transfer_amount, + None, + Some(REMOTE_DOMAIN + 1), + spl_token_2022::id(), + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_process_authority_not_signer() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let mailbox_program_id = mailbox_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let _mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &mint, + &spl_token_2022::id(), + ); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try calling directly into the message handler, skipping the mailbox. + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MessageRecipientInstruction::Handle(HandleInstruction { + origin: REMOTE_DOMAIN, + sender: remote_router, + message: TokenMessage::new(recipient, 12345u64.into(), vec![]).to_vec(), + }) + .encode() + .unwrap(), + vec![ + // Recipient.handle accounts + // 0. [signer] Mailbox process authority + // 1. [executable] system_program + // 2. [] hyperlane_token storage + // 3. [] recipient wallet address + // 4. [executable] SPL token 2022 program. + // 5. [executable] SPL associated token account. + // 6. [writeable] Mint account. + // 7. [writeable] Recipient associated token account. + // 8. [writeable] ATA payer PDA account. + // 9. [writeable] Escrow account. + AccountMeta::new_readonly( + hyperlane_token_accounts.mailbox_process_authority, + false, + ), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(recipient_pubkey, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(spl_associated_token_account::id(), false), + AccountMeta::new(mint, false), + AccountMeta::new(recipient_associated_token_account, false), + AccountMeta::new(hyperlane_token_accounts.ata_payer, false), + AccountMeta::new(hyperlane_token_accounts.escrow, false), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Verify the remote router was enrolled. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.remote_routers, + vec![(REMOTE_DOMAIN, remote_router)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + // Use the mint authority as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let result = enroll_remote_router( + &mut banks_client, + &program_id, + &mint_authority, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + H256::random(), + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the mint authority as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain: REMOTE_DOMAIN, + router: Some(H256::random()), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&mint_authority.pubkey()), + &[&mint_authority], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_ownership() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new owner is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.owner, new_owner); +} + +#[tokio::test] +async fn test_transfer_ownership_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Try transferring ownership using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mint_authority.pubkey(), true), + ], + )], + Some(&mint_authority.pubkey()), + &[&mint_authority], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} + +#[tokio::test] +async fn test_set_interchain_security_module() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, _mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Set the ISM + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new ISM is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_security_module, new_ism); +} + +#[tokio::test] +async fn test_set_interchain_security_module_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_collateral_id(); + let spl_token_program_id = spl_token_2022::id(); + + let (mut banks_client, payer) = setup_client().await; + + let (mint, mint_authority) = initialize_mint( + &mut banks_client, + &payer, + LOCAL_DECIMALS, + &spl_token_program_id, + ) + .await; + + let hyperlane_token_accounts = initialize_hyperlane_token( + &program_id, + &mut banks_client, + &payer, + &mint, + &spl_token_program_id, + ) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Try setting the ISM using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mint_authority.pubkey(), true), + ], + )], + Some(&mint_authority.pubkey()), + &[&mint_authority], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native/Cargo.toml b/rust/sealevel/programs/hyperlane-sealevel-token-native/Cargo.toml new file mode 100644 index 0000000000..ed29443c59 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native/Cargo.toml @@ -0,0 +1,38 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-token-native" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +spl-noop.workspace = true +thiserror.workspace = true + +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-sealevel-token-lib = { path = "../../libraries/hyperlane-sealevel-token" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[dev-dependencies] +solana-program-test.workspace = true +solana-sdk.workspace = true + +hyperlane-test-utils = { path = "../../libraries/test-utils" } +hyperlane-sealevel-test-ism = { path = "../ism/test-ism", features = ["no-entrypoint"] } +# Unfortunately required for some functions in `solana-program-test`, and is not +# re-exported +tarpc = "*" + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native/src/instruction.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/instruction.rs new file mode 100644 index 0000000000..356d176b09 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/instruction.rs @@ -0,0 +1,32 @@ +//! Instructions for the program. + +use hyperlane_sealevel_token_lib::instruction::{init_instruction as lib_init_instruction, Init}; + +use crate::hyperlane_token_native_collateral_pda_seeds; + +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + init: Init, +) -> Result { + let mut instruction = lib_init_instruction(program_id, payer, init)?; + + // Add additional account metas: + // 0. [writable] The native collateral PDA account. + + let (native_collateral_key, _native_collatera_bump) = + Pubkey::find_program_address(hyperlane_token_native_collateral_pda_seeds!(), &program_id); + + instruction + .accounts + .append(&mut vec![AccountMeta::new(native_collateral_key, false)]); + + Ok(instruction) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native/src/lib.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/lib.rs new file mode 100644 index 0000000000..78ec3b2d33 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/lib.rs @@ -0,0 +1,11 @@ +//! Hyperlane token program for native tokens. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod instruction; +pub mod plugin; +pub mod processor; + +pub use spl_noop; diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native/src/plugin.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/plugin.rs new file mode 100644 index 0000000000..596fe9750e --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/plugin.rs @@ -0,0 +1,199 @@ +//! A plugin for the Hyperlane token program that transfers native +//! tokens in from a sender when sending to a remote chain, and transfers +//! native tokens out to recipients when receiving from a remote chain. + +use account_utils::{create_pda_account, verify_rent_exempt}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneToken, message::TokenMessage, processor::HyperlaneSealevelTokenPlugin, +}; +use serializable_account_meta::SerializableAccountMeta; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + instruction::AccountMeta, + program::{invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_instruction, + sysvar::Sysvar, +}; + +/// Seeds relating to the PDA account that holds native collateral. +#[macro_export] +macro_rules! hyperlane_token_native_collateral_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"native_collateral"] + }}; + + ($bump_seed:expr) => {{ + &[ + b"hyperlane_token", + b"-", + b"native_collateral", + &[$bump_seed], + ] + }}; +} + +/// A plugin for the Hyperlane token program that transfers native +/// tokens in from a sender when sending to a remote chain, and transfers +/// native tokens out to recipients when receiving from a remote chain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default)] +pub struct NativePlugin { + /// The bump seed for the native collateral PDA account. + pub native_collateral_bump: u8, +} + +impl NativePlugin { + /// Returns Ok(()) if the native collateral account info is valid. + /// Errors if the key or owner is incorrect. + fn verify_native_collateral_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + native_collateral_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let native_collateral_seeds: &[&[u8]] = + hyperlane_token_native_collateral_pda_seeds!(token.plugin_data.native_collateral_bump); + let expected_native_collateral_key = + Pubkey::create_program_address(native_collateral_seeds, program_id)?; + + if native_collateral_account_info.key != &expected_native_collateral_key { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } +} + +impl HyperlaneSealevelTokenPlugin for NativePlugin { + /// Initializes the plugin. + /// + /// Accounts: + /// 0. [writable] The native collateral PDA account. + fn initialize<'a, 'b>( + program_id: &Pubkey, + system_program: &'a AccountInfo<'b>, + _token_account: &'a AccountInfo<'b>, + payer_account: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + ) -> Result { + // Account 0: Native collateral PDA account. + let native_collateral_account = next_account_info(accounts_iter)?; + let (native_collateral_key, native_collateral_bump) = Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + program_id, + ); + if &native_collateral_key != native_collateral_account.key { + return Err(ProgramError::InvalidArgument); + } + + // Create native collateral PDA account. + // Assign ownership to the system program so it can transfer tokens. + create_pda_account( + payer_account, + &Rent::get()?, + 0, + &solana_program::system_program::id(), + system_program, + native_collateral_account, + hyperlane_token_native_collateral_pda_seeds!(native_collateral_bump), + )?; + + Ok(Self { + native_collateral_bump, + }) + } + + /// Transfers tokens into the program so they can be sent to a remote chain. + /// Burns the tokens from the sender's associated token account. + /// + /// Accounts: + /// 0. [executable] The system program. + /// 1. [writeable] The native token collateral PDA account. + fn transfer_in<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + sender_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: System program. + let system_program = next_account_info(accounts_iter)?; + if system_program.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Native collateral PDA account. + let native_collateral_account = next_account_info(accounts_iter)?; + Self::verify_native_collateral_account_info(program_id, token, native_collateral_account)?; + + // Transfer tokens into the native collateral account. + invoke( + &system_instruction::transfer(sender_wallet.key, native_collateral_account.key, amount), + &[sender_wallet.clone(), native_collateral_account.clone()], + ) + } + + /// Transfers tokens out to a recipient's associated token account as a + /// result of a transfer to this chain from a remote chain. + /// + /// Accounts: + /// 0. [executable] The system program. + /// 1. [writeable] The native token collateral PDA account. + fn transfer_out<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + _system_program: &'a AccountInfo<'b>, + recipient_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: System program. + let system_program = next_account_info(accounts_iter)?; + if system_program.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Native collateral PDA account. + let native_collateral_account = next_account_info(accounts_iter)?; + Self::verify_native_collateral_account_info(program_id, token, native_collateral_account)?; + + invoke_signed( + &system_instruction::transfer( + native_collateral_account.key, + recipient_wallet.key, + amount, + ), + &[native_collateral_account.clone(), recipient_wallet.clone()], + &[hyperlane_token_native_collateral_pda_seeds!( + token.plugin_data.native_collateral_bump + )], + )?; + + // Ensure the native collateral account is still rent exempt. + verify_rent_exempt(native_collateral_account, &Rent::get()?)?; + + Ok(()) + } + + /// Returns the accounts required for `transfer_out`. + fn transfer_out_account_metas( + program_id: &Pubkey, + _token: &HyperlaneToken, + _token_message: &TokenMessage, + ) -> Result<(Vec, bool), ProgramError> { + let (native_collateral_key, _native_collateral_bump) = Pubkey::find_program_address( + hyperlane_token_native_collateral_pda_seeds!(), + program_id, + ); + + Ok(( + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false).into(), + AccountMeta::new(native_collateral_key, false).into(), + ], + // Recipient wallet must be writeable to send lamports to it. + true, + )) + } +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native/src/processor.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/processor.rs new file mode 100644 index 0000000000..45a1674c3b --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native/src/processor.rs @@ -0,0 +1,209 @@ +//! Program processor. + +use account_utils::DiscriminatorDecode; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + instruction::{Init, Instruction as TokenIxn, TransferRemote}, + processor::HyperlaneSealevelToken, +}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; + +use crate::plugin::NativePlugin; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// Processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // First, check if the instruction has a discriminant relating to + // the message recipient interface. + if let Ok(message_recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) + { + return match message_recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + interchain_security_module_account_metas(program_id) + } + MessageRecipientInstruction::Handle(handle) => transfer_from_remote( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ), + MessageRecipientInstruction::HandleAccountMetas(handle) => { + transfer_from_remote_account_metas( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ) + } + }; + } + + // Otherwise, try decoding a "normal" token instruction + match TokenIxn::decode(instruction_data)? { + TokenIxn::Init(init) => initialize(program_id, accounts, init), + TokenIxn::TransferRemote(xfer) => transfer_remote(program_id, accounts, xfer), + TokenIxn::EnrollRemoteRouter(config) => enroll_remote_router(program_id, accounts, config), + TokenIxn::EnrollRemoteRouters(configs) => { + enroll_remote_routers(program_id, accounts, configs) + } + TokenIxn::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + TokenIxn::SetInterchainSecurityModule(new_ism) => { + set_interchain_security_module(program_id, accounts, new_ism) + } + } + .map_err(|err| { + msg!("{}", err); + err + }) +} + +/// Initializes the program. +/// +/// Accounts: +/// 0. [executable] The system program. +/// 1. [writable] The token PDA account. +/// 2. [writable] The dispatch authority PDA account. +/// 3. [signer] The payer and mailbox payer. +/// 4. [writable] The native collateral PDA account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + HyperlaneSealevelToken::::initialize(program_id, accounts, init) +} + +/// Transfers tokens to a remote. +/// Burns the tokens from the sender's associated token account and +/// then dispatches a message to the remote recipient. +/// +/// Accounts: +/// 0. [executable] The system program. +/// 1. [executable] The spl_noop program. +/// 2. [] The token PDA account. +/// 3. [executable] The mailbox program. +/// 4. [writeable] The mailbox outbox account. +/// 5. [] Message dispatch authority. +/// 6. [signer] The token sender and mailbox payer. +/// 7. [signer] Unique message account. +/// 8. [writeable] Message storage PDA. +/// 9. [executable] The system program. +/// 10. [writeable] The native token collateral PDA account. +fn transfer_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemote, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_remote(program_id, accounts, transfer) +} + +/// Accounts: +/// 0. [signer] Mailbox processor authority specific to this program. +/// 1. [executable] system_program +/// 2. [] hyperlane_token storage +/// 3. [writeable] recipient wallet address +/// 4. [executable] The system program. +/// 5. [writeable] The native token collateral PDA account. +fn transfer_from_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote(program_id, accounts, transfer) +} + +fn transfer_from_remote_account_metas( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote_account_metas( + program_id, accounts, transfer, + ) +} + +/// Enrolls a remote router. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The owner. +fn enroll_remote_router( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: RemoteRouterConfig, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_router(program_id, accounts, config) +} + +/// Enrolls remote routers. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The owner. +fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_routers(program_id, accounts, configs) +} + +/// Transfers ownership. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The current owner. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_ownership(program_id, accounts, new_owner) +} + +/// Gets the interchain security module, returning it as a serialized Option. +/// +/// Accounts: +/// 0. [] The token PDA account. +fn interchain_security_module(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module(program_id, accounts) +} + +/// Gets the account metas for getting the interchain security module. +/// +/// Accounts: +/// None +fn interchain_security_module_account_metas(program_id: &Pubkey) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module_account_metas(program_id) +} + +/// Lets the owner set the interchain security module. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The access control owner. +fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_ism: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_security_module( + program_id, accounts, new_ism, + ) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token-native/tests/functional.rs b/rust/sealevel/programs/hyperlane-sealevel-token-native/tests/functional.rs new file mode 100644 index 0000000000..68efda9abf --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token-native/tests/functional.rs @@ -0,0 +1,932 @@ +//! Contains functional tests for things that cannot be done +//! strictly in unit tests. This includes CPIs, like creating +//! new PDA accounts. + +use account_utils::DiscriminatorEncode; +use hyperlane_core::{Encode, HyperlaneMessage, H256, U256}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey, + pubkey::Pubkey, +}; +use std::collections::HashMap; + +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_mailbox::{ + accounts::{DispatchedMessage, DispatchedMessageAccount}, + mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds, + mailbox_process_authority_pda_seeds, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + accounts::{convert_decimals, HyperlaneToken, HyperlaneTokenAccount}, + hyperlane_token_pda_seeds, + instruction::{Init, Instruction as HyperlaneTokenInstruction, TransferRemote}, + message::TokenMessage, +}; +use hyperlane_sealevel_token_native::{ + hyperlane_token_native_collateral_pda_seeds, plugin::NativePlugin, + processor::process_instruction, +}; +use hyperlane_test_utils::{ + assert_lamports, assert_transaction_error, initialize_mailbox, mailbox_id, new_funded_keypair, + process, transfer_lamports, +}; +use solana_program_test::*; +use solana_sdk::{ + commitment_config::CommitmentLevel, + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; +use tarpc::context::Context; + +/// There are 1e9 lamports in one SOL. +const ONE_SOL_IN_LAMPORTS: u64 = 1000000000; +const LOCAL_DOMAIN: u32 = 1234; +const LOCAL_DECIMALS: u8 = 9; +const LOCAL_DECIMALS_U32: u32 = LOCAL_DECIMALS as u32; +const REMOTE_DOMAIN: u32 = 4321; +const REMOTE_DECIMALS: u8 = 18; + +fn hyperlane_sealevel_token_native_id() -> Pubkey { + pubkey!("CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga") +} + +async fn setup_client() -> (BanksClient, Keypair) { + let program_id = hyperlane_sealevel_token_native_id(); + let mut program_test = ProgramTest::new( + "hyperlane_sealevel_token_native", + program_id, + processor!(process_instruction), + ); + + program_test.add_program("spl_noop", spl_noop::id(), processor!(spl_noop::noop)); + + let mailbox_program_id = mailbox_id(); + program_test.add_program( + "hyperlane_sealevel_mailbox", + mailbox_program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + // This serves as the default ISM on the Mailbox + program_test.add_program( + "hyperlane_sealevel_test_ism", + hyperlane_sealevel_test_ism::id(), + processor!(hyperlane_sealevel_test_ism::program::process_instruction), + ); + + let (banks_client, payer, _recent_blockhash) = program_test.start().await; + + (banks_client, payer) +} + +struct HyperlaneTokenAccounts { + token: Pubkey, + token_bump: u8, + mailbox_process_authority: Pubkey, + dispatch_authority: Pubkey, + dispatch_authority_bump: u8, + native_collateral: Pubkey, + native_collateral_bump: u8, +} + +async fn initialize_hyperlane_token( + program_id: &Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, +) -> Result { + let (mailbox_process_authority_key, _mailbox_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &mailbox_id(), + ); + + let (token_account_key, token_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let (dispatch_authority_key, dispatch_authority_seed) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + + let (native_collateral_account_key, native_collateral_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_native_collateral_pda_seeds!(), program_id); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::Init(Init { + mailbox: mailbox_id(), + interchain_security_module: None, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + }) + .encode() + .unwrap(), + vec![ + // 0. [executable] The system program. + // 1. [writable] The token PDA account. + // 2. [writable] The dispatch authority PDA account. + // 3. [signer] The payer and mailbox payer. + // 4. [writable] The native collateral PDA account. + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(token_account_key, false), + AccountMeta::new(dispatch_authority_key, false), + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new(native_collateral_account_key, false), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(HyperlaneTokenAccounts { + token: token_account_key, + token_bump: token_account_bump_seed, + mailbox_process_authority: mailbox_process_authority_key, + dispatch_authority: dispatch_authority_key, + dispatch_authority_bump: dispatch_authority_seed, + native_collateral: native_collateral_account_key, + native_collateral_bump: native_collateral_account_bump_seed, + }) +} + +async fn enroll_remote_router( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + router: H256, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain, + router: Some(router), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Get the token account. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token, + Box::new(HyperlaneToken { + bump: hyperlane_token_accounts.token_bump, + mailbox: mailbox_accounts.program, + mailbox_process_authority: hyperlane_token_accounts.mailbox_process_authority, + dispatch_authority_bump: hyperlane_token_accounts.dispatch_authority_bump, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + owner: Some(payer.pubkey()), + interchain_security_module: None, + remote_routers: HashMap::new(), + plugin_data: NativePlugin { + native_collateral_bump: hyperlane_token_accounts.native_collateral_bump, + }, + }), + ); + + // Verify the ATA payer account was created. + let native_collateral_account = banks_client + .get_account(hyperlane_token_accounts.native_collateral) + .await + .unwrap() + .unwrap(); + assert!(native_collateral_account.lamports > 0); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let other_payer = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // To ensure a different signature is used, we'll use a different payer + let init_result = + initialize_hyperlane_token(&program_id, &mut banks_client, &other_payer).await; + + assert_transaction_error( + init_result, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); +} + +#[tokio::test] +async fn test_transfer_remote() { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Send 100 SOL for the token sender to start with. + let token_sender = + new_funded_keypair(&mut banks_client, &payer, 100 * ONE_SOL_IN_LAMPORTS).await; + let token_sender_pubkey = token_sender.pubkey(); + + // Call transfer_remote + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_program_id, + ); + + let remote_token_recipient = H256::random(); + // Transfer 69 tokens. + let transfer_amount = 69 * ONE_SOL_IN_LAMPORTS; + let remote_transfer_amount = + convert_decimals(transfer_amount.into(), LOCAL_DECIMALS, REMOTE_DECIMALS).unwrap(); + + let sender_balance_before = banks_client.get_balance(token_sender_pubkey).await.unwrap(); + let native_collateral_account_lamports_before = banks_client + .get_balance(hyperlane_token_accounts.native_collateral) + .await + .unwrap(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferRemote(TransferRemote { + destination_domain: REMOTE_DOMAIN, + recipient: remote_token_recipient, + amount_or_id: transfer_amount.into(), + }) + .encode() + .unwrap(), + // 0. [executable] The system program. + // 1. [executable] The spl_noop program. + // 2. [] The token PDA account. + // 3. [executable] The mailbox program. + // 4. [writeable] The mailbox outbox account. + // 5. [] Message dispatch authority. + // 6. [signer] The token sender and mailbox payer. + // 7. [signer] Unique message account. + // 8. [writeable] Message storage PDA. + // 9. [executable] The system program. + // 10. [writeable] The native token collateral PDA account. + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mailbox_accounts.program, false), + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(hyperlane_token_accounts.dispatch_authority, false), + AccountMeta::new_readonly(token_sender_pubkey, true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_key, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.native_collateral, false), + ], + )], + Some(&token_sender_pubkey), + &[&token_sender, &unique_message_account_keypair], + recent_blockhash, + ); + + let transaction_fee = banks_client + .get_fee_for_message_with_commitment_and_context( + Context::current(), + CommitmentLevel::Processed, + transaction.message.clone(), + ) + .await + .unwrap() + .unwrap(); + + let tx_signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await.unwrap(); + + // The transaction fee doesn't seem to be entirely accurate - + // this may be due to a mismatch between the SDK and the actual + // transaction fee calculation. + // For now, we'll just check that the sender's balance roughly correct. + let sender_balance_after = banks_client.get_balance(token_sender_pubkey).await.unwrap(); + let expected_balance_after = sender_balance_before - transfer_amount - transaction_fee; + // Allow 0.005 SOL of extra transaction fees + assert!( + sender_balance_after >= expected_balance_after - 5000000 + && sender_balance_after <= expected_balance_after + ); + + // And that the native collateral account's balance is 69 tokens. + assert_lamports( + &mut banks_client, + &hyperlane_token_accounts.native_collateral, + native_collateral_account_lamports_before + transfer_amount, + ) + .await; + + // And let's take a look at the dispatched message account data to verify the message looks right. + let dispatched_message_account_data = banks_client + .get_account(dispatched_message_key) + .await + .unwrap() + .unwrap() + .data; + let dispatched_message = + DispatchedMessageAccount::fetch(&mut &dispatched_message_account_data[..]) + .unwrap() + .into_inner(); + + let transfer_remote_tx_status = banks_client + .get_transaction_status(tx_signature) + .await + .unwrap() + .unwrap(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: program_id.to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient: remote_router, + // Expect the remote_transfer_amount to be in the message. + body: TokenMessage::new(remote_token_recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + assert_eq!( + dispatched_message, + Box::new(DispatchedMessage::new( + message.nonce, + transfer_remote_tx_status.slot, + unique_message_account_keypair.pubkey(), + message.to_vec(), + )), + ); +} + +async fn transfer_from_remote( + initial_native_collateral_balance: u64, + remote_transfer_amount: U256, + sender_override: Option, + origin_override: Option, +) -> Result<(BanksClient, HyperlaneTokenAccounts, Pubkey), BanksClientError> { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // The native collateral account will have some lamports because it's rent-exempt. + let current_native_collateral_balance = banks_client + .get_balance(hyperlane_token_accounts.native_collateral) + .await + .unwrap(); + + // Give an initial balance to the native collateral account which will be used by the + // transfer_from_remote. + transfer_lamports( + &mut banks_client, + &payer, + &hyperlane_token_accounts.native_collateral, + initial_native_collateral_balance.saturating_sub(current_native_collateral_balance), + ) + .await; + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: origin_override.unwrap_or(REMOTE_DOMAIN), + // Default to the remote router as the sender + sender: sender_override.unwrap_or(remote_router), + destination: LOCAL_DOMAIN, + recipient: program_id.to_bytes().into(), + body: TokenMessage::new(recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await?; + + Ok((banks_client, hyperlane_token_accounts, recipient_pubkey)) +} + +// Tests when the SPL token is the non-2022 version +#[tokio::test] +async fn test_transfer_from_success() { + let initial_native_collateral_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let (mut banks_client, hyperlane_token_accounts, recipient_associated_token_account) = + transfer_from_remote( + initial_native_collateral_balance, + remote_transfer_amount, + None, + None, + ) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_lamports( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; + + // And that the native collateral's balance is lower because it was spent in the transfer. + assert_lamports( + &mut banks_client, + &hyperlane_token_accounts.native_collateral, + initial_native_collateral_balance - local_transfer_amount, + ) + .await; +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_sender_not_router() { + let initial_native_collateral_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + // Same remote domain origin, but wrong sender. + let result = transfer_from_remote( + initial_native_collateral_balance, + remote_transfer_amount, + Some(H256::random()), + None, + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); + + // Wrong remote domain origin, but correct sender. + let result = transfer_from_remote( + initial_native_collateral_balance, + remote_transfer_amount, + None, + Some(REMOTE_DOMAIN + 1), + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_process_authority_not_signer() { + let program_id = hyperlane_sealevel_token_native_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let _mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try calling directly into the message handler, skipping the mailbox. + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MessageRecipientInstruction::Handle(HandleInstruction { + origin: REMOTE_DOMAIN, + sender: remote_router, + message: TokenMessage::new(recipient, 12345u64.into(), vec![]).to_vec(), + }) + .encode() + .unwrap(), + vec![ + // Recipient.handle accounts + // 0. [signer] Mailbox processor authority specific to this program. + // 1. [executable] system_program + // 2. [] hyperlane_token storage + // 3. [writeable] recipient wallet address + // 4. [executable] The system program. + // 5. [writeable] The native token collateral PDA account. + AccountMeta::new_readonly( + hyperlane_token_accounts.mailbox_process_authority, + false, + ), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new(recipient_pubkey, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(hyperlane_token_accounts.native_collateral, false), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Verify the remote router was enrolled. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.remote_routers, + vec![(REMOTE_DOMAIN, remote_router)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Use the mint authority as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let result = enroll_remote_router( + &mut banks_client, + &program_id, + &non_owner, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + H256::random(), + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the mint authority as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain: REMOTE_DOMAIN, + router: Some(H256::random()), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_ownership() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new owner is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.owner, new_owner); +} + +#[tokio::test] +async fn test_transfer_ownership_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try transferring ownership using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} + +#[tokio::test] +async fn test_set_interchain_security_module() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Set the ISM + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new ISM is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_security_module, new_ism); +} + +#[tokio::test] +async fn test_set_interchain_security_module_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_native_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try setting the ISM using the non_owner key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token/Cargo.toml b/rust/sealevel/programs/hyperlane-sealevel-token/Cargo.toml new file mode 100644 index 0000000000..a56e4ef1c2 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token/Cargo.toml @@ -0,0 +1,38 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-token" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +spl-associated-token-account.workspace = true +spl-noop.workspace = true +spl-token-2022.workspace = true +spl-token.workspace = true +thiserror.workspace = true + +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-connection-client = { path = "../../libraries/hyperlane-sealevel-connection-client" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-sealevel-token-lib = { path = "../../libraries/hyperlane-sealevel-token" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[dev-dependencies] +solana-program-test.workspace = true +solana-sdk.workspace = true + +hyperlane-test-utils = { path = "../../libraries/test-utils" } +hyperlane-sealevel-test-ism = { path = "../ism/test-ism", features = ["no-entrypoint"] } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/hyperlane-sealevel-token/src/instruction.rs b/rust/sealevel/programs/hyperlane-sealevel-token/src/instruction.rs new file mode 100644 index 0000000000..594e5836e5 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token/src/instruction.rs @@ -0,0 +1,37 @@ +//! Instructions for the program. + +use hyperlane_sealevel_token_lib::instruction::{init_instruction as lib_init_instruction, Init}; + +use crate::{hyperlane_token_ata_payer_pda_seeds, hyperlane_token_mint_pda_seeds}; + +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + init: Init, +) -> Result { + let mut instruction = lib_init_instruction(program_id, payer, init)?; + + // Add additional account metas: + // 0. [writable] The mint / mint authority PDA account. + // 1. [writable] The ATA payer PDA account. + + let (mint_key, _mint_bump) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), &program_id); + + let (ata_payer_key, _ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), &program_id); + + instruction.accounts.append(&mut vec![ + AccountMeta::new(mint_key, false), + AccountMeta::new(ata_payer_key, false), + ]); + + Ok(instruction) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token/src/lib.rs b/rust/sealevel/programs/hyperlane-sealevel-token/src/lib.rs new file mode 100644 index 0000000000..69bcde8238 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token/src/lib.rs @@ -0,0 +1,14 @@ +//! Hyperlane Token program for synthetic tokens. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod instruction; +pub mod plugin; +pub mod processor; + +pub use spl_associated_token_account; +pub use spl_noop; +pub use spl_token; +pub use spl_token_2022; diff --git a/rust/sealevel/programs/hyperlane-sealevel-token/src/plugin.rs b/rust/sealevel/programs/hyperlane-sealevel-token/src/plugin.rs new file mode 100644 index 0000000000..fd248162bd --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token/src/plugin.rs @@ -0,0 +1,355 @@ +//! A plugin for the Hyperlane token program that mints synthetic +//! tokens upon receiving a transfer from a remote chain, and burns +//! synthetic tokens when transferring out to a remote chain. + +use account_utils::{create_pda_account, verify_rent_exempt}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_token_lib::{ + accounts::HyperlaneToken, message::TokenMessage, processor::HyperlaneSealevelTokenPlugin, +}; +use serializable_account_meta::SerializableAccountMeta; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + instruction::AccountMeta, + program::{invoke, invoke_signed}, + program_error::ProgramError, + program_pack::Pack as _, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, + instruction::create_associated_token_account_idempotent, +}; +use spl_token_2022::instruction::{burn_checked, mint_to_checked}; + +/// Seeds relating to the PDA account that acts both as the mint +/// *and* the mint authority. +#[macro_export] +macro_rules! hyperlane_token_mint_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"mint"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"mint", &[$bump_seed]] + }}; +} + +/// Seeds relating to the PDA account that acts as the ATA payer. +#[macro_export] +macro_rules! hyperlane_token_ata_payer_pda_seeds { + () => {{ + &[b"hyperlane_token", b"-", b"ata_payer"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane_token", b"-", b"ata_payer", &[$bump_seed]] + }}; +} + +/// A plugin for the Hyperlane token program that mints synthetic +/// tokens upon receiving a transfer from a remote chain, and burns +/// synthetic tokens when transferring out to a remote chain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default)] +pub struct SyntheticPlugin { + /// The mint / mint authority PDA account. + pub mint: Pubkey, + /// The bump seed for the mint / mint authority PDA account. + pub mint_bump: u8, + /// The bump seed for the ATA payer PDA account. + pub ata_payer_bump: u8, +} + +impl SyntheticPlugin { + const MINT_ACCOUNT_SIZE: usize = spl_token_2022::state::Mint::LEN; + + /// Returns Ok(()) if the mint account info is valid. + /// Errors if the key or owner is incorrect. + fn verify_mint_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + mint_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let mint_seeds: &[&[u8]] = hyperlane_token_mint_pda_seeds!(token.plugin_data.mint_bump); + let expected_mint_key = Pubkey::create_program_address(mint_seeds, program_id)?; + if mint_account_info.key != &expected_mint_key { + return Err(ProgramError::InvalidArgument); + } + if *mint_account_info.key != token.plugin_data.mint { + return Err(ProgramError::InvalidArgument); + } + if mint_account_info.owner != &spl_token_2022::id() { + return Err(ProgramError::IncorrectProgramId); + } + + Ok(()) + } + + fn verify_ata_payer_account_info( + program_id: &Pubkey, + token: &HyperlaneToken, + ata_payer_account_info: &AccountInfo, + ) -> Result<(), ProgramError> { + let ata_payer_seeds: &[&[u8]] = + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump); + let expected_ata_payer_account = + Pubkey::create_program_address(ata_payer_seeds, program_id)?; + if ata_payer_account_info.key != &expected_ata_payer_account { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } +} + +impl HyperlaneSealevelTokenPlugin for SyntheticPlugin { + /// Initializes the plugin. + /// Note this will create a PDA account that will serve as the mint, + /// so the transaction calling this instruction must include a subsequent + /// instruction initializing the mint with the SPL token 2022 program. + /// + /// Accounts: + /// 0. [writable] The mint / mint authority PDA account. + /// 1. [writable] The ATA payer PDA account. + fn initialize<'a, 'b>( + program_id: &Pubkey, + system_program: &'a AccountInfo<'b>, + _token_account: &'a AccountInfo<'b>, + payer_account: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + ) -> Result { + // Account 0: Mint / mint authority + let mint_account = next_account_info(accounts_iter)?; + let (mint_key, mint_bump) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), program_id); + if &mint_key != mint_account.key { + return Err(ProgramError::InvalidArgument); + } + + let rent = Rent::get()?; + + // Create mint / mint authority PDA. + // Grant ownership to the SPL token 2022 program. + create_pda_account( + payer_account, + &rent, + Self::MINT_ACCOUNT_SIZE, + &spl_token_2022::id(), + system_program, + mint_account, + hyperlane_token_mint_pda_seeds!(mint_bump), + )?; + + // Account 1: ATA payer. + let ata_payer_account = next_account_info(accounts_iter)?; + let (ata_payer_key, ata_payer_bump) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + if &ata_payer_key != ata_payer_account.key { + return Err(ProgramError::InvalidArgument); + } + + // Create the ATA payer. + // This is a separate PDA because the ATA program requires + // the payer to have no data in it. + create_pda_account( + payer_account, + &rent, + 0, + // Grant ownership to the system program so that the ATA program + // can call into the system program with the ATA payer as the + // payer. + &solana_program::system_program::id(), + system_program, + ata_payer_account, + hyperlane_token_ata_payer_pda_seeds!(ata_payer_bump), + )?; + + Ok(Self { + mint: mint_key, + mint_bump, + ata_payer_bump, + }) + } + + /// Transfers tokens into the program so they can be sent to a remote chain. + /// Burns the tokens from the sender's associated token account. + /// + /// Accounts: + /// 0. [executable] The spl_token_2022 program. + /// 1. [writeable] The mint / mint authority PDA account. + /// 2. [writeable] The token sender's associated token account, from which tokens will be burned. + fn transfer_in<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + sender_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // 0. SPL token 2022 program + let spl_token_2022 = next_account_info(accounts_iter)?; + if spl_token_2022.key != &spl_token_2022::id() || !spl_token_2022.executable { + return Err(ProgramError::InvalidArgument); + } + + // 1. The mint / mint authority. + let mint_account = next_account_info(accounts_iter)?; + Self::verify_mint_account_info(program_id, token, mint_account)?; + + // 2. The sender's associated token account. + let sender_ata = next_account_info(accounts_iter)?; + let expected_sender_associated_token_account = get_associated_token_address_with_program_id( + sender_wallet.key, + mint_account.key, + &spl_token_2022::id(), + ); + if sender_ata.key != &expected_sender_associated_token_account { + return Err(ProgramError::InvalidArgument); + } + + let burn_ixn = burn_checked( + &spl_token_2022::id(), + sender_ata.key, + mint_account.key, + sender_wallet.key, + &[sender_wallet.key], + amount, + token.decimals, + )?; + // Sender wallet is expected to have signed this transaction + invoke( + &burn_ixn, + &[ + sender_ata.clone(), + mint_account.clone(), + sender_wallet.clone(), + ], + )?; + + Ok(()) + } + + /// Transfers tokens out to a recipient's associated token account as a + /// result of a transfer to this chain from a remote chain. + /// + /// Accounts: + /// 0. [executable] SPL token 2022 program + /// 1. [executable] SPL associated token account + /// 2. [writeable] Mint account + /// 3. [writeable] Recipient associated token account + /// 4. [writeable] ATA payer PDA account. + fn transfer_out<'a, 'b>( + program_id: &Pubkey, + token: &HyperlaneToken, + system_program: &'a AccountInfo<'b>, + recipient_wallet: &'a AccountInfo<'b>, + accounts_iter: &mut std::slice::Iter<'a, AccountInfo<'b>>, + amount: u64, + ) -> Result<(), ProgramError> { + // Account 0: SPL token 2022 program + let spl_token_2022 = next_account_info(accounts_iter)?; + if spl_token_2022.key != &spl_token_2022::id() || !spl_token_2022.executable { + return Err(ProgramError::InvalidArgument); + } + // Account 1: SPL associated token account + let spl_ata = next_account_info(accounts_iter)?; + if spl_ata.key != &spl_associated_token_account::id() || !spl_ata.executable { + return Err(ProgramError::InvalidArgument); + } + + // Account 2: Mint account + let mint_account = next_account_info(accounts_iter)?; + Self::verify_mint_account_info(program_id, token, mint_account)?; + + // Account 3: Recipient associated token account + let recipient_ata = next_account_info(accounts_iter)?; + let expected_recipient_associated_token_account = + get_associated_token_address_with_program_id( + recipient_wallet.key, + mint_account.key, + &spl_token_2022::id(), + ); + if recipient_ata.key != &expected_recipient_associated_token_account { + return Err(ProgramError::InvalidArgument); + } + + // Account 4: ATA payer PDA account + let ata_payer_account = next_account_info(accounts_iter)?; + Self::verify_ata_payer_account_info(program_id, token, ata_payer_account)?; + + // Create and init (this does both) associated token account if necessary. + invoke_signed( + &create_associated_token_account_idempotent( + ata_payer_account.key, + recipient_wallet.key, + mint_account.key, + &spl_token_2022::id(), + ), + &[ + ata_payer_account.clone(), + recipient_ata.clone(), + recipient_wallet.clone(), + mint_account.clone(), + system_program.clone(), + spl_token_2022.clone(), + ], + &[hyperlane_token_ata_payer_pda_seeds!( + token.plugin_data.ata_payer_bump + )], + )?; + + // After potentially paying for the ATA creation, we need to make sure + // the ATA payer still meets the rent-exemption requirements. + verify_rent_exempt(recipient_ata, &Rent::get()?)?; + + let mint_ixn = mint_to_checked( + &spl_token_2022::id(), + mint_account.key, + recipient_ata.key, + mint_account.key, + &[], + amount, + token.decimals, + )?; + invoke_signed( + &mint_ixn, + &[ + mint_account.clone(), + recipient_ata.clone(), + mint_account.clone(), + ], + &[hyperlane_token_mint_pda_seeds!(token.plugin_data.mint_bump)], + )?; + + Ok(()) + } + + fn transfer_out_account_metas( + program_id: &Pubkey, + token: &HyperlaneToken, + token_message: &TokenMessage, + ) -> Result<(Vec, bool), ProgramError> { + let ata_payer_account_key = Pubkey::create_program_address( + hyperlane_token_ata_payer_pda_seeds!(token.plugin_data.ata_payer_bump), + program_id, + )?; + + let recipient_associated_token_account = get_associated_token_address_with_program_id( + &Pubkey::new_from_array(token_message.recipient().into()), + &token.plugin_data.mint, + &spl_token_2022::id(), + ); + + Ok(( + vec![ + AccountMeta::new_readonly(spl_token_2022::id(), false).into(), + AccountMeta::new_readonly(spl_associated_token_account::id(), false).into(), + AccountMeta::new(token.plugin_data.mint, false).into(), + AccountMeta::new(recipient_associated_token_account, false).into(), + AccountMeta::new(ata_payer_account_key, false).into(), + ], + // The recipient does not need to be writeable + false, + )) + } +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token/src/processor.rs b/rust/sealevel/programs/hyperlane-sealevel-token/src/processor.rs new file mode 100644 index 0000000000..4d404c2af5 --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token/src/processor.rs @@ -0,0 +1,216 @@ +//! Program processor. + +use account_utils::DiscriminatorDecode; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token_lib::{ + instruction::{Init, Instruction as TokenIxn, TransferRemote}, + processor::HyperlaneSealevelToken, +}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; + +use crate::plugin::SyntheticPlugin; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// Processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // First, check if the instruction has a discriminant relating to + // the message recipient interface. + if let Ok(message_recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) + { + return match message_recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + interchain_security_module_account_metas(program_id) + } + MessageRecipientInstruction::Handle(handle) => transfer_from_remote( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ), + MessageRecipientInstruction::HandleAccountMetas(handle) => { + transfer_from_remote_account_metas( + program_id, + accounts, + HandleInstruction { + origin: handle.origin, + sender: handle.sender, + message: handle.message, + }, + ) + } + }; + } + + // Otherwise, try decoding a "normal" token instruction + match TokenIxn::decode(instruction_data)? { + TokenIxn::Init(init) => initialize(program_id, accounts, init), + TokenIxn::TransferRemote(xfer) => transfer_remote(program_id, accounts, xfer), + TokenIxn::EnrollRemoteRouter(config) => enroll_remote_router(program_id, accounts, config), + TokenIxn::EnrollRemoteRouters(configs) => { + enroll_remote_routers(program_id, accounts, configs) + } + TokenIxn::SetInterchainSecurityModule(new_ism) => { + set_interchain_security_module(program_id, accounts, new_ism) + } + TokenIxn::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + } + .map_err(|err| { + msg!("{}", err); + err + }) +} + +/// Initializes the program. +/// +/// Accounts: +/// 0. [executable] The system program. +/// 1. [writable] The token PDA account. +/// 2. [writable] The dispatch authority PDA account. +/// 3. [signer] The payer. +/// 4. [writable] The mint / mint authority PDA account. +/// 5. [writable] The ATA payer PDA account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + HyperlaneSealevelToken::::initialize(program_id, accounts, init) +} + +/// Transfers tokens to a remote. +/// Burns the tokens from the sender's associated token account and +/// then dispatches a message to the remote recipient. +/// +/// Accounts: +/// 0. [executable] The system program. +/// 1. [executable] The spl_noop program. +/// 2. [] The token PDA account. +/// 3. [executable] The mailbox program. +/// 4. [writeable] The mailbox outbox account. +/// 5. [] Message dispatch authority. +/// 6. [signer] The token sender and mailbox payer. +/// 7. [signer] Unique message account. +/// 8. [writeable] Message storage PDA. +/// 9. [signer] The token sender. +/// 10. [executable] The spl_token_2022 program. +/// 11. [writeable] The mint / mint authority PDA account. +/// 12. [writeable] The token sender's associated token account, from which tokens will be burned. +fn transfer_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: TransferRemote, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_remote(program_id, accounts, transfer) +} + +// Accounts: +// 0. [signer] Mailbox process authority specific to this program. +// 1. [executable] system_program +// 2. [] hyperlane_token storage +// 3. [] recipient wallet address +// 4. [executable] SPL token 2022 program +// 5. [executable] SPL associated token account +// 6. [writeable] Mint account +// 7. [writeable] Recipient associated token account +// 8. [writeable] ATA payer PDA account. +fn transfer_from_remote( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote(program_id, accounts, transfer) +} + +/// Gets the account metas required for the `HandleInstruction` instruction. +fn transfer_from_remote_account_metas( + program_id: &Pubkey, + accounts: &[AccountInfo], + transfer: HandleInstruction, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_from_remote_account_metas( + program_id, accounts, transfer, + ) +} + +/// Enrolls a remote router. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The owner. +fn enroll_remote_router( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: RemoteRouterConfig, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_router(program_id, accounts, config) +} + +/// Enrolls remote routers. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The owner. +fn enroll_remote_routers( + program_id: &Pubkey, + accounts: &[AccountInfo], + configs: Vec, +) -> ProgramResult { + HyperlaneSealevelToken::::enroll_remote_routers(program_id, accounts, configs) +} + +/// Transfers ownership. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The current owner. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::transfer_ownership(program_id, accounts, new_owner) +} + +/// Gets the interchain security module, returning it as a serialized Option. +/// +/// Accounts: +/// 0. [] The token PDA account. +fn interchain_security_module(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module(program_id, accounts) +} + +/// Gets the account metas for getting the interchain security module. +/// +/// Accounts: +/// None +fn interchain_security_module_account_metas(program_id: &Pubkey) -> ProgramResult { + HyperlaneSealevelToken::::interchain_security_module_account_metas(program_id) +} + +/// Lets the owner set the interchain security module. +/// +/// Accounts: +/// 0. [writeable] The token PDA account. +/// 1. [signer] The access control owner. +fn set_interchain_security_module( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_ism: Option, +) -> ProgramResult { + HyperlaneSealevelToken::::set_interchain_security_module( + program_id, accounts, new_ism, + ) +} diff --git a/rust/sealevel/programs/hyperlane-sealevel-token/tests/functional.rs b/rust/sealevel/programs/hyperlane-sealevel-token/tests/functional.rs new file mode 100644 index 0000000000..4b51c461ef --- /dev/null +++ b/rust/sealevel/programs/hyperlane-sealevel-token/tests/functional.rs @@ -0,0 +1,965 @@ +//! Contains functional tests for things that cannot be done +//! strictly in unit tests. This includes CPIs, like creating +//! new PDA accounts. + +use account_utils::DiscriminatorEncode; +use hyperlane_core::{Encode, HyperlaneMessage, H256, U256}; +use hyperlane_sealevel_connection_client::router::RemoteRouterConfig; +use hyperlane_sealevel_mailbox::{ + accounts::{DispatchedMessage, DispatchedMessageAccount}, + mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds, + mailbox_process_authority_pda_seeds, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use hyperlane_sealevel_token::{ + hyperlane_token_ata_payer_pda_seeds, hyperlane_token_mint_pda_seeds, plugin::SyntheticPlugin, + processor::process_instruction, +}; +use hyperlane_sealevel_token_lib::{ + accounts::{convert_decimals, HyperlaneToken, HyperlaneTokenAccount}, + hyperlane_token_pda_seeds, + instruction::{Init, Instruction as HyperlaneTokenInstruction, TransferRemote}, + message::TokenMessage, +}; +use hyperlane_test_utils::{ + assert_token_balance, assert_transaction_error, initialize_mailbox, mailbox_id, + new_funded_keypair, process, transfer_lamports, MailboxAccounts, +}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey, + pubkey::Pubkey, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; +use spl_token_2022::instruction::initialize_mint2; +use std::collections::HashMap; + +/// There are 1e9 lamports in one SOL. +const ONE_SOL_IN_LAMPORTS: u64 = 1000000000; +const LOCAL_DOMAIN: u32 = 1234; +const LOCAL_DECIMALS: u8 = 8; +const LOCAL_DECIMALS_U32: u32 = LOCAL_DECIMALS as u32; +const REMOTE_DOMAIN: u32 = 4321; +const REMOTE_DECIMALS: u8 = 18; + +fn hyperlane_sealevel_token_id() -> Pubkey { + pubkey!("3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH") +} + +async fn setup_client() -> (BanksClient, Keypair) { + let program_id = hyperlane_sealevel_token_id(); + let mut program_test = ProgramTest::new( + "hyperlane_sealevel_token", + program_id, + processor!(process_instruction), + ); + + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(spl_token_2022::processor::Processor::process), + ); + + program_test.add_program( + "spl_associated_token_account", + spl_associated_token_account::id(), + processor!(spl_associated_token_account::processor::process_instruction), + ); + + program_test.add_program("spl_noop", spl_noop::id(), processor!(spl_noop::noop)); + + let mailbox_program_id = mailbox_id(); + program_test.add_program( + "hyperlane_sealevel_mailbox", + mailbox_program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + // This serves as the default ISM on the Mailbox + program_test.add_program( + "hyperlane_sealevel_test_ism", + hyperlane_sealevel_test_ism::id(), + processor!(hyperlane_sealevel_test_ism::program::process_instruction), + ); + + let (banks_client, payer, _recent_blockhash) = program_test.start().await; + + (banks_client, payer) +} + +struct HyperlaneTokenAccounts { + token: Pubkey, + token_bump: u8, + mailbox_process_authority: Pubkey, + dispatch_authority: Pubkey, + dispatch_authority_bump: u8, + mint: Pubkey, + mint_bump: u8, + ata_payer: Pubkey, + ata_payer_bump: u8, +} + +async fn initialize_hyperlane_token( + program_id: &Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, +) -> Result { + let (mailbox_process_authority_key, _mailbox_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &mailbox_id(), + ); + + let (token_account_key, token_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_pda_seeds!(), program_id); + + let (dispatch_authority_key, dispatch_authority_seed) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + + let (mint_account_key, mint_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_mint_pda_seeds!(), program_id); + + let (ata_payer_account_key, ata_payer_account_bump_seed) = + Pubkey::find_program_address(hyperlane_token_ata_payer_pda_seeds!(), program_id); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::Init(Init { + mailbox: mailbox_id(), + interchain_security_module: None, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + }) + .encode() + .unwrap(), + vec![ + // 0. [executable] The system program. + // 1. [writable] The token PDA account. + // 2. [writable] The dispatch authority PDA account. + // 3. [signer] The payer. + // 4. [writable] The mint / mint authority PDA account. + // 5. [writable] The ATA payer PDA account. + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(token_account_key, false), + AccountMeta::new(dispatch_authority_key, false), + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new(mint_account_key, false), + AccountMeta::new(ata_payer_account_key, false), + ], + ), + initialize_mint2( + &spl_token_2022::id(), + &mint_account_key, + &mint_account_key, + None, + LOCAL_DECIMALS, + ) + .unwrap(), + ], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(HyperlaneTokenAccounts { + token: token_account_key, + token_bump: token_account_bump_seed, + mailbox_process_authority: mailbox_process_authority_key, + dispatch_authority: dispatch_authority_key, + dispatch_authority_bump: dispatch_authority_seed, + mint: mint_account_key, + mint_bump: mint_account_bump_seed, + ata_payer: ata_payer_account_key, + ata_payer_bump: ata_payer_account_bump_seed, + }) +} + +async fn enroll_remote_router( + banks_client: &mut BanksClient, + program_id: &Pubkey, + payer: &Keypair, + token_account: &Pubkey, + domain: u32, + router: H256, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + *program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain, + router: Some(router), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(*token_account, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Get the token account. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token, + Box::new(HyperlaneToken { + bump: hyperlane_token_accounts.token_bump, + mailbox: mailbox_accounts.program, + mailbox_process_authority: hyperlane_token_accounts.mailbox_process_authority, + dispatch_authority_bump: hyperlane_token_accounts.dispatch_authority_bump, + decimals: LOCAL_DECIMALS, + remote_decimals: REMOTE_DECIMALS, + owner: Some(payer.pubkey()), + interchain_security_module: None, + remote_routers: HashMap::new(), + plugin_data: SyntheticPlugin { + mint: hyperlane_token_accounts.mint, + mint_bump: hyperlane_token_accounts.mint_bump, + ata_payer_bump: hyperlane_token_accounts.ata_payer_bump, + }, + }), + ); + + // Verify the mint account was created. + let mint_account = banks_client + .get_account(hyperlane_token_accounts.mint) + .await + .unwrap() + .unwrap(); + assert_eq!(mint_account.owner, spl_token_2022::id()); + assert!(!mint_account.data.is_empty()); + + // Verify the ATA payer account was created. + let ata_payer_account = banks_client + .get_account(hyperlane_token_accounts.ata_payer) + .await + .unwrap() + .unwrap(); + assert!(ata_payer_account.lamports > 0); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_payer = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // To ensure a different signature is used, we'll use a different payer + let init_result = initialize_hyperlane_token(&program_id, &mut banks_client, &new_payer).await; + + assert_transaction_error( + init_result, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); +} + +async fn transfer_from_remote( + remote_transfer_amount: U256, + sender_override: Option, + origin_override: Option, + recipient_wallet: Option, +) -> Result< + ( + BanksClient, + Keypair, + MailboxAccounts, + HyperlaneTokenAccounts, + Pubkey, + ), + BanksClientError, +> { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + // ATA payer must have a balance to create new ATAs + transfer_lamports( + &mut banks_client, + &payer, + &hyperlane_token_accounts.ata_payer, + ONE_SOL_IN_LAMPORTS, + ) + .await; + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = recipient_wallet.unwrap_or_else(Pubkey::new_unique); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &hyperlane_token_accounts.mint, + &spl_token_2022::id(), + ); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: origin_override.unwrap_or(REMOTE_DOMAIN), + // Default to the remote router as the sender + sender: sender_override.unwrap_or(remote_router), + destination: LOCAL_DOMAIN, + recipient: program_id.to_bytes().into(), + body: TokenMessage::new(recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await?; + + Ok(( + banks_client, + payer, + mailbox_accounts, + hyperlane_token_accounts, + recipient_associated_token_account, + )) +} + +// Tests when the SPL token is the 2022 version +#[tokio::test] +async fn test_transfer_from_remote() { + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + let ( + mut banks_client, + _payer, + _mailbox_accounts, + _hyperlane_token_accounts, + recipient_associated_token_account, + ) = transfer_from_remote(remote_transfer_amount, None, None, None) + .await + .unwrap(); + + // Check that the recipient's ATA got the tokens! + assert_token_balance( + &mut banks_client, + &recipient_associated_token_account, + local_transfer_amount, + ) + .await; +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_sender_not_router() { + let local_transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = convert_decimals( + local_transfer_amount.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(); + + // Same remote domain origin, but wrong sender. + let result = + transfer_from_remote(remote_transfer_amount, Some(H256::random()), None, None).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); + + // Wrong remote domain origin, but correct sender. + let result = + transfer_from_remote(remote_transfer_amount, None, Some(REMOTE_DOMAIN + 1), None).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData), + ); +} + +#[tokio::test] +async fn test_transfer_from_remote_errors_if_process_authority_not_signer() { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let (mut banks_client, payer) = setup_client().await; + + let _mailbox_accounts = + initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + let recipient_pubkey = Pubkey::new_unique(); + let recipient: H256 = recipient_pubkey.to_bytes().into(); + + let recipient_associated_token_account = + spl_associated_token_account::get_associated_token_address_with_program_id( + &recipient_pubkey, + &hyperlane_token_accounts.mint, + &spl_token_2022::id(), + ); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Try calling directly into the message handler, skipping the mailbox. + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MessageRecipientInstruction::Handle(HandleInstruction { + origin: REMOTE_DOMAIN, + sender: remote_router, + message: TokenMessage::new(recipient, 12345u64.into(), vec![]).to_vec(), + }) + .encode() + .unwrap(), + vec![ + // Recipient.handle accounts + // 0. [signer] Mailbox process authority specific to this program. + // 1. [executable] system_program + // 2. [] hyperlane_token storage + // 3. [] recipient wallet address + // 4. [executable] SPL token 2022 program + // 5. [executable] SPL associated token account + // 6. [writeable] Mint account + // 7. [writeable] Recipient associated token account + // 8. [writeable] ATA payer PDA account. + AccountMeta::new_readonly( + hyperlane_token_accounts.mailbox_process_authority, + false, + ), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(recipient_pubkey, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(spl_associated_token_account::id(), false), + AccountMeta::new(hyperlane_token_accounts.mint, false), + AccountMeta::new(recipient_associated_token_account, false), + AccountMeta::new(hyperlane_token_accounts.ata_payer, false), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_remote() { + let program_id = hyperlane_sealevel_token_id(); + let mailbox_program_id = mailbox_id(); + + let token_sender = Keypair::new(); + let token_sender_pubkey = token_sender.pubkey(); + + // Mint 100 tokens to the token sender's ATA. + // We do this by just faking a transfer from remote. + let sender_initial_balance = 100 * 10u64.pow(LOCAL_DECIMALS_U32); + let (mut banks_client, payer, mailbox_accounts, hyperlane_token_accounts, token_sender_ata) = + transfer_from_remote( + // The amount of remote tokens is expected + convert_decimals( + sender_initial_balance.into(), + LOCAL_DECIMALS, + REMOTE_DECIMALS, + ) + .unwrap(), + None, + None, + Some(token_sender_pubkey), + ) + .await + .unwrap(); + + // Give the token_sender a SOL balance to pay tx fees. + transfer_lamports( + &mut banks_client, + &payer, + &token_sender_pubkey, + ONE_SOL_IN_LAMPORTS, + ) + .await; + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Call transfer_remote + let unique_message_account_keypair = Keypair::new(); + let (dispatched_message_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_program_id, + ); + + let remote_token_recipient = H256::random(); + // Transfer 69 tokens. + let transfer_amount = 69 * 10u64.pow(LOCAL_DECIMALS_U32); + let remote_transfer_amount = + convert_decimals(transfer_amount.into(), LOCAL_DECIMALS, REMOTE_DECIMALS).unwrap(); + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferRemote(TransferRemote { + destination_domain: REMOTE_DOMAIN, + recipient: remote_token_recipient, + amount_or_id: transfer_amount.into(), + }) + .encode() + .unwrap(), + // 0. [executable] The system program. + // 1. [executable] The spl_noop program. + // 2. [] The token PDA account. + // 3. [executable] The mailbox program. + // 4. [writeable] The mailbox outbox account. + // 5. [] Message dispatch authority. + // 6. [signer] The token sender and mailbox payer. + // 7. [signer] Unique message account. + // 8. [writeable] Message storage PDA. + // 9. [executable] The spl_token_2022 program. + // 10. [writeable] The mint / mint authority PDA account. + // 11. [writeable] The token sender's associated token account, from which tokens will be burned. + vec![ + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new_readonly(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(mailbox_accounts.program, false), + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(hyperlane_token_accounts.dispatch_authority, false), + AccountMeta::new_readonly(token_sender_pubkey, true), + AccountMeta::new_readonly(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_key, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(hyperlane_token_accounts.mint, false), + AccountMeta::new(token_sender_ata, false), + ], + )], + Some(&token_sender_pubkey), + &[&token_sender, &unique_message_account_keypair], + recent_blockhash, + ); + let tx_signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the token sender's ATA balance went down + assert_token_balance( + &mut banks_client, + &token_sender_ata, + sender_initial_balance - transfer_amount, + ) + .await; + + // And let's take a look at the dispatched message account data to verify the message looks right. + let dispatched_message_account_data = banks_client + .get_account(dispatched_message_key) + .await + .unwrap() + .unwrap() + .data; + let dispatched_message = + DispatchedMessageAccount::fetch(&mut &dispatched_message_account_data[..]) + .unwrap() + .into_inner(); + + let transfer_remote_tx_status = banks_client + .get_transaction_status(tx_signature) + .await + .unwrap() + .unwrap(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: program_id.to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient: remote_router, + // Expect the remote_transfer_amount to be in the message. + body: TokenMessage::new(remote_token_recipient, remote_transfer_amount, vec![]).to_vec(), + }; + + assert_eq!( + dispatched_message, + Box::new(DispatchedMessage::new( + message.nonce, + transfer_remote_tx_status.slot, + unique_message_account_keypair.pubkey(), + message.to_vec(), + )), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + // Enroll the remote router + let remote_router = H256::random(); + enroll_remote_router( + &mut banks_client, + &program_id, + &payer, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + remote_router, + ) + .await + .unwrap(); + + // Verify the remote router was enrolled. + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!( + token.remote_routers, + vec![(REMOTE_DOMAIN, remote_router)].into_iter().collect(), + ); +} + +#[tokio::test] +async fn test_enroll_remote_router_errors_if_not_signed_by_owner() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Use the mint authority as the payer, which has a balance but is not the owner, + // so we expect this to fail. + let result = enroll_remote_router( + &mut banks_client, + &program_id, + &non_owner, + &hyperlane_token_accounts.token, + REMOTE_DOMAIN, + H256::random(), + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Also try using the mint authority as the payer and specifying the correct + // owner account, but the owner isn't a signer: + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + // Enroll the remote router + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::EnrollRemoteRouter(RemoteRouterConfig { + domain: REMOTE_DOMAIN, + router: Some(H256::random()), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} + +#[tokio::test] +async fn test_transfer_ownership() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new owner is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.owner, new_owner); +} + +#[tokio::test] +async fn test_transfer_ownership_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_owner = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try transferring ownership using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::TransferOwnership(new_owner) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} + +#[tokio::test] +async fn test_set_interchain_security_module() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + + // Set the ISM + // Transfer ownership + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(payer.pubkey(), true), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + // Verify the new ISM is set + let token_account_data = banks_client + .get_account(hyperlane_token_accounts.token) + .await + .unwrap() + .unwrap() + .data; + let token = HyperlaneTokenAccount::::fetch(&mut &token_account_data[..]) + .unwrap() + .into_inner(); + assert_eq!(token.interchain_security_module, new_ism); +} + +#[tokio::test] +async fn test_set_interchain_security_module_errors_if_owner_not_signer() { + let program_id = hyperlane_sealevel_token_id(); + + let (mut banks_client, payer) = setup_client().await; + + let hyperlane_token_accounts = + initialize_hyperlane_token(&program_id, &mut banks_client, &payer) + .await + .unwrap(); + + let new_ism = Some(Pubkey::new_unique()); + let non_owner = new_funded_keypair(&mut banks_client, &payer, ONE_SOL_IN_LAMPORTS).await; + + // Try setting the ISM using the mint authority key + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &HyperlaneTokenInstruction::SetInterchainSecurityModule(new_ism) + .encode() + .unwrap(), + vec![ + AccountMeta::new(hyperlane_token_accounts.token, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + )], + Some(&non_owner.pubkey()), + &[&non_owner], + recent_blockhash, + ); + let result = banks_client.process_transaction(transaction).await; + + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); +} diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/Cargo.toml b/rust/sealevel/programs/ism/multisig-ism-message-id/Cargo.toml new file mode 100644 index 0000000000..5a7af82e06 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/Cargo.toml @@ -0,0 +1,36 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-multisig-ism-message-id" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +thiserror.workspace = true + +access-control = { path = "../../../libraries/access-control" } +account-utils = { path = "../../../libraries/account-utils" } +ecdsa-signature = { path = "../../../libraries/ecdsa-signature" } +hyperlane-core = { path = "../../../../hyperlane-core" } +hyperlane-sealevel-interchain-security-module-interface = { path = "../../../libraries/interchain-security-module-interface" } +hyperlane-sealevel-mailbox = { path = "../../mailbox", features = ["no-entrypoint"] } +multisig-ism = { path = "../../../libraries/multisig-ism" } +serializable-account-meta = { path = "../../../libraries/serializable-account-meta" } + +[dev-dependencies] +hyperlane-sealevel-multisig-ism-message-id = { path = "../multisig-ism-message-id" } + +solana-program-test.workspace = true +solana-sdk.workspace = true +hex.workspace = true +multisig-ism = { path = "../../../libraries/multisig-ism", features = ["test-data"] } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/src/accounts.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/src/accounts.rs new file mode 100644 index 0000000000..9b49eecf10 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/src/accounts.rs @@ -0,0 +1,59 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +use access_control::AccessControl; +use account_utils::{AccountData, SizedData}; +use solana_program::{program_error::ProgramError, pubkey::Pubkey}; + +use crate::instruction::ValidatorsAndThreshold; + +/// The data of a "domain data" PDA account. +/// One of these exists for each domain that's been enrolled. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, PartialEq)] +pub struct DomainData { + pub bump_seed: u8, + pub validators_and_threshold: ValidatorsAndThreshold, +} + +pub type DomainDataAccount = AccountData; + +/// The data of the access control PDA account. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, PartialEq)] +pub struct AccessControlData { + pub bump_seed: u8, + pub owner: Option, +} + +impl SizedData for AccessControlData { + fn size(&self) -> usize { + // 1 byte bump seed + 1 byte Option variant + 32 byte owner pubkey + 1 + 1 + 32 + } +} + +impl AccessControl for AccessControlData { + fn owner(&self) -> Option<&Pubkey> { + self.owner.as_ref() + } + + fn set_owner(&mut self, new_owner: Option) -> Result<(), ProgramError> { + self.owner = new_owner; + Ok(()) + } +} + +pub type AccessControlAccount = AccountData; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_access_control_data_size() { + let data = AccessControlData { + bump_seed: 0, + owner: Some(Pubkey::new_unique()), + }; + let serialized = data.try_to_vec().unwrap(); + assert_eq!(data.size(), serialized.len()); + } +} diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/src/error.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/src/error.rs new file mode 100644 index 0000000000..ecca350fd0 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/src/error.rs @@ -0,0 +1,45 @@ +//! Hyperlane Sealevel mailbox contract specific errors. + +use solana_program::program_error::ProgramError; + +use multisig_ism::error::MultisigIsmError; + +#[derive(Copy, Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] +#[repr(u32)] +pub enum Error { + #[error("Account not found in the correct order")] + AccountOutOfOrder = 1, + #[error("Account is not owner")] + AccountNotOwner = 2, + #[error("Program ID is not owner")] + ProgramIdNotOwner = 3, + #[error("Account not initialized")] + AccountNotInitialized = 4, + #[error("Invalid signature recovery ID")] + InvalidSignatureRecoveryId = 5, + #[error("Invalid signature")] + InvalidSignature = 6, + #[error("Threshold not met")] + ThresholdNotMet = 7, + #[error("Invalid validators and threshold")] + InvalidValidatorsAndThreshold = 8, + #[error("Already initialized")] + AlreadyInitialized = 9, + #[error("Invalid metadata")] + InvalidMetadata = 10, +} + +impl From for Error { + fn from(err: MultisigIsmError) -> Self { + match err { + MultisigIsmError::InvalidSignature => Error::InvalidSignature, + MultisigIsmError::ThresholdNotMet => Error::ThresholdNotMet, + } + } +} + +impl From for ProgramError { + fn from(err: Error) -> Self { + ProgramError::Custom(err as u32) + } +} diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/src/instruction.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/src/instruction.rs new file mode 100644 index 0000000000..058affea61 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/src/instruction.rs @@ -0,0 +1,187 @@ +use account_utils::{DiscriminatorData, DiscriminatorEncode, PROGRAM_INSTRUCTION_DISCRIMINATOR}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::H160; +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use std::collections::HashSet; + +use crate::{access_control_pda_seeds, error::Error}; + +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub enum Instruction { + /// Initializes the program. + /// + /// Accounts: + /// 0. `[signer]` The new owner and payer of the access control PDA. + /// 1. `[writable]` The access control PDA account. + /// 2. `[executable]` The system program account. + Initialize, + /// Input: domain ID, validators, & threshold to set. + /// + /// Accounts: + /// 0. `[signer]` The access control owner and payer of the domain PDA. + /// 1. `[]` The access control PDA account. + /// 2. `[writable]` The PDA relating to the provided domain. + /// 3. `[executable]` OPTIONAL - The system program account. Required if creating the domain PDA. + SetValidatorsAndThreshold(Domained), + /// Gets the owner from the access control data. + /// + /// Accounts: + /// 0. `[]` The access control PDA account. + GetOwner, + /// Sets the owner in the access control data. + /// + /// Accounts: + /// 0. `[signer]` The current access control owner. + /// 1. `[]` The access control PDA account. + TransferOwnership(Option), +} + +impl DiscriminatorData for Instruction { + const DISCRIMINATOR: [u8; Self::DISCRIMINATOR_LENGTH] = PROGRAM_INSTRUCTION_DISCRIMINATOR; +} + +impl TryFrom<&[u8]> for Instruction { + type Error = ProgramError; + + fn try_from(data: &[u8]) -> Result { + Self::try_from_slice(data).map_err(|_| ProgramError::InvalidInstructionData) + } +} + +/// Holds data relating to a specific domain. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Clone)] +pub struct Domained { + pub domain: u32, + pub data: T, +} + +/// A configuration of a validator set and threshold. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Default, Clone)] +pub struct ValidatorsAndThreshold { + pub validators: Vec, + pub threshold: u8, +} + +impl ValidatorsAndThreshold { + /// Validates the validator set and threshold. + /// Returns an error if the set is empty, the threshold is zero, the threshold exceeds the + /// number of validators, or if the validator set has any duplicates. + pub fn validate(&self) -> Result<(), ProgramError> { + let validators_len = self.validators.len(); + + // Ensure the threshold is non-zero and doesn't exceed the number of validators. + if self.threshold == 0 || self.threshold as usize > validators_len { + return Err(Error::InvalidValidatorsAndThreshold.into()); + } + + // If the set has any duplicates, error. + let mut set = HashSet::with_capacity(validators_len); + for validator in &self.validators { + if !set.insert(validator) { + return Err(Error::InvalidValidatorsAndThreshold.into()); + } + } + + Ok(()) + } +} + +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, +) -> Result { + let (access_control_pda_key, _access_control_pda_bump) = + Pubkey::try_find_program_address(access_control_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + let ixn = Instruction::Initialize; + + // Accounts: + // 0. `[signer]` The new owner and payer of the access control PDA. + // 1. `[writable]` The access control PDA account. + // 2. `[executable]` The system program account. + let accounts = vec![ + AccountMeta::new(payer, true), + AccountMeta::new(access_control_pda_key, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + ]; + + let instruction = SolanaInstruction { + program_id, + data: ixn.encode()?, + accounts, + }; + + Ok(instruction) +} + +#[cfg(test)] +mod test { + use super::*; + + use hyperlane_core::H160; + + #[test] + fn test_validators_and_threshold_validate_success() { + let v = ValidatorsAndThreshold { + validators: vec![H160::zero(), H160::random()], + threshold: 1, + }; + assert!(v.validate().is_ok()); + + // Threshold equals validator set size + let v = ValidatorsAndThreshold { + validators: vec![H160::zero(), H160::random()], + threshold: 2, + }; + assert!(v.validate().is_ok()); + } + + #[test] + fn test_validators_and_threshold_validate_errors() { + // Threshold 0 and validators empty + let v = ValidatorsAndThreshold { + validators: vec![], + threshold: 0, + }; + assert_eq!( + v.validate().unwrap_err(), + Error::InvalidValidatorsAndThreshold.into() + ); + + // Threshold 0 and validators not empty + let v = ValidatorsAndThreshold { + validators: vec![H160::zero()], + threshold: 0, + }; + assert_eq!( + v.validate().unwrap_err(), + Error::InvalidValidatorsAndThreshold.into() + ); + + // Threshold exceeds validator set size + let v = ValidatorsAndThreshold { + validators: vec![H160::zero()], + threshold: 2, + }; + assert_eq!( + v.validate().unwrap_err(), + Error::InvalidValidatorsAndThreshold.into() + ); + + // Validator set has duplicates + let v = ValidatorsAndThreshold { + validators: vec![H160::zero(), H160::zero()], + threshold: 2, + }; + assert_eq!( + v.validate().unwrap_err(), + Error::InvalidValidatorsAndThreshold.into() + ); + } +} diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/src/lib.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/src/lib.rs new file mode 100644 index 0000000000..5675859320 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/src/lib.rs @@ -0,0 +1,12 @@ +//! A multisig Interchain Security Module that accepts signatures over +//! a checkpoint the message ID that matches the message being verified. +//! No merkle proofs. + +#![deny(warnings)] +#![deny(unsafe_code)] + +pub mod accounts; +pub mod error; +pub mod instruction; +pub mod metadata; +pub mod processor; diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/src/metadata.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/src/metadata.rs new file mode 100644 index 0000000000..423fae12c1 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/src/metadata.rs @@ -0,0 +1,139 @@ +use ecdsa_signature::EcdsaSignature; +use hyperlane_core::{Encode, H256}; + +use crate::error::Error; + +#[derive(Debug)] +pub struct MultisigIsmMessageIdMetadata { + pub origin_mailbox: H256, + pub merkle_root: H256, + pub validator_signatures: Vec, +} + +const ORIGIN_MAILBOX_OFFSET: usize = 0; +const MERKLE_ROOT_OFFSET: usize = 32; +const SIGNATURES_OFFSET: usize = 64; +const SIGNATURE_LENGTH: usize = 65; + +/// Format of metadata: +/// [ 0: 32] Origin mailbox address +/// [ 32: 64] Merkle root +/// [ 64:????] Validator signatures (length := threshold) +/// Note that the validator signatures being the length of the threshold is +/// not enforced here and should be enforced by the caller. +impl TryFrom> for MultisigIsmMessageIdMetadata { + type Error = Error; + + fn try_from(bytes: Vec) -> Result { + let bytes_len = bytes.len(); + // Require the bytes to be at least big enough to include a single signature. + if bytes_len < SIGNATURES_OFFSET + SIGNATURE_LENGTH { + return Err(Error::InvalidMetadata); + } + + let origin_mailbox = H256::from_slice(&bytes[ORIGIN_MAILBOX_OFFSET..MERKLE_ROOT_OFFSET]); + let merkle_root = H256::from_slice(&bytes[MERKLE_ROOT_OFFSET..SIGNATURES_OFFSET]); + + let signature_bytes_len = bytes_len - SIGNATURES_OFFSET; + // Require the signature bytes to be a multiple of the signature length. + // We don't need to check if signature_bytes_len is 0 because this is checked + // above. + if signature_bytes_len % SIGNATURE_LENGTH != 0 { + return Err(Error::InvalidMetadata); + } + let signature_count = signature_bytes_len / SIGNATURE_LENGTH; + let mut validator_signatures = Vec::with_capacity(signature_count); + for i in 0..signature_count { + let signature_offset = SIGNATURES_OFFSET + (i * SIGNATURE_LENGTH); + let signature = EcdsaSignature::from_bytes( + &bytes[signature_offset..signature_offset + SIGNATURE_LENGTH], + ) + .map_err(|_| Error::InvalidMetadata)?; + validator_signatures.push(signature); + } + + Ok(Self { + origin_mailbox, + merkle_root, + validator_signatures, + }) + } +} + +impl Encode for MultisigIsmMessageIdMetadata { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: std::io::Write, + { + let mut bytes_written = 0; + bytes_written += writer.write(self.origin_mailbox.as_ref())?; + bytes_written += writer.write(self.merkle_root.as_ref())?; + for signature in &self.validator_signatures { + bytes_written += writer.write(&signature.as_fixed_bytes()[..])?; + } + Ok(bytes_written) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_decode_correctly_formatted_metadata() { + let origin_mailbox = H256::random(); + let merkle_root = H256::random(); + let validator_signatures = vec![ + EcdsaSignature { + serialized_rs: [11u8; 64], + recovery_id: 0, + }, + EcdsaSignature { + serialized_rs: [12u8; 64], + recovery_id: 1, + }, + EcdsaSignature { + serialized_rs: [13u8; 64], + recovery_id: 0, + }, + ]; + let mut metadata_bytes = origin_mailbox.as_bytes().to_vec(); + metadata_bytes.extend_from_slice(merkle_root.as_bytes()); + for signature in &validator_signatures { + metadata_bytes.extend_from_slice(&signature.as_fixed_bytes()[..]); + } + + let metadata = MultisigIsmMessageIdMetadata::try_from(metadata_bytes).unwrap(); + assert_eq!(metadata.origin_mailbox, origin_mailbox); + assert_eq!(metadata.merkle_root, merkle_root); + assert_eq!(metadata.validator_signatures, validator_signatures); + } + + #[test] + fn test_decode_no_signatures_is_err() { + let origin_mailbox = H256::random(); + let merkle_root = H256::random(); + let metadata_bytes = origin_mailbox + .as_bytes() + .iter() + .chain(merkle_root.as_bytes().iter()) + .cloned() + .collect::>(); + + let result = MultisigIsmMessageIdMetadata::try_from(metadata_bytes); + assert!(result.unwrap_err() == Error::InvalidMetadata); + } + + #[test] + fn test_decode_incorrect_signature_length_is_err() { + let origin_mailbox = H256::random(); + let merkle_root = H256::random(); + let mut metadata_bytes = origin_mailbox.as_bytes().to_vec(); + metadata_bytes.extend_from_slice(merkle_root.as_bytes()); + // 64 byte signature instead of 65. + metadata_bytes.extend_from_slice(&[1u8; 64]); + + let result = MultisigIsmMessageIdMetadata::try_from(metadata_bytes); + assert!(result.unwrap_err() == Error::InvalidMetadata); + } +} diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/src/processor.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/src/processor.rs new file mode 100644 index 0000000000..3e360868c6 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/src/processor.rs @@ -0,0 +1,893 @@ +use hyperlane_core::{Checkpoint, CheckpointWithMessageId, Decode, HyperlaneMessage, ModuleType}; + +use access_control::AccessControl; +use account_utils::{create_pda_account, DiscriminatorDecode, SizedData}; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + instruction::AccountMeta, + program::set_return_data, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; + +use crate::{ + accounts::{AccessControlAccount, AccessControlData, DomainData, DomainDataAccount}, + error::Error, + instruction::{Domained, Instruction, ValidatorsAndThreshold}, + metadata::MultisigIsmMessageIdMetadata, +}; + +use hyperlane_sealevel_interchain_security_module_interface::InterchainSecurityModuleInstruction; +use multisig_ism::{interface::MultisigIsmInstruction, multisig::MultisigIsm}; + +use borsh::BorshSerialize; + +const ISM_TYPE: ModuleType = ModuleType::MessageIdMultisig; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// PDA seeds relating to the access control PDA account. +#[macro_export] +macro_rules! access_control_pda_seeds { + () => {{ + &[b"multisig_ism_message_id", b"-", b"access_control"] + }}; + + ($bump_seed:expr) => {{ + &[ + b"multisig_ism_message_id", + b"-", + b"access_control", + &[$bump_seed], + ] + }}; +} + +/// PDA seeds relating to a domain data PDA account. +/// A distinct account exists for each domain. +#[macro_export] +macro_rules! domain_data_pda_seeds { + ($domain:expr) => {{ + &[ + b"multisig_ism_message_id", + b"-", + &$domain.to_le_bytes(), + b"-", + b"domain_data", + ] + }}; + + ($domain:expr, $bump_seed:expr) => {{ + &[ + b"multisig_ism_message_id", + b"-", + &$domain.to_le_bytes(), + b"-", + b"domain_data", + &[$bump_seed], + ] + }}; +} + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // First, try to decode the instruction as an interchain security module + // interface supported function based off the discriminator. + if let Ok(ism_instruction) = InterchainSecurityModuleInstruction::decode(instruction_data) { + return match ism_instruction { + InterchainSecurityModuleInstruction::Type => { + set_return_data( + &SimulationReturnData::new(ISM_TYPE as u32) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + return Ok(()); + } + InterchainSecurityModuleInstruction::Verify(verify_data) => verify( + program_id, + accounts, + verify_data.metadata, + verify_data.message, + ), + InterchainSecurityModuleInstruction::VerifyAccountMetas(verify_data) => { + let account_metas = verify_account_metas( + program_id, + accounts, + verify_data.metadata, + verify_data.message, + )?; + // Wrap it in the SimulationReturnData because serialized account_metas + // may end with zero byte(s), which are incorrectly truncated as + // simulated transaction return data. + // See `SimulationReturnData` for details. + let bytes = SimulationReturnData::new(account_metas) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + set_return_data(&bytes[..]); + Ok(()) + } + }; + } + + // Next, try to decode the instruction as a multisig ISM instruction. + if let Ok(multisig_ism_instruction) = MultisigIsmInstruction::decode(instruction_data) { + return match multisig_ism_instruction { + // Gets the validators and threshold to verify the provided message. + // + // Accounts passed into this must be those returned by the + // ValidatorsAndThresholdAccountMetas instruction. + MultisigIsmInstruction::ValidatorsAndThreshold(message_bytes) => { + let message = HyperlaneMessage::read_from(&mut &message_bytes[..]) + .map_err(|_| ProgramError::InvalidArgument)?; + // No need to wrap in SimulationReturnData because the threshold + // should always be the last serialized byte and non-zero. + get_validators_and_threshold(program_id, accounts, message.origin) + } + MultisigIsmInstruction::ValidatorsAndThresholdAccountMetas(message_bytes) => { + let message = HyperlaneMessage::read_from(&mut &message_bytes[..]) + .map_err(|_| ProgramError::InvalidArgument)?; + let account_metas = get_validators_and_threshold_account_metas( + program_id, + accounts, + message.origin, + )?; + // Wrap it in the SimulationReturnData because serialized account_metas + // may end with zero byte(s), which are incorrectly truncated as + // simulated transaction return data. + // See `SimulationReturnData` for details. + let bytes = SimulationReturnData::new(account_metas) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + set_return_data(&bytes[..]); + Ok(()) + } + }; + } + + match Instruction::decode(instruction_data)? { + // Initializes the program. + Instruction::Initialize => initialize(program_id, accounts), + // Sets the validators and threshold for a given domain. + Instruction::SetValidatorsAndThreshold(config) => { + set_validators_and_threshold(program_id, accounts, config) + } + // Gets the owner of this program from the access control account. + Instruction::GetOwner => get_owner(program_id, accounts), + // Sets the owner of this program in the access control account. + Instruction::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + } +} + +/// Initializes the program, creating the access control PDA account. +/// +/// Accounts: +/// 0. `[signer]` The new owner and payer of the access control PDA. +/// 1. `[writable]` The access control PDA account. +/// 2. `[executable]` The system program account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: The new owner of this program and payer of the access control PDA. + let owner_account = next_account_info(accounts_iter)?; + if !owner_account.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 1: The access control PDA account. + let access_control_pda_account = next_account_info(accounts_iter)?; + let (access_control_pda_key, access_control_pda_bump_seed) = + Pubkey::find_program_address(access_control_pda_seeds!(), program_id); + if *access_control_pda_account.key != access_control_pda_key { + return Err(Error::AccountOutOfOrder.into()); + } + + // Ensure the access control PDA account isn't already initialized. + if let Ok(Some(_)) = + AccessControlAccount::fetch_data(&mut &access_control_pda_account.data.borrow()[..]) + { + return Err(Error::AlreadyInitialized.into()); + } + + // Account 2: The system program account. + let system_program_account = next_account_info(accounts_iter)?; + if !solana_program::system_program::check_id(system_program_account.key) { + return Err(Error::AccountOutOfOrder.into()); + } + + // Create the access control PDA account. + let access_control_account = AccessControlAccount::from(AccessControlData { + bump_seed: access_control_pda_bump_seed, + owner: Some(*owner_account.key), + }); + let access_control_account_data_size = access_control_account.size(); + create_pda_account( + owner_account, + &Rent::get()?, + access_control_account_data_size, + program_id, + system_program_account, + access_control_pda_account, + access_control_pda_seeds!(access_control_pda_bump_seed), + )?; + + // Store the access control data. + access_control_account.store(access_control_pda_account, false)?; + + Ok(()) +} + +/// Verifies a message has been signed by at least the configured threshold of the +/// configured validators for the message's origin domain. +/// +/// Accounts: +/// 0. `[]` The PDA relating to the message's origin domain. +fn verify( + program_id: &Pubkey, + accounts: &[AccountInfo], + metadata_bytes: Vec, + message_bytes: Vec, +) -> ProgramResult { + let metadata = MultisigIsmMessageIdMetadata::try_from(metadata_bytes)?; + let message = HyperlaneMessage::read_from(&mut &message_bytes[..]) + .map_err(|_| ProgramError::InvalidArgument)?; + + let validators_and_threshold = validators_and_threshold(program_id, accounts, message.origin)?; + + let multisig_ism = MultisigIsm::new( + CheckpointWithMessageId { + checkpoint: Checkpoint { + mailbox_address: metadata.origin_mailbox, + mailbox_domain: message.origin, + root: metadata.merkle_root, + index: message.nonce, + }, + message_id: message.id(), + }, + metadata.validator_signatures, + validators_and_threshold.validators, + validators_and_threshold.threshold, + ); + + multisig_ism + .verify() + .map_err(|err| Into::::into(err).into()) +} + +/// Gets the list of AccountMetas required by the `Verify` instruction. +/// +/// Accounts: +/// 0. `[]` This program's PDA relating to the seeds VERIFY_ACCOUNT_METAS_PDA_SEEDS. +/// Note this is not actually used / required in this implementation. +fn verify_account_metas( + program_id: &Pubkey, + _accounts: &[AccountInfo], + _metadata_bytes: Vec, + message_bytes: Vec, +) -> Result, ProgramError> { + let message = HyperlaneMessage::read_from(&mut &message_bytes[..]) + .map_err(|_| ProgramError::InvalidArgument)?; + let (domain_pda_key, _) = + Pubkey::find_program_address(domain_data_pda_seeds!(message.origin), program_id); + + Ok(vec![AccountMeta::new_readonly(domain_pda_key, false).into()]) +} + +/// Gets the validators and threshold for a given domain, and returns it as return data. +/// Intended to be used by instructions querying the validators and threshold. +/// +/// Accounts: +/// 0. `[]` The PDA relating to the provided domain. +fn get_validators_and_threshold( + program_id: &Pubkey, + accounts: &[AccountInfo], + domain: u32, +) -> ProgramResult { + let validators_and_threshold = validators_and_threshold(program_id, accounts, domain)?; + set_return_data( + &validators_and_threshold + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?, + ); + Ok(()) +} + +/// Returns a list of account metas that are required for a call to `get_validators_and_threshold`, +/// which is called by the MultisigIsmInstruction::ValidatorsAndThreshold instruction. +/// +/// Accounts: +/// 0. `[]` This program's PDA relating to the seeds VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS. +/// Note this is not actually used / required in this implementation. +fn get_validators_and_threshold_account_metas( + program_id: &Pubkey, + _accounts: &[AccountInfo], + domain: u32, +) -> Result, ProgramError> { + let (domain_pda_key, _) = + Pubkey::find_program_address(domain_data_pda_seeds!(domain), program_id); + + Ok(vec![AccountMeta::new_readonly(domain_pda_key, false).into()]) +} + +/// Gets the validators and threshold for a given domain. +/// +/// Accounts: +/// 0. `[]` The PDA relating to the provided domain. +fn validators_and_threshold( + program_id: &Pubkey, + accounts: &[AccountInfo], + domain: u32, +) -> Result { + let accounts_iter = &mut accounts.iter(); + + // Account 0: The PDA relating to the provided domain. + let domain_pda_account = next_account_info(accounts_iter)?; + if domain_pda_account.owner != program_id { + return Err(Error::ProgramIdNotOwner.into()); + } + + let domain_data = DomainDataAccount::fetch_data(&mut &domain_pda_account.data.borrow()[..])? + .ok_or(Error::AccountNotInitialized)?; + + let domain_pda_key = Pubkey::create_program_address( + domain_data_pda_seeds!(domain, domain_data.bump_seed), + program_id, + )?; + // This check validates that the provided domain_pda_account is valid + if *domain_pda_account.key != domain_pda_key { + return Err(Error::AccountOutOfOrder.into()); + } + + Ok(domain_data.validators_and_threshold) +} + +/// Set the validators and threshold for a given domain. +/// +/// Accounts: +/// 0. `[signer]` The access control owner and payer of the domain PDA. +/// 1. `[]` The access control PDA account. +/// 2. `[writable]` The PDA relating to the provided domain. +/// 3. `[executable]` OPTIONAL - The system program account. Required if creating the domain PDA. +fn set_validators_and_threshold( + program_id: &Pubkey, + accounts: &[AccountInfo], + config: Domained, +) -> ProgramResult { + // Validate the provided validators and threshold. + config.data.validate()?; + + let accounts_iter = &mut accounts.iter(); + + // Account 0: The owner of this program. + // This is verified as correct further below. + let owner_account = next_account_info(accounts_iter)?; + + // Account 1: The access control PDA account. + let access_control_pda_account = next_account_info(accounts_iter)?; + let access_control_data = access_control_data(program_id, access_control_pda_account)?; + // Ensure the owner account is the owner of this program. + access_control_data.ensure_owner_signer(owner_account)?; + + // Account 2: The PDA relating to the provided domain. + let domain_pda_account = next_account_info(accounts_iter)?; + + let domain_data = DomainDataAccount::fetch_data(&mut &domain_pda_account.data.borrow()[..]); + + let bump_seed = match domain_data { + Ok(Some(domain_data)) => { + // The PDA account exists already, we need to confirm the key of the domain_pda_account + // is the PDA with the stored bump seed. + let domain_pda_key = Pubkey::create_program_address( + domain_data_pda_seeds!(config.domain, domain_data.bump_seed), + program_id, + )?; + // This check validates that the provided domain_pda_account is valid + if *domain_pda_account.key != domain_pda_key { + return Err(Error::AccountOutOfOrder.into()); + } + // Extra sanity check that the owner of the PDA account is this program + if domain_pda_account.owner != program_id { + return Err(Error::ProgramIdNotOwner.into()); + } + + domain_data.bump_seed + } + Ok(None) | Err(_) => { + // Create the domain PDA account if it doesn't exist. + + // This is the initial size - because reallocations are allowed + // in the `store` call further below, it's possible that the + // size will be increased. + let domain_pda_size: usize = 1024; + + // First find the key and bump seed for the domain PDA, and ensure + // it matches the provided account. + let (domain_pda_key, domain_pda_bump) = + Pubkey::find_program_address(domain_data_pda_seeds!(config.domain), program_id); + if *domain_pda_account.key != domain_pda_key { + return Err(Error::AccountOutOfOrder.into()); + } + + // Account 3: The system program account. + let system_program_account = next_account_info(accounts_iter)?; + if !solana_program::system_program::check_id(system_program_account.key) { + return Err(Error::AccountOutOfOrder.into()); + } + + // Create the domain PDA account. + create_pda_account( + owner_account, + &Rent::get()?, + domain_pda_size, + program_id, + system_program_account, + domain_pda_account, + domain_data_pda_seeds!(config.domain, domain_pda_bump), + )?; + + domain_pda_bump + } + }; + + // Now store the new domain data according to the config: + DomainDataAccount::from(DomainData { + bump_seed, + validators_and_threshold: config.data, + }) + .store(domain_pda_account, true)?; + + Ok(()) +} + +/// Gets the owner of this program from the access control account, and returns it as return data. +/// Intended to be used by instructions querying the owner. +/// +/// Accounts: +/// 0. `[]` The access control PDA account. +fn get_owner(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: The access control PDA account. + let access_control_pda_account = next_account_info(accounts_iter)?; + + let access_control_data = access_control_data(program_id, access_control_pda_account)?; + + set_return_data( + &access_control_data + .owner + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?, + ); + Ok(()) +} + +/// Gets the access control data of this program. +/// Returns an Err if the provided account isn't the access control PDA. +fn access_control_data( + program_id: &Pubkey, + access_control_pda_account: &AccountInfo, +) -> Result { + let access_control_data = + AccessControlAccount::fetch_data(&mut &access_control_pda_account.data.borrow()[..])? + .ok_or(Error::AccountNotInitialized)?; + // Confirm the key of the access_control_pda_account is the correct PDA + // using the stored bump seed. + let access_control_pda_key = Pubkey::create_program_address( + access_control_pda_seeds!(access_control_data.bump_seed), + program_id, + )?; + // This check validates that the provided access_control_pda_account is valid + if *access_control_pda_account.key != access_control_pda_key { + return Err(Error::AccountOutOfOrder.into()); + } + // Extra sanity check that the owner of the PDA account is this program + if access_control_pda_account.owner != program_id { + return Err(Error::ProgramIdNotOwner.into()); + } + + Ok(*access_control_data) +} + +/// Transfers ownership to a new access control owner. +/// +/// Accounts: +/// 0. `[signer]` The current access control owner. +/// 1. `[]` The access control PDA account. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: The current access control owner. + // This is verified as correct further below. + let owner_account = next_account_info(accounts_iter)?; + + // Account 1: The access control PDA account. + let access_control_pda_account = next_account_info(accounts_iter)?; + let mut access_control_data = access_control_data(program_id, access_control_pda_account)?; + + // Transfer ownership. This errors if `owner_account` is not a signer or the owner. + access_control_data.transfer_ownership(owner_account, new_owner)?; + + // Store the new access control owner. + AccessControlAccount::from(access_control_data).store(access_control_pda_account, false)?; + + Ok(()) +} + +#[cfg(test)] +pub mod test { + use super::*; + + use account_utils::DiscriminatorEncode; + use ecdsa_signature::EcdsaSignature; + use hyperlane_core::{Encode, HyperlaneMessage, H160}; + use hyperlane_sealevel_interchain_security_module_interface::{ + InterchainSecurityModuleInstruction, VerifyInstruction, + }; + use multisig_ism::test_data::{get_multisig_ism_test_data, MultisigIsmTestData}; + use solana_program::stake_history::Epoch; + use std::str::FromStr; + + const ORIGIN_DOMAIN: u32 = 1234u32; + + fn id() -> Pubkey { + Pubkey::from_str("2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw").unwrap() + } + + #[test] + fn test_verify() { + let program_id = id(); + + let (domain_pda_key, domain_pda_bump_seed) = + Pubkey::find_program_address(domain_data_pda_seeds!(ORIGIN_DOMAIN), &program_id); + + let MultisigIsmTestData { + message, + checkpoint, + validators, + signatures, + } = get_multisig_ism_test_data(); + + let mut domain_account_lamports = 0; + let mut domain_account_data = vec![0_u8; 2048]; + let domain_pda_account = AccountInfo::new( + &domain_pda_key, + false, + true, + &mut domain_account_lamports, + &mut domain_account_data, + &program_id, + false, + Epoch::default(), + ); + let init_domain_data = DomainData { + bump_seed: domain_pda_bump_seed, + validators_and_threshold: ValidatorsAndThreshold { + validators, + threshold: 2, + }, + }; + DomainDataAccount::from(init_domain_data) + .store(&domain_pda_account, false) + .unwrap(); + + let message_bytes = message.to_vec(); + + // A quorum of signatures in the correct order. + // Expect no error. + let result = process_instruction( + &program_id, + &[domain_pda_account.clone()], + // Use the InterchainSecurityModuleInstruction enum to ensure the instruction + // is handled in compliance with what the Mailbox expects + InterchainSecurityModuleInstruction::Verify(VerifyInstruction { + metadata: MultisigIsmMessageIdMetadata { + origin_mailbox: checkpoint.mailbox_address, + merkle_root: checkpoint.root, + validator_signatures: vec![ + EcdsaSignature::from_bytes(&signatures[0]).unwrap(), + EcdsaSignature::from_bytes(&signatures[1]).unwrap(), + ], + } + .to_vec(), + message: message_bytes.clone(), + }) + .encode() + .unwrap() + .as_slice(), + ); + assert!(result.is_ok()); + + // A quorum of signatures NOT in the correct order. + // Expect an error. + let result = process_instruction( + &program_id, + &[domain_pda_account.clone()], + // Use the InterchainSecurityModuleInstruction enum to ensure the instruction + // is handled in compliance with what the Mailbox expects + InterchainSecurityModuleInstruction::Verify(VerifyInstruction { + metadata: MultisigIsmMessageIdMetadata { + origin_mailbox: checkpoint.mailbox_address, + merkle_root: checkpoint.root, + validator_signatures: vec![ + EcdsaSignature::from_bytes(&signatures[1]).unwrap(), + EcdsaSignature::from_bytes(&signatures[0]).unwrap(), + ], + } + .to_vec(), + message: message_bytes.clone(), + }) + .encode() + .unwrap() + .as_slice(), + ); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::ThresholdNotMet.into()); + + // A quorum valid signatures. Includes one invalid signature. + // Expect no error. + let result = process_instruction( + &program_id, + &[ + domain_pda_account.clone(), + ], + // Use the InterchainSecurityModuleInstruction enum to ensure the instruction + // is handled in compliance with what the Mailbox expects + InterchainSecurityModuleInstruction::Verify(VerifyInstruction { + metadata: MultisigIsmMessageIdMetadata { + origin_mailbox: checkpoint.mailbox_address, + merkle_root: checkpoint.root, + validator_signatures: vec![ + EcdsaSignature::from_bytes(&signatures[0]).unwrap(), + EcdsaSignature::from_bytes(&signatures[2]).unwrap(), + // Signature from a non-validator: + // Address: 0xB92752D900573BC114D18e023D81312bBC32e266 + // Private Key: 0x2e09250a71f712e5f834285cc60f1d62578360c65a0f4836daa0a5caa27199cf + EcdsaSignature::from_bytes(&hex::decode("c75dca903d963f30f169ba99c2554572108474c097bd40c2a29fbcf4739fdb564e795fce8e0ae3b860dfd4e0b3f93420ccb6454e87fa3235c8754a5437a78f781b").unwrap()).unwrap(), + ], + }.to_vec(), + message: message_bytes, + }).encode().unwrap().as_slice(), + ); + assert!(result.is_ok()); + + // A quorum of signatures, but the message has a different nonce & therefore ID + let result = process_instruction( + &program_id, + &[domain_pda_account.clone()], + // Use the InterchainSecurityModuleInstruction enum to ensure the instruction + // is handled in compliance with what the Mailbox expects + InterchainSecurityModuleInstruction::Verify(VerifyInstruction { + metadata: MultisigIsmMessageIdMetadata { + origin_mailbox: checkpoint.mailbox_address, + merkle_root: checkpoint.root, + validator_signatures: vec![ + EcdsaSignature::from_bytes(&signatures[0]).unwrap(), + EcdsaSignature::from_bytes(&signatures[1]).unwrap(), + ], + } + .to_vec(), + message: HyperlaneMessage { + nonce: 420, + ..message + } + .to_vec(), + }) + .encode() + .unwrap() + .as_slice(), + ); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::ThresholdNotMet.into()); + } + + #[test] + fn test_transfer_ownership() { + let program_id = id(); + + let owner_key = Pubkey::new_unique(); + let mut owner_account_lamports = 0; + let mut owner_account_data = vec![]; + let system_program_id = solana_program::system_program::id(); + let owner_account = AccountInfo::new( + &owner_key, + true, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &system_program_id, + false, + Epoch::default(), + ); + + let (access_control_pda_key, access_control_pda_bump_seed) = + Pubkey::find_program_address(access_control_pda_seeds!(), &program_id); + + let mut access_control_account_lamports = 0; + let mut access_control_account_data = vec![0u8; 1024]; + let access_control_pda_account = AccountInfo::new( + &access_control_pda_key, + false, + true, + &mut access_control_account_lamports, + &mut access_control_account_data, + &program_id, + false, + Epoch::default(), + ); + let init_access_control_data = AccessControlData { + bump_seed: access_control_pda_bump_seed, + owner: Some(owner_key), + }; + AccessControlAccount::from(init_access_control_data) + .store(&access_control_pda_account, false) + .unwrap(); + + let new_owner_key = Pubkey::new_unique(); + + let mut accounts = vec![owner_account, access_control_pda_account]; + + // First, we test that the owner must sign. + + // Temporarily set the owner account as a non-signer + accounts[0].is_signer = false; + let result = process_instruction( + &program_id, + &accounts, + Instruction::TransferOwnership(Some(new_owner_key)) + .encode() + .unwrap() + .as_slice(), + ); + assert_eq!(result, Err(ProgramError::MissingRequiredSignature)); + // Set is_signer back to true + accounts[0].is_signer = true; + + // Now successfully set ownership to new_owner_key + process_instruction( + &program_id, + &accounts, + Instruction::TransferOwnership(Some(new_owner_key)) + .encode() + .unwrap() + .as_slice(), + ) + .unwrap(); + + let access_control_data = + AccessControlAccount::fetch_data(&mut &accounts[1].data.borrow()[..]) + .unwrap() + .unwrap(); + assert_eq!( + access_control_data, + Box::new(AccessControlData { + bump_seed: access_control_pda_bump_seed, + owner: Some(new_owner_key), + }) + ); + + // And now let's try to set the owner again, but with the old owner signing. + let result = process_instruction( + &program_id, + &accounts, + Instruction::TransferOwnership(Some(new_owner_key)) + .encode() + .unwrap() + .as_slice(), + ); + assert_eq!(result, Err(ProgramError::InvalidArgument)); + } + + // Only tests the case where a domain data PDA account has already been created. + // For testing a case where it must be created, see the functional tests. + #[test] + fn test_set_validators_and_threshold() { + let program_id = id(); + + let domain = 1234u32; + + let (domain_pda_key, domain_pda_bump_seed) = + Pubkey::find_program_address(domain_data_pda_seeds!(domain), &program_id); + + let mut domain_account_lamports = 0; + let mut domain_account_data = vec![0_u8; 2048]; + let domain_pda_account = AccountInfo::new( + &domain_pda_key, + false, + true, + &mut domain_account_lamports, + &mut domain_account_data, + &program_id, + false, + Epoch::default(), + ); + let init_domain_data = DomainData { + bump_seed: domain_pda_bump_seed, + validators_and_threshold: ValidatorsAndThreshold { + validators: vec![H160::random()], + threshold: 1, + }, + }; + DomainDataAccount::from(init_domain_data) + .store(&domain_pda_account, false) + .unwrap(); + + let owner_key = Pubkey::new_unique(); + let mut owner_account_lamports = 0; + let mut owner_account_data = vec![]; + let system_program_id = solana_program::system_program::id(); + let owner_account = AccountInfo::new( + &owner_key, + true, + false, + &mut owner_account_lamports, + &mut owner_account_data, + &system_program_id, + false, + Epoch::default(), + ); + + let (access_control_pda_key, access_control_pda_bump_seed) = + Pubkey::find_program_address(access_control_pda_seeds!(), &program_id); + + let mut access_control_account_lamports = 0; + let mut access_control_account_data = vec![0u8; 1024]; + let access_control_pda_account = AccountInfo::new( + &access_control_pda_key, + false, + true, + &mut access_control_account_lamports, + &mut access_control_account_data, + &program_id, + false, + Epoch::default(), + ); + let init_access_control_data = AccessControlData { + bump_seed: access_control_pda_bump_seed, + owner: Some(owner_key), + }; + AccessControlAccount::from(init_access_control_data) + .store(&access_control_pda_account, false) + .unwrap(); + + let config = Domained { + domain, + data: ValidatorsAndThreshold { + validators: vec![H160::random(), H160::random()], + threshold: 2, + }, + }; + + let accounts = vec![ + owner_account, + access_control_pda_account, + domain_pda_account, + ]; + + set_validators_and_threshold(&program_id, &accounts, config.clone()).unwrap(); + + let domain_data = + DomainDataAccount::fetch_data(&mut &accounts[2].try_borrow_data().unwrap()[..]) + .unwrap() + .unwrap(); + assert_eq!( + domain_data, + Box::new(DomainData { + bump_seed: domain_pda_bump_seed, + validators_and_threshold: config.data, + }) + ); + } +} diff --git a/rust/sealevel/programs/ism/multisig-ism-message-id/tests/functional.rs b/rust/sealevel/programs/ism/multisig-ism-message-id/tests/functional.rs new file mode 100644 index 0000000000..f062e3e392 --- /dev/null +++ b/rust/sealevel/programs/ism/multisig-ism-message-id/tests/functional.rs @@ -0,0 +1,526 @@ +//! Contains functional tests for things that cannot be done +//! strictly in unit tests. This includes CPIs, like creating +//! new PDA accounts. + +use account_utils::DiscriminatorEncode; +use borsh::BorshDeserialize; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey, + pubkey::Pubkey, +}; + +use ecdsa_signature::EcdsaSignature; +use hyperlane_core::{Encode, HyperlaneMessage, ModuleType, H160, H256}; +use hyperlane_sealevel_interchain_security_module_interface::{ + InterchainSecurityModuleInstruction, VerifyInstruction, VERIFY_ACCOUNT_METAS_PDA_SEEDS, +}; +use hyperlane_sealevel_multisig_ism_message_id::{ + access_control_pda_seeds, + accounts::{AccessControlAccount, AccessControlData, DomainData, DomainDataAccount}, + domain_data_pda_seeds, + error::Error as MultisigIsmError, + instruction::{Domained, Instruction as MultisigIsmProgramInstruction, ValidatorsAndThreshold}, + metadata::MultisigIsmMessageIdMetadata, + processor::process_instruction, +}; +use multisig_ism::interface::{ + MultisigIsmInstruction, VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS, +}; +#[cfg(test)] +use multisig_ism::test_data::{get_multisig_ism_test_data, MultisigIsmTestData}; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_program_test::*; +use solana_sdk::{ + hash::Hash, + instruction::InstructionError, + message::Message, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; + +pub fn multisig_ism_message_id_id() -> Pubkey { + pubkey!("2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw") +} + +async fn new_funded_keypair( + banks_client: &mut BanksClient, + payer: &Keypair, + lamports: u64, +) -> Keypair { + let keypair = Keypair::new(); + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[solana_sdk::system_instruction::transfer( + &payer.pubkey(), + &keypair.pubkey(), + lamports, + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + keypair +} + +async fn initialize( + program_id: Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: Hash, +) -> Result<(Pubkey, u8), BanksClientError> { + let (access_control_pda_key, _access_control_pda_bump_seed) = + Pubkey::find_program_address(access_control_pda_seeds!(), &program_id); + + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MultisigIsmProgramInstruction::Initialize.encode().unwrap(), + vec![ + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new(access_control_pda_key, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok((access_control_pda_key, _access_control_pda_bump_seed)) +} + +async fn set_validators_and_threshold( + program_id: Pubkey, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: Hash, + access_control_pda_key: Pubkey, + domain: u32, + validators_and_threshold: ValidatorsAndThreshold, +) -> Result<(Pubkey, u8), BanksClientError> { + let (domain_data_pda_key, domain_data_pda_bump_seed) = + Pubkey::find_program_address(domain_data_pda_seeds!(domain), &program_id); + + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MultisigIsmProgramInstruction::SetValidatorsAndThreshold(Domained { + domain, + data: validators_and_threshold.clone(), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(access_control_pda_key, false), + AccountMeta::new(domain_data_pda_key, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + ], + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + Ok((domain_data_pda_key, domain_data_pda_bump_seed)) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = multisig_ism_message_id_id(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_ism_multisig_ism", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + let (access_control_pda_key, access_control_pda_bump_seed) = + initialize(program_id, &mut banks_client, &payer, recent_blockhash) + .await + .unwrap(); + + let access_control_account_data = banks_client + .get_account(access_control_pda_key) + .await + .unwrap() + .unwrap() + .data; + let access_control = AccessControlAccount::fetch_data(&mut &access_control_account_data[..]) + .unwrap() + .unwrap(); + assert_eq!( + access_control, + Box::new(AccessControlData { + bump_seed: access_control_pda_bump_seed, + owner: Some(payer.pubkey()), + }), + ); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = multisig_ism_message_id_id(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_ism_multisig_ism", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + initialize(program_id, &mut banks_client, &payer, recent_blockhash) + .await + .unwrap(); + + // Create a new payer as a hack to get a new tx ID, because the + // instruction data is the same and the recent blockhash is the same + let new_payer = new_funded_keypair(&mut banks_client, &payer, 1000000).await; + let result = initialize(program_id, &mut banks_client, &new_payer, recent_blockhash).await; + + // BanksClientError doesn't implement Eq, but TransactionError does + if let BanksClientError::TransactionError(tx_err) = result.err().unwrap() { + assert_eq!( + tx_err, + TransactionError::InstructionError( + 0, + InstructionError::Custom(MultisigIsmError::AlreadyInitialized as u32) + ) + ); + } else { + panic!("expected TransactionError"); + } +} + +#[tokio::test] +async fn test_set_validators_and_threshold_creates_pda_account() { + let program_id = multisig_ism_message_id_id(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_ism_multisig_ism", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + let (access_control_pda_key, _) = + initialize(program_id, &mut banks_client, &payer, recent_blockhash) + .await + .unwrap(); + + let domain: u32 = 1234; + + let validators_and_threshold = ValidatorsAndThreshold { + validators: vec![H160::random(), H160::random(), H160::random()], + threshold: 2, + }; + + let (domain_data_pda_key, domain_data_pda_bump_seed) = set_validators_and_threshold( + program_id, + &mut banks_client, + &payer, + recent_blockhash, + access_control_pda_key, + domain, + validators_and_threshold.clone(), + ) + .await + .unwrap(); + + let domain_data_account_data = banks_client + .get_account(domain_data_pda_key) + .await + .unwrap() + .unwrap() + .data; + let domain_data = DomainDataAccount::fetch_data(&mut &domain_data_account_data[..]) + .unwrap() + .unwrap(); + assert_eq!( + domain_data, + Box::new(DomainData { + bump_seed: domain_data_pda_bump_seed, + validators_and_threshold, + }), + ); + + // And now for good measure, try to set the validators and threshold again after the domain data + // PDA has been created. By not passing in the system program, we can be sure that + // the create_account path certainly doesn't get hit + + // Change it up + let validators_and_threshold = ValidatorsAndThreshold { + validators: vec![H160::random(), H160::random(), H160::random()], + threshold: 1, + }; + + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &MultisigIsmProgramInstruction::SetValidatorsAndThreshold(Domained { + domain, + data: validators_and_threshold.clone(), + }) + .encode() + .unwrap(), + vec![ + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(access_control_pda_key, false), + AccountMeta::new(domain_data_pda_key, false), + ], + )], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + let domain_data_account_data = banks_client + .get_account(domain_data_pda_key) + .await + .unwrap() + .unwrap() + .data; + let domain_data = DomainDataAccount::fetch_data(&mut &domain_data_account_data[..]) + .unwrap() + .unwrap(); + assert_eq!( + domain_data, + Box::new(DomainData { + bump_seed: domain_data_pda_bump_seed, + validators_and_threshold: validators_and_threshold.clone(), + }), + ); + + // For good measure, let's also use the MultisigIsmInstruction::ValidatorsAndThreshold + // instruction, and also use the MultisigIsmInstruction::ValidatorsAndThresholdAccountMetas + // to fetch the account metas required for the instruction. + + let test_message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: domain, + sender: H256::random(), + destination: domain + 1, + recipient: H256::random(), + body: vec![1, 2, 3, 4, 5], + }; + + // First, call MultisigIsmInstruction::ValidatorsAndThresholdAccountMetas to get the metas + // for our future call to MultisigIsmInstruction::ValidatorsAndThreshold + let (account_metas_pda_key, _) = Pubkey::find_program_address( + VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS, + &program_id, + ); + let account_metas_return_data = banks_client + .simulate_transaction(Transaction::new_unsigned(Message::new_with_blockhash( + &[Instruction::new_with_bytes( + program_id, + &MultisigIsmInstruction::ValidatorsAndThresholdAccountMetas(test_message.to_vec()) + .encode() + .unwrap(), + vec![AccountMeta::new(account_metas_pda_key, false)], + )], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await + .unwrap() + .simulation_details + .unwrap() + .return_data + .unwrap() + .data; + + let account_metas: Vec = + SimulationReturnData::>::try_from_slice( + account_metas_return_data.as_slice(), + ) + .unwrap() + .return_data; + let account_metas: Vec = account_metas + .into_iter() + .map(|serializable_account_meta| serializable_account_meta.into()) + .collect(); + + // Now let it rip with MultisigIsmInstruction::ValidatorsAndThreshold + let validators_and_threshold_bytes = banks_client + .simulate_transaction(Transaction::new_unsigned(Message::new_with_blockhash( + &[Instruction::new_with_bytes( + program_id, + &MultisigIsmInstruction::ValidatorsAndThreshold(test_message.to_vec()) + .encode() + .unwrap(), + account_metas, + )], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await + .unwrap() + .simulation_details + .unwrap() + .return_data + .unwrap() + .data; + assert_eq!( + ValidatorsAndThreshold::try_from_slice(validators_and_threshold_bytes.as_slice()).unwrap(), + validators_and_threshold + ); +} + +#[tokio::test] +async fn test_ism_verify() { + let program_id = multisig_ism_message_id_id(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_ism_multisig_ism", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + let (access_control_pda_key, _) = + initialize(program_id, &mut banks_client, &payer, recent_blockhash) + .await + .unwrap(); + + let MultisigIsmTestData { + message, + checkpoint, + validators, + signatures, + } = get_multisig_ism_test_data(); + + let origin_domain = message.origin; + let validators_and_threshold = ValidatorsAndThreshold { + validators: validators.clone(), + threshold: 2, + }; + + set_validators_and_threshold( + program_id, + &mut banks_client, + &payer, + recent_blockhash, + access_control_pda_key, + origin_domain, + validators_and_threshold.clone(), + ) + .await + .unwrap(); + + // A valid verify instruction with a quorum + let verify_instruction = VerifyInstruction { + metadata: MultisigIsmMessageIdMetadata { + origin_mailbox: checkpoint.mailbox_address, + merkle_root: checkpoint.root, + validator_signatures: vec![ + EcdsaSignature::from_bytes(&signatures[0]).unwrap(), + EcdsaSignature::from_bytes(&signatures[1]).unwrap(), + ], + } + .to_vec(), + message: message.to_vec(), + }; + + // First get the account metas needed + let (account_metas_pda_key, _) = + Pubkey::find_program_address(VERIFY_ACCOUNT_METAS_PDA_SEEDS, &program_id); + let account_metas_return_data = banks_client + .simulate_transaction(Transaction::new_unsigned(Message::new_with_blockhash( + &[Instruction::new_with_bytes( + program_id, + &InterchainSecurityModuleInstruction::VerifyAccountMetas( + verify_instruction.clone(), + ) + .encode() + .unwrap(), + vec![AccountMeta::new(account_metas_pda_key, false)], + )], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await + .unwrap() + .simulation_details + .unwrap() + .return_data + .unwrap() + .data; + let account_metas: Vec = + SimulationReturnData::>::try_from_slice( + account_metas_return_data.as_slice(), + ) + .unwrap() + .return_data; + let account_metas: Vec = account_metas + .into_iter() + .map(|serializable_account_meta| serializable_account_meta.into()) + .collect(); + + // Now let it rip with MultisigIsmInstruction::ValidatorsAndThreshold + let verify_simulation_logs = banks_client + .simulate_transaction(Transaction::new_unsigned(Message::new_with_blockhash( + &[Instruction::new_with_bytes( + program_id, + &InterchainSecurityModuleInstruction::Verify(verify_instruction) + .encode() + .unwrap(), + account_metas, + )], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await + .unwrap() + .simulation_details + .unwrap() + .logs; + // The only real indication of success in the interface we're given is the final log + // indicating success + assert_eq!( + verify_simulation_logs[verify_simulation_logs.len() - 1], + format!("Program {} success", program_id), + ); +} + +#[tokio::test] +async fn test_ism_type() { + let program_id = multisig_ism_message_id_id(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_ism_multisig_ism", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + let type_bytes = banks_client + .simulate_transaction(Transaction::new_unsigned(Message::new_with_blockhash( + &[Instruction::new_with_bytes( + program_id, + &InterchainSecurityModuleInstruction::Type.encode().unwrap(), + vec![], + )], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await + .unwrap() + .simulation_details + .unwrap() + .return_data + .unwrap() + .data; + let type_u32 = SimulationReturnData::::try_from_slice(type_bytes.as_slice()) + .unwrap() + .return_data; + assert_eq!(type_u32, ModuleType::MessageIdMultisig as u32); +} diff --git a/rust/sealevel/programs/ism/test-ism/Cargo.toml b/rust/sealevel/programs/ism/test-ism/Cargo.toml new file mode 100644 index 0000000000..d3ad8e19a4 --- /dev/null +++ b/rust/sealevel/programs/ism/test-ism/Cargo.toml @@ -0,0 +1,26 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-test-ism" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-client = ["dep:solana-program-test", "dep:solana-sdk", "dep:hyperlane-test-transaction-utils"] + +[dependencies] +borsh.workspace = true +solana-program.workspace = true +solana-program-test = { workspace = true, optional = true } +solana-sdk = { workspace = true, optional = true } + +account-utils = { path = "../../../libraries/account-utils" } +hyperlane-core = { path = "../../../../hyperlane-core" } +hyperlane-sealevel-interchain-security-module-interface = { path = "../../../libraries/interchain-security-module-interface" } +hyperlane-sealevel-mailbox = { path = "../../mailbox", features = ["no-entrypoint"] } +serializable-account-meta = { path = "../../../libraries/serializable-account-meta" } +hyperlane-test-transaction-utils = { path = "../../../libraries/test-transaction-utils", optional = true } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/ism/test-ism/src/lib.rs b/rust/sealevel/programs/ism/test-ism/src/lib.rs new file mode 100644 index 0000000000..4ec815e374 --- /dev/null +++ b/rust/sealevel/programs/ism/test-ism/src/lib.rs @@ -0,0 +1,12 @@ +//! Interchain Security Module that unconditionally approves. +//! **NOT INTENDED FOR USE IN PRODUCTION** + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod program; +#[cfg(feature = "test-client")] +pub mod test_client; + +solana_program::declare_id!("CWVYdRomCv3bksSsRTuds9SRR5y17Ft5nPqhaXjp4tnb"); diff --git a/rust/sealevel/programs/ism/test-ism/src/program.rs b/rust/sealevel/programs/ism/test-ism/src/program.rs new file mode 100644 index 0000000000..4b4c87638e --- /dev/null +++ b/rust/sealevel/programs/ism/test-ism/src/program.rs @@ -0,0 +1,195 @@ +//! Interchain Security Module used for testing. + +use account_utils::{create_pda_account, AccountData, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_interchain_security_module_interface::InterchainSecurityModuleInstruction; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + instruction::AccountMeta, + program::set_return_data, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_program, + sysvar::Sysvar, +}; + +use hyperlane_core::ModuleType; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +const ISM_TYPE: ModuleType = ModuleType::Unused; + +/// Custom errors for the program. +pub enum TestIsmError { + /// The verify instruction was not accepted. + VerifyNotAccepted = 69420, +} + +/// The PDA seeds relating to storage +#[macro_export] +macro_rules! test_ism_storage_pda_seeds { + () => {{ + &[b"test_ism", b"-", b"storage"] + }}; + + ($bump_seed:expr) => {{ + &[b"test_ism", b"-", b"storage", &[$bump_seed]] + }}; +} + +/// The storage account. +pub type TestIsmStorageAccount = AccountData; + +/// The storage account's data. +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Default)] +pub struct TestIsmStorage { + /// Whether messages should be accepted / verified. + pub accept: bool, +} + +impl SizedData for TestIsmStorage { + fn size(&self) -> usize { + // 1 byte bool + 1 + } +} + +/// Instructions for the program. +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug)] +pub enum TestIsmInstruction { + /// Initializes the program. + Init, + /// Sets whether messages should be accepted / verified. + SetAccept(bool), +} + +/// Processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if let Ok(ism_instruction) = InterchainSecurityModuleInstruction::decode(instruction_data) { + return match ism_instruction { + InterchainSecurityModuleInstruction::Verify(_) => verify(program_id, accounts), + InterchainSecurityModuleInstruction::VerifyAccountMetas(_) => { + verify_account_metas(program_id, accounts) + } + InterchainSecurityModuleInstruction::Type => { + set_return_data( + &SimulationReturnData::new(ISM_TYPE as u32) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + Ok(()) + } + }; + } + + let instruction = TestIsmInstruction::try_from_slice(instruction_data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + match instruction { + TestIsmInstruction::Init => init(program_id, accounts), + TestIsmInstruction::SetAccept(accept) => set_accept(program_id, accounts, accept), + } +} + +/// Creates the storage PDA. +/// +/// Accounts: +/// 0. [executable] System program. +/// 1. [signer] Payer. +/// 2. [writeable] Storage PDA. +fn init(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: System program. + let system_program_info = next_account_info(accounts_iter)?; + if system_program_info.key != &system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Payer. + let payer_info = next_account_info(accounts_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 2: Storage PDA. + let storage_info = next_account_info(accounts_iter)?; + let (storage_pda_key, storage_pda_bump_seed) = + Pubkey::find_program_address(test_ism_storage_pda_seeds!(), program_id); + if storage_info.key != &storage_pda_key { + return Err(ProgramError::InvalidArgument); + } + + let storage_account = TestIsmStorageAccount::from(TestIsmStorage { accept: true }); + create_pda_account( + payer_info, + &Rent::get()?, + storage_account.size(), + program_id, + system_program_info, + storage_info, + test_ism_storage_pda_seeds!(storage_pda_bump_seed), + )?; + // Store it + storage_account.store(storage_info, false)?; + + Ok(()) +} + +/// Accounts: +/// 0. [writeable] Storage PDA. +fn set_accept(_program_id: &Pubkey, accounts: &[AccountInfo], accept: bool) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Storage PDA. + // Not bothering to check for validity because this is a test program + let storage_info = next_account_info(accounts_iter)?; + let mut storage = + TestIsmStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + storage.accept = accept; + TestIsmStorageAccount::from(storage).store(storage_info, false)?; + + Ok(()) +} + +/// Accounts: +/// 0. [] Storage PDA. +fn verify(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Storage PDA. + let storage_info = next_account_info(accounts_iter)?; + let storage = TestIsmStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + if !storage.accept { + return Err(ProgramError::Custom(TestIsmError::VerifyNotAccepted as u32)); + } + + Ok(()) +} + +fn verify_account_metas(program_id: &Pubkey, _accounts: &[AccountInfo]) -> ProgramResult { + let (storage_pda_key, _storage_pda_bump) = + Pubkey::find_program_address(test_ism_storage_pda_seeds!(), program_id); + + let account_metas: Vec = + vec![AccountMeta::new_readonly(storage_pda_key, false).into()]; + + // Wrap it in the SimulationReturnData because serialized account_metas + // may end with zero byte(s), which are incorrectly truncated as + // simulated transaction return data. + // See `SimulationReturnData` for details. + let bytes = SimulationReturnData::new(account_metas) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + set_return_data(&bytes[..]); + + Ok(()) +} diff --git a/rust/sealevel/programs/ism/test-ism/src/test_client.rs b/rust/sealevel/programs/ism/test-ism/src/test_client.rs new file mode 100644 index 0000000000..f2bf0f6af5 --- /dev/null +++ b/rust/sealevel/programs/ism/test-ism/src/test_client.rs @@ -0,0 +1,96 @@ +//! Test client for the Test ISM program. + +use borsh::BorshSerialize; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; +use solana_program_test::*; +use solana_sdk::{signature::Signer, signer::keypair::Keypair}; + +use hyperlane_test_transaction_utils::process_instruction; + +use crate::{id, program::TestIsmInstruction, test_ism_storage_pda_seeds}; + +/// Test client for the Test ISM program. +pub struct TestIsmTestClient { + banks_client: BanksClient, + payer: Keypair, +} + +impl TestIsmTestClient { + /// Creates a new `TestIsmTestClient`. + pub fn new(banks_client: BanksClient, payer: Keypair) -> Self { + Self { + banks_client, + payer, + } + } + + /// Initializes the Test ISM program. + pub async fn init(&mut self) -> Result<(), BanksClientError> { + let program_id = id(); + + let payer_pubkey = self.payer.pubkey(); + + let instruction = Instruction { + program_id, + data: TestIsmInstruction::Init.try_to_vec().unwrap(), + accounts: vec![ + // 0. [executable] System program. + // 1. [signer] Payer. + // 2. [writeable] Storage PDA. + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(payer_pubkey, true), + AccountMeta::new(Self::get_storage_pda_key(), false), + ], + }; + + process_instruction( + &mut self.banks_client, + instruction, + &self.payer, + &[&self.payer], + ) + .await?; + + Ok(()) + } + + /// Sets the Test ISM to accept or reject. + pub async fn set_accept(&mut self, accept: bool) -> Result<(), BanksClientError> { + let program_id = id(); + + let instruction = Instruction { + program_id, + data: TestIsmInstruction::SetAccept(accept).try_to_vec().unwrap(), + accounts: vec![ + // 0. [writeable] Storage PDA. + AccountMeta::new(Self::get_storage_pda_key(), false), + ], + }; + + process_instruction( + &mut self.banks_client, + instruction, + &self.payer, + &[&self.payer], + ) + .await?; + + Ok(()) + } + + fn get_storage_pda_key() -> Pubkey { + let program_id = id(); + let (storage_pda_key, _storage_pda_bump) = + Pubkey::find_program_address(test_ism_storage_pda_seeds!(), &program_id); + storage_pda_key + } + + /// Gets the program ID. + pub fn id(&self) -> Pubkey { + id() + } +} diff --git a/rust/sealevel/programs/mailbox-test/Cargo.toml b/rust/sealevel/programs/mailbox-test/Cargo.toml new file mode 100644 index 0000000000..a6fabc330c --- /dev/null +++ b/rust/sealevel/programs/mailbox-test/Cargo.toml @@ -0,0 +1,36 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-mailbox-test" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dev-dependencies] +base64.workspace = true +borsh.workspace = true +itertools.workspace = true +log.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program-test.workspace = true +solana-program.workspace = true +solana-sdk.workspace = true +spl-noop.workspace = true +thiserror.workspace = true + +access-control = { path = "../../libraries/access-control" } +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-interchain-security-module-interface = { path = "../../libraries/interchain-security-module-interface" } +hyperlane-sealevel-mailbox = { path = "../mailbox" } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-sealevel-test-ism = { path = "../ism/test-ism", features = ["test-client"] } +hyperlane-sealevel-test-send-receiver = { path = "../test-send-receiver", features = ["test-client"] } +hyperlane-test-utils = { path = "../../libraries/test-utils" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/mailbox-test/README.md b/rust/sealevel/programs/mailbox-test/README.md new file mode 100644 index 0000000000..a5ce61d594 --- /dev/null +++ b/rust/sealevel/programs/mailbox-test/README.md @@ -0,0 +1,7 @@ +# mailbox-test + +These are functional tests that ordinarily would live in `programs/mailbox/tests`, however +due to some funky dependency resolution, the building the SBF .so files included some dev dependencies +that would result in errors when trying to deploy the programs. + +As a (slightly hacky) fix, these tests are pulled into an entirely separate crate. diff --git a/rust/sealevel/programs/mailbox-test/src/functional.rs b/rust/sealevel/programs/mailbox-test/src/functional.rs new file mode 100644 index 0000000000..3f46d6cfa8 --- /dev/null +++ b/rust/sealevel/programs/mailbox-test/src/functional.rs @@ -0,0 +1,1054 @@ +use borsh::BorshDeserialize; +use hyperlane_core::{ + accumulator::incremental::IncrementalMerkle as MerkleTree, HyperlaneMessage, H256, +}; + +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::InstructionError, + message::Message, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; + +use hyperlane_sealevel_mailbox::{ + accounts::{Inbox, InboxAccount, Outbox}, + error::Error as MailboxError, + instruction::{Instruction as MailboxInstruction, OutboxDispatch}, + mailbox_dispatched_message_pda_seeds, +}; + +use hyperlane_sealevel_test_ism::{program::TestIsmError, test_client::TestIsmTestClient}; +use hyperlane_sealevel_test_send_receiver::{ + program::{HandleMode, IsmReturnDataMode, TestSendReceiverError}, + test_client::TestSendReceiverTestClient, +}; + +use hyperlane_test_utils::{ + assert_transaction_error, clone_keypair, get_process_account_metas, get_recipient_ism, + initialize_mailbox, mailbox_id, new_funded_keypair, process, process_instruction, + process_with_accounts, +}; + +use crate::utils::{ + assert_dispatched_message, assert_inbox, assert_message_not_processed, assert_outbox, + assert_processed_message, dispatch_from_payer, +}; + +const LOCAL_DOMAIN: u32 = 13775; +const REMOTE_DOMAIN: u32 = 69420; + +async fn setup_client() -> ( + BanksClient, + Keypair, + TestSendReceiverTestClient, + TestIsmTestClient, +) { + let program_id = mailbox_id(); + let mut program_test = ProgramTest::new( + "hyperlane_sealevel_mailbox", + program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + program_test.add_program("spl_noop", spl_noop::id(), processor!(spl_noop::noop)); + + let mailbox_program_id = mailbox_id(); + program_test.add_program( + "hyperlane_sealevel_mailbox", + mailbox_program_id, + processor!(hyperlane_sealevel_mailbox::processor::process_instruction), + ); + + program_test.add_program( + "hyperlane_sealevel_test_ism", + hyperlane_sealevel_test_ism::id(), + processor!(hyperlane_sealevel_test_ism::program::process_instruction), + ); + + program_test.add_program( + "hyperlane_sealevel_test_send_receiver", + hyperlane_sealevel_test_send_receiver::id(), + processor!(hyperlane_sealevel_test_send_receiver::program::process_instruction), + ); + + let (banks_client, payer, _recent_blockhash) = program_test.start().await; + + let test_ism = TestIsmTestClient::new(banks_client.clone(), clone_keypair(&payer)); + + let mut test_send_receiver = + TestSendReceiverTestClient::new(banks_client.clone(), clone_keypair(&payer)); + test_send_receiver.init().await.unwrap(); + test_send_receiver + .set_ism( + Some(hyperlane_sealevel_test_ism::id()), + IsmReturnDataMode::EncodeOption, + ) + .await + .unwrap(); + + (banks_client, payer, test_send_receiver, test_ism) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + // Make sure the outbox account was created. + assert_outbox( + &mut banks_client, + mailbox_accounts.outbox, + Outbox { + local_domain: LOCAL_DOMAIN, + outbox_bump_seed: mailbox_accounts.outbox_bump_seed, + owner: Some(payer.pubkey()), + tree: MerkleTree::default(), + }, + ) + .await; + + // Make sure the inbox account was created. + let inbox_account = banks_client + .get_account(mailbox_accounts.inbox) + .await + .unwrap() + .unwrap(); + assert_eq!(inbox_account.owner, program_id); + + let inbox = InboxAccount::fetch(&mut &inbox_account.data[..]) + .unwrap() + .into_inner(); + assert_eq!( + *inbox, + Inbox { + local_domain: LOCAL_DOMAIN, + inbox_bump_seed: mailbox_accounts.inbox_bump_seed, + default_ism: hyperlane_sealevel_test_ism::id(), + processed_count: 0, + } + ); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + // Different local domain to force a different transaction signature, + // otherwise we'll get a (silent) duplicate transaction error. + let result = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN + 1).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized), + ); +} + +#[tokio::test] +async fn test_dispatch_from_eoa() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient = H256::random(); + let message_body = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + let outbox_dispatch = OutboxDispatch { + sender: payer.pubkey(), + destination_domain: REMOTE_DOMAIN, + recipient, + message_body: message_body.clone(), + }; + + let (dispatch_tx_signature, dispatch_unique_keypair, dispatched_message_account_key) = + dispatch_from_payer( + &mut banks_client, + &payer, + &mailbox_accounts, + outbox_dispatch, + ) + .await + .unwrap(); + + let expected_message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient, + body: message_body, + }; + + assert_dispatched_message( + &mut banks_client, + dispatch_tx_signature, + dispatch_unique_keypair.pubkey(), + dispatched_message_account_key, + &expected_message, + ) + .await; + + let mut expected_tree = MerkleTree::default(); + expected_tree.ingest(expected_message.id()); + + // Make sure the outbox account was updated. + assert_outbox( + &mut banks_client, + mailbox_accounts.outbox, + Outbox { + local_domain: LOCAL_DOMAIN, + outbox_bump_seed: mailbox_accounts.outbox_bump_seed, + owner: Some(payer.pubkey()), + tree: expected_tree, + }, + ) + .await; + + // Dispatch another so we can make sure the nonce is incremented correctly + let recipient = H256::random(); + let message_body = vec![69, 42, 0]; + let outbox_dispatch = OutboxDispatch { + sender: payer.pubkey(), + destination_domain: REMOTE_DOMAIN, + recipient, + message_body: message_body.clone(), + }; + + let (dispatch_tx_signature, dispatch_unique_keypair, dispatched_message_account_key) = + dispatch_from_payer( + &mut banks_client, + &payer, + &mailbox_accounts, + outbox_dispatch, + ) + .await + .unwrap(); + + let expected_message = HyperlaneMessage { + version: 0, + nonce: 1, + origin: LOCAL_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient, + body: message_body, + }; + + assert_dispatched_message( + &mut banks_client, + dispatch_tx_signature, + dispatch_unique_keypair.pubkey(), + dispatched_message_account_key, + &expected_message, + ) + .await; + + expected_tree.ingest(expected_message.id()); + + // Make sure the outbox account was updated. + assert_outbox( + &mut banks_client, + mailbox_accounts.outbox, + Outbox { + local_domain: LOCAL_DOMAIN, + outbox_bump_seed: mailbox_accounts.outbox_bump_seed, + owner: Some(payer.pubkey()), + tree: expected_tree, + }, + ) + .await; +} + +#[tokio::test] +async fn test_dispatch_from_program() { + let program_id = mailbox_id(); + let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await; + let test_sender_receiver_program_id = test_send_receiver.id(); + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient = H256::random(); + let message_body = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + let outbox_dispatch = OutboxDispatch { + // Set the sender to the sending program ID + sender: test_sender_receiver_program_id, + destination_domain: REMOTE_DOMAIN, + recipient, + message_body: message_body.clone(), + }; + + let (dispatch_tx_signature, dispatch_unique_keypair, dispatched_message_account_key) = + test_send_receiver + .dispatch(&mailbox_accounts, outbox_dispatch) + .await + .unwrap(); + + let expected_message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: LOCAL_DOMAIN, + // The sender should be the program ID because its dispatch authority signed + sender: test_sender_receiver_program_id.to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient, + body: message_body, + }; + + assert_dispatched_message( + &mut banks_client, + dispatch_tx_signature, + dispatch_unique_keypair.pubkey(), + dispatched_message_account_key, + &expected_message, + ) + .await; + + let mut expected_tree = MerkleTree::default(); + expected_tree.ingest(expected_message.id()); + + // Make sure the outbox account was updated. + assert_outbox( + &mut banks_client, + mailbox_accounts.outbox, + Outbox { + local_domain: LOCAL_DOMAIN, + outbox_bump_seed: mailbox_accounts.outbox_bump_seed, + owner: Some(payer.pubkey()), + tree: expected_tree, + }, + ) + .await; +} + +#[tokio::test] +async fn test_dispatch_errors_if_message_too_large() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient = H256::random(); + let message_body = vec![1; 2049]; + let outbox_dispatch = OutboxDispatch { + sender: payer.pubkey(), + destination_domain: REMOTE_DOMAIN, + recipient, + message_body, + }; + + let result = dispatch_from_payer( + &mut banks_client, + &payer, + &mailbox_accounts, + outbox_dispatch, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError( + 0, + InstructionError::Custom(MailboxError::MaxMessageSizeExceeded as u32), + ), + ); +} + +#[tokio::test] +async fn test_dispatch_returns_message_id() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient = H256::random(); + let message_body = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + let outbox_dispatch = OutboxDispatch { + sender: payer.pubkey(), + destination_domain: REMOTE_DOMAIN, + recipient, + message_body: message_body.clone(), + }; + let expected_message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: REMOTE_DOMAIN, + recipient, + body: message_body, + }; + + let unique_message_account_keypair = Keypair::new(); + + let (dispatched_message_account_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_accounts.program, + ); + + let instruction = Instruction { + program_id: mailbox_accounts.program, + data: MailboxInstruction::OutboxDispatch(outbox_dispatch) + .into_instruction_data() + .unwrap(), + accounts: vec![ + // 0. [writeable] Outbox PDA. + // 1. [signer] Message sender signer. + // 2. [executable] System program. + // 3. [executable] SPL Noop program. + // 4. [signer] Payer. + // 5. [signer] Unique message account. + // 6. [writeable] Dispatched message PDA. An empty message PDA relating to the seeds + // `mailbox_dispatched_message_pda_seeds` where the message contents will be stored. + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_account_key, false), + ], + }; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let simulation_data = banks_client + .simulate_transaction(Transaction::new_unsigned(Message::new_with_blockhash( + &[instruction], + Some(&payer.pubkey()), + &recent_blockhash, + ))) + .await + .unwrap() + .simulation_details + .unwrap() + .return_data + .unwrap() + .data; + + let message_id = H256::try_from_slice(&simulation_data).unwrap(); + assert_eq!(message_id, expected_message.id()); +} + +#[tokio::test] +async fn test_get_recipient_ism_when_specified() { + let program_id = mailbox_id(); + let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = test_send_receiver.id(); + + let ism = Some(Pubkey::new_unique()); + + test_send_receiver + .set_ism(ism, IsmReturnDataMode::EncodeOption) + .await + .unwrap(); + + let recipient_ism = + get_recipient_ism(&mut banks_client, &payer, &mailbox_accounts, recipient_id) + .await + .unwrap(); + assert_eq!(recipient_ism, ism.unwrap()); +} + +#[tokio::test] +async fn test_get_recipient_ism_when_option_none_returned() { + let program_id = mailbox_id(); + let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = test_send_receiver.id(); + + let ism = None; + + test_send_receiver + .set_ism(ism, IsmReturnDataMode::EncodeOption) + .await + .unwrap(); + + let recipient_ism = + get_recipient_ism(&mut banks_client, &payer, &mailbox_accounts, recipient_id) + .await + .unwrap(); + // Expect the default ISM to be used + assert_eq!(recipient_ism, mailbox_accounts.default_ism); +} + +#[tokio::test] +async fn test_get_recipient_ism_when_no_return_data() { + let program_id = mailbox_id(); + let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = test_send_receiver.id(); + + let ism = None; + + test_send_receiver + .set_ism( + ism, + // Return nothing! + IsmReturnDataMode::ReturnNothing, + ) + .await + .unwrap(); + + let recipient_ism = + get_recipient_ism(&mut banks_client, &payer, &mailbox_accounts, recipient_id) + .await + .unwrap(); + // Expect the default ISM to be used + assert_eq!(recipient_ism, mailbox_accounts.default_ism); +} + +#[tokio::test] +async fn test_get_recipient_ism_errors_with_malformatted_recipient_ism_return_data() { + let program_id = mailbox_id(); + let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = test_send_receiver.id(); + + let ism = None; + + test_send_receiver + .set_ism( + ism, + // Return some malformmated data + IsmReturnDataMode::ReturnMalformmatedData, + ) + .await + .unwrap(); + + let result = + get_recipient_ism(&mut banks_client, &payer, &mailbox_accounts, recipient_id).await; + // Expect a BorshIoError + assert!(matches!( + result, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(_, InstructionError::BorshIoError(_)) + )) + )); +} + +#[tokio::test] +async fn test_process_successful_verify_and_handle() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = hyperlane_sealevel_test_send_receiver::id(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: recipient_id.to_bytes().into(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + let (process_tx_signature, processed_message_account_key) = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await + .unwrap(); + + // Expect the message's processed account to be created + assert_processed_message( + &mut banks_client, + process_tx_signature, + processed_message_account_key, + &message, + 0, + ) + .await; + + // Send another to illustrate that the sequence is incremented + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: recipient_id.to_bytes().into(), + body: vec![42, 0, 69], + }; + + let (process_tx_signature, processed_message_account_key) = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await + .unwrap(); + + // Expect the message's processed account to be created + assert_processed_message( + &mut banks_client, + process_tx_signature, + processed_message_account_key, + &message, + 1, + ) + .await; +} + +#[tokio::test] +async fn test_process_errors_if_message_already_processed() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = hyperlane_sealevel_test_send_receiver::id(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: recipient_id.to_bytes().into(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await + .unwrap(); + + let result = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await; + assert_transaction_error( + result, + TransactionError::InstructionError( + 0, + InstructionError::Custom(MailboxError::MessageAlreadyProcessed as u32), + ), + ) +} + +#[tokio::test] +async fn test_process_errors_if_ism_verify_fails() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, mut test_ism) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = hyperlane_sealevel_test_send_receiver::id(); + + test_ism.set_accept(false).await.unwrap(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: recipient_id.to_bytes().into(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + let result = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError( + 0, + InstructionError::Custom(TestIsmError::VerifyNotAccepted as u32), + ), + ); + + assert_message_not_processed(&mut banks_client, &mailbox_accounts, message.id()).await; +} + +#[tokio::test] +async fn test_process_errors_if_recipient_handle_fails() { + let program_id = mailbox_id(); + let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = hyperlane_sealevel_test_send_receiver::id(); + + test_send_receiver + .set_handle_mode(HandleMode::Fail) + .await + .unwrap(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: recipient_id.to_bytes().into(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + let result = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError( + 0, + InstructionError::Custom(TestSendReceiverError::HandleFailed as u32), + ), + ); + + assert_message_not_processed(&mut banks_client, &mailbox_accounts, message.id()).await; +} + +#[tokio::test] +async fn test_process_errors_if_incorrect_destination_domain() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = hyperlane_sealevel_test_send_receiver::id(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + // Incorrect destination domain + destination: LOCAL_DOMAIN + 1, + recipient: recipient_id.to_bytes().into(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + let result = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError( + 0, + InstructionError::Custom(MailboxError::DestinationDomainNotLocalDomain as u32), + ), + ); + + assert_message_not_processed(&mut banks_client, &mailbox_accounts, message.id()).await; +} + +#[tokio::test] +async fn test_process_errors_if_wrong_message_version() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let recipient_id = hyperlane_sealevel_test_send_receiver::id(); + + let message = HyperlaneMessage { + version: 1, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: recipient_id.to_bytes().into(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + let result = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await; + + assert_transaction_error( + result, + TransactionError::InstructionError( + 0, + InstructionError::Custom(MailboxError::UnsupportedMessageVersion as u32), + ), + ); + + assert_message_not_processed(&mut banks_client, &mailbox_accounts, message.id()).await; +} + +#[tokio::test] +async fn test_process_errors_if_recipient_not_a_program() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let message = HyperlaneMessage { + version: 1, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: H256::random(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + let result = process( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await; + + assert_transaction_error(result, TransactionError::ProgramAccountNotFound); + + assert_message_not_processed(&mut banks_client, &mailbox_accounts, message.id()).await; +} + +#[tokio::test] +async fn test_process_errors_if_reentrant() { + let program_id = mailbox_id(); + let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + test_send_receiver + .set_handle_mode(HandleMode::ReenterProcess) + .await + .unwrap(); + + let recipient_id = hyperlane_sealevel_test_send_receiver::id(); + + let message = HyperlaneMessage { + version: 0, + nonce: 0, + origin: REMOTE_DOMAIN, + sender: payer.pubkey().to_bytes().into(), + destination: LOCAL_DOMAIN, + recipient: recipient_id.to_bytes().into(), + body: vec![0, 1, 2, 3, 4, 5, 6, 7, 8], + }; + + let mut accounts = get_process_account_metas( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + ) + .await + .unwrap(); + // Add the same accounts to the end, because the test recipient that attempts + // to reenter will use the rest of the accounts provided in its handler to reenter. + accounts.extend(accounts.clone()); + + let result = process_with_accounts( + &mut banks_client, + &payer, + &mailbox_accounts, + vec![], + &message, + accounts, + ) + .await; + + // We use a RefMut of the Inbox PDA's data as a reentrancy guard, so we expect `AccountBorrowFailed` + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::AccountBorrowFailed), + ); + + assert_message_not_processed(&mut banks_client, &mailbox_accounts, message.id()).await; +} + +#[tokio::test] +async fn test_inbox_set_default_ism() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let new_default_ism = Pubkey::new_unique(); + + // Set the default ISM to the test ISM + let instruction = Instruction { + program_id: mailbox_accounts.program, + data: MailboxInstruction::InboxSetDefaultIsm(new_default_ism) + .into_instruction_data() + .unwrap(), + accounts: vec![ + // 0. [writeable] - The Inbox PDA account. + // 1. [] - The Outbox PDA account. + // 2. [signer] - The owner of the Mailbox. + AccountMeta::new(mailbox_accounts.inbox, false), + AccountMeta::new_readonly(mailbox_accounts.outbox, false), + AccountMeta::new(payer.pubkey(), true), + ], + }; + + process_instruction(&mut banks_client, instruction, &payer, &[&payer]) + .await + .unwrap(); + + // Make sure the inbox account was updated. + assert_inbox( + &mut banks_client, + mailbox_accounts.inbox, + Inbox { + local_domain: LOCAL_DOMAIN, + inbox_bump_seed: mailbox_accounts.inbox_bump_seed, + default_ism: new_default_ism, + processed_count: 0, + }, + ) + .await; +} + +#[tokio::test] +async fn test_inbox_set_default_ism_errors_if_owner_not_signer() { + let program_id = mailbox_id(); + let (mut banks_client, payer, _, _) = setup_client().await; + + let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN) + .await + .unwrap(); + + let new_default_ism = Pubkey::new_unique(); + + let non_owner = new_funded_keypair(&mut banks_client, &payer, 1000000000).await; + + // Where the payer is a signer but not the owner + let instruction = Instruction { + program_id: mailbox_accounts.program, + data: MailboxInstruction::InboxSetDefaultIsm(new_default_ism) + .into_instruction_data() + .unwrap(), + accounts: vec![ + // 0. [writeable] - The Inbox PDA account. + // 1. [] - The Outbox PDA account. + // 2. [signer] - The owner of the Mailbox. + AccountMeta::new(mailbox_accounts.inbox, false), + AccountMeta::new_readonly(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(non_owner.pubkey(), true), + ], + }; + let result = + process_instruction(&mut banks_client, instruction, &non_owner, &[&non_owner]).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::InvalidArgument), + ); + + // Where the owner is correct but not a signer + let instruction = Instruction { + program_id: mailbox_accounts.program, + data: MailboxInstruction::InboxSetDefaultIsm(new_default_ism) + .into_instruction_data() + .unwrap(), + accounts: vec![ + // 0. [writeable] - The Inbox PDA account. + // 1. [] - The Outbox PDA account. + // 2. [signer] - The owner of the Mailbox. + AccountMeta::new(mailbox_accounts.inbox, false), + AccountMeta::new_readonly(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + }; + let result = + process_instruction(&mut banks_client, instruction, &non_owner, &[&non_owner]).await; + assert_transaction_error( + result, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature), + ); +} diff --git a/rust/sealevel/programs/mailbox-test/src/lib.rs b/rust/sealevel/programs/mailbox-test/src/lib.rs new file mode 100644 index 0000000000..93ba63b164 --- /dev/null +++ b/rust/sealevel/programs/mailbox-test/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(test)] +mod functional; + +#[cfg(test)] +mod utils; diff --git a/rust/sealevel/programs/mailbox-test/src/utils.rs b/rust/sealevel/programs/mailbox-test/src/utils.rs new file mode 100644 index 0000000000..434dce00bb --- /dev/null +++ b/rust/sealevel/programs/mailbox-test/src/utils.rs @@ -0,0 +1,202 @@ +use hyperlane_core::{Encode, HyperlaneMessage, H256}; + +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; +use solana_program_test::*; +use solana_sdk::{ + signature::{Signature, Signer}, + signer::keypair::Keypair, + transaction::Transaction, +}; + +use hyperlane_sealevel_mailbox::{ + accounts::{ + DispatchedMessage, DispatchedMessageAccount, Inbox, InboxAccount, Outbox, OutboxAccount, + ProcessedMessage, ProcessedMessageAccount, + }, + instruction::{Instruction as MailboxInstruction, OutboxDispatch}, + mailbox_dispatched_message_pda_seeds, mailbox_processed_message_pda_seeds, +}; + +use hyperlane_test_utils::MailboxAccounts; + +pub async fn dispatch_from_payer( + banks_client: &mut BanksClient, + payer: &Keypair, + mailbox_accounts: &MailboxAccounts, + outbox_dispatch: OutboxDispatch, +) -> Result<(Signature, Keypair, Pubkey), BanksClientError> { + let unique_message_account_keypair = Keypair::new(); + + let (dispatched_message_account_key, _dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_accounts.program, + ); + + let instruction = Instruction { + program_id: mailbox_accounts.program, + data: MailboxInstruction::OutboxDispatch(outbox_dispatch) + .into_instruction_data() + .unwrap(), + accounts: vec![ + // 0. [writeable] Outbox PDA. + // 1. [signer] Message sender signer. + // 2. [executable] System program. + // 3. [executable] SPL Noop program. + // 4. [signer] Payer. + // 5. [signer] Unique message account. + // 6. [writeable] Dispatched message PDA. An empty message PDA relating to the seeds + // `mailbox_dispatched_message_pda_seeds` where the message contents will be stored. + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_account_key, false), + ], + }; + + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[payer, &unique_message_account_keypair], + recent_blockhash, + ); + let tx_signature = transaction.signatures[0]; + banks_client.process_transaction(transaction).await?; + + Ok(( + tx_signature, + unique_message_account_keypair, + dispatched_message_account_key, + )) +} + +pub async fn assert_dispatched_message( + banks_client: &mut BanksClient, + dispatch_tx_signature: Signature, + dispatch_unique_account_pubkey: Pubkey, + dispatched_message_account_key: Pubkey, + expected_message: &HyperlaneMessage, +) { + // Get the slot of the tx + let dispatch_tx_status = banks_client + .get_transaction_status(dispatch_tx_signature) + .await + .unwrap() + .unwrap(); + let dispatch_slot = dispatch_tx_status.slot; + + // Get the dispatched message account + let dispatched_message_account = banks_client + .get_account(dispatched_message_account_key) + .await + .unwrap() + .unwrap(); + let dispatched_message = + DispatchedMessageAccount::fetch(&mut &dispatched_message_account.data[..]) + .unwrap() + .into_inner(); + assert_eq!( + *dispatched_message, + DispatchedMessage::new( + expected_message.nonce, + dispatch_slot, + dispatch_unique_account_pubkey, + expected_message.to_vec(), + ), + ); +} + +pub async fn assert_outbox( + banks_client: &mut BanksClient, + outbox_pubkey: Pubkey, + expected_outbox: Outbox, +) { + // Check that the outbox account was updated. + let outbox_account = banks_client + .get_account(outbox_pubkey) + .await + .unwrap() + .unwrap(); + + let outbox = OutboxAccount::fetch(&mut &outbox_account.data[..]) + .unwrap() + .into_inner(); + + assert_eq!(*outbox, expected_outbox,); +} + +pub async fn assert_inbox( + banks_client: &mut BanksClient, + inbox_pubkey: Pubkey, + expected_inbox: Inbox, +) { + // Check that the inbox account was updated. + let inbox_account = banks_client + .get_account(inbox_pubkey) + .await + .unwrap() + .unwrap(); + + let inbox = InboxAccount::fetch(&mut &inbox_account.data[..]) + .unwrap() + .into_inner(); + + assert_eq!(*inbox, expected_inbox,); +} + +pub async fn assert_processed_message( + banks_client: &mut BanksClient, + process_tx_signature: Signature, + processed_message_account_key: Pubkey, + expected_message: &HyperlaneMessage, + expected_sequence: u64, +) { + // Get the slot of the tx + let process_tx_status = banks_client + .get_transaction_status(process_tx_signature) + .await + .unwrap() + .unwrap(); + let process_slot = process_tx_status.slot; + + // Get the processed message account + let processed_message_account = banks_client + .get_account(processed_message_account_key) + .await + .unwrap() + .unwrap(); + let processed_message = + ProcessedMessageAccount::fetch(&mut &processed_message_account.data[..]) + .unwrap() + .into_inner(); + assert_eq!( + *processed_message, + ProcessedMessage::new(expected_sequence, expected_message.id(), process_slot,), + ); +} + +pub async fn assert_message_not_processed( + banks_client: &mut BanksClient, + mailbox_accounts: &MailboxAccounts, + message_id: H256, +) { + let (processed_message_account_key, _processed_message_account_bump) = + Pubkey::find_program_address( + mailbox_processed_message_pda_seeds!(&message_id), + &mailbox_accounts.program, + ); + + // Get the processed message account + let processed_message_account = banks_client + .get_account(processed_message_account_key) + .await + .unwrap(); + assert!(processed_message_account.is_none()); +} diff --git a/rust/sealevel/programs/mailbox/Cargo.toml b/rust/sealevel/programs/mailbox/Cargo.toml new file mode 100644 index 0000000000..5b7d189198 --- /dev/null +++ b/rust/sealevel/programs/mailbox/Cargo.toml @@ -0,0 +1,36 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-mailbox" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] +no-spl-noop = [] + +[dependencies] +borsh.workspace = true +num-derive.workspace = true +num-traits.workspace = true +solana-program.workspace = true +thiserror.workspace = true +spl-noop.workspace = true +# Required to allow dependencies `getrandom` but to preserve determinism required by programs, see +# https://github.com/solana-labs/solana/blob/master/docs/src/developing/on-chain-programs/developing-rust.md#depending-on-rand +getrandom = { workspace = true, features = ["custom"] } + +access-control = { path = "../../libraries/access-control" } +account-utils = { path = "../../libraries/account-utils" } +hyperlane-core = { path = "../../../hyperlane-core" } +hyperlane-sealevel-interchain-security-module-interface = { path = "../../libraries/interchain-security-module-interface" } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[dev-dependencies] +base64.workspace = true +itertools.workspace = true +log.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/mailbox/README.md b/rust/sealevel/programs/mailbox/README.md new file mode 100644 index 0000000000..a65d403b2d --- /dev/null +++ b/rust/sealevel/programs/mailbox/README.md @@ -0,0 +1,17 @@ +# Mailbox + +The Mailbox program lives here. + +To build: + +``` +cargo build-sbf --arch sbf +``` + +To run unit tests: + +``` +cargo test +``` + +To run functional/integration tests, see the crate `../mailbox-test`. diff --git a/rust/sealevel/programs/mailbox/src/accounts.rs b/rust/sealevel/programs/mailbox/src/accounts.rs new file mode 100644 index 0000000000..15436009b3 --- /dev/null +++ b/rust/sealevel/programs/mailbox/src/accounts.rs @@ -0,0 +1,390 @@ +//! Hyperlane Sealevel Mailbox data account layouts. + +use core::cell::RefMut; +use std::io::Read; + +use access_control::AccessControl; +use account_utils::{AccountData, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{accumulator::incremental::IncrementalMerkle as MerkleTree, H256}; +use solana_program::{ + account_info::AccountInfo, clock::Slot, program_error::ProgramError, pubkey::Pubkey, +}; + +use crate::{mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds}; + +/// The Inbox account. +pub type InboxAccount = AccountData; + +/// The Inbox account data, which is used when processing messages. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, PartialEq, Eq)] +pub struct Inbox { + /// The local domain. + pub local_domain: u32, + /// The bump seed of the inbox PDA. + pub inbox_bump_seed: u8, + /// The default ISM. + pub default_ism: Pubkey, + /// The number of messages processed. Used for easy indexing of processed messages. + pub processed_count: u64, +} + +impl SizedData for Inbox { + fn size(&self) -> usize { + // 4 byte local_domain + // 1 byte inbox_bump_seed + // 32 byte default_ism + // 8 byte processed_count + 4 + 1 + 32 + 8 + } +} + +impl Inbox { + /// Verifies that the given account is the canonical Inbox PDA and returns the deserialized inner data. + pub fn verify_account_and_fetch_inner( + program_id: &Pubkey, + inbox_account_info: &AccountInfo<'_>, + ) -> Result { + let inbox = InboxAccount::fetch(&mut &inbox_account_info.data.borrow()[..])?.into_inner(); + let expected_inbox_key = Pubkey::create_program_address( + mailbox_inbox_pda_seeds!(inbox.inbox_bump_seed), + program_id, + )?; + if inbox_account_info.key != &expected_inbox_key { + return Err(ProgramError::InvalidArgument); + } + if inbox_account_info.owner != program_id { + return Err(ProgramError::IllegalOwner); + } + + Ok(*inbox) + } + + /// Verifies that the given account is the canonical Inbox PDA, and returns the deserialized inner data + /// alongside a RefMut of the account data. + /// Intended to be used when the account data's RefMut is required, e.g. as a + /// reentrancy guard. + pub fn verify_account_and_fetch_inner_with_data_refmut<'a, 'b>( + program_id: &'b Pubkey, + inbox_account_info: &'b AccountInfo<'a>, + ) -> Result<(Self, RefMut<'b, &'a mut [u8]>), ProgramError> { + let data_refmut = inbox_account_info.try_borrow_mut_data()?; + + let inbox = InboxAccount::fetch(&mut &data_refmut[..])?.into_inner(); + let expected_inbox_key = Pubkey::create_program_address( + mailbox_inbox_pda_seeds!(inbox.inbox_bump_seed), + program_id, + )?; + if inbox_account_info.key != &expected_inbox_key { + return Err(ProgramError::InvalidArgument); + } + if inbox_account_info.owner != program_id { + return Err(ProgramError::IllegalOwner); + } + + Ok((*inbox, data_refmut)) + } +} + +/// The Outbox account. +pub type OutboxAccount = AccountData; + +/// The Outbox account data, which is used when dispatching messages. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, PartialEq, Eq)] +pub struct Outbox { + /// The local domain. + pub local_domain: u32, + /// The bump seed of the outbox PDA. + pub outbox_bump_seed: u8, + /// The owner of this program, which has privileged permissions. + pub owner: Option, + /// The merkle tree of dispatched messages. + pub tree: MerkleTree, +} + +impl SizedData for Outbox { + fn size(&self) -> usize { + // 4 byte local_domain + // 1 byte outbox_bump_seed + // 33 byte owner (1 byte enum variant, 32 byte pubkey) + // 1032 byte tree (32 * 32 = 1024 byte branch, 8 byte count) + 4 + 1 + 33 + 1032 + } +} + +impl AccessControl for Outbox { + fn owner(&self) -> Option<&Pubkey> { + self.owner.as_ref() + } + + fn set_owner(&mut self, owner: Option) -> Result<(), ProgramError> { + self.owner = owner; + Ok(()) + } +} + +impl Outbox { + /// Verifies that the given account is the canonical Outbox PDA and returns the deserialized inner data. + pub fn verify_account_and_fetch_inner( + program_id: &Pubkey, + outbox_account_info: &AccountInfo, + ) -> Result { + let outbox = + OutboxAccount::fetch(&mut &outbox_account_info.data.borrow()[..])?.into_inner(); + let expected_outbox_key = Pubkey::create_program_address( + mailbox_outbox_pda_seeds!(outbox.outbox_bump_seed), + program_id, + )?; + if outbox_account_info.key != &expected_outbox_key { + return Err(ProgramError::InvalidArgument); + } + if outbox_account_info.owner != program_id { + return Err(ProgramError::IllegalOwner); + } + + Ok(*outbox) + } +} + +/// An account corresponding to a dispatched message. +pub type DispatchedMessageAccount = AccountData; + +/// A discriminator used to easily identify dispatched message accounts. +/// This is the first 8 bytes of the account data. +pub const DISPATCHED_MESSAGE_DISCRIMINATOR: &[u8; 8] = b"DISPATCH"; + +/// A dispatched message. +#[derive(Debug, Default, Eq, PartialEq)] +pub struct DispatchedMessage { + /// The discriminator, intended to be set to `DISPATCHED_MESSAGE_DISCRIMINATOR`. + pub discriminator: [u8; 8], + /// The nonce of the message. + pub nonce: u32, + /// The slot in which the message was dispatched. + pub slot: Slot, + /// The unique message pubkey used when the message was dispatched. + pub unique_message_pubkey: Pubkey, + /// The encoded message. + pub encoded_message: Vec, +} + +impl DispatchedMessage { + /// Creates a new dispatched message. + pub fn new( + nonce: u32, + slot: Slot, + unique_message_pubkey: Pubkey, + encoded_message: Vec, + ) -> Self { + Self { + discriminator: *DISPATCHED_MESSAGE_DISCRIMINATOR, + nonce, + slot, + unique_message_pubkey, + encoded_message, + } + } +} + +impl SizedData for DispatchedMessage { + fn size(&self) -> usize { + // 8 byte discriminator + // 4 byte nonce + // 8 byte slot + // 32 byte unique_message_pubkey + // encoded_message.len() bytes + 8 + 4 + 8 + 32 + self.encoded_message.len() + } +} + +/// For tighter packing and explicit endianness, we implement our own serialization. +impl BorshSerialize for DispatchedMessage { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + writer.write_all(DISPATCHED_MESSAGE_DISCRIMINATOR)?; + writer.write_all(&self.nonce.to_le_bytes())?; + writer.write_all(&self.slot.to_le_bytes())?; + writer.write_all(&self.unique_message_pubkey.to_bytes())?; + writer.write_all(&self.encoded_message)?; + Ok(()) + } +} + +/// For tighter packing, explicit endianness, and errors on an invalid discriminator, we implement our own deserialization. +impl BorshDeserialize for DispatchedMessage { + fn deserialize(reader: &mut &[u8]) -> std::io::Result { + let mut discriminator = [0u8; 8]; + reader.read_exact(&mut discriminator)?; + if &discriminator != DISPATCHED_MESSAGE_DISCRIMINATOR { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid discriminator", + )); + } + + let mut nonce = [0u8; 4]; + reader.read_exact(&mut nonce)?; + + let mut slot = [0u8; 8]; + reader.read_exact(&mut slot)?; + + let mut unique_message_pubkey = [0u8; 32]; + reader.read_exact(&mut unique_message_pubkey)?; + + let mut encoded_message = vec![]; + reader.read_to_end(&mut encoded_message)?; + + Ok(Self { + discriminator, + nonce: u32::from_le_bytes(nonce), + slot: u64::from_le_bytes(slot), + unique_message_pubkey: Pubkey::new_from_array(unique_message_pubkey), + encoded_message, + }) + } +} + +/// An account corresponding to a processed message. +pub type ProcessedMessageAccount = AccountData; + +/// A discriminator used to easily identify processed message accounts. +pub const PROCESSED_MESSAGE_DISCRIMINATOR: &[u8; 8] = b"PROCESSD"; + +/// A processed message. +#[derive(Debug, Default, Eq, PartialEq, BorshSerialize)] +pub struct ProcessedMessage { + /// The discriminator, intended to be set to `PROCESSED_MESSAGE_DISCRIMINATOR`. + pub discriminator: [u8; 8], + /// The sequence of the processed message, which increases from 0 for each processed message. + /// This way, we can easily index processed messages. + pub sequence: u64, + /// The message ID of the processed message. + pub message_id: H256, + /// The slot in which the message was processed. + pub slot: Slot, +} + +impl ProcessedMessage { + /// Creates a new processed message. + pub fn new(sequence: u64, message_id: H256, slot: Slot) -> Self { + Self { + discriminator: *PROCESSED_MESSAGE_DISCRIMINATOR, + sequence, + message_id, + slot, + } + } +} + +impl SizedData for ProcessedMessage { + fn size(&self) -> usize { + // 8 byte discriminator + // 8 byte sequence + // 32 byte message_id + // 8 byte slot + 8 + 8 + 32 + 8 + } +} + +/// To error upon invalid data, we implement our own deserialization. +impl BorshDeserialize for ProcessedMessage { + fn deserialize(reader: &mut &[u8]) -> std::io::Result { + let mut discriminator = [0u8; 8]; + reader.read_exact(&mut discriminator)?; + if &discriminator != PROCESSED_MESSAGE_DISCRIMINATOR { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid discriminator", + )); + } + + let mut sequence = [0u8; 8]; + reader.read_exact(&mut sequence)?; + + let mut message_id = [0u8; 32]; + reader.read_exact(&mut message_id)?; + + let mut slot = [0u8; 8]; + reader.read_exact(&mut slot)?; + + Ok(Self { + discriminator, + sequence: u64::from_le_bytes(sequence), + message_id: H256::from_slice(&message_id), + slot: u64::from_le_bytes(slot), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use solana_program::pubkey::Pubkey; + + #[test] + fn test_outbox_ser_deser() { + let outbox = Outbox { + local_domain: 420, + outbox_bump_seed: 69, + owner: Some(Pubkey::new_unique()), + tree: MerkleTree::default(), + }; + + let mut serialized = vec![]; + outbox.serialize(&mut serialized).unwrap(); + + let deserialized = Outbox::deserialize(&mut serialized.as_slice()).unwrap(); + + assert_eq!(outbox, deserialized); + assert_eq!(serialized.len(), outbox.size()); + } + + #[test] + fn test_inbox_ser_deser() { + let inbox = Inbox { + local_domain: 420, + inbox_bump_seed: 69, + default_ism: Pubkey::new_unique(), + processed_count: 69696969, + }; + + let mut serialized = vec![]; + inbox.serialize(&mut serialized).unwrap(); + + let deserialized = Inbox::deserialize(&mut serialized.as_slice()).unwrap(); + + assert_eq!(inbox, deserialized); + assert_eq!(serialized.len(), inbox.size()); + } + + #[test] + fn test_dispatched_message_ser_deser() { + let dispatched_message = DispatchedMessage::new( + 420, + 69696969, + Pubkey::new_unique(), + vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + ); + + let mut serialized = vec![]; + dispatched_message.serialize(&mut serialized).unwrap(); + + let deserialized = DispatchedMessage::deserialize(&mut serialized.as_slice()).unwrap(); + + assert_eq!(dispatched_message, deserialized); + assert_eq!(serialized.len(), dispatched_message.size()); + } + + #[test] + fn test_processed_message_ser_deser() { + let processed_message = ProcessedMessage::new(420420420, H256::random(), 69696969); + + let mut serialized = vec![]; + processed_message.serialize(&mut serialized).unwrap(); + + let deserialized = ProcessedMessage::deserialize(&mut serialized.as_slice()).unwrap(); + + assert_eq!(processed_message, deserialized); + assert_eq!(serialized.len(), processed_message.size()); + } +} diff --git a/rust/sealevel/programs/mailbox/src/error.rs b/rust/sealevel/programs/mailbox/src/error.rs new file mode 100644 index 0000000000..566d1f6d1c --- /dev/null +++ b/rust/sealevel/programs/mailbox/src/error.rs @@ -0,0 +1,36 @@ +//! Hyperlane Sealevel Mailbox custom errors. + +use solana_program::program_error::ProgramError; + +/// Custom errors type for the Mailbox program. +#[derive(Copy, Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] +#[repr(u32)] +pub enum Error { + /// Some kind of encoding error occurred. + #[error("Encoding error")] + EncodeError = 1, + /// Some kind of decoding error occurred. + #[error("Decoding error")] + DecodeError = 2, + /// The message version is not supported. + #[error("Unsupported message version")] + UnsupportedMessageVersion = 3, + /// The destination domain of the message is not the local domain. + #[error("Message's destination domain is not the local domain")] + DestinationDomainNotLocalDomain = 4, + /// The message has already been processed. + #[error("Message has already been processed")] + MessageAlreadyProcessed = 5, + /// Unused account(s) were provided. + #[error("Unused account(s) provided")] + ExtraneousAccount = 6, + /// The message is too large. + #[error("Message is larger than the maximum allowed")] + MaxMessageSizeExceeded = 7, +} + +impl From for ProgramError { + fn from(err: Error) -> Self { + ProgramError::Custom(err as u32) + } +} diff --git a/rust/sealevel/programs/mailbox/src/instruction.rs b/rust/sealevel/programs/mailbox/src/instruction.rs new file mode 100644 index 0000000000..26728c07ca --- /dev/null +++ b/rust/sealevel/programs/mailbox/src/instruction.rs @@ -0,0 +1,121 @@ +//! Instructions for the Hyperlane Sealevel Mailbox program. + +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::H256; +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::{mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds}; + +/// The current message version. +pub const VERSION: u8 = 0; + +/// Maximum bytes per message = 2 KiB (somewhat arbitrarily set to begin). +pub const MAX_MESSAGE_BODY_BYTES: usize = 2 * 2_usize.pow(10); + +/// Instructions supported by the Mailbox program. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub enum Instruction { + /// Initializes the program. + Init(Init), + /// Processes a message. + InboxProcess(InboxProcess), + /// Sets the default ISM. + InboxSetDefaultIsm(Pubkey), + /// Gets the recipient's ISM. + InboxGetRecipientIsm(Pubkey), + /// Dispatches a message. + OutboxDispatch(OutboxDispatch), + /// Gets the number of messages that have been dispatched. + OutboxGetCount, + /// Gets the latest checkpoint. + OutboxGetLatestCheckpoint, + /// Gets the root of the dispatched message merkle tree. + OutboxGetRoot, + /// Gets the owner of the Mailbox. + GetOwner, + /// Transfers ownership of the Mailbox. + TransferOwnership(Option), +} + +impl Instruction { + /// Deserializes an instruction from a slice. + pub fn from_instruction_data(data: &[u8]) -> Result { + Self::try_from_slice(data).map_err(|_| ProgramError::InvalidInstructionData) + } + + /// Serializes an instruction into a vector of bytes. + pub fn into_instruction_data(self) -> Result, ProgramError> { + self.try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string())) + } +} + +/// Instruction data for the Init instruction. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct Init { + /// The local domain of the Mailbox. + pub local_domain: u32, + /// The default ISM. + pub default_ism: Pubkey, +} + +/// Instruction data for the OutboxDispatch instruction. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct OutboxDispatch { + /// The sender of the message. + /// This is required and not implied because a program uses a dispatch authority PDA + /// to sign the CPI on its behalf. Instruction processing logic prevents a program from + /// specifying any message sender it wants by requiring the relevant dispatch authority + /// to sign the CPI. + pub sender: Pubkey, + /// The destination domain of the message. + pub destination_domain: u32, + /// The remote recipient of the message. + pub recipient: H256, + /// The message body. + pub message_body: Vec, +} + +/// Instruction data for the InboxProcess instruction. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)] +pub struct InboxProcess { + /// The metadata required by the ISM to process the message. + pub metadata: Vec, + /// The encoded message. + pub message: Vec, +} + +/// Creates an Init instruction. +pub fn init_instruction( + program_id: Pubkey, + local_domain: u32, + default_ism: Pubkey, + payer: Pubkey, +) -> Result { + let (inbox_account, _inbox_bump) = + Pubkey::try_find_program_address(mailbox_inbox_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + let (outbox_account, _outbox_bump) = + Pubkey::try_find_program_address(mailbox_outbox_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + let instruction = SolanaInstruction { + program_id, + data: Instruction::Init(Init { + local_domain, + default_ism, + }) + .into_instruction_data()?, + accounts: vec![ + AccountMeta::new(solana_program::system_program::id(), false), + AccountMeta::new(payer, true), + AccountMeta::new(inbox_account, false), + AccountMeta::new(outbox_account, false), + ], + }; + Ok(instruction) +} diff --git a/rust/sealevel/programs/mailbox/src/lib.rs b/rust/sealevel/programs/mailbox/src/lib.rs new file mode 100644 index 0000000000..3cf7cbdd78 --- /dev/null +++ b/rust/sealevel/programs/mailbox/src/lib.rs @@ -0,0 +1,13 @@ +//! Hyperlane Mailbox contract for Sealevel-compatible (Solana Virtual Machine) chains. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod accounts; +pub mod error; +pub mod instruction; +pub mod pda_seeds; +pub mod processor; + +pub use spl_noop; diff --git a/rust/sealevel/programs/mailbox/src/pda_seeds.rs b/rust/sealevel/programs/mailbox/src/pda_seeds.rs new file mode 100644 index 0000000000..f1cf628454 --- /dev/null +++ b/rust/sealevel/programs/mailbox/src/pda_seeds.rs @@ -0,0 +1,118 @@ +//! This file contains the PDA seeds for the Mailbox program. + +/// PDA seeds for the Inbox account. +#[macro_export] +macro_rules! mailbox_inbox_pda_seeds { + () => {{ + &[b"hyperlane", b"-", b"inbox"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane", b"-", b"inbox", &[$bump_seed]] + }}; +} + +/// PDA seeds for the Outbox account. +#[macro_export] +macro_rules! mailbox_outbox_pda_seeds { + () => {{ + &[b"hyperlane", b"-", b"outbox"] + }}; + + ($bump_seed:expr) => {{ + &[b"hyperlane", b"-", b"outbox", &[$bump_seed]] + }}; +} + +/// Gets the PDA seeds for a message storage account that's +/// based upon the pubkey of a unique message account. +#[macro_export] +macro_rules! mailbox_dispatched_message_pda_seeds { + ($unique_message_pubkey:expr) => {{ + &[ + b"hyperlane", + b"-", + b"dispatched_message", + b"-", + $unique_message_pubkey.as_ref(), + ] + }}; + + ($unique_message_pubkey:expr, $bump_seed:expr) => {{ + &[ + b"hyperlane", + b"-", + b"dispatched_message", + b"-", + $unique_message_pubkey.as_ref(), + &[$bump_seed], + ] + }}; +} + +/// The PDA seeds relating to a program's dispatch authority. +#[macro_export] +macro_rules! mailbox_message_dispatch_authority_pda_seeds { + () => {{ + &[b"hyperlane_dispatcher", b"-", b"dispatch_authority"] + }}; + + ($bump_seed:expr) => {{ + &[ + b"hyperlane_dispatcher", + b"-", + b"dispatch_authority", + &[$bump_seed], + ] + }}; +} + +/// The PDA seeds relating to the Mailbox's process authority for a particular recipient. +#[macro_export] +macro_rules! mailbox_process_authority_pda_seeds { + ($recipient_pubkey:expr) => {{ + &[ + b"hyperlane", + b"-", + b"process_authority", + b"-", + $recipient_pubkey.as_ref(), + ] + }}; + + ($recipient_pubkey:expr, $bump_seed:expr) => {{ + &[ + b"hyperlane", + b"-", + b"process_authority", + b"-", + $recipient_pubkey.as_ref(), + &[$bump_seed], + ] + }}; +} + +/// The PDA seeds relating to the Mailbox's process authority for a particular recipient. +#[macro_export] +macro_rules! mailbox_processed_message_pda_seeds { + ($message_id_h256:expr) => {{ + &[ + b"hyperlane", + b"-", + b"processed_message", + b"-", + $message_id_h256.as_bytes(), + ] + }}; + + ($message_id_h256:expr, $bump_seed:expr) => {{ + &[ + b"hyperlane", + b"-", + b"processed_message", + b"-", + $message_id_h256.as_bytes(), + &[$bump_seed], + ] + }}; +} diff --git a/rust/sealevel/programs/mailbox/src/processor.rs b/rust/sealevel/programs/mailbox/src/processor.rs new file mode 100644 index 0000000000..ee22ddd1ca --- /dev/null +++ b/rust/sealevel/programs/mailbox/src/processor.rs @@ -0,0 +1,829 @@ +//! Entrypoint, dispatch, and execution for the Hyperlane Sealevel mailbox instruction. + +use access_control::AccessControl; +use account_utils::SizedData; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::{ + accumulator::incremental::IncrementalMerkle as MerkleTree, Decode, Encode, HyperlaneMessage, + H256, +}; +#[cfg(not(feature = "no-entrypoint"))] +use solana_program::entrypoint; +use solana_program::{ + account_info::next_account_info, + account_info::AccountInfo, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, + msg, + program::{get_return_data, invoke, invoke_signed, set_return_data}, + program_error::ProgramError, + pubkey::Pubkey, + sysvar::{clock::Clock, rent::Rent, Sysvar}, +}; + +use account_utils::{create_pda_account, verify_account_uninitialized}; +use hyperlane_sealevel_interchain_security_module_interface::{ + InterchainSecurityModuleInstruction, VerifyInstruction, +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use serializable_account_meta::SimulationReturnData; + +use crate::{ + accounts::{ + DispatchedMessage, DispatchedMessageAccount, Inbox, InboxAccount, Outbox, OutboxAccount, + ProcessedMessage, ProcessedMessageAccount, + }, + error::Error, + instruction::{ + InboxProcess, Init, Instruction as MailboxIxn, OutboxDispatch, MAX_MESSAGE_BODY_BYTES, + VERSION, + }, + mailbox_dispatched_message_pda_seeds, mailbox_inbox_pda_seeds, + mailbox_message_dispatch_authority_pda_seeds, mailbox_outbox_pda_seeds, + mailbox_process_authority_pda_seeds, mailbox_processed_message_pda_seeds, +}; + +#[cfg(not(feature = "no-entrypoint"))] +entrypoint!(process_instruction); + +/// Entrypoint for the Mailbox program. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + match MailboxIxn::from_instruction_data(instruction_data)? { + MailboxIxn::Init(init) => initialize(program_id, accounts, init), + MailboxIxn::InboxProcess(process) => inbox_process(program_id, accounts, process), + MailboxIxn::InboxSetDefaultIsm(ism) => inbox_set_default_ism(program_id, accounts, ism), + MailboxIxn::InboxGetRecipientIsm(recipient) => { + inbox_get_recipient_ism(program_id, accounts, recipient) + } + MailboxIxn::OutboxDispatch(dispatch) => outbox_dispatch(program_id, accounts, dispatch), + MailboxIxn::OutboxGetCount => outbox_get_count(program_id, accounts), + MailboxIxn::OutboxGetLatestCheckpoint => outbox_get_latest_checkpoint(program_id, accounts), + MailboxIxn::OutboxGetRoot => outbox_get_root(program_id, accounts), + MailboxIxn::GetOwner => get_owner(program_id, accounts), + MailboxIxn::TransferOwnership(new_owner) => { + transfer_ownership(program_id, accounts, new_owner) + } + } + .map_err(|err| { + msg!("{}", err); + err + }) +} + +/// Initializes the Mailbox. +/// +/// Accounts: +/// 0. [executable] The system program. +/// 1. [signer, writable] The payer account and owner of the Mailbox. +/// 2. [writable] The inbox PDA account. +/// 3. [writable] The outbox PDA account. +fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let rent = Rent::get()?; + + // Account 0: The system program. + let system_program_info = next_account_info(accounts_iter)?; + if system_program_info.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: The payer account and owner of the Mailbox. + let payer_info = next_account_info(accounts_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 2: The inbox PDA account. + let inbox_info = next_account_info(accounts_iter)?; + let (inbox_key, inbox_bump) = + Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), program_id); + if &inbox_key != inbox_info.key { + return Err(ProgramError::InvalidArgument); + } + verify_account_uninitialized(inbox_info)?; + + let inbox_account = InboxAccount::from(Inbox { + local_domain: init.local_domain, + inbox_bump_seed: inbox_bump, + default_ism: init.default_ism, + processed_count: 0, + }); + + // Create the inbox PDA account. + create_pda_account( + payer_info, + &rent, + inbox_account.size(), + program_id, + system_program_info, + inbox_info, + mailbox_inbox_pda_seeds!(inbox_bump), + )?; + // Store the inbox account. + inbox_account.store(inbox_info, false)?; + + // Account 3: The outbox PDA account. + let outbox_info = next_account_info(accounts_iter)?; + let (outbox_key, outbox_bump) = + Pubkey::find_program_address(mailbox_outbox_pda_seeds!(), program_id); + if &outbox_key != outbox_info.key { + return Err(ProgramError::InvalidArgument); + } + verify_account_uninitialized(outbox_info)?; + + let outbox_account = OutboxAccount::from(Outbox { + local_domain: init.local_domain, + outbox_bump_seed: outbox_bump, + owner: Some(*payer_info.key), + tree: MerkleTree::default(), + }); + + // Create the outbox PDA account. + create_pda_account( + payer_info, + &rent, + outbox_account.size(), + program_id, + system_program_info, + outbox_info, + mailbox_outbox_pda_seeds!(outbox_bump), + )?; + // Store the outbox account. + outbox_account.store(outbox_info, false)?; + + Ok(()) +} + +/// Process a message. Non-reentrant through the use of a RefMut. +/// +// Accounts: +// 0. [signer] Payer account. This pays for the creation of the processed message PDA. +// 1. [executable] The system program. +// 2. [writable] Inbox PDA account. +// 3. [] Mailbox process authority specific to the message recipient. +// 4. [writable] Processed message PDA. +// 5..N [??] Accounts required to invoke the recipient's InterchainSecurityModule instruction. +// N+1. [executable] SPL noop +// N+2. [executable] ISM +// N+2..M. [??] Accounts required to invoke the ISM's Verify instruction. +// M+1. [executable] Recipient program. +// M+2..K. [??] Accounts required to invoke the recipient's Handle instruction. +fn inbox_process( + program_id: &Pubkey, + accounts: &[AccountInfo], + process: InboxProcess, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter().peekable(); + + // Decode the message bytes. + let message = HyperlaneMessage::read_from(&mut std::io::Cursor::new(&process.message)) + .map_err(|_| ProgramError::from(Error::DecodeError))?; + let message_id = message.id(); + + // Require the message version to match what we expect. + if message.version != VERSION { + return Err(ProgramError::from(Error::UnsupportedMessageVersion)); + } + let recipient_program_id = Pubkey::new_from_array(message.recipient.0); + + // Account 0: Payer account. + let payer_info = next_account_info(accounts_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 1: The system program. + let system_program_info = next_account_info(accounts_iter)?; + if system_program_info.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 2: Inbox PDA. + let inbox_info = next_account_info(accounts_iter)?; + // By holding a refmut of the Inbox data, we effectively have a reentrancy guard + // that prevents any of the CPIs performed by this function to call back into + // this function. + let (mut inbox, mut inbox_data_refmut) = + Inbox::verify_account_and_fetch_inner_with_data_refmut(program_id, inbox_info)?; + + // Verify the message's destination matches the inbox's local domain. + if inbox.local_domain != message.destination { + return Err(Error::DestinationDomainNotLocalDomain.into()); + } + + // Account 3: Process authority account that is specific to the + // message recipient. + let process_authority_info = next_account_info(accounts_iter)?; + // Future versions / changes should consider requiring the process authority to + // store its bump seed as account data. + let (expected_process_authority_key, expected_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(&recipient_program_id), + program_id, + ); + if process_authority_info.key != &expected_process_authority_key { + return Err(ProgramError::InvalidArgument); + } + + // Account 4: Processed message PDA. + let processed_message_account_info = next_account_info(accounts_iter)?; + let (expected_processed_message_key, expected_processed_message_bump) = + Pubkey::find_program_address(mailbox_processed_message_pda_seeds!(message_id), program_id); + if processed_message_account_info.key != &expected_processed_message_key { + return Err(ProgramError::InvalidArgument); + } + // If the processed message account already exists, then the message + // has been processed already. + if verify_account_uninitialized(processed_message_account_info).is_err() { + return Err(Error::MessageAlreadyProcessed.into()); + } + + let spl_noop_id = spl_noop::id(); + + // Accounts 5..N: the accounts required for getting the ISM the recipient wants to use. + let mut get_ism_infos = vec![]; + let mut get_ism_account_metas = vec![]; + loop { + // We expect there to always be a new account as we loop through + // and use the SPL noop account ID as a marker for the end of the + // accounts required for getting the ISM. + let next_info = accounts_iter.peek().ok_or(ProgramError::InvalidArgument)?; + if next_info.key == &spl_noop_id { + break; + } + + let account_info = next_account_info(accounts_iter)?; + let meta = AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + }; + + get_ism_infos.push(account_info.clone()); + get_ism_account_metas.push(meta); + } + + // Call into the recipient program to get the ISM to use. + let ism = get_recipient_ism( + &recipient_program_id, + get_ism_infos, + get_ism_account_metas, + inbox.default_ism, + )?; + + // Account N: SPL Noop program. + let spl_noop_info = next_account_info(accounts_iter)?; + if spl_noop_info.key != &spl_noop_id { + return Err(ProgramError::InvalidArgument); + } + + #[cfg(not(feature = "no-spl-noop"))] + if !spl_noop_info.executable { + return Err(ProgramError::InvalidArgument); + } + + // Account N+1: The ISM. + let ism_info = next_account_info(accounts_iter)?; + if &ism != ism_info.key { + return Err(ProgramError::InvalidArgument); + } + + // Account N+2..M: The accounts required for ISM verification. + let mut ism_verify_infos = vec![]; + let mut ism_verify_account_metas = vec![]; + loop { + // We expect there to always be a new account as we loop through + // and use the recipient program ID as a marker for the end of the + // accounts required for ISM verification. + let next_info = accounts_iter.peek().ok_or(ProgramError::InvalidArgument)?; + if next_info.key == &recipient_program_id { + break; + } + + let account_info = next_account_info(accounts_iter)?; + let meta = AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + }; + ism_verify_infos.push(account_info.clone()); + ism_verify_account_metas.push(meta); + } + + // Account M+1: The recipient program. + let recipient_info = next_account_info(accounts_iter)?; + if &recipient_program_id != recipient_info.key { + return Err(ProgramError::InvalidArgument); + } + if !recipient_info.executable { + return Err(ProgramError::InvalidAccountData); + } + + // Account M+2..K: The accounts required for the recipient program handler. + let mut recipient_infos = vec![process_authority_info.clone()]; + let mut recipient_account_metas = vec![AccountMeta { + pubkey: *process_authority_info.key, + is_signer: true, + is_writable: false, + }]; + for account_info in accounts_iter { + recipient_infos.push(account_info.clone()); + recipient_account_metas.push(AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + }); + } + + // Call into the ISM to verify the message. + let verify_instruction = InterchainSecurityModuleInstruction::Verify(VerifyInstruction { + metadata: process.metadata, + message: process.message, + }); + let verify = + Instruction::new_with_bytes(ism, &verify_instruction.encode()?, ism_verify_account_metas); + invoke(&verify, &ism_verify_infos)?; + + // Mark the message as delivered by creating the processed message account. + let processed_message_account_data = ProcessedMessageAccount::from(ProcessedMessage::new( + inbox.processed_count, + message_id, + Clock::get()?.slot, + )); + let processed_message_account_data_size = processed_message_account_data.size(); + create_pda_account( + payer_info, + &Rent::get()?, + processed_message_account_data_size, + program_id, + system_program_info, + processed_message_account_info, + mailbox_processed_message_pda_seeds!(message_id, expected_processed_message_bump), + )?; + // Write the processed message data to the processed message account. + processed_message_account_data.store(processed_message_account_info, false)?; + + // Increment the processed count and store the updated Inbox account. + inbox.processed_count += 1; + InboxAccount::from(inbox) + .store_in_slice(&mut inbox_data_refmut) + .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; + + // Now call into the recipient program with the verified message! + let handle_intruction = Instruction::new_with_bytes( + recipient_program_id, + &MessageRecipientInstruction::Handle(HandleInstruction::new( + message.origin, + message.sender, + message.body, + )) + .encode()?, + recipient_account_metas, + ); + invoke_signed( + &handle_intruction, + &recipient_infos, + &[mailbox_process_authority_pda_seeds!( + &recipient_program_id, + expected_process_authority_bump + )], + )?; + + #[cfg(not(feature = "no-spl-noop"))] + { + let noop_cpi_log = Instruction { + program_id: spl_noop::id(), + accounts: vec![], + data: format!("Hyperlane inbox: {:?}", message_id).into_bytes(), + }; + invoke(&noop_cpi_log, &[])?; + } + + msg!("Hyperlane inbox processed message {:?}", message_id); + + Ok(()) +} + +/// Gets the ISM to use for a recipient program and sets it as return data. +/// +/// Accounts: +/// 0. [] - The Inbox PDA. +/// 1. [] - The recipient program. +/// 2..N. [??] - The accounts required to make the CPI into the recipient program. +/// These can be retrieved from the recipient using the +/// `MessageRecipientInstruction::InterchainSecurityModuleAccountMetas` instruction. +fn inbox_get_recipient_ism( + program_id: &Pubkey, + accounts: &[AccountInfo], + recipient: Pubkey, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Inbox PDA. + let inbox_info = next_account_info(accounts_iter)?; + let inbox = Inbox::verify_account_and_fetch_inner(program_id, inbox_info)?; + + // Account 1: The recipient program. + let recipient_info = next_account_info(accounts_iter)?; + if &recipient != recipient_info.key || !recipient_info.executable { + return Err(ProgramError::InvalidArgument); + } + + // Account 2..N: The accounts required to make the CPI into the recipient program. + let mut account_infos = vec![]; + let mut account_metas = vec![]; + for account_info in accounts_iter { + account_infos.push(account_info.clone()); + account_metas.push(AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + }); + } + + let ism = get_recipient_ism(&recipient, account_infos, account_metas, inbox.default_ism)?; + + // Return the borsh serialized ISM pubkey. + set_return_data( + &SimulationReturnData::new(ism) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + + Ok(()) +} + +/// Get the ISM to use for a recipient program. +/// +/// Expects `account_infos` and `account_metas` to be those required +/// by the recipient program's InterchainSecurityModule instruction. +fn get_recipient_ism( + recipient_program_id: &Pubkey, + account_infos: Vec, + account_metas: Vec, + default_ism: Pubkey, +) -> Result { + let get_ism_instruction = Instruction::new_with_bytes( + *recipient_program_id, + &MessageRecipientInstruction::InterchainSecurityModule.encode()?, + account_metas, + ); + invoke(&get_ism_instruction, &account_infos)?; + + // Default to the default ISM if there is no return data or Option::None was returned. + let ism = if let Some((returning_program_id, returned_data)) = get_return_data() { + if &returning_program_id != recipient_program_id { + return Err(ProgramError::InvalidAccountData); + } + // It's possible for the Some above to match but there is no return data. + // We just want to default to the default ISM in that case. + if returned_data.is_empty() { + default_ism + } else { + // If the recipient program returned data, use that as the ISM. + // If they returned an encoded Option::::None, then use + // the default ISM. + Option::::try_from_slice(&returned_data[..]) + .map_err(|err| ProgramError::BorshIoError(err.to_string()))? + .unwrap_or(default_ism) + } + } else { + // If no return data, default to the default ISM. + default_ism + }; + + Ok(ism) +} + +/// Sets the default ISM. +/// +/// Accounts: +/// 0. [writeable] - The Inbox PDA account. +/// 1. [] - The Outbox PDA account. +/// 2. [signer] - The owner of the Mailbox. +fn inbox_set_default_ism( + program_id: &Pubkey, + accounts: &[AccountInfo], + ism: Pubkey, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Inbox PDA account. + let inbox_info = next_account_info(accounts_iter)?; + let mut inbox = Inbox::verify_account_and_fetch_inner(program_id, inbox_info)?; + + // Account 1: Outbox PDA account. + let outbox_info = next_account_info(accounts_iter)?; + let outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?; + + // Account 2: The owner of the Mailbox. + let owner_info = next_account_info(accounts_iter)?; + // Errors if the owner account isn't correct or isn't a signer. + outbox.ensure_owner_signer(owner_info)?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + // Set the new default ISM. + inbox.default_ism = ism; + // Store the updated inbox. + InboxAccount::from(inbox).store(inbox_info, false)?; + + Ok(()) +} + +/// Dispatches a message. +/// If the message sender is a program, the message sender signer *must* be +/// the PDA for the sending program with the seeds `mailbox_message_dispatch_authority_pda_seeds!()`. +/// in order for the sender field of the message to be set to the sending program +/// ID. Otherwise, the sender field of the message is set to the message sender signer. +/// +/// Sets the ID of the message as return data. +/// +/// Accounts: +/// 0. [writeable] Outbox PDA. +/// 1. [signer] Message sender signer. +/// 2. [executable] System program. +/// 3. [executable] SPL Noop program. +/// 4. [signer] Payer. +/// 5. [signer] Unique message account. +/// 6. [writeable] Dispatched message PDA. An empty message PDA relating to the seeds +/// `mailbox_dispatched_message_pda_seeds` where the message contents will be stored. +fn outbox_dispatch( + program_id: &Pubkey, + accounts: &[AccountInfo], + dispatch: OutboxDispatch, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Outbox PDA. + let outbox_info = next_account_info(accounts_iter)?; + let mut outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?; + + // Account 1: Message sender signer. + let sender_signer_info = next_account_info(accounts_iter)?; + if !sender_signer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + // If the sender signer key differs from the specified dispatch.sender, + // we need to confirm that the sender signer has the authority to sign + // on behalf of the dispatch.sender! + if *sender_signer_info.key != dispatch.sender { + // Future versions / changes should consider requiring the dispatch authority to + // store its bump seed as account data. + let (expected_signer_key, _expected_signer_bump) = Pubkey::find_program_address( + mailbox_message_dispatch_authority_pda_seeds!(), + &dispatch.sender, + ); + // If the sender_signer isn't the expected dispatch authority for the + // specified dispatch.sender, fail. + if expected_signer_key != *sender_signer_info.key { + return Err(ProgramError::MissingRequiredSignature); + } + } + + // Account 2: System program. + let system_program_info = next_account_info(accounts_iter)?; + if system_program_info.key != &solana_program::system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 3: SPL Noop program. + let spl_noop_info = next_account_info(accounts_iter)?; + if spl_noop_info.key != &spl_noop::id() { + return Err(ProgramError::InvalidArgument); + } + + #[cfg(not(feature = "no-spl-noop"))] + if !spl_noop_info.executable { + return Err(ProgramError::InvalidArgument); + } + + // Account 4: Payer. + let payer_info = next_account_info(accounts_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 5: Unique message account. + // Uniqueness is enforced by making sure the message storage PDA based on + // this unique message account is empty, which is done next. + let unique_message_account_info = next_account_info(accounts_iter)?; + if !unique_message_account_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 6: Dispatched message PDA. + let dispatched_message_account_info = next_account_info(accounts_iter)?; + let (dispatched_message_key, dispatched_message_bump) = Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(unique_message_account_info.key), + program_id, + ); + if dispatched_message_key != *dispatched_message_account_info.key { + return Err(ProgramError::InvalidArgument); + } + // Make sure an account can't be written to that already exists. + verify_account_uninitialized(dispatched_message_account_info)?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + if dispatch.message_body.len() > MAX_MESSAGE_BODY_BYTES { + return Err(ProgramError::from(Error::MaxMessageSizeExceeded)); + } + + let count = outbox + .tree + .count() + .try_into() + .expect("Too many messages in outbox tree"); + let message = HyperlaneMessage { + version: VERSION, + nonce: count, + origin: outbox.local_domain, + sender: H256(dispatch.sender.to_bytes()), + destination: dispatch.destination_domain, + recipient: dispatch.recipient, + body: dispatch.message_body, + }; + let mut encoded_message = vec![]; + message + .write_to(&mut encoded_message) + .map_err(|_| ProgramError::from(Error::EncodeError))?; + + let id = message.id(); + outbox.tree.ingest(id); + + // Create the dispatched message PDA. + let dispatched_message_account = DispatchedMessageAccount::from(DispatchedMessage::new( + message.nonce, + Clock::get()?.slot, + *unique_message_account_info.key, + encoded_message, + )); + let dispatched_message_account_size: usize = dispatched_message_account.size(); + create_pda_account( + payer_info, + &Rent::get()?, + dispatched_message_account_size, + program_id, + system_program_info, + dispatched_message_account_info, + mailbox_dispatched_message_pda_seeds!( + unique_message_account_info.key, + dispatched_message_bump + ), + )?; + dispatched_message_account.store(dispatched_message_account_info, false)?; + + // Log the message using the SPL Noop program. + #[cfg(not(feature = "no-spl-noop"))] + { + let noop_cpi_log = Instruction { + program_id: *spl_noop_info.key, + accounts: vec![], + data: dispatched_message_account_info.data.borrow().to_vec(), + }; + invoke(&noop_cpi_log, &[])?; + } + + msg!( + "Dispatched message to {}, ID {:?}", + dispatch.destination_domain, + id + ); + + // Store the Outbox with the new updates. + OutboxAccount::from(outbox).store(outbox_info, true)?; + + set_return_data(id.as_ref()); + Ok(()) +} + +/// Gets the number of dispatched messages as little endian encoded return data. +/// +/// Accounts: +/// 0. [] Outbox PDA account. +fn outbox_get_count(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Outbox PDA. + let outbox_info = next_account_info(accounts_iter)?; + let outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + let count: u32 = outbox + .tree + .count() + .try_into() + .expect("Too many messages in outbox tree"); + set_return_data(&count.to_le_bytes()); + Ok(()) +} + +/// Gets the latest checkpoint as return data. +/// +/// Accounts: +/// 0. [] Outbox PDA account. +fn outbox_get_latest_checkpoint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let outbox_info = next_account_info(accounts_iter)?; + let outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + let root = outbox.tree.root(); + let count: u32 = outbox + .tree + .count() + .try_into() + .expect("Too many messages in outbox tree"); + + let mut ret_buf = [0; 36]; + ret_buf[0..31].copy_from_slice(root.as_ref()); + ret_buf[32..].copy_from_slice(&count.to_le_bytes()); + set_return_data(&ret_buf); + Ok(()) +} + +/// Gets the root as return data. +/// +/// Accounts: +/// 0. [] Outbox PDA account. +fn outbox_get_root(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Outbox PDA. + let outbox_info = next_account_info(accounts_iter)?; + let outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?; + + if accounts_iter.next().is_some() { + return Err(ProgramError::from(Error::ExtraneousAccount)); + } + + let root = outbox.tree.root(); + set_return_data(root.as_ref()); + Ok(()) +} + +/// Gets the owner as return data. +/// +/// Accounts: +/// 0. `[]` The Outbox PDA account. +fn get_owner(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Outbox PDA. + let outbox_info = next_account_info(accounts_iter)?; + let outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?; + + set_return_data( + &outbox + .owner + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?, + ); + Ok(()) +} + +/// Transfers ownership. +/// +/// Accounts: +/// 0. `[]` The Outbox PDA account. +/// 1. `[signer]` The current owner. +fn transfer_ownership( + program_id: &Pubkey, + accounts: &[AccountInfo], + new_owner: Option, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Outbox PDA. + let outbox_info = next_account_info(accounts_iter)?; + let mut outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?; + + // Account 1: Current owner. + let owner_info = next_account_info(accounts_iter)?; + // Errors if the owner_account is not the actual owner or is not a signer. + outbox.transfer_ownership(owner_info, new_owner)?; + + // Store the updated outbox. + OutboxAccount::from(outbox).store(outbox_info, false)?; + + Ok(()) +} diff --git a/rust/sealevel/programs/test-send-receiver/Cargo.toml b/rust/sealevel/programs/test-send-receiver/Cargo.toml new file mode 100644 index 0000000000..3e0daf980e --- /dev/null +++ b/rust/sealevel/programs/test-send-receiver/Cargo.toml @@ -0,0 +1,26 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-test-send-receiver" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-client = ["dep:solana-program-test", "dep:solana-sdk", "dep:hyperlane-test-utils", "dep:spl-noop"] + +[dependencies] +borsh.workspace = true +solana-program-test = { workspace = true, optional = true } +solana-program.workspace = true +solana-sdk = { workspace = true, optional = true } +spl-noop = { workspace = true, optional = true } + +account-utils = { path = "../../libraries/account-utils" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" } +hyperlane-test-utils = { path = "../../libraries/test-utils", optional = true } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/test-send-receiver/src/lib.rs b/rust/sealevel/programs/test-send-receiver/src/lib.rs new file mode 100644 index 0000000000..568ef4b4fa --- /dev/null +++ b/rust/sealevel/programs/test-send-receiver/src/lib.rs @@ -0,0 +1,11 @@ +//! A test program that sends and receives messages. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod program; +#[cfg(feature = "test-client")] +pub mod test_client; + +solana_program::declare_id!("FZ8hyduJy4GQAfBu9zEiuQtk429Gjc6inwHgEW5MvsEm"); diff --git a/rust/sealevel/programs/test-send-receiver/src/program.rs b/rust/sealevel/programs/test-send-receiver/src/program.rs new file mode 100644 index 0000000000..66e7c86c61 --- /dev/null +++ b/rust/sealevel/programs/test-send-receiver/src/program.rs @@ -0,0 +1,468 @@ +//! Test message sender and receiver. +//! **NOT INTENDED FOR USE IN PRODUCTION** + +use account_utils::{create_pda_account, AccountData, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_sealevel_mailbox::{ + instruction::{InboxProcess, Instruction as MailboxInstruction, OutboxDispatch}, + mailbox_message_dispatch_authority_pda_seeds, + mailbox_process_authority_pda_seeds, + // mailbox_inbox_pda_seeds +}; +use hyperlane_sealevel_message_recipient_interface::{ + HandleInstruction, MessageRecipientInstruction, +}; +use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, + msg, + program::{invoke, invoke_signed, set_return_data}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_program, + sysvar::Sysvar, +}; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// Custom errors for the program. +pub enum TestSendReceiverError { + /// The handle instruction failed. + HandleFailed = 6942069, +} + +/// The mode for returning data when getting the ISM, intended to test +/// that the Mailbox can handle different ISM getter return data. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum IsmReturnDataMode { + /// Encodes the ISM as an Option. + EncodeOption, + /// Returns no data. + ReturnNothing, + /// Returns malformatted data. + ReturnMalformmatedData, +} + +impl Default for IsmReturnDataMode { + fn default() -> Self { + Self::EncodeOption + } +} + +/// The storage account. +pub type TestSendReceiverStorageAccount = AccountData; + +/// The storage account's data. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default)] +pub struct TestSendReceiverStorage { + /// The mailbox. + pub mailbox: Pubkey, + /// The ISM. + pub ism: Option, + /// The mode for returning data from the ISM. + pub ism_return_data_mode: IsmReturnDataMode, + /// Modes of handling a message. + pub handle_mode: HandleMode, +} + +impl SizedData for TestSendReceiverStorage { + fn size(&self) -> usize { + // 32 for mailbox + // 1 + 32 for ism + // 1 for ism_return_data_mode + // 1 for handle_mode + 32 + 1 + 32 + 1 + 1 + } +} + +/// The PDA seeds relating to storage +#[macro_export] +macro_rules! test_send_receiver_storage_pda_seeds { + () => {{ + &[b"test_send_receiver", b"-", b"storage"] + }}; + + ($bump_seed:expr) => {{ + &[b"test_send_receiver", b"-", b"storage", &[$bump_seed]] + }}; +} + +/// Modes of handling a message. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum HandleMode { + /// Handling a message is successful. + Success, + /// Handling a message fails. + Fail, + /// Handling a message reenters the Mailbox with the process instruction. + ReenterProcess, +} + +impl Default for HandleMode { + fn default() -> Self { + Self::Success + } +} + +/// Instructions for the program. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum TestSendReceiverInstruction { + /// Initializes the program. + Init(Pubkey), + /// Dispatches a message using the dispatch authority. + Dispatch(OutboxDispatch), + /// Sets the ISM. + SetInterchainSecurityModule(Option, IsmReturnDataMode), + /// Sets the behavior when handling a message. + SetHandleMode(HandleMode), +} + +/// The program's entrypoint. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if let Ok(recipient_instruction) = MessageRecipientInstruction::decode(instruction_data) { + return match recipient_instruction { + MessageRecipientInstruction::InterchainSecurityModule => { + get_interchain_security_module(program_id, accounts) + } + MessageRecipientInstruction::InterchainSecurityModuleAccountMetas => { + set_account_meta_return_data(program_id) + } + MessageRecipientInstruction::Handle(instruction) => { + handle(program_id, accounts, instruction) + } + MessageRecipientInstruction::HandleAccountMetas(_) => { + set_account_meta_return_data(program_id) + } + }; + } + + let instruction = TestSendReceiverInstruction::try_from_slice(instruction_data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + match instruction { + TestSendReceiverInstruction::Init(mailbox) => init(program_id, accounts, mailbox), + TestSendReceiverInstruction::Dispatch(outbox_dispatch) => { + dispatch(program_id, accounts, outbox_dispatch) + } + TestSendReceiverInstruction::SetInterchainSecurityModule(ism, ism_return_data_mode) => { + set_interchain_security_module(program_id, accounts, ism, ism_return_data_mode) + } + TestSendReceiverInstruction::SetHandleMode(mode) => { + set_handle_mode(program_id, accounts, mode) + } + } +} + +/// Creates the storage PDA. +/// +/// Accounts: +/// 0. [executable] System program. +/// 1. [signer] Payer. +/// 2. [writeable] Storage PDA. +fn init(program_id: &Pubkey, accounts: &[AccountInfo], mailbox: Pubkey) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: System program. + let system_program_info = next_account_info(accounts_iter)?; + if system_program_info.key != &system_program::id() { + return Err(ProgramError::InvalidArgument); + } + + // Account 1: Payer. + let payer_info = next_account_info(accounts_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 2: Storage PDA. + let storage_info = next_account_info(accounts_iter)?; + let (storage_pda_key, storage_pda_bump_seed) = + Pubkey::find_program_address(test_send_receiver_storage_pda_seeds!(), program_id); + if storage_info.key != &storage_pda_key { + return Err(ProgramError::InvalidArgument); + } + + let storage_account = TestSendReceiverStorageAccount::from(TestSendReceiverStorage { + mailbox, + ism: None, + ism_return_data_mode: IsmReturnDataMode::EncodeOption, + handle_mode: HandleMode::Success, + }); + create_pda_account( + payer_info, + &Rent::get()?, + storage_account.size(), + program_id, + system_program_info, + storage_info, + test_send_receiver_storage_pda_seeds!(storage_pda_bump_seed), + )?; + // Store it + storage_account.store(storage_info, false)?; + + Ok(()) +} + +/// Dispatches a message using the dispatch authority. +/// +/// Accounts: +/// 0. [executable] The Mailbox program. +/// And now the accounts expected by the Mailbox's OutboxDispatch instruction: +/// 2. [writeable] Outbox PDA. +/// 3. [] This program's dispatch authority. +/// 4. [executable] System program. +/// 5. [executable] SPL Noop program. +/// 6. [signer] Payer. +/// 7. [signer] Unique message account. +/// 8. [writeable] Dispatched message PDA. An empty message PDA relating to the seeds +/// `mailbox_dispatched_message_pda_seeds` where the message contents will be stored. +fn dispatch( + program_id: &Pubkey, + accounts: &[AccountInfo], + outbox_dispatch: OutboxDispatch, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Mailbox program. + let mailbox_info = next_account_info(accounts_iter)?; + + // Account 1: Outbox PDA. + let mailbox_outbox_info = next_account_info(accounts_iter)?; + + // Account 2: Dispatch authority. + let dispatch_authority_info = next_account_info(accounts_iter)?; + let (expected_dispatch_authority_key, expected_dispatch_authority_bump) = + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), program_id); + if dispatch_authority_info.key != &expected_dispatch_authority_key { + return Err(ProgramError::InvalidArgument); + } + + // Account 3: System program. + let system_program_info = next_account_info(accounts_iter)?; + + // Account 4: SPL Noop program. + let spl_noop_info = next_account_info(accounts_iter)?; + + // Account 5: Payer. + let payer_info = next_account_info(accounts_iter)?; + + // Account 6: Unique message account. + let unique_message_account_info = next_account_info(accounts_iter)?; + + // Account 7: Dispatched message PDA. + let dispatched_message_info = next_account_info(accounts_iter)?; + + // Dispatch + let instruction = Instruction { + program_id: *mailbox_info.key, + data: MailboxInstruction::OutboxDispatch(outbox_dispatch).into_instruction_data()?, + accounts: vec![ + AccountMeta::new(*mailbox_outbox_info.key, false), + AccountMeta::new_readonly(*dispatch_authority_info.key, true), + AccountMeta::new_readonly(*system_program_info.key, false), + AccountMeta::new_readonly(*spl_noop_info.key, false), + AccountMeta::new(*payer_info.key, true), + AccountMeta::new_readonly(*unique_message_account_info.key, true), + AccountMeta::new(*dispatched_message_info.key, false), + ], + }; + invoke_signed( + &instruction, + &[ + mailbox_outbox_info.clone(), + dispatch_authority_info.clone(), + system_program_info.clone(), + spl_noop_info.clone(), + payer_info.clone(), + unique_message_account_info.clone(), + dispatched_message_info.clone(), + ], + &[mailbox_message_dispatch_authority_pda_seeds!( + expected_dispatch_authority_bump + )], + ) +} + +/// Handles a message. +/// +/// Accounts: +/// 0. [writeable] Process authority specific to this program. +/// 1. [] Storage PDA account. +pub fn handle( + program_id: &Pubkey, + accounts: &[AccountInfo], + handle: HandleInstruction, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Process authority specific to this program. + let process_authority = next_account_info(accounts_iter)?; + + // Account 1: Storage PDA account. + let storage_info = next_account_info(accounts_iter)?; + let storage = + TestSendReceiverStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + // Verify the process authority + let (expected_process_authority_key, _expected_process_authority_bump) = + Pubkey::find_program_address( + mailbox_process_authority_pda_seeds!(program_id), + &storage.mailbox, + ); + if process_authority.key != &expected_process_authority_key { + return Err(ProgramError::InvalidArgument); + } + if !process_authority.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + match storage.handle_mode { + HandleMode::Success => { + // Nothing! + } + HandleMode::Fail => { + return Err(ProgramError::Custom( + TestSendReceiverError::HandleFailed as u32, + )); + } + HandleMode::ReenterProcess => { + // Reenter the Mailbox with the process instruction + + let mut reenter_accounts = vec![]; + let mut reenter_account_metas = vec![]; + + for account in accounts_iter { + reenter_accounts.push(account.clone()); + reenter_account_metas.push(AccountMeta { + pubkey: *account.key, + is_signer: account.is_signer, + is_writable: account.is_writable, + }); + } + + let instruction = Instruction { + program_id: storage.mailbox, + data: MailboxInstruction::InboxProcess(InboxProcess { + metadata: vec![], + // The encoded HyperlaneMessage to reenter with is + // expected to be in the body of the message currently + // being processed. + message: handle.message.clone(), + }) + .into_instruction_data()?, + accounts: reenter_account_metas, + }; + invoke(&instruction, reenter_accounts.as_slice())?; + } + } + + msg!("hyperlane-sealevel-test-send-receiver: {:?}", handle); + + Ok(()) +} + +/// Accounts: +/// 0. [writeable] Storage PDA account. +fn set_interchain_security_module( + _program_id: &Pubkey, + accounts: &[AccountInfo], + ism: Option, + ism_return_data_mode: IsmReturnDataMode, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Storage PDA account. + // Not bothering with validity checks because this is a test program + let storage_info = next_account_info(accounts_iter)?; + let mut storage = + TestSendReceiverStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + storage.ism = ism; + storage.ism_return_data_mode = ism_return_data_mode; + + // Store it + TestSendReceiverStorageAccount::from(storage).store(storage_info, false)?; + + Ok(()) +} + +/// Accounts: +/// 0. [] Storage PDA account. +fn get_interchain_security_module(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Storage PDA account. + // Not bothering with validity checks because this is a test program + let storage_info = next_account_info(accounts_iter)?; + let storage = TestSendReceiverStorageAccount::fetch(&mut &storage_info.data.borrow()[..]) + .unwrap() + .into_inner(); + + match storage.ism_return_data_mode { + IsmReturnDataMode::EncodeOption => { + set_return_data( + &storage + .ism + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?[..], + ); + } + IsmReturnDataMode::ReturnNothing => { + // Nothing! + } + IsmReturnDataMode::ReturnMalformmatedData => { + set_return_data(&[0x00, 0x01, 0x02, 0x03]); + } + } + + Ok(()) +} + +/// Accounts: +/// 0. [writeable] Storage PDA account. +fn set_handle_mode( + _program_id: &Pubkey, + accounts: &[AccountInfo], + mode: HandleMode, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + // Account 0: Storage PDA account. + // Not bothering with validity checks because this is a test program + let storage_info = next_account_info(accounts_iter)?; + let mut storage = + TestSendReceiverStorageAccount::fetch(&mut &storage_info.data.borrow()[..])?.into_inner(); + + storage.handle_mode = mode; + + // Store it + TestSendReceiverStorageAccount::from(storage).store(storage_info, false)?; + + Ok(()) +} + +fn set_account_meta_return_data(program_id: &Pubkey) -> ProgramResult { + let (storage_pda_key, _storage_pda_bump) = + Pubkey::find_program_address(test_send_receiver_storage_pda_seeds!(), program_id); + + let account_metas: Vec = + vec![AccountMeta::new_readonly(storage_pda_key, false).into()]; + + // Wrap it in the SimulationReturnData because serialized account_metas + // may end with zero byte(s), which are incorrectly truncated as + // simulated transaction return data. + // See `SimulationReturnData` for details. + let bytes = SimulationReturnData::new(account_metas) + .try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string()))?; + set_return_data(&bytes[..]); + + Ok(()) +} diff --git a/rust/sealevel/programs/test-send-receiver/src/test_client.rs b/rust/sealevel/programs/test-send-receiver/src/test_client.rs new file mode 100644 index 0000000000..3096c10f7b --- /dev/null +++ b/rust/sealevel/programs/test-send-receiver/src/test_client.rs @@ -0,0 +1,210 @@ +//! Test client for the TestSendReceiver program. + +use borsh::BorshSerialize; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; +use solana_program_test::*; +use solana_sdk::{signature::Signature, signature::Signer, signer::keypair::Keypair}; + +use hyperlane_sealevel_mailbox::{ + instruction::OutboxDispatch, mailbox_dispatched_message_pda_seeds, + mailbox_message_dispatch_authority_pda_seeds, +}; +use hyperlane_test_utils::{mailbox_id, process_instruction, MailboxAccounts}; + +use crate::{ + id, + program::{HandleMode, IsmReturnDataMode, TestSendReceiverInstruction}, + test_send_receiver_storage_pda_seeds, +}; + +/// Test client for the TestSendReceiver program. +pub struct TestSendReceiverTestClient { + banks_client: BanksClient, + payer: Keypair, +} + +impl TestSendReceiverTestClient { + /// Creates a new `TestSendReceiverTestClient`. + pub fn new(banks_client: BanksClient, payer: Keypair) -> Self { + Self { + banks_client, + payer, + } + } + + /// Initializes the TestSendReceiver program. + pub async fn init(&mut self) -> Result<(), BanksClientError> { + let program_id = id(); + + let payer_pubkey = self.payer.pubkey(); + + let instruction = Instruction { + program_id, + data: TestSendReceiverInstruction::Init(mailbox_id()) + .try_to_vec() + .unwrap(), + accounts: vec![ + // 0. [executable] System program. + // 1. [signer] Payer. + // 2. [writeable] Storage PDA. + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(payer_pubkey, true), + AccountMeta::new(Self::get_storage_pda_key(), false), + ], + }; + + process_instruction( + &mut self.banks_client, + instruction, + &self.payer, + &[&self.payer], + ) + .await?; + + Ok(()) + } + + /// Sets the ISM. + pub async fn set_ism( + &mut self, + ism: Option, + ism_return_data_mode: IsmReturnDataMode, + ) -> Result<(), BanksClientError> { + let program_id = id(); + + let instruction = Instruction { + program_id, + data: TestSendReceiverInstruction::SetInterchainSecurityModule( + ism, + ism_return_data_mode, + ) + .try_to_vec() + .unwrap(), + accounts: vec![ + // 0. [writeable] Storage PDA. + AccountMeta::new(Self::get_storage_pda_key(), false), + ], + }; + + process_instruction( + &mut self.banks_client, + instruction, + &self.payer, + &[&self.payer], + ) + .await?; + + Ok(()) + } + + /// Sets the behavior when handling a message. + pub async fn set_handle_mode(&mut self, mode: HandleMode) -> Result<(), BanksClientError> { + let program_id = id(); + + let (storage_pda_key, _storage_pda_bump) = + Pubkey::find_program_address(test_send_receiver_storage_pda_seeds!(), &program_id); + + let instruction = Instruction { + program_id, + data: TestSendReceiverInstruction::SetHandleMode(mode) + .try_to_vec() + .unwrap(), + accounts: vec![ + // 0. [writeable] Storage PDA. + AccountMeta::new(storage_pda_key, false), + ], + }; + + process_instruction( + &mut self.banks_client, + instruction, + &self.payer, + &[&self.payer], + ) + .await?; + + Ok(()) + } + + /// Dispatches a message. + pub async fn dispatch( + &mut self, + mailbox_accounts: &MailboxAccounts, + outbox_dispatch: OutboxDispatch, + ) -> Result<(Signature, Keypair, Pubkey), BanksClientError> { + let program_id = id(); + + let unique_message_account_keypair = Keypair::new(); + + let (dispatch_authority_key, _expected_dispatch_authority_bump) = + Self::get_dispatch_authority(); + + let (dispatched_message_account_key, _dispatched_message_bump) = + Pubkey::find_program_address( + mailbox_dispatched_message_pda_seeds!(&unique_message_account_keypair.pubkey()), + &mailbox_accounts.program, + ); + + let instruction = Instruction { + program_id, + data: TestSendReceiverInstruction::Dispatch(outbox_dispatch) + .try_to_vec() + .unwrap(), + accounts: vec![ + // 0. [executable] The Mailbox program. + // And now the accounts expected by the Mailbox's OutboxDispatch instruction: + // 1. [writeable] Outbox PDA. + // 2. [] This program's dispatch authority. + // 3. [executable] System program. + // 4. [executable] SPL Noop program. + // 5. [signer] Payer. + // 6. [signer] Unique message account. + // 7. [writeable] Dispatched message PDA. An empty message PDA relating to the seeds + // `mailbox_dispatched_message_pda_seeds` where the message contents will be stored. + AccountMeta::new_readonly(mailbox_accounts.program, false), + AccountMeta::new(mailbox_accounts.outbox, false), + AccountMeta::new_readonly(dispatch_authority_key, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_noop::id(), false), + AccountMeta::new(self.payer.pubkey(), true), + AccountMeta::new(unique_message_account_keypair.pubkey(), true), + AccountMeta::new(dispatched_message_account_key, false), + ], + }; + + let tx_signature = process_instruction( + &mut self.banks_client, + instruction, + &self.payer, + &[&self.payer, &unique_message_account_keypair], + ) + .await?; + + Ok(( + tx_signature, + unique_message_account_keypair, + dispatched_message_account_key, + )) + } + + fn get_storage_pda_key() -> Pubkey { + let program_id = id(); + let (storage_pda_key, _storage_pda_bump) = + Pubkey::find_program_address(test_send_receiver_storage_pda_seeds!(), &program_id); + storage_pda_key + } + + fn get_dispatch_authority() -> (Pubkey, u8) { + let program_id = id(); + Pubkey::find_program_address(mailbox_message_dispatch_authority_pda_seeds!(), &program_id) + } + + /// Returns the program ID. + pub fn id(&self) -> Pubkey { + id() + } +} diff --git a/rust/sealevel/programs/validator-announce/Cargo.toml b/rust/sealevel/programs/validator-announce/Cargo.toml new file mode 100644 index 0000000000..6372f5f0a8 --- /dev/null +++ b/rust/sealevel/programs/validator-announce/Cargo.toml @@ -0,0 +1,28 @@ +cargo-features = ["workspace-inheritance"] + +[package] +name = "hyperlane-sealevel-validator-announce" +version = "0.1.0" +edition = "2021" + +[features] +no-entrypoint = [] + +[dependencies] +borsh.workspace = true +solana-program.workspace = true +thiserror.workspace = true + +account-utils = { path = "../../libraries/account-utils" } +ecdsa-signature = { path = "../../libraries/ecdsa-signature" } +hyperlane-sealevel-mailbox = { path = "../mailbox", features = ["no-entrypoint"] } +hyperlane-core = { path = "../../../hyperlane-core" } +serializable-account-meta = { path = "../../libraries/serializable-account-meta" } + +[dev-dependencies] +hex.workspace = true +solana-program-test = "1.14.13" +solana-sdk.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/rust/sealevel/programs/validator-announce/src/accounts.rs b/rust/sealevel/programs/validator-announce/src/accounts.rs new file mode 100644 index 0000000000..bedac1c783 --- /dev/null +++ b/rust/sealevel/programs/validator-announce/src/accounts.rs @@ -0,0 +1,115 @@ +//! Accounts used by the ValidatorAnnounce program. + +use account_utils::{AccountData, SizedData}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::validator_announce_pda_seeds; + +/// An account that holds common data used for verifying validator announcements. +pub type ValidatorAnnounceAccount = AccountData; + +/// Data used for verifying validator announcements. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct ValidatorAnnounce { + /// The bump seed used to derive the PDA for this account. + pub bump_seed: u8, + /// The local Mailbox program. + pub mailbox: Pubkey, + /// The local domain. + pub local_domain: u32, +} + +impl SizedData for ValidatorAnnounce { + fn size(&self) -> usize { + 1 + 32 + 4 + } +} + +impl ValidatorAnnounce { + /// Verifies that the provided account info is the expected canonical ValidatorAnnounce PDA account. + pub fn verify_self_account_info( + &self, + program_id: &Pubkey, + maybe_self: &AccountInfo, + ) -> Result<(), ProgramError> { + let expected_key = Pubkey::create_program_address( + validator_announce_pda_seeds!(self.bump_seed), + program_id, + )?; + if maybe_self.owner != program_id || maybe_self.key != &expected_key { + return Err(ProgramError::InvalidAccountData); + } + Ok(()) + } +} + +/// An account that holds a validator's announced storage locations. +/// It is a PDA based off the validator's address, and can therefore +/// hold up to 10 KiB of data. +pub type ValidatorStorageLocationsAccount = AccountData; + +/// Storage locations for a validator. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct ValidatorStorageLocations { + /// The bump seed used to derive the PDA for this account. + pub bump_seed: u8, + /// Storage locations for this validator. + pub storage_locations: Vec, +} + +impl SizedData for ValidatorStorageLocations { + /// This is O(storage_locations.len()), and is therefore + /// not suggested to be used apart from the very first + /// announcement for a validator. + /// + /// For subsequent announcements for the validator, the + /// new size can be determined by adding size required by + /// the new storage location to the account data's existing size. + /// I.e. 4 (for the len of the location) + storage_location.len() + /// to the AccountInfo's data_len. + /// + /// This is tested in functional tests. + fn size(&self) -> usize { + // 1 byte bump seed + // 4 byte len of storage_locations + // for each storage location: + // 4 byte len of the storage location + // len bytes of the storage location + 1 + 4 + + self + .storage_locations + .iter() + .map(|s| ValidatorStorageLocations::size_increase_for_new_storage_location(s)) + .sum::() + } +} + +impl ValidatorStorageLocations { + /// An O(1) method for determining how much to increase the size of the + /// account data by when adding a new storage location. + /// Only intended to be used for subsequent announcements for a validator, + /// not the first announcement. + pub fn size_increase_for_new_storage_location(new_storage_location: &str) -> usize { + // The only difference in the account is the new storage location, which is Borsh-serialized + // as the u32 length of the string + the Vec it is serialized into. + // See https://borsh.io/ for details. + 4 + new_storage_location.len() + } +} + +/// An account whose presence is used as a replay protection mechanism. +/// Replay protection account addresses are PDAs based off the hash of +/// a validator's storage location. So these ultimately serve like a +/// HashMap to tell if a storage location has already been announced. +pub type ReplayProtectionAccount = AccountData; + +/// Empty account data used as a replay protection mechanism. +#[derive(BorshSerialize, BorshDeserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct ReplayProtection(pub ()); + +impl SizedData for ReplayProtection { + fn size(&self) -> usize { + 0 + } +} diff --git a/rust/sealevel/programs/validator-announce/src/error.rs b/rust/sealevel/programs/validator-announce/src/error.rs new file mode 100644 index 0000000000..8036090f9b --- /dev/null +++ b/rust/sealevel/programs/validator-announce/src/error.rs @@ -0,0 +1,21 @@ +//! Custom errors for the program. + +use solana_program::program_error::ProgramError; + +/// Custom errors for the program. +#[derive(Copy, Clone, Debug, Eq, thiserror::Error, PartialEq)] +#[repr(u32)] +pub enum Error { + /// An error occurred while verifying a signature. + #[error("Signature error")] + SignatureError = 1, + /// The recovered signer does not match the expected signer. + #[error("Signer mismatch")] + SignerMismatch = 2, +} + +impl From for ProgramError { + fn from(err: Error) -> Self { + ProgramError::Custom(err as u32) + } +} diff --git a/rust/sealevel/programs/validator-announce/src/instruction.rs b/rust/sealevel/programs/validator-announce/src/instruction.rs new file mode 100644 index 0000000000..6501bbd398 --- /dev/null +++ b/rust/sealevel/programs/validator-announce/src/instruction.rs @@ -0,0 +1,99 @@ +//! Instruction types for the ValidatorAnnounce program. + +use borsh::{BorshDeserialize, BorshSerialize}; +use hyperlane_core::H160; +use solana_program::{ + instruction::{AccountMeta, Instruction as SolanaInstruction}, + keccak, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::validator_announce_pda_seeds; + +/// Instructions for the ValidatorAnnounce program. +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] +pub enum Instruction { + /// Initializes the program. + Init(InitInstruction), + /// Announces a validator's storage location. + Announce(AnnounceInstruction), +} + +impl Instruction { + /// Deserializes an instruction from a slice. + pub fn from_instruction_data(data: &[u8]) -> Result { + Self::try_from_slice(data).map_err(|_| ProgramError::InvalidInstructionData) + } + + /// Serializes an instruction into a byte vector. + pub fn into_instruction_data(self) -> Result, ProgramError> { + self.try_to_vec() + .map_err(|err| ProgramError::BorshIoError(err.to_string())) + } +} + +/// Init data. +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] +pub struct InitInstruction { + /// The local Mailbox program. + pub mailbox: Pubkey, + /// The local domain. + pub local_domain: u32, +} + +/// Announcement data. +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] +pub struct AnnounceInstruction { + /// The validator's address. + pub validator: H160, + /// The validator's storage location. + pub storage_location: String, + /// The validator's signature attesting to the storage location. + pub signature: Vec, +} + +impl AnnounceInstruction { + /// Returns the replay ID for this announcement. + pub fn replay_id(&self) -> [u8; 32] { + let mut hasher = keccak::Hasher::default(); + hasher.hash(self.validator.as_bytes()); + hasher.hash(self.storage_location.as_bytes()); + hasher.result().to_bytes() + } +} + +/// Gets an instruction to initialize the program. +pub fn init_instruction( + program_id: Pubkey, + payer: Pubkey, + mailbox_program_id: Pubkey, + local_domain: u32, +) -> Result { + let (validator_announce_account, _validator_announce_bump) = + Pubkey::try_find_program_address(validator_announce_pda_seeds!(), &program_id) + .ok_or(ProgramError::InvalidSeeds)?; + + let ixn = Instruction::Init(InitInstruction { + mailbox: mailbox_program_id, + local_domain, + }); + + // Accounts: + // 0. [signer] The payer. + // 1. [executable] The system program. + // 2. [writable] The ValidatorAnnounce PDA account. + let accounts = vec![ + AccountMeta::new_readonly(payer, true), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + AccountMeta::new(validator_announce_account, false), + ]; + + let instruction = SolanaInstruction { + program_id, + data: ixn.into_instruction_data()?, + accounts, + }; + + Ok(instruction) +} diff --git a/rust/sealevel/programs/validator-announce/src/lib.rs b/rust/sealevel/programs/validator-announce/src/lib.rs new file mode 100644 index 0000000000..f34419de08 --- /dev/null +++ b/rust/sealevel/programs/validator-announce/src/lib.rs @@ -0,0 +1,11 @@ +//! A contract for publicly announcing validator storage locations. + +#![deny(warnings)] +#![deny(missing_docs)] +#![deny(unsafe_code)] + +pub mod accounts; +pub mod error; +pub mod instruction; +pub mod pda_seeds; +pub mod processor; diff --git a/rust/sealevel/programs/validator-announce/src/pda_seeds.rs b/rust/sealevel/programs/validator-announce/src/pda_seeds.rs new file mode 100644 index 0000000000..7e185bffbc --- /dev/null +++ b/rust/sealevel/programs/validator-announce/src/pda_seeds.rs @@ -0,0 +1,68 @@ +//! Program-specific PDA seeds. + +/// PDA seeds for the ValidatorAnnounce account. +#[macro_export] +macro_rules! validator_announce_pda_seeds { + () => {{ + &[b"hyperlane_validator_announce", b"-", b"validator_announce"] + }}; + + ($bump_seed:expr) => {{ + &[ + b"hyperlane_validator_announce", + b"-", + b"validator_announce", + &[$bump_seed], + ] + }}; +} + +/// PDA seeds for validator-specific ValidatorStorageLocations accounts. +#[macro_export] +macro_rules! validator_storage_locations_pda_seeds { + ($validator_h160:expr) => {{ + &[ + b"hyperlane_validator_announce", + b"-", + b"storage_locations", + b"-", + $validator_h160.as_bytes(), + ] + }}; + + ($validator_h160:expr, $bump_seed:expr) => {{ + &[ + b"hyperlane_validator_announce", + b"-", + b"storage_locations", + b"-", + $validator_h160.as_bytes(), + &[$bump_seed], + ] + }}; +} + +/// PDA seeds for replay protection accounts. +#[macro_export] +macro_rules! replay_protection_pda_seeds { + ($replay_id_bytes:expr) => {{ + &[ + b"hyperlane_validator_announce", + b"-", + b"replay_protection", + b"-", + &$replay_id_bytes[..], + ] + }}; + + ($replay_id_bytes:expr, $bump_seed:expr) => {{ + &[ + b"hyperlane_validator_announce", + b"-", + b"replay_protection", + b"-", + &$replay_id_bytes[..], + &[$bump_seed], + ] + }}; +} diff --git a/rust/sealevel/programs/validator-announce/src/processor.rs b/rust/sealevel/programs/validator-announce/src/processor.rs new file mode 100644 index 0000000000..253c58d569 --- /dev/null +++ b/rust/sealevel/programs/validator-announce/src/processor.rs @@ -0,0 +1,444 @@ +//! Program processor. + +use account_utils::{create_pda_account, SizedData}; +use ecdsa_signature::EcdsaSignature; +use hyperlane_core::{Announcement, Signable}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + program::invoke, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_instruction, + sysvar::Sysvar, +}; + +use crate::{ + accounts::{ + ReplayProtection, ReplayProtectionAccount, ValidatorAnnounce, ValidatorAnnounceAccount, + ValidatorStorageLocations, ValidatorStorageLocationsAccount, + }, + error::Error, + instruction::{AnnounceInstruction, InitInstruction, Instruction}, + replay_protection_pda_seeds, validator_announce_pda_seeds, + validator_storage_locations_pda_seeds, +}; + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +/// The entrypoint of the program that processes an instruction. +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + match Instruction::from_instruction_data(instruction_data)? { + Instruction::Init(init) => { + process_init(program_id, accounts, init)?; + } + Instruction::Announce(announce) => { + process_announce(program_id, accounts, announce)?; + } + } + + Ok(()) +} + +/// Initializes the program. +/// +/// Accounts: +/// 0. [signer] The payer. +/// 1. [executable] The system program. +/// 2. [writable] The ValidatorAnnounce PDA account. +pub fn process_init( + program_id: &Pubkey, + accounts: &[AccountInfo], + init: InitInstruction, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let system_program_id = solana_program::system_program::id(); + + // Account 0: The payer. + let payer_info = next_account_info(account_info_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 1: The system program. + let system_program_info = next_account_info(account_info_iter)?; + if system_program_info.key != &system_program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 2: The ValidatorAnnounce PDA account. + let validator_announce_info = next_account_info(account_info_iter)?; + if !validator_announce_info.is_writable { + return Err(ProgramError::InvalidAccountData); + } + if validator_announce_info.owner != &system_program_id + || !validator_announce_info.data_is_empty() + { + return Err(ProgramError::AccountAlreadyInitialized); + } + let (validator_announce_key, validator_announce_bump_seed) = + Pubkey::find_program_address(validator_announce_pda_seeds!(), program_id); + if validator_announce_info.key != &validator_announce_key { + return Err(ProgramError::IncorrectProgramId); + } + + // Create the validator announce account. + let validator_announce = ValidatorAnnounce { + bump_seed: validator_announce_bump_seed, + mailbox: init.mailbox, + local_domain: init.local_domain, + }; + let validator_announce_account = ValidatorAnnounceAccount::from(validator_announce); + let validator_announce_account_size = validator_announce_account.size(); + create_pda_account( + payer_info, + &Rent::get()?, + validator_announce_account_size, + program_id, + system_program_info, + validator_announce_info, + validator_announce_pda_seeds!(validator_announce_bump_seed), + )?; + + // Store the validator_announce_account. + validator_announce_account.store(validator_announce_info, false)?; + + Ok(()) +} + +/// Announces a validator. +/// +/// Accounts: +/// 0. [signer] The payer. +/// 1. [executable] The system program. +/// 2. [] The ValidatorAnnounce PDA account. +/// 3. [writeable] The validator-specific ValidatorStorageLocationsAccount PDA account. +/// 4. [writeable] The ReplayProtection PDA account specific to the announcement being made. +fn process_announce( + program_id: &Pubkey, + accounts: &[AccountInfo], + announcement: AnnounceInstruction, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let system_program_id = solana_program::system_program::id(); + + // Account 0: The payer. + let payer_info = next_account_info(account_info_iter)?; + if !payer_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Account 1: The system program. + let system_program_info = next_account_info(account_info_iter)?; + if system_program_info.key != &system_program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Account 2: The ValidatorAnnounce PDA account. + let validator_announce_info = next_account_info(account_info_iter)?; + let validator_announce = + ValidatorAnnounceAccount::fetch(&mut &validator_announce_info.data.borrow()[..])? + .into_inner(); + // Verify the legitimacy of the account. + validator_announce.verify_self_account_info(program_id, validator_announce_info)?; + + // Account 3: The validator-specific ValidatorStorageLocationsAccount PDA account. + let validator_storage_locations_info = next_account_info(account_info_iter)?; + + // Account 4: The ReplayProtection PDA account specific to the announcement being made. + let replay_protection_info = next_account_info(account_info_iter)?; + let replay_id = announcement.replay_id(); + let (expected_replay_protection_key, replay_protection_bump_seed) = + Pubkey::find_program_address(replay_protection_pda_seeds!(replay_id), program_id); + if replay_protection_info.key != &expected_replay_protection_key { + return Err(ProgramError::IncorrectProgramId); + } + if !replay_protection_info.is_writable { + return Err(ProgramError::InvalidAccountData); + } + // Verify that the ReplayProtection account is not already initialized. + // If it is, it means that the announcement has already been made. + if !replay_protection_info.data_is_empty() { + return Err(ProgramError::AccountAlreadyInitialized); + } + + // Errors if the announcement is not signed by the validator. + verify_validator_signed_announcement(&announcement, &validator_announce)?; + + // Update the stored storage locations. + update_validator_storage_locations( + program_id, + payer_info, + system_program_info, + validator_storage_locations_info, + &announcement, + )?; + + // Create the ReplayProtection account so this cannot be announced again. + create_replay_protection_account( + program_id, + payer_info, + system_program_info, + replay_protection_info, + replay_id, + replay_protection_bump_seed, + )?; + + Ok(()) +} + +/// Updates the validator-specific ValidatorStorageLocationsAccount PDA account +/// with the new storage location. +/// The legitimacy of `validator_storage_locations_info` is verified within +/// this function. +/// If the account does not exist, it is created. +/// If the account does exist, the storage location is appended to the existing +/// storage locations. +fn update_validator_storage_locations<'a>( + program_id: &Pubkey, + payer_info: &AccountInfo<'a>, + system_program_info: &AccountInfo<'a>, + validator_storage_locations_info: &AccountInfo<'a>, + announcement: &AnnounceInstruction, +) -> Result<(), ProgramError> { + // At this point, we still have not verified the legitimacy of the account info passed in. + // This is done just below in the if / else. + let validator_storage_locations_initialized = validator_storage_locations_info.owner + == program_id + && !validator_storage_locations_info.data_is_empty(); + + let (validator_storage_locations, new_serialized_size) = + if validator_storage_locations_initialized { + // If the account is initialized, fetch it and append the storage location. + + let mut validator_storage_locations = ValidatorStorageLocationsAccount::fetch( + &mut &validator_storage_locations_info.data.borrow()[..], + )? + .into_inner(); + + // Verify the ID of the account using `create_program_address` and the stored bump seed. + let expected_validator_storage_locations_key = Pubkey::create_program_address( + validator_storage_locations_pda_seeds!( + announcement.validator, + validator_storage_locations.bump_seed + ), + program_id, + )?; + if validator_storage_locations_info.key != &expected_validator_storage_locations_key { + return Err(ProgramError::IncorrectProgramId); + } + + // Calculate the new serialized size. + let new_serialized_size = validator_storage_locations_info.data_len() + + ValidatorStorageLocations::size_increase_for_new_storage_location( + &announcement.storage_location, + ); + + // Append the storage location. + validator_storage_locations + .storage_locations + .push(announcement.storage_location.clone()); + + (*validator_storage_locations, new_serialized_size) + } else { + // If not initialized, we need to create the account. + + let (validator_storage_locations_key, validator_storage_locations_bump_seed) = + Pubkey::find_program_address( + validator_storage_locations_pda_seeds!(announcement.validator), + program_id, + ); + // Verify the ID of the account using `find_program_address`. + if validator_storage_locations_info.key != &validator_storage_locations_key { + return Err(ProgramError::IncorrectProgramId); + } + + let validator_storage_locations = ValidatorStorageLocations { + bump_seed: validator_storage_locations_bump_seed, + storage_locations: vec![announcement.storage_location.clone()], + }; + let validator_storage_locations_account = + ValidatorStorageLocationsAccount::from(validator_storage_locations); + let validator_storage_locations_size = validator_storage_locations_account.size(); + + // Create the account. + create_pda_account( + payer_info, + &Rent::get()?, + validator_storage_locations_size, + program_id, + system_program_info, + validator_storage_locations_info, + validator_storage_locations_pda_seeds!( + announcement.validator, + validator_storage_locations_bump_seed + ), + )?; + + ( + *validator_storage_locations_account.into_inner(), + validator_storage_locations_size, + ) + }; + + // Because it's possible that a realloc needs to occur, ensure the account + // would be rent-exempt. + let existing_serialized_size = validator_storage_locations_info.data_len(); + let required_rent = Rent::get()?.minimum_balance(new_serialized_size); + let lamports = validator_storage_locations_info.lamports(); + if lamports < required_rent { + invoke( + &system_instruction::transfer( + payer_info.key, + validator_storage_locations_info.key, + required_rent - lamports, + ), + &[payer_info.clone(), validator_storage_locations_info.clone()], + )?; + } + if existing_serialized_size != new_serialized_size { + validator_storage_locations_info.realloc(new_serialized_size, false)?; + } + + // Store the updated validator_storage_locations. + ValidatorStorageLocationsAccount::from(validator_storage_locations) + .store(validator_storage_locations_info, false)?; + + Ok(()) +} + +fn create_replay_protection_account<'a>( + program_id: &Pubkey, + payer_info: &AccountInfo<'a>, + system_program_info: &AccountInfo<'a>, + replay_protection_info: &AccountInfo<'a>, + replay_id: [u8; 32], + replay_protection_bump_seed: u8, +) -> Result<(), ProgramError> { + let replay_protection_account = ReplayProtectionAccount::from(ReplayProtection(())); + let replay_protection_account_size = replay_protection_account.size(); + + // Create the account. + create_pda_account( + payer_info, + &Rent::get()?, + replay_protection_account_size, + program_id, + system_program_info, + replay_protection_info, + replay_protection_pda_seeds!(replay_id, replay_protection_bump_seed), + )?; + + Ok(()) +} + +fn verify_validator_signed_announcement( + announce: &AnnounceInstruction, + validator_announce: &ValidatorAnnounce, +) -> Result<(), ProgramError> { + let announcement = Announcement { + validator: announce.validator, + mailbox_address: validator_announce.mailbox.to_bytes().into(), + mailbox_domain: validator_announce.local_domain, + storage_location: announce.storage_location.clone(), + }; + let announcement_digest = announcement.eth_signed_message_hash(); + let signature = EcdsaSignature::from_bytes(&announce.signature[..]) + .map_err(|_| ProgramError::from(Error::SignatureError))?; + + let recovered_signer = signature + .secp256k1_recover_ethereum_address(&announcement_digest[..]) + .map_err(|_| ProgramError::from(Error::SignatureError))?; + + if recovered_signer != announcement.validator { + return Err(ProgramError::InvalidAccountData); + } + + Ok(()) +} + +#[cfg(test)] +mod test { + // See tests/functional.rs for the rest of the tests that could not be + // done as unit tests due to required system program CPIs. + + use hyperlane_core::{H160, H256}; + use std::str::FromStr; + + use super::*; + + #[test] + fn test_verify_validator_signed_announcement() { + // Announcement from https://hyperlane-mainnet2-ethereum-validator-0.s3.us-east-1.amazonaws.com/announcement.json + + let announce_instruction = AnnounceInstruction { + validator: H160::from_str("0x4c327ccb881a7542be77500b2833dc84c839e7b7").unwrap(), + storage_location: "s3://hyperlane-mainnet2-ethereum-validator-0/us-east-1".to_owned(), + // The `serialized_signature` component of the announcement, + // which is the 65-byte serialized ECDSA signature + signature: hex::decode("20ac937917284eaa3d67287278fc51875874241fffab5eb5fd8ae899a7074c5679be15f0bdb5b4f7594cefc5cba17df59b68ba3c55836053a23307db5a95610d1b").unwrap(), + }; + let mailbox = + H256::from_str("0x00000000000000000000000035231d4c2d8b8adcb5617a638a0c4548684c7c70") + .unwrap(); + let validator_announce = ValidatorAnnounce { + // Bump seed is not used/verified in this test + bump_seed: 255, + mailbox: Pubkey::new_from_array(mailbox.0), + // The ethereum domain + local_domain: 1, + }; + + // Expect a successful verification + assert!( + verify_validator_signed_announcement(&announce_instruction, &validator_announce) + .is_ok() + ); + + // Let's change the local domain to something else, expecting an error now + assert!(verify_validator_signed_announcement( + &announce_instruction, + &ValidatorAnnounce { + local_domain: 2, + ..validator_announce + }, + ) + .is_err()); + + // Change the validator to something else, also expect an error + assert!(verify_validator_signed_announcement( + &AnnounceInstruction { + validator: H160::random(), + ..announce_instruction.clone() + }, + &validator_announce, + ) + .is_err()); + + // Change the storage location to something else, also expect an error + assert!(verify_validator_signed_announcement( + &AnnounceInstruction { + storage_location: "fooooooooooooooo".to_owned(), + ..announce_instruction + }, + &validator_announce, + ) + .is_err()); + + // Change the signature to something else, also expect an error + assert!(verify_validator_signed_announcement( + &AnnounceInstruction { + signature: vec![4u8; 65], + ..announce_instruction + }, + &validator_announce, + ) + .is_err()); + } +} diff --git a/rust/sealevel/programs/validator-announce/tests/functional.rs b/rust/sealevel/programs/validator-announce/tests/functional.rs new file mode 100644 index 0000000000..003c6445b3 --- /dev/null +++ b/rust/sealevel/programs/validator-announce/tests/functional.rs @@ -0,0 +1,404 @@ +use hyperlane_core::{Announcement, H160}; + +use std::str::FromStr; + +use account_utils::SizedData; +use borsh::BorshSerialize; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey, + pubkey::Pubkey, + system_program, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, +}; + +use hyperlane_sealevel_validator_announce::{ + accounts::{ + ReplayProtection, ReplayProtectionAccount, ValidatorAnnounce, ValidatorAnnounceAccount, + ValidatorStorageLocations, ValidatorStorageLocationsAccount, + }, + instruction::{ + AnnounceInstruction, InitInstruction, Instruction as ValidatorAnnounceInstruction, + }, + processor::process_instruction, + replay_protection_pda_seeds, validator_announce_pda_seeds, + validator_storage_locations_pda_seeds, +}; + +// The Ethereum mailbox & domain chosen for easy testing +const TEST_MAILBOX: &str = "00000000000000000000000035231d4c2d8b8adcb5617a638a0c4548684c7c70"; +const TEST_DOMAIN: u32 = 1; + +fn validator_announce_id() -> Pubkey { + pubkey!("DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn") +} + +fn get_test_mailbox() -> Pubkey { + let mailbox_bytes = hex::decode(TEST_MAILBOX).unwrap(); + Pubkey::new(&mailbox_bytes[..]) +} + +fn get_test_announcements() -> Vec<(Announcement, Vec)> { + // Signed by the following validator: + // + // Address: 0x13DFDeB827D4D7fACE707fAdbfd4D651438B4aB3 + // Private Key: 0x2053099fadf2520efd407cbf043f89fe10eaf91a356d585e9ad12a5eb5f771dd + + let announcement0 = Announcement { + validator: H160::from_str("0x13DFDeB827D4D7fACE707fAdbfd4D651438B4aB3").unwrap(), + mailbox_address: get_test_mailbox().to_bytes().into(), + mailbox_domain: TEST_DOMAIN, + storage_location: "s3://test-storage-location-foo/us-east-1".to_string(), + }; + // Got using ethers.js to sign `announcement0.signing_hash()`, which is + // 0x6a4f7bcbbcf3f700c4f4da16d3d14ae907ced31d79779e196f4f40af710cfa85 + // + // > await (new ethers.Wallet('0x2053099fadf2520efd407cbf043f89fe10eaf91a356d585e9ad12a5eb5f771dd')).signMessage(ethers.utils.arrayify('0x6a4f7bcbbcf3f700c4f4da16d3d14ae907ced31d79779e196f4f40af710cfa85')) + let signature0 = hex::decode("fa0d375457d9a98b3cd6c6ee308464ea23abc2f2368e80d942dacf0b2e3cc4d66ac51efabe169b7cb29170894c588221c91807e500a7a9f9648a8b1c47eceecc1c").unwrap(); + + // UTF-8 characters in the storage location + let announcement1 = Announcement { + validator: H160::from_str("0x13DFDeB827D4D7fACE707fAdbfd4D651438B4aB3").unwrap(), + mailbox_address: get_test_mailbox().to_bytes().into(), + mailbox_domain: TEST_DOMAIN, + storage_location: "s3://test-storage-location-Здравствуйте/us-east-1".to_string(), + }; + // Got using ethers.js to sign `announcement1.signing_hash()`, which is + // 0xb647a8e18b8152d7cc122ef3e88b643a0dcd2b702dded70ac2d1c94477ca3090 + // + // > await (new ethers.Wallet('0x2053099fadf2520efd407cbf043f89fe10eaf91a356d585e9ad12a5eb5f771dd')).signMessage(ethers.utils.arrayify('0xb647a8e18b8152d7cc122ef3e88b643a0dcd2b702dded70ac2d1c94477ca3090')) + let signature1 = hex::decode("983b941ae9bf939bf59abcf81e7d2e66735da5e2726f955b915aea247ae16afa3a03b69fe7d7c83154caca29d8ad62f2a1ccbbb5c56f67dab10c98a2d4aac3b01c").unwrap(); + + vec![(announcement0, signature0), (announcement1, signature1)] +} + +async fn send_transaction_with_instruction( + banks_client: &mut BanksClient, + payer: &Keypair, + instruction: Instruction, +) -> Result<(), BanksClientError> { + let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await?; + + Ok(()) +} + +async fn initialize( + banks_client: &mut BanksClient, + payer: &Keypair, + mailbox: Pubkey, +) -> Result<(Pubkey, u8), BanksClientError> { + let program_id = validator_announce_id(); + + let (validator_announce_key, validator_announce_bump_seed) = + Pubkey::find_program_address(validator_announce_pda_seeds!(), &program_id); + + // Accounts: + // 0. [signer] The payer. + // 1. [executable] The system program. + // 2. [writable] The ValidatorAnnounce PDA account. + let init_instruction = Instruction::new_with_borsh( + program_id, + &ValidatorAnnounceInstruction::Init(InitInstruction { + mailbox, + local_domain: TEST_DOMAIN, + }), + vec![ + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(validator_announce_key, false), + ], + ); + + send_transaction_with_instruction(banks_client, payer, init_instruction).await?; + + Ok((validator_announce_key, validator_announce_bump_seed)) +} + +#[tokio::test] +async fn test_initialize() { + let program_id = validator_announce_id(); + let (mut banks_client, payer, _recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_validator_announce", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + let mailbox = get_test_mailbox(); + let (validator_announce_key, validator_announce_bump_seed) = + initialize(&mut banks_client, &payer, mailbox) + .await + .unwrap(); + + // Expect the validator announce account to be initialized. + let validator_announce_account = banks_client + .get_account(validator_announce_key) + .await + .unwrap() + .unwrap(); + assert_eq!(validator_announce_account.owner, program_id); + + let validator_announce = + ValidatorAnnounceAccount::fetch(&mut &validator_announce_account.data[..]) + .unwrap() + .into_inner(); + assert_eq!( + validator_announce, + Box::new(ValidatorAnnounce { + bump_seed: validator_announce_bump_seed, + mailbox, + local_domain: TEST_DOMAIN, + }), + ); +} + +#[tokio::test] +async fn test_initialize_errors_if_called_twice() { + let program_id = validator_announce_id(); + let (mut banks_client, payer, _recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_validator_announce", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + let mailbox = get_test_mailbox(); + initialize(&mut banks_client, &payer, mailbox) + .await + .unwrap(); + + // Using the same mailbox / payer in the new initialize will result in the same + // tx hash because a new blockhash isn't used for the new transaction. + // As a workaround, use a different mailbox + let init_result = initialize(&mut banks_client, &payer, Pubkey::new_unique()).await; + + // BanksClientError doesn't implement Eq, but TransactionError does + if let BanksClientError::TransactionError(tx_err) = init_result.err().unwrap() { + assert_eq!( + tx_err, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized,) + ); + } else { + panic!("expected TransactionError"); + } +} + +async fn announce( + banks_client: &mut BanksClient, + payer: &Keypair, + program_id: Pubkey, + validator_announce_key: Pubkey, + announce_instruction: AnnounceInstruction, +) -> Result<(Pubkey, u8, Pubkey, u8), BanksClientError> { + let (validator_storage_locations_key, validator_storage_locations_bump_seed) = + Pubkey::find_program_address( + validator_storage_locations_pda_seeds!(announce_instruction.validator), + &program_id, + ); + + let replay_id = announce_instruction.replay_id(); + + let (replay_protection_key, replay_protection_bump_seed) = + Pubkey::find_program_address(replay_protection_pda_seeds!(replay_id), &program_id); + + // Accounts: + // 0. [signer] The payer. + // 1. [executable] The system program. + // 2. [] The ValidatorAnnounce PDA account. + // 3. [writeable] The validator-specific ValidatorStorageLocationsAccount PDA account. + // 4. [writeable] The ReplayProtection PDA account specific to the announcement being made. + let announce_instruction = Instruction::new_with_borsh( + program_id, + &ValidatorAnnounceInstruction::Announce(announce_instruction), + vec![ + AccountMeta::new_readonly(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(validator_announce_key, false), + AccountMeta::new(validator_storage_locations_key, false), + AccountMeta::new(replay_protection_key, false), + ], + ); + + send_transaction_with_instruction(banks_client, payer, announce_instruction).await?; + + Ok(( + validator_storage_locations_key, + validator_storage_locations_bump_seed, + replay_protection_key, + replay_protection_bump_seed, + )) +} + +async fn assert_successful_announcement( + banks_client: &mut BanksClient, + program_id: Pubkey, + validator_storage_locations_key: Pubkey, + replay_protection_key: Pubkey, + expected_validator_storage_locations: ValidatorStorageLocations, +) { + // Expect the validator storage locations account to be created & with the new announcement. + let validator_storage_locations_account = banks_client + .get_account(validator_storage_locations_key) + .await + .unwrap() + .unwrap(); + assert_eq!(validator_storage_locations_account.owner, program_id); + + let validator_storage_locations = + ValidatorStorageLocationsAccount::fetch(&mut &validator_storage_locations_account.data[..]) + .unwrap() + .into_inner(); + assert_eq!( + validator_storage_locations, + Box::new(expected_validator_storage_locations.clone()), + ); + // Also sanity check that the sizing logic is correct! + assert_eq!( + validator_storage_locations_account.data.len(), + // Plus 1 for the initialized byte + expected_validator_storage_locations + .try_to_vec() + .unwrap() + .len() + + 1, + ); + assert_eq!( + validator_storage_locations_account.data.len(), + ValidatorStorageLocationsAccount::from(expected_validator_storage_locations).size(), + ); + + // Expect the replay protection account to be created + let replay_protection_account = banks_client + .get_account(replay_protection_key) + .await + .unwrap() + .unwrap(); + assert_eq!(replay_protection_account.owner, program_id); + + assert!(!validator_storage_locations_account.data.is_empty()); + let replay_protection = + ReplayProtectionAccount::fetch_data(&mut &validator_storage_locations_account.data[..]) + .unwrap(); + assert_eq!(replay_protection, Some(Box::new(ReplayProtection(()))),); +} + +#[tokio::test] +async fn test_announce() { + let program_id = validator_announce_id(); + let (mut banks_client, payer, _recent_blockhash) = ProgramTest::new( + "hyperlane_sealevel_validator_announce", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + let mailbox = get_test_mailbox(); + let (validator_announce_key, _validator_announce_bump_seed) = + initialize(&mut banks_client, &payer, mailbox) + .await + .unwrap(); + + let test_announcements = get_test_announcements(); + + // Make the first announcement + let (announcement, signature) = test_announcements[0].clone(); + let announce_instruction = AnnounceInstruction { + validator: announcement.validator, + storage_location: announcement.storage_location, + signature, + }; + let ( + validator_storage_locations_key, + validator_storage_locations_bump_seed, + replay_protection_key, + _replay_protection_bump_seed, + ) = announce( + &mut banks_client, + &payer, + program_id, + validator_announce_key, + announce_instruction.clone(), + ) + .await + .unwrap(); + + assert_successful_announcement( + &mut banks_client, + program_id, + validator_storage_locations_key, + replay_protection_key, + ValidatorStorageLocations { + bump_seed: validator_storage_locations_bump_seed, + storage_locations: vec![announce_instruction.storage_location.clone()], + }, + ) + .await; + + // And ensure that the announcement can't be made again! + let announce_result = announce( + &mut banks_client, + &payer, + program_id, + validator_announce_key, + announce_instruction.clone(), + ) + .await; + // BanksClientError doesn't implement Eq, but TransactionError does + if let BanksClientError::TransactionError(tx_err) = announce_result.err().unwrap() { + assert_eq!( + tx_err, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized,) + ); + } else { + panic!("expected TransactionError"); + } + + // And then announce the second storage location, which we expect to be successful + let (announcement, signature) = test_announcements[1].clone(); + let announce_instruction1 = AnnounceInstruction { + validator: announcement.validator, + storage_location: announcement.storage_location, + signature, + }; + + let (_, _, replay_protection_key, _replay_protection_bump_seed) = announce( + &mut banks_client, + &payer, + program_id, + validator_announce_key, + announce_instruction1.clone(), + ) + .await + .unwrap(); + + assert_successful_announcement( + &mut banks_client, + program_id, + validator_storage_locations_key, + replay_protection_key, + ValidatorStorageLocations { + bump_seed: validator_storage_locations_bump_seed, + storage_locations: vec![ + announce_instruction.storage_location.clone(), + announce_instruction1.storage_location.clone(), + ], + }, + ) + .await; +} diff --git a/rust/utils/abigen/Cargo.toml b/rust/utils/abigen/Cargo.toml index afdf004172..beb601469d 100644 --- a/rust/utils/abigen/Cargo.toml +++ b/rust/utils/abigen/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "abigen" documentation.workspace = true @@ -9,11 +11,11 @@ version.workspace = true [dependencies] -Inflector = "0.11" +Inflector.workspace = true ethers = { workspace = true, optional = true } fuels = { workspace = true, optional = true } fuels-code-gen = { workspace = true, optional = true } -which = { version = "4.3", optional = true } +which = { workspace = true, optional = true } [features] default = [] diff --git a/rust/utils/backtrace-oneline/Cargo.toml b/rust/utils/backtrace-oneline/Cargo.toml index 85b4083779..7f11187988 100644 --- a/rust/utils/backtrace-oneline/Cargo.toml +++ b/rust/utils/backtrace-oneline/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "backtrace-oneline" version = "0.1.0" diff --git a/rust/utils/hex/Cargo.toml b/rust/utils/hex/Cargo.toml index bc198e809e..6c91b685fd 100644 --- a/rust/utils/hex/Cargo.toml +++ b/rust/utils/hex/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "hex" version = "0.1.0" diff --git a/rust/utils/run-locally/Cargo.toml b/rust/utils/run-locally/Cargo.toml index c9bf82ae30..6b53d1365b 100644 --- a/rust/utils/run-locally/Cargo.toml +++ b/rust/utils/run-locally/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["workspace-inheritance"] + [package] name = "run-locally" documentation.workspace = true @@ -14,4 +16,4 @@ maplit = "1.0" nix = { version = "0.26", default-features = false, features = ["signal"] } tempfile = "3.3" ureq = { version = "2.4", default-features = false } -which = "4.4" \ No newline at end of file +which = "4.4" diff --git a/rust/utils/sealevel-test.bash b/rust/utils/sealevel-test.bash new file mode 100755 index 0000000000..8e84fd7055 --- /dev/null +++ b/rust/utils/sealevel-test.bash @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +if [ -z $SOLAR_ECLIPSE_DIR ]; then + echo '$SOLAR_ECLIPSE_DIR must be set' +fi + +if [ -z $ECLIPSE_PROGRAM_LIBRARY_DIR ]; then + echo '$ECLIPSE_PROGRAM_LIBRARY_DIR must be set' +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TEST_KEYS_DIR="${SCRIPT_DIR}/../config/sealevel/test-keys" +KEYPAIR="${TEST_KEYS_DIR}/test_deployer-keypair.json" +TARGET_DIR="${SCRIPT_DIR}/../target" +SEALEVEL_DIR="${SCRIPT_DIR}/../sealevel" +DEPLOY_DIR="${TARGET_DIR}/deploy" +BIN_DIR="${TARGET_DIR}/debug" +SPL_TOKEN="${ECLIPSE_PROGRAM_LIBRARY_DIR}/target/debug/spl-token" +CHAIN_ID="13375" +REMOTE_CHAIN_ID="13376" + +# Ensure that the solar-eclipse `solana` binary is used +alias solana="${SOLAR_ECLIPSE_DIR}/target/debug/solana" + +# first arg = path to .so file +# second arg = path to directory to build program in if the .so file doesn't exist +# third arg = whether to force build the program +build_program() { + if $3 || [ ! -e $1 ]; then + # .so file doesn't exist, build it + pushd "${2}" + cargo build-sbf + popd + fi +} + +# first arg = path to .so file +# second arg = path to directory to build program in if the .so file doesn't exist +build_and_copy_program() { + build_program $1 $2 $3 + + # essentially cp, but -u won't copy if the source is older than the destination. + # used as a workaround to prevent copying to the same destination as the source + rsync -u $1 $DEPLOY_DIR +} + +build_programs() { + local force_build="${1}" + + # token programs + build_program "${ECLIPSE_PROGRAM_LIBRARY_DIR}/target/deploy/spl_token.so" "${ECLIPSE_PROGRAM_LIBRARY_DIR}/token/program" "${force_build}" + build_program "${ECLIPSE_PROGRAM_LIBRARY_DIR}/target/deploy/spl_token_2022.so" "${ECLIPSE_PROGRAM_LIBRARY_DIR}/token/program-2022" "${force_build}" + build_program "${ECLIPSE_PROGRAM_LIBRARY_DIR}/target/deploy/spl_associated_token_account.so" "${ECLIPSE_PROGRAM_LIBRARY_DIR}/associated-token-account/program" "${force_build}" + + # noop + build_program "${ECLIPSE_PROGRAM_LIBRARY_DIR}/account-compression/target/deploy/spl_noop.so" "${ECLIPSE_PROGRAM_LIBRARY_DIR}/account-compression/programs/noop" "${force_build}" + + # hyperlane sealevel programs + build_and_copy_program "${TARGET_DIR}/deploy/hyperlane_sealevel_mailbox.so" "${SEALEVEL_DIR}/programs/mailbox" "${force_build}" + build_and_copy_program "${TARGET_DIR}/deploy/hyperlane_sealevel_validator_announce.so" "${SEALEVEL_DIR}/programs/validator-announce" "${force_build}" + build_and_copy_program "${TARGET_DIR}/deploy/hyperlane_sealevel_multisig_ism_message_id.so" "${SEALEVEL_DIR}/programs/ism/multisig-ism-message-id" "${force_build}" + build_and_copy_program "${TARGET_DIR}/deploy/hyperlane_sealevel_token.so" "${SEALEVEL_DIR}/programs/hyperlane-sealevel-token" "${force_build}" + build_and_copy_program "${TARGET_DIR}/deploy/hyperlane_sealevel_token_native.so" "${SEALEVEL_DIR}/programs/hyperlane-sealevel-token-native" "${force_build}" + build_and_copy_program "${TARGET_DIR}/deploy/hyperlane_sealevel_token_collateral.so" "${SEALEVEL_DIR}/programs/hyperlane-sealevel-token-collateral" "${force_build}" +} + +build_spl_token_cli() { + if [ ! -e $SPL_TOKEN ]; then + pushd "${ECLIPSE_PROGRAM_LIBRARY_DIR}/token/cli" + cargo build + popd + fi +} + +setup_multisig_ism_message_id() { + "${BIN_DIR}/hyperlane-sealevel-client" -k "${KEYPAIR}" multisig-ism-message-id set-validators-and-threshold --domain "${CHAIN_ID}" --validators 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 --threshold 1 --program-id "4RSV6iyqW9X66Xq3RDCVsKJ7hMba5uv6XP8ttgxjVUB1" +} + +announce_validator() { + "${BIN_DIR}/hyperlane-sealevel-client" -k "${KEYPAIR}" validator-announce announce --validator 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 --storage-location "file:///tmp/test_sealevel_checkpoints_0x70997970c51812dc3a010c7d01b50e0d17dc79c8" --signature "0xcd87b715cd4c2e3448be9e34204cf16376a6ba6106e147a4965e26ea946dd2ab19598140bf26f1e9e599c23f6b661553c7d89e8db22b3609068c91eb7f0fa2f01b" +} + +test_token() { + + setup_multisig_ism_message_id + + announce_validator + + "${BIN_DIR}/hyperlane-sealevel-client" -k "${KEYPAIR}" --compute-budget 200000 warp-route deploy --warp-route-name testwarproute --environment local-e2e --environments-dir "${SEALEVEL_DIR}/environments" --built-so-dir "${DEPLOY_DIR}" --token-config-file "${SEALEVEL_DIR}/environments/local-e2e/warp-routes/testwarproute/token-config.json" --chain-config-file "${SEALEVEL_DIR}/environments/local-e2e/warp-routes/chain-config.json" --ata-payer-funding-amount 1000000000 + + local token_type="" + local program_id="" + + local recipient_token_type="" + local recipient_program_id="" + + token_type="native" + program_id="CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga" + + recipient_token_type="synthetic" + recipient_program_id="3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH" + + local amount=10000000000 # lamports + + local -r sender_keypair="${KEYPAIR}" + local -r sender="$(solana -ul -k "${sender_keypair}" address)" + local -r recipient="${sender}" + + local -r sender_balance="$(solana -ul balance "${sender}" | cut -d ' ' -f 1)" + local -r amount_float="$(python -c "print(${amount} / 1000000000)")" + if (( $(bc -l <<< "${sender_balance} < ${amount_float}") )); then + echo "Insufficient sender funds" + exit 1 + fi + + solana -ul balance "${sender}" + + # Transfer the lamports + "${BIN_DIR}/hyperlane-sealevel-client" \ + -k "${KEYPAIR}" \ + token transfer-remote "${sender_keypair}" "${amount}" "${REMOTE_CHAIN_ID}" "${recipient}" "${token_type}" --program-id "${program_id}" + + # Wait for token transfer message to appear in the destination Mailbox. + # This ID was manually gotten from running the Relayer and observing the logs - fragile, I know! + while "${BIN_DIR}/hyperlane-sealevel-client" -k "${KEYPAIR}" mailbox delivered --message-id 0x7b8ba684e5ce44f898c5fa81785c83a00e32b5bef3412e648eb7a17bec497685 --program-id "9tCUWNjpqcf3NUSrtp7vquYVCwbEByvLjZUrhG5dgvhj" | grep -q 'Message not delivered' + do + sleep 3 + done + + solana -ul balance "${recipient}" + + "${BIN_DIR}/hyperlane-sealevel-client" -k "${KEYPAIR}" mailbox query + "${BIN_DIR}/hyperlane-sealevel-client" -k "${KEYPAIR}" token query "${token_type}" --program-id "${program_id}" +} + +main() { + if [ "${1}" = "build-only" ]; then + build_programs true + exit 0 + fi + + # build the client + pushd "${SCRIPT_DIR}/../sealevel/client" + cargo build + popd + + # build all the required sealevel programs + if [ "${1}" = "force-build-programs" ]; then + build_programs true + else + build_programs false + fi + + # build the SPL token CLI + build_spl_token_cli + + "${BIN_DIR}/hyperlane-sealevel-client" --compute-budget 200000 -k "${KEYPAIR}" core deploy --local-domain "${CHAIN_ID}" --environment local-e2e --use-existing-keys --environments-dir "${SEALEVEL_DIR}/environments" --built-so-dir "${DEPLOY_DIR}" --chain sealeveltest1 + "${BIN_DIR}/hyperlane-sealevel-client" --compute-budget 200000 -k "${KEYPAIR}" core deploy --local-domain "${REMOTE_CHAIN_ID}" --environment local-e2e --use-existing-keys --environments-dir "${SEALEVEL_DIR}/environments" --built-so-dir "${DEPLOY_DIR}" --chain sealeveltest2 + + test_token true +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -ex + main "$@" +fi