diff --git a/.github/workflows/stellar-build-and-test.yml b/.github/workflows/stellar-build-and-test.yml index bd23e862..9c2cffe0 100644 --- a/.github/workflows/stellar-build-and-test.yml +++ b/.github/workflows/stellar-build-and-test.yml @@ -27,7 +27,7 @@ jobs: - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: - toolchain: 1.79.0 + toolchain: 1.81.0 target: wasm32-unknown-unknown override: true profile: minimal diff --git a/Cargo.lock b/Cargo.lock index 36dc3585..224d4770 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "ahash" version = "0.7.6" @@ -34,30 +19,12 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anyhow" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arrayvec" version = "0.7.4" @@ -70,21 +37,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base16ct" version = "0.1.1" @@ -97,12 +49,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - [[package]] name = "base64" version = "0.11.0" @@ -121,12 +67,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "base64ct" version = "1.6.0" @@ -214,12 +154,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - [[package]] name = "byte-slice-cast" version = "1.2.2" @@ -241,31 +175,12 @@ dependencies = [ "serde", ] -[[package]] -name = "bytes-lit" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" -dependencies = [ - "num-bigint", - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", -] - [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -[[package]] -name = "centralized-connection" -version = "0.0.0" -dependencies = [ - "soroban-sdk", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -279,10 +194,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", - "iana-time-zone", "num-traits", - "serde", - "winapi", +] + +[[package]] +name = "cluster-connection" +version = "0.0.0" +dependencies = [ + "soroban-rlp", + "soroban-sdk", ] [[package]] @@ -299,7 +219,7 @@ dependencies = [ "displaydoc", "dyn-clone", "hex", - "hex-literal 0.3.4", + "hex-literal", "ibc-proto", "ics23", "pbjson", @@ -325,12 +245,6 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cosmwasm" version = "0.7.2" @@ -432,17 +346,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crate-git-revision" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" -dependencies = [ - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "crunchy" version = "0.2.2" @@ -483,16 +386,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctor" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" -dependencies = [ - "quote 1.0.33", - "syn 2.0.42", -] - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -506,33 +399,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", -] - [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -684,7 +550,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 0.15.1", "derivative", - "itertools 0.10.5", + "itertools", "prost 0.9.0", "schemars 0.8.12", "serde", @@ -702,7 +568,7 @@ dependencies = [ "cw-storage-plus 1.0.1", "cw-utils 1.0.1", "derivative", - "itertools 0.10.5", + "itertools", "k256 0.11.6", "prost 0.9.0", "schemars 0.8.12", @@ -930,41 +796,6 @@ dependencies = [ "serde", ] -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2 1.0.70", - "quote 1.0.33", - "strsim", - "syn 2.0.42", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote 1.0.33", - "syn 2.0.42", -] - [[package]] name = "debug_print" version = "1.0.0" @@ -1002,17 +833,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derive_arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" -dependencies = [ - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", -] - [[package]] name = "derive_more" version = "0.99.17" @@ -1062,12 +882,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "dyn-clone" version = "1.0.11" @@ -1109,16 +923,6 @@ dependencies = [ "signature 1.6.4", ] -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -1132,28 +936,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ed25519-dalek" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" -dependencies = [ - "curve25519-dalek 4.1.3", - "ed25519 2.2.3", - "rand_core 0.6.4", - "serde", - "sha2 0.10.8", - "subtle", - "zeroize", -] - [[package]] name = "ed25519-zebra" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek", "hashbrown 0.12.3", "hex", "rand_core 0.6.4", @@ -1243,18 +1032,6 @@ dependencies = [ "libc", ] -[[package]] -name = "escape-bytes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" - -[[package]] -name = "ethnum" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" - [[package]] name = "fastrand" version = "1.9.0" @@ -1284,12 +1061,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "fixed-hash" version = "0.8.0" @@ -1314,12 +1085,6 @@ dependencies = [ "paste", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "forward_ref" version = "1.0.0" @@ -1405,18 +1170,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "group" version = "0.12.1" @@ -1471,9 +1228,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-buffer-serde" @@ -1491,12 +1245,6 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" -[[package]] -name = "hex-literal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" - [[package]] name = "hmac" version = "0.12.1" @@ -1506,29 +1254,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[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 = "ibc" version = "0.32.0" @@ -1595,12 +1320,6 @@ dependencies = [ "sha3", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "impl-serde" version = "0.4.0" @@ -1629,7 +1348,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -1640,15 +1358,8 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.3", - "serde", ] -[[package]] -name = "indexmap-nostd" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" - [[package]] name = "instant" version = "0.1.12" @@ -1678,30 +1389,12 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" -[[package]] -name = "js-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "k256" version = "0.11.6" @@ -1749,12 +1442,6 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1773,41 +1460,12 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "miniz_oxide" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" -dependencies = [ - "adler2", -] - -[[package]] -name = "mock-dapp-multi" -version = "0.0.0" -dependencies = [ - "soroban-rlp", - "soroban-sdk", - "soroban-xcall-lib", - "xcall", -] - [[package]] name = "multimap" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-derive" version = "0.3.3" @@ -1819,26 +1477,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1848,15 +1486,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.18.0" @@ -1869,18 +1498,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder", - "sha2 0.10.8", -] - [[package]] name = "parity-scale-codec" version = "3.6.9" @@ -1928,7 +1545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdbb7b706f2afc610f3853550cdbbf6372fd324824a087806bd4480ea4996e24" dependencies = [ "heck", - "itertools 0.10.5", + "itertools", "prost 0.11.9", "prost-types", ] @@ -1990,15 +1607,6 @@ dependencies = [ "spki 0.7.3", ] -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.1.25" @@ -2009,25 +1617,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "prettyplease" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" -dependencies = [ - "proc-macro2 1.0.70", - "syn 2.0.42", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve 0.13.8", -] - [[package]] name = "primitive-types" version = "0.12.2" @@ -2114,12 +1703,12 @@ checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ "bytes", "heck", - "itertools 0.10.5", + "itertools", "lazy_static", "log", "multimap", "petgraph", - "prettyplease 0.1.25", + "prettyplease", "prost 0.11.9", "prost-types", "regex", @@ -2135,7 +1724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools", "proc-macro2 1.0.70", "quote 1.0.33", "syn 1.0.109", @@ -2148,7 +1737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools", "proc-macro2 1.0.70", "quote 1.0.33", "syn 1.0.109", @@ -2181,27 +1770,6 @@ dependencies = [ "proc-macro2 1.0.70", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_core" version = "0.5.1" @@ -2282,27 +1850,12 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc-hex" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.37.19" @@ -2572,36 +2125,6 @@ dependencies = [ "syn 2.0.42", ] -[[package]] -name = "serde_with" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.1.0", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" -dependencies = [ - "darling", - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", -] - [[package]] name = "sha2" version = "0.9.9" @@ -2656,12 +2179,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - [[package]] name = "snafu" version = "0.5.0" @@ -2683,210 +2200,6 @@ dependencies = [ "syn 0.15.44", ] -[[package]] -name = "soroban-builtin-sdk-macros" -version = "21.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" -dependencies = [ - "itertools 0.11.0", - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", -] - -[[package]] -name = "soroban-env-common" -version = "21.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" -dependencies = [ - "arbitrary", - "crate-git-revision", - "ethnum", - "num-derive 0.4.2", - "num-traits", - "serde", - "soroban-env-macros", - "soroban-wasmi", - "static_assertions", - "stellar-xdr", - "wasmparser", -] - -[[package]] -name = "soroban-env-guest" -version = "21.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" -dependencies = [ - "soroban-env-common", - "static_assertions", -] - -[[package]] -name = "soroban-env-host" -version = "21.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" -dependencies = [ - "backtrace", - "curve25519-dalek 4.1.3", - "ecdsa 0.16.9", - "ed25519-dalek", - "elliptic-curve 0.13.8", - "generic-array", - "getrandom", - "hex-literal 0.4.1", - "hmac", - "k256 0.13.1", - "num-derive 0.4.2", - "num-integer", - "num-traits", - "p256", - "rand", - "rand_chacha", - "sec1 0.7.3", - "sha2 0.10.8", - "sha3", - "soroban-builtin-sdk-macros", - "soroban-env-common", - "soroban-wasmi", - "static_assertions", - "stellar-strkey", - "wasmparser", -] - -[[package]] -name = "soroban-env-macros" -version = "21.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" -dependencies = [ - "itertools 0.11.0", - "proc-macro2 1.0.70", - "quote 1.0.33", - "serde", - "serde_json", - "stellar-xdr", - "syn 2.0.42", -] - -[[package]] -name = "soroban-ledger-snapshot" -version = "21.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84589856911dfd6731695c9b51c858aed6d4540118c0a1e5c4c858ea13bc744c" -dependencies = [ - "serde", - "serde_json", - "serde_with", - "soroban-env-common", - "soroban-env-host", - "thiserror", -] - -[[package]] -name = "soroban-rlp" -version = "0.1.0" -dependencies = [ - "soroban-sdk", -] - -[[package]] -name = "soroban-sdk" -version = "21.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b888f866ce621257311dd779ea49280ce29faa5ee37f22270ba7c32a8fe25d" -dependencies = [ - "arbitrary", - "bytes-lit", - "ctor", - "ed25519-dalek", - "rand", - "serde", - "serde_json", - "soroban-env-guest", - "soroban-env-host", - "soroban-ledger-snapshot", - "soroban-sdk-macros", - "stellar-strkey", -] - -[[package]] -name = "soroban-sdk-macros" -version = "21.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c2173f1aacd56b4405eed71cb2a9694dff99d51ba72d4f0cbc5e4961fdabf4" -dependencies = [ - "crate-git-revision", - "darling", - "itertools 0.11.0", - "proc-macro2 1.0.70", - "quote 1.0.33", - "rustc_version", - "sha2 0.10.8", - "soroban-env-common", - "soroban-spec", - "soroban-spec-rust", - "stellar-xdr", - "syn 2.0.42", -] - -[[package]] -name = "soroban-spec" -version = "21.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7705bffbcc747c08e81698b87b4a787f8b268c25d88f777160091dc1ee8121cb" -dependencies = [ - "base64 0.13.1", - "stellar-xdr", - "thiserror", - "wasmparser", -] - -[[package]] -name = "soroban-spec-rust" -version = "21.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48207ebc8616c2804a17203d1d86c53c3d3c804b682cbab011a135893db1cf78" -dependencies = [ - "prettyplease 0.2.15", - "proc-macro2 1.0.70", - "quote 1.0.33", - "sha2 0.10.8", - "soroban-spec", - "stellar-xdr", - "syn 2.0.42", - "thiserror", -] - -[[package]] -name = "soroban-wasmi" -version = "0.31.1-soroban.20.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" -dependencies = [ - "smallvec", - "spin", - "wasmi_arena", - "wasmi_core", - "wasmparser-nostd", -] - -[[package]] -name = "soroban-xcall-lib" -version = "0.1.0" -dependencies = [ - "soroban-sdk", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spki" version = "0.6.0" @@ -2913,39 +2226,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stellar-strkey" -version = "0.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" -dependencies = [ - "base32", - "crate-git-revision", - "thiserror", -] - -[[package]] -name = "stellar-xdr" -version = "21.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" -dependencies = [ - "arbitrary", - "base64 0.13.1", - "crate-git-revision", - "escape-bytes", - "hex", - "serde", - "serde_with", - "stellar-strkey", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.25.0" @@ -3040,7 +2320,7 @@ checksum = "cda53c85447577769cdfc94c10a56f34afef2c00e4108badb57fce6b1a0c75eb" dependencies = [ "bytes", "digest 0.10.7", - "ed25519 1.5.3", + "ed25519", "ed25519-consensus", "flex-error", "futures", @@ -3082,7 +2362,7 @@ checksum = "c943f78c929cdf14553842f705f2c30324bc35b9179caaa5c9b80620f60652e6" dependencies = [ "bytes", "flex-error", - "num-derive 0.3.3", + "num-derive", "num-traits", "prost 0.11.9", "prost-types", @@ -3101,7 +2381,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test 0.16.4", "hex", - "hex-literal 0.3.4", + "hex-literal", "ibc-proto", "prost 0.11.9", "serde", @@ -3134,8 +2414,6 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ - "itoa", - "serde", "time-core", "time-macros", ] @@ -3256,98 +2534,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" -dependencies = [ - "quote 1.0.33", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" -dependencies = [ - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" - -[[package]] -name = "wasmi_arena" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" - -[[package]] -name = "wasmi_core" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" -dependencies = [ - "downcast-rs", - "libm", - "num-traits", - "paste", -] - -[[package]] -name = "wasmparser" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" -dependencies = [ - "indexmap 2.1.0", - "semver", -] - -[[package]] -name = "wasmparser-nostd" -version = "0.100.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" -dependencies = [ - "indexmap-nostd", -] - [[package]] name = "which" version = "4.4.0" @@ -3359,37 +2545,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -3438,22 +2593,6 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3466,12 +2605,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3484,12 +2617,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3502,18 +2629,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3526,12 +2641,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3544,12 +2653,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3562,12 +2665,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3580,12 +2677,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "winnow" version = "0.5.30" @@ -3595,36 +2686,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "xcall" -version = "0.1.0" -dependencies = [ - "soroban-rlp", - "soroban-sdk", - "soroban-xcall-lib", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2 1.0.70", - "quote 1.0.33", - "syn 2.0.42", -] - [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 8ee82d52..19f8128c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] members = [ - "contracts/cosmwasm-vm/*", - "contracts/soroban/contracts/*", - "contracts/soroban/libs/*" + "contracts/cosmwasm-vm/*" ] [workspace.package] @@ -38,8 +36,6 @@ cw-common={ git="https://github.com/icon-project/IBC-Integration.git", branch = cw-mock-dapp = {path="contracts/cosmwasm-vm/cw-mock-dapp"} cw-mock-dapp-multi = { path="contracts/cosmwasm-vm/cw-mock-dapp-multi"} -soroban-sdk = "21.6.0" - [profile.release] opt-level = 'z' debug = false diff --git a/contracts/evm/contracts/adapters/ClusterConnection.sol b/contracts/evm/contracts/adapters/ClusterConnection.sol new file mode 100644 index 00000000..2f333310 --- /dev/null +++ b/contracts/evm/contracts/adapters/ClusterConnection.sol @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; +pragma abicoder v2; + +import {console2 } from "forge-std/Test.sol"; + +import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@xcall/utils/Types.sol"; +import "@xcall/contracts/xcall/interfaces/IConnection.sol"; +import "@iconfoundation/xcall-solidity-library/interfaces/ICallService.sol"; +import "@iconfoundation/xcall-solidity-library/utils/RLPEncode.sol"; +import "@iconfoundation/xcall-solidity-library/utils/RLPEncode.sol"; + +contract ClusterConnection is Initializable, IConnection { + + using RLPEncode for bytes; + using RLPEncode for string; + using RLPEncode for uint256; + + mapping(string => uint256) private messageFees; + mapping(string => uint256) private responseFees; + mapping(string => mapping(uint256 => bool)) receipts; + + address private xCall; + address private relayerAddress; + address private adminAddress; + uint256 public connSn; + address[] private validators; + uint8 private validatorsThreshold; + + event Message(string targetNetwork, uint256 sn, bytes _msg); + event ValidatorSetAdded(bytes[] _validator, uint8 _threshold); + + modifier onlyRelayer() { + require(msg.sender == this.relayer(), "OnlyRelayer"); + _; + } + + modifier onlyAdmin() { + require(msg.sender == this.admin(), "OnlyAdmin"); + _; + } + + function initialize(address _relayer, address _xCall) public initializer { + xCall = _xCall; + adminAddress = msg.sender; + relayerAddress = _relayer; + } + + function listValidators() external view returns (address[] memory) { + return validators; + } + + function updateValidators(bytes[] memory _validators, uint8 _threshold) external onlyAdmin { + delete validators; + for (uint i = 0; i < _validators.length; i++) { + address validators_address = publicKeyToAddress(_validators[i]); + if(!isValidator(validators_address) && validators_address != address(0)) { + validators.push(validators_address); + } + } + require(validators.length >= _threshold, "Not enough validators"); + validatorsThreshold = _threshold; + emit ValidatorSetAdded(_validators, _threshold); + } + + function isValidator(address signer) public view returns (bool) { + for (uint i = 0; i < validators.length; i++) { + if (validators[i] == signer) { + return true; + } + } + } + + /** + @notice Sets the fee to the target network + @param networkId String Network Id of target chain + @param messageFee Integer ( The fee needed to send a Message ) + @param responseFee Integer (The fee of the response ) + */ + function setFee( + string calldata networkId, + uint256 messageFee, + uint256 responseFee + ) external onlyRelayer { + messageFees[networkId] = messageFee; + responseFees[networkId] = responseFee; + } + + /** + @notice Gets the fee to the target network + @param to String Network Id of target chain + @param response Boolean ( Whether the responding fee is included ) + @return fee Integer (The fee of sending a message to a given destination network ) + */ + function getFee( + string memory to, + bool response + ) external view returns (uint256 fee) { + uint256 messageFee = messageFees[to]; + if (response == true) { + uint256 responseFee = responseFees[to]; + return messageFee + responseFee; + } + return messageFee; + } + + /** + @notice Sends the message to a specific network. + @param sn : positive for two-way message, zero for one-way message, negative for response + @param to String ( Network Id of destination network ) + @param _svc String ( name of the service ) + @param sn Integer ( serial number of the xcall message ) + @param _msg Bytes ( serialized bytes of Service Message ) + */ + function sendMessage( + string calldata to, + string calldata _svc, + int256 sn, + bytes calldata _msg + ) external payable override { + require(msg.sender == xCall, "Only Xcall can call sendMessage"); + uint256 fee; + if (sn > 0) { + fee = this.getFee(to, true); + } else if (sn == 0) { + fee = this.getFee(to, false); + } + require(msg.value >= fee, "Fee is not Sufficient"); + connSn++; + emit Message(to, connSn, _msg); + } + + /** + @notice Sends the message to a xCall. + @param srcNetwork String ( Network Id ) + @param _connSn Integer ( connection message sn ) + @param _msg Bytes ( serialized bytes of Service Message ) + */ + function recvMessageWithSignatures( + string memory srcNetwork, + uint256 _connSn, + bytes calldata _msg, + bytes[] calldata _signedMessages + ) public onlyRelayer { + require(_signedMessages.length >= validatorsThreshold, "Not enough signatures passed"); + bytes32 messageHash = getMessageHash(srcNetwork, _connSn, _msg); + uint signerCount = 0; + address[] memory collectedSigners = new address[](_signedMessages.length); + + for (uint i = 0; i < _signedMessages.length; i++) { + address signer = recoverSigner(messageHash, _signedMessages[i]); + require(signer != address(0), "Invalid signature"); + if (!isValidatorProcessed(collectedSigners, signer) && existsInValidators(signer)){ + collectedSigners[signerCount] = signer; + signerCount++; + } + } + require(signerCount >= validatorsThreshold,"Not enough valid signatures passed"); + recvMessage(srcNetwork,_connSn,_msg); + } + + function existsInValidators(address signer) internal view returns (bool) { + for (uint i = 0; i < validators.length; i++){ + if (validators[i] == signer) return true; + } + return false; + } + + function isValidatorProcessed(address[] memory processedSigners, address signer) public pure returns (bool) { + for (uint i = 0; i < processedSigners.length; i++) { + if (processedSigners[i] == signer) { + return true; + } + } + return false; + } + + function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) { + require(signature.length == 65, "Invalid signature length"); + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := byte(0, mload(add(signature, 96))) + } + if (v < 27) { + v += 27; + } + require(v == 27 || v == 28, "Invalid signature 'v' value"); + return ecrecover(messageHash, v, r, s); + } + + function recvMessage( + string memory srcNetwork, + uint256 _connSn, + bytes calldata _msg + ) internal { + require(!receipts[srcNetwork][_connSn], "Duplicate Message"); + receipts[srcNetwork][_connSn] = true; + ICallService(xCall).handleMessage(srcNetwork, _msg); + } + + /** + @notice Sends the balance of the contract to the owner(relayer) + + */ + function claimFees() public onlyRelayer { + payable(relayerAddress).transfer(address(this).balance); + } + + /** + @notice Revert a messages, used in special cases where message can't just be dropped + @param sn Integer ( serial number of the xcall message ) + */ + function revertMessage(uint256 sn) public onlyRelayer { + ICallService(xCall).handleError(sn); + } + + /** + @notice Gets a message receipt + @param srcNetwork String ( Network Id ) + @param _connSn Integer ( connection message sn ) + @return boolean if is has been recived or not + */ + function getReceipt( + string memory srcNetwork, + uint256 _connSn + ) public view returns (bool) { + return receipts[srcNetwork][_connSn]; + } + + /** + @notice Set the address of the admin. + @param _address The address of the admin. + */ + function setAdmin(address _address) external onlyAdmin { + adminAddress = _address; + } + + /** + @notice Set the address of the relayer. + @param _address The address of the relayer. + */ + function setRelayer(address _address) external onlyAdmin { + relayerAddress = _address; + } + + /** + @notice Gets the address of relayer + @return (Address) the address of relayer + */ + function relayer() external view returns (address) { + return relayerAddress; + } + + /** + @notice Gets the address of admin + @return (Address) the address of admin + */ + function admin() external view returns (address) { + return adminAddress; + } + + /** + @notice Set the required signature count for verification. + @param _count The desired count. + */ + function setRequiredValidatorCount(uint8 _count) external onlyAdmin() { + validatorsThreshold = _count; + } + + function getRequiredValidatorCount() external view returns (uint8) { + return validatorsThreshold; + } + + function getMessageHash(string memory srcNetwork, uint256 _connSn, bytes calldata _msg) internal pure returns (bytes32) { + bytes memory rlp = abi.encodePacked( + srcNetwork.encodeString(), + _connSn.encodeUint(), + _msg.encodeBytes() + ).encodeList(); + return keccak256(rlp); + } + + function publicKeyToAddress(bytes memory publicKey) internal pure returns (address addr) { + require(publicKey.length == 65, "Invalid public key length"); + + bytes32 hash; + + assembly { + let publicKeyStart := add(publicKey, 0x20) + let destinationStart := add(publicKeyStart, 1) + hash := keccak256(destinationStart, 64) + } + + addr = address(uint160(uint256(hash))); + } + +} diff --git a/contracts/evm/forge_build.sh b/contracts/evm/forge_build.sh index 76d931ce..c4bbd8cf 100755 --- a/contracts/evm/forge_build.sh +++ b/contracts/evm/forge_build.sh @@ -5,5 +5,6 @@ forge build mkdir artifacts cat out/CallService.sol/CallService.json | jq '{"abi": .abi, "bytecode": .bytecode.object}' > artifacts/xcall_abi_bytecode.json cat out/CentralizedConnection.sol/CentralizedConnection.json | jq '{"abi": .abi, "bytecode": .bytecode.object}' > artifacts/centralized_connection_abi_byte_code.json +cat out/ClusterConnection.sol/ClusterConnection.json | jq '{"abi": .abi, "bytecode": .bytecode.object}' > artifacts/cluster_connection_abi_byte_code.json cat out/LayerZeroAdapter.sol/LayerZeroAdapter.json | jq '{"abi": .abi, "bytecode": .bytecode.object}' > artifacts/layer_zero_adapter_abi_bytecode.json cat out/WormholeAdapter.sol/WormholeAdapter.json | jq '{"abi": .abi, "bytecode": .bytecode.object}' > artifacts/wormhole_adapter_abi.json diff --git a/contracts/evm/lib/openzeppelin-contracts-upgradeable b/contracts/evm/lib/openzeppelin-contracts-upgradeable index 625fb3c2..4ca003c9 160000 --- a/contracts/evm/lib/openzeppelin-contracts-upgradeable +++ b/contracts/evm/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 625fb3c2b2696f1747ba2e72d1e1113066e6c177 +Subproject commit 4ca003c9635d2c16756cf8c9db6760e2d3653dee diff --git a/contracts/evm/test/adapters/ClusterConnection.t.sol b/contracts/evm/test/adapters/ClusterConnection.t.sol new file mode 100644 index 00000000..2612acab --- /dev/null +++ b/contracts/evm/test/adapters/ClusterConnection.t.sol @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 } from "forge-std/Test.sol"; +import {LZEndpointMock} from "@lz-contracts/mocks/LZEndpointMock.sol"; +import "@xcall/contracts/adapters/ClusterConnection.sol"; +import "@xcall/contracts/xcall/CallService.sol"; +import "@xcall/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol"; +import "@xcall/utils/Types.sol"; +import "@iconfoundation/xcall-solidity-library/utils/RLPEncode.sol"; + +contract ClusterConnectionTest is Test { + + using RLPEncode for bytes; + using RLPEncode for string; + using RLPEncode for uint256; + using RLPEncodeStruct for Types.CSMessage; + using RLPEncodeStruct for Types.CSMessageRequestV2; + + event CallExecuted(uint256 indexed _reqId, int _code, string _msg); + + event RollbackExecuted(uint256 indexed _sn); + + event Message(string targetNetwork, int256 sn, bytes msg); + + event ResponseOnHold(uint256 indexed _sn); + + MultiProtocolSampleDapp dappSource; + MultiProtocolSampleDapp dappTarget; + + CallService xCallSource; + CallService xCallTarget; + + ClusterConnection adapterSource; + ClusterConnection adapterTarget; + + address public sourceRelayer; + address public destinationRelayer; + + string public nidSource = "icon.local"; + string public nidTarget = "evm.local"; + + address public owner = address(uint160(uint256(keccak256("owner")))); + address public admin = address(uint160(uint256(keccak256("admin")))); + address public user = address(uint160(uint256(keccak256("user")))); + + event CallMessage( + string indexed _from, + string indexed _to, + uint256 indexed _sn, + uint256 _reqId, + bytes _data + ); + + address public source_relayer = + address(uint160(uint256(keccak256("source_relayer")))); + address public destination_relayer = + address(uint160(uint256(keccak256("destination_relayer")))); + + function _setupSource() internal { + console2.log("------>setting up source<-------"); + xCallSource = new CallService(); + xCallSource.initialize(nidSource); + + dappSource = new MultiProtocolSampleDapp(); + dappSource.initialize(address(xCallSource)); + + adapterSource = new ClusterConnection(); + adapterSource.initialize(source_relayer, address(xCallSource)); + + xCallSource.setDefaultConnection(nidTarget, address(adapterSource)); + + console2.log(ParseAddress.toString(address(xCallSource))); + console2.log(ParseAddress.toString(address(user))); + } + + function _setupTarget() internal { + console2.log("------>setting up target<-------"); + + xCallTarget = new CallService(); + xCallTarget.initialize(nidTarget); + + dappTarget = new MultiProtocolSampleDapp(); + dappTarget.initialize(address(xCallTarget)); + + adapterTarget = new ClusterConnection(); + adapterTarget.initialize(destination_relayer, address(xCallTarget)); + + xCallTarget.setDefaultConnection(nidSource, address(adapterTarget)); + } + + /** + * @dev Sets up the initial state for the test. + */ + function setUp() public { + vm.startPrank(owner); + + _setupSource(); + _setupTarget(); + + vm.stopPrank(); + + // deal some gas + vm.deal(admin, 10 ether); + vm.deal(user, 10 ether); + } + + function testSetAdmin() public { + vm.prank(owner); + adapterSource.setAdmin(user); + assertEq(adapterSource.admin(), user); + } + + function testSetAdminUnauthorized() public { + vm.prank(user); + vm.expectRevert("OnlyAdmin"); + adapterSource.setAdmin(user); + } + + function testSendMessage() public { + vm.startPrank(user); + string memory to = NetworkAddress.networkAddress( + nidTarget, + ParseAddress.toString(address(dappTarget)) + ); + + uint256 cost = adapterSource.getFee(nidTarget, false); + + bytes memory data = bytes("test"); + bytes memory rollback = bytes(""); + + dappSource.sendMessage{value: cost}(to, data, rollback); + vm.stopPrank(); + } + + function testRevertMessage() public { + vm.startPrank(destination_relayer); + vm.expectRevert("CallRequestNotFound"); + adapterTarget.revertMessage(1); + vm.stopPrank(); + } + + function testRevertMessageUnauthorized() public { + vm.startPrank(user); + vm.expectRevert("OnlyRelayer"); + adapterTarget.revertMessage(1); + vm.stopPrank(); + } + + function testSetFees() public { + vm.prank(source_relayer); + adapterSource.setFee(nidTarget, 5 ether, 5 ether); + + assertEq(adapterSource.getFee(nidTarget, true), 10 ether); + assertEq(adapterSource.getFee(nidTarget, false), 5 ether); + } + + function testSetFeesUnauthorized() public { + vm.prank(user); + + vm.expectRevert("OnlyRelayer"); + adapterSource.setFee(nidTarget, 5 ether, 5 ether); + } + + function testClaimFeesUnauthorized() public { + vm.prank(user); + + vm.expectRevert("OnlyRelayer"); + adapterSource.claimFees(); + } + + function testClaimFees() public { + testSetFees(); + vm.startPrank(user); + string memory to = NetworkAddress.networkAddress( + nidTarget, + ParseAddress.toString(address(dappTarget)) + ); + + uint256 cost = adapterSource.getFee(nidTarget, true); + + bytes memory data = bytes("test"); + bytes memory rollback = bytes("rollback"); + + dappSource.sendMessage{value: cost}(to, data, rollback); + vm.stopPrank(); + + assert(address(adapterSource).balance == 10 ether); + + vm.startPrank(source_relayer); + adapterSource.claimFees(); + vm.stopPrank(); + + assert(source_relayer.balance == 10 ether); + } + + function testRecvMessageWithMultiSignatures() public { + bytes memory data = bytes("test"); + string memory iconDapp = NetworkAddress.networkAddress( + nidSource, + "0xa" + ); + Types.CSMessageRequestV2 memory request = Types.CSMessageRequestV2( + iconDapp, + ParseAddress.toString(address(dappSource)), + 1, + Types.CALL_MESSAGE_TYPE, + data, + new string[](0) + ); + Types.CSMessage memory message = Types.CSMessage( + Types.CS_REQUEST, + request.encodeCSMessageRequestV2() + ); + uint256 pk = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + uint256 pk2 = 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a; + uint256 pk3 = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; + uint256 pk4 = 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6; + bytes32 hash = getMessageHash(nidSource, 1, RLPEncodeStruct.encodeCSMessage(message)); + vm.startPrank(owner); + bytes[] memory validators = new bytes[](4); + validators[0] = bytes(hex"048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5"); + validators[1] = bytes(hex"04bf6ee64a8d2fdc551ec8bb9ef862ef6b4bcb1805cdc520c3aa5866c0575fd3b514c5562c3caae7aec5cd6f144b57135c75b6f6cea059c3d08d1f39a9c227219d"); + validators[2] = bytes(hex"04ba5734d8f7091719471e7f7ed6b9df170dc70cc661ca05e688601ad984f068b0d67351e5f06073092499336ab0839ef8a521afd334e53807205fa2f08eec74f4"); + validators[3] = bytes(hex"043255458e24278e31d5940f304b16300fdff3f6efd3e2a030b5818310ac67af45e28d057e6a332d07e0c5ab09d6947fd4eed1a646edbf224e2d2fec6f49f90abc"); + adapterTarget.updateValidators(validators, 4); + adapterTarget.listValidators(); + vm.stopPrank(); + + vm.startPrank(destination_relayer); + vm.expectEmit(); + emit CallMessage(iconDapp, ParseAddress.toString(address(dappSource)), 1, 1, data); + vm.expectCall(address(xCallTarget), abi.encodeCall(xCallTarget.handleMessage, (nidSource,RLPEncodeStruct.encodeCSMessage(message)))); + bytes[] memory signatures = new bytes[](4) ; + signatures[0] = signMessage(pk,hash); + signatures[1] = signMessage(pk2,hash); + signatures[2] = signMessage(pk3,hash); + signatures[3] = signMessage(pk4,hash); + adapterTarget.recvMessageWithSignatures( + nidSource, + 1, + RLPEncodeStruct.encodeCSMessage(message), + signatures + ); + vm.stopPrank(); + } + + function signMessage(uint256 pk,bytes32 hash) private pure returns (bytes memory){ + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, hash); + address signer = vm.addr(pk); + bytes memory signature = combineSignature(r,s,v); + + address recoverSigner=ecrecover(hash,v,r,s); + return signature; + } + + function combineSignature(bytes32 r, bytes32 s, uint8 v) private pure returns (bytes memory) { + return abi.encodePacked(r, s, v); + } + + function hexStringToUint256(string memory hexString) public pure returns (uint256) { + bytes memory hexBytes = bytes(hexString); + uint256 number = 0; + + for (uint i = 0; i < hexBytes.length; i++) { + uint8 hexDigit = uint8(hexBytes[i]); + + // Convert ASCII characters 0-9 and A-F or a-f to their numeric values + if (hexDigit >= 48 && hexDigit <= 57) { + number = number * 16 + (hexDigit - 48); // 0-9 + } else if (hexDigit >= 65 && hexDigit <= 70) { + number = number * 16 + (hexDigit - 55); // A-F + } else if (hexDigit >= 97 && hexDigit <= 102) { + number = number * 16 + (hexDigit - 87); // a-f + } else { + revert("Invalid hex character"); + } + } + return number; + } + + function testAddValidator() public { + vm.startPrank(owner); + + bytes[] memory validators = new bytes[](4); + validators[0] = bytes(hex"048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5"); + validators[1] = bytes(hex"04bf6ee64a8d2fdc551ec8bb9ef862ef6b4bcb1805cdc520c3aa5866c0575fd3b514c5562c3caae7aec5cd6f144b57135c75b6f6cea059c3d08d1f39a9c227219d"); + validators[2] = bytes(hex"04ba5734d8f7091719471e7f7ed6b9df170dc70cc661ca05e688601ad984f068b0d67351e5f06073092499336ab0839ef8a521afd334e53807205fa2f08eec74f4"); + validators[3] = bytes(hex"043255458e24278e31d5940f304b16300fdff3f6efd3e2a030b5818310ac67af45e28d057e6a332d07e0c5ab09d6947fd4eed1a646edbf224e2d2fec6f49f90abc"); + adapterTarget.updateValidators(validators, 4); + console2.log(adapterTarget.listValidators()[0]); + assertEq(4, adapterTarget.listValidators().length); + vm.stopPrank(); + } + + + function testRequiredCount() public { + vm.startPrank(owner); + adapterTarget.setRequiredValidatorCount(3); + assertEq(3, adapterTarget.getRequiredValidatorCount()); + vm.stopPrank(); + } + + function testRecvMessageWithMultiSignatureNotEnoughSign() public { + bytes memory data = bytes("test"); + string memory iconDapp = NetworkAddress.networkAddress( + nidSource, + "0xa" + ); + Types.CSMessageRequestV2 memory request = Types.CSMessageRequestV2( + iconDapp, + ParseAddress.toString(address(dappSource)), + 1, + Types.CALL_MESSAGE_TYPE, + data, + new string[](0) + ); + Types.CSMessage memory message = Types.CSMessage( + Types.CS_REQUEST, + request.encodeCSMessageRequestV2() + ); + uint256 pk = hexStringToUint256("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); + bytes32 hash = keccak256(RLPEncodeStruct.encodeCSMessage(message)); + vm.startPrank(owner); + bytes[] memory validators = new bytes[](2); + validators[0] = bytes(hex"048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5"); + validators[1] = bytes(hex"04bf6ee64a8d2fdc551ec8bb9ef862ef6b4bcb1805cdc520c3aa5866c0575fd3b514c5562c3caae7aec5cd6f144b57135c75b6f6cea059c3d08d1f39a9c227219d"); + adapterTarget.updateValidators(validators, 2); + vm.stopPrank(); + vm.startPrank(destination_relayer); + bytes[] memory signatures = new bytes[](2) ; + signatures[0] = signMessage(pk,hash); + signatures[1] = signMessage(pk,hash); + vm.expectRevert("Not enough valid signatures passed"); + adapterTarget.recvMessageWithSignatures( + nidSource, + 1, + RLPEncodeStruct.encodeCSMessage(message), + signatures + ); + vm.stopPrank(); + } + + function getMessageHash(string memory srcNetwork, uint256 _connSn, bytes memory _msg) internal pure returns (bytes32) { + bytes memory rlp = abi.encodePacked( + srcNetwork.encodeString(), + _connSn.encodeUint(), + _msg.encodeBytes() + ).encodeList(); + return keccak256(rlp); + } +} diff --git a/contracts/javascore/aggregator/build.gradle b/contracts/javascore/aggregator/build.gradle new file mode 100644 index 00000000..426026ca --- /dev/null +++ b/contracts/javascore/aggregator/build.gradle @@ -0,0 +1,41 @@ +version = '0.1.0' + +dependencies { + testImplementation 'foundation.icon:javaee-unittest:0.11.1' + testImplementation project(':test-lib') +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +optimizedJar { + mainClassName = 'relay.aggregator.RelayAggregator' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + mainnet { + uri = 'https://ctz.solidwallet.io/api/v3' + nid = 0x1 + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_admin', "hxb6b5791be0b5ef67063b3c10b840fb81514db2fd") + } +} \ No newline at end of file diff --git a/contracts/javascore/aggregator/src/main/java/relay/aggregator/Packet.java b/contracts/javascore/aggregator/src/main/java/relay/aggregator/Packet.java new file mode 100644 index 00000000..0568de37 --- /dev/null +++ b/contracts/javascore/aggregator/src/main/java/relay/aggregator/Packet.java @@ -0,0 +1,176 @@ +package relay.aggregator; + +import java.math.BigInteger; + +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +public class Packet { + + /** + * The ID of the source network (chain) from where the packet originated. + */ + private final String srcNetwork; + + /** + * The contract address on the source network (chain). + */ + private final String srcContractAddress; + + /** + * The sequence number of the packet in the source network (chain). + */ + private final BigInteger srcSn; + + /** + * The source height of the packet in the source network (chain). + */ + private final BigInteger srcHeight; + + /** + * The ID of the destination network (chain) where the packet is being sent. + */ + private final String dstNetwork; + + /** + * The contract address on the destination network (chain). + */ + private final String dstContractAddress; + + /** + * The payload data associated with this packet. + */ + private final byte[] data; + + /** + * Constructs a new {@code Packet} object with the specified {@code PacketID}, + * destination network, and data. + * All parameters must be non-null. + * + * @param id the unique identifier for the packet. + * @param dstNetwork the ID of the destination network (chain). + * @param data the payload data for this packet. + * @throws IllegalArgumentException if {@code srcNetwork}, + * {@code srcContractAddress}, {@code srcSn}, + * {@code srcHeight}, + * {@code dstNetwork}, + * {@code dstContractAddress}, or {@code data} + * is + * {@code null}. + */ + public Packet(String srcNetwork, String srcContractAddress, BigInteger srcSn, BigInteger srcHeight, + String dstNetwork, String dstContractAddress, + byte[] data) { + Boolean isIllegalArg = srcNetwork == null || srcContractAddress == null || srcSn == null + || srcHeight == null || dstNetwork == null || dstContractAddress == null || data == null; + Context.require(!isIllegalArg, + "srcNetwork, contractAddress, srcSn, srcHeight, dstNetwork, and data cannot be null"); + if (isIllegalArg) { + } + this.srcNetwork = srcNetwork; + this.srcContractAddress = srcContractAddress; + this.srcSn = srcSn; + this.srcHeight = srcHeight; + this.dstNetwork = dstNetwork; + this.dstContractAddress = dstContractAddress; + this.data = data; + } + + public String getId() { + return createId(this.srcNetwork, this.srcContractAddress, this.srcSn); + } + + public static String createId(String srcNetwork, String contractAddress, BigInteger srcSn) { + return srcNetwork + "/" + contractAddress + "/" + srcSn.toString(); + } + + /** + * Returns the source network (chain) from where the packet originated. + * + * @return the source network ID. + */ + public String getSrcNetwork() { + return srcNetwork; + } + + /** + * Returns the contract address on the source network (chain). + * + * @return the source contract address. + */ + public String getSrcContractAddress() { + return srcContractAddress; + } + + /** + * Returns the sequence number of the packet in the source network (chain). + * + * @return the sequence number. + */ + public BigInteger getSrcSn() { + return srcSn; + } + + /** + * Returns the height of the packet in the source network (chain). + * + * @return the source height. + */ + public BigInteger getSrcHeight() { + return srcHeight; + } + + /** + * Returns the destination network (chain) where the packet is being sent. + * + * @return the destination network ID. + */ + public String getDstNetwork() { + return dstNetwork; + } + + /** + * Returns the contract address on the destination network (chain). + * + * @return the destination contract address. + */ + public String getDstContractAddress() { + return dstContractAddress; + } + + /** + * Returns a copy of the data associated with this packet. + * + * @return a byte array containing the packet data. + */ + public byte[] getData() { + return data; + } + + public static void writeObject(ObjectWriter w, Packet p) { + w.beginList(7); + w.write(p.srcNetwork); + w.write(p.srcContractAddress); + w.write(p.srcSn); + w.write(p.srcHeight); + w.write(p.dstNetwork); + w.write(p.dstContractAddress); + w.writeNullable(p.data); + w.end(); + } + + public static Packet readObject(ObjectReader r) { + r.beginList(); + Packet p = new Packet( + r.readString(), + r.readString(), + r.readBigInteger(), + r.readBigInteger(), + r.readString(), + r.readString(), + r.readNullable(byte[].class)); + r.end(); + return p; + } +} diff --git a/contracts/javascore/aggregator/src/main/java/relay/aggregator/RelayAggregator.java b/contracts/javascore/aggregator/src/main/java/relay/aggregator/RelayAggregator.java new file mode 100644 index 00000000..ec475a99 --- /dev/null +++ b/contracts/javascore/aggregator/src/main/java/relay/aggregator/RelayAggregator.java @@ -0,0 +1,317 @@ +/* + * Copyright 2022 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package relay.aggregator; + +import java.math.BigInteger; + +import score.Context; +import score.Address; +import score.ArrayDB; +import score.VarDB; +import score.DictDB; +import score.BranchDB; +import score.ByteArrayObjectWriter; +import score.annotation.EventLog; +import score.annotation.External; +import score.ObjectReader; +import scorex.util.ArrayList; +import scorex.util.HashMap; + +public class RelayAggregator { + private final Integer DEFAULT_SIGNATURE_THRESHOLD = 1; + + private final VarDB signatureThreshold = Context.newVarDB("signatureThreshold", Integer.class); + + private final VarDB
admin = Context.newVarDB("admin", Address.class); + + private final ArrayDB
relayers = Context.newArrayDB("relayers", Address.class); + private final DictDB relayersLookup = Context.newDictDB("relayersLookup", Boolean.class); + + private final DictDB packets = Context.newDictDB("packets", Packet.class); + + private final BranchDB> signatures = Context.newBranchDB("signatures", + byte[].class); + + public RelayAggregator(Address _admin) { + if (admin.get() == null) { + admin.set(_admin); + signatureThreshold.set(DEFAULT_SIGNATURE_THRESHOLD); + addRelayer(_admin); + } + } + + @External + public void setAdmin(Address _admin) { + adminOnly(); + + Context.require(admin.get() != _admin, "admin already set"); + + // add new admin as relayer + addRelayer(_admin); + + // remove old admin from relayer list + removeRelayer(admin.get()); + + admin.set(_admin); + } + + @External(readonly = true) + public Address getAdmin() { + return admin.get(); + } + + @External + public void setSignatureThreshold(int threshold) { + adminOnly(); + Context.require(threshold > 0 && threshold <= relayers.size(), + "threshold value should be at least 1 and not greater than relayers size"); + signatureThreshold.set(threshold); + } + + @External(readonly = true) + public int getSignatureThreshold() { + return signatureThreshold.get(); + } + + @External(readonly = true) + public Address[] getRelayers() { + Address[] rlrs = new Address[relayers.size()]; + for (int i = 0; i < relayers.size(); i++) { + rlrs[i] = relayers.get(i); + } + return rlrs; + } + + @External + public void setRelayers(Address[] newRelayers, int threshold) { + adminOnly(); + + if (newRelayers.length > 0) { + HashMap newRelayersMap = new HashMap(); + for (Address newRelayer : newRelayers) { + newRelayersMap.put(newRelayer, true); + addRelayer(newRelayer); + } + + Address adminAdrr = admin.get(); + for (int i = 0; i < relayers.size(); i++) { + Address oldRelayer = relayers.get(i); + if (!oldRelayer.equals(adminAdrr) && !newRelayersMap.containsKey(oldRelayer)) { + removeRelayer(oldRelayer); + } + } + } + + Context.require(threshold > 0 && threshold <= relayers.size(), + "threshold value should be at least 1 and not greater than relayers size"); + + signatureThreshold.set(threshold); + } + + @External(readonly = true) + public boolean packetSubmitted( + Address relayer, + String srcNetwork, + String srcContractAddress, + BigInteger srcSn) { + String pktID = Packet.createId(srcNetwork, srcContractAddress, srcSn); + byte[] existingSign = signatures.at(pktID).get(relayer); + return existingSign != null; + } + + @External + public void submitPacket( + String srcNetwork, + String srcContractAddress, + BigInteger srcSn, + BigInteger srcHeight, + String dstNetwork, + String dstContractAddress, + byte[] data, + byte[] signature) { + + relayersOnly(); + + Packet pkt = new Packet(srcNetwork, srcContractAddress, srcSn, srcHeight, dstNetwork, dstContractAddress, data); + String pktID = pkt.getId(); + + if (packets.get(pktID) == null) { + packets.set(pktID, pkt); + if (signatureThreshold.get() > 1) { + PacketRegistered( + pkt.getSrcNetwork(), + pkt.getSrcContractAddress(), + pkt.getSrcSn(), + pkt.getSrcHeight(), + pkt.getDstNetwork(), + pkt.getDstContractAddress(), + pkt.getData()); + } + + } + + byte[] existingSign = signatures.at(pktID).get(Context.getCaller()); + Context.require(existingSign == null, "Signature already exists"); + + setSignature(pktID, Context.getCaller(), signature); + + if (signatureThresholdReached(pktID)) { + byte[][] sigs = getSignatures(srcNetwork, srcContractAddress, srcSn); + byte[] encodedSigs = serializeSignatures(sigs); + PacketAcknowledged( + pkt.getSrcNetwork(), + pkt.getSrcContractAddress(), + pkt.getSrcSn(), + pkt.getSrcHeight(), + pkt.getDstNetwork(), + pkt.getDstContractAddress(), + pkt.getData(), + encodedSigs); + removePacket(pktID); + } + } + + private byte[][] getSignatures(String srcNetwork, String srcContractAddress, BigInteger srcSn) { + String pktID = Packet.createId(srcNetwork, srcContractAddress, srcSn); + DictDB signDict = signatures.at(pktID); + ArrayList signatureList = new ArrayList(); + + for (int i = 0; i < relayers.size(); i++) { + Address relayer = relayers.get(i); + byte[] sign = signDict.get(relayer); + if (sign != null) { + signatureList.add(sign); + } + } + + byte[][] sigs = new byte[signatureList.size()][]; + for (int i = 0; i < signatureList.size(); i++) { + sigs[i] = signatureList.get(i); + } + return sigs; + } + + protected void setSignature(String pktID, Address addr, byte[] sign) { + signatures.at(pktID).set(addr, sign); + } + + protected static byte[] serializeSignatures(byte[][] sigs) { + ByteArrayObjectWriter w = Context.newByteArrayObjectWriter("RLPn"); + w.beginList(sigs.length); + + for (byte[] sig : sigs) { + w.write(sig); + } + + w.end(); + return w.toByteArray(); + } + + protected static byte[][] deserializeSignatures(byte[] encodedSigs) { + ObjectReader r = Context.newByteArrayObjectReader("RLPn", encodedSigs); + + ArrayList sigList = new ArrayList<>(); + + r.beginList(); + while (r.hasNext()) { + sigList.add(r.readByteArray()); + } + r.end(); + + byte[][] sigs = new byte[sigList.size()][]; + for (int i = 0; i < sigList.size(); i++) { + sigs[i] = sigList.get(i); + } + + return sigs; + } + + private void adminOnly() { + Context.require(Context.getCaller().equals(admin.get()), "Unauthorized: caller is not the leader relayer"); + } + + private void relayersOnly() { + Address caller = Context.getCaller(); + Boolean isRelayer = relayersLookup.get(caller); + Context.require(isRelayer != null && isRelayer, "Unauthorized: caller is not a registered relayer"); + } + + private void addRelayer(Address newRelayer) { + if (relayersLookup.get(newRelayer) == null) { + relayers.add(newRelayer); + relayersLookup.set(newRelayer, true); + } + } + + private void removeRelayer(Address oldRelayer) { + if (relayersLookup.get(oldRelayer)) { + relayersLookup.set(oldRelayer, null); + Address top = relayers.pop(); + for (int i = 0; i < relayers.size(); i++) { + if (oldRelayer.equals(relayers.get(i))) { + relayers.set(i, top); + break; + } + } + } + } + + private Boolean signatureThresholdReached(String pktID) { + int noOfSignatures = 0; + for (int i = 0; i < relayers.size(); i++) { + Address relayer = relayers.get(i); + byte[] relayerSign = signatures.at(pktID).get(relayer); + if (relayerSign != null) { + noOfSignatures++; + } + } + return noOfSignatures >= signatureThreshold.get(); + } + + private void removePacket(String pktID) { + packets.set(pktID, null); + DictDB signDict = signatures.at(pktID); + + for (int i = 0; i < relayers.size(); i++) { + Address relayer = relayers.get(i); + signDict.set(relayer, null); + } + } + + @EventLog(indexed = 2) + public void PacketRegistered( + String srcNetwork, + String srcContractAddress, + BigInteger srcSn, + BigInteger srcHeight, + String dstNetwork, + String dstContractAddress, + byte[] data) { + } + + @EventLog(indexed = 2) + public void PacketAcknowledged( + String srcNetwork, + String srcContractAddress, + BigInteger srcSn, + BigInteger srcHeight, + String dstNetwork, + String dstContractAddress, + byte[] data, + byte[] signatures) { + } +} \ No newline at end of file diff --git a/contracts/javascore/aggregator/src/test/java/relay/aggregator/RelayAggregatorTest.java b/contracts/javascore/aggregator/src/test/java/relay/aggregator/RelayAggregatorTest.java new file mode 100644 index 00000000..0da3391f --- /dev/null +++ b/contracts/javascore/aggregator/src/test/java/relay/aggregator/RelayAggregatorTest.java @@ -0,0 +1,367 @@ +package relay.aggregator; + +import java.math.BigInteger; +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import score.Address; +import score.Context; +import score.UserRevertedException; +import scorex.util.HashSet; + +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; + +import foundation.icon.icx.KeyWallet; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class RelayAggregatorTest extends TestBase { + private final ServiceManager sm = getServiceManager(); + + private KeyWallet admin; + private Account adminAc; + + private KeyWallet relayerOne; + private Account relayerOneAc; + + private KeyWallet relayerTwo; + private Account relayerTwoAc; + + private KeyWallet relayerThree; + private Account relayerThreeAc; + + private KeyWallet relayerFour; + private Account relayerFourAc; + + private Score aggregator; + private RelayAggregator aggregatorSpy; + + @BeforeEach + void setup() throws Exception { + admin = KeyWallet.create(); + adminAc = sm.getAccount(Address.fromString(admin.getAddress().toString())); + + relayerOne = KeyWallet.create(); + relayerOneAc = sm.getAccount(Address.fromString(relayerOne.getAddress().toString())); + + relayerTwo = KeyWallet.create(); + relayerTwoAc = sm.getAccount(Address.fromString(relayerTwo.getAddress().toString())); + + relayerThree = KeyWallet.create(); + relayerThreeAc = sm.getAccount(Address.fromString(relayerThree.getAddress().toString())); + + relayerFour = KeyWallet.create(); + relayerFourAc = sm.getAccount(Address.fromString(relayerFour.getAddress().toString())); + + aggregator = sm.deploy(adminAc, RelayAggregator.class, adminAc.getAddress()); + + Address[] relayers = new Address[] { adminAc.getAddress(), relayerOneAc.getAddress(), relayerTwoAc.getAddress(), + relayerThreeAc.getAddress() }; + + aggregator.invoke(adminAc, "setRelayers", (Object) relayers, 2); + + aggregatorSpy = (RelayAggregator) spy(aggregator.getInstance()); + aggregator.setInstance(aggregatorSpy); + } + + @Test + public void testSetAdmin() { + Address oldAdmin = (Address) aggregator.call("getAdmin"); + + Account newAdminAc = sm.createAccount(); + aggregator.invoke(adminAc, "setAdmin", newAdminAc.getAddress()); + + Address newAdmin = (Address) aggregator.call("getAdmin"); + assertEquals(newAdminAc.getAddress(), newAdmin); + + Address[] relayers = (Address[]) aggregator.call("getRelayers"); + + boolean containsNewAdmin = Arrays.asList(relayers).contains(newAdmin); + boolean containsOldAdmin = Arrays.asList(relayers).contains(oldAdmin); + + assertTrue(containsNewAdmin); + assertFalse(containsOldAdmin); + } + + @Test + public void testSetAdmin_unauthorized() { + Account normalAc = sm.createAccount(); + Account newAdminAc = sm.createAccount(); + + Executable action = () -> aggregator.invoke(normalAc, "setAdmin", newAdminAc.getAddress()); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): Unauthorized: caller is not the leader relayer", e.getMessage()); + } + + @Test + public void testSetSignatureThreshold() { + int threshold = 3; + aggregator.invoke(adminAc, "setSignatureThreshold", threshold); + + Integer result = (Integer) aggregator.call("getSignatureThreshold"); + assertEquals(threshold, result); + } + + @Test + public void testSetSignatureThreshold_unauthorised() { + int threshold = 3; + + Executable action = () -> aggregator.invoke(relayerOneAc, + "setSignatureThreshold", threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): Unauthorized: caller is not the leader relayer", + e.getMessage()); + } + + @Test + public void testSetRelayers() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 3; + aggregator.invoke(adminAc, "setRelayers", (Object) newRelayers, threshold); + + Address[] updatedRelayers = (Address[]) aggregator.call("getRelayers"); + + Address[] expectedRelayers = new Address[] { adminAc.getAddress(), relayerThreeAc.getAddress(), + relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + HashSet
updatedRelayersSet = new HashSet
(); + for (Address rlr : updatedRelayers) { + updatedRelayersSet.add(rlr); + } + + HashSet
expectedRelayersSet = new HashSet
(); + for (Address rlr : expectedRelayers) { + expectedRelayersSet.add(rlr); + } + + assertEquals(expectedRelayersSet, updatedRelayersSet); + + Integer result = (Integer) aggregator.call("getSignatureThreshold"); + assertEquals(threshold, result); + } + + @Test + public void testSetRelayers_unauthorized() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 3; + Executable action = () -> aggregator.invoke(relayerOneAc, "setRelayers", + (Object) newRelayers, threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): Unauthorized: caller is not the leader relayer", + e.getMessage()); + + } + + @Test + public void testSetRelayers_invalidThreshold() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 5; + Executable action = () -> aggregator.invoke(adminAc, "setRelayers", + (Object) newRelayers, threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): threshold value should be at least 1 and not greater than relayers size", + e.getMessage()); + + } + + @Test + public void testSetRelayers_invalidThresholdZero() { + Account relayerFiveAc = sm.createAccount(); + Address[] newRelayers = new Address[] { relayerThreeAc.getAddress(), relayerFourAc.getAddress(), + relayerFiveAc.getAddress() }; + + Integer threshold = 0; + Executable action = () -> aggregator.invoke(adminAc, "setRelayers", + (Object) newRelayers, threshold); + UserRevertedException e = assertThrows(UserRevertedException.class, action); + + assertEquals("Reverted(0): threshold value should be at least 1 and not greater than relayers size", + e.getMessage()); + + } + + @Test + public void testPacketSubmitted_true() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerOne.sign(dataHash); + + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + sign); + + boolean submitted = (boolean) aggregator.call("packetSubmitted", + relayerOneAc.getAddress(), srcNetwork, + srcContractAddress, srcSn); + assertEquals(submitted, true); + } + + @Test + public void testPacketSubmitted_false() throws Exception { + String srcNetwork = "0x2.icon"; + BigInteger srcSn = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + + boolean submitted = (boolean) aggregator.call("packetSubmitted", + relayerOneAc.getAddress(), srcNetwork, + srcContractAddress, srcSn); + assertEquals(submitted, false); + } + + @Test + public void testSubmitPacket() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerOne.sign(dataHash); + + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + sign); + + String pktID = Packet.createId(srcNetwork, srcContractAddress, srcSn); + verify(aggregatorSpy).PacketRegistered(srcNetwork, srcContractAddress, srcSn, + srcHeight, dstNetwork, + dstContractAddress, data); + verify(aggregatorSpy).setSignature(pktID, relayerOneAc.getAddress(), sign); + } + + @Test + public void testSubmitPacket_thresholdReached() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + + byte[] signAdmin = admin.sign(dataHash); + aggregator.invoke(adminAc, "submitPacket", srcNetwork, srcContractAddress, + srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + signAdmin); + + byte[] signOne = relayerOne.sign(dataHash); + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, + data, + signOne); + + byte[][] sigs = new byte[2][]; + sigs[0] = signAdmin; + sigs[1] = signOne; + + byte[] encodedSigs = RelayAggregator.serializeSignatures(sigs); + byte[][] decodedSigs = RelayAggregator.deserializeSignatures(encodedSigs); + + assertArrayEquals(signAdmin, decodedSigs[0]); + assertArrayEquals(signOne, decodedSigs[1]); + + verify(aggregatorSpy).PacketAcknowledged(srcNetwork, srcContractAddress, + srcSn, srcHeight, dstNetwork, + dstContractAddress, data, + encodedSigs); + } + + @Test + public void testSubmitPacket_unauthorized() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerFour.sign(dataHash); + + Executable action = () -> aggregator.invoke(relayerFourAc, "submitPacket", + srcNetwork, srcContractAddress, + srcSn, + srcHeight, dstNetwork, dstContractAddress, data, sign); + + UserRevertedException e = assertThrows(UserRevertedException.class, action); + assertEquals("Reverted(0): Unauthorized: caller is not a registered relayer", + e.getMessage()); + } + + @Test + public void testSubmitPacket_duplicate() throws Exception { + String srcNetwork = "0x2.icon"; + String dstNetwork = "sui"; + BigInteger srcSn = BigInteger.ONE; + BigInteger srcHeight = BigInteger.ONE; + String srcContractAddress = "hxjuiod"; + String dstContractAddress = "hxjuiod"; + byte[] data = new byte[] { 0x01, 0x02 }; + + aggregator.invoke(adminAc, "setSignatureThreshold", 2); + + byte[] dataHash = Context.hash("sha-256", data); + byte[] sign = relayerOne.sign(dataHash); + + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, data, sign); + + Executable action = () -> aggregator.invoke(relayerOneAc, "submitPacket", + srcNetwork, srcContractAddress, srcSn, + srcHeight, dstNetwork, dstContractAddress, + data, sign); + ; + UserRevertedException e = assertThrows(UserRevertedException.class, action); + assertEquals("Reverted(0): Signature already exists", e.getMessage()); + } +} diff --git a/contracts/javascore/cluster-connection/build.gradle b/contracts/javascore/cluster-connection/build.gradle new file mode 100644 index 00000000..91358eed --- /dev/null +++ b/contracts/javascore/cluster-connection/build.gradle @@ -0,0 +1,55 @@ +version = '0.1.0' + +dependencies { + implementation project(':xcall-lib') + + testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' + testImplementation 'foundation.icon:javaee-unittest:0.11.1' + testImplementation project(':test-lib') +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +optimizedJar { + dependsOn(project(':xcall-lib').jar) + mainClassName = 'xcall.adapter.cluster.ClusterConnection' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + berlin { + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + } + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + mainnet { + uri = 'https://ctz.solidwallet.io/api/v3' + nid = 0x1 + } + uat { + uri = project.findProperty('uat.host') as String + nid = property('uat.nid') as Integer + to = "$mockDApp"?:null + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_relayer', "hxb6b5791be0b5ef67063b3c10b840fb81514db2fd") + arg('_xCall', "$xCall") + } +} \ No newline at end of file diff --git a/contracts/javascore/cluster-connection/src/main/java/xcall/adapter/cluster/ClusterConnection.java b/contracts/javascore/cluster-connection/src/main/java/xcall/adapter/cluster/ClusterConnection.java new file mode 100644 index 00000000..6546187f --- /dev/null +++ b/contracts/javascore/cluster-connection/src/main/java/xcall/adapter/cluster/ClusterConnection.java @@ -0,0 +1,364 @@ +/* + * Copyright 2022 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xcall.adapter.cluster; + +import java.math.BigInteger; +import score.Context; + +import score.Address; +import score.BranchDB; +import score.ByteArrayObjectWriter; +import score.DictDB; +import score.VarDB; +import score.ArrayDB; +import scorex.util.ArrayList; + +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Payable; + +import java.util.Arrays; +import java.util.List; + + + +public class ClusterConnection { + protected final VarDB
xCall = Context.newVarDB("callService", Address.class); + protected final VarDB
adminAddress = Context.newVarDB("admin", Address.class); + protected final VarDB
relayerAddress = Context.newVarDB("relayer", Address.class); + protected final VarDB validatorsThreshold = Context.newVarDB("reqValidatorCnt", BigInteger.class); + private final VarDB connSn = Context.newVarDB("connSn", BigInteger.class); + private final ArrayDB validators = Context.newArrayDB("signers", String.class); + + protected final DictDB messageFees = Context.newDictDB("messageFees", BigInteger.class); + protected final DictDB responseFees = Context.newDictDB("responseFees", BigInteger.class); + protected final BranchDB> receipts = Context.newBranchDB("receipts", + Boolean.class); + public ClusterConnection(Address _relayer, Address _xCall) { + if (xCall.get() == null) { + xCall.set(_xCall); + adminAddress.set(Context.getCaller()); + relayerAddress.set(_relayer); + connSn.set(BigInteger.ZERO); + } + } + + /** + * Retrieves the validators. + * + * @return The validators . + */ + @External(readonly = true) + public String[] listValidators() { + String[] sgs = new String[validators.size()]; + for(int i = 0; i < validators.size(); i++) { + sgs[i] = validators.get(i); + } + return sgs; + } + +/** + * Adds a list of validators and sets the validation threshold. + * + * Clears existing validators and adds the provided addresses as validators. + * Ensures that the caller is an admin and that the number of validators + * meets or exceeds the specified threshold. + * + * @param _validators an array of compressed publickey bytes to be added as validators + * @param _threshold the minimum required number of validators + * @throws Exception if the number of validators is less than the threshold + */ + @External + public void updateValidators(byte[][] _validators, BigInteger _threshold) { + OnlyAdmin(); + clearValidators(); + for (byte[] validator : _validators) { + String hexValidator = bytesToHex(validator); + if(!isValidator(hexValidator)) { + validators.add(bytesToHex(validator)); + } + } + Context.require(validators.size() >= _threshold.intValue(), "Not enough validators"); + validatorsThreshold.set(_threshold); + ValidatorSetAdded(_validators.toString(), _threshold); + } + + /** + * Clear the current validators. + * + * This is a private helper method called by addValidator. + */ + private void clearValidators() { + for(int i = 0; i < validators.size(); i++) { + validators.set(i, null); + } + } + +/** + * Checks if the provided compressed pubkey bytes is a validator. + * + * @param validator the compressed publickey bytes to check for validation + * @return true if the compressed pubkey bytes is a validator, false otherwise + */ + private boolean isValidator(String validator) { + for(int i = 0; i < validators.size(); i++) { + if(validator.equals(validators.get(i))) { + return true; + } + } + return false; + } + + @EventLog(indexed = 2) + public void Message(String targetNetwork, BigInteger connSn, byte[] msg) { + } + + @EventLog(indexed = 0) + public void ValidatorSetAdded(String _validators, BigInteger _threshold) { + } + + /** + * Sets the relayer address. + * + * @param _relayer the new admin address + */ + @External + public void setRelayer(Address _relayer) { + OnlyAdmin(); + relayerAddress.set(_relayer); + } + + /** + * Sets the admin address. + * + * @param _admin the new admin address + */ + @External + public void setAdmin(Address _admin) { + OnlyAdmin(); + adminAddress.set(_admin); + } + + /** + * Retrieves the admin address. + * + * @return The admin address. + */ + @External(readonly = true) + public Address admin() { + return adminAddress.get(); + } + + /** + * Sets the required validator count + * + * @param _validatorCnt the new required validator count + */ + @External + public void setRequiredValidatorCount(BigInteger _validatorCnt) { + OnlyAdmin(); + validatorsThreshold.set(_validatorCnt); + } + + /** + * Retrieves the required validator count. + * + * @return The required validator count. + */ + @External(readonly = true) + public BigInteger requiredValidatorCount() { + return validatorsThreshold.get(); + } + + /** + * Sets the fee to the target network + * + * @param networkId String Network Id of target chain + * @param messageFee The fee needed to send a Message + * @param responseFee The fee of the response + */ + @External + public void setFee(String networkId, BigInteger messageFee, BigInteger responseFee) { + OnlyRelayer(); + messageFees.set(networkId, messageFee); + responseFees.set(networkId, responseFee); + } + + /** + * Returns the fee associated with the given destination address. + * + * @param to String Network Id of target chain + * @param response whether the responding fee is included + * @return The fee of sending a message to a given destination network + */ + @External(readonly = true) + public BigInteger getFee(String to, boolean response) { + BigInteger messageFee = messageFees.getOrDefault(to, BigInteger.ZERO); + if (response) { + BigInteger responseFee = responseFees.getOrDefault(to, BigInteger.ZERO); + return messageFee.add(responseFee); + } + return messageFee; + } + + /** + * Sends a message to the specified network. + * + * @param to Network Id of destination network + * @param svc name of the service + * @param sn positive for two-way message, zero for one-way message, negative + * for response(for xcall message) + * @param msg serialized bytes of Service Message + */ + @Payable + @External + public void sendMessage(String to, String svc, BigInteger sn, byte[] msg) { + Context.require(Context.getCaller().equals(xCall.get()), "Only xCall can send messages"); + BigInteger fee = BigInteger.ZERO; + if (sn.compareTo(BigInteger.ZERO) > 0) { + fee = getFee(to, true); + } else if (sn.equals(BigInteger.ZERO)) { + fee = getFee(to, false); + } + + BigInteger nextConnSn = connSn.get().add(BigInteger.ONE); + connSn.set(nextConnSn); + + Context.require(Context.getValue().compareTo(fee) >= 0, "Insufficient balance"); + Message(to, nextConnSn, msg); + } + + /** + * Receives a message from a source network. + * + * @param srcNetwork the source network id from which the message is received + * @param _connSn the serial number of the connection message + * @param msg serialized bytes of Service Message + * @param signatures array of signatures + */ + @External + public void recvMessageWithSignatures(String srcNetwork, BigInteger _connSn, byte[] msg, + byte[][] signatures) { + OnlyRelayer(); + Context.require(signatures.length >= validatorsThreshold.get().intValue(), "Not enough signatures"); + byte[] messageHash = getMessageHash(srcNetwork, _connSn, msg); + List uniqueValidators = new ArrayList<>(); + for (byte[] signature : signatures) { + byte[] validator = getValidator(messageHash, signature); + String hexValidator = bytesToHex(validator); + Context.require(isValidator(hexValidator), "Invalid signature provided"); + if (!uniqueValidators.contains(hexValidator)) { + uniqueValidators.add(hexValidator); + } + } + Context.require(uniqueValidators.size() >= validatorsThreshold.get().intValue(), "Not enough valid signatures"); + recvMessage(srcNetwork, _connSn, msg); + } + + private void recvMessage(String srcNetwork, BigInteger _connSn, byte[] msg) { + Context.require(!receipts.at(srcNetwork).getOrDefault(_connSn, false), "Duplicate Message"); + receipts.at(srcNetwork).set(_connSn, true); + Context.call(xCall.get(), "handleMessage", srcNetwork, msg); + } + + private String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); // Mask with 0xff to handle negative values correctly + if (hex.length() == 1) { + hexString.append('0'); // Add a leading zero if hex length is 1 + } + hexString.append(hex); + } + return hexString.toString(); + } + + private byte[] getValidator(byte[] msg, byte[] sig){ + return Context.recoverKey("ecdsa-secp256k1", msg, sig, false); + } + + /** + * Reverts a message. + * + * @param sn the serial number of xcall message representing the message to + * revert + */ + @External + public void revertMessage(BigInteger sn) { + OnlyRelayer(); + Context.call(xCall.get(), "handleError", sn); + } + + /** + * Claim the fees. + * + */ + @External + public void claimFees() { + OnlyRelayer(); + Context.transfer(relayerAddress.get(), Context.getBalance(Context.getAddress())); + } + + /** + * Get the receipts for a given source network and serial number. + * + * @param srcNetwork the source network id + * @param _connSn the serial number of connection message + * @return the receipt if is has been recived or not + */ + @External(readonly = true) + public boolean getReceipts(String srcNetwork, BigInteger _connSn) { + return receipts.at(srcNetwork).getOrDefault(_connSn, false); + } + + /** + * Checks if the caller of the function is the admin. + * + * @return true if the caller is the admin, false otherwise + */ + private void OnlyRelayer() { + Context.require(Context.getCaller().equals(relayerAddress.get()), "Only relayer can call this function"); + } + + /** + * Checks if the caller of the function is the admin. + * + * @return true if the caller is the admin, false otherwise + */ + private void OnlyAdmin() { + Context.require(Context.getCaller().equals(adminAddress.get()), "Only admin can call this function"); + } + + /** + * Gets the hash of a message. + * + * @param srcNetwork the source network id + * @param _connSn the serial number of connection message + * @param msg the message to hash + * @return the hash of the message + */ + private byte[] getMessageHash(String srcNetwork, BigInteger _connSn, byte[] msg) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(3); + writer.write(srcNetwork); + writer.write(_connSn); + writer.write(msg); + writer.end(); + return Context.hash("keccak-256", writer.toByteArray()); + } + +} \ No newline at end of file diff --git a/contracts/javascore/cluster-connection/src/test/java/xcall/adapter/cluster/ClusterConnectionTest.java b/contracts/javascore/cluster-connection/src/test/java/xcall/adapter/cluster/ClusterConnectionTest.java new file mode 100644 index 00000000..c856f5a7 --- /dev/null +++ b/contracts/javascore/cluster-connection/src/test/java/xcall/adapter/cluster/ClusterConnectionTest.java @@ -0,0 +1,258 @@ +package xcall.adapter.cluster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.nio.charset.StandardCharsets; +import java.security.*; + +import java.math.BigInteger; +import java.util.Arrays; + +import score.Context; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import foundation.icon.icx.KeyWallet; + + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; + +import score.UserRevertedException; +import score.Address; +import score.ByteArrayObjectWriter; +import foundation.icon.xcall.CallService; +import foundation.icon.xcall.CallServiceScoreInterface; + + +import xcall.icon.test.MockContract; + +public class ClusterConnectionTest extends TestBase { + protected final ServiceManager sm = getServiceManager(); + + protected final Account owner = sm.createAccount(); + protected final Account user = sm.createAccount(); + protected final Account admin = sm.createAccount(); + protected final Account xcallMock = sm.createAccount(); + + protected final Account source_relayer = sm.createAccount(); + protected final Account destination_relayer = sm.createAccount(); + + protected Score xcall, connection; + protected CallService xcallSpy; + protected ClusterConnection connectionSpy; + + protected static String nidSource = "nid.source"; + protected static String nidTarget = "nid.target"; + + // static MockedStatic contextMock; + + protected MockContract callservice; + + @BeforeEach + public void setup() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + callservice = new MockContract<>(CallServiceScoreInterface.class, CallService.class, sm, owner); + + + connection = sm.deploy(owner, ClusterConnection.class, source_relayer.getAddress(), + callservice.getAddress()); + connectionSpy = (ClusterConnection) spy(connection.getInstance()); + connection.setInstance(connectionSpy); + } + + @Test + public void testSetAdmin() { + + connection.invoke(owner, "setAdmin", admin.getAddress()); + assertEquals(connection.call("admin"), admin.getAddress()); + } + + @Test + public void testSetAdmin_unauthorized() { + UserRevertedException e = assertThrows(UserRevertedException.class, + () -> connection.invoke(user, "setAdmin", admin.getAddress())); + assertEquals("Reverted(0): " + "Only admin can call this function", e.getMessage()); + } + + @Test + public void setFee() { + connection.invoke(source_relayer, "setFee", nidTarget, BigInteger.TEN, BigInteger.TEN); + assertEquals(connection.call("getFee", nidTarget, true), BigInteger.TEN.add(BigInteger.TEN)); + } + + @Test + public void sendMessage() { + connection.invoke(callservice.account, "sendMessage", nidTarget, "xcall", BigInteger.ONE, "test".getBytes()); + verify(connectionSpy).Message(nidTarget, BigInteger.ONE, "test".getBytes()); + } + + @Test + public void testSendMessage_unauthorized() { + UserRevertedException e = assertThrows(UserRevertedException.class, + () -> connection.invoke(user, "sendMessage", nidTarget, "xcall", BigInteger.ONE, "test".getBytes())); + assertEquals("Reverted(0): " + "Only xCall can send messages", e.getMessage()); + } + + @Test + public void testRevertMessage() { + + connection.invoke(source_relayer, "revertMessage", BigInteger.ONE); + } + + @Test + public void testRevertMessage_unauthorized(){ + UserRevertedException e = assertThrows(UserRevertedException.class, ()->connection.invoke(user, "revertMessage", BigInteger.ONE)); + assertEquals("Reverted(0): "+"Only relayer can call this function", e.getMessage()); + + } + + @Test + public void testSetFeesUnauthorized(){ + UserRevertedException e = assertThrows(UserRevertedException.class,() -> connection.invoke(user, "setFee", "0xevm", + BigInteger.TEN, BigInteger.TEN)); + assertEquals("Reverted(0): "+"Only relayer can call this function", e.getMessage()); + } + + @Test + public void testClaimFees(){ + setFee(); + connection.invoke(source_relayer, "claimFees"); + assertEquals(source_relayer.getBalance(), BigInteger.ZERO); + + UserRevertedException e = assertThrows(UserRevertedException.class,() -> connection.invoke(callservice.account, "sendMessage", nidTarget, + "xcall", BigInteger.ONE, "null".getBytes())); + assertEquals(e.getMessage(), "Reverted(0): Insufficient balance"); + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.getValue()).thenReturn(BigInteger.valueOf(20)); + connection.invoke(callservice.account, "sendMessage", nidTarget,"xcall", BigInteger.ONE, "null".getBytes()); + } + + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.getBalance(connection.getAddress())).thenReturn(BigInteger.valueOf(20)); + contextMock.when(() -> Context.transfer(source_relayer.getAddress(),BigInteger.valueOf(20))).then(invocationOnMock -> null); + connection.invoke(source_relayer, "claimFees"); + } + } + + @Test + public void testClaimFees_unauthorized(){ + setFee(); + UserRevertedException e = assertThrows(UserRevertedException.class,() -> connection.invoke(user, "claimFees")); + assertEquals(e.getMessage(), "Reverted(0): "+"Only relayer can call this function"); + } + + public MockedStatic.Verification value() { + return () -> Context.getValue(); + } + + @Test + public void testRecvMessageWithSignatures() throws Exception{ + byte[] data = "test".getBytes(); + byte[] messageHash = getMessageHash(nidSource, BigInteger.ONE, data); + byte[][] byteArray = new byte[1][]; + KeyWallet wallet = KeyWallet.create(); + byteArray[0] = wallet.sign(messageHash); + byte[][] validators = new byte[][] { + wallet.getPublicKey().toByteArray(), + }; + connection.invoke(owner, "updateValidators", validators, BigInteger.ONE); + connection.invoke(source_relayer, "recvMessageWithSignatures", nidSource, BigInteger.ONE, data, byteArray); + verify(callservice.mock).handleMessage(eq(nidSource), eq("test".getBytes())); + } + + + @Test + public void testRecvMessageWithMultiSignatures() throws Exception{ + byte[] data = "test".getBytes(); + byte[] messageHash = getMessageHash(nidSource, BigInteger.ONE, data); + byte[][] byteArray = new byte[2][]; + KeyWallet wallet = KeyWallet.create(); + KeyWallet wallet2 = KeyWallet.create(); + byteArray[0] = wallet.sign(messageHash); + byteArray[1] = wallet2.sign(messageHash); + byte[][] validators = new byte[][] { + wallet.getPublicKey().toByteArray(), + wallet2.getPublicKey().toByteArray(), + }; + connection.invoke(owner, "updateValidators", validators, BigInteger.TWO); + connection.invoke(source_relayer, "recvMessageWithSignatures", nidSource, BigInteger.ONE, data, byteArray); + verify(callservice.mock).handleMessage(eq(nidSource), eq("test".getBytes())); + } + + @Test + public void testRecvMessageWithSignaturesNotEnoughSignatures() throws Exception{ + byte[] data = "test".getBytes(); + byte[] messageHash = getMessageHash(nidSource, BigInteger.ONE, data); + KeyWallet wallet = KeyWallet.create(); + KeyWallet wallet2 = KeyWallet.create(); + byte[][] byteArray = new byte[1][]; + byteArray[0] = wallet.sign(messageHash); + byte[][] validators = new byte[][] { + wallet.getPublicKey().toByteArray(), + wallet2.getPublicKey().toByteArray(), + }; + connection.invoke(owner, "updateValidators", validators, BigInteger.TWO); + UserRevertedException e = assertThrows(UserRevertedException.class, + ()->connection.invoke(source_relayer, "recvMessageWithSignatures", nidSource, BigInteger.ONE, data, byteArray)); + assertEquals("Reverted(0): Not enough signatures", e.getMessage()); + verifyNoInteractions(callservice.mock); + } + + @Test + public void testRecvMessageWithSignaturesNotEnoughValidSignatures() throws Exception{ + byte[] data = "test".getBytes(); + byte[] messageHash = getMessageHash(nidSource, BigInteger.ONE, data); + KeyWallet wallet = KeyWallet.create(); + KeyWallet wallet2 = KeyWallet.create(); + byte[][] byteArray = new byte[2][]; + byteArray[0] = wallet.sign(messageHash); + byteArray[1] = wallet.sign(messageHash); + byte[][] validators = new byte[][] { + wallet.getPublicKey().toByteArray(), + wallet2.getPublicKey().toByteArray(), + }; + connection.invoke(owner, "updateValidators", validators, BigInteger.TWO); + UserRevertedException e = assertThrows(UserRevertedException.class, + ()->connection.invoke(source_relayer, "recvMessageWithSignatures", nidSource, BigInteger.ONE, data, byteArray)); + assertEquals("Reverted(0): Not enough valid signatures", e.getMessage()); + verifyNoInteractions(callservice.mock); + } + + + public static byte[] getMessageHash(String srcNetwork, BigInteger _connSn, byte[] msg) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(3); + writer.write(srcNetwork); + writer.write(_connSn); + writer.write(msg); + writer.end(); + return Context.hash("keccak-256", writer.toByteArray()); + } + @Test + public void testAddSigners() throws Exception{ + KeyWallet wallet = KeyWallet.create(); + KeyWallet wallet2 = KeyWallet.create(); + byte[][] validators = new byte[][] { + wallet.getPublicKey().toByteArray(), + wallet2.getPublicKey().toByteArray(), + }; + connection.invoke(owner, "updateValidators", validators, BigInteger.TWO); + String[] signers = connection.call(String[].class,"listValidators"); + assertEquals(signers.length, 2); + } + +} \ No newline at end of file diff --git a/contracts/javascore/keystore.json b/contracts/javascore/keystore.json new file mode 100644 index 00000000..4632fa28 --- /dev/null +++ b/contracts/javascore/keystore.json @@ -0,0 +1 @@ +{"address":"hx5f7afdb96154bbe9dbeb2dde3a58afcb1efbfaff","id":"7a8237b8-a61f-4501-9896-b96a39e9c72b","version":3,"coinType":"icx","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"c2e5a31a638992a36e3afe96d38ccfda"},"ciphertext":"68d61f1325e1198ca7fd0ee961997e31541ead36ff24b29fc04e6f39cb05eeac","kdf":"scrypt","kdfparams":{"dklen":32,"n":65536,"r":8,"p":1,"salt":"53663f4c4320aff5"},"mac":"388b1466f2a75736fee1f576e2307da2fcaa0cf3d00b968d3c45a98eb9c06f84"}} \ No newline at end of file diff --git a/contracts/javascore/settings.gradle b/contracts/javascore/settings.gradle index 6e6fdcdf..bfa80980 100644 --- a/contracts/javascore/settings.gradle +++ b/contracts/javascore/settings.gradle @@ -3,7 +3,9 @@ include( 'test-lib', 'xcall', 'xcall-lib', - 'centralized-connection' + 'centralized-connection', + 'cluster-connection' + 'aggregator' ) include(':dapp-simple') diff --git a/contracts/soroban/Cargo.lock b/contracts/soroban/Cargo.lock index e571a9ff..fbf3411e 100644 --- a/contracts/soroban/Cargo.lock +++ b/contracts/soroban/Cargo.lock @@ -130,6 +130,8 @@ name = "centralized-connection" version = "0.0.0" dependencies = [ "soroban-sdk", + "soroban-xcall-lib", + "xcall", ] [[package]] @@ -151,6 +153,14 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "cluster-connection" +version = "0.0.0" +dependencies = [ + "soroban-rlp", + "soroban-sdk", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -338,7 +348,6 @@ dependencies = [ "elliptic-curve", "rfc6979", "signature", - "spki", ] [[package]] @@ -353,15 +362,16 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", "rand_core", "serde", "sha2", + "subtle", "zeroize", ] @@ -383,7 +393,6 @@ dependencies = [ "ff", "generic-array", "group", - "pkcs8", "rand_core", "sec1", "subtle", @@ -597,9 +606,7 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "once_cell", "sha2", - "signature", ] [[package]] @@ -716,6 +723,18 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "paste" version = "1.0.14" @@ -760,6 +779,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -848,7 +876,6 @@ dependencies = [ "base16ct", "der", "generic-array", - "pkcs8", "subtle", "zeroize", ] @@ -959,9 +986,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "soroban-builtin-sdk-macros" -version = "20.3.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc32c6e817f3ca269764ec0d7d14da6210b74a5bf14d4e745aa3ee860558900" +checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" dependencies = [ "itertools", "proc-macro2", @@ -971,9 +998,9 @@ dependencies = [ [[package]] name = "soroban-env-common" -version = "20.3.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c14e18d879c520ff82612eaae0590acaf6a7f3b977407e1abb1c9e31f94c7814" +checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" dependencies = [ "arbitrary", "crate-git-revision", @@ -985,13 +1012,14 @@ dependencies = [ "soroban-wasmi", "static_assertions", "stellar-xdr", + "wasmparser", ] [[package]] name = "soroban-env-guest" -version = "20.3.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5122ca2abd5ebcc1e876a96b9b44f87ce0a0e06df8f7c09772ddb58b159b7454" +checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" dependencies = [ "soroban-env-common", "static_assertions", @@ -999,13 +1027,16 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "20.3.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "114a0fa0d0cc39d0be16b1ee35b6e5f4ee0592ddcf459bde69391c02b03cf520" +checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" dependencies = [ "backtrace", "curve25519-dalek", + "ecdsa", "ed25519-dalek", + "elliptic-curve", + "generic-array", "getrandom", "hex-literal", "hmac", @@ -1013,8 +1044,10 @@ dependencies = [ "num-derive", "num-integer", "num-traits", + "p256", "rand", "rand_chacha", + "sec1", "sha2", "sha3", "soroban-builtin-sdk-macros", @@ -1022,13 +1055,14 @@ dependencies = [ "soroban-wasmi", "static_assertions", "stellar-strkey", + "wasmparser", ] [[package]] name = "soroban-env-macros" -version = "20.3.0" +version = "21.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13e3f8c86f812e0669e78fcb3eae40c385c6a9dd1a4886a1de733230b4fcf27" +checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" dependencies = [ "itertools", "proc-macro2", @@ -1041,9 +1075,9 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" -version = "20.5.0" +version = "21.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a54708f44890e0546180db6b4f530e2a88d83b05a9b38a131caa21d005e25a" +checksum = "956476365ff3f9bf429ff23fa11ac75798347a2bfc3c9e5e12638dbe3a6b17a8" dependencies = [ "serde", "serde_json", @@ -1062,9 +1096,9 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "20.5.0" +version = "21.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fc8be9068dd4e0212d8b13ad61089ea87e69ac212c262914503a961c8dc3a3" +checksum = "c7767472f00a4053e86d5c37b3c814a6bc01c9230004713328d73d2a3444e72e" dependencies = [ "arbitrary", "bytes-lit", @@ -1082,9 +1116,9 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "20.5.0" +version = "21.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db20def4ead836663633f58d817d0ed8e1af052c9650a04adf730525af85b964" +checksum = "be8cf8fa10f3ad62509ff7b25cd696fb837da692c40264d1abb393e117aad75c" dependencies = [ "crate-git-revision", "darling", @@ -1102,9 +1136,9 @@ dependencies = [ [[package]] name = "soroban-spec" -version = "20.5.0" +version = "21.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eefeb5d373b43f6828145d00f0c5cc35e96db56a6671ae9614f84beb2711cab" +checksum = "12d306f61ef5c1247dca1562e04cc74b6e3adf107631c168b2ce0d5f1cf1fa13" dependencies = [ "base64 0.13.1", "stellar-xdr", @@ -1114,9 +1148,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "20.5.0" +version = "21.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3152bca4737ef734ac37fe47b225ee58765c9095970c481a18516a2b287c7a33" +checksum = "bed06e0f622fb878fc439643f2fd86163223ac33a468beeea96e5d33f79b08b3" dependencies = [ "prettyplease", "proc-macro2", @@ -1183,9 +1217,9 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "20.1.0" +version = "21.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e59cdf3eb4467fb5a4b00b52e7de6dca72f67fac6f9b700f55c95a5d86f09c9d" +checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" dependencies = [ "arbitrary", "base64 0.13.1", @@ -1369,11 +1403,12 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.88.0" +version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8cf7dd82407fe68161bedcd57fde15596f32ebf6e9b3bdbf3ae1da20e38e5e" +checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.6", + "semver", ] [[package]] diff --git a/contracts/soroban/Cargo.toml b/contracts/soroban/Cargo.toml index cddad916..7cb79f87 100644 --- a/contracts/soroban/Cargo.toml +++ b/contracts/soroban/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.dependencies] -soroban-sdk = "20.5.0" +soroban-sdk = "21.7.4" [profile.release] opt-level = "z" diff --git a/contracts/soroban/contracts/centralized-connection/Cargo.toml b/contracts/soroban/contracts/centralized-connection/Cargo.toml index c4f294fa..e34ece52 100644 --- a/contracts/soroban/contracts/centralized-connection/Cargo.toml +++ b/contracts/soroban/contracts/centralized-connection/Cargo.toml @@ -10,6 +10,8 @@ doctest = false [dependencies] soroban-sdk = { workspace = true, features = ["alloc"] } +xcall = { path = "../xcall" } +soroban-xcall-lib = { path = "../../libs/soroban-xcall-lib" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/soroban/contracts/centralized-connection/src/contract.rs b/contracts/soroban/contracts/centralized-connection/src/contract.rs index 507f5451..03444560 100644 --- a/contracts/soroban/contracts/centralized-connection/src/contract.rs +++ b/contracts/soroban/contracts/centralized-connection/src/contract.rs @@ -125,11 +125,13 @@ impl CentralizedConnection { helpers::ensure_upgrade_authority(&env)?; env.deployer().update_current_contract_wasm(new_wasm_hash); + let current_version = storage::get_contract_version(&env); + storage::set_contract_version(&env, current_version + 1); + Ok(()) } - pub fn extend_instance_storage(env: Env) -> Result<(), ContractError> { - storage::extend_instance(&env); - Ok(()) + pub fn version(env: Env) -> u32 { + storage::get_contract_version(&env) } } diff --git a/contracts/soroban/contracts/centralized-connection/src/storage.rs b/contracts/soroban/contracts/centralized-connection/src/storage.rs index 4a410aa0..a4a2fa24 100644 --- a/contracts/soroban/contracts/centralized-connection/src/storage.rs +++ b/contracts/soroban/contracts/centralized-connection/src/storage.rs @@ -50,13 +50,6 @@ pub fn native_token(e: &Env) -> Result { .ok_or(ContractError::Uninitialized) } -pub fn get_conn_sn(e: &Env) -> Result { - e.storage() - .instance() - .get(&StorageKey::ConnSn) - .ok_or(ContractError::Uninitialized) -} - pub fn get_next_conn_sn(e: &Env) -> u128 { let mut sn = e.storage().instance().get(&StorageKey::ConnSn).unwrap_or(0); sn += 1; @@ -103,6 +96,19 @@ pub fn get_sn_receipt(e: &Env, network_id: String, sn: u128) -> bool { is_received } +pub fn get_contract_version(e: &Env) -> u32 { + e.storage() + .instance() + .get(&StorageKey::Version) + .unwrap_or(1) +} + +pub fn set_contract_version(e: &Env, new_version: u32) { + e.storage() + .instance() + .set(&StorageKey::Version, &new_version); +} + pub fn store_receipt(e: &Env, network_id: String, sn: u128) { let key = StorageKey::Receipts(network_id, sn); e.storage().persistent().set(&key, &true); diff --git a/contracts/soroban/contracts/centralized-connection/src/test.rs b/contracts/soroban/contracts/centralized-connection/src/test.rs index a44bd217..7556c384 100644 --- a/contracts/soroban/contracts/centralized-connection/src/test.rs +++ b/contracts/soroban/contracts/centralized-connection/src/test.rs @@ -2,9 +2,14 @@ extern crate std; -mod xcall { +mod xcall_module { soroban_sdk::contractimport!(file = "../../target/wasm32-unknown-unknown/release/xcall.wasm"); } +mod connection { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/centralized_connection.wasm" + ); +} use crate::{ contract::{CentralizedConnection, CentralizedConnectionClient}, @@ -13,9 +18,14 @@ use crate::{ types::InitializeMsg, }; use soroban_sdk::{ - symbol_short, + bytes, symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, Events}, - token, vec, Address, Bytes, Env, IntoVal, String, Symbol, + token, vec, Address, Bytes, Env, IntoVal, String, Symbol, Vec, +}; +use soroban_xcall_lib::{messages::msg_type::MessageType, network_address::NetworkAddress}; +use xcall::{ + storage as xcall_storage, + types::{message::CSMessage, request::CSMessageRequest, rollback::Rollback}, }; pub struct TestContext { @@ -33,11 +43,12 @@ impl TestContext { pub fn default() -> Self { let env = Env::default(); let token_admin = Address::generate(&env); + let native_token_contract = env.register_stellar_asset_contract_v2(token_admin.clone()); Self { - xcall: env.register_contract_wasm(None, xcall::WASM), + xcall: env.register_contract_wasm(None, xcall_module::WASM), contract: env.register_contract(None, CentralizedConnection), relayer: Address::generate(&env), - native_token: env.register_stellar_asset_contract(token_admin.clone()), + native_token: native_token_contract.address(), nid: String::from_str(&env, "icon"), upgrade_authority: Address::generate(&env), env, @@ -54,6 +65,23 @@ impl TestContext { xcall_address: self.xcall.clone(), upgrade_authority: self.upgrade_authority.clone(), }); + + self.init_xcall_state(); + } + + pub fn init_xcall_state(&self) { + let xcall_client = xcall_module::Client::new(&self.env, &self.xcall); + + let initialize_msg = xcall_module::InitializeMsg { + native_token: self.native_token.clone(), + network_id: self.nid.clone(), + sender: Address::generate(&self.env), + upgrade_authority: self.upgrade_authority.clone(), + }; + xcall_client.initialize(&initialize_msg); + + xcall_client.set_protocol_fee(&100_u128); + xcall_client.set_default_connection(&self.nid, &self.contract) } pub fn init_send_message(&self, client: &CentralizedConnectionClient<'static>) { @@ -65,9 +93,11 @@ impl TestContext { } fn get_dummy_initialize_msg(env: &Env) -> InitializeMsg { + let native_token_contract = env.register_stellar_asset_contract_v2(Address::generate(&env)); + InitializeMsg { relayer: Address::generate(&env), - native_token: env.register_stellar_asset_contract(Address::generate(&env)), + native_token: native_token_contract.address(), xcall_address: Address::generate(&env), upgrade_authority: Address::generate(&env), } @@ -343,3 +373,108 @@ fn test_get_receipt_returns_false() { let receipt = client.get_receipt(&ctx.nid, &sequence_no); assert_eq!(receipt, true) } + +#[test] +fn test_recv_message() { + let ctx = TestContext::default(); + let client = CentralizedConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let protocols: Vec = vec![&ctx.env, ctx.contract.to_string()]; + let from = NetworkAddress::new( + &ctx.env, + String::from_str(&ctx.env, "0x2.icon"), + ctx.xcall.to_string(), + ); + let request = CSMessageRequest::new( + from, + Address::generate(&ctx.env).to_string(), + 1, + protocols, + MessageType::CallMessagePersisted, + bytes!(&ctx.env, 0xabc), + ); + let cs_message = CSMessage::from_request(&ctx.env, &request); + let encoded = cs_message.encode(&ctx.env); + + let conn_sn = 1; + let from_nid = String::from_str(&ctx.env, "0x2.icon"); + client.recv_message(&from_nid, &conn_sn, &encoded); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn test_recv_message_duplicate_connection_sequence() { + let ctx = TestContext::default(); + let client = CentralizedConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let protocols: Vec = vec![&ctx.env, ctx.contract.to_string()]; + let from = NetworkAddress::new( + &ctx.env, + String::from_str(&ctx.env, "0x2.icon"), + ctx.xcall.to_string(), + ); + let request = CSMessageRequest::new( + from, + Address::generate(&ctx.env).to_string(), + 1, + protocols, + MessageType::CallMessagePersisted, + bytes!(&ctx.env, 0xabc), + ); + let cs_message = CSMessage::from_request(&ctx.env, &request); + let encoded = cs_message.encode(&ctx.env); + + let conn_sn = 1; + let from_nid = String::from_str(&ctx.env, "0x2.icon"); + client.recv_message(&from_nid, &conn_sn, &encoded); + + client.recv_message(&from_nid, &conn_sn, &encoded); +} + +#[test] +pub fn test_revert_message() { + let ctx = TestContext::default(); + let client = CentralizedConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let sequence_no = 1; + let protocols: Vec = vec![&ctx.env, ctx.contract.to_string()]; + let to = NetworkAddress::new( + &ctx.env, + String::from_str(&ctx.env, "0x2.icon"), + ctx.xcall.to_string(), + ); + let rollback = Rollback::new( + Address::generate(&ctx.env), + to, + protocols.clone(), + bytes!(&ctx.env, 0xabc), + false, + ); + ctx.env.as_contract(&ctx.xcall, || { + xcall_storage::store_rollback(&ctx.env, sequence_no, &rollback); + }); + + client.revert_message(&sequence_no); + + ctx.env.as_contract(&ctx.xcall, || { + // rollback should be enabled + let rollback = xcall_storage::get_rollback(&ctx.env, sequence_no).unwrap(); + assert_eq!(rollback.enabled, true); + }); +} + +#[test] +fn test_upgrade() { + let ctx = TestContext::default(); + let client = CentralizedConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let wasm_hash = ctx.env.deployer().upload_contract_wasm(connection::WASM); + assert_eq!(client.version(), 1); + + client.upgrade(&wasm_hash); + assert_eq!(client.version(), 2); +} diff --git a/contracts/soroban/contracts/centralized-connection/src/types.rs b/contracts/soroban/contracts/centralized-connection/src/types.rs index e3f08bdb..6b8c2fab 100644 --- a/contracts/soroban/contracts/centralized-connection/src/types.rs +++ b/contracts/soroban/contracts/centralized-connection/src/types.rs @@ -8,6 +8,7 @@ pub enum StorageKey { UpgradeAuthority, Xlm, ConnSn, + Version, NetworkFee(String), Receipts(String, u128), } diff --git a/contracts/soroban/contracts/cluster-connection/Cargo.toml b/contracts/soroban/contracts/cluster-connection/Cargo.toml new file mode 100644 index 00000000..e6f6b482 --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cluster-connection" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true, features = ["alloc"] } +soroban-rlp = { path = "../../libs/soroban-rlp" } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/soroban/contracts/cluster-connection/src/contract.rs b/contracts/soroban/contracts/cluster-connection/src/contract.rs new file mode 100644 index 00000000..b02d54ee --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/contract.rs @@ -0,0 +1,185 @@ +use soroban_sdk::{contract, contractimpl, token, Address, Bytes, BytesN, Env, String, Vec}; + +use crate::{errors::ContractError, event, helpers, storage, types::InitializeMsg}; + +#[contract] +pub struct ClusterConnection; + +#[contractimpl] +impl ClusterConnection { + pub fn initialize(env: Env, msg: InitializeMsg) -> Result<(), ContractError> { + storage::is_initialized(&env)?; + + storage::store_native_token(&env, msg.native_token); + storage::store_conn_sn(&env, 0); + storage::store_relayer(&env, msg.relayer); + storage::store_admin(&env, msg.admin); + storage::store_xcall(&env, msg.xcall_address); + storage::store_upgrade_authority(&env, msg.upgrade_authority); + storage::store_validator_threshold(&env, 0); + storage::store_validators(&env, Vec::new(&env)); + + Ok(()) + } + + pub fn get_admin(env: Env) -> Result { + let address = storage::admin(&env)?; + Ok(address) + } + + pub fn set_admin(env: Env, address: Address) -> Result<(), ContractError> { + helpers::ensure_admin(&env)?; + storage::store_admin(&env, address); + Ok(()) + } + + pub fn get_upgrade_authority(env: Env) -> Result { + let address = storage::get_upgrade_authority(&env)?; + Ok(address) + } + + pub fn set_upgrade_authority(env: &Env, address: Address) -> Result<(), ContractError> { + helpers::ensure_upgrade_authority(&env)?; + storage::store_upgrade_authority(&env, address); + + Ok(()) + } + + pub fn set_relayer(env: Env, address: Address) -> Result<(), ContractError> { + helpers::ensure_admin(&env)?; + storage::store_relayer(&env, address); + Ok(()) + } + + pub fn send_message( + env: Env, + tx_origin: Address, + to: String, + sn: i64, + msg: Bytes, + ) -> Result<(), ContractError> { + helpers::ensure_xcall(&env)?; + + let next_conn_sn = storage::get_next_conn_sn(&env); + storage::store_conn_sn(&env, next_conn_sn); + + let mut fee: u128 = 0; + if sn >= 0 { + fee = helpers::get_network_fee(&env, to.clone(), sn > 0)?; + } + if fee > 0 { + helpers::transfer_token(&env, &tx_origin, &env.current_contract_address(), &fee)?; + } + event::send_message(&env, to, next_conn_sn, msg); + + Ok(()) + } + + pub fn recv_message_with_signatures( + env: Env, + src_network: String, + conn_sn: u128, + msg: Bytes, + signatures: Vec>, + ) -> Result<(), ContractError> { + helpers::ensure_relayer(&env)?; + + if !helpers::verify_signatures(&env, signatures, &src_network, &conn_sn, &msg){ + return Err(ContractError::SignatureVerificationFailed); + }; + + if storage::get_sn_receipt(&env, src_network.clone(), conn_sn) { + return Err(ContractError::DuplicateMessage); + } + storage::store_receipt(&env, src_network.clone(), conn_sn); + + helpers::call_xcall_handle_message(&env, &src_network, msg)?; + Ok(()) + } + + pub fn set_fee( + env: Env, + network_id: String, + message_fee: u128, + response_fee: u128, + ) -> Result<(), ContractError> { + helpers::ensure_relayer(&env)?; + + storage::store_network_fee(&env, network_id, message_fee, response_fee); + Ok(()) + } + + pub fn claim_fees(env: Env) -> Result<(), ContractError> { + let admin = helpers::ensure_relayer(&env)?; + + let token_addr = storage::native_token(&env)?; + let client = token::Client::new(&env, &token_addr); + let balance = client.balance(&env.current_contract_address()); + + client.transfer(&env.current_contract_address(), &admin, &balance); + Ok(()) + } + + pub fn update_validators(env: Env, pub_keys: Vec>, threshold: u32) -> Result<(), ContractError> { + helpers::ensure_admin(&env)?; + let mut validators = Vec::new(&env); + + for address in pub_keys.clone() { + if !validators.contains(&address) { + validators.push_back(address); + } + } + if (validators.len() as u32) < threshold { + return Err(ContractError::ThresholdExceeded); + + } + storage::store_validators(&env, pub_keys); + storage::store_validator_threshold(&env, threshold); + Ok(()) + } + + pub fn get_validators_threshold(env: Env) -> Result { + let threshold = storage::get_validators_threshold(&env).unwrap(); + Ok(threshold) + } + + pub fn set_validators_threshold(env: Env, threshold: u32) -> Result<(), ContractError> { + helpers::ensure_admin(&env)?; + let validators = storage::get_validators(&env).unwrap(); + if (validators.len() as u32) < threshold { + return Err(ContractError::ThresholdExceeded); + } + storage::store_validator_threshold(&env, threshold); + Ok(()) + } + + pub fn get_validators(env: Env) -> Result>, ContractError> { + let validators = storage::get_validators(&env).unwrap(); + Ok(validators) + } + + pub fn get_relayer(env: Env) -> Result { + let address = storage::relayer(&env)?; + Ok(address) + } + + pub fn get_fee(env: Env, network_id: String, response: bool) -> Result { + helpers::get_network_fee(&env, network_id, response) + } + + pub fn get_receipt(env: Env, network_id: String, sn: u128) -> bool { + storage::get_sn_receipt(&env, network_id, sn) + } + + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), ContractError> { + helpers::ensure_upgrade_authority(&env)?; + env.deployer().update_current_contract_wasm(new_wasm_hash); + + Ok(()) + } + + pub fn extend_instance_storage(env: Env) -> Result<(), ContractError> { + storage::extend_instance(&env); + Ok(()) + } +} diff --git a/contracts/soroban/contracts/cluster-connection/src/errors.rs b/contracts/soroban/contracts/cluster-connection/src/errors.rs new file mode 100644 index 00000000..b4d50517 --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/errors.rs @@ -0,0 +1,18 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +#[repr(u32)] +pub enum ContractError { + OnlyAdmin = 1, + Uninitialized = 2, + AlreadyInitialized = 3, + InsufficientFund = 4, + DuplicateMessage = 5, + NetworkNotSupported = 6, + CannotRemoveAdmin = 7, + ThresholdExceeded = 8, + ValidatorNotFound = 9, + ValidatorAlreadyAdded = 10, + SignatureVerificationFailed = 11, +} diff --git a/contracts/soroban/contracts/cluster-connection/src/event.rs b/contracts/soroban/contracts/cluster-connection/src/event.rs new file mode 100644 index 00000000..6db6d215 --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/event.rs @@ -0,0 +1,19 @@ +#![allow(non_snake_case)] + +use soroban_sdk::{contracttype, Bytes, Env, String}; + +#[contracttype] +pub struct SendMsgEvent { + pub targetNetwork: String, + pub connSn: u128, + pub msg: Bytes, +} + +pub(crate) fn send_message(e: &Env, targetNetwork: String, connSn: u128, msg: Bytes) { + let emit_message = SendMsgEvent { + targetNetwork, + connSn, + msg, + }; + e.events().publish(("Message",), emit_message); +} diff --git a/contracts/soroban/contracts/cluster-connection/src/helpers.rs b/contracts/soroban/contracts/cluster-connection/src/helpers.rs new file mode 100644 index 00000000..f37ed14f --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/helpers.rs @@ -0,0 +1,132 @@ +use soroban_sdk::{token, vec, Address, Bytes, BytesN, Env, Map, String, Vec}; +use crate::{errors::ContractError, interfaces::interface_xcall::XcallClient, storage}; +use soroban_rlp::encoder; + +pub fn ensure_relayer(e: &Env) -> Result { + let relayer = storage::relayer(&e)?; + relayer.require_auth(); + + Ok(relayer) +} + +pub fn ensure_admin(e: &Env) -> Result { + let admin = storage::admin(&e)?; + admin.require_auth(); + + Ok(admin) +} + +pub fn ensure_upgrade_authority(e: &Env) -> Result { + let authority = storage::get_upgrade_authority(&e)?; + authority.require_auth(); + + Ok(authority) +} + +pub fn ensure_xcall(e: &Env) -> Result { + let xcall = storage::get_xcall(&e)?; + xcall.require_auth(); + + Ok(xcall) +} + +pub fn get_network_fee( + env: &Env, + network_id: String, + response: bool, +) -> Result { + let mut fee = storage::get_msg_fee(&env, network_id.clone())?; + if response { + fee += storage::get_res_fee(&env, network_id)?; + } + + Ok(fee) +} + +pub fn transfer_token( + e: &Env, + from: &Address, + to: &Address, + amount: &u128, +) -> Result<(), ContractError> { + let native_token = storage::native_token(&e)?; + let client = token::Client::new(&e, &native_token); + + client.transfer(&from, &to, &(*amount as i128)); + Ok(()) +} + +pub fn verify_signatures( + e: &Env, + signatures: Vec>, + src_network: &String, + conn_sn: &u128, + message: &Bytes, +) -> bool { + let validators = storage::get_validators(e).unwrap(); + let threshold = storage::get_validators_threshold(e).unwrap(); + + if signatures.len() < threshold { + return false + } + let message_hash = e.crypto().keccak256(&get_encoded_message(e, src_network, conn_sn, message)); + let mut unique_validators = Map::new(e); + let mut count = 0; + + + for sig in signatures.iter() { + let r_s_v = sig.to_array(); + // Separate signature (r + s) and recovery ID + let signature_array: [u8; 64] = r_s_v[..64].try_into().unwrap(); // r + s part + let recovery_code = match r_s_v[64] { + rc if rc >= 27 => rc - 27, + rc => rc, + }; + let signature = BytesN::<64>::from_array(e, &signature_array); + + let public_key = e.crypto().secp256k1_recover(&message_hash, &signature, recovery_code as u32); + + if validators.contains(&public_key) { + if !unique_validators.contains_key(public_key.clone()) { + unique_validators.set(public_key, count); + count += 1; + } + } + } + (unique_validators.len() as u32) >= threshold + +} + + +pub fn get_encoded_message(e: &Env, src_network: &String, conn_sn: &u128, message: &Bytes) -> Bytes { + let mut list = vec![&e]; + list.push_back(encoder::encode_string(&e, src_network.clone())); + list.push_back(encoder::encode_u128(&e, conn_sn.clone())); + list.push_back(encoder::encode(&e, message.clone())); + + encoder::encode_list(&e, list, false) +} + +#[cfg(not(test))] +pub fn call_xcall_handle_message(e: &Env, nid: &String, msg: Bytes) -> Result<(), ContractError> { + let xcall_addr = storage::get_xcall(&e)?; + let client = XcallClient::new(&e, &xcall_addr); + client.handle_message(&e.current_contract_address(), nid, &msg); + + Ok(()) +} + +#[cfg(test)] +pub fn call_xcall_handle_message(_e: &Env, _nid: &String, _msg: Bytes) -> Result<(), ContractError> { + Ok(()) +} + + + +pub fn call_xcall_handle_error(e: &Env, sn: u128) -> Result<(), ContractError> { + let xcall_addr = storage::get_xcall(&e)?; + let client = XcallClient::new(&e, &xcall_addr); + client.handle_error(&e.current_contract_address(), &sn); + + Ok(()) +} diff --git a/contracts/soroban/contracts/cluster-connection/src/interfaces/interface_xcall.rs b/contracts/soroban/contracts/cluster-connection/src/interfaces/interface_xcall.rs new file mode 100644 index 00000000..c49d6c9c --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/interfaces/interface_xcall.rs @@ -0,0 +1,15 @@ +use soroban_sdk::{contractclient, Address, Bytes, Env, String}; + +use crate::errors::ContractError; + +#[contractclient(name = "XcallClient")] +pub trait IXcall { + fn handle_message( + env: Env, + sender: Address, + from_nid: String, + msg: Bytes, + ) -> Result<(), ContractError>; + + fn handle_error(env: Env, sender: Address, sequence_no: u128) -> Result<(), ContractError>; +} diff --git a/contracts/soroban/contracts/cluster-connection/src/interfaces/mod.rs b/contracts/soroban/contracts/cluster-connection/src/interfaces/mod.rs new file mode 100644 index 00000000..61f8fdfe --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/interfaces/mod.rs @@ -0,0 +1 @@ +pub mod interface_xcall; diff --git a/contracts/soroban/contracts/cluster-connection/src/lib.rs b/contracts/soroban/contracts/cluster-connection/src/lib.rs new file mode 100644 index 00000000..1fd06073 --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/lib.rs @@ -0,0 +1,10 @@ +#![no_std] + +pub mod contract; +pub mod errors; +pub mod event; +pub mod helpers; +pub mod interfaces; +pub mod storage; +pub mod test; +pub mod types; diff --git a/contracts/soroban/contracts/cluster-connection/src/storage.rs b/contracts/soroban/contracts/cluster-connection/src/storage.rs new file mode 100644 index 00000000..bdc017a1 --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/storage.rs @@ -0,0 +1,187 @@ +use soroban_sdk::{Address, BytesN, Env, String, Vec}; + +use crate::{ + errors::ContractError, + types::{NetworkFee, StorageKey}, +}; + +const DAY_IN_LEDGERS: u32 = 17280; // assumes 5s a ledger + +const LEDGER_THRESHOLD_INSTANCE: u32 = DAY_IN_LEDGERS * 30; // ~ 30 days +const LEDGER_BUMP_INSTANCE: u32 = LEDGER_THRESHOLD_INSTANCE + DAY_IN_LEDGERS; // ~ 31 days + +const LEDGER_THRESHOLD_PERSISTENT: u32 = DAY_IN_LEDGERS * 30; // ~ 30 days +const LEDGER_BUMP_PERSISTENT: u32 = LEDGER_THRESHOLD_PERSISTENT + DAY_IN_LEDGERS; // ~ 31 days + +pub fn is_initialized(e: &Env) -> Result<(), ContractError> { + let initialized = e.storage().instance().has(&StorageKey::Admin); + if initialized { + Err(ContractError::AlreadyInitialized) + } else { + Ok(()) + } +} + +pub fn admin(e: &Env) -> Result { + e.storage() + .instance() + .get(&StorageKey::Admin) + .ok_or(ContractError::Uninitialized) +} + +pub fn relayer(e: &Env) -> Result { + e.storage() + .instance() + .get(&StorageKey::Relayer) + .ok_or(ContractError::Uninitialized) +} + +pub fn get_upgrade_authority(e: &Env) -> Result { + e.storage() + .instance() + .get(&StorageKey::UpgradeAuthority) + .ok_or(ContractError::Uninitialized) +} + +pub fn get_xcall(e: &Env) -> Result { + e.storage() + .instance() + .get(&StorageKey::Xcall) + .ok_or(ContractError::Uninitialized) +} + +pub fn native_token(e: &Env) -> Result { + e.storage() + .instance() + .get(&StorageKey::Xlm) + .ok_or(ContractError::Uninitialized) +} + +pub fn get_conn_sn(e: &Env) -> Result { + e.storage() + .instance() + .get(&StorageKey::ConnSn) + .ok_or(ContractError::Uninitialized) +} + +pub fn get_next_conn_sn(e: &Env) -> u128 { + let mut sn = e.storage().instance().get(&StorageKey::ConnSn).unwrap_or(0); + sn += 1; + sn +} + +pub fn get_msg_fee(e: &Env, network_id: String) -> Result { + let key = StorageKey::NetworkFee(network_id); + let network_fee: NetworkFee = e + .storage() + .persistent() + .get(&key) + .unwrap_or(NetworkFee::default()); + + if network_fee.message_fee > 0 { + extend_persistent(e, &key); + } + + Ok(network_fee.message_fee) +} + +pub fn get_res_fee(e: &Env, network_id: String) -> Result { + let key = StorageKey::NetworkFee(network_id); + let network_fee: NetworkFee = e + .storage() + .persistent() + .get(&key) + .unwrap_or(NetworkFee::default()); + + if network_fee.response_fee > 0 { + extend_persistent(e, &key); + } + + Ok(network_fee.response_fee) +} + +pub fn get_sn_receipt(e: &Env, network_id: String, sn: u128) -> bool { + let key = StorageKey::Receipts(network_id, sn); + let is_received = e.storage().persistent().get(&key).unwrap_or(false); + if is_received { + extend_persistent(e, &key); + } + + is_received +} + +pub fn get_validators_threshold(e: &Env) -> Result { + e.storage() + .instance() + .get(&StorageKey::ValidatorThreshold) + .ok_or(ContractError::Uninitialized) +} + +pub fn get_validators(e: &Env) -> Result>, ContractError> { + e.storage() + .instance() + .get(&StorageKey::Validators) + .ok_or(ContractError::Uninitialized) +} + +pub fn store_receipt(e: &Env, network_id: String, sn: u128) { + let key = StorageKey::Receipts(network_id, sn); + e.storage().persistent().set(&key, &true); + extend_persistent(e, &key); +} + +pub fn store_relayer(e: &Env, relayer: Address) { + e.storage().instance().set(&StorageKey::Relayer, &relayer); +} + +pub fn store_admin(e: &Env, admin: Address) { + e.storage().instance().set(&StorageKey::Admin, &admin); +} + +pub fn store_upgrade_authority(e: &Env, address: Address) { + e.storage() + .instance() + .set(&StorageKey::UpgradeAuthority, &address); +} + +pub fn store_xcall(e: &Env, xcall: Address) { + e.storage().instance().set(&StorageKey::Xcall, &xcall); +} + +pub fn store_native_token(e: &Env, address: Address) { + e.storage().instance().set(&StorageKey::Xlm, &address); +} + +pub fn store_conn_sn(e: &Env, sn: u128) { + e.storage().instance().set(&StorageKey::ConnSn, &sn); +} + +pub fn store_validator_threshold(e: &Env, threshold: u32) { + e.storage().instance().set(&StorageKey::ValidatorThreshold, &threshold); +} + +pub fn store_validators(e: &Env, validators: Vec>) { + e.storage().instance().set(&StorageKey::Validators, &validators); +} + +pub fn store_network_fee(e: &Env, network_id: String, message_fee: u128, response_fee: u128) { + let key = StorageKey::NetworkFee(network_id); + let network_fee = NetworkFee { + message_fee, + response_fee, + }; + e.storage().persistent().set(&key, &network_fee); + extend_persistent(e, &key); +} + +pub fn extend_instance(e: &Env) { + e.storage() + .instance() + .extend_ttl(LEDGER_THRESHOLD_INSTANCE, LEDGER_BUMP_INSTANCE); +} + +pub fn extend_persistent(e: &Env, key: &StorageKey) { + e.storage() + .persistent() + .extend_ttl(key, LEDGER_THRESHOLD_PERSISTENT, LEDGER_BUMP_PERSISTENT); +} diff --git a/contracts/soroban/contracts/cluster-connection/src/test.rs b/contracts/soroban/contracts/cluster-connection/src/test.rs new file mode 100644 index 00000000..fe375ba5 --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/test.rs @@ -0,0 +1,515 @@ +#![cfg(test)] + +extern crate std; + +mod xcall { + soroban_sdk::contractimport!(file = "../../target/wasm32-unknown-unknown/release/xcall.wasm"); +} + +use crate::{ + contract::{ClusterConnection, ClusterConnectionClient}, + event::SendMsgEvent, + storage, + types::InitializeMsg, +}; +use soroban_sdk::{ + symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, Events}, token, vec, Address, Bytes, BytesN, Env, IntoVal, String, Symbol, Vec +}; + +pub struct TestContext { + env: Env, + xcall: Address, + contract: Address, + admin:Address, + relayer: Address, + native_token: Address, + token_admin: Address, + nid: String, + upgrade_authority: Address, +} + +impl TestContext { + pub fn default() -> Self { + let env = Env::default(); + let token_admin = Address::generate(&env); + let xcall = env.register_contract_wasm(None, xcall::WASM); + Self { + xcall: xcall.clone(), + contract: env.register_contract(None, ClusterConnection), + relayer: Address::generate(&env), + admin: Address::generate(&env), + native_token: env.register_stellar_asset_contract(token_admin.clone()), + nid: String::from_str(&env, "0x2.icon"), + upgrade_authority: Address::generate(&env), + env, + token_admin, + } + } + + pub fn init_context(&self, client: &ClusterConnectionClient<'static>) { + self.env.mock_all_auths(); + + client.initialize(&InitializeMsg { + admin: self.admin.clone(), + relayer: self.relayer.clone(), + native_token: self.native_token.clone(), + xcall_address: self.xcall.clone(), + upgrade_authority: self.upgrade_authority.clone(), + }); + + } + + pub fn init_send_message(&self, client: &ClusterConnectionClient<'static>) { + self.init_context(&client); + self.env.mock_all_auths_allowing_non_root_auth(); + + client.set_fee(&self.nid, &100, &100); + } +} + +fn get_dummy_initialize_msg(env: &Env) -> InitializeMsg { + InitializeMsg { + admin: Address::generate(&env), + relayer: Address::generate(&env), + native_token: env.register_stellar_asset_contract(Address::generate(&env)), + xcall_address: Address::generate(&env), + upgrade_authority: Address::generate(&env), + } +} + + +#[test] +fn test_initialize() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let admin = client.get_admin(); + assert_eq!(admin, ctx.admin) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn test_initialize_fail_on_double_initialize() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + client.initialize(&get_dummy_initialize_msg(&ctx.env)); +} + +#[test] +fn test_set_admin() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let new_admin = Address::generate(&ctx.env); + client.set_admin(&new_admin); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + client.address.clone(), + symbol_short!("set_admin"), + (new_admin.clone(),).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ) +} + +#[test] +#[should_panic] +fn test_set_admin_fail() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let new_admin = Address::generate(&ctx.env); + client.set_admin(&new_admin); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.xcall, + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + client.address.clone(), + symbol_short!("set_admin"), + (new_admin.clone(),).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ) +} + +#[test] +fn test_set_upgrade_authority() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let new_upgrade_authority = Address::generate(&ctx.env); + client.set_upgrade_authority(&new_upgrade_authority); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.upgrade_authority.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.contract.clone(), + Symbol::new(&ctx.env, "set_upgrade_authority"), + (&new_upgrade_authority,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + + let autorhity = client.get_upgrade_authority(); + assert_eq!(autorhity, new_upgrade_authority); +} + +#[test] +fn test_set_fee() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let nid = String::from_str(&ctx.env, "icon"); + client.set_fee(&nid, &10, &10); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.relayer, + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + client.address.clone(), + symbol_short!("set_fee"), + (nid.clone(), 10_u128, 10_u128).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(client.get_fee(&nid, &true), 20); + assert_eq!(client.get_fee(&nid, &false), 10); +} + +#[test] +fn test_claim_fees() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let token_client = token::Client::new(&ctx.env, &ctx.native_token); + let asset_client = token::StellarAssetClient::new(&ctx.env, &ctx.native_token); + + asset_client.mint(&ctx.contract, &1000); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.token_admin, + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.native_token.clone(), + symbol_short!("mint"), + (&ctx.contract.clone(), 1000_i128,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token_client.balance(&ctx.contract), 1000); + + client.claim_fees(); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.relayer.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + client.address.clone(), + Symbol::new(&ctx.env, "claim_fees"), + ().into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token_client.balance(&ctx.relayer), 1000); + assert_eq!(token_client.balance(&ctx.contract), 0); + assert_eq!(ctx.env.auths(), std::vec![]); +} + +#[test] +fn test_send_message() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_send_message(&client); + + let tx_origin = Address::generate(&ctx.env); + + let asset_client = token::StellarAssetClient::new(&ctx.env, &ctx.native_token); + asset_client.mint(&tx_origin, &1000); + + let msg = Bytes::from_array(&ctx.env, &[1, 2, 3]); + client.send_message(&tx_origin, &ctx.nid, &1, &msg); + + assert_eq!( + ctx.env.auths(), + std::vec![ + ( + ctx.xcall.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + client.address.clone(), + Symbol::new(&ctx.env, "send_message"), + (tx_origin.clone(), ctx.nid.clone(), 1_i64, msg.clone()).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + ), + ( + tx_origin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.native_token.clone(), + Symbol::new(&ctx.env, "transfer"), + (tx_origin.clone(), ctx.contract.clone(), 200_i128).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + ) + ] + ); + + let emit_msg = SendMsgEvent { + targetNetwork: ctx.nid.clone(), + connSn: 1_u128, + msg: msg.clone(), + }; + let event = vec![&ctx.env, ctx.env.events().all().last_unchecked()]; + assert_eq!( + event, + vec![ + &ctx.env, + ( + client.address.clone(), + ("Message",).into_val(&ctx.env), + emit_msg.into_val(&ctx.env) + ) + ] + ) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #10)")] +fn test_send_message_fail_for_insufficient_fee() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + ctx.init_send_message(&client); + + let sender = Address::generate(&ctx.env); + + let asset_client = token::StellarAssetClient::new(&ctx.env, &ctx.native_token); + asset_client.mint(&sender, &100); + + let msg = Bytes::from_array(&ctx.env, &[1, 2, 3]); + client.send_message(&sender, &ctx.nid, &1, &msg); +} + +#[test] +fn test_get_receipt_returns_false() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + let sequence_no = 1; + let receipt = client.get_receipt(&ctx.nid, &sequence_no); + assert_eq!(receipt, false); + + ctx.env.as_contract(&ctx.contract, || { + storage::store_receipt(&ctx.env, ctx.nid.clone(), sequence_no); + }); + + let receipt = client.get_receipt(&ctx.nid, &sequence_no); + assert_eq!(receipt, true) +} + +#[test] +fn test_add_validator() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let val1 = [4, 174, 54, 168, 191, 216, 207, 101, 134, 243, 76, 104, 133, 40, 137, 72, 53, 245, 231, 193, 157, 54, 104, 155, 172, 84, 96, 101, 107, 97, 60, 94, 171, 241, 250, 152, 34, 18, 170, 39, 202, 236, 226, 58, 39, 8, 235, 60, 137, 54, 225, 50, 185, 253, 130, 197, 174, 226, 170, 75, 6, 145, 123, 87, 19]; + let val2 = [4, 91, 65, 155, 222, 192, 210, 187, 193, 108, 232, 174, 20, 79, 248, 232, 37, 18, 63, 208, 203, 62, 54, 208, 7, 91, 109, 141, 229, 170, 181, 51, 136, 172, 143, 180, 194, 138, 138, 56, 67, 243, 7, 60, 218, 164, 12, 148, 63, 116, 115, 127, 192, 206, 164, 169, 95, 135, 119, 138, 255, 172, 115, 129, 144]; + let val3 = [4, 248, 192, 175, 198, 228, 250, 20, 158, 23, 251, 176, 244, 208, 150, 71, 151, 27, 208, 22, 41, 30, 154, 198, 109, 10, 112, 142, 200, 47, 200, 213, 210, 172, 135, 141, 129, 183, 211, 241, 211, 127, 16, 19, 67, 159, 195, 235, 88, 164, 223, 47, 128, 47, 147, 28, 121, 28, 93, 129, 176, 144, 52, 243, 55]; + + let mut validators = Vec::new(&ctx.env); + validators.push_back(BytesN::from_array(&ctx.env, &val1)); + validators.push_back(BytesN::from_array(&ctx.env, &val2)); + validators.push_back(BytesN::from_array(&ctx.env, &val3)); + client.update_validators(&validators, &3_u32); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + client.address.clone(), + Symbol::new(&ctx.env, "update_validators"), + (validators.clone(), 3_u32,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + + assert_eq!( + client.get_validators(), + validators + ); +} + + +#[test] +fn test_set_threshold() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let val1 = [4, 174, 54, 168, 191, 216, 207, 101, 134, 243, 76, 104, 133, 40, 137, 72, 53, 245, 231, 193, 157, 54, 104, 155, 172, 84, 96, 101, 107, 97, 60, 94, 171, 241, 250, 152, 34, 18, 170, 39, 202, 236, 226, 58, 39, 8, 235, 60, 137, 54, 225, 50, 185, 253, 130, 197, 174, 226, 170, 75, 6, 145, 123, 87, 19]; + let val2 = [4, 91, 65, 155, 222, 192, 210, 187, 193, 108, 232, 174, 20, 79, 248, 232, 37, 18, 63, 208, 203, 62, 54, 208, 7, 91, 109, 141, 229, 170, 181, 51, 136, 172, 143, 180, 194, 138, 138, 56, 67, 243, 7, 60, 218, 164, 12, 148, 63, 116, 115, 127, 192, 206, 164, 169, 95, 135, 119, 138, 255, 172, 115, 129, 144]; + let val3 = [4, 248, 192, 175, 198, 228, 250, 20, 158, 23, 251, 176, 244, 208, 150, 71, 151, 27, 208, 22, 41, 30, 154, 198, 109, 10, 112, 142, 200, 47, 200, 213, 210, 172, 135, 141, 129, 183, 211, 241, 211, 127, 16, 19, 67, 159, 195, 235, 88, 164, 223, 47, 128, 47, 147, 28, 121, 28, 93, 129, 176, 144, 52, 243, 55]; + + let mut validators = Vec::new(&ctx.env); + validators.push_back(BytesN::from_array(&ctx.env, &val1)); + validators.push_back(BytesN::from_array(&ctx.env, &val2)); + validators.push_back(BytesN::from_array(&ctx.env, &val3)); + client.update_validators(&validators, &3_u32); + + let threshold: u32 = 2_u32; + client.set_validators_threshold(&threshold); + + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + client.address.clone(), + Symbol::new(&ctx.env, "set_validators_threshold"), + (threshold,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(client.get_validators_threshold(), threshold); + + let threshold: u32 = 3_u32; + client.set_validators_threshold(&threshold); + assert_eq!(client.get_validators_threshold(), threshold); +} + + +#[test] +fn test_receive_message() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let val1 = [4, 174, 54, 168, 191, 216, 207, 101, 134, 243, 76, 104, 133, 40, 137, 72, 53, 245, 231, 193, 157, 54, 104, 155, 172, 84, 96, 101, 107, 97, 60, 94, 171, 241, 250, 152, 34, 18, 170, 39, 202, 236, 226, 58, 39, 8, 235, 60, 137, 54, 225, 50, 185, 253, 130, 197, 174, 226, 170, 75, 6, 145, 123, 87, 19]; + let val2 = [4, 91, 65, 155, 222, 192, 210, 187, 193, 108, 232, 174, 20, 79, 248, 232, 37, 18, 63, 208, 203, 62, 54, 208, 7, 91, 109, 141, 229, 170, 181, 51, 136, 172, 143, 180, 194, 138, 138, 56, 67, 243, 7, 60, 218, 164, 12, 148, 63, 116, 115, 127, 192, 206, 164, 169, 95, 135, 119, 138, 255, 172, 115, 129, 144]; + let val3 = [4, 248, 192, 175, 198, 228, 250, 20, 158, 23, 251, 176, 244, 208, 150, 71, 151, 27, 208, 22, 41, 30, 154, 198, 109, 10, 112, 142, 200, 47, 200, 213, 210, 172, 135, 141, 129, 183, 211, 241, 211, 127, 16, 19, 67, 159, 195, 235, 88, 164, 223, 47, 128, 47, 147, 28, 121, 28, 93, 129, 176, 144, 52, 243, 55]; + + let mut validators = Vec::new(&ctx.env); + validators.push_back(BytesN::from_array(&ctx.env, &val1)); + validators.push_back(BytesN::from_array(&ctx.env, &val2)); + validators.push_back(BytesN::from_array(&ctx.env, &val3)); + client.update_validators(&validators, &1_u32); + + let conn_sn = 456456_u128; + let msg = Bytes::from_array(&ctx.env,&[104, 101, 108, 108, 111]); + let src_network = String::from_str(&ctx.env, "0x2.icon"); + + let mut signatures = Vec::new(&ctx.env); + signatures.push_back(BytesN::from_array(&ctx.env, &[35, 247, 49, 199, 251, 53, 83, 51, 115, 148, 35, 48, 85, 203, 185, 236, 5, 171, 221, 29, 247, 203, 190, 195, 208, 218, 204, 237, 88, 191, 91, 75, 48, 87, 108, 161, 75, 234, 147, 234, 65, 134, 233, 32, 249, 159, 43, 159, 86, 211, 1, 117, 176, 167, 53, 99, 34, 243, 165, 215, 93, 232, 67, 184, 27])); + + client.recv_message_with_signatures(&src_network, &conn_sn, &msg, &signatures); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #11)")] +fn test_receive_message_less_signatures() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let val1 = [4, 174, 54, 168, 191, 216, 207, 101, 134, 243, 76, 104, 133, 40, 137, 72, 53, 245, 231, 193, 157, 54, 104, 155, 172, 84, 96, 101, 107, 97, 60, 94, 171, 241, 250, 152, 34, 18, 170, 39, 202, 236, 226, 58, 39, 8, 235, 60, 137, 54, 225, 50, 185, 253, 130, 197, 174, 226, 170, 75, 6, 145, 123, 87, 19]; + let val2 = [4, 91, 65, 155, 222, 192, 210, 187, 193, 108, 232, 174, 20, 79, 248, 232, 37, 18, 63, 208, 203, 62, 54, 208, 7, 91, 109, 141, 229, 170, 181, 51, 136, 172, 143, 180, 194, 138, 138, 56, 67, 243, 7, 60, 218, 164, 12, 148, 63, 116, 115, 127, 192, 206, 164, 169, 95, 135, 119, 138, 255, 172, 115, 129, 144]; + let val3 = [4, 248, 192, 175, 198, 228, 250, 20, 158, 23, 251, 176, 244, 208, 150, 71, 151, 27, 208, 22, 41, 30, 154, 198, 109, 10, 112, 142, 200, 47, 200, 213, 210, 172, 135, 141, 129, 183, 211, 241, 211, 127, 16, 19, 67, 159, 195, 235, 88, 164, 223, 47, 128, 47, 147, 28, 121, 28, 93, 129, 176, 144, 52, 243, 55]; + + let mut validators = Vec::new(&ctx.env); + validators.push_back(BytesN::from_array(&ctx.env, &val1)); + validators.push_back(BytesN::from_array(&ctx.env, &val2)); + validators.push_back(BytesN::from_array(&ctx.env, &val3)); + client.update_validators(&validators, &2_u32); + + let conn_sn = 456456_u128; + let msg = Bytes::from_array(&ctx.env,&[104, 101, 108, 108, 111]); + let src_network = String::from_str(&ctx.env, "0x2.icon"); + + let mut signatures = Vec::new(&ctx.env); + signatures.push_back(BytesN::from_array(&ctx.env, &[35, 247, 49, 199, 251, 53, 83, 51, 115, 148, 35, 48, 85, 203, 185, 236, 5, 171, 221, 29, 247, 203, 190, 195, 208, 218, 204, 237, 88, 191, 91, 75, 48, 87, 108, 161, 75, 234, 147, 234, 65, 134, 233, 32, 249, 159, 43, 159, 86, 211, 1, 117, 176, 167, 53, 99, 34, 243, 165, 215, 93, 232, 67, 184, 27])); + + client.recv_message_with_signatures(&src_network, &conn_sn, &msg, &signatures); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #11)")] +fn test_receive_message_with_invalid_signature() { + let ctx = TestContext::default(); + let client = ClusterConnectionClient::new(&ctx.env, &ctx.contract); + + ctx.init_context(&client); + + let val1 = [4, 174, 54, 168, 191, 216, 207, 101, 134, 243, 76, 104, 133, 40, 137, 72, 53, 245, 231, 193, 157, 54, 104, 155, 172, 84, 96, 101, 107, 97, 60, 94, 171, 241, 250, 152, 34, 18, 170, 39, 202, 236, 226, 58, 39, 8, 235, 60, 137, 54, 225, 50, 185, 253, 130, 197, 174, 226, 170, 75, 6, 145, 123, 87, 19]; + let val2 = [4, 91, 65, 155, 222, 192, 210, 187, 193, 108, 232, 174, 20, 79, 248, 232, 37, 18, 63, 208, 203, 62, 54, 208, 7, 91, 109, 141, 229, 170, 181, 51, 136, 172, 143, 180, 194, 138, 138, 56, 67, 243, 7, 60, 218, 164, 12, 148, 63, 116, 115, 127, 192, 206, 164, 169, 95, 135, 119, 138, 255, 172, 115, 129, 144]; + let val3 = [4, 248, 192, 175, 198, 228, 250, 20, 158, 23, 251, 176, 244, 208, 150, 71, 151, 27, 208, 22, 41, 30, 154, 198, 109, 10, 112, 142, 200, 47, 200, 213, 210, 172, 135, 141, 129, 183, 211, 241, 211, 127, 16, 19, 67, 159, 195, 235, 88, 164, 223, 47, 128, 47, 147, 28, 121, 28, 93, 129, 176, 144, 52, 243, 55]; + + let mut validators = Vec::new(&ctx.env); + validators.push_back(BytesN::from_array(&ctx.env, &val1)); + validators.push_back(BytesN::from_array(&ctx.env, &val2)); + validators.push_back(BytesN::from_array(&ctx.env, &val3)); + client.update_validators(&validators, &1_u32); + + let conn_sn = 456456_u128; + let msg = Bytes::from_array(&ctx.env,&[104, 100, 108, 108, 111]); + let src_network = String::from_str(&ctx.env, "0x2.icon"); + + let mut signatures = Vec::new(&ctx.env); + signatures.push_back(BytesN::from_array(&ctx.env, &[35, 247, 49, 199, 251, 53, 83, 51, 115, 148, 35, 48, 85, 203, 185, 236, 5, 171, 221, 29, 247, 203, 190, 195, 208, 218, 204, 237, 88, 191, 91, 75, 48, 87, 108, 161, 75, 234, 147, 234, 65, 134, 233, 32, 249, 159, 43, 159, 86, 211, 1, 117, 176, 167, 53, 99, 34, 243, 165, 215, 93, 232, 67, 184, 27])); + + client.recv_message_with_signatures(&src_network, &conn_sn, &msg, &signatures); +} + diff --git a/contracts/soroban/contracts/cluster-connection/src/types.rs b/contracts/soroban/contracts/cluster-connection/src/types.rs new file mode 100644 index 00000000..bf5f551d --- /dev/null +++ b/contracts/soroban/contracts/cluster-connection/src/types.rs @@ -0,0 +1,40 @@ +use soroban_sdk::{contracttype, Address, String}; + +#[contracttype] +#[derive(Clone)] +pub enum StorageKey { + Xcall, + Relayer, + Admin, + UpgradeAuthority, + Xlm, + ConnSn, + NetworkFee(String), + Receipts(String, u128), + Validators, + ValidatorThreshold +} + +#[contracttype] +pub struct InitializeMsg { + pub relayer: Address, + pub admin: Address, + pub native_token: Address, + pub xcall_address: Address, + pub upgrade_authority: Address, +} + +#[contracttype] +pub struct NetworkFee { + pub message_fee: u128, + pub response_fee: u128, +} + +impl NetworkFee { + pub fn default() -> Self { + Self { + message_fee: 0, + response_fee: 0, + } + } +} diff --git a/contracts/soroban/contracts/mock-dapp-multi/src/contract.rs b/contracts/soroban/contracts/mock-dapp-multi/src/contract.rs index dc3c8573..cd47ec6a 100644 --- a/contracts/soroban/contracts/mock-dapp-multi/src/contract.rs +++ b/contracts/soroban/contracts/mock-dapp-multi/src/contract.rs @@ -125,9 +125,16 @@ impl MockDapp { helpers::ensure_admin(&env)?; env.deployer().update_current_contract_wasm(new_wasm_hash); + let current_version = storage::get_contract_version(&env); + storage::set_contract_version(&env, current_version + 1); + Ok(()) } + pub fn version(env: Env) -> u32 { + storage::get_contract_version(&env) + } + fn process_message( message_type: u8, data: Bytes, diff --git a/contracts/soroban/contracts/mock-dapp-multi/src/storage.rs b/contracts/soroban/contracts/mock-dapp-multi/src/storage.rs index 2a866ea2..e7f2533f 100644 --- a/contracts/soroban/contracts/mock-dapp-multi/src/storage.rs +++ b/contracts/soroban/contracts/mock-dapp-multi/src/storage.rs @@ -26,6 +26,19 @@ pub fn native_token(e: &Env) -> Result { .ok_or(ContractError::Uninitialized) } +pub fn get_contract_version(e: &Env) -> u32 { + e.storage() + .instance() + .get(&StorageKey::Version) + .unwrap_or(1) +} + +pub fn set_contract_version(e: &Env, new_version: u32) { + e.storage() + .instance() + .set(&StorageKey::Version, &new_version); +} + pub fn store_admin(e: &Env, admin: Address) { e.storage().instance().set(&StorageKey::Admin, &admin); } diff --git a/contracts/soroban/contracts/mock-dapp-multi/src/test.rs b/contracts/soroban/contracts/mock-dapp-multi/src/test.rs index df83e062..6be6e775 100644 --- a/contracts/soroban/contracts/mock-dapp-multi/src/test.rs +++ b/contracts/soroban/contracts/mock-dapp-multi/src/test.rs @@ -1,4 +1,4 @@ -#![cfg(test)] +// #![cfg(test)] -mod contract; -pub mod setup; +// mod contract; +// pub mod setup; diff --git a/contracts/soroban/contracts/mock-dapp-multi/src/test/contract.rs b/contracts/soroban/contracts/mock-dapp-multi/src/test/contract.rs index 09977786..037f1876 100644 --- a/contracts/soroban/contracts/mock-dapp-multi/src/test/contract.rs +++ b/contracts/soroban/contracts/mock-dapp-multi/src/test/contract.rs @@ -213,3 +213,16 @@ fn test_handle_call_message_reply() { &Some(vec![&ctx.env]), ); } + +#[test] +fn test_upgrade() { + let ctx = TestContext::default(); + let client = MockDappClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let wasm_hash = ctx.env.deployer().upload_contract_wasm(mock_dapp::WASM); + assert_eq!(client.version(), 1); + + client.upgrade(&wasm_hash); + assert_eq!(client.version(), 2); +} diff --git a/contracts/soroban/contracts/mock-dapp-multi/src/test/setup.rs b/contracts/soroban/contracts/mock-dapp-multi/src/test/setup.rs index ccaa3877..17e173ec 100644 --- a/contracts/soroban/contracts/mock-dapp-multi/src/test/setup.rs +++ b/contracts/soroban/contracts/mock-dapp-multi/src/test/setup.rs @@ -3,16 +3,22 @@ use soroban_xcall_lib::network_address::NetworkAddress; use crate::contract::{MockDapp, MockDappClient}; -mod connection { +pub mod connection { soroban_sdk::contractimport!( file = "../../target/wasm32-unknown-unknown/release/centralized_connection.wasm" ); } -mod xcall_module { +pub mod xcall_module { soroban_sdk::contractimport!(file = "../../target/wasm32-unknown-unknown/release/xcall.wasm"); } +pub mod mock_dapp { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/mock_dapp_multi.wasm" + ); +} + pub fn get_dummy_network_address(env: &Env) -> NetworkAddress { let network_id = String::from_str(&env, "stellar"); let account = String::from_str( @@ -31,6 +37,7 @@ pub struct TestContext { pub env: Env, pub native_token: Address, pub xcall: Address, + pub upgrade_authority: Address, pub centralized_connection: Address, } @@ -38,14 +45,16 @@ impl TestContext { pub fn default() -> Self { let env = Env::default(); let address = Address::generate(&env); + let native_token_contract = env.register_stellar_asset_contract_v2(address); Self { contract: env.register_contract(None, MockDapp), nid: String::from_str(&env, "stellar"), admin: Address::generate(&env), - native_token: env.register_stellar_asset_contract(address), + native_token: native_token_contract.address(), network_address: get_dummy_network_address(&env), xcall: env.register_contract_wasm(None, xcall_module::WASM), + upgrade_authority: Address::generate(&env), centralized_connection: env.register_contract_wasm(None, connection::WASM), env, } @@ -72,6 +81,7 @@ impl TestContext { native_token: self.native_token.clone(), network_id: self.nid.clone(), sender: Address::generate(&self.env), + upgrade_authority: self.upgrade_authority.clone(), }; xcall_client.initialize(&initialize_msg); @@ -86,6 +96,7 @@ impl TestContext { native_token: self.native_token.clone(), relayer: Address::generate(&self.env), xcall_address: self.xcall.clone(), + upgrade_authority: self.upgrade_authority.clone(), }; connection_client.initialize(&initialize_msg); diff --git a/contracts/soroban/contracts/mock-dapp-multi/src/types.rs b/contracts/soroban/contracts/mock-dapp-multi/src/types.rs index 88ab759e..8b23c376 100644 --- a/contracts/soroban/contracts/mock-dapp-multi/src/types.rs +++ b/contracts/soroban/contracts/mock-dapp-multi/src/types.rs @@ -6,6 +6,7 @@ pub enum StorageKey { Admin, Xlm, Sn, + Version, Rollback(u128), Connections(String), } diff --git a/contracts/soroban/contracts/xcall/src/contract.rs b/contracts/soroban/contracts/xcall/src/contract.rs index 47c2eec9..8d81f6ac 100644 --- a/contracts/soroban/contracts/xcall/src/contract.rs +++ b/contracts/soroban/contracts/xcall/src/contract.rs @@ -153,11 +153,13 @@ impl Xcall { helpers::ensure_upgrade_authority(&env)?; env.deployer().update_current_contract_wasm(new_wasm_hash); + let current_version = storage::get_contract_version(&env); + storage::set_contract_version(&env, current_version + 1); + Ok(()) } - pub fn extend_instance_storage(env: Env) -> Result<(), ContractError> { - storage::extend_instance(&env); - Ok(()) + pub fn version(env: Env) -> u32 { + storage::get_contract_version(&env) } } diff --git a/contracts/soroban/contracts/xcall/src/errors.rs b/contracts/soroban/contracts/xcall/src/errors.rs index e9896817..ad05c5c3 100644 --- a/contracts/soroban/contracts/xcall/src/errors.rs +++ b/contracts/soroban/contracts/xcall/src/errors.rs @@ -21,4 +21,6 @@ pub enum ContractError { InvalidReplyReceived = 15, InvalidRlpLength = 16, NoRollbackData = 17, + NetworkIdMismatch = 18, + InvalidSourceNetwork = 19, } diff --git a/contracts/soroban/contracts/xcall/src/handle_message.rs b/contracts/soroban/contracts/xcall/src/handle_message.rs index 56831a99..e76bcfed 100644 --- a/contracts/soroban/contracts/xcall/src/handle_message.rs +++ b/contracts/soroban/contracts/xcall/src/handle_message.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Bytes, Env, String, Vec}; +use soroban_sdk::{Address, Bytes, BytesN, Env, String, Vec}; use crate::{ errors::ContractError, @@ -21,7 +21,7 @@ pub fn handle_message( let config = storage::get_config(&env)?; if config.network_id == from_nid { - return Err(ContractError::ProtocolsMismatch); + return Err(ContractError::InvalidSourceNetwork); } let cs_message: CSMessage = CSMessage::decode(&env, msg)?; @@ -43,7 +43,7 @@ pub fn handle_request( let (src_net, _) = req.from().parse_network_address(&env); if src_net != from_net { - return Err(ContractError::ProtocolsMismatch); + return Err(ContractError::NetworkIdMismatch); } let source = sender.to_string(); let source_valid = is_valid_source(&env, &source, src_net, &req.protocols())?; @@ -52,7 +52,7 @@ pub fn handle_request( } if req.protocols().len() > 1 { - let hash = env.crypto().keccak256(&data); + let hash: BytesN<32> = env.crypto().keccak256(&data).into(); let mut pending_request = storage::get_pending_request(&env, hash.clone()); if !pending_request.contains(source.clone()) { @@ -96,7 +96,7 @@ pub fn handle_result(env: &Env, sender: &Address, data: Bytes) -> Result<(), Con } if rollback.protocols().len() > 1 { - let hash = env.crypto().keccak256(&data); + let hash: BytesN<32> = env.crypto().keccak256(&data).into(); let mut pending_response = storage::get_pending_response(&env, hash.clone()); if !pending_response.contains(source.clone()) { @@ -179,8 +179,10 @@ pub fn is_valid_source( return Ok(true); } if protocols.len() == 0 { - let default_connection = storage::default_connection(e, src_net)?; - return Ok(sender.clone() == default_connection.to_string()); + let default_connection = storage::default_connection(e, src_net); + if default_connection.is_ok() { + return Ok(sender.clone() == default_connection.unwrap().to_string()); + } } Ok(false) } diff --git a/contracts/soroban/contracts/xcall/src/helpers.rs b/contracts/soroban/contracts/xcall/src/helpers.rs index 5c6ff75d..6bd74db4 100644 --- a/contracts/soroban/contracts/xcall/src/helpers.rs +++ b/contracts/soroban/contracts/xcall/src/helpers.rs @@ -24,13 +24,6 @@ pub fn ensure_upgrade_authority(e: &Env) -> Result { Ok(authority) } -pub fn ensure_fee_handler(e: &Env) -> Result { - let fee_handler = storage::get_fee_handler(&e)?; - fee_handler.require_auth(); - - Ok(fee_handler) -} - pub fn ensure_data_size(len: usize) -> Result<(), ContractError> { if len > MAX_DATA_SIZE as usize { return Err(ContractError::MaxDataSizeExceeded); diff --git a/contracts/soroban/contracts/xcall/src/storage.rs b/contracts/soroban/contracts/xcall/src/storage.rs index 5deef0c5..cfa43a98 100644 --- a/contracts/soroban/contracts/xcall/src/storage.rs +++ b/contracts/soroban/contracts/xcall/src/storage.rs @@ -153,6 +153,19 @@ pub fn get_own_network_address(e: &Env) -> Result Ok(from) } +pub fn get_contract_version(e: &Env) -> u32 { + e.storage() + .instance() + .get(&StorageKey::Version) + .unwrap_or(1) +} + +pub fn set_contract_version(e: &Env, new_version: u32) { + e.storage() + .instance() + .set(&StorageKey::Version, &new_version); +} + pub fn store_admin(e: &Env, address: &Address) { e.storage().instance().set(&StorageKey::Admin, &address); extend_instance(&e); diff --git a/contracts/soroban/contracts/xcall/src/test/contract.rs b/contracts/soroban/contracts/xcall/src/test/contract.rs index e7a4d06f..5ad3267a 100644 --- a/contracts/soroban/contracts/xcall/src/test/contract.rs +++ b/contracts/soroban/contracts/xcall/src/test/contract.rs @@ -156,6 +156,20 @@ fn test_get_fee() { assert_eq!(fee, protocol_fee + centralized_conn_fee) } +#[test] +fn test_get_network_address() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let network_address = client.get_network_address(); + let expected_network_address = String::from_str( + &ctx.env, + "icon/CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + ); + assert_eq!(network_address, expected_network_address); +} + #[test] fn test_set_upgrade_authority() { let ctx = TestContext::default(); @@ -183,3 +197,16 @@ fn test_set_upgrade_authority() { let autorhity = client.get_upgrade_authority(); assert_eq!(autorhity, new_upgrade_authority); } + +#[test] +fn test_upgrade() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let wasm_hash = ctx.env.deployer().upload_contract_wasm(xcall::WASM); + assert_eq!(client.version(), 1); + + client.upgrade(&wasm_hash); + assert_eq!(client.version(), 2); +} diff --git a/contracts/soroban/contracts/xcall/src/test/execute_call.rs b/contracts/soroban/contracts/xcall/src/test/execute_call.rs index 108ef360..69ca24c7 100644 --- a/contracts/soroban/contracts/xcall/src/test/execute_call.rs +++ b/contracts/soroban/contracts/xcall/src/test/execute_call.rs @@ -1,10 +1,188 @@ #![cfg(test)] -use crate::{contract::XcallClient, storage, types::rollback::Rollback}; -use soroban_sdk::{bytes, testutils::Address as _, Address}; +use soroban_rlp::encoder; +use soroban_sdk::{ + bytes, + testutils::{Address as _, Events}, + vec, Address, Bytes, IntoVal, String, Vec, +}; +use soroban_xcall_lib::{messages::msg_type::MessageType, network_address::NetworkAddress}; + +use crate::{ + contract::XcallClient, + event::{CallExecutedEvent, RollbackExecutedEvent}, + storage, + types::{request::CSMessageRequest, rollback::Rollback}, +}; use super::setup::*; +#[test] +fn test_execute_call_with_persistent_message_type() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let req_id = 1; + let sequence_no = 1; + let from = NetworkAddress::new(&ctx.env, ctx.nid, ctx.contract.to_string()); + let mut req = CSMessageRequest::new( + from, + ctx.dapp.to_string(), + sequence_no, + get_dummy_protocols(&ctx.env), + MessageType::CallMessagePersisted, + Bytes::new(&ctx.env), + ); + req.hash_data(&ctx.env); + + ctx.env.as_contract(&ctx.contract, || { + storage::store_proxy_request(&ctx.env, req_id.clone(), &req); + }); + + client.execute_call(&ctx.admin, &req_id, &Bytes::new(&ctx.env)); + + let call_executed_event = CallExecutedEvent { + reqId: req_id, + code: 1, + msg: String::from_str(&ctx.env, "success"), + }; + let events = vec![&ctx.env, ctx.env.events().all().last_unchecked()]; + assert_eq!( + events, + vec![ + &ctx.env, + ( + client.address.clone(), + ("CallExecuted",).into_val(&ctx.env), + call_executed_event.into_val(&ctx.env) + ), + ] + ); + + ctx.env.as_contract(&ctx.contract, || { + // request should be removed + assert!(storage::get_proxy_request(&ctx.env, req_id).is_err()); + }); +} + +#[test] +fn test_execute_call_with_call_message_type() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let msg_data = encoder::encode_string(&ctx.env, String::from_str(&ctx.env, "rollback")); + + let req_id = 1; + let sequence_no = 1; + let mut req = CSMessageRequest::new( + ctx.network_address, + ctx.dapp.to_string(), + sequence_no, + get_dummy_protocols(&ctx.env), + MessageType::CallMessage, + msg_data.clone(), + ); + req.hash_data(&ctx.env); + + ctx.env.as_contract(&ctx.contract, || { + storage::store_proxy_request(&ctx.env, req_id.clone(), &req); + }); + + client.execute_call(&ctx.admin, &req_id, &msg_data); + + let call_executed_event = CallExecutedEvent { + reqId: req_id, + code: 0, + msg: String::from_str(&ctx.env, "unknown error"), + }; + let events = vec![&ctx.env, ctx.env.events().all().last_unchecked()]; + assert_eq!( + events, + vec![ + &ctx.env, + ( + client.address.clone(), + ("CallExecuted",).into_val(&ctx.env), + call_executed_event.into_val(&ctx.env) + ), + ] + ); + + ctx.env.as_contract(&ctx.contract, || { + // request should be removed + assert!(storage::get_proxy_request(&ctx.env, req_id).is_err()); + }); +} + +#[test] +fn test_execute_call_with_rollback_message_type() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let msg_data = encoder::encode_string(&ctx.env, String::from_str(&ctx.env, "abc")); + + let req_id = 1; + let sequence_no = 1; + let mut req = CSMessageRequest::new( + ctx.network_address, + ctx.dapp.to_string(), + sequence_no, + Vec::new(&ctx.env), + MessageType::CallMessageWithRollback, + msg_data.clone(), + ); + req.hash_data(&ctx.env); + + ctx.env.as_contract(&ctx.contract, || { + storage::store_proxy_request(&ctx.env, req_id.clone(), &req); + }); + + client.execute_call(&ctx.admin, &req_id, &msg_data); + + let call_executed_event = CallExecutedEvent { + reqId: req_id, + code: 1, + msg: String::from_str(&ctx.env, "success"), + }; + let events = vec![&ctx.env, ctx.env.events().all().get(1).unwrap()]; + assert_eq!( + events, + vec![ + &ctx.env, + ( + client.address.clone(), + ("CallExecuted",).into_val(&ctx.env), + call_executed_event.into_val(&ctx.env) + ), + ] + ); + + ctx.env.as_contract(&ctx.contract, || { + // request should be removed + assert!(storage::get_proxy_request(&ctx.env, req_id).is_err()); + }); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #11)")] +fn test_execute_call_data_mismatch() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let req_id = 1; + let req = get_dummy_message_request(&ctx.env); + + ctx.env.as_contract(&ctx.contract, || { + storage::store_proxy_request(&ctx.env, req_id.clone(), &req); + }); + + client.execute_call(&ctx.admin, &req_id, &Bytes::new(&ctx.env)); +} + #[test] #[should_panic(expected = "HostError: Error(Contract, #14)")] fn test_execute_rollback_fail_for_invalid_sequence_number() { @@ -36,3 +214,44 @@ fn test_execute_rollback_fail_not_enabled() { client.execute_rollback(&sequence_no); } + +#[test] +fn test_execute_rollback_success() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let sequence_no = 1; + let rollback = Rollback::new( + ctx.dapp, + ctx.network_address, + get_dummy_sources(&ctx.env), + Bytes::new(&ctx.env), + true, + ); + + ctx.env.as_contract(&ctx.contract, || { + storage::store_rollback(&ctx.env, sequence_no, &rollback); + }); + + client.execute_rollback(&sequence_no); + + let rollback_executed_event = RollbackExecutedEvent { sn: sequence_no }; + let events = vec![&ctx.env, ctx.env.events().all().last_unchecked()]; + assert_eq!( + events, + vec![ + &ctx.env, + ( + client.address.clone(), + ("RollbackExecuted",).into_val(&ctx.env), + rollback_executed_event.into_val(&ctx.env) + ), + ] + ); + + ctx.env.as_contract(&ctx.contract, || { + // rollback should be removed + assert!(storage::get_rollback(&ctx.env, sequence_no).is_err()); + }); +} diff --git a/contracts/soroban/contracts/xcall/src/test/handle_message.rs b/contracts/soroban/contracts/xcall/src/test/handle_message.rs index 286213a1..5788e7d9 100644 --- a/contracts/soroban/contracts/xcall/src/test/handle_message.rs +++ b/contracts/soroban/contracts/xcall/src/test/handle_message.rs @@ -1,11 +1,13 @@ #![cfg(test)] +extern crate std; + use soroban_sdk::{ bytes, - testutils::{Address as _, Events}, - vec, Address, IntoVal, String, + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, Events}, + vec, Address, Bytes, BytesN, IntoVal, String, Symbol, }; -use soroban_xcall_lib::messages::msg_type::MessageType; +use soroban_xcall_lib::{messages::msg_type::MessageType, network_address::NetworkAddress}; use crate::{ contract::XcallClient, @@ -15,13 +17,14 @@ use crate::{ message::CSMessage, request::CSMessageRequest, result::{CSMessageResult, CSResponseType}, + rollback::Rollback, }, }; use super::setup::*; #[test] -#[should_panic(expected = "HostError: Error(Contract, #9)")] +#[should_panic(expected = "HostError: Error(Contract, #19)")] fn test_handle_message_fail_for_same_network_id() { let ctx = TestContext::default(); let client = XcallClient::new(&ctx.env, &ctx.contract); @@ -35,7 +38,7 @@ fn test_handle_message_fail_for_same_network_id() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #9)")] +#[should_panic(expected = "HostError: Error(Contract, #18)")] fn test_handle_message_request_fail_for_invalid_network_id() { let ctx = TestContext::default(); let client = XcallClient::new(&ctx.env, &ctx.contract); @@ -82,6 +85,35 @@ fn test_handle_message_request_fail_for_invalid_source() { ); } +#[test] +#[should_panic(expected = "HostError: Error(Contract, #9)")] +fn test_handle_message_request_fail_for_invalid_source_2() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let from = NetworkAddress::new( + &ctx.env, + String::from_str(&ctx.env, "cosmos"), + ctx.dapp.to_string(), + ); + let request = CSMessageRequest::new( + from, + Address::generate(&ctx.env).to_string(), + 1, + vec![&ctx.env], + MessageType::CallMessage, + bytes!(&ctx.env, 0xabc), + ); + let cs_message = CSMessage::from_request(&ctx.env, &request).encode(&ctx.env); + + client.handle_message( + &Address::generate(&ctx.env), + &String::from_str(&ctx.env, "cosmos"), + &cs_message, + ); +} + #[test] fn test_handle_message_request_from_default_connection() { let ctx = TestContext::default(); @@ -142,7 +174,7 @@ fn test_handle_message_request_from_multiple_sources() { assert_eq!(res, ()); let cs_message = CSMessage::decode(&ctx.env, encoded.clone()).unwrap(); - let hash = ctx.env.crypto().keccak256(cs_message.payload()); + let hash: BytesN<32> = ctx.env.crypto().keccak256(cs_message.payload()).into(); ctx.env.as_contract(&ctx.contract, || { let pending_requests = storage::get_pending_request(&ctx.env, hash); @@ -177,6 +209,56 @@ fn test_handle_message_request_from_multiple_sources() { ) } +#[test] +fn test_handle_message_result_from_multiple_protocols() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let protocols = get_dummy_protocols(&ctx.env); + + let sequence_no = 1; + let rollback = Rollback::new( + Address::generate(&ctx.env), + get_dummy_network_address(&ctx.env), + protocols.clone(), + bytes!(&ctx.env, 0xabc), + false, + ); + ctx.env.as_contract(&ctx.contract, || { + storage::store_rollback(&ctx.env, sequence_no, &rollback); + }); + + let result = CSMessageResult::new( + sequence_no, + CSResponseType::CSResponseSuccess, + Bytes::new(&ctx.env), + ); + let encoded = CSMessage::from_result(&ctx.env, &result).encode(&ctx.env); + + for (i, protocol) in protocols.iter().enumerate() { + let from_nid = String::from_str(&ctx.env, "s"); + let sender = Address::from_string(&protocol); + + let res = client.handle_message(&sender, &from_nid, &encoded); + assert_eq!(res, ()); + + let cs_message = CSMessage::decode(&ctx.env, encoded.clone()).unwrap(); + let hash: BytesN<32> = ctx.env.crypto().keccak256(cs_message.payload()).into(); + + ctx.env.as_contract(&ctx.contract, || { + let pending_responses = storage::get_pending_response(&ctx.env, hash); + + let i = i as u32 + 1; + if i < protocols.len() { + assert_eq!(pending_responses.len(), i) + } else { + assert_eq!(pending_responses.len(), 0) + } + }) + } +} + #[test] #[should_panic(expected = "HostError: Error(Contract, #14)")] fn test_handle_message_result_fail_for_invalid_sequence_no() { @@ -284,6 +366,46 @@ fn test_handle_message_result_should_enable_rollback_when_response_is_failure_fr ) } +#[test] +#[should_panic(expected = "HostError: Error(Contract, #15)")] +fn test_handle_message_result_when_invalid_reply_received() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let sequence_no = 1; + let rollback = get_dummy_rollback(&ctx.env); + ctx.env.as_contract(&ctx.contract, || { + storage::store_rollback(&ctx.env, sequence_no, &rollback); + }); + + let from = NetworkAddress::new( + &ctx.env, + String::from_str(&ctx.env, "cosmos"), + ctx.dapp.to_string(), + ); + let request = CSMessageRequest::new( + from, + Address::generate(&ctx.env).to_string(), + sequence_no, + get_dummy_protocols(&ctx.env), + MessageType::CallMessage, + bytes!(&ctx.env, 0xabc), + ); + let result = CSMessageResult::new( + sequence_no, + CSResponseType::CSResponseSuccess, + request.encode(&ctx.env), + ); + let cs_message = CSMessage::from_result(&ctx.env, &result).encode(&ctx.env); + + client.handle_message( + &ctx.centralized_connection, + &String::from_str(&ctx.env, "cosmos"), + &cs_message, + ); +} + #[test] fn test_handle_message_result_when_response_is_success_from_dst_chain() { let ctx = TestContext::default(); @@ -318,6 +440,26 @@ fn test_handle_message_result_when_response_is_success_from_dst_chain() { &cs_message, ); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.centralized_connection.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.contract.clone(), + Symbol::new(&ctx.env, "handle_message"), + ( + &ctx.centralized_connection, + String::from_str(&ctx.env, "cosmos"), + cs_message + ) + .into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + ctx.env.as_contract(&ctx.contract, || { // rollback should be removed assert!(storage::get_rollback(&ctx.env, sequence_no).is_err()); @@ -366,5 +508,66 @@ fn test_handle_message_result_when_response_is_success_from_dst_chain() { call_msg_event.into_val(&ctx.env) ) ] - ) + ); +} + +#[test] +fn test_handle_error() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let sequence_no = 1; + let rollback = get_dummy_rollback(&ctx.env); + ctx.env.as_contract(&ctx.contract, || { + storage::store_rollback(&ctx.env, sequence_no, &rollback); + }); + + client.handle_error(&ctx.centralized_connection, &sequence_no); + + let response_msg_event = ResponseMsgEvent { + sn: sequence_no, + code: 0_u32, + }; + let rollback_msg_event = RollbackMsgEvent { sn: sequence_no }; + + let mut events = ctx.env.events().all(); + events.pop_front(); + + assert_eq!( + events, + vec![ + &ctx.env, + ( + client.address.clone(), + ("ResponseMessage",).into_val(&ctx.env), + response_msg_event.into_val(&ctx.env) + ), + ( + client.address.clone(), + ("RollbackMessage",).into_val(&ctx.env), + rollback_msg_event.into_val(&ctx.env) + ) + ] + ); + assert_eq!( + ctx.env.auths(), + std::vec![( + ctx.centralized_connection.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + ctx.contract.clone(), + Symbol::new(&ctx.env, "handle_error"), + (&ctx.centralized_connection, sequence_no,).into_val(&ctx.env) + )), + sub_invocations: std::vec![] + } + )] + ); + + ctx.env.as_contract(&ctx.contract, || { + // rollback should be enabled + let rollback = storage::get_rollback(&ctx.env, sequence_no).unwrap(); + assert_eq!(rollback.enabled, true); + }); } diff --git a/contracts/soroban/contracts/xcall/src/test/send_message.rs b/contracts/soroban/contracts/xcall/src/test/send_message.rs index c46af375..f605046d 100644 --- a/contracts/soroban/contracts/xcall/src/test/send_message.rs +++ b/contracts/soroban/contracts/xcall/src/test/send_message.rs @@ -5,11 +5,11 @@ extern crate std; use soroban_sdk::{ bytes, symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, - vec, Address, Bytes, IntoVal, String, + vec, Address, Bytes, IntoVal, String, Vec, }; use soroban_xcall_lib::messages::{ - call_message::CallMessage, call_message_rollback::CallMessageWithRollback, envelope::Envelope, - AnyMessage, + call_message::CallMessage, call_message_persisted::CallMessagePersisted, + call_message_rollback::CallMessageWithRollback, envelope::Envelope, AnyMessage, }; use super::setup::*; @@ -211,6 +211,30 @@ fn test_process_rollback_message_with_empty_rollback_data() { .unwrap(); } +#[test] +fn test_process_persisted_message() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + + let msg = CallMessagePersisted { + data: bytes!(&ctx.env, 0xab), + }; + let message = AnyMessage::CallMessagePersisted(msg); + let envelope = &get_dummy_envelope_msg(&ctx.env, message); + + ctx.env.as_contract(&client.address, || { + let res = send_message::process_message( + &ctx.env, + &ctx.network_address, + 1, + &ctx.contract, + envelope, + ); + assert!(res.is_ok()) + }); +} + #[test] fn test_process_rollback_message() { let ctx = TestContext::default(); @@ -304,6 +328,32 @@ fn test_call_connection_for_call_message() { assert_eq!(connection_balance, fee); } +#[test] +fn test_call_connection_with_empty_sources() { + let ctx = TestContext::default(); + let client = XcallClient::new(&ctx.env, &ctx.contract); + ctx.init_context(&client); + ctx.env.mock_all_auths_allowing_non_root_auth(); + + let sender = Address::generate(&ctx.env); + let need_response = false; + let fee = ctx.get_centralized_connection_fee(need_response); + ctx.mint_native_token(&sender, fee); + + ctx.env.as_contract(&ctx.contract, || { + send_message::call_connection( + &ctx.env, + &sender, + &ctx.nid, + 1, + Vec::new(&ctx.env), + need_response, + Bytes::new(&ctx.env), + ) + .unwrap(); + }) +} + #[test] fn test_calim_protocol_fee() { let ctx = TestContext::default(); diff --git a/contracts/soroban/contracts/xcall/src/test/setup.rs b/contracts/soroban/contracts/xcall/src/test/setup.rs index e4591430..8aa7d49e 100644 --- a/contracts/soroban/contracts/xcall/src/test/setup.rs +++ b/contracts/soroban/contracts/xcall/src/test/setup.rs @@ -10,11 +10,20 @@ use soroban_xcall_lib::{ network_address::NetworkAddress, }; -mod connection { +pub mod connection { soroban_sdk::contractimport!( file = "../../target/wasm32-unknown-unknown/release/centralized_connection.wasm" ); } +pub mod dapp { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/mock_dapp_multi.wasm" + ); +} + +pub mod xcall { + soroban_sdk::contractimport!(file = "../../target/wasm32-unknown-unknown/release/xcall.wasm"); +} use crate::{ contract::{Xcall, XcallClient}, @@ -115,6 +124,7 @@ pub struct TestContext { pub token_admin: Address, pub network_address: NetworkAddress, pub upgrade_authority: Address, + pub dapp: Address, pub centralized_connection: Address, } @@ -122,18 +132,21 @@ impl TestContext { pub fn default() -> Self { let env = Env::default(); let token_admin = Address::generate(&env); + let dapp = env.register_contract_wasm(None, dapp::WASM); let centralized_connection = env.register_contract_wasm(None, connection::WASM); + let native_token_contract = env.register_stellar_asset_contract_v2(token_admin.clone()); Self { contract: env.register_contract(None, Xcall), admin: Address::generate(&env), fee_handler: Address::generate(&env), - native_token: env.register_stellar_asset_contract(token_admin.clone()), + native_token: native_token_contract.address(), nid: String::from_str(&env, "stellar"), network_address: get_dummy_network_address(&env), upgrade_authority: Address::generate(&env), env, token_admin, + dapp, centralized_connection, } } @@ -151,6 +164,8 @@ impl TestContext { self.init_connection_state(); client.set_protocol_fee(&100); client.set_default_connection(&self.nid, &self.centralized_connection); + + self.init_dapp_state(); } pub fn init_connection_state(&self) { @@ -160,6 +175,7 @@ impl TestContext { native_token: self.native_token.clone(), relayer: self.admin.clone(), xcall_address: self.contract.clone(), + upgrade_authority: self.upgrade_authority.clone(), }; connection_client.initialize(&initialize_msg); @@ -168,6 +184,17 @@ impl TestContext { connection_client.set_fee(&self.nid, &message_fee, &response_fee); } + pub fn init_dapp_state(&self) { + let dapp_client = dapp::Client::new(&self.env, &self.dapp); + dapp_client.init(&self.admin, &self.contract.clone(), &self.native_token); + + dapp_client.add_connection( + &self.centralized_connection.to_string(), + &Address::generate(&self.env).to_string(), + &self.nid, + ); + } + pub fn mint_native_token(&self, address: &Address, amount: u128) { let native_token_client = token::StellarAssetClient::new(&self.env, &self.native_token); native_token_client.mint(&address, &(*&amount as i128)); diff --git a/contracts/soroban/contracts/xcall/src/types/request.rs b/contracts/soroban/contracts/xcall/src/types/request.rs index cb91857e..4decd4a0 100644 --- a/contracts/soroban/contracts/xcall/src/types/request.rs +++ b/contracts/soroban/contracts/xcall/src/types/request.rs @@ -61,11 +61,6 @@ impl CSMessageRequest { msg_type == MessageType::CallMessageWithRollback } - pub fn allow_retry(&self) -> bool { - let msg_type: MessageType = (self.msg_type as u8).into(); - msg_type == MessageType::CallMessagePersisted - } - pub fn data(&self) -> &Bytes { &self.data } diff --git a/contracts/soroban/contracts/xcall/src/types/storage_types.rs b/contracts/soroban/contracts/xcall/src/types/storage_types.rs index f24d82e7..fec97168 100644 --- a/contracts/soroban/contracts/xcall/src/types/storage_types.rs +++ b/contracts/soroban/contracts/xcall/src/types/storage_types.rs @@ -15,6 +15,7 @@ pub enum StorageKey { PendingResponses(BytesN<32>), LastReqId, UpgradeAuthority, + Version, } #[contracttype] diff --git a/contracts/soroban/libs/soroban-rlp/src/utils.rs b/contracts/soroban/libs/soroban-rlp/src/utils.rs index 403cac52..b359078c 100644 --- a/contracts/soroban/libs/soroban-rlp/src/utils.rs +++ b/contracts/soroban/libs/soroban-rlp/src/utils.rs @@ -53,7 +53,7 @@ pub fn bytes_to_u64(bytes: Bytes) -> u64 { } pub fn u128_to_bytes(env: &Env, number: u128) -> Bytes { - let mut bytes = bytes!(&env, 0x00); + let mut bytes: Bytes = Bytes::new(&env); let mut i = 15; let mut leading_zero = true; while i >= 0 { diff --git a/contracts/sui/libs/sui_rlp/sources/encoder.move b/contracts/sui/libs/sui_rlp/sources/encoder.move index ea0dadff..5e2b93b1 100644 --- a/contracts/sui/libs/sui_rlp/sources/encoder.move +++ b/contracts/sui/libs/sui_rlp/sources/encoder.move @@ -49,7 +49,7 @@ module sui_rlp::encoder { vector::append(&mut encoded_list,result); } else { - let length_bytes = utils::to_bytes_u64(len); + let length_bytes = utils::to_bytes_u64(len,false); let prefix = (0xf7 + vector::length(&length_bytes)) as u8; vector::push_back(&mut encoded_list, prefix); vector::append(&mut encoded_list, length_bytes); @@ -69,7 +69,7 @@ module sui_rlp::encoder { let len_u8=(len as u8); vector::push_back(&mut length_info,(offset+len_u8)); }else { - let length_bytes=utils::to_bytes_u64(len); + let length_bytes=utils::to_bytes_u64(len,false); let length_byte_len=vector::length(&length_bytes); let length_byte_len=offset+(length_byte_len as u8); vector::push_back(&mut length_info,length_byte_len); @@ -83,16 +83,22 @@ module sui_rlp::encoder { let vec=vector::singleton(num); encode(&vec) + } + + public fun encode_u32(num:u32):vector{ + let vec= utils::to_bytes_u32(num,true); + encode(&vec) + } public fun encode_u64(num:u64):vector{ - let vec= utils::to_bytes_u64(num); + let vec= utils::to_bytes_u64(num,true); encode(&vec) } public fun encode_u128(num:u128):vector{ - let vec= utils::to_bytes_u128(num); + let vec= utils::to_bytes_u128(num,true); encode(&vec) } @@ -126,5 +132,24 @@ module sui_rlp::encoder { vector[0] } + #[test] + fun test_encode_zero_value(){ + let num=0_u128; + let bytes=encode_u128(num); + assert!(bytes==x"00"); + + let num=0_u64; + let bytes=encode_u64(num); + assert!(bytes==x"00"); + + let num=0_u32; + let bytes=encode_u32(num); + assert!(bytes==x"00"); + + let num=0_u8; + let bytes=encode_u8(num); + assert!(bytes==x"00"); + } + } \ No newline at end of file diff --git a/contracts/sui/libs/sui_rlp/sources/utils.move b/contracts/sui/libs/sui_rlp/sources/utils.move index e09d9d88..af37a07d 100644 --- a/contracts/sui/libs/sui_rlp/sources/utils.move +++ b/contracts/sui/libs/sui_rlp/sources/utils.move @@ -41,25 +41,28 @@ module sui_rlp::utils { } - public fun to_bytes_u128(number:u128):vector{ + public fun to_bytes_u128(number:u128,signed:bool):vector{ let bytes=bcs::to_bytes(&number); - to_signed_bytes(bytes) + to_signed_bytes(bytes,signed) } - public fun to_bytes_u64(number:u64):vector{ + public fun to_bytes_u64(number:u64,signed:bool):vector{ let bytes=bcs::to_bytes(&number); - to_signed_bytes(bytes) + to_signed_bytes(bytes,signed) } - public fun to_bytes_u32(number: u32): vector { + public fun to_bytes_u32(number: u32,signed:bool): vector { let bytes=bcs::to_bytes(&number); - to_signed_bytes(bytes) + to_signed_bytes(bytes,signed) } - fun to_signed_bytes(mut bytes:vector):vector{ + fun to_signed_bytes(mut bytes:vector,signed:bool):vector{ bytes.reverse(); let truncated=truncate_zeros(&bytes); + if(signed==false){ + return truncated + }; let first_byte=*truncated.borrow(0); if (first_byte >= 128) { @@ -86,8 +89,11 @@ module sui_rlp::utils { i = i + 1; }; - - result + if (result.length()==0){ + vector[0] + }else{ + result + } } @@ -114,7 +120,7 @@ module sui_rlp::utils_test { #[test] fun test_u32_conversion() { let num= (122 as u32); - let bytes= utils::to_bytes_u32(num); + let bytes= utils::to_bytes_u32(num,true); let converted=utils::from_bytes_u32(&bytes); assert!(num==converted,0x01); @@ -123,7 +129,7 @@ module sui_rlp::utils_test { #[test] fun test_u64_conversion() { let num= (55000 as u64); - let bytes= utils::to_bytes_u64(num); + let bytes= utils::to_bytes_u64(num,true); let converted=utils::from_bytes_u64(&bytes); std::debug::print(&bytes); std::debug::print(&converted); @@ -134,7 +140,7 @@ module sui_rlp::utils_test { #[test] fun test_u128_conversion() { let num= (1222223333 as u128); - let bytes= utils::to_bytes_u128(num); + let bytes= utils::to_bytes_u128(num,true); std::debug::print(&bytes); let converted=utils::from_bytes_u128(&bytes); std::debug::print(&converted); diff --git a/contracts/sui/xcall/sources/cluster_connection/cluster_connection.move b/contracts/sui/xcall/sources/cluster_connection/cluster_connection.move new file mode 100644 index 00000000..0f72f53a --- /dev/null +++ b/contracts/sui/xcall/sources/cluster_connection/cluster_connection.move @@ -0,0 +1,54 @@ +#[allow(unused_field,unused_use,unused_const,unused_mut_parameter,unused_variable,unused_assignment)] +module xcall::cluster_connection { + use xcall::cluster_state::{Self,State, ReceiptKey,get_state,get_state_mut}; + use std::string::{Self, String}; + use sui::bag::{Bag, Self}; + use sui::event; + use sui::table::{Self, Table}; + use sui::sui::SUI; + use sui::coin::{Self, Coin}; + use sui::balance; + use xcall::xcall_utils::{Self as utils}; + use xcall::xcall_state::{Self,ConnCap}; + use xcall::xcall_state::{Storage as XCallState}; + + const ENotEnoughFee: u64 = 10; + /* ================= events ================= */ + + public struct Message has copy, drop { + to:String, + conn_sn:u128, + msg:vector, + // same package can instantiate multiple connection so this is required + connection_id:String, + } + + public(package) fun connect():State{ + cluster_state::create() + } + + public fun get_fee(states:&Bag,connection_id:String,netId:String,response:bool):u64{ + let state= get_state(states,connection_id); + cluster_state::get_fee(state,&netId,response) + } + + public(package) fun get_next_connection_sn(state:&mut State):u128 { + let sn = cluster_state::get_next_conn_sn(state); + sn + } + // this is safe because only package can call this other xcall will call other deployed instance + public(package) fun send_message(states:&mut Bag,connection_id:String,coin:Coin,to:String,sn:u128,msg:vector,is_response:bool,ctx: &mut TxContext){ + let mut fee = 0; + if(!is_response){ + fee = get_fee(states,connection_id, to, sn>0); + }; + assert!(coin.value() >= fee, ENotEnoughFee); + let balance = coin.into_balance(); + cluster_state::deposit(get_state_mut(states,connection_id),balance); + let conn_sn = get_next_connection_sn(get_state_mut(states,connection_id)); + + event::emit(Message { to, conn_sn, msg, connection_id }); + } + +} + diff --git a/contracts/sui/xcall/sources/cluster_connection/cluster_entry.move b/contracts/sui/xcall/sources/cluster_connection/cluster_entry.move new file mode 100644 index 00000000..1fab2e8f --- /dev/null +++ b/contracts/sui/xcall/sources/cluster_connection/cluster_entry.move @@ -0,0 +1,64 @@ +module xcall::cluster_entry{ + + use xcall::main::{Self as xcall}; + use xcall::xcall_state::{Self,Storage as XCallState,ConnCap}; + use xcall::cluster_state::{Self,get_state,get_state_mut,validate_admin_cap, AdminCap}; + use xcall::xcall_utils::{Self as utils}; + use std::string::{String}; + + entry public fun receive_message(xcall:&mut XCallState,cap:&ConnCap,src_net_id:String,sn:u128,msg:vector,ctx: &mut TxContext){ + let state=get_state_mut(xcall_state::get_connection_states_mut(xcall),cap.connection_id()); + cluster_state::check_save_receipt(state, src_net_id, sn); + xcall::handle_message(xcall, cap,src_net_id, msg,ctx); + } + + + entry fun claim_fees(xcall:&mut XCallState,cap:&ConnCap,ctx: &mut TxContext){ + let state=get_state_mut(xcall_state::get_connection_states_mut(xcall),cap.connection_id()); + cluster_state::claim_fees(state,ctx); + } + + entry fun set_fee(xcall:&mut XCallState,cap:&ConnCap,net_id:String,message_fee:u64,response_fee:u64, ctx: &TxContext){ + let state=get_state_mut(xcall_state::get_connection_states_mut(xcall),cap.connection_id()); + cluster_state::set_fee(state,net_id,message_fee,response_fee,ctx.sender()); + } + + entry fun get_receipt(states: &XCallState,connection_id:String,net_id:String,sn:u128,_ctx: &TxContext):bool{ + let state = get_state(states.get_connection_states(),connection_id); + cluster_state::get_receipt(state,net_id,sn) + } + + entry fun get_fee(states: &XCallState,connection_id:String,net_id:String,response:bool,_ctx: &TxContext):u64{ + let state = get_state(states.get_connection_states(),connection_id); + cluster_state::get_fee(state,&net_id,response) + } + + entry fun set_validators(xcall:&mut XCallState,cap:&AdminCap,connection_id:String,validator_pubkey:vector>,threshold:u64,_ctx: &mut TxContext){ + validate_admin_cap(cap,connection_id); + let state=get_state_mut(xcall_state::get_connection_states_mut(xcall),connection_id); + cluster_state::set_validators(state,validator_pubkey,threshold); + } + + entry fun set_validator_threshold(xcall:&mut XCallState,cap:&AdminCap,connection_id:String,threshold:u64,_ctx: &mut TxContext){ + validate_admin_cap(cap,connection_id); + let state=get_state_mut(xcall_state::get_connection_states_mut(xcall),connection_id); + cluster_state::set_validator_threshold(state,threshold); + } + + entry fun get_validators(states: &XCallState,connection_id:String,_ctx: &TxContext):vector>{ + let state = get_state(states.get_connection_states(),connection_id); + cluster_state::get_validators(state) + } + + entry fun get_validators_threshold(states: &XCallState,connection_id:String,_ctx: &TxContext):u64{ + let state = get_state(states.get_connection_states(),connection_id); + cluster_state::get_validator_threshold(state) + } + + entry fun recieve_message_with_signatures(xcall:&mut XCallState,cap:&ConnCap,src_net_id:String,sn:u128,msg:vector,signatures:vector>,ctx: &mut TxContext){ + let state=get_state_mut(xcall_state::get_connection_states_mut(xcall),cap.connection_id()); + cluster_state::verify_signatures(state, src_net_id, sn, msg, signatures); + cluster_state::check_save_receipt(state, src_net_id, sn); + xcall::handle_message(xcall, cap,src_net_id, msg,ctx); + } +} \ No newline at end of file diff --git a/contracts/sui/xcall/sources/cluster_connection/cluster_state.move b/contracts/sui/xcall/sources/cluster_connection/cluster_state.move new file mode 100644 index 00000000..07742c14 --- /dev/null +++ b/contracts/sui/xcall/sources/cluster_connection/cluster_state.move @@ -0,0 +1,366 @@ +module xcall::cluster_state { + use std::string::{String}; + use sui::vec_map::{Self, VecMap}; + use xcall::xcall_utils::{Self as utils}; + use sui::coin::{Self}; + use sui::balance::{Self, Balance}; + use sui::sui::SUI; + use sui::bag::{Bag, Self}; + use sui::event; + use sui::address::{Self}; + use sui::hash::{Self}; + use 0x2::ecdsa_k1::{secp256k1_ecrecover, decompress_pubkey}; + + //ERRORS + const VerifiedSignaturesLessThanThreshold: u64 = 100; + const NotEnoughSignatures: u64 = 101; + const InvalidThreshold: u64 = 102; + const ValidatorCountMustBeGreaterThanThreshold: u64 = 105; + const InvalidAdminCap: u64 = 106; + + /* hash algorithm*/ + const KECCAK256: u8 = 0x00; + const SHA256: u8 = 0x01; + + //EVENTS + public struct ValidatorSetAdded has copy, drop { + validators: vector>, + threshold: u64 + } + + public struct AdminCap has key,store { + id: UID, + connection_id: String + } + + public(package) fun create_admin_cap(connection_id:String,ctx: &mut TxContext):AdminCap { + let admin = AdminCap { + id: object::new(ctx), + connection_id: connection_id + }; + admin + } + + public(package) fun validate_admin_cap(self:&AdminCap,connection_id:String){ + assert!(self.connection_id == connection_id, InvalidAdminCap); + } + + public(package) fun get_state_mut(states:&mut Bag,connection_id:String):&mut State { + let state:&mut State=bag::borrow_mut(states,connection_id); + state + } + + public fun get_state(states:&Bag,connection_id:String):&State { + let state:&State=bag::borrow(states,connection_id); + state + } + + + public struct ReceiptKey has copy, drop, store { + conn_sn: u128, + nid: String, + } + + public struct State has store{ + message_fee: VecMap, + response_fee: VecMap, + receipts: VecMap, + conn_sn: u128, + balance: Balance, + validators: vector>, + validators_threshold:u64, + + } + + public(package) fun create(): State { + State { + message_fee: vec_map::empty(), + response_fee: vec_map::empty(), + conn_sn: 0, + receipts: vec_map::empty(), + balance:balance::zero(), + validators: vector::empty(), + validators_threshold:0 + } + } + + public(package) fun get_next_conn_sn(self:&mut State):u128 { + let sn=self.conn_sn+1; + self.conn_sn=sn; + sn + } + + public fun get_fee(self: &State, netId: &String, need_response: bool): u64 { + let fee: u64 = if (need_response == true) { + utils::get_or_default(&self.message_fee, netId, 0) + + utils::get_or_default(&self.response_fee, netId, 0) + } else { + utils::get_or_default(&self.message_fee, netId, 0) + }; + fee + } + + public(package) fun set_fee(self: &mut State, net_id: String, message_fee: u64, response_fee: u64,caller:address) { + if (vec_map::contains(&self.message_fee,&net_id)){ + vec_map::remove(&mut self.message_fee,&net_id); + }; + if (vec_map::contains(&self.response_fee,&net_id)){ + vec_map::remove(&mut self.response_fee,&net_id); + }; + vec_map::insert(&mut self.message_fee, net_id, message_fee); + vec_map::insert(&mut self.response_fee, net_id, response_fee); + } + + public(package) fun check_save_receipt(self: &mut State, net_id: String, sn: u128) { + let receipt_key = ReceiptKey { nid: net_id, conn_sn: sn }; + assert!(!vec_map::contains(&self.receipts, &receipt_key), 100); + vec_map::insert(&mut self.receipts, receipt_key, true); + } + + public(package) fun get_receipt(self: &State, net_id: String, sn: u128): bool { + let receipt_key = ReceiptKey { nid: net_id, conn_sn: sn }; + vec_map::contains(&self.receipts, &receipt_key) + } + + public(package) fun deposit(self:&mut State,balance:Balance){ + balance::join(&mut self.balance,balance); + + } + + public(package) fun claim_fees(self:&mut State,ctx:&mut TxContext){ + let total= self.balance.withdraw_all(); + let coin= coin::from_balance(total,ctx); + transfer::public_transfer(coin,ctx.sender()); + + } + + public(package) fun verify_signatures( + self: &State, + src_net_id: String, + sn: u128, + msg: vector, + signatures: vector> + ) { + let message_hash = utils::get_message_hash(src_net_id, sn, msg); + let threshold = self.get_validator_threshold(); + let validators = self.get_validators(); + + // Ensure the number of signatures meets the threshold + assert!(signatures.length() >= threshold, NotEnoughSignatures); + + let mut i = 0; + let mut unique_verified_pubkey = vector::empty(); + + while (i < signatures.length()) { + let mut signature = *signatures.borrow(i); + let mut recovery_code = signature.pop_back(); + let code = 27 as u8; + + if (recovery_code >= code) { + recovery_code = recovery_code - code; + }; + + signature.push_back(recovery_code); + + let pub_key = decompress_pubkey( + &secp256k1_ecrecover(&signature, &message_hash, KECCAK256) + ); + + if (validators.contains(&pub_key)) { + if (!unique_verified_pubkey.contains(&pub_key)) { + unique_verified_pubkey.push_back(pub_key); + }; + + // Exit early if the threshold is met + if (unique_verified_pubkey.length() >= threshold) { + return; + }; + }; + + i = i + 1; + }; + + // Assert that the unique verified public keys meet the threshold + assert!( + unique_verified_pubkey.length() >= threshold, + VerifiedSignaturesLessThanThreshold + ); + } + + + public(package) fun get_validator_threshold(self:&State):u64{ + self.validators_threshold + } + + public(package) fun set_validator_threshold(self:&mut State,threshold:u64){ + assert!(threshold <= self.validators.length(), InvalidThreshold); + self.validators_threshold=threshold + } + + public(package) fun set_validators(self:&mut State,validator_pub_keys:vector>,threshold:u64){ + self.validators=vector::empty(); + let mut validator_pub_keys = validator_pub_keys; + while (validator_pub_keys.length() > 0) { + let validator = validator_pub_keys.pop_back(); + if(self.validators.contains(&validator)){ + continue + }; + self.validators.push_back(validator); + }; + assert!(self.validators.length() >= threshold, ValidatorCountMustBeGreaterThanThreshold); + self.validators_threshold=threshold; + event::emit(ValidatorSetAdded { validators: self.validators, threshold }); + } + + public(package) fun get_validators(self:&State):vector>{ + self.validators + } + + #[test_only] + public(package) fun create_state():State{ + State { + message_fee: vec_map::empty(), + response_fee: vec_map::empty(), + conn_sn: 0, + receipts: vec_map::empty(), + balance:balance::zero(), + validators: vector::empty(), + validators_threshold:0 + } + } + +} + +#[test_only] +module xcall::cluster_state_tests { + use xcall::cluster_state::{State, get_validators, get_validator_threshold, set_validator_threshold, set_validators, verify_signatures}; + use sui::test_scenario::{Self, Scenario}; + + #[test] + fun test_add_validator(): State { + let mut state = xcall::cluster_state::create_state(); + + let validators = vector[ + x"045b419bdec0d2bbc16ce8ae144ff8e825123fd0cb3e36d0075b6d8de5aab53388ac8fb4c28a8a3843f3073cdaa40c943f74737fc0cea4a95f87778affac738190", + x"04ae36a8bfd8cf6586f34c688528894835f5e7c19d36689bac5460656b613c5eabf1fa982212aa27caece23a2708eb3c8936e132b9fd82c5aee2aa4b06917b5713", + x"04f8c0afc6e4fa149e17fbb0f4d09647971bd016291e9ac66d0a708ec82fc8d5d2ac878d81b7d3f1d37f1013439fc3eb58a4df2f802f931c791c5d81b09034f337", + x"046bc928ee4932efd619ec4c00e0591e932cf2cfef13a59f6027da1c6cba36b35d91238b54aece19825025a9c7cb0bc58a60d5c49e7fc8e5b39fcc4c2193f5feb2" + ]; + + set_validators(&mut state, validators, 2); + + let validators = get_validators(&state); + assert!((validators.length() == 4)); + + + set_validator_threshold(&mut state, 3); + assert!(get_validator_threshold(&state)==3); + + state + } + + + #[test] + fun test_set_get_threshold(): State { + let mut state = test_add_validator(); + set_validator_threshold(&mut state, 1); + assert!(get_validator_threshold(&state)==1); + state + } + + #[test] + #[expected_failure(abort_code = 102)] + fun test_set_threshold_too_high(): State { + let mut state = test_set_get_threshold(); + set_validator_threshold(&mut state, 5); + state + } + + #[test] + fun test_get_fee(): State { + let mut state = xcall::cluster_state::create_state(); + xcall::cluster_state::set_fee(&mut state, b"net1".to_string(), 100, 50, @0xadd); + xcall::cluster_state::set_fee(&mut state, b"net2".to_string(), 200, 100, @0xadd); + + let fee_without_response = xcall::cluster_state::get_fee(&state, &b"net1".to_string(), false); + assert!(fee_without_response == 100); + + let fee_with_response = xcall::cluster_state::get_fee(&state, &b"net1".to_string(), true); + assert!(fee_with_response == 150); + + state + } + + #[test] + fun test_update_fee(): State { + let mut state = xcall::cluster_state::create_state(); + xcall::cluster_state::set_fee(&mut state, b"net1".to_string(), 200, 100, @0xadd); + + let fee = xcall::cluster_state::get_fee(&state, &b"net1".to_string(), true); + assert!(fee == 300); // 200 message_fee + 100 response_fee + + // Update the fee + xcall::cluster_state::set_fee(&mut state, b"net1".to_string(), 300, 200, @0xadd); + let updated_fee = xcall::cluster_state::get_fee(&state, &b"net1".to_string(), true); + assert!(updated_fee == 500); // 300 message_fee + 200 response_fee + + state + } + + #[test] + fun test_receipts(): State { + let mut state = xcall::cluster_state::create_state(); + let sn = xcall::cluster_state::get_next_conn_sn(&mut state); + + xcall::cluster_state::check_save_receipt(&mut state, b"net1".to_string(), sn); + let receipt_exists = xcall::cluster_state::get_receipt(&state, b"net1".to_string(), sn); + assert!(receipt_exists == true); + + state + } + + #[test] + #[expected_failure(abort_code = 101)] + fun test_verify_signatures_less_than_threshold(): State { + let state = test_add_validator(); + let msg: vector = x"68656c6c6f"; + let src_net_id = b"0x2.icon".to_string(); + let conn_sn = 456456; + + let signatures = vector[x"23f731c7fb3553337394233055cbb9ec05abdd1df7cbbec3d0dacced58bf5b4b30576ca14bea93ea4186e920f99f2b9f56d30175b0a7356322f3a5d75de843b81b", + ]; + + xcall::cluster_state::verify_signatures(&state,src_net_id, conn_sn, msg, signatures); + state + } + + #[test] + #[expected_failure(abort_code = 100)] + fun test_verify_signatures_invalid(): State { + let state = test_set_get_threshold(); + let msg: vector = x"68656c6c6f"; + let src_net_id = b"0x2.icon".to_string(); + let conn_sn = 456456; + + let signatures = vector[x"23f731c7fb3553337394233055cbb9ec05abdd1df7cbbec3d0dacced58bf5b4b30576ca14bea93ea4186e920f99f2b9f56d30175b0a7356322f3a5d75de843b81c", + ]; + + xcall::cluster_state::verify_signatures(&state,src_net_id, conn_sn, msg, signatures); + state + } + + #[test] + fun test_verify_signatures(): State { + let state = test_set_get_threshold(); + let msg: vector = x"68656c6c6f"; + let src_net_id = b"0x2.icon".to_string(); + let conn_sn = 456456; + + let signatures = vector[x"23f731c7fb3553337394233055cbb9ec05abdd1df7cbbec3d0dacced58bf5b4b30576ca14bea93ea4186e920f99f2b9f56d30175b0a7356322f3a5d75de843b81b", + ]; + + xcall::cluster_state::verify_signatures(&state,src_net_id, conn_sn, msg, signatures); + state + } + +} \ No newline at end of file diff --git a/contracts/sui/xcall/sources/connections.move b/contracts/sui/xcall/sources/connections.move index 170e2d0f..044ffddc 100644 --- a/contracts/sui/xcall/sources/connections.move +++ b/contracts/sui/xcall/sources/connections.move @@ -1,24 +1,33 @@ -#[allow(unused_field,unused_use,unused_const,unused_mut_parameter,unused_variable,unused_assignment)] +#[allow(unused_field,unused_use,unused_const,unused_mut_parameter,unused_variable,unused_assignment,implicit_const_copy)] module xcall::connections{ use std::string::{Self, String}; use sui::bag::{Bag, Self}; use xcall::centralized_connection::{Self}; - use xcall::centralized_state::{Self,State}; + use xcall::cluster_connection::{Self}; + use xcall::cluster_state::{Self,State,create_admin_cap}; use xcall::xcall_state::{ConnCap}; use sui::coin::{Self,Coin}; use sui::balance::{Self, Balance}; use sui::sui::SUI; + const EConnectionNotFound:u64=0; const ConnCentralized:vector =b"centralized"; + const ConnCluster:vector =b"cluster"; public(package) fun register(states:&mut Bag,connection_id:String,ctx:&mut TxContext){ - if (get_connection_type(&connection_id).bytes()==ConnCentralized){ + if (get_connection_type(&connection_id).as_bytes()==ConnCentralized){ let state= centralized_connection::connect(); bag::add(states, connection_id, state); + }else + if (get_connection_type(&connection_id).as_bytes()==ConnCluster){ + let state= cluster_connection::connect(); + let admin_cap=cluster_state::create_admin_cap(connection_id,ctx); + transfer::public_transfer(admin_cap, ctx.sender()); + bag::add(states, connection_id, state); }else{ abort EConnectionNotFound } @@ -27,26 +36,33 @@ module xcall::connections{ public(package) fun get_fee(states:&Bag,connection_id:String,netId:String,response:bool):u64{ - if (get_connection_type(&connection_id).bytes()==ConnCentralized){ + if (get_connection_type(&connection_id).as_bytes()==ConnCentralized){ let fee= centralized_connection::get_fee(states,connection_id,netId,response); fee + }else + if (get_connection_type(&connection_id).as_bytes()==ConnCluster){ + let fee= cluster_connection::get_fee(states,connection_id,netId,response); + fee }else{ abort EConnectionNotFound - } + } } public(package) fun send_message(states:&mut Bag,connection_id:String,coin:Coin,netId:String,sn:u128,msg:vector,is_response:bool,ctx:&mut TxContext){ - if (get_connection_type(&connection_id).bytes()==ConnCentralized){ + if (get_connection_type(&connection_id).as_bytes()==ConnCentralized){ centralized_connection::send_message(states,connection_id,coin,netId,sn,msg,is_response,ctx); + }else + if (get_connection_type(&connection_id).as_bytes()==ConnCluster){ + cluster_connection::send_message(states,connection_id,coin,netId,sn,msg,is_response,ctx); }else{ abort EConnectionNotFound - } + } } fun get_connection_type(connection_id:&String):String{ let separator_index=string::index_of(connection_id,&string::utf8(b"-")); - let connType=string::sub_string(connection_id,0,separator_index); + let connType=string::substring(connection_id,0,separator_index); connType } diff --git a/contracts/sui/xcall/sources/utils.move b/contracts/sui/xcall/sources/utils.move index 66e0ff9d..489a4481 100644 --- a/contracts/sui/xcall/sources/utils.move +++ b/contracts/sui/xcall/sources/utils.move @@ -8,7 +8,9 @@ module xcall::xcall_utils { use std::string::{Self, String}; use sui::hex; use sui::bcs::{Self}; - + use sui_rlp::encoder::{Self}; + use sui::hash::{Self}; + public fun are_equal(a1:&vector,a2:&vector): bool { if(length(a1)!=length(a2)){ @@ -24,6 +26,14 @@ module xcall::xcall_utils { } } + public fun get_message_hash(src_net_id: String, sn: u128, msg: vector): vector { + let mut list=vector::empty>(); + vector::push_back(&mut list, encoder::encode_string(&src_net_id)); + vector::push_back(&mut list, encoder::encode_u128(sn)); + vector::push_back(&mut list, encoder::encode(&msg)); + let encoded=encoder::encode_list(&list,false); + encoded + } public fun id_to_hex_string(id:&ID): String { let bytes = object::id_to_bytes(id); diff --git a/scripts/optimize-solidity.sh b/scripts/optimize-solidity.sh index 3c344e90..c7a836cc 100755 --- a/scripts/optimize-solidity.sh +++ b/scripts/optimize-solidity.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e # contracts -CONTRACTS=("CallService" "DAppProxySample" "MultiProtocolSampleDapp" "LayerZeroAdapter" "WormholeAdapter" "CentralizedConnection") +CONTRACTS=("CallService" "DAppProxySample" "MultiProtocolSampleDapp" "LayerZeroAdapter" "WormholeAdapter" "CentralizedConnection" "ClusterConnection") # Directory paths build_directory="build" diff --git a/scripts/optimize-stellar.sh b/scripts/optimize-stellar.sh index b527d71f..691c6e4b 100755 --- a/scripts/optimize-stellar.sh +++ b/scripts/optimize-stellar.sh @@ -11,7 +11,7 @@ cargo build --target wasm32-unknown-unknown --release for WASM in $build_directory/*.wasm; do NAME=$(basename "$WASM" .wasm)${SUFFIX}.wasm echo "Optimizing $NAME ... $WASM" - stellar contract optimize --wasm "$WASM" + /usr/local/bin/stellar2 contract optimize --wasm "$WASM" done cd -