diff --git a/.github/workflows/stellar-build-and-test.yml b/.github/workflows/stellar-build-and-test.yml index bd23e8620..9c2cffe0e 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 36dc35858..224d4770e 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 8ee82d526..19f8128c3 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 000000000..2f333310a --- /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 76d931ce7..c4bbd8cf5 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 625fb3c2b..4ca003c96 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 000000000..2612acaba --- /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 000000000..426026cac --- /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 000000000..16d600cf7 --- /dev/null +++ b/contracts/javascore/aggregator/src/main/java/relay/aggregator/Packet.java @@ -0,0 +1,177 @@ +package relay.aggregator; + +import java.math.BigInteger; + +import score.ByteArrayObjectWriter; +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"); + this.srcNetwork = srcNetwork; + this.srcContractAddress = srcContractAddress; + this.srcSn = srcSn; + this.srcHeight = srcHeight; + this.dstNetwork = dstNetwork; + this.dstContractAddress = dstContractAddress; + this.data = data; + } + + public byte[] getId() { + return Context.hash("sha-256", this.toBytes()); + } + + /** + * 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; + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + Packet.writeObject(writer, this); + return writer.toByteArray(); + } +} 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 000000000..440b8c199 --- /dev/null +++ b/contracts/javascore/aggregator/src/main/java/relay/aggregator/RelayAggregator.java @@ -0,0 +1,313 @@ +/* + * 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); + } + } + + @External + public void setAdmin(Address _admin) { + adminOnly(); + + Context.require(admin.get() != _admin, "admin already set"); + + 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); + } + + for (int i = 0; i < relayers.size(); i++) { + Address oldRelayer = relayers.get(i); + if (!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, + BigInteger srcHeight, + String dstNetwork, + String dstContractAddress, + byte[] data) { + Packet pkt = new Packet(srcNetwork, srcContractAddress, srcSn, srcHeight, dstNetwork, dstContractAddress, data); + byte[] pktID = pkt.getId(); + 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); + byte[] 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(pktID); + 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(byte[] pktID) { + 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(byte[] 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 admin"); + } + + 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(byte[] 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(byte[] 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 000000000..4165d4e66 --- /dev/null +++ b/contracts/javascore/aggregator/src/test/java/relay/aggregator/RelayAggregatorTest.java @@ -0,0 +1,361 @@ +package relay.aggregator; + +import java.math.BigInteger; + +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.assertThrows; +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[] { 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() { + Account newAdminAc = sm.createAccount(); + aggregator.invoke(adminAc, "setAdmin", newAdminAc.getAddress()); + + Address newAdmin = (Address) aggregator.call("getAdmin"); + assertEquals(newAdminAc.getAddress(), newAdmin); + } + + @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 admin", 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 admin", + 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[] { 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 admin", + 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, srcHeight, dstNetwork, dstContractAddress, data); + assertEquals(submitted, true); + } + + @Test + public void testPacketSubmitted_false() 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 }; + + boolean submitted = (boolean) aggregator.call("packetSubmitted", + relayerOneAc.getAddress(), srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, dstContractAddress, data); + 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); + + Packet pkt = new Packet(srcNetwork, srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, data); + byte[] pktID = pkt.getId(); + 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[] signOne = relayerOne.sign(dataHash); + aggregator.invoke(relayerOneAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, + data, + signOne); + + byte[] signTwo = relayerTwo.sign(dataHash); + aggregator.invoke(relayerTwoAc, "submitPacket", srcNetwork, + srcContractAddress, srcSn, srcHeight, dstNetwork, + dstContractAddress, + data, + signTwo); + + byte[][] sigs = new byte[2][]; + sigs[0] = signOne; + sigs[1] = signTwo; + + byte[] encodedSigs = RelayAggregator.serializeSignatures(sigs); + byte[][] decodedSigs = RelayAggregator.deserializeSignatures(encodedSigs); + + assertArrayEquals(signOne, decodedSigs[0]); + assertArrayEquals(signTwo, 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 000000000..91358eedf --- /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 000000000..6546187fa --- /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 000000000..c856f5a79 --- /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 000000000..4632fa285 --- /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 6e6fdcdf9..4f1da5a87 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/solana/libs/xcall-lib/src/xcall_type.rs b/contracts/solana/libs/xcall-lib/src/xcall_type.rs index 3ec5cf072..aaececfa8 100644 --- a/contracts/solana/libs/xcall-lib/src/xcall_type.rs +++ b/contracts/solana/libs/xcall-lib/src/xcall_type.rs @@ -25,12 +25,14 @@ pub struct HandleMessageArgs { pub from_nid: String, pub message: Vec, pub sequence_no: u128, + pub conn_sn: u128, } #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] pub struct HandleRequestArgs { pub from_nid: String, pub msg_payload: Vec, + pub conn_sn: u128, } #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] @@ -38,6 +40,7 @@ pub struct HandleResultArgs { pub from_nid: String, pub msg_payload: Vec, pub sequence_no: u128, + pub conn_sn: u128, } #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] @@ -48,4 +51,7 @@ pub struct HandleErrorArgs { #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] pub struct HandleForcedRollback { pub req_id: u128, + pub from_nid: String, + pub conn_sn: u128, + pub connection: Pubkey, } diff --git a/contracts/solana/programs/centralized-connection/src/helper.rs b/contracts/solana/programs/centralized-connection/src/helper.rs index 927c17a5f..96ead7ab0 100644 --- a/contracts/solana/programs/centralized-connection/src/helper.rs +++ b/contracts/solana/programs/centralized-connection/src/helper.rs @@ -46,12 +46,14 @@ pub fn call_xcall_handle_message<'info>( from_nid: String, message: Vec, sequence_no: u128, + conn_sn: u128, ) -> Result<()> { let mut data = vec![]; let args = xcall_type::HandleMessageArgs { from_nid, message, sequence_no, + conn_sn, }; args.serialize(&mut data)?; diff --git a/contracts/solana/programs/centralized-connection/src/instructions/query_accounts.rs b/contracts/solana/programs/centralized-connection/src/instructions/query_accounts.rs index 7f1496235..9f21112fc 100644 --- a/contracts/solana/programs/centralized-connection/src/instructions/query_accounts.rs +++ b/contracts/solana/programs/centralized-connection/src/instructions/query_accounts.rs @@ -2,7 +2,7 @@ use anchor_lang::{ prelude::*, solana_program::{ instruction::Instruction, - program::{get_return_data, invoke}, + program::{get_return_data, invoke, invoke_signed}, system_program, }, }; @@ -35,8 +35,8 @@ pub fn query_send_message_accounts<'info>( }) } -pub fn query_recv_message_accounts( - ctx: Context, +pub fn query_recv_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, src_network: String, conn_sn: u128, msg: Vec, @@ -65,8 +65,8 @@ pub fn query_recv_message_accounts( AccountMetadata::new(authority, false), ]; - let mut xcall_account_metas = vec![]; - let mut xcall_account_infos = vec![]; + let mut xcall_account_metas = vec![AccountMeta::new_readonly(config.key(), true)]; + let mut xcall_account_infos = vec![config.to_account_info()]; for (_, account) in ctx.remaining_accounts.iter().enumerate() { if account.is_writable { @@ -78,7 +78,7 @@ pub fn query_recv_message_accounts( xcall_account_infos.push(account.to_account_info()) } - let ix_data = get_handle_message_ix_data(src_network, msg, sequence_no)?; + let ix_data = get_handle_message_ix_data(src_network, msg, sequence_no, conn_sn)?; let ix = Instruction { program_id: config.xcall, @@ -86,7 +86,11 @@ pub fn query_recv_message_accounts( data: ix_data, }; - invoke(&ix, &xcall_account_infos)?; + invoke_signed( + &ix, + &xcall_account_infos, + &[&[Config::SEED_PREFIX.as_bytes(), &[config.bump]]], + )?; let (_, data) = get_return_data().unwrap(); let mut data_slice: &[u8] = &data; @@ -171,12 +175,14 @@ pub fn get_handle_message_ix_data( from_nid: String, message: Vec, sequence_no: u128, + conn_sn: u128, ) -> Result> { let mut ix_args_data = vec![]; let ix_args = xcall_type::HandleMessageArgs { from_nid, message, sequence_no, + conn_sn, }; ix_args.serialize(&mut ix_args_data)?; diff --git a/contracts/solana/programs/centralized-connection/src/lib.rs b/contracts/solana/programs/centralized-connection/src/lib.rs index 7bb0ac673..a7755eef9 100644 --- a/contracts/solana/programs/centralized-connection/src/lib.rs +++ b/contracts/solana/programs/centralized-connection/src/lib.rs @@ -72,7 +72,7 @@ pub mod centralized_connection { msg: Vec, sequence_no: u128, ) -> Result<()> { - helper::call_xcall_handle_message(ctx, src_network, msg, sequence_no) + helper::call_xcall_handle_message(ctx, src_network, msg, sequence_no, conn_sn) } pub fn revert_message<'info>( diff --git a/contracts/solana/programs/mock-dapp-multi/src/instructions/execute_forced_rollback.rs b/contracts/solana/programs/mock-dapp-multi/src/instructions/execute_forced_rollback.rs index 3fe45ae78..7bd63c642 100644 --- a/contracts/solana/programs/mock-dapp-multi/src/instructions/execute_forced_rollback.rs +++ b/contracts/solana/programs/mock-dapp-multi/src/instructions/execute_forced_rollback.rs @@ -6,8 +6,11 @@ use crate::{state::*, xcall}; pub fn execute_forced_rollback<'info>( ctx: Context<'_, '_, '_, 'info, ExecuteForcedRollbackCtx<'info>>, req_id: u128, + from_nid: String, + conn_sn: u128, + connection: Pubkey, ) -> Result<()> { - let ix_data = xcall::get_handle_forced_rollback_ix_data(req_id)?; + let ix_data = xcall::get_handle_forced_rollback_ix_data(req_id, from_nid, conn_sn, connection)?; xcall::call_xcall_handle_forced_rollback( &ix_data, diff --git a/contracts/solana/programs/mock-dapp-multi/src/lib.rs b/contracts/solana/programs/mock-dapp-multi/src/lib.rs index c56b9bb88..2b0f3a866 100644 --- a/contracts/solana/programs/mock-dapp-multi/src/lib.rs +++ b/contracts/solana/programs/mock-dapp-multi/src/lib.rs @@ -64,8 +64,11 @@ pub mod mock_dapp_multi { pub fn execute_forced_rollback<'info>( ctx: Context<'_, '_, '_, 'info, ExecuteForcedRollbackCtx<'info>>, req_id: u128, + from_nid: String, + conn_sn: u128, + connection: Pubkey, ) -> Result<()> { - instructions::execute_forced_rollback(ctx, req_id) + instructions::execute_forced_rollback(ctx, req_id, from_nid, conn_sn, connection) } #[allow(unused_variables)] diff --git a/contracts/solana/programs/mock-dapp-multi/src/xcall.rs b/contracts/solana/programs/mock-dapp-multi/src/xcall.rs index d4cc32f7a..7fd367fd1 100644 --- a/contracts/solana/programs/mock-dapp-multi/src/xcall.rs +++ b/contracts/solana/programs/mock-dapp-multi/src/xcall.rs @@ -100,9 +100,19 @@ pub fn get_send_call_ix_data(msg: Vec, to: NetworkAddress) -> Result Ok(ix_data) } -pub fn get_handle_forced_rollback_ix_data(req_id: u128) -> Result> { +pub fn get_handle_forced_rollback_ix_data( + req_id: u128, + from_nid: String, + conn_sn: u128, + connection: Pubkey, +) -> Result> { let mut ix_args_data = vec![]; - let ix_args = xcall_type::HandleForcedRollback { req_id }; + let ix_args = xcall_type::HandleForcedRollback { + req_id, + from_nid, + conn_sn, + connection, + }; ix_args.serialize(&mut ix_args_data)?; let ix_data = helpers::get_instruction_data(HANDLE_FORCED_ROLLBACK_IX, ix_args_data); diff --git a/contracts/solana/programs/xcall/src/event.rs b/contracts/solana/programs/xcall/src/event.rs index c21900546..3d4bf6531 100644 --- a/contracts/solana/programs/xcall/src/event.rs +++ b/contracts/solana/programs/xcall/src/event.rs @@ -16,6 +16,8 @@ pub struct CallMessage { pub sn: u128, pub reqId: u128, pub data: Vec, + pub connection: Pubkey, + pub connSn: u128, } #[event] diff --git a/contracts/solana/programs/xcall/src/instructions/execute_call.rs b/contracts/solana/programs/xcall/src/instructions/execute_call.rs index 1ea976b76..329350344 100644 --- a/contracts/solana/programs/xcall/src/instructions/execute_call.rs +++ b/contracts/solana/programs/xcall/src/instructions/execute_call.rs @@ -96,7 +96,7 @@ pub fn execute_call<'info>( } #[derive(Accounts)] -#[instruction(req_id : u128)] +#[instruction(req_id : u128, from_nid: String, conn_sn: u128, connection: Pubkey)] pub struct ExecuteCallCtx<'info> { /// The account that signs and pays for the transaction. This account is mutable /// because it will be debited for any fees or rent required during the transaction. @@ -125,7 +125,7 @@ pub struct ExecuteCallCtx<'info> { /// calls. The account is closed after use, with any remaining funds sent to the `admin`. #[account( mut, - seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), from_nid.as_bytes(), &conn_sn.to_be_bytes(), &connection.to_bytes()], bump = proxy_request.bump, close = admin )] diff --git a/contracts/solana/programs/xcall/src/instructions/handle_forced_rollback.rs b/contracts/solana/programs/xcall/src/instructions/handle_forced_rollback.rs index 19b02b37a..424ec86d4 100644 --- a/contracts/solana/programs/xcall/src/instructions/handle_forced_rollback.rs +++ b/contracts/solana/programs/xcall/src/instructions/handle_forced_rollback.rs @@ -77,7 +77,7 @@ pub fn handle_forced_rollback<'info>( } #[derive(Accounts)] -#[instruction(req_id: u128)] +#[instruction(req_id: u128, from_nid: String, conn_sn: u128, connection: Pubkey)] pub struct HandleForcedRollbackCtx<'info> { /// The account that signs and pays for the transaction. This account is mutable because /// it will be debited for any fees or rent required during the transaction. @@ -111,7 +111,7 @@ pub struct HandleForcedRollbackCtx<'info> { /// calls and is closed after use, with any remaining funds sent to the `admin`. #[account( mut, - seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), from_nid.as_bytes(), &conn_sn.to_be_bytes(), &connection.to_bytes()], bump = proxy_request.bump, close = admin )] diff --git a/contracts/solana/programs/xcall/src/instructions/handle_message.rs b/contracts/solana/programs/xcall/src/instructions/handle_message.rs index 562ea80e7..ef3e6617d 100644 --- a/contracts/solana/programs/xcall/src/instructions/handle_message.rs +++ b/contracts/solana/programs/xcall/src/instructions/handle_message.rs @@ -26,6 +26,8 @@ use crate::{ /// - `message`: The encoded message payload received from the chain. /// - `sequence_no`: The sequence number associated with the message, used to track message /// ordering and responses. +/// - `conn_sn`: The sequence number of connection associated with the message, used to derive +/// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if successful, or an appropriate error if any validation or @@ -35,6 +37,7 @@ pub fn handle_message<'info>( from_nid: String, message: Vec, sequence_no: u128, + conn_sn: u128, ) -> Result<()> { let config = &ctx.accounts.config; if config.network_id == from_nid.to_string() { @@ -48,7 +51,7 @@ pub fn handle_message<'info>( return Err(XcallError::PendingResponseAccountMustNotBeSpecified.into()); } - invoke_handle_request(ctx, from_nid, cs_message.payload)? + invoke_handle_request(ctx, from_nid, cs_message.payload, conn_sn)? } CSMessageType::CSMessageResult => { let rollback_account = ctx @@ -67,7 +70,7 @@ pub fn handle_message<'info>( return Ok(()); } - invoke_handle_result(ctx, from_nid, cs_message.payload, sequence_no)?; + invoke_handle_result(ctx, from_nid, cs_message.payload, sequence_no, conn_sn)?; } } Ok(()) @@ -84,6 +87,8 @@ pub fn handle_message<'info>( /// - `ctx`: Context containing all relevant accounts and program-specific information. /// - `from_nid`: Network ID of the source chain that sent the request. /// - `payload`: Encoded payload of the request message. +/// - `conn_sn`: The sequence number of connection associated with the message, used to derive +/// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if the request is successfully processed, or an error if @@ -92,6 +97,7 @@ pub fn handle_request( ctx: Context, from_nid: String, payload: &[u8], + conn_sn: u128, ) -> Result<()> { let mut req: CSMessageRequest = payload.try_into()?; @@ -116,6 +122,11 @@ pub fn handle_request( pending_request.sources.push(source.owner.to_owned()) } if pending_request.sources.len() != req.protocols().len() { + // close the proxy request as it's no longer needed + ctx.accounts + .proxy_request + .close(ctx.accounts.signer.to_account_info())?; + return Ok(()); } pending_request.close(ctx.accounts.admin.clone())?; @@ -128,7 +139,9 @@ pub fn handle_request( to: req.to().clone(), sn: req.sequence_no(), reqId: req_id, - data: req.data() + data: req.data(), + connection: source.owner.to_owned(), + connSn: conn_sn }); let proxy_request = &mut ctx.accounts.proxy_request; @@ -151,11 +164,13 @@ pub fn handle_request( /// # Arguments /// - `ctx`: The context of accounts involved in the operation. /// - `payload`: The raw result data from the cross-chain operation, which is decoded and processed. +/// - `conn_sn`: The sequence number of connection associated with the message, used to derive +/// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if the operation completes successfully, or an error if something /// goes wrong. -pub fn handle_result(ctx: Context, payload: &[u8]) -> Result<()> { +pub fn handle_result(ctx: Context, payload: &[u8], conn_sn: u128) -> Result<()> { let result: CSMessageResult = payload.try_into()?; let proxy_request = &ctx.accounts.proxy_request; let rollback_account = &mut ctx.accounts.rollback_account; @@ -180,7 +195,7 @@ pub fn handle_result(ctx: Context, payload: &[u8]) -> Result<() success_res.success = true; if let Some(message) = &mut result.message() { - handle_reply(ctx, message)?; + handle_reply(ctx, message, conn_sn)?; } else { if proxy_request.is_some() { return Err(XcallError::ProxyRequestAccountMustNotBeSpecified.into()); @@ -250,11 +265,17 @@ pub fn handle_error(ctx: Context, sequence_no: u128) -> Result<( /// # Arguments /// * `ctx` - The context containing relevant accounts for handling the reply. /// * `reply` - The mutable reference to the incoming reply message to be processed. +/// * `conn_sn`: The sequence number of connection associated with the message, used to derive +/// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if the operation completes successfully, or an error if something /// goes wrong. -pub fn handle_reply(ctx: Context, reply: &mut CSMessageRequest) -> Result<()> { +pub fn handle_reply( + ctx: Context, + reply: &mut CSMessageRequest, + conn_sn: u128, +) -> Result<()> { let rollback = &ctx.accounts.rollback_account.rollback; if rollback.to().nid() != reply.from().nid() { return Err(XcallError::InvalidReplyReceived.into()); @@ -267,7 +288,9 @@ pub fn handle_reply(ctx: Context, reply: &mut CSMessageRequest) to: reply.to().clone(), sn: reply.sequence_no(), reqId: req_id, - data: reply.data() + data: reply.data(), + connection: ctx.accounts.connection.owner.to_owned(), + connSn: conn_sn }); let proxy_request = ctx @@ -295,6 +318,8 @@ pub fn handle_reply(ctx: Context, reply: &mut CSMessageRequest) /// - `ctx`: The context containing the accounts and program-specific info needed for the instruction. /// - `from_nid`: The network ID of the chain that sent the request. /// - `msg_payload`: The payload of the request message received from the source chain. +/// - `conn_sn`: The sequence number of connection associated with the message, used to derive +/// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Indicates whether the invocation was successful or encountered an error. @@ -302,11 +327,13 @@ pub fn invoke_handle_request<'info>( ctx: Context<'_, '_, '_, 'info, HandleMessageCtx<'info>>, from_nid: String, msg_payload: Vec, + conn_sn: u128, ) -> Result<()> { let mut data = vec![]; let args = xcall_lib::xcall_type::HandleRequestArgs { from_nid, msg_payload, + conn_sn, }; args.serialize(&mut data)?; let ix_data = helper::get_instruction_data(xcall_lib::xcall_type::HANDLE_REQUEST_IX, data); @@ -362,6 +389,8 @@ pub fn invoke_handle_request<'info>( /// - `from_nid`: The network ID of the chain that sent the response. /// - `msg_payload`: The payload of the message received from the destination chain. /// - `sequence_no`: The sequence number associated with the original request message. +/// - `conn_sn`: The sequence number of connection associated with the message, used to derive +/// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Indicates whether the invocation was successful or encountered an error. @@ -370,12 +399,14 @@ pub fn invoke_handle_result<'info>( from_nid: String, msg_payload: Vec, sequence_no: u128, + conn_sn: u128, ) -> Result<()> { let mut data = vec![]; let args = xcall_lib::xcall_type::HandleResultArgs { from_nid, msg_payload, sequence_no, + conn_sn, }; args.serialize(&mut data)?; let ix_data = helper::get_instruction_data(xcall_lib::xcall_type::HANDLE_RESULT_IX, data); @@ -547,7 +578,7 @@ pub struct HandleMessageCtx<'info> { } #[derive(Accounts)] -#[instruction(from_nid: String, msg_payload: Vec)] +#[instruction(from_nid: String, msg_payload: Vec, conn_sn: u128)] pub struct HandleRequestCtx<'info> { /// The account that signs and pays for the transaction. This account is mutable /// because it will be debited for any fees or rent required during the transaction. @@ -590,7 +621,7 @@ pub struct HandleRequestCtx<'info> { init_if_needed, payer = signer, space = ProxyRequest::SIZE, - seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &(config.last_req_id + 1).to_be_bytes()], + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), from_nid.as_bytes(), &conn_sn.to_be_bytes(), &connection.owner.to_bytes()], bump )] pub proxy_request: Account<'info, ProxyRequest>, @@ -609,7 +640,7 @@ pub struct HandleRequestCtx<'info> { } #[derive(Accounts)] -#[instruction(from_nid: String, msg_payload: Vec, sequence_no: u128)] +#[instruction(from_nid: String, msg_payload: Vec, sequence_no: u128, conn_sn: u128)] pub struct HandleResultCtx<'info> { /// The account that signs and pays for the transaction. This account is mutable /// because it will be debited for any fees or rent required during the transaction. @@ -664,7 +695,7 @@ pub struct HandleResultCtx<'info> { init_if_needed, payer = signer, space = ProxyRequest::SIZE, - seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &(config.last_req_id + 1).to_be_bytes()], + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), from_nid.as_bytes(), &conn_sn.to_be_bytes(), &connection.owner.to_bytes()], bump )] pub proxy_request: Option>, diff --git a/contracts/solana/programs/xcall/src/instructions/query_accounts.rs b/contracts/solana/programs/xcall/src/instructions/query_accounts.rs index 23038e50c..296e64837 100644 --- a/contracts/solana/programs/xcall/src/instructions/query_accounts.rs +++ b/contracts/solana/programs/xcall/src/instructions/query_accounts.rs @@ -28,15 +28,20 @@ use crate::{ pub fn query_handle_message_accounts( ctx: Context, + from_nid: String, msg: Vec, + conn_sn: u128, ) -> Result { + let connection = &ctx.accounts.connection; let config = &ctx.accounts.config; let admin = config.admin; let (proxy_request, _) = Pubkey::find_program_address( &[ ProxyRequest::SEED_PREFIX.as_bytes(), - &(config.last_req_id + 1).to_be_bytes(), + from_nid.as_bytes(), + &conn_sn.to_be_bytes(), + &connection.owner.to_bytes(), ], &id(), ); @@ -149,7 +154,9 @@ pub fn query_handle_message_accounts( pub fn query_execute_call_accounts( ctx: Context, - req_id: u128, + from_nid: String, + conn_sn: u128, + connection: Pubkey, data: Vec, page: u8, limit: u8, @@ -158,7 +165,12 @@ pub fn query_execute_call_accounts( let req = &ctx.accounts.proxy_request.req; let (proxy_request, _) = Pubkey::find_program_address( - &[ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + &[ + ProxyRequest::SEED_PREFIX.as_bytes(), + from_nid.as_bytes(), + &conn_sn.to_be_bytes(), + &connection.to_bytes(), + ], &id(), ); @@ -342,7 +354,7 @@ pub fn query_connection_send_message_accoounts<'info>( } #[derive(Accounts)] -#[instruction(req_id: u128, data: Vec)] +#[instruction(req_id: u128, from_nid: String, conn_sn: u128, connection: Pubkey, data: Vec)] pub struct QueryExecuteCallAccountsCtx<'info> { #[account( seeds = [Config::SEED_PREFIX.as_bytes()], @@ -351,7 +363,7 @@ pub struct QueryExecuteCallAccountsCtx<'info> { pub config: Account<'info, Config>, #[account( - seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), from_nid.as_bytes(), &conn_sn.to_be_bytes(), &connection.to_bytes()], bump = proxy_request.bump )] pub proxy_request: Account<'info, ProxyRequest>, @@ -360,6 +372,8 @@ pub struct QueryExecuteCallAccountsCtx<'info> { #[derive(Accounts)] #[instruction(from_nid: String, msg: Vec, sequence_no: u128)] pub struct QueryHandleMessageAccountsCtx<'info> { + pub connection: Signer<'info>, + #[account( seeds = [Config::SEED_PREFIX.as_bytes()], bump = config.bump, diff --git a/contracts/solana/programs/xcall/src/lib.rs b/contracts/solana/programs/xcall/src/lib.rs index 4506dc4e5..c4179343d 100644 --- a/contracts/solana/programs/xcall/src/lib.rs +++ b/contracts/solana/programs/xcall/src/lib.rs @@ -137,6 +137,8 @@ pub mod xcall { /// - `msg`: The encoded message payload received from the chain. /// - `sequence_no`: The sequence number associated with the message, used to track message /// ordering and responses. + /// - `conn_sn`: The sequence number of connection associated with the message, used to derive + /// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if the message is successfully handled, or an error if any @@ -146,8 +148,9 @@ pub mod xcall { from_nid: String, msg: Vec, sequence_no: u128, + conn_sn: u128, ) -> Result<()> { - instructions::handle_message(ctx, from_nid, msg, sequence_no) + instructions::handle_message(ctx, from_nid, msg, sequence_no, conn_sn) } /// Instruction: Handle Request @@ -162,16 +165,20 @@ pub mod xcall { /// - `ctx`: Context containing all relevant accounts and program-specific information. /// - `from_nid`: Network ID of the chain that sent the request. /// - `msg_payload`: Encoded payload of the request message. + /// - `conn_sn`: The sequence number of connection associated with the message, used to derive + /// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if the request is processed successfully, or an error if /// validation or processing fails. + #[allow(unused_variables)] pub fn handle_request<'info>( ctx: Context<'_, '_, '_, 'info, HandleRequestCtx<'info>>, from_nid: String, msg_payload: Vec, + conn_sn: u128, ) -> Result<()> { - instructions::handle_request(ctx, from_nid, &msg_payload) + instructions::handle_request(ctx, from_nid, &msg_payload, conn_sn) } /// Instruction: Handle Result @@ -187,6 +194,8 @@ pub mod xcall { /// - `from_nid`: Network ID of the chain that sent the result. /// - `msg_payload`: Encoded payload of the result message. /// - `sequence_no`: Unique sequence number of the result message. + /// - `conn_sn`: The sequence number of connection associated with the message, used to derive + /// unique proxy request account with the combination of other parameters /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if the result is processed successfully, or an error if @@ -197,8 +206,9 @@ pub mod xcall { from_nid: String, msg_payload: Vec, sequence_no: u128, + conn_sn: u128, ) -> Result<()> { - instructions::handle_result(ctx, &msg_payload) + instructions::handle_result(ctx, &msg_payload, conn_sn) } /// Instruction: Handle Error @@ -357,13 +367,22 @@ pub mod xcall { /// # Parameters /// - `ctx`: The context of the solana program instruction /// - `req_id`: The unique identifier for the request being processed. + /// - `from_nid`: Network ID of the chain that sent the request. + /// - `conn_sn`: The sequence number of connection associated with the message, used to derive + /// unique proxy request account with the combination of other parameters + /// - `connection`: The connection key used to derive proxy request account with the combination + /// of other parameters /// - `data`: The data associated with the call request, which will be verified and processed. /// /// # Returns /// - `Result<()>`: Returns `Ok(())` if the call was executed successfully, or an error if it failed. + #[allow(unused_variables)] pub fn execute_call<'info>( ctx: Context<'_, '_, '_, 'info, ExecuteCallCtx<'info>>, req_id: u128, + from_nid: String, + conn_sn: u128, + connection: Pubkey, data: Vec, ) -> Result<()> { instructions::execute_call(ctx, req_id, data) @@ -402,6 +421,11 @@ pub mod xcall { /// # Arguments /// * `ctx` - Context containing the accounts required for processing the forced rollback. /// * `req_id` - The unique request ID associated with the message being rolled back. + /// - `from_nid`: Network ID of the chain that sent the request. + /// - `conn_sn`: The sequence number of connection associated with the message, used to derive + /// unique proxy request account with the combination of other parameters + /// - `connection`: The connection key used to derive proxy request account with the combination + /// of other parameters /// /// # Returns /// * `Result<()>` - Returns `Ok(())` on successful execution, or an error if the rollback process @@ -410,18 +434,27 @@ pub mod xcall { pub fn handle_forced_rollback<'info>( ctx: Context<'_, '_, '_, 'info, HandleForcedRollbackCtx<'info>>, req_id: u128, + from_nid: String, + conn_sn: u128, + connection: Pubkey, ) -> Result<()> { instructions::handle_forced_rollback(ctx) } + #[allow(unused_variables)] pub fn query_execute_call_accounts<'info>( ctx: Context<'_, '_, '_, 'info, QueryExecuteCallAccountsCtx<'info>>, req_id: u128, + from_nid: String, + conn_sn: u128, + connection: Pubkey, data: Vec, page: u8, limit: u8, ) -> Result { - instructions::query_execute_call_accounts(ctx, req_id, data, page, limit) + instructions::query_execute_call_accounts( + ctx, from_nid, conn_sn, connection, data, page, limit, + ) } #[allow(unused_variables)] @@ -440,8 +473,9 @@ pub mod xcall { from_nid: String, msg: Vec, sequence_no: u128, + conn_sn: u128, ) -> Result { - instructions::query_handle_message_accounts(ctx, msg) + instructions::query_handle_message_accounts(ctx, from_nid, msg, conn_sn) } pub fn query_handle_error_accounts( diff --git a/contracts/solana/tests/centralized-connection/centralized-connection.ts b/contracts/solana/tests/centralized-connection/centralized-connection.ts index 7d3d51f2d..8b873a64f 100644 --- a/contracts/solana/tests/centralized-connection/centralized-connection.ts +++ b/contracts/solana/tests/centralized-connection/centralized-connection.ts @@ -182,6 +182,7 @@ describe("CentralizedConnection", () => { ).encode(); let recvMessageAccounts = await ctx.getRecvMessageAccounts( + fromNetwork, connSn, nextSequenceNo, cs_message, @@ -212,7 +213,11 @@ describe("CentralizedConnection", () => { expect(await ctx.getReceipt(fromNetwork, nextSequenceNo)).to.be.empty; // expect proxy request in xcall PDA's account - let proxyRequest = await xcallCtx.getProxyRequest(nextReqId); + let proxyRequest = await xcallCtx.getProxyRequest( + fromNetwork, + connSn, + connectionProgram.programId + ); expect(proxyRequest.req.protocols).to.includes( connectionProgram.programId.toString() ); @@ -229,17 +234,30 @@ describe("CentralizedConnection", () => { // call xcall execute_call let executeCallAccounts = await xcallCtx.getExecuteCallAccounts( nextReqId, + fromNetwork, + connSn, + connectionProgram.programId, data ); await xcallProgram.methods - .executeCall(new anchor.BN(nextReqId), Buffer.from(data)) + .executeCall( + new anchor.BN(nextReqId), + fromNetwork, + new anchor.BN(connSn), + connectionProgram.programId, + Buffer.from(data) + ) .accounts({ signer: ctx.admin.publicKey, systemProgram: SYSTEM_PROGRAM_ID, config: XcallPDA.config().pda, admin: xcallConfig.admin, - proxyRequest: XcallPDA.proxyRequest(nextReqId).pda, + proxyRequest: XcallPDA.proxyRequest( + fromNetwork, + connSn, + connectionProgram.programId + ).pda, }) .remainingAccounts([...executeCallAccounts.slice(4)]) .signers([ctx.admin]) @@ -348,6 +366,7 @@ describe("CentralizedConnection", () => { ).encode(); let recvMessageAccounts = await ctx.getRecvMessageAccounts( + ctx.dstNetworkId, connSn, nextSequenceNo, csMessage, @@ -467,6 +486,7 @@ describe("CentralizedConnection", () => { ).encode(); let recvMessageAccounts = await ctx.getRecvMessageAccounts( + ctx.dstNetworkId, connSn, nextSequenceNo, csMessage, @@ -664,6 +684,7 @@ describe("CentralizedConnection", () => { ).encode(); let recvMessageAccounts = await ctx.getRecvMessageAccounts( + fromNetwork, connSn, nextSequenceNo, cs_message, @@ -691,7 +712,12 @@ describe("CentralizedConnection", () => { await sleep(2); let executeForcedRollbackIx = await mockDappProgram.methods - .executeForcedRollback(new anchor.BN(nextReqId)) + .executeForcedRollback( + new anchor.BN(nextReqId), + fromNetwork, + new anchor.BN(connSn), + connectionProgram.programId + ) .accountsStrict({ config: DappPDA.config().pda, systemProgram: SYSTEM_PROGRAM_ID, @@ -710,7 +736,11 @@ describe("CentralizedConnection", () => { isWritable: true, }, { - pubkey: XcallPDA.proxyRequest(nextReqId).pda, + pubkey: XcallPDA.proxyRequest( + fromNetwork, + connSn, + connectionProgram.programId + ).pda, isSigner: false, isWritable: true, }, diff --git a/contracts/solana/tests/centralized-connection/setup.ts b/contracts/solana/tests/centralized-connection/setup.ts index de6eacb2e..02e7dee63 100644 --- a/contracts/solana/tests/centralized-connection/setup.ts +++ b/contracts/solana/tests/centralized-connection/setup.ts @@ -77,6 +77,7 @@ export class TestContext { } async getRecvMessageAccounts( + fromNetwork: string, connSn: number, sequenceNo: number, csMessage: Uint8Array, @@ -106,7 +107,7 @@ export class TestContext { let res = await connectionProgram.methods .queryRecvMessageAccounts( - this.dstNetworkId, + fromNetwork, new anchor.BN(connSn), Buffer.from(csMessage), new anchor.BN(sequenceNo), diff --git a/contracts/solana/tests/xcall/setup.ts b/contracts/solana/tests/xcall/setup.ts index 7cf19feef..f826cd430 100644 --- a/contracts/solana/tests/xcall/setup.ts +++ b/contracts/solana/tests/xcall/setup.ts @@ -82,12 +82,27 @@ export class TestContext { await sleep(2); } - async getExecuteCallAccounts(reqId: number, data: Uint8Array) { + async getExecuteCallAccounts( + reqId: number, + fromNetwork: string, + connSn: number, + connection: PublicKey, + data: Uint8Array + ) { const res = await xcallProgram.methods - .queryExecuteCallAccounts(new anchor.BN(reqId), Buffer.from(data), 1, 30) + .queryExecuteCallAccounts( + new anchor.BN(reqId), + fromNetwork, + new anchor.BN(connSn), + connection, + Buffer.from(data), + 1, + 30 + ) .accountsStrict({ config: XcallPDA.config().pda, - proxyRequest: XcallPDA.proxyRequest(reqId).pda, + proxyRequest: XcallPDA.proxyRequest(fromNetwork, connSn, connection) + .pda, }) .remainingAccounts([ { @@ -145,9 +160,13 @@ export class TestContext { return await xcallProgram.account.config.fetch(pda); } - async getProxyRequest(requestId: number) { + async getProxyRequest( + fromNetwork: string, + connSn: number, + connection: PublicKey + ) { return await xcallProgram.account.proxyRequest.fetch( - XcallPDA.proxyRequest(requestId).pda, + XcallPDA.proxyRequest(fromNetwork, connSn, connection).pda, "confirmed" ); } @@ -192,9 +211,18 @@ export class XcallPDA { return { bump, pda }; } - static proxyRequest(requestId: number) { + static proxyRequest( + fromNetwork: string, + connSn: number, + connection: PublicKey + ) { const [pda, bump] = PublicKey.findProgramAddressSync( - [Buffer.from("proxy"), uint128ToArray(requestId)], + [ + Buffer.from("proxy"), + Buffer.from(fromNetwork), + uint128ToArray(connSn), + connection.toBuffer(), + ], xcallProgram.programId ); diff --git a/contracts/soroban/Cargo.lock b/contracts/soroban/Cargo.lock index e571a9ffa..fbf3411ee 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 cddad9162..7cb79f87f 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 c4f294fa7..e34ece525 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 507f54515..03444560f 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 4a410aa07..a4a2fa243 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 a44bd2175..7556c384a 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 e3f08bdbb..6b8c2fab9 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 000000000..e6f6b4820 --- /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 000000000..b02d54eec --- /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 000000000..b4d505179 --- /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 000000000..6db6d215e --- /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 000000000..f37ed14fb --- /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 000000000..c49d6c9c7 --- /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 000000000..61f8fdfe8 --- /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 000000000..1fd060736 --- /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 000000000..bdc017a16 --- /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 000000000..fe375ba57 --- /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 000000000..bf5f551d7 --- /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 dc3c85732..cd47ec6a0 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 2a866ea25..e7f2533f1 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 df83e0625..6be6e7759 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 09977786f..037f18762 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 ccaa3877d..17e173ec3 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 88ab759e1..8b23c3769 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 47c2eec9f..6c5affce0 100644 --- a/contracts/soroban/contracts/xcall/src/contract.rs +++ b/contracts/soroban/contracts/xcall/src/contract.rs @@ -153,9 +153,16 @@ 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 version(env: Env) -> u32 { + storage::get_contract_version(&env) + } + pub fn extend_instance_storage(env: Env) -> Result<(), ContractError> { storage::extend_instance(&env); Ok(()) diff --git a/contracts/soroban/contracts/xcall/src/errors.rs b/contracts/soroban/contracts/xcall/src/errors.rs index e98968170..ad05c5c3a 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 3506b45e0..e76bcfed0 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()) { @@ -160,6 +160,7 @@ pub fn handle_reply( } pub fn handle_error(env: &Env, sender: Address, sequence_no: u128) -> Result<(), ContractError> { + sender.require_auth(); let cs_message_result = CSMessageResult::new( sequence_no, CSResponseType::CSResponseFailure, @@ -178,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 5c6ff75d6..6bd74db42 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 5deef0c50..1fb56adda 100644 --- a/contracts/soroban/contracts/xcall/src/storage.rs +++ b/contracts/soroban/contracts/xcall/src/storage.rs @@ -18,8 +18,8 @@ const LEDGER_BUMP_INSTANCE: u32 = LEDGER_THRESHOLD_INSTANCE + DAY_IN_LEDGERS; // const LEDGER_THRESHOLD_PERSISTENT: u32 = DAY_IN_LEDGERS * 30; // ~ 30 days const LEDGER_BUMP_PERSISTENT: u32 = LEDGER_THRESHOLD_PERSISTENT + DAY_IN_LEDGERS; // ~ 31 days -const LEDGER_THRESHOLD_REQUEST: u32 = DAY_IN_LEDGERS * 7; // ~ 7 days -const LEDGER_BUMP_REQUEST: u32 = LEDGER_THRESHOLD_REQUEST + DAY_IN_LEDGERS; // ~ 8 days +const LEDGER_THRESHOLD_REQUEST: u32 = DAY_IN_LEDGERS * 3; // ~ 3 days +const LEDGER_BUMP_REQUEST: u32 = LEDGER_THRESHOLD_REQUEST + DAY_IN_LEDGERS; // ~ 4 days pub const MAX_ROLLBACK_SIZE: u64 = 1024; pub const MAX_DATA_SIZE: u64 = 2048; @@ -72,10 +72,9 @@ pub fn default_connection(e: &Env, nid: String) -> Result Result bool { let key = StorageKey::SuccessfulResponses(sn); let res = e.storage().persistent().get(&key).unwrap_or(false); - if res { - extend_persistent(e, &key) - } - res } @@ -114,31 +107,21 @@ pub fn get_proxy_request(e: &Env, req_id: u128) -> Result) -> Vec { let key = StorageKey::PendingRequests(hash); - let pending_request = e.storage().persistent().get(&key).unwrap_or(Vec::new(&e)); - if pending_request.len() > 0 { - extend_persistent_request(e, &key); - } - + let pending_request = e.storage().temporary().get(&key).unwrap_or(Vec::new(&e)); pending_request } pub fn get_pending_response(e: &Env, hash: BytesN<32>) -> Vec { let key = StorageKey::PendingResponses(hash); - let pending_response = e.storage().persistent().get(&key).unwrap_or(Vec::new(&e)); - if pending_response.len() > 0 { - extend_persistent_request(e, &key); - } - + let pending_response = e.storage().temporary().get(&key).unwrap_or(Vec::new(&e)); pending_response } @@ -153,6 +136,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); @@ -183,53 +179,52 @@ pub fn store_protocol_fee(e: &Env, fee: u128) { pub fn store_default_connection(e: &Env, nid: String, address: &Address) { let key = StorageKey::DefaultConnections(nid); - e.storage().persistent().set(&key, &address); - extend_persistent(e, &key); + e.storage().instance().set(&key, &address); } pub fn store_rollback(e: &Env, sn: u128, rollback: &Rollback) { let key = StorageKey::Rollback(sn); - e.storage().persistent().set(&key, rollback); - extend_persistent_request(e, &key) + e.storage().temporary().set(&key, rollback); + extend_temporary_request(e, &key) } pub fn remove_rollback(e: &Env, sn: u128) { - e.storage().persistent().remove(&StorageKey::Rollback(sn)); + e.storage().temporary().remove(&StorageKey::Rollback(sn)); } pub fn store_proxy_request(e: &Env, req_id: u128, request: &CSMessageRequest) { let key = StorageKey::ProxyRequest(req_id); - e.storage().persistent().set(&key, request); - extend_persistent_request(e, &key) + e.storage().temporary().set(&key, request); + extend_temporary_request(e, &key) } pub fn remove_proxy_request(e: &Env, req_id: u128) { e.storage() - .persistent() + .temporary() .remove(&StorageKey::ProxyRequest(req_id)) } pub fn store_pending_request(e: &Env, hash: BytesN<32>, sources: &Vec) { let key = StorageKey::PendingRequests(hash.clone()); - e.storage().persistent().set(&key, sources); - extend_persistent_request(e, &key) + e.storage().temporary().set(&key, sources); + extend_temporary_request(e, &key) } pub fn remove_pending_request(e: &Env, hash: BytesN<32>) { e.storage() - .persistent() + .temporary() .remove(&StorageKey::PendingRequests(hash)) } pub fn store_pending_response(e: &Env, hash: BytesN<32>, sources: &Vec) { let key = StorageKey::PendingResponses(hash); - e.storage().persistent().set(&key, sources); - extend_persistent_request(e, &key) + e.storage().temporary().set(&key, sources); + extend_temporary_request(e, &key) } pub fn remove_pending_response(e: &Env, hash: BytesN<32>) { e.storage() - .persistent() + .temporary() .remove(&StorageKey::PendingResponses(hash)) } @@ -262,8 +257,9 @@ pub fn extend_persistent(e: &Env, key: &StorageKey) { .extend_ttl(key, LEDGER_THRESHOLD_PERSISTENT, LEDGER_BUMP_PERSISTENT); } -pub fn extend_persistent_request(e: &Env, key: &StorageKey) { +pub fn extend_temporary_request(e: &Env, key: &StorageKey) { e.storage() - .persistent() + .temporary() .extend_ttl(key, LEDGER_THRESHOLD_REQUEST, LEDGER_BUMP_REQUEST); } + diff --git a/contracts/soroban/contracts/xcall/src/test/contract.rs b/contracts/soroban/contracts/xcall/src/test/contract.rs index e7a4d06f6..5ad3267aa 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 108ef3608..69ca24c73 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 286213a1c..5788e7d98 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 c46af3757..f605046de 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 e45914307..8aa7d49ed 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 cb91857eb..4decd4a0a 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 f24d82e71..fec971686 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/decoder.rs b/contracts/soroban/libs/soroban-rlp/src/decoder.rs index 37b83405b..9d05760db 100644 --- a/contracts/soroban/libs/soroban-rlp/src/decoder.rs +++ b/contracts/soroban/libs/soroban-rlp/src/decoder.rs @@ -131,6 +131,9 @@ pub fn decode_u64(env: &Env, bytes: Bytes) -> u64 { } pub fn decode_u128(env: &Env, bytes: Bytes) -> u128 { + if bytes.len() == 1 { + return bytes_to_u128(bytes); + } let decoded = decode(&env, bytes); bytes_to_u128(decoded) } diff --git a/contracts/sui/libs/sui_rlp/sources/decoder.move b/contracts/sui/libs/sui_rlp/sources/decoder.move index 3fa18a968..acd27d514 100644 --- a/contracts/sui/libs/sui_rlp/sources/decoder.move +++ b/contracts/sui/libs/sui_rlp/sources/decoder.move @@ -20,7 +20,6 @@ module sui_rlp::decoder { } else { let length_len = byte - 0xb7; let length_bytes = utils::slice_vector(encoded, 1, length_len as u64); - //debug::print(&length_bytes); let length = utils::from_bytes_u64(&length_bytes); let data_start = (length_len + 1) as u64; utils::slice_vector(encoded, data_start, length) @@ -151,5 +150,11 @@ module sui_rlp::decoder { bcs::peel_address(&mut bcs) } + public fun decode_bool(vec:&vector):bool{ + let val= *vector::borrow(vec,0); + val==1 + + } + } \ No newline at end of file diff --git a/contracts/sui/libs/sui_rlp/sources/encoder.move b/contracts/sui/libs/sui_rlp/sources/encoder.move index 88c43c3d1..1e63672bf 100644 --- a/contracts/sui/libs/sui_rlp/sources/encoder.move +++ b/contracts/sui/libs/sui_rlp/sources/encoder.move @@ -21,14 +21,11 @@ module sui_rlp::encoder { vector::append(&mut result,*bytes); result }; - //std::debug::print(&encoded); encoded } public fun encode_list(list:&vector>,raw:bool):vector{ - //std::debug::print(&b"ENCODELIST".to_string()); - //std::debug::print(list); let mut result=vector::empty(); let mut encoded_list = vector::empty(); let mut list=*list; @@ -40,7 +37,6 @@ module sui_rlp::encoder { vector::append(&mut result,encode(&vector::pop_back(&mut list))); }else{ vector::append(&mut result,vector::pop_back(&mut list)); - //std::debug::print(&result); }; }; @@ -48,32 +44,22 @@ module sui_rlp::encoder { let total_length = result.length(); let len=vector::length(&result); - if( total_length<= 55){ - encoded_list=encode_length(len,0xc0); - vector::append(&mut encoded_list,result); + if( total_length<= 55){ + encoded_list=encode_length(len,0xc0); + vector::append(&mut encoded_list,result); - } else { - let length_bytes = utils::to_bytes_u64(len); - let prefix = (0xf7 + vector::length(&length_bytes)) as u8; - //std::debug::print(&b"PREFIX".to_string()); - //std::debug::print(&prefix); - vector::push_back(&mut encoded_list, prefix); - //std::debug::print(&encoded_list); - vector::append(&mut encoded_list, length_bytes); - //std::debug::print(&encoded_list); - - vector::append(&mut encoded_list, result); - //std::debug::print(&encoded_list); - - - } + } else { + let length_bytes = utils::to_bytes_u64_sign(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); + vector::append(&mut encoded_list, result); + } }else{ vector::push_back(&mut encoded_list,0xc0); }; - //std::debug::print(&b"FINAL_ENCODED_LIST".to_string()); - //std::debug::print(&encoded_list); encoded_list } @@ -83,11 +69,11 @@ 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_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); - vector::append(&mut length_info,length_bytes); + let length_bytes=utils::to_bytes_u64_sign(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); + vector::append(&mut length_info,length_bytes); }; length_info } @@ -97,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_sign(num,true); + encode(&vec) + } public fun encode_u64(num:u64):vector{ - let vec= utils::to_bytes_u64(num); + let vec= utils::to_bytes_u64_sign(num,true); encode(&vec) } public fun encode_u128(num:u128):vector{ - let vec= utils::to_bytes_u128(num); + let vec= utils::to_bytes_u128_sign(num,true); encode(&vec) } @@ -132,8 +124,32 @@ module sui_rlp::encoder { let vec= bcs::to_bytes(addr); encode(&vec) } -} + + public fun encode_bool(val:bool):vector{ + if(val==true){ + return vector[1] + }; + 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 f73f0472e..53529c3d2 100644 --- a/contracts/sui/libs/sui_rlp/sources/utils.move +++ b/contracts/sui/libs/sui_rlp/sources/utils.move @@ -1,50 +1,96 @@ module sui_rlp::utils { - use std::vector::{Self}; - use std::string::{Self,String}; - public fun to_bytes_u32(number: u32): vector { - let mut bytes: vector = vector::empty(); - let mut i:u8=0; - while(i < 4){ - let val =( (number>>(i * 8) & 0xFF) as u8) ; - vector::push_back(&mut bytes,val); - i=i+1; - }; + use std::bcs; + + + // Convert bytes to u32 + public fun from_bytes_u32(bytes: &vector): u32 { + let mut bytes= truncate_zeros(bytes); bytes.reverse(); - bytes + let mut diff= 4-bytes.length(); + while (diff > 0) { + bytes.push_back(0_u8); + diff=diff-1; + }; + sui::bcs::peel_u32(&mut sui::bcs::new(bytes)) } - // Convert bytes to u32 - public fun from_bytes_u32(bytes: &vector): u32 {let mut result = 0; - let mut multiplier = 1; - let length = vector::length(bytes); - - let mut i = length; - while (i > 0) { - i = i - 1; - //std::debug::print(vector::borrow(bytes, i)); - result = result + ((*vector::borrow(bytes, i) as u32) * multiplier); - //std::debug::print(&result); - - if (i > 0) { - multiplier = multiplier * 256 - }; - + + // Convert bytes to u64 + public fun from_bytes_u64(bytes: &vector): u64 { + let mut bytes= truncate_zeros(bytes); + bytes.reverse(); + let mut diff= 8-bytes.length(); + while (diff > 0) { + bytes.push_back(0_u8); + diff=diff-1; }; - result + sui::bcs::peel_u64(&mut sui::bcs::new(bytes)) + } - public fun to_bytes_u64(number: u64): vector { - let mut bytes: vector = vector::empty(); - let mut i:u8=0; - while(i < 8){ - let val =( (number>>(i * 8) & 0xFF) as u8) ; - vector::push_back(&mut bytes,val); - i=i+1; + // Convert bytes to u128 + public fun from_bytes_u128(bytes: &vector): u128 { + let mut bytes= truncate_zeros(bytes); + bytes.reverse(); + let mut diff= 16-bytes.length(); + while (diff > 0) { + bytes.push_back(0_u8); + diff=diff-1; }; + sui::bcs::peel_u128(&mut sui::bcs::new(bytes)) + + } + + //Deprecated + public fun to_bytes_u128(number:u128):vector{ + let bytes=bcs::to_bytes(&number); + to_signed_bytes(bytes,true) + } + + public fun to_bytes_u128_sign(number:u128,signed:bool):vector{ + let bytes=bcs::to_bytes(&number); + to_signed_bytes(bytes,signed) + } + + //Deprecated + public fun to_bytes_u64(number:u64):vector{ + let bytes=bcs::to_bytes(&number); + to_signed_bytes(bytes,true) + } + + public fun to_bytes_u64_sign(number:u64,signed:bool):vector{ + let bytes=bcs::to_bytes(&number); + to_signed_bytes(bytes,signed) + } + + //Deprecated + public fun to_bytes_u32(number: u32): vector { + let bytes=bcs::to_bytes(&number); + to_signed_bytes(bytes,true) + } + + public fun to_bytes_u32_sign(number: u32,signed:bool): vector { + let bytes=bcs::to_bytes(&number); + to_signed_bytes(bytes,signed) + } + + fun to_signed_bytes(mut bytes:vector,signed:bool):vector{ bytes.reverse(); - let mut prefix = vector[0]; - prefix.append(truncate_zeros(&bytes)); - prefix + let truncated=truncate_zeros(&bytes); + if(signed==false){ + return truncated + }; + let first_byte=*truncated.borrow(0); + + if (first_byte >= 128) { + let mut prefix = vector[0]; + prefix.append(truncated); + prefix + + }else { + truncated + } + } fun truncate_zeros(bytes: &vector): vector { @@ -60,72 +106,17 @@ module sui_rlp::utils { i = i + 1; }; - - result - } - - // Convert bytes to u64 - public fun from_bytes_u64(bytes: &vector): u64 { - let bytes = truncate_zeros(bytes); - let mut result = 0; - let mut multiplier = 1; - let length = vector::length(&bytes); - - let mut i = length; - while (i > 0) { - i = i - 1; - //std::debug::print(vector::borrow(bytes, i)); - result = result + ((*vector::borrow(&bytes, i) as u64) * (multiplier)); - //std::debug::print(&result); - if (i > 0) { - multiplier = multiplier * 256 - }; - - }; - result + if (result.length()==0){ + vector[0] + }else{ + result + } } - - - // Convert u128 to bytes - public fun to_bytes_u128(number: u128): vector { - let mut bytes: vector = vector::empty(); - let mut i:u8=0; - while(i < 16){ - let val = ((number>>(i * 8) & 0xFF) as u8) ; - vector::push_back(&mut bytes,val); - i=i+1; - }; - bytes.reverse(); - let mut prefix = vector[0]; - prefix.append(truncate_zeros(&bytes)); - prefix - } - // Convert bytes to u128 - public fun from_bytes_u128(bytes: &vector): u128 { - let bytes = truncate_zeros(bytes); - let mut result = 0; - let mut multiplier = 1; - let length = vector::length(&bytes); - - let mut i = length; - while (i > 0) { - i = i - 1; - //std::debug::print(vector::borrow(bytes, i)); - result = result + ((*vector::borrow(&bytes, i) as u128) * multiplier); - //std::debug::print(&result); - - if (i > 0) { - multiplier = multiplier * 256 - }; - - }; - result - } /* end is exclusive in slice*/ - public fun slice_vector(vec: &vector, start: u64, length: u64): vector { + public fun slice_vector(vec: &vector, start: u64, length: u64): vector { let mut result = vector::empty(); let mut i = 0; while (i < length) { @@ -133,49 +124,45 @@ module sui_rlp::utils { vector::push_back(&mut result, value); i = i + 1; }; - //std::debug::print(&result); result } - + } module sui_rlp::utils_test { use sui_rlp::utils::{Self}; - use std::vector::{Self}; - use std::debug; - use sui::bcs::{Self}; - #[test] + #[test] fun test_u32_conversion() { let num= (122 as u32); - let bytes= utils::to_bytes_u32(num); + let bytes= utils::to_bytes_u32_sign(num,true); let converted=utils::from_bytes_u32(&bytes); assert!(num==converted,0x01); - + } #[test] fun test_u64_conversion() { let num= (55000 as u64); - let bytes= utils::to_bytes_u64(num); + let bytes= utils::to_bytes_u64_sign(num,true); let converted=utils::from_bytes_u64(&bytes); std::debug::print(&bytes); std::debug::print(&converted); assert!(num==converted,0x01); - + } #[test] fun test_u128_conversion() { let num= (1222223333 as u128); - let bytes= utils::to_bytes_u128(num); + let bytes= utils::to_bytes_u128_sign(num,true); std::debug::print(&bytes); let converted=utils::from_bytes_u128(&bytes); std::debug::print(&converted); assert!(num==converted,0x01); - + } #[test] @@ -183,16 +170,13 @@ module sui_rlp::utils_test { let data=create_vector(10); let slice= utils::slice_vector(&data,0,3); let expected= create_vector(3); - //debug::print(&expected); - //debug::print(&slice); - //debug::print(&data); assert!(slice==expected,0x01); - + } fun create_vector(n:u8):vector{ - let mut data=vector::empty(); + let mut data=vector::empty(); let mut i=0; while(i < n){ vector::push_back(&mut data,i); diff --git a/contracts/sui/libs/sui_rlp/tests/rlp_tests.move b/contracts/sui/libs/sui_rlp/tests/rlp_tests.move index 2632c0748..11ded18d6 100644 --- a/contracts/sui/libs/sui_rlp/tests/rlp_tests.move +++ b/contracts/sui/libs/sui_rlp/tests/rlp_tests.move @@ -80,6 +80,80 @@ module sui_rlp::rlp_tests { list } + + #[test] + fun test_encoding_u128(){ + let num:u128=100; + let bytes=x"64"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + //// + /// + let num:u128=200; + let bytes=x"8200c8"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + /// + /// + let num:u128=3000000; + let bytes=x"832dc6c0"; + let encoded= encoder::encode_u128(num); + std::debug::print(&encoded); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + let num:u128=273468273; + let bytes=x"84104ccb71"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + let num:u128=2342312; + let bytes=x"8323bda8"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + let num:u128=1233; + let bytes=x"8204d1"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + let num:u128=412926; + let bytes=x"83064cfe"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + let num:u128=9434628989898; + let bytes=x"860894abb5a3ca"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + let num:u128=92625222222121112; + let bytes=x"88014912261bca8898"; + let encoded= encoder::encode_u128(num); + assert!(encoded==bytes); + let decoded=decoder::decode_u128(&decoder::decode(&encoded)); + assert!(decoded==num); + + + + } + diff --git a/contracts/sui/multisig/Move.lock b/contracts/sui/multisig/Move.lock new file mode 100644 index 000000000..940ee6ce0 --- /dev/null +++ b/contracts/sui/multisig/Move.lock @@ -0,0 +1,26 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 2 +manifest_digest = "0BAC026A0C518E4F4E63B8A7BB1FA36291E16B8919911B03E105CDD71EA40B5E" +deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" +dependencies = [ + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.30.3" +edition = "2024.beta" +flavor = "sui" diff --git a/contracts/sui/multisig/Move.toml b/contracts/sui/multisig/Move.toml new file mode 100644 index 000000000..0299139be --- /dev/null +++ b/contracts/sui/multisig/Move.toml @@ -0,0 +1,37 @@ +[package] +name = "multisig" +edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +multisig = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/contracts/sui/multisig/README.md b/contracts/sui/multisig/README.md new file mode 100644 index 000000000..98f71cb16 --- /dev/null +++ b/contracts/sui/multisig/README.md @@ -0,0 +1,153 @@ +# Multisig Contract on Sui + +This README provides an overview of the Multisig Contract on Sui, which serves as a signature manager rather than a traditional multisig contract. Unlike multisig implementations on EVM or CosmWasm, Sui has a built-in multisig mechanism accessible via the CLI. However, since using the CLI may not be practical for everyone, this contract simplifies multisig management, ensuring a secure and efficient way to handle multisig transactions. + +## Features + +- **Signature Management**: Add members to create a multisig address. The address remains consistent across contracts. +- **Tamper-Proof**: The contract is designed solely as a manager, ensuring no vulnerabilities even if tampered with. + +## Contract Overview + +The contract comprises several core functions essential for creating and managing multisig transactions: + +- **`create_multi_signature`**: Initializes a multisig instance with the required signatures and signers. +- **`create_proposal`**: Creates a proposal with the serialized transaction bytes. +- **`approve_proposal`**: Allows members to approve a transaction by submitting their signatures. +- **`get_execute_command`**: Retrieves the command to execute a multisig transaction once the threshold is met. + +### Function Signatures + +- `create_multi_signature(raw_signatures:&vector>, signers:&vector, threshold:u16)` +- `create_proposal(storage:&mut Storage,title:String,tx_bytes_64:String,multisig_address:address,ctx:&TxContext)` +- `approve_proposal(storage:&mut Storage, proposal_id:u64, raw_signature_64:String, ctx:&TxContext)` +- `get_execute_command(storage:&Storage, proposal_id:u64): String` + +## Step-by-Step Guide + +### 1. Publishing the Contract + +First, deploy the multisig contract to the Sui network. This contract will act as a manager for your multisig addresses and will not hold any funds or perform any transactions on its own. + +### 2. Adding Members and Creating a Multisig Address + +To create a multisig address: + +- **Register Wallet**: You'll need the public key, not the address, to register a wallet in Sui. Refer to the [Sui Cryptography Documentation](https://docs.sui.io/concepts/cryptography/transaction-auth/keys-addresses) for more details on keys and addresses. +- **Frontend Interaction**: Since the Sui wallet does not directly expose public keys, when you connect your wallet to the multisig frontend, it will fetch the public key for you. +- **Setup**: After obtaining public keys, register each wallet and set the threshold for the number of required signatures. + +### 3. Creating and Proposing Transactions + +To propose a transaction: + +- **Serialize Transaction**: Generate serialized bytes of the transaction you wish to execute from the multisig. +- **Handle Large Transactions**: If the transaction bytes are too large, save them to a file and create a proposal with the digest of the intent. This digest can be obtained by signing the transaction bytes using the Sui CLI. When signing and executing later, you must provide the original transaction bytes, not the digest. + ```bash + digest=$(sui keytool sign --data $result --address $active_address --json | jq '.digest') + echo $digest + ``` +- **Create Proposal**: Use the `create_proposal` function to initiate the multisig process by creating a proposal with the serialized transaction bytes. + + **Important Note:** Ensure to provide a gas object that is held by the multisig address when creating the proposal. This is crucial because the transaction will need to be executed by the multisig, and therefore it requires its own gas object to complete the transaction. + + +### 4. Approving a Transaction + +To approve a transaction: + +- **Sign with Member**: Each member of the multisig must sign the serialized transaction bytes. +- **Submit Signature**: Members submit their signatures by calling the `approve_proposal` function with their signature and the proposal id. + +### 5. Executing the Transaction + +Once the required number of signatures (as per the threshold) is collected: + +- **Retrieve Execute Command**: Call the `get_execute_command` function to get the command required to execute the signed transaction. + ```bash + sui client execute-signed-tx --tx-bytes ${ORIGINAL_TX_BYTES} --signatures + ``` +- **Execute via CLI**: Replace ${ORIGINAL_TX_BYTES} with the original transaction bytes. Run the command retrieved from the previous step on the Sui CLI to execute the multisig transaction. + +And that's it! The transaction will be signed by multiple members and executed successfully. + +## Frontend Integration Guide for Multisig Contract on Sui + +This guide is for the frontend development team to integrate the multisig contract functionalities into the user interface. The backend handles wallet registration and proposal creation, while the frontend focuses on the signing, approving, and executing stages. + +### Workflow Overview + +1. **Wallet Registration and Proposal Creation**: Handled by the backend team. +2. **Signing and Approving a Proposal**: Handled by the frontend. +3. **Retrieving the Execute Command**: Handled by the frontend and provided to the user for execution via the CLI. + +### 1. Signing and Approving a Proposal + +#### Retrieving and Displaying Proposals + +To enable users to view the current proposals: + +- **Retrieve Proposals**: The frontend should call the `get_proposals` function to fetch the list of proposals stored in the contract. + +- The function returns a reference to a table containing the proposals, which can be displayed in the frontend for users to view and interact with. + +```javascript +// Fetch proposals from the contract +const proposals = await getProposalsFunction(storage_id: storage_id); +``` + +#### Step 1: User Provides Transaction Bytes + +- The frontend should allow users to input or upload the serialized transaction bytes that need to be signed. + +#### Step 2: Signing the Transaction Bytes + +- Once the user provides the transaction bytes, the frontend should facilitate the signing process using the connected wallet. + +- You can use the following code snippet to sign the transaction bytes: + +```javascript +// Assume 'kp' is the keypair obtained from the connected wallet +const message = transactionBytes; // This is the serialized transaction bytes provided by the user +const signature = (await kp.signPersonalMessage(message)).signature; +``` + +#### Step 3: Calling the Approve Function + +- After signing the transaction bytes, the frontend should call the contract's `approve_proposal` function with the obtained signature. + +- The function call would look something like this: + +```javascript +// Approve proposal using the obtained signature +await approveProposalFunction({ + storage_id: storage_id, + proposalId: proposalId, + rawSignature64: signature, +}); +``` + +- Here, `proposalId` is the ID of the proposal, `rawSignature64` is the signature obtained from the signing process. + +### 2. Retrieving and Presenting the Execute Command + +#### Step 1: Calling the Get Execute Command Function + +- Once the necessary number of approvals (as per the threshold) is met, the frontend should call the contract's `get_execute_command` function to retrieve the execute command. + +- The function call could be structured as follows: + +```javascript +// Retrieve the execute command +const executeCommand = await getExecuteCommandFunction({ + storage_id: storage_id, + proposalId: proposalId +}); +``` +- Here, `proposalId` is the ID of the proposal. + +#### Step 2: Presenting the Execute Command to the User + +- The frontend should then display the execute command to the user (likely the proposal creator or designated executor) so that they can copy, fill the transaction bytes blank space and run it in their CLI to execute the multisig transaction. + +- You can provide a simple UI element like a copy button next to the command to facilitate easy copying. diff --git a/contracts/sui/multisig/sources/base64.move b/contracts/sui/multisig/sources/base64.move new file mode 100644 index 000000000..2ef0330b0 --- /dev/null +++ b/contracts/sui/multisig/sources/base64.move @@ -0,0 +1,134 @@ +module multisig::base64 { + use std::vector::{Self}; + use std::string::{Self,String}; + use sui::vec_map::{Self, VecMap}; + + const BASE64_CHARS: vector = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const PADDING_CHAR: vector = b"="; + + + public fun encode(input:&vector):String { + + let mut output:vector = vector::empty(); + let mut i=0; + while (i > 18) & 0x3F) as u64)); + output.push_back(*BASE64_CHARS.borrow(((triple >> 12) & 0x3F) as u64)); + + if (i + 1 < input.length()) { + output.push_back(*BASE64_CHARS.borrow(((triple >> 6) & 0x3F) as u64)); + } else { + output.push_back(*PADDING_CHAR.borrow(0)); + }; + + if (i + 2 < input.length()) { + output.push_back(*BASE64_CHARS.borrow((triple & 0x3F) as u64)); + } else { + output.push_back(*PADDING_CHAR.borrow(0)); + }; + + i =i+ 3; + }; + string::utf8(output) + } + + public fun decode(input:&String):vector{ + let char_index=get_char_map(); + let mut output = vector::empty(); + let input_bytes = input.as_bytes(); + let mut i = 0; + while( i < input_bytes.length()){ + let b1 = *char_index.get(input_bytes.borrow(i)); + let b2 = *char_index.get(input_bytes.borrow(i + 1)); + let b3 = if (i + 2 < input_bytes.length()) { + let key=input_bytes.borrow(i+2); + let val:u32 = if (char_index.contains(key)) { + *char_index.get(key) + } else { + 64 + }; + val + } else { + 64 + }; + + let b4 = if (i + 3 < input_bytes.length()) { + let key=input_bytes.borrow(i+3); + let val:u32= if (char_index.contains(key)) { + *char_index.get(key) + } else { + 64 + }; + val + } else { + 64 + }; + + let triple = (b1 << 18) | (b2 << 12) | (b3 << 6) | b4; + + output.push_back(((triple >> 16) & 0xFF) as u8); + + if (b3 != 64) { + output.push_back(((triple >> 8) & 0xFF) as u8); + }; + + if (b4 != 64) { + output.push_back((triple & 0xFF) as u8); + }; + + i = i+4; + + }; + output + + } + + + + fun get_char_map():VecMap{ + let mut char_map = vec_map::empty(); + let mut i:u64=0; + while( i < BASE64_CHARS.length()){ + let c=*BASE64_CHARS.borrow(i); + char_map.insert(c,(i as u32)); + i=i+1; + }; + char_map + } +} + + +#[test_only] +module multisig::base64_tests { + use sui::hash::{Self}; + use multisig::base64::{Self}; + #[test] + fun test_base64(){ + let input = b"Hello, World!"; + let encoded = base64::encode(&input); + assert!(encoded.as_bytes()==b"SGVsbG8sIFdvcmxkIQ=="); + + let decoded = base64::decode(&encoded); + std::debug::print(&encoded); + assert!(decoded==input); + } + + +} + + + diff --git a/contracts/sui/multisig/sources/multisig.move b/contracts/sui/multisig/sources/multisig.move new file mode 100644 index 000000000..a21a55762 --- /dev/null +++ b/contracts/sui/multisig/sources/multisig.move @@ -0,0 +1,605 @@ + +module multisig::multisig { + use std::vector::{Self}; + use sui::linked_table::{Self, LinkedTable}; + use sui::types as sui_types; + use std::string::{Self, String}; + use sui::event; + use sui::hash::{Self}; + use sui::vec_map::{Self, VecMap}; + use sui::table::{Table,Self}; + use sui::bcs::{Self}; + use sui::address::{Self}; + use multisig::base64::{Self}; + use sui::{ed25519::ed25519_verify}; + use sui::{ecdsa_k1::secp256k1_verify}; + use sui::{ecdsa_r1::secp256r1_verify}; + + //ERROR + const EAlreadyApproved: u64 = 10; + const EApprovalThresholdNotMet: u64 = 11; + const EOnlyMember: u64 = 12; + + /** signature schemes*/ + const FlagED25519 :u8= 0x00; + const FlagSecp256k1 :u8= 0x01; + const FlagSecp256r1 :u8= 0x02; + const FlagMultiSig :u8= 0x03; + + /* hash algorithm*/ + const KECCAK256: u8 = 0x00; + const SHA256: u8 = 0x01; + + public struct Executed has copy, drop{ + proposal_id:u64, + command:String, + + } + + + public struct Signer has store,drop{ + pub_key:vector, + sui_address:address, + weight:u8 + } + + public fun new_signer(pub_key:vector,weight:u8):Signer{ + let sui_address = address::from_bytes(hash::blake2b256(&pub_key)); + Signer { + pub_key, + sui_address, + weight + } + } + + public struct MultisigWallet has store { + multisig_address:address, + signers:vector, + threshold:u16, + + } + + public fun multisig_address(self:&MultisigWallet):address { + return self.multisig_address + } + + public struct MultiSignature has drop{ + signatures:vector>, + bitmap:u16, + multi_pubkey:MultiPubKey + + + } + + public struct MultiPubKey has drop { + weighted_pubkey:vector>, + threshold:u16 + } + + public struct Proposal has store,drop,copy{ + id:u64, + title:String, + multisig_address:address, + tx_data:vector, + is_digest:bool, + approved:bool + } + + public struct Vote has store,drop{ + signature:vector, + voter:address, + } + + public struct VoteKey has store,drop,copy{ + proposal_id:u64, + sui_address:address, + } + + + public struct Storage has key,store{ + id:UID, + wallets:VecMap, + wallet_proposals:Table>, + proposals:Table, + votes:Table, + proposal_count:u64, + } + + + public fun get_wallets(self:&Storage):&VecMap{ + &self.wallets + } + + public fun get_proposals(self:&Storage):vector{ + let mut i = 1; + let mut proposals = vector::empty(); + while (i <= self.proposal_count){ + let proposal = self.proposals.borrow(i); + proposals.push_back(*proposal); + i=i+1; + }; + proposals + } + public struct AdminCap has key,store { + id: UID + } + + + fun init(ctx: &mut TxContext) { + let admin = AdminCap { + id: object::new(ctx), + }; + let storage = Storage { + id:object::new(ctx), + wallets:vec_map::empty(), + wallet_proposals: table::new(ctx), + proposals:table::new(ctx), + votes: table::new(ctx), + proposal_count:0u64, + }; + transfer::transfer(admin, tx_context::sender(ctx)); + transfer::share_object(storage); + + } + + + + public fun create_multisig_address(pubkeys:vector>,weights:vector,threshold:u16):address{ + let mut bytes= vector::empty(); + bytes.push_back(FlagMultiSig); + let threshold_bytes=bcs::to_bytes(&threshold); + bytes.append(threshold_bytes); + let mut i=0; + while(i < pubkeys.length()){ + bytes.append(*pubkeys.borrow(i)); + bytes.push_back(*weights.borrow(i)); + i=i+1; + }; + + let address_bytes=hash::blake2b256(&bytes); + address::from_bytes(address_bytes) + + } + + entry fun register_wallet(storage:&mut Storage,_admin:&AdminCap, pub_keys:vector,weights:vector,threshold:u16){ + assert!(pub_keys.length()==weights.length()); + assert!(threshold>0); + let mut pub_keys_bytes:vector> = vector::empty(); + let mut signers:vector = vector::empty(); + let mut i=0; + while(i < pub_keys.length()){ + let bytes=base64::decode(pub_keys.borrow(i)); + pub_keys_bytes.push_back(bytes); + let signer_1=new_signer(bytes,*weights.borrow(i)); + signers.push_back(signer_1); + i=i+1; + }; + let multisig_addr= create_multisig_address(pub_keys_bytes,weights,threshold); + let multisig_wallet= MultisigWallet{ + multisig_address:multisig_addr, + signers:signers, + threshold:threshold, + }; + storage.wallets.insert(multisig_addr,multisig_wallet); + storage.wallet_proposals.add(multisig_addr, vector::empty()); + } + + entry fun create_proposal(storage:&mut Storage,title:String,tx_bytes_64:String,multisig_address:address,ctx:&TxContext){ + let tx_bytes=base64::decode(&tx_bytes_64); + let wallet=storage.wallets.get(&multisig_address); + assert!(only_member(wallet,ctx.sender())==true, EOnlyMember); + let is_digest=tx_bytes.length()==32; + let proposal_id=get_proposal_id(storage); + let proposal= Proposal{ + id:proposal_id, + title:title, + multisig_address:multisig_address, + tx_data:tx_bytes, + is_digest, + approved:false + }; + storage.proposals.add(proposal_id,proposal); + storage.wallet_proposals.borrow_mut(multisig_address).push_back(proposal_id); + + } + + entry fun approve_proposal(storage:&mut Storage,proposal_id:u64,raw_signature_64:String,ctx:&TxContext){ + let raw_signature=base64::decode(&raw_signature_64); + let proposal = storage.proposals.borrow(proposal_id); + let wallet= storage.wallets.get(&proposal.multisig_address); + assert!(only_member(wallet,ctx.sender())==true, EOnlyMember); + let (index,pubkey)=get_pubkey(wallet,ctx.sender()); + assert!(index!=0); + assert!(verify_pubkey(&pubkey,&proposal.tx_data,&raw_signature,proposal.is_digest)==true); + let vote_key=VoteKey{ + proposal_id:proposal_id, + sui_address:ctx.sender() + }; + assert!(storage.votes.contains(vote_key)==false, EAlreadyApproved); + storage.votes.add(vote_key, Vote{ + signature:raw_signature, + voter:ctx.sender() + }); + + let mut signatures:vector> = vector::empty(); + let mut i=0; + while( i < wallet.signers.length()){ + + let signer_1= wallet.signers.borrow(i); + let key=VoteKey{ + proposal_id:proposal_id, + sui_address:signer_1.sui_address + }; + if (storage.votes.contains(key)){ + signatures.push_back(storage.votes.borrow(key).signature) + }; + i=i+1; + + }; + + if (signatures.length()>=wallet.threshold as u64){ + storage.proposals.borrow_mut(proposal_id).approved=true; + } + } + + entry fun execute_event(storage:&Storage,proposal_id:u64){ + let command= get_execute_command(storage,proposal_id); + event::emit(Executed {proposal_id:proposal_id,command:command}); + } + + public fun has_member_voted(storage:&Storage,proposal_id:u64,member:address):bool{ + let key=VoteKey{ + proposal_id:proposal_id, + sui_address:member + }; + storage.votes.contains(key) + } + + public fun get_execute_command(storage:&Storage,proposal_id:u64):String{ + let proposal=storage.proposals.borrow(proposal_id); + let wallet=storage.wallets.get(&proposal.multisig_address); + let mut signatures:vector> = vector::empty(); + + let mut i=0; + while( i =wallet.threshold as u64, EApprovalThresholdNotMet); + + let multisig= create_multi_signature(&signatures,&wallet.signers,wallet.threshold); + let multisig_serialized_64= base64::encode(&serialize_multisig(&multisig)); + + let mut command=vector::empty(); + command.append(b"sui client execute-signed-tx --tx-bytes "); + let tx_data_64= base64::encode(&proposal.tx_data); + + if(proposal.is_digest){ + command.append(b"${ORIGINAL_TX_BYTES}"); + }else { + command.append(*tx_data_64.as_bytes()); + }; + + command.append(b" --signatures "); + command.append(*multisig_serialized_64.as_bytes()); + string::utf8(command) + } + + fun only_member(wallet:&MultisigWallet,caller:address):bool{ + let mut i=0; + let mut is_member=false; + while(i < wallet.signers.length() && is_member==false){ + if (wallet.signers.borrow(i).sui_address==caller){ + is_member=true; + }; + i=i+1; + }; + is_member + + } + + fun get_proposal_id(storage:&mut Storage):u64{ + let count=storage.proposal_count+1; + storage.proposal_count=count; + count + } + + public fun create_multi_signature(raw_signatures:&vector>,signers:&vector,threshold:u16):MultiSignature{ + let mut bitmap:u16=0; + let mut i:u64=0; + let mut signatures:vector> = vector::empty(); + let mut weighted_pubkey:vector> =vector::empty(); + while( i < raw_signatures.length()){ + let (sig,pub,scheme)=split_signature(raw_signatures.borrow(i)); + let mut index= get_pub_key_index(signers,pub); + assert!(index>0); + index=index-1; + + bitmap =bitmap | (1 << index); + std::debug::print(&bitmap); + let mut full_sig:vector =vector::empty(); + full_sig.push_back(scheme); + full_sig.append(sig); + signatures.push_back(full_sig); + i=i+1; + + + }; + i=0; + while(i < signers.length()){ + let mut pubkey= signers.borrow(i).pub_key; + pubkey.push_back(signers.borrow(i).weight); + weighted_pubkey.push_back(pubkey); + i=i+1; + }; + let multi_pubkey=MultiPubKey { + weighted_pubkey, + threshold + }; + + MultiSignature { + signatures, + bitmap, + multi_pubkey + } + } + + public fun serialize_multisig(sig:&MultiSignature):vector{ + let mut serialized:vector = vector::empty(); + serialized.push_back(FlagMultiSig); + serialized.push_back(sig.signatures.length() as u8); + let mut i=0; + while(i < sig.signatures.length()){ + serialized.append(*sig.signatures.borrow(i)); + i=i+1; + }; + let bitmap=std::bcs::to_bytes(&sig.bitmap); + serialized.append(bitmap); + serialized.push_back(sig.multi_pubkey.weighted_pubkey.length() as u8); + i=0; + while( i < sig.multi_pubkey.weighted_pubkey.length()){ + serialized.append(*sig.multi_pubkey.weighted_pubkey.borrow(i)); + i=i+1; + }; + serialized.append(std::bcs::to_bytes(&sig.multi_pubkey.threshold)); + serialized + } + + fun get_pub_key_index(signers:&vector,pubkey:vector):u8{ + let mut index:u64=0; + while(index < signers.length()){ + let signer_pubkey=signers.borrow(index).pub_key; + let public_key=slice_vector(&signer_pubkey,1,signer_pubkey.length()-1); + if(public_key == pubkey){ + return ((index+1) as u8) + }; + index=index+1; + }; + 0u8 + + + } + + public fun verify_pubkey(key:&vector,data:&vector,raw_signature:&vector,is_digest:bool):bool{ + let flag=*key.borrow(0); + let public_key=slice_vector(key,1,key.length()-1); + let (signature,pub,_scheme)= split_signature(raw_signature); + assert!(public_key==pub); + let digest= if(is_digest==true){ + data + }else { + let intent_msg= get_intent_message(data); + let digest= hash::blake2b256(&intent_msg); + &digest + }; + let verify= if(flag==FlagED25519){ + + ed25519_verify(&signature,&public_key,digest) + + }else if(flag==FlagSecp256k1){ + + secp256k1_verify(&signature,&public_key,digest,SHA256) + + }else if (flag==FlagSecp256r1){ + + secp256r1_verify(&signature,&public_key,digest,SHA256) + }else { + return false + }; + verify + + } + + + fun get_intent_message(msg:&vector):vector{ + let mut intent_message:vector =vector::empty(); + intent_message.push_back(0x00); + intent_message.push_back(0x00); + intent_message.push_back(0x00); + intent_message.append(*msg); + intent_message + + } + + fun split_signature(raw_signature:&vector):(vector,vector,u8){ + let scheme=*raw_signature.borrow(0); + let length= if (scheme==FlagED25519){32}else{33}; + let signature=slice_vector(raw_signature,1,64); + let pubkey=slice_vector(raw_signature,raw_signature.length()-length,length); + return (signature,pubkey,scheme) + } + + fun get_pubkey(wallet:&MultisigWallet,caller:address):(u64,vector){ + let mut i=0; + while(i , start: u64, length: u64): vector { + let mut result = vector::empty(); + let mut i = 0; + while (i < length) { + let value = *vector::borrow(vec, start + i); + vector::push_back(&mut result, value); + i = i + 1; + }; + result + } + + #[test_only] use sui::test_scenario::{Self,Scenario}; + #[test_only] + public fun init_state(admin:address,mut scenario:Scenario):Scenario{ + init(scenario.ctx()); + scenario.next_tx(admin); + scenario + } + + +} +#[test_only] +module multisig::tests { + use sui::hash::{Self}; + use multisig::multisig::{create_multisig_address,verify_pubkey,Signer,new_signer,create_multi_signature,serialize_multisig}; + + + #[test] + fun test_public__key_address(){ + let pubkey = x"01033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814"; + let sui_address=hash::blake2b256(&pubkey); + assert!(x"29a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c204589"==sui_address); + + + } + + #[test] + fun test_multisig_address(){ + let mut public_keys:vector> =vector::empty(); + public_keys.push_back(x"01033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814"); + public_keys.push_back(x"00016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f481"); + public_keys.push_back(x"01034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d63804"); + public_keys.push_back(x"02023ecb8ff6cf8eb4748ef6fb062eb52f862b093eb3a42d629d89e43d9645108f9e"); + public_keys.push_back(x"0103b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598"); + + let mut weights:vector = vector::empty(); + weights.push_back(1u8); + weights.push_back(1u8); + weights.push_back(1u8); + weights.push_back(1u8); + weights.push_back(1u8); + + let threshold:u16=3u16; + + let sui_addr= create_multisig_address(public_keys,weights,threshold); + std::debug::print(&sui_addr); + let expected= @0x34f45f30d3af0393474ce42fc7a1de48aa8a9ddf03383062d8fcd1842d627a2f; + assert!(expected==sui_addr); + + + + } + + #[test] + fun test_verify_pubkey(){ + let data=x"00000200203fab45fb191ca013a74ccfc3b7d5ed27a3ef6dce79adc4d1e39555f01a361bf801001b61730f57e4d64241cecb40ac259c58ced75018bd231acdbe463ddb9ac99176480000000000000020e12acf2025db82d420e8696d3645dfd25c6542382883f6e3762cc655791a007e01010101010001000029a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c20458901c2a6df77449ebce38397d97785f52c2341abafc21c47523e9cfb71d141d5df614800000000000000209e4e3d3f48881797ecc3690b237e0353e16a8be1cd79ac75210657229942e12b29a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c204589e80300000000000078be2d000000000000"; + + + let mut public_keys:vector> = vector::empty(); + public_keys.push_back(x"01033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814"); + public_keys.push_back(x"00016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f481"); + public_keys.push_back(x"01034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d63804"); + public_keys.push_back(x"02023ecb8ff6cf8eb4748ef6fb062eb52f862b093eb3a42d629d89e43d9645108f9e"); + public_keys.push_back(x"0103b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598"); + + let mut signatures:vector> = vector::empty(); + signatures.push_back(x"0196e3d1a05e3d9d900281da7a3719dada72b66fdcfd4147275634f3028d71dba02896bf6958bdc97d93462bd0aa7245a04f05caa3e7f09465d725fa79f91fcc76033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814"); + signatures.push_back(x"00339e5f6df9aeac4e902767679407b1a6ae0e14db0036c4b19d8b45825036099299bb1948ef907b42df3a250b24395d49d1ff24e75748fadd32892934b22aa70e016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f481"); + signatures.push_back(x"019d51371b1fe243fc6bf92b0a6b58feb5cc7bfc0a90ee8900d2f5db8c22f7122f564dda95aff1ea5da752226686ff89e5c7c7e175d752bdc95d42c2bcf71cf160034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d63804"); + signatures.push_back(x"0222194e51b82f0a13293c4ca9db40ef1a8c4115ace0047466627ece5c0e229766479486a6423f8332b13d8a13f2baba5d72f86c4a5d55c5b8c2c134f2a367299e023ecb8ff6cf8eb4748ef6fb062eb52f862b093eb3a42d629d89e43d9645108f9e"); + signatures.push_back(x"016a21ed6ace0e5c28dd6396dc004a8a9321f9a4c446f037275cdbaad81797c544570cd9e470d4f46d11eb512d8b8a2b6f00c16356e9f2505ead5bafd492387e4303b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598"); + let mut i=0; + while( i < public_keys.length()){ + std::debug::print(public_keys.borrow(i)); + let signature=signatures.borrow(i); + let verify=verify_pubkey(public_keys.borrow(i),&data,signature,false); + i=i+1; + + assert!(verify==true); + }; + + } + + #[test] + fun test_create_multisignature(){ + + let mut signers:vector =vector::empty(); + signers.push_back(new_signer(x"01033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814",1)); + signers.push_back(new_signer(x"00016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f481",1)); + signers.push_back(new_signer(x"01034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d63804",1)); + signers.push_back(new_signer(x"02023ecb8ff6cf8eb4748ef6fb062eb52f862b093eb3a42d629d89e43d9645108f9e",1)); + signers.push_back(new_signer(x"0103b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598",1)); + let mut raw_signatures:vector> = vector::empty(); + raw_signatures.push_back(x"0196e3d1a05e3d9d900281da7a3719dada72b66fdcfd4147275634f3028d71dba02896bf6958bdc97d93462bd0aa7245a04f05caa3e7f09465d725fa79f91fcc76033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814"); + raw_signatures.push_back(x"00339e5f6df9aeac4e902767679407b1a6ae0e14db0036c4b19d8b45825036099299bb1948ef907b42df3a250b24395d49d1ff24e75748fadd32892934b22aa70e016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f481"); + raw_signatures.push_back(x"019d51371b1fe243fc6bf92b0a6b58feb5cc7bfc0a90ee8900d2f5db8c22f7122f564dda95aff1ea5da752226686ff89e5c7c7e175d752bdc95d42c2bcf71cf160034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d63804"); + + let multisignature= create_multi_signature(&raw_signatures,&signers,3_u16); + + std::debug::print(&serialize_multisig(&multisignature)); + + let expected=x"03030196e3d1a05e3d9d900281da7a3719dada72b66fdcfd4147275634f3028d71dba02896bf6958bdc97d93462bd0aa7245a04f05caa3e7f09465d725fa79f91fcc7600339e5f6df9aeac4e902767679407b1a6ae0e14db0036c4b19d8b45825036099299bb1948ef907b42df3a250b24395d49d1ff24e75748fadd32892934b22aa70e019d51371b1fe243fc6bf92b0a6b58feb5cc7bfc0a90ee8900d2f5db8c22f7122f564dda95aff1ea5da752226686ff89e5c7c7e175d752bdc95d42c2bcf71cf16007000501033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba907261268140100016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f4810101034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d638040102023ecb8ff6cf8eb4748ef6fb062eb52f862b093eb3a42d629d89e43d9645108f9e010103b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598010300"; + assert!(expected==serialize_multisig(&multisignature)); + + + + } + + #[test] + fun test_create_multisignature_2(){ + + let mut signers:vector =vector::empty(); + signers.push_back(new_signer(x"01033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814",1)); + signers.push_back(new_signer(x"00016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f481",1)); + signers.push_back(new_signer(x"01034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d63804",1)); + signers.push_back(new_signer(x"02023ecb8ff6cf8eb4748ef6fb062eb52f862b093eb3a42d629d89e43d9645108f9e",1)); + signers.push_back(new_signer(x"0103b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598",1)); + let mut raw_signatures:vector> = vector::empty(); + raw_signatures.push_back(x"0196e3d1a05e3d9d900281da7a3719dada72b66fdcfd4147275634f3028d71dba02896bf6958bdc97d93462bd0aa7245a04f05caa3e7f09465d725fa79f91fcc76033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814"); + raw_signatures.push_back(x"019d51371b1fe243fc6bf92b0a6b58feb5cc7bfc0a90ee8900d2f5db8c22f7122f564dda95aff1ea5da752226686ff89e5c7c7e175d752bdc95d42c2bcf71cf160034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d63804"); + raw_signatures.push_back(x"016a21ed6ace0e5c28dd6396dc004a8a9321f9a4c446f037275cdbaad81797c544570cd9e470d4f46d11eb512d8b8a2b6f00c16356e9f2505ead5bafd492387e4303b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598"); + + let multisignature= create_multi_signature(&raw_signatures,&signers,3_u16); + + std::debug::print(&serialize_multisig(&multisignature)); + + let expected=x"03030196e3d1a05e3d9d900281da7a3719dada72b66fdcfd4147275634f3028d71dba02896bf6958bdc97d93462bd0aa7245a04f05caa3e7f09465d725fa79f91fcc76019d51371b1fe243fc6bf92b0a6b58feb5cc7bfc0a90ee8900d2f5db8c22f7122f564dda95aff1ea5da752226686ff89e5c7c7e175d752bdc95d42c2bcf71cf160016a21ed6ace0e5c28dd6396dc004a8a9321f9a4c446f037275cdbaad81797c544570cd9e470d4f46d11eb512d8b8a2b6f00c16356e9f2505ead5bafd492387e4315000501033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba907261268140100016e02b7a72826d951789791f3c053f92af9bc28b27a8e2c60fc474ca536f4810101034195b5a61eeebee6fd2b959ef6e23f7393b2b0717b6458eebe6ff72778d638040102023ecb8ff6cf8eb4748ef6fb062eb52f862b093eb3a42d629d89e43d9645108f9e010103b13036d4a4adf7b9c36c5cd165613c82734e7541e3a47864fb5b8727e7920598010300"; + assert!(expected==serialize_multisig(&multisignature)); + + + + } + + + + + + + + + +} + + diff --git a/contracts/sui/multisig/tests/multisig_tests.move b/contracts/sui/multisig/tests/multisig_tests.move new file mode 100644 index 000000000..94bfc0eb2 --- /dev/null +++ b/contracts/sui/multisig/tests/multisig_tests.move @@ -0,0 +1,148 @@ +#[test_only] +module multisig::multisig_tests { + // uncomment this line to import the module + use multisig::multisig; + use sui::test_scenario::{Self, Scenario}; + use multisig::multisig::AdminCap; + use multisig::multisig::Storage; + use std::string::{Self, String}; + use multisig::base64::{ Self }; + + const ENotImplemented: u64 = 0; + + #[test_only] + fun setup_test(admin: address): Scenario { + let mut scenario = test_scenario::begin(admin); + scenario.next_tx(admin); + scenario = multisig::init_state(admin, scenario); + scenario + } + + #[test_only] + fun get_test_pub_keys(): vector { + let mut pubkeys: vector = vector::empty(); + pubkeys.push_back(string::utf8(b"AQM6YkAASHEsBpZFbeiCwm0Rmj3y/jFsWrFzi6kHJhJoFA==")); + pubkeys.push_back(string::utf8(b"AAFuArenKCbZUXiXkfPAU/kq+bwosnqOLGD8R0ylNvSB")); + pubkeys.push_back(string::utf8(b"AQNBlbWmHu6+5v0rlZ724j9zk7KwcXtkWO6+b/cneNY4BA==")); + pubkeys.push_back(string::utf8(b"AgI+y4/2z460dI72+wYutS+GKwk+s6QtYp2J5D2WRRCPng==")); + pubkeys.push_back(string::utf8(b"AQOxMDbUpK33ucNsXNFlYTyCc051QeOkeGT7W4cn55IFmA==")); + pubkeys + + } + + fun get_test_weights(): vector { + let mut weights: vector = vector::empty(); + weights.push_back(1); + weights.push_back(1); + weights.push_back(1); + weights.push_back(1); + weights.push_back(1); + weights + } + + #[test] + fun test_register_wallet() { + let admin = @0xBABE; + + let mut _scenario = setup_test(admin); + let scenario = &_scenario; + + { + let adminCap = test_scenario::take_from_sender(scenario); + let mut storage = test_scenario::take_shared(scenario); + // storage:&mut Storage,_admin:&AdminCap, pub_keys:vector,weights:vector,threshold:u16 + let pubkeys: vector = get_test_pub_keys(); + let weights: vector = get_test_weights(); + let expected = @0x34f45f30d3af0393474ce42fc7a1de48aa8a9ddf03383062d8fcd1842d627a2f; + + multisig::register_wallet(&mut storage, &adminCap, pubkeys, weights, 3); + assert!(storage.get_wallets().size() > 0); + let wallet = storage.get_wallets().get(&expected); + assert!(wallet.multisig_address() == expected); + scenario.return_to_sender(adminCap); + test_scenario::return_shared(storage); + }; + _scenario.end(); + + } + + #[test] + fun test_create_proposal() { + + let admin = @0x29a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c204589; + + let mut scenario = setup_test(admin); + { + let adminCap = test_scenario::take_from_sender(&scenario); + let mut storage = test_scenario::take_shared(&scenario); + // storage:&mut Storage,_admin:&AdminCap, pub_keys:vector,weights:vector,threshold:u16 + let pubkeys: vector = get_test_pub_keys(); + let weights: vector = get_test_weights(); + + multisig::register_wallet(&mut storage, &adminCap, pubkeys, weights, 3); + + scenario.return_to_sender(adminCap); + test_scenario::return_shared(storage); + + }; + test_scenario::next_tx(&mut scenario, admin); + { + let adminCap = test_scenario::take_from_sender(&scenario); + let mut storage = test_scenario::take_shared(&scenario); + let expected = @0x34f45f30d3af0393474ce42fc7a1de48aa8a9ddf03383062d8fcd1842d627a2f; + let tx_data = x"00000200203fab45fb191ca013a74ccfc3b7d5ed27a3ef6dce79adc4d1e39555f01a361bf801001b61730f57e4d64241cecb40ac259c58ced75018bd231acdbe463ddb9ac99176480000000000000020e12acf2025db82d420e8696d3645dfd25c6542382883f6e3762cc655791a007e01010101010001000029a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c20458901c2a6df77449ebce38397d97785f52c2341abafc21c47523e9cfb71d141d5df614800000000000000209e4e3d3f48881797ecc3690b237e0353e16a8be1cd79ac75210657229942e12b29a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c204589e80300000000000078be2d000000000000"; + let tx_data_64 = base64::encode(&tx_data); + multisig::create_proposal( + &mut storage, + string::utf8(b"test proposal"), + tx_data_64, + expected, + scenario.ctx() + ); + assert!(storage.get_proposals().length() > 0); + scenario.return_to_sender(adminCap); + test_scenario::return_shared(storage); + + }; + + scenario.end(); + + } + + #[test] + fun test_approve_proposal() { + + let admin = @0x29a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c204589; + + let mut scenario = setup_test(admin); + { + let adminCap = test_scenario::take_from_sender(&scenario); + let mut storage = test_scenario::take_shared(&scenario); + // storage:&mut Storage,_admin:&AdminCap, pub_keys:vector,weights:vector,threshold:u16 + let pubkeys: vector = get_test_pub_keys(); + let weights: vector = get_test_weights(); + let expected = @0x34f45f30d3af0393474ce42fc7a1de48aa8a9ddf03383062d8fcd1842d627a2f; + let tx_data = x"00000200203fab45fb191ca013a74ccfc3b7d5ed27a3ef6dce79adc4d1e39555f01a361bf801001b61730f57e4d64241cecb40ac259c58ced75018bd231acdbe463ddb9ac99176480000000000000020e12acf2025db82d420e8696d3645dfd25c6542382883f6e3762cc655791a007e01010101010001000029a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c20458901c2a6df77449ebce38397d97785f52c2341abafc21c47523e9cfb71d141d5df614800000000000000209e4e3d3f48881797ecc3690b237e0353e16a8be1cd79ac75210657229942e12b29a0918bee7a7e37d1a7d0613efc3f4455883ea217046f7db91d53e69c204589e80300000000000078be2d000000000000"; + let tx_data_64 = base64::encode(&tx_data); + multisig::register_wallet(&mut storage, &adminCap, pubkeys, weights, 3); + test_scenario::next_tx(&mut scenario, admin); + multisig::create_proposal( + &mut storage, + string::utf8(b"test proposal"), + tx_data_64, + expected, + scenario.ctx() + ); + test_scenario::next_tx(&mut scenario, admin); + assert!(storage.get_proposals().length() > 0); + let signature = x"0196e3d1a05e3d9d900281da7a3719dada72b66fdcfd4147275634f3028d71dba02896bf6958bdc97d93462bd0aa7245a04f05caa3e7f09465d725fa79f91fcc76033a62400048712c0696456de882c26d119a3df2fe316c5ab1738ba90726126814"; + let signature_64 = base64::encode(&signature); + multisig::approve_proposal(&mut storage, 1, signature_64, scenario.ctx()); + scenario.return_to_sender(adminCap); + test_scenario::return_shared(storage); + + }; + scenario.end(); + + } +} 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 000000000..0f72f53aa --- /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 000000000..1fab2e8f6 --- /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 000000000..07742c14e --- /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 170e2d0ff..044ffddce 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/types/message_result.move b/contracts/sui/xcall/sources/types/message_result.move index 0f53df481..9130009c0 100644 --- a/contracts/sui/xcall/sources/types/message_result.move +++ b/contracts/sui/xcall/sources/types/message_result.move @@ -87,7 +87,7 @@ module xcall::message_result_tests { let msg= message_result::create(1,message_result::success(),vector::empty()); let encoded= message_result::encode(&msg); std::debug::print(&encoded); - assert!(encoded==x"c58200010180",0x01); + assert!(encoded==x"c3010180",0x01); let decoded=message_result::decode(&encoded); assert!(decoded==msg,0x01); @@ -99,7 +99,7 @@ module xcall::message_result_tests { let msg= message_result::create(2,message_result::failure(),vector::empty()); let encoded= message_result::encode(&msg); std::debug::print(&encoded); - assert!(encoded==x"c58200020080",0x01); + assert!(encoded==x"c3020080",0x01); let decoded=message_result::decode(&encoded); assert!(decoded==msg,0x01); diff --git a/contracts/sui/xcall/sources/utils.move b/contracts/sui/xcall/sources/utils.move index 66e0ff9dc..489a44819 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 3c344e90c..c7a836ccf 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"