diff --git a/Cargo.lock b/Cargo.lock index 91b7e1a96..634ae01d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -79,7 +79,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -126,6 +126,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "alloy-primitives" version = "0.7.7" @@ -137,7 +143,7 @@ dependencies = [ "cfg-if", "const-hex", "derive_more", - "getrandom", + "getrandom 0.2.15", "hex-literal", "itoa", "k256", @@ -228,6 +234,7 @@ dependencies = [ "base64 0.21.7", "bcs", "clap", + "clarity", "config", "cosmrs", "cosmwasm-std", @@ -248,7 +255,7 @@ dependencies = [ "evm-gateway", "faux", "futures", - "generic-array", + "generic-array 0.14.7", "hex", "humantime-serde", "itertools 0.11.0", @@ -683,7 +690,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time", + "time 0.3.36", ] [[package]] @@ -932,7 +939,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha1", + "sha1 0.10.6", "sync_wrapper 0.1.2", "tokio", "tokio-tungstenite", @@ -1303,7 +1310,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1312,7 +1319,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1321,7 +1328,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1563,6 +1570,29 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "clarity" +version = "0.0.1" +source = "git+https://github.com/stacks-network/stacks-core?tag=2.5.0.0.7#bed29bca57eab8264970188ed6bbb90486578a5b" +dependencies = [ + "hashbrown 0.14.5", + "integer-sqrt", + "lazy_static", + "mutants", + "rand", + "rand_chacha", + "regex", + "rstest", + "rstest_reuse", + "serde", + "serde_derive", + "serde_json", + "serde_stacker", + "slog", + "stacks-common", + "time 0.2.27", +] + [[package]] name = "client" version = "1.0.0" @@ -1672,6 +1702,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_fn" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" + [[package]] name = "constant_time_eq" version = "0.3.0" @@ -1755,7 +1791,7 @@ dependencies = [ "cosmos-sdk-proto", "ecdsa", "eyre", - "getrandom", + "getrandom 0.2.15", "k256", "rand_core 0.6.4", "serde", @@ -1885,7 +1921,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "subtle", "zeroize", @@ -1897,7 +1933,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "typenum", ] @@ -1920,6 +1956,20 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" +dependencies = [ + "byteorder", + "digest 0.8.1", + "rand_core 0.5.1", + "serde", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -2322,13 +2372,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -2385,6 +2444,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "displaydoc" version = "0.2.4" @@ -2452,6 +2517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8 0.10.2", + "serde", "signature 2.2.0", ] @@ -2479,6 +2545,7 @@ dependencies = [ "curve25519-dalek 4.1.3", "ed25519 2.2.3", "rand_core 0.6.4", + "serde", "sha2 0.10.8", "subtle", "zeroize", @@ -2515,7 +2582,7 @@ dependencies = [ "crypto-bigint", "digest 0.10.7", "ff", - "generic-array", + "generic-array 0.14.7", "group", "pem-rfc7468 0.7.0", "pkcs8 0.10.2", @@ -2739,7 +2806,7 @@ dependencies = [ "const-hex", "elliptic-curve", "ethabi", - "generic-array", + "generic-array 0.14.7", "k256", "num_enum 0.7.2", "once_cell", @@ -2883,7 +2950,7 @@ dependencies = [ "ed25519-consensus", "elliptic-curve", "fastcrypto-derive", - "generic-array", + "generic-array 0.14.7", "hex", "hex-literal", "hkdf", @@ -2896,7 +2963,7 @@ dependencies = [ "rfc6979", "rsa", "schemars", - "secp256k1", + "secp256k1 0.27.0", "serde", "serde_json", "serde_with 2.3.3", @@ -3326,6 +3393,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3338,6 +3414,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -3347,7 +3434,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -3487,6 +3574,11 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", + "serde", +] [[package]] name = "hashers" @@ -3519,7 +3611,7 @@ dependencies = [ "http 0.2.12", "httpdate", "mime", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -3549,6 +3641,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -4004,7 +4102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ "block-padding", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -4031,6 +4129,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "integration-tests" version = "1.0.0" @@ -4119,6 +4226,17 @@ dependencies = [ "nom", ] +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -4542,6 +4660,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -4580,7 +4707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -5050,7 +5177,7 @@ dependencies = [ "ed25519-dalek", "enum-display-derive", "error-stack", - "getrandom", + "getrandom 0.2.15", "goldie", "hex", "itertools 0.11.0", @@ -5089,7 +5216,7 @@ dependencies = [ "evm-gateway", "gateway", "gateway-api", - "generic-array", + "generic-array 0.14.7", "goldie", "hex", "itertools 0.11.0", @@ -5141,6 +5268,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + [[package]] name = "mysten-metrics" version = "0.7.0" @@ -5271,13 +5404,26 @@ dependencies = [ "blstrs", "byteorder", "ff", - "generic-array", + "generic-array 0.14.7", "log", "pasta_curves", "serde", "trait-set", ] +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.3" @@ -5436,7 +5582,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -5649,6 +5795,28 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "p256k1" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40a031a559eb38c35a14096f21c366254501a06d41c4b327d2a7515d713a5b7" +dependencies = [ + "bitvec 1.0.1", + "bs58 0.4.0", + "cc", + "hex", + "itertools 0.10.5", + "num-traits", + "primitive-types 0.12.2", + "proc-macro2 1.0.85", + "quote 1.0.36", + "rand_core 0.6.4", + "rustfmt-wrapper", + "serde", + "sha2 0.10.8", + "syn 2.0.68", +] + [[package]] name = "pairing" version = "0.23.0" @@ -6051,6 +6219,16 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "polynomial" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27abb6e4638dcecc65a92b50d7f1d87dd6dea987ba71db987b6bf881f4877e9d" +dependencies = [ + "num-traits", + "serde", +] + [[package]] name = "polyval" version = "0.6.2" @@ -6223,6 +6401,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "0.4.30" @@ -6525,6 +6709,9 @@ name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] [[package]] name = "rand_core" @@ -6532,7 +6719,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -6570,7 +6757,7 @@ checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" dependencies = [ "pem 1.1.1", "ring 0.16.20", - "time", + "time 0.3.36", "yasna", ] @@ -6600,7 +6787,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror", ] @@ -6818,7 +7005,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -6949,6 +7136,44 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version 0.4.0", +] + +[[package]] +name = "rstest_macros" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" +dependencies = [ + "cfg-if", + "proc-macro2 1.0.85", + "quote 1.0.36", + "rustc_version 0.4.0", + "syn 1.0.109", + "unicode-ident", +] + +[[package]] +name = "rstest_reuse" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f80dcc84beab3a327bbe161f77db25f336a1452428176787c8c79ac79d7073" +dependencies = [ + "quote 1.0.36", + "rand", + "rustc_version 0.4.0", + "syn 1.0.109", +] + [[package]] name = "ruint" version = "1.12.3" @@ -7007,6 +7232,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.3.3" @@ -7025,6 +7259,19 @@ dependencies = [ "semver 1.0.23", ] +[[package]] +name = "rustfmt-wrapper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1adc9dfed5cc999077978cc7163b9282c5751c8d39827c4ea8c8c220ca5a440" +dependencies = [ + "serde", + "tempfile", + "thiserror", + "toml 0.8.14", + "toolchain_find", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -7313,12 +7560,22 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der 0.7.9", - "generic-array", + "generic-array 0.14.7", "pkcs8 0.10.2", "subtle", "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" +dependencies = [ + "secp256k1-sys 0.6.1", + "serde", +] + [[package]] name = "secp256k1" version = "0.27.0" @@ -7327,7 +7584,16 @@ checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" dependencies = [ "bitcoin_hashes 0.12.0", "rand", - "secp256k1-sys", + "secp256k1-sys 0.8.1", +] + +[[package]] +name = "secp256k1-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +dependencies = [ + "cc", ] [[package]] @@ -7362,13 +7628,22 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + [[package]] name = "semver" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ - "semver-parser", + "semver-parser 0.10.2", ] [[package]] @@ -7380,6 +7655,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "semver-parser" version = "0.10.2" @@ -7525,6 +7806,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_stacker" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "babfccff5773ff80657f0ecf553c7c516bdc2eb16389c0918b36b73e7015276e" +dependencies = [ + "serde", + "stacker", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -7550,7 +7841,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros 2.3.3", - "time", + "time 0.3.36", ] [[package]] @@ -7568,7 +7859,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_with_macros 3.11.0", - "time", + "time 0.3.36", ] [[package]] @@ -7664,6 +7955,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7675,6 +7975,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.9.9" @@ -7800,7 +8106,7 @@ dependencies = [ "num-bigint 0.4.5", "num-traits", "thiserror", - "time", + "time 0.3.36", ] [[package]] @@ -7828,6 +8134,37 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-json" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1e53f61af1e3c8b852eef0a9dee29008f55d6dd63794f3f12cef786cf0f219" +dependencies = [ + "serde", + "serde_json", + "slog", + "time 0.3.36", +] + +[[package]] +name = "slog-term" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" +dependencies = [ + "is-terminal", + "slog", + "term", + "thread_local", + "time 0.3.36", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -7911,12 +8248,100 @@ dependencies = [ "winapi", ] +[[package]] +name = "stacks-common" +version = "0.0.1" +source = "git+https://github.com/stacks-network/stacks-core?tag=2.5.0.0.7#bed29bca57eab8264970188ed6bbb90486578a5b" +dependencies = [ + "chrono", + "curve25519-dalek 2.0.0", + "ed25519-dalek", + "hashbrown 0.14.5", + "lazy_static", + "libc", + "nix", + "percent-encoding", + "rand", + "ripemd", + "secp256k1 0.24.3", + "serde", + "serde_derive", + "serde_json", + "serde_stacker", + "sha2 0.10.8", + "sha3", + "slog", + "slog-json", + "slog-term", + "time 0.2.27", + "winapi", + "wsts", +] + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2 1.0.85", + "quote 1.0.36", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2 1.0.85", + "quote 1.0.36", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "stellar" version = "1.0.0" @@ -8499,7 +8924,7 @@ dependencies = [ "subtle", "subtle-encoding", "tendermint-proto 0.32.2", - "time", + "time 0.3.36", "zeroize", ] @@ -8527,7 +8952,7 @@ dependencies = [ "subtle", "subtle-encoding", "tendermint-proto 0.33.0", - "time", + "time 0.3.36", "zeroize", ] @@ -8559,7 +8984,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time", + "time 0.3.36", ] [[package]] @@ -8576,7 +9001,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time", + "time 0.3.36", ] [[package]] @@ -8588,7 +9013,7 @@ dependencies = [ "bytes", "flex-error", "futures", - "getrandom", + "getrandom 0.2.15", "http 0.2.12", "hyper 0.14.29", "hyper-proxy", @@ -8605,7 +9030,7 @@ dependencies = [ "tendermint-config", "tendermint-proto 0.33.0", "thiserror", - "time", + "time 0.3.36", "tokio", "tracing", "url", @@ -8613,6 +9038,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -8693,6 +9129,21 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi", +] + [[package]] name = "time" version = "0.3.36" @@ -8705,7 +9156,7 @@ dependencies = [ "powerfmt", "serde", "time-core", - "time-macros", + "time-macros 0.2.18", ] [[package]] @@ -8714,6 +9165,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + [[package]] name = "time-macros" version = "0.2.18" @@ -8724,6 +9185,19 @@ dependencies = [ "time-core", ] +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2 1.0.85", + "quote 1.0.36", + "standback", + "syn 1.0.109", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -9038,6 +9512,19 @@ dependencies = [ "tonic 0.11.0", ] +[[package]] +name = "toolchain_find" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc8c9a7f0a2966e1acdaf0461023d0b01471eeead645370cf4c3f5cff153f2a" +dependencies = [ + "home", + "once_cell", + "regex", + "semver 1.0.23", + "walkdir", +] + [[package]] name = "tower" version = "0.4.13" @@ -9221,7 +9708,7 @@ dependencies = [ "log", "rand", "rustls 0.21.12", - "sha1", + "sha1 0.10.6", "thiserror", "url", "utf-8", @@ -9408,7 +9895,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "getrandom", + "getrandom 0.2.15", "rand", ] @@ -9528,6 +10015,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9902,6 +10395,28 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wsts" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "538ed71c766b41946e7a663e9d9ab317fd45c3c9b61090edc1d202a281ca195c" +dependencies = [ + "aes-gcm", + "bs58 0.5.1", + "hashbrown 0.14.5", + "hex", + "num-traits", + "p256k1", + "polynomial", + "primitive-types 0.12.2", + "rand_core 0.6.4", + "serde", + "sha2 0.10.8", + "thiserror", + "tracing", + "tracing-subscriber", +] + [[package]] name = "wyz" version = "0.2.0" @@ -9932,7 +10447,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time", + "time 0.3.36", ] [[package]] @@ -9962,7 +10477,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time", + "time 0.3.36", ] [[package]] diff --git a/ampd/Cargo.toml b/ampd/Cargo.toml index fee1d54ac..cc3e271a2 100644 --- a/ampd/Cargo.toml +++ b/ampd/Cargo.toml @@ -12,6 +12,7 @@ axum = "0.7.5" base64 = "0.21.2" bcs = { workspace = true } clap = { version = "4.2.7", features = ["derive", "cargo"] } +clarity = { git = "https://github.com/stacks-network/stacks-core", tag = "2.5.0.0.7", default-features = false, features = ["slog_json"] } config = "0.13.2" cosmrs = { version = "0.14.0", features = ["cosmwasm", "grpc"] } cosmwasm-std = { workspace = true, features = ["stargate"] } diff --git a/ampd/src/config.rs b/ampd/src/config.rs index eaab6ff91..7ff235288 100644 --- a/ampd/src/config.rs +++ b/ampd/src/config.rs @@ -132,6 +132,19 @@ mod tests { type = 'StellarVerifierSetVerifier' cosmwasm_contract = '{}' rpc_url = 'http://localhost:7545' + + [[handlers]] + type = 'StacksMsgVerifier' + cosmwasm_contract = '{}' + http_url = 'http://localhost:8000' + its_address = 'its_address' + reference_native_interchain_token_address = 'interchain_token_address' + reference_token_manager_address = 'token_manager_address' + + [[handlers]] + type = 'StacksVerifierSetVerifier' + cosmwasm_contract = '{}' + http_url = 'http://localhost:8000' ", TMAddress::random(PREFIX), TMAddress::random(PREFIX), @@ -143,10 +156,12 @@ mod tests { TMAddress::random(PREFIX), TMAddress::random(PREFIX), TMAddress::random(PREFIX), + TMAddress::random(PREFIX), + TMAddress::random(PREFIX), ); let cfg: Config = toml::from_str(config_str.as_str()).unwrap(); - assert_eq!(cfg.handlers.len(), 10); + assert_eq!(cfg.handlers.len(), 12); } #[test] @@ -350,6 +365,22 @@ mod tests { ), rpc_url: Url::from_str("http://127.0.0.1").unwrap(), }, + HandlerConfig::StacksMsgVerifier { + cosmwasm_contract: TMAddress::from( + AccountId::new("axelar", &[0u8; 32]).unwrap(), + ), + http_url: Url::from_str("http://127.0.0.1").unwrap(), + its_address: "its_address".to_string(), + reference_native_interchain_token_address: "interchain_token_address" + .to_string(), + reference_token_manager_address: "token_manager_address".to_string(), + }, + HandlerConfig::StacksVerifierSetVerifier { + cosmwasm_contract: TMAddress::from( + AccountId::new("axelar", &[0u8; 32]).unwrap(), + ), + http_url: Url::from_str("http://127.0.0.1").unwrap(), + }, ], ..Config::default() } diff --git a/ampd/src/handlers/config.rs b/ampd/src/handlers/config.rs index c7bb9da9b..88299dfbd 100644 --- a/ampd/src/handlers/config.rs +++ b/ampd/src/handlers/config.rs @@ -63,6 +63,17 @@ pub enum Config { cosmwasm_contract: TMAddress, rpc_url: Url, }, + StacksMsgVerifier { + cosmwasm_contract: TMAddress, + http_url: Url, + its_address: String, + reference_native_interchain_token_address: String, + reference_token_manager_address: String, + }, + StacksVerifierSetVerifier { + cosmwasm_contract: TMAddress, + http_url: Url, + }, } fn validate_evm_verifier_set_verifier_configs<'de, D>(configs: &[Config]) -> Result<(), D::Error> @@ -159,6 +170,16 @@ where Config::StellarVerifierSetVerifier, "Stellar verifier set verifier" )?; + ensure_unique_config!( + &configs, + Config::StacksMsgVerifier, + "Stacks message verifier" + )?; + ensure_unique_config!( + &configs, + Config::StacksVerifierSetVerifier, + "Stacks verifier set verifier" + )?; Ok(configs) } @@ -305,5 +326,45 @@ mod tests { Err(e) if e.to_string().contains("only one Stellar verifier set verifier config is allowed") ) ); + + let configs = vec![ + Config::StacksMsgVerifier { + cosmwasm_contract: TMAddress::random(PREFIX), + http_url: "http://localhost:8080/".parse().unwrap(), + its_address: "its_address".to_string(), + reference_native_interchain_token_address: "interchain_token_address".to_string(), + reference_token_manager_address: "token_manager_address".to_string(), + }, + Config::StacksMsgVerifier { + cosmwasm_contract: TMAddress::random(PREFIX), + http_url: "http://localhost:8080/".parse().unwrap(), + its_address: "its_address".to_string(), + reference_native_interchain_token_address: "interchain_token_address".to_string(), + reference_token_manager_address: "token_manager_address".to_string(), + }, + ]; + + assert!( + matches!(deserialize_handler_configs(to_value(configs).unwrap()), + Err(e) if e.to_string().contains("only one Stacks message verifier config is allowed") + ) + ); + + let configs = vec![ + Config::StacksVerifierSetVerifier { + cosmwasm_contract: TMAddress::random(PREFIX), + http_url: "http://localhost:8080/".parse().unwrap(), + }, + Config::StacksVerifierSetVerifier { + cosmwasm_contract: TMAddress::random(PREFIX), + http_url: "http://localhost:8080/".parse().unwrap(), + }, + ]; + + assert!( + matches!(deserialize_handler_configs(to_value(configs).unwrap()), + Err(e) if e.to_string().contains("only one Stacks verifier set verifier config is allowed") + ) + ); } } diff --git a/ampd/src/handlers/mod.rs b/ampd/src/handlers/mod.rs index 1f8868164..48c6f03b5 100644 --- a/ampd/src/handlers/mod.rs +++ b/ampd/src/handlers/mod.rs @@ -5,6 +5,8 @@ pub mod evm_verify_verifier_set; pub mod multisig; pub mod mvx_verify_msg; pub mod mvx_verify_verifier_set; +pub mod stacks_verify_msg; +pub mod stacks_verify_verifier_set; pub(crate) mod stellar_verify_msg; pub(crate) mod stellar_verify_verifier_set; pub mod sui_verify_msg; diff --git a/ampd/src/handlers/stacks_verify_msg.rs b/ampd/src/handlers/stacks_verify_msg.rs new file mode 100644 index 000000000..63990fc94 --- /dev/null +++ b/ampd/src/handlers/stacks_verify_msg.rs @@ -0,0 +1,429 @@ +use std::collections::HashSet; +use std::convert::TryInto; + +use async_trait::async_trait; +use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; +use axelar_wasm_std::voting::{PollId, Vote}; +use cosmrs::cosmwasm::MsgExecuteContract; +use cosmrs::tx::Msg; +use cosmrs::Any; +use error_stack::ResultExt; +use events::Error::EventTypeMismatch; +use events::Event; +use events_derive::try_from; +use futures::future; +use router_api::ChainName; +use serde::Deserialize; +use tokio::sync::watch::Receiver; +use tracing::{info, info_span}; +use valuable::Valuable; +use voting_verifier::msg::ExecuteMsg; + +use crate::event_processor::EventHandler; +use crate::handlers::errors::Error; +use crate::stacks::http_client::Client; +use crate::stacks::verifier::verify_message; +use crate::types::{Hash, TMAddress}; + +type Result = error_stack::Result; + +#[derive(Deserialize, Debug)] +pub struct Message { + pub message_id: HexTxHashAndEventIndex, + pub destination_address: String, + pub destination_chain: ChainName, + pub source_address: String, + pub payload_hash: Hash, +} + +#[derive(Deserialize, Debug)] +#[try_from("wasm-messages_poll_started")] +struct PollStartedEvent { + poll_id: PollId, + source_chain: ChainName, + source_gateway_address: String, + messages: Vec, + participants: Vec, + expires_at: u64, +} + +pub struct Handler { + verifier: TMAddress, + voting_verifier_contract: TMAddress, + http_client: Client, + latest_block_height: Receiver, + its_address: String, + reference_native_interchain_token_code: String, + reference_token_manager_code: String, +} + +impl Handler { + pub async fn new( + verifier: TMAddress, + voting_verifier_contract: TMAddress, + http_client: Client, + latest_block_height: Receiver, + its_address: String, + reference_native_interchain_token_address: String, + reference_token_manager_address: String, + ) -> error_stack::Result { + let reference_native_interchain_token_info = http_client + .get_contract_info(reference_native_interchain_token_address.as_str()) + .await?; + + let reference_token_manager_info = http_client + .get_contract_info(reference_token_manager_address.as_str()) + .await?; + + Ok(Self { + verifier, + voting_verifier_contract, + http_client, + latest_block_height, + its_address, + reference_native_interchain_token_code: reference_native_interchain_token_info + .source_code, + reference_token_manager_code: reference_token_manager_info.source_code, + }) + } + + fn vote_msg(&self, poll_id: PollId, votes: Vec) -> MsgExecuteContract { + MsgExecuteContract { + sender: self.verifier.as_ref().clone(), + contract: self.voting_verifier_contract.as_ref().clone(), + msg: serde_json::to_vec(&ExecuteMsg::Vote { poll_id, votes }) + .expect("vote msg should serialize"), + funds: vec![], + } + } +} + +#[async_trait] +impl EventHandler for Handler { + type Err = Error; + + async fn handle(&self, event: &Event) -> Result> { + if !event.is_from_contract(self.voting_verifier_contract.as_ref()) { + return Ok(vec![]); + } + + let PollStartedEvent { + poll_id, + source_chain, + source_gateway_address, + messages, + participants, + expires_at, + .. + } = match event.try_into() as error_stack::Result<_, _> { + Err(report) if matches!(report.current_context(), EventTypeMismatch(_)) => { + return Ok(vec![]); + } + event => event.change_context(Error::DeserializeEvent)?, + }; + + if !participants.contains(&self.verifier) { + return Ok(vec![]); + } + + let latest_block_height = *self.latest_block_height.borrow(); + if latest_block_height >= expires_at { + info!(poll_id = poll_id.to_string(), "skipping expired poll"); + + return Ok(vec![]); + } + + let tx_hashes: HashSet = messages + .iter() + .map(|message| message.message_id.tx_hash.into()) + .collect(); + let transactions = self.http_client.get_transactions(tx_hashes).await; + + let message_ids = messages + .iter() + .map(|message| message.message_id.to_string()) + .collect::>(); + + let votes = info_span!( + "verify messages in poll", + poll_id = poll_id.to_string(), + source_chain = source_chain.to_string(), + message_ids = message_ids.as_value() + ) + .in_scope(|| async { + info!("ready to verify messages in poll",); + + let futures = messages.iter().map(|msg| async { + match transactions.get(&msg.message_id.tx_hash.into()) { + Some(transaction) => { + verify_message( + &source_chain, + &source_gateway_address, + &self.its_address, + transaction, + msg, + &self.http_client, + &self.reference_native_interchain_token_code, + &self.reference_token_manager_code, + ) + .await + } + None => Vote::NotFound, + } + }); + + let votes: Vec = future::join_all(futures).await; + + info!( + votes = votes.as_value(), + "ready to vote for messages in poll" + ); + + votes + }) + .await; + + Ok(vec![self + .vote_msg(poll_id, votes) + .into_any() + .expect("vote msg should serialize")]) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::convert::TryInto; + + use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; + use cosmrs::cosmwasm::MsgExecuteContract; + use cosmrs::tx::Msg; + use cosmwasm_std; + use error_stack::Result; + use tokio::sync::watch; + use tokio::test as async_test; + use voting_verifier::events::{PollMetadata, PollStarted, TxEventConfirmation}; + + use super::{Handler, Message, PollStartedEvent}; + use crate::event_processor::EventHandler; + use crate::handlers::tests::into_structured_event; + use crate::stacks::http_client::{Client, ContractInfo}; + use crate::types::{EVMAddress, Hash, TMAddress}; + use crate::PREFIX; + + #[test] + fn should_deserialize_poll_started_event() { + let event: Result = into_structured_event( + poll_started_event(participants(5, None)), + &TMAddress::random(PREFIX), + ) + .try_into(); + + assert!(event.is_ok()); + + let event = event.unwrap(); + + assert!(event.poll_id == 100u64.into()); + assert!( + event.source_gateway_address + == "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway" + ); + + let message: &Message = event.messages.first().unwrap(); + + assert!(message.message_id.event_index == 1u64); + assert!(message.destination_chain == "ethereum"); + assert!(message.source_address == "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"); + } + + // Should not handle event if it is not a poll started event + #[async_test] + async fn not_poll_started_event() { + let event = into_structured_event( + cosmwasm_std::Event::new("transfer"), + &TMAddress::random(PREFIX), + ); + + let handler = get_handler().await; + + assert!(handler.handle(&event).await.is_ok()); + } + + // Should not handle event if it is not emitted from voting verifier + #[async_test] + async fn contract_is_not_voting_verifier() { + let event = into_structured_event( + poll_started_event(participants(5, None)), + &TMAddress::random(PREFIX), + ); + + let handler = get_handler().await; + + assert!(handler.handle(&event).await.is_ok()); + } + + // Should not handle event if worker is not a poll participant + #[async_test] + async fn verifier_is_not_a_participant() { + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: "()".to_string(), + }) + }); + + let voting_verifier = TMAddress::random(PREFIX); + let event = + into_structured_event(poll_started_event(participants(5, None)), &voting_verifier); + + let handler = super::Handler::new( + TMAddress::random(PREFIX), + voting_verifier, + client, + watch::channel(0).1, + "its_address".to_string(), + "native_interchain_token_code".to_string(), + "token_manager_code".to_string(), + ) + .await + .unwrap(); + + assert!(handler.handle(&event).await.is_ok()); + } + + #[async_test] + async fn should_vote_correctly() { + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: "()".to_string(), + }) + }); + faux::when!(client.get_transactions).then(|_| HashMap::new()); + + let voting_verifier = TMAddress::random(PREFIX); + let worker = TMAddress::random(PREFIX); + let event = into_structured_event( + poll_started_event(participants(5, Some(worker.clone()))), + &voting_verifier, + ); + + let handler = super::Handler::new( + worker, + voting_verifier, + client, + watch::channel(0).1, + "its_address".to_string(), + "native_interchain_token_code".to_string(), + "token_manager_code".to_string(), + ) + .await + .unwrap(); + + let actual = handler.handle(&event).await.unwrap(); + assert_eq!(actual.len(), 1); + assert!(MsgExecuteContract::from_any(actual.first().unwrap()).is_ok()); + } + + #[async_test] + async fn should_skip_expired_poll() { + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: "()".to_string(), + }) + }); + faux::when!(client.get_transactions).then(|_| HashMap::new()); + + let voting_verifier = TMAddress::random(PREFIX); + let worker = TMAddress::random(PREFIX); + let expiration = 100u64; + let event = into_structured_event( + poll_started_event(participants(5, Some(worker.clone()))), + &voting_verifier, + ); + + let (tx, rx) = watch::channel(expiration - 1); + + let handler = super::Handler::new( + worker, + voting_verifier, + client, + rx, + "its_address".to_string(), + "native_interchain_token_code".to_string(), + "token_manager_code".to_string(), + ) + .await + .unwrap(); + + // poll is not expired yet, should hit proxy + let actual = handler.handle(&event).await.unwrap(); + assert_eq!(actual.len(), 1); + + let _ = tx.send(expiration + 1); + + // poll is expired + assert_eq!(handler.handle(&event).await.unwrap(), vec![]); + } + + async fn get_handler() -> Handler { + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: "()".to_string(), + }) + }); + + let handler = Handler::new( + TMAddress::random(PREFIX), + TMAddress::random(PREFIX), + client, + watch::channel(0).1, + "its_address".to_string(), + "native_interchain_token_code".to_string(), + "token_manager_code".to_string(), + ) + .await + .unwrap(); + + handler + } + + fn poll_started_event(participants: Vec) -> PollStarted { + let msg_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + PollStarted::Messages { + metadata: PollMetadata { + poll_id: "100".parse().unwrap(), + source_chain: "stacks".parse().unwrap(), + source_gateway_address: "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway" + .parse() + .unwrap(), + confirmation_height: 15, + expires_at: 100, + participants: participants + .into_iter() + .map(|addr| cosmwasm_std::Addr::unchecked(addr.to_string())) + .collect(), + }, + #[allow(deprecated)] // TODO: The below events use the deprecated tx_id and event_index fields. Remove this attribute when those fields are removed + messages: vec![TxEventConfirmation { + tx_id: msg_id.tx_hash_as_hex(), + event_index: u32::try_from(msg_id.event_index).unwrap(), + message_id: msg_id.to_string().parse().unwrap(), + source_address: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".parse().unwrap(), + destination_chain: "ethereum".parse().unwrap(), + destination_address: format!("0x{:x}", EVMAddress::random()).parse().unwrap(), + payload_hash: Hash::random().to_fixed_bytes(), + }], + } + } + + fn participants(n: u8, worker: Option) -> Vec { + (0..n) + .map(|_| TMAddress::random(PREFIX)) + .chain(worker) + .collect() + } +} diff --git a/ampd/src/handlers/stacks_verify_verifier_set.rs b/ampd/src/handlers/stacks_verify_verifier_set.rs new file mode 100644 index 000000000..051b6444d --- /dev/null +++ b/ampd/src/handlers/stacks_verify_verifier_set.rs @@ -0,0 +1,352 @@ +use std::convert::TryInto; + +use async_trait::async_trait; +use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; +use axelar_wasm_std::voting::{PollId, Vote}; +use cosmrs::cosmwasm::MsgExecuteContract; +use cosmrs::tx::Msg; +use cosmrs::Any; +use error_stack::ResultExt; +use events::Error::EventTypeMismatch; +use events::Event; +use events_derive::try_from; +use multisig::verifier_set::VerifierSet; +use serde::Deserialize; +use tokio::sync::watch::Receiver; +use tracing::{info, info_span}; +use valuable::Valuable; +use voting_verifier::msg::ExecuteMsg; + +use crate::event_processor::EventHandler; +use crate::handlers::errors::Error; +use crate::stacks::http_client::Client; +use crate::stacks::verifier::verify_verifier_set; +use crate::types::TMAddress; + +#[derive(Deserialize, Debug)] +pub struct VerifierSetConfirmation { + pub message_id: HexTxHashAndEventIndex, + pub verifier_set: VerifierSet, +} + +#[derive(Deserialize, Debug)] +#[try_from("wasm-verifier_set_poll_started")] +struct PollStartedEvent { + poll_id: PollId, + source_gateway_address: String, + verifier_set: VerifierSetConfirmation, + participants: Vec, + expires_at: u64, +} + +pub struct Handler { + verifier: TMAddress, + voting_verifier_contract: TMAddress, + http_client: Client, + latest_block_height: Receiver, +} + +impl Handler { + pub fn new( + verifier: TMAddress, + voting_verifier_contract: TMAddress, + http_client: Client, + latest_block_height: Receiver, + ) -> Self { + Self { + verifier, + voting_verifier_contract, + http_client, + latest_block_height, + } + } + + fn vote_msg(&self, poll_id: PollId, vote: Vote) -> MsgExecuteContract { + MsgExecuteContract { + sender: self.verifier.as_ref().clone(), + contract: self.voting_verifier_contract.as_ref().clone(), + msg: serde_json::to_vec(&ExecuteMsg::Vote { + poll_id, + votes: vec![vote], + }) + .expect("vote msg should serialize"), + funds: vec![], + } + } +} + +#[async_trait] +impl EventHandler for Handler { + type Err = Error; + + async fn handle(&self, event: &Event) -> error_stack::Result, Error> { + if !event.is_from_contract(self.voting_verifier_contract.as_ref()) { + return Ok(vec![]); + } + + let PollStartedEvent { + poll_id, + source_gateway_address, + verifier_set, + participants, + expires_at, + .. + } = match event.try_into() as error_stack::Result<_, _> { + Err(report) if matches!(report.current_context(), EventTypeMismatch(_)) => { + return Ok(vec![]); + } + event => event.change_context(Error::DeserializeEvent)?, + }; + + if !participants.contains(&self.verifier) { + return Ok(vec![]); + } + + let latest_block_height = *self.latest_block_height.borrow(); + if latest_block_height >= expires_at { + info!(poll_id = poll_id.to_string(), "skipping expired poll"); + return Ok(vec![]); + } + + let transaction = self + .http_client + .get_valid_transaction(&verifier_set.message_id.tx_hash.into()) + .await; + + let vote = info_span!( + "verify a new verifier set for Stacks", + poll_id = poll_id.to_string(), + id = verifier_set.message_id.to_string(), + ) + .in_scope(|| { + info!("ready to verify a new worker set in poll"); + + let vote = transaction.map_or(Vote::NotFound, |transaction| { + verify_verifier_set(&source_gateway_address, &transaction, verifier_set) + }); + info!( + vote = vote.as_value(), + "ready to vote for a new worker set in poll" + ); + + vote + }); + + Ok(vec![self + .vote_msg(poll_id, vote) + .into_any() + .expect("vote msg should serialize")]) + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; + use cosmrs::cosmwasm::MsgExecuteContract; + use cosmrs::tx::Msg; + use cosmwasm_std; + use cosmwasm_std::{HexBinary, Uint128}; + use error_stack::Result; + use multisig::key::KeyType; + use multisig::test::common::{build_verifier_set, ecdsa_test_data}; + use tokio::sync::watch; + use tokio::test as async_test; + use voting_verifier::events::{PollMetadata, PollStarted, VerifierSetConfirmation}; + + use super::PollStartedEvent; + use crate::event_processor::EventHandler; + use crate::handlers::tests::into_structured_event; + use crate::stacks::http_client::Client; + use crate::types::{Hash, TMAddress}; + use crate::PREFIX; + + #[test] + fn should_deserialize_verifier_set_poll_started_event() { + let event: Result = into_structured_event( + verifier_set_poll_started_event(participants(5, None), 100), + &TMAddress::random(PREFIX), + ) + .try_into(); + + assert!(event.is_ok()); + + let event = event.unwrap(); + + assert!(event.poll_id == 100u64.into()); + assert!( + event.source_gateway_address + == "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway" + ); + + let verifier_set = event.verifier_set; + + assert!(verifier_set.message_id.event_index == 1u64); + assert!(verifier_set.verifier_set.signers.len() == 3); + assert_eq!(verifier_set.verifier_set.threshold, Uint128::from(2u128)); + + let mut signers = verifier_set.verifier_set.signers.values(); + let signer1 = signers.next().unwrap(); + let signer2 = signers.next().unwrap(); + + assert_eq!( + signer1.pub_key.as_ref(), + HexBinary::from_hex( + "025e0231bfad810e5276e2cf9eb2f3f380ce0bdf6d84c3b6173499d3ddcc008856", + ) + .unwrap() + .as_ref() + ); + assert_eq!(signer1.weight, Uint128::from(1u128)); + + assert_eq!( + signer2.pub_key.as_ref(), + HexBinary::from_hex( + "036ff6f4b2bc5e08aba924bd8fd986608f3685ca651a015b3d9d6a656de14769fe", + ) + .unwrap() + .as_ref() + ); + assert_eq!(signer2.weight, Uint128::from(1u128)); + } + + #[async_test] + async fn not_poll_started_event() { + let event = into_structured_event( + cosmwasm_std::Event::new("transfer"), + &TMAddress::random(PREFIX), + ); + + let handler = super::Handler::new( + TMAddress::random(PREFIX), + TMAddress::random(PREFIX), + Client::faux(), + watch::channel(0).1, + ); + + assert_eq!(handler.handle(&event).await.unwrap(), vec![]); + } + + #[async_test] + async fn contract_is_not_voting_verifier() { + let event = into_structured_event( + verifier_set_poll_started_event(participants(5, None), 100), + &TMAddress::random(PREFIX), + ); + + let handler = super::Handler::new( + TMAddress::random(PREFIX), + TMAddress::random(PREFIX), + Client::faux(), + watch::channel(0).1, + ); + + assert_eq!(handler.handle(&event).await.unwrap(), vec![]); + } + + #[async_test] + async fn verifier_is_not_a_participant() { + let voting_verifier = TMAddress::random(PREFIX); + let event = into_structured_event( + verifier_set_poll_started_event(participants(5, None), 100), + &voting_verifier, + ); + + let handler = super::Handler::new( + TMAddress::random(PREFIX), + voting_verifier, + Client::faux(), + watch::channel(0).1, + ); + + assert_eq!(handler.handle(&event).await.unwrap(), vec![]); + } + + #[async_test] + async fn should_skip_expired_poll() { + let mut client = Client::faux(); + faux::when!(client.get_valid_transaction).then(|_| None); + + let voting_verifier = TMAddress::random(PREFIX); + let verifier = TMAddress::random(PREFIX); + let expiration = 100u64; + let event = into_structured_event( + verifier_set_poll_started_event( + vec![verifier.clone()].into_iter().collect(), + expiration, + ), + &voting_verifier, + ); + + let (tx, rx) = watch::channel(expiration - 1); + + let handler = super::Handler::new(verifier, voting_verifier, client, rx); + + // poll is not expired yet, should hit proxy + let actual = handler.handle(&event).await.unwrap(); + assert_eq!(actual.len(), 1); + + let _ = tx.send(expiration + 1); + + // poll is expired + assert_eq!(handler.handle(&event).await.unwrap(), vec![]); + } + + #[async_test] + async fn should_vote_correctly() { + let mut client = Client::faux(); + faux::when!(client.get_valid_transaction).then(|_| None); + + let voting_verifier = TMAddress::random(PREFIX); + let worker = TMAddress::random(PREFIX); + + let event = into_structured_event( + verifier_set_poll_started_event(participants(5, Some(worker.clone())), 100), + &voting_verifier, + ); + + let handler = super::Handler::new(worker, voting_verifier, client, watch::channel(0).1); + + let actual = handler.handle(&event).await.unwrap(); + assert_eq!(actual.len(), 1); + assert!(MsgExecuteContract::from_any(actual.first().unwrap()).is_ok()); + } + + fn verifier_set_poll_started_event( + participants: Vec, + expires_at: u64, + ) -> PollStarted { + let msg_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + PollStarted::VerifierSet { + metadata: PollMetadata { + poll_id: "100".parse().unwrap(), + source_chain: "multiversx".parse().unwrap(), + source_gateway_address: "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway" + .parse() + .unwrap(), + confirmation_height: 15, + expires_at, + participants: participants + .into_iter() + .map(|addr| cosmwasm_std::Addr::unchecked(addr.to_string())) + .collect(), + }, + #[allow(deprecated)] // TODO: The below events use the deprecated tx_id and event_index fields. Remove this attribute when those fields are removed + verifier_set: VerifierSetConfirmation { + tx_id: msg_id.tx_hash_as_hex(), + event_index: u32::try_from(msg_id.event_index).unwrap(), + message_id: msg_id.to_string().parse().unwrap(), + verifier_set: build_verifier_set(KeyType::Ecdsa, &ecdsa_test_data::signers()), + }, + } + } + + fn participants(n: u8, worker: Option) -> Vec { + (0..n) + .map(|_| TMAddress::random(PREFIX)) + .chain(worker) + .collect() + } +} diff --git a/ampd/src/lib.rs b/ampd/src/lib.rs index 75d258504..1d029b1be 100644 --- a/ampd/src/lib.rs +++ b/ampd/src/lib.rs @@ -40,6 +40,7 @@ mod health_check; mod json_rpc; mod mvx; mod queue; +mod stacks; mod stellar; mod sui; mod tm_client; @@ -51,6 +52,7 @@ pub use grpc::{client, proto}; use crate::asyncutil::future::RetryPolicy; use crate::broadcaster::confirm_tx::TxConfirmer; +use crate::stacks::http_client::Client; const PREFIX: &str = "axelar"; const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(3); @@ -388,6 +390,40 @@ where ), event_processor_config.clone(), ), + handlers::config::Config::StacksMsgVerifier { + cosmwasm_contract, + http_url, + its_address, + reference_native_interchain_token_address, + reference_token_manager_address, + } => self.create_handler_task( + "stacks-msg-verifier", + handlers::stacks_verify_msg::Handler::new( + verifier.clone(), + cosmwasm_contract, + Client::new_http(http_url.to_string().trim_end_matches('/').into()), + self.block_height_monitor.latest_block_height(), + its_address, + reference_native_interchain_token_address, + reference_token_manager_address, + ) + .await + .change_context(Error::Connection)?, + event_processor_config.clone(), + ), + handlers::config::Config::StacksVerifierSetVerifier { + cosmwasm_contract, + http_url, + } => self.create_handler_task( + "stacks-verifier-set-verifier", + handlers::stacks_verify_verifier_set::Handler::new( + verifier.clone(), + cosmwasm_contract, + Client::new_http(http_url.to_string().trim_end_matches('/').into()), + self.block_height_monitor.latest_block_height(), + ), + event_processor_config.clone(), + ), }; self.event_processor = self.event_processor.add_task(task); } diff --git a/ampd/src/stacks/error.rs b/ampd/src/stacks/error.rs new file mode 100644 index 000000000..af270acab --- /dev/null +++ b/ampd/src/stacks/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("required property is empty")] + PropertyEmpty, + #[error("invalid encoding")] + InvalidEncoding, + #[error("provided key is not ecdsa")] + NotEcdsaKey, + #[error("contract call is invalid")] + InvalidCall, +} diff --git a/ampd/src/stacks/http_client.rs b/ampd/src/stacks/http_client.rs new file mode 100644 index 000000000..0d8856b0f --- /dev/null +++ b/ampd/src/stacks/http_client.rs @@ -0,0 +1,290 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; + +use futures::future::join_all; +use hex::ToHex; +use serde::Deserialize; +use thiserror::Error; + +use crate::types::Hash; + +const GET_TRANSACTION: &str = "extended/v1/tx/0x"; +const GET_CONTRACT_INFO: &str = "extended/v1/contract/"; + +const STATUS_SUCCESS: &str = "success"; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to create client")] + Client, + #[error("invalid tx hash")] + TxHash, + #[error("invalid contract")] + Contract, +} + +#[derive(Debug, Deserialize, Default)] +pub struct ContractLogValue { + pub hex: String, +} + +#[derive(Debug, Deserialize, Default)] +pub struct ContractLog { + pub contract_id: String, + pub topic: String, + pub value: ContractLogValue, +} + +#[derive(Debug, Deserialize, Default)] +pub struct TransactionEvents { + pub event_index: u64, + pub tx_id: String, + pub contract_log: Option, +} + +#[derive(Debug, Deserialize, Default)] +pub struct Transaction { + pub tx_id: Hash, + pub nonce: u64, + pub sender_address: String, + pub tx_status: String, // 'success' + pub events: Vec, +} + +#[derive(Debug, Deserialize, Default)] +pub struct ContractInfo { + pub source_code: String, +} + +#[cfg_attr(test, faux::create)] +pub struct Client { + api_url: String, + client: reqwest::Client, +} + +#[cfg_attr(test, faux::methods)] +impl Client { + pub fn new_http(api_url: String) -> Self { + Client { + api_url, + client: reqwest::Client::new(), + } + } + + pub async fn get_transactions(&self, tx_hashes: HashSet) -> HashMap { + let tx_hashes = Vec::from_iter(tx_hashes); + + let txs = join_all( + tx_hashes + .iter() + .map(|tx_hash| self.get_valid_transaction(tx_hash)), + ) + .await; + + tx_hashes + .into_iter() + .zip(txs) + .filter_map(|(hash, tx)| { + tx.as_ref()?; + + Some((hash, tx.unwrap())) + }) + .collect() + } + + pub async fn get_valid_transaction(&self, tx_hash: &Hash) -> Option { + self.get_transaction(tx_hash.encode_hex::().as_str()) + .await + .ok() + .filter(Self::is_valid_transaction) + } + + async fn get_transaction(&self, tx_id: &str) -> Result { + let endpoint = GET_TRANSACTION.to_string() + tx_id; + + let endpoint = self.get_endpoint(endpoint.as_str()); + + self.client + .get(endpoint) + .send() + .await + .map_err(|_| Error::TxHash)? + .json::() + .await + .map_err(|_| Error::Client) + } + + pub async fn get_contract_info(&self, contract_id: &str) -> Result { + let endpoint = GET_CONTRACT_INFO.to_string() + contract_id; + + let endpoint = self.get_endpoint(endpoint.as_str()); + + self.client + .get(endpoint) + .send() + .await + .map_err(|_| Error::Contract)? + .json::() + .await + .map_err(|_| Error::Client) + } + + fn get_endpoint(&self, endpoint: &str) -> String { + format!("{}/{}", self.api_url, endpoint) + } + + fn is_valid_transaction(tx: &Transaction) -> bool { + tx.tx_status == *STATUS_SUCCESS + } +} + +#[cfg(test)] +mod tests { + use super::{Client, Transaction}; + + #[test] + fn parse_transaction() { + let data = r#" +{ + "tx_id": "0xee0049faf8dde5507418140ed72bd64f73cc001b08de98e0c16a3a8d9f2c38cf", + "nonce": 2, + "fee_rate": "943", + "sender_address": "SP3F7B2PGN7TVMTNBS1HBJBEC6M64DMCY944MXDD0", + "sponsored": false, + "post_condition_mode": "deny", + "post_conditions": [], + "anchor_mode": "any", + "block_hash": "0x9248a412fc98e245820160aba1f89defefe5380af920bff73bc6617207284aa9", + "block_height": 168868, + "block_time": 1728309360, + "block_time_iso": "2024-10-07T13:56:00.000Z", + "burn_block_time": 1728309301, + "burn_block_height": 864594, + "burn_block_time_iso": "2024-10-07T13:55:01.000Z", + "parent_burn_block_time": 1728308843, + "parent_burn_block_time_iso": "2024-10-07T13:47:23.000Z", + "canonical": true, + "tx_index": 85, + "tx_status": "success", + "tx_result": { + "hex": "0x0703", + "repr": "(ok true)" + }, + "event_count": 1, + "parent_block_hash": "0x1cbb43f502524bfa0edbb16b5f2a98350de6d8041c93dd54eab35347a90f6a68", + "is_unanchored": false, + "microblock_hash": "0x", + "microblock_sequence": 2147483647, + "microblock_canonical": true, + "execution_cost_read_count": 6, + "execution_cost_read_length": 13939, + "execution_cost_runtime": 46110, + "execution_cost_write_count": 1, + "execution_cost_write_length": 125, + "events": [ + { + "event_index": 0, + "event_type": "smart_contract_log", + "tx_id": "0xee0049faf8dde5507418140ed72bd64f73cc001b08de98e0c16a3a8d9f2c38cf", + "contract_log": { + "contract_id": "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.leo-cats", + "topic": "print", + "value": { + "hex": "0x0c0000000501610d0000000c6c6973742d696e2d757374780a636f6d6d697373696f6e06162bcf9762d5b90bc36dc1b4759b1727690f92ddd31367616d6d612d636f6d6d697373696f6e2d76310269640100000000000000000000000000000d7105707269636501000000000000000000000000004e89b307726f79616c74790100000000000000000000000000000000", + "repr": "(tuple (a \"list-in-ustx\") (commission 'SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.gamma-commission-v1) (id u3441) (price u5147059) (royalty u0))" + } + } + }, + { + "event_index": 1, + "event_type": "fungible_token_asset", + "tx_id": "0xea34df6d263a274ec852b04f3d9bc13b989811f263c58e02293504c3e66164fd", + "asset": { + "asset_event_type": "transfer", + "asset_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.brc20-db20::brc20-db20", + "sender": "SPP2B792YYNWTM5W8N3TBJT51745K8HPSCP9EFTT", + "recipient": "SP38AN2F75Y4AP8ZVA7402XPK77F82TBQX05R8EH6", + "amount": "1548865732" + } + } + ], + "tx_type": "contract_call", + "contract_call": { + "contract_id": "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.leo-cats", + "function_name": "list-in-ustx", + "function_signature": "(define-public (list-in-ustx (id uint) (price uint) (comm-trait trait_reference)))", + "function_args": [ + { + "hex": "0x0100000000000000000000000000000d71", + "repr": "u3441", + "name": "id", + "type": "uint" + } + ] + } +} + "#; + + let transaction = serde_json::from_str::(data).unwrap(); + assert_eq!( + transaction.tx_id, + "0xee0049faf8dde5507418140ed72bd64f73cc001b08de98e0c16a3a8d9f2c38cf" + .parse() + .unwrap() + ); + assert_eq!(transaction.nonce, 2); + assert_eq!( + transaction.sender_address, + "SP3F7B2PGN7TVMTNBS1HBJBEC6M64DMCY944MXDD0" + ); + assert_eq!(transaction.tx_status, "success"); + assert_eq!(transaction.events.len(), 2); + + let event = transaction.events.get(0).unwrap(); + + assert_eq!(event.event_index, 0); + assert_eq!( + event.tx_id, + "0xee0049faf8dde5507418140ed72bd64f73cc001b08de98e0c16a3a8d9f2c38cf" + ); + assert!(event.contract_log.is_some()); + + let contract_log = event.contract_log.as_ref().unwrap(); + + assert_eq!( + contract_log.contract_id, + "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.leo-cats" + ); + assert_eq!(contract_log.topic, "print"); + assert_eq!(contract_log.value.hex, "0x0c0000000501610d0000000c6c6973742d696e2d757374780a636f6d6d697373696f6e06162bcf9762d5b90bc36dc1b4759b1727690f92ddd31367616d6d612d636f6d6d697373696f6e2d76310269640100000000000000000000000000000d7105707269636501000000000000000000000000004e89b307726f79616c74790100000000000000000000000000000000"); + + let token_event = transaction.events.get(1).unwrap(); + + assert_eq!(token_event.event_index, 1); + assert_eq!( + token_event.tx_id, + "0xea34df6d263a274ec852b04f3d9bc13b989811f263c58e02293504c3e66164fd" + ); + assert!(token_event.contract_log.is_none()); + } + + #[test] + fn should_not_be_valid_transaction_invalid_status() { + let tx = Transaction { + tx_status: "pending".into(), + ..Transaction::default() + }; + + assert!(!Client::is_valid_transaction(&tx)); + } + + #[test] + fn should_be_valid_transaction() { + let tx = Transaction { + tx_status: "success".into(), + ..Transaction::default() + }; + + assert!(Client::is_valid_transaction(&tx)); + } +} diff --git a/ampd/src/stacks/its_verifier.rs b/ampd/src/stacks/its_verifier.rs new file mode 100644 index 000000000..90b7fea77 --- /dev/null +++ b/ampd/src/stacks/its_verifier.rs @@ -0,0 +1,886 @@ +use clarity::codec::StacksMessageCodec; +use clarity::vm::types::{ + BufferLength, SequenceSubtype, StringSubtype, TupleData, TupleTypeSignature, TypeSignature, +}; +use clarity::vm::{ClarityName, Value}; +use ethers_core::abi::{encode, Token}; +use sha3::{Digest, Keccak256}; + +use crate::stacks::error::Error; +use crate::stacks::http_client::{Client, TransactionEvents}; +use crate::types::Hash; + +const MESSAGE_TYPE_INTERCHAIN_TRANSFER: u128 = 0; +const MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN: u128 = 1; +const MESSAGE_TYPE_SEND_TO_HUB: u128 = 3; + +const VERIFY_INTERCHAIN_TOKEN: &str = "verify-interchain-token"; +const VERIFY_TOKEN_MANAGER: &str = "verify-token-manager"; + +pub fn get_its_hub_payload_hash( + event: &TransactionEvents, +) -> Result> { + let tuple_data = get_its_hub_call_params(event)?; + + // All messages should go through ITS hub + if !tuple_data + .get("type")? + .eq(&Value::UInt(MESSAGE_TYPE_SEND_TO_HUB)) + { + return Err(Error::InvalidCall.into()); + } + + let destination_chain = tuple_data + .get("destination-chain")? + .clone() + .expect_ascii()?; + let payload = tuple_data.get_owned("payload")?.expect_buff(63_000)?; + + let subtuple_type_signature = + TupleTypeSignature::try_from(vec![(ClarityName::from("type"), TypeSignature::UIntType)])?; + + let original_its_call = Value::try_deserialize_bytes( + &payload, + &TypeSignature::TupleType(subtuple_type_signature), + true, + )? + .expect_tuple()?; + + let its_type = original_its_call.get_owned("type")?.expect_u128()?; + + let abi_payload = match its_type { + MESSAGE_TYPE_INTERCHAIN_TRANSFER => get_its_interchain_transfer_abi_payload(payload), + MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN => { + get_its_deploy_interchain_token_abi_payload(payload) + } + _ => { + return Err(Error::InvalidCall.into()); + } + }?; + + // Convert to ITS payload and use its hash to verify the message + let abi_payload = encode(&[ + Token::Uint(MESSAGE_TYPE_SEND_TO_HUB.into()), + Token::String(destination_chain), + Token::Bytes(abi_payload), + ]); + + let payload_hash: [u8; 32] = Keccak256::digest(abi_payload).into(); + + Ok(payload_hash.into()) +} + +fn get_its_hub_call_params( + event: &TransactionEvents, +) -> Result> { + let payload = get_payload_from_contract_call_event(event)?; + + let its_send_to_hub_signature = TupleTypeSignature::try_from(vec![ + (ClarityName::from("type"), TypeSignature::UIntType), + ( + ClarityName::from("destination-chain"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(20u32)?, + ))), + ), + ( + ClarityName::from("payload"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 63_000u32, + )?)), + ), + ])?; + + let its_hub_value = Value::try_deserialize_bytes( + &payload, + &TypeSignature::TupleType(its_send_to_hub_signature), + true, + )? + .expect_tuple()?; + + Ok(its_hub_value) +} + +fn get_payload_from_contract_call_event( + event: &TransactionEvents, +) -> Result, Box> { + let contract_log = event.contract_log.as_ref().ok_or(Error::PropertyEmpty)?; + + let contract_call_signature = TupleTypeSignature::try_from(vec![( + ClarityName::from("payload"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 64_000u32, + )?)), + )])?; + + let hex = contract_log + .value + .hex + .strip_prefix("0x") + .ok_or(Error::PropertyEmpty)?; + + let contract_call_value = Value::try_deserialize_hex( + hex, + &TypeSignature::TupleType(contract_call_signature), + true, + )?; + + let payload = contract_call_value + .expect_tuple()? + .get_owned("payload")? + .expect_buff(64_000)?; + + Ok(payload) +} + +fn get_its_interchain_transfer_abi_payload( + payload: Vec, +) -> Result, Box> { + let tuple_type_signature = TupleTypeSignature::try_from(vec![ + ( + ClarityName::from("token-id"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 32u32, + )?)), + ), + ( + ClarityName::from("source-address"), + TypeSignature::PrincipalType, + ), + ( + ClarityName::from("destination-address"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 128u32, + )?)), + ), + (ClarityName::from("amount"), TypeSignature::UIntType), + ( + ClarityName::from("data"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 62_000u32, + )?)), + ), + ])?; + + let mut original_value = Value::try_deserialize_bytes( + &payload, + &TypeSignature::TupleType(tuple_type_signature), + true, + )? + .expect_tuple()?; + + let abi_payload = encode(&[ + Token::Uint(MESSAGE_TYPE_INTERCHAIN_TRANSFER.into()), + Token::FixedBytes( + original_value + .data_map + .remove("token-id") + .ok_or(Error::InvalidCall)? + .expect_buff(32)?, + ), + Token::Bytes( + original_value + .data_map + .remove("source-address") + .ok_or(Error::InvalidCall)? + .expect_principal()? + .serialize_to_vec(), + ), + Token::Bytes( + original_value + .data_map + .remove("destination-address") + .ok_or(Error::InvalidCall)? + .expect_buff(128)?, + ), + Token::Uint( + original_value + .data_map + .remove("amount") + .ok_or(Error::InvalidCall)? + .expect_u128()? + .into(), + ), + Token::Bytes( + original_value + .data_map + .remove("data") + .ok_or(Error::InvalidCall)? + .expect_buff(62_000)?, + ), + ]); + + Ok(abi_payload) +} + +fn get_its_deploy_interchain_token_abi_payload( + payload: Vec, +) -> Result, Box> { + let tuple_type_signature = TupleTypeSignature::try_from(vec![ + ( + ClarityName::from("token-id"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 32u32, + )?)), + ), + ( + ClarityName::from("name"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(32u32)?, + ))), + ), + ( + ClarityName::from("symbol"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(32u32)?, + ))), + ), + (ClarityName::from("decimals"), TypeSignature::UIntType), + ( + ClarityName::from("minter"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 128u32, + )?)), + ), + ])?; + + let mut original_value = Value::try_deserialize_bytes( + &payload, + &TypeSignature::TupleType(tuple_type_signature), + true, + )? + .expect_tuple()?; + + let abi_payload = encode(&[ + Token::Uint(MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN.into()), + Token::FixedBytes( + original_value + .data_map + .remove("token-id") + .ok_or(Error::InvalidCall)? + .expect_buff(32)?, + ), + Token::String( + original_value + .data_map + .remove("name") + .ok_or(Error::InvalidCall)? + .expect_ascii()?, + ), + Token::String( + original_value + .data_map + .remove("symbol") + .ok_or(Error::InvalidCall)? + .expect_ascii()?, + ), + Token::Uint( + original_value + .data_map + .remove("decimals") + .ok_or(Error::InvalidCall)? + .expect_u128()? + .into(), + ), + Token::Bytes( + original_value + .data_map + .remove("minter") + .ok_or(Error::InvalidCall)? + .expect_buff(128)?, + ), + ]); + + Ok(abi_payload) +} + +pub async fn its_verify_contract_code( + event: &TransactionEvents, + http_client: &Client, + reference_native_interchain_token_code: &String, + reference_token_manager_code: &String, +) -> Result> { + let (payload, verify_type) = get_its_verify_call_params(event)?; + + match verify_type.as_str() { + VERIFY_INTERCHAIN_TOKEN => { + return its_verify_interchain_token( + payload, + http_client, + reference_native_interchain_token_code, + ) + .await; + } + VERIFY_TOKEN_MANAGER => { + return its_verify_token_manager(payload, http_client, reference_token_manager_code) + .await; + } + _ => {} + } + + Ok(false) +} + +fn get_its_verify_call_params( + event: &TransactionEvents, +) -> Result<(Vec, String), Box> { + let payload = get_payload_from_contract_call_event(event)?; + + let verify_type_signature = TupleTypeSignature::try_from(vec![( + ClarityName::from("type"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(23u32)?, + ))), + )])?; + + let verify_type = Value::try_deserialize_bytes( + &payload, + &TypeSignature::TupleType(verify_type_signature), + true, + )? + .expect_tuple()? + .get_owned("type")? + .expect_ascii()?; + + Ok((payload, verify_type)) +} + +async fn its_verify_interchain_token( + payload: Vec, + http_client: &Client, + reference_native_interchain_token: &String, +) -> Result> { + let tuple_type_signature = TupleTypeSignature::try_from(vec![( + ClarityName::from("token-address"), + TypeSignature::PrincipalType, + )])?; + + let mut value = Value::try_deserialize_bytes( + &payload, + &TypeSignature::TupleType(tuple_type_signature), + true, + )? + .expect_tuple()?; + + let token_address = value + .data_map + .remove("token-address") + .ok_or(Error::InvalidCall)? + .expect_principal()?; + + let token_info = http_client + .get_contract_info(format!("{}", token_address).as_str()) + .await?; + + Ok(&token_info.source_code == reference_native_interchain_token) +} + +async fn its_verify_token_manager( + payload: Vec, + http_client: &Client, + reference_token_manager_code: &String, +) -> Result> { + let tuple_type_signature = TupleTypeSignature::try_from(vec![( + ClarityName::from("token-manager-address"), + TypeSignature::PrincipalType, + )])?; + + let mut value = Value::try_deserialize_bytes( + &payload, + &TypeSignature::TupleType(tuple_type_signature), + true, + )? + .expect_tuple()?; + + let token_manager_address = value + .data_map + .remove("token-manager-address") + .ok_or(Error::InvalidCall)? + .expect_principal()?; + + let token_manager_info = http_client + .get_contract_info(format!("{}", token_manager_address).as_str()) + .await?; + + Ok(&token_manager_info.source_code == reference_token_manager_code) +} + +#[cfg(test)] +mod tests { + use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; + use axelar_wasm_std::voting::Vote; + use router_api::ChainName; + use tokio::test as async_test; + + use crate::handlers::stacks_verify_msg::Message; + use crate::stacks::http_client::{ + Client, ContractInfo, ContractLog, ContractLogValue, Transaction, TransactionEvents, + }; + use crate::stacks::verifier::verify_message; + use crate::types::Hash; + + // test verify message its hub + #[async_test] + async fn should_not_verify_its_hub_interchain_transfer_invalid_payload_hash() { + let (source_chain, gateway_address, its_address, tx, mut msg) = + get_matching_its_hub_interchain_transfer_msg_and_tx(); + + msg.payload_hash = "0xaa38573718f5cd6d7e5a90adcdebd28b097f99574ad6febffea9a40adb17f4aa" + .parse() + .unwrap(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_verify_msg_its_hub_interchain_transfer() { + let (source_chain, gateway_address, its_address, tx, msg) = + get_matching_its_hub_interchain_transfer_msg_and_tx(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::SucceededOnChain + ); + } + + #[async_test] + async fn should_not_verify_its_hub_deploy_interchain_token_invalid_payload_hash() { + let (source_chain, gateway_address, its_address, tx, mut msg) = + get_matching_its_hub_deploy_interchain_token_msg_and_tx(); + + msg.payload_hash = "0xaa38573718f5cd6d7e5a90adcdebd28b097f99574ad6febffea9a40adb17f4aa" + .parse() + .unwrap(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_verify_msg_its_hub_deploy_interchain_token() { + let (source_chain, gateway_address, its_address, tx, msg) = + get_matching_its_hub_deploy_interchain_token_msg_and_tx(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::SucceededOnChain + ); + } + + #[async_test] + async fn should_not_verify_msg_its_verify_interchain_token_invalid_contract_code() { + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: "()".to_string(), + }) + }); + + let (source_chain, gateway_address, its_address, tx, msg) = + get_matching_its_verify_interchain_token_msg_and_tx(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &client, + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_verify_msg_its_verify_interchain_token() { + let source_code = "native_interchain_token_code"; + + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: source_code.to_string(), + }) + }); + + let (source_chain, gateway_address, its_address, tx, msg) = + get_matching_its_verify_interchain_token_msg_and_tx(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &client, + &source_code.to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::SucceededOnChain + ); + } + + #[async_test] + async fn should_not_verify_msg_its_verify_token_manager_invalid_contract_code() { + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: "()".to_string(), + }) + }); + + let (source_chain, gateway_address, its_address, tx, msg) = + get_matching_its_verify_token_manager_msg_and_tx(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &client, + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_verify_msg_its_verify_token_manager() { + let source_code = "token_manager_code"; + + let mut client = Client::faux(); + faux::when!(client.get_contract_info).then(|_| { + Ok(ContractInfo { + source_code: source_code.to_string(), + }) + }); + + let (source_chain, gateway_address, its_address, tx, msg) = + get_matching_its_verify_token_manager_msg_and_tx(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &client, + &"native_interchain_token_code".to_string(), + &source_code.to_string(), + ) + .await, + Vote::SucceededOnChain + ); + } + + fn get_matching_its_hub_interchain_transfer_msg_and_tx( + ) -> (ChainName, String, String, Transaction, Message) { + let source_chain = "stacks"; + let gateway_address = "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway"; + let its_address = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.interchain-token-service"; + + let message_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + let msg = Message { + message_id: message_id.clone(), + source_address: its_address.to_string(), + destination_chain: "axelar".parse().unwrap(), + destination_address: "cosmwasm".to_string(), + payload_hash: "0x99cdb5935274c6a59d3ce9cd6c47b58acc0ef461d6b3cab7162c2842c182b94a" + .parse() + .unwrap(), + }; + + let wrong_event = TransactionEvents { + event_index: 0, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: None, + }; + + /* + payload is: + { + type: u3, + destination-chain: "ethereum", + payload: { + type: u0, + token-id: 0x753306c46380848b5189cd9db90107b15d25decccd93dcb175c0098958f18b6f, + source-address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, + destination-address: 0x00, + amount: u100000, + data: 0x00 + } + } + */ + let event = TransactionEvents { + event_index: 1, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: Some(ContractLog { + contract_id: gateway_address.to_string(), + topic: "print".to_string(), + value: ContractLogValue { + hex: "0x0c000000061164657374696e6174696f6e2d636861696e0d000000066178656c61721c64657374696e6174696f6e2d636f6e74726163742d616464726573730d00000008636f736d7761736d077061796c6f616402000000f20c000000031164657374696e6174696f6e2d636861696e0d00000008657468657265756d077061796c6f616402000000ab0c0000000606616d6f756e7401000000000000000000000000000186a004646174610200000001001364657374696e6174696f6e2d616464726573730200000001000e736f757263652d61646472657373051a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce08746f6b656e2d69640200000020753306c46380848b5189cd9db90107b15d25decccd93dcb175c0098958f18b6f04747970650100000000000000000000000000000000047479706501000000000000000000000000000000030c7061796c6f61642d6861736802000000203dc0763c57c9c7912d2c072718e6ef2ae2d595ce2da31d8b248205d67ad7c3ab0673656e646572061a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce18696e746572636861696e2d746f6b656e2d7365727669636504747970650d0000000d636f6e74726163742d63616c6c".to_string(), + } + }), + }; + + let transaction = Transaction { + tx_id: message_id.tx_hash.into(), + nonce: 1, + sender_address: "whatever".to_string(), + tx_status: "success".to_string(), + events: vec![wrong_event, event], + }; + + ( + source_chain.parse().unwrap(), + gateway_address.to_string(), + its_address.to_string(), + transaction, + msg, + ) + } + + fn get_matching_its_hub_deploy_interchain_token_msg_and_tx( + ) -> (ChainName, String, String, Transaction, Message) { + let source_chain = "stacks"; + let gateway_address = "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway"; + let its_address = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.interchain-token-service"; + + let message_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + let msg = Message { + message_id: message_id.clone(), + source_address: its_address.to_string(), + destination_chain: "axelar".parse().unwrap(), + destination_address: "0x00".to_string(), + payload_hash: "0x63b56229fc520914aa0f690e136517fceae159a49082f5f18f866a9ba5e3ce15" + .parse() + .unwrap(), + }; + + let wrong_event = TransactionEvents { + event_index: 0, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: None, + }; + + /* + payload is: + { + type: u3, + destination-chain: "ethereum", + payload: { + type: u1, + token-id: 0x42fad3435446674f88b47510fe7d2d144c8867c405d4933007705db85f37ded5, + name: "sample", + symbol: "sample", + decimals: u6, + minter: 0x00 + } + } + */ + let event = TransactionEvents { + event_index: 1, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: Some(ContractLog { + contract_id: gateway_address.to_string(), + topic: "print".to_string(), + value: ContractLogValue { + hex: "0x0c000000061164657374696e6174696f6e2d636861696e0d000000066178656c61721c64657374696e6174696f6e2d636f6e74726163742d616464726573730d0000000430783030077061796c6f616402000000d90c000000031164657374696e6174696f6e2d636861696e0d00000008657468657265756d077061796c6f616402000000920c0000000608646563696d616c730100000000000000000000000000000006066d696e746572020000000100046e616d650d0000000673616d706c650673796d626f6c0d0000000673616d706c6508746f6b656e2d69640200000020563dc3698c0f2c5adf375ff350bb54ecf86d2be109e3aacaf38111cdf171df7804747970650100000000000000000000000000000001047479706501000000000000000000000000000000030c7061796c6f61642d6861736802000000207bcf62a3e8aed07d1eb704a1c4b142de9c1f429d2a6cf835c3347763ae8e05ab0673656e646572061a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce18696e746572636861696e2d746f6b656e2d7365727669636504747970650d0000000d636f6e74726163742d63616c6c".to_string(), + } + }), + }; + + let transaction = Transaction { + tx_id: message_id.tx_hash.into(), + nonce: 1, + sender_address: "whatever".to_string(), + tx_status: "success".to_string(), + events: vec![wrong_event, event], + }; + + ( + source_chain.parse().unwrap(), + gateway_address.to_string(), + its_address.to_string(), + transaction, + msg, + ) + } + + fn get_matching_its_verify_interchain_token_msg_and_tx( + ) -> (ChainName, String, String, Transaction, Message) { + let source_chain = "stacks"; + let gateway_address = "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway"; + let its_address = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.interchain-token-service"; + + let message_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + let msg = Message { + message_id: message_id.clone(), + source_address: its_address.to_string(), + destination_chain: "stacks".parse().unwrap(), + destination_address: its_address.to_string(), + payload_hash: "0xe0a3c74b09fa9fc9ce46ab8b6984ffb079f49fc08862a949a14a6eb6ad063c75" + .parse() + .unwrap(), + }; + + let wrong_event = TransactionEvents { + event_index: 0, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: None, + }; + + /* + payload is: + { + type: 'verify-interchain-token', + token-address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sample-sip-010 + } + */ + let event = TransactionEvents { + event_index: 1, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: Some(ContractLog { + contract_id: gateway_address.to_string(), + topic: "print".to_string(), + value: ContractLogValue { + hex: "0x0c000000061164657374696e6174696f6e2d636861696e0d00000006737461636b731c64657374696e6174696f6e2d636f6e74726163742d616464726573730d00000042535431505148514b5630524a585a465931444758384d4e534e5956453356475a4a53525450475a474d2e696e746572636861696e2d746f6b656e2d73657276696365077061796c6f616402000001c40c000000080a6d6573736167652d69640d0000002c617070726f7665642d696e746572636861696e2d746f6b656e2d6465706c6f796d656e742d6d657373616765077061796c6f616402000000a60c0000000608646563696d616c7301000000000000000000000000000000120c6d696e7465722d6279746573020000000100046e616d650d000000176e61746976652d696e746572636861696e2d746f6b656e0673796d626f6c0d0000000349545408746f6b656e2d696402000000206c96e90b60cd71d0b948ae26be1046377a10f46441d595a6d5dd4f4a6a850372047479706501000000000000000000000000000000010e736f757263652d616464726573730d00000004307830300c736f757263652d636861696e0d00000008657468657265756d0d746f6b656e2d61646472657373061a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce0e73616d706c652d7369702d30313008746f6b656e2d696402000000206c96e90b60cd71d0b948ae26be1046377a10f46441d595a6d5dd4f4a6a8503720a746f6b656e2d74797065010000000000000000000000000000000004747970650d000000177665726966792d696e746572636861696e2d746f6b656e0c7061796c6f61642d686173680200000020e0a3c74b09fa9fc9ce46ab8b6984ffb079f49fc08862a949a14a6eb6ad063c750673656e646572061a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce18696e746572636861696e2d746f6b656e2d7365727669636504747970650d0000000d636f6e74726163742d63616c6c".to_string(), + } + }), + }; + + let transaction = Transaction { + tx_id: message_id.tx_hash.into(), + nonce: 1, + sender_address: "whatever".to_string(), + tx_status: "success".to_string(), + events: vec![wrong_event, event], + }; + + ( + source_chain.parse().unwrap(), + gateway_address.to_string(), + its_address.to_string(), + transaction, + msg, + ) + } + + fn get_matching_its_verify_token_manager_msg_and_tx( + ) -> (ChainName, String, String, Transaction, Message) { + let source_chain = "stacks"; + let gateway_address = "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway"; + let its_address = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.interchain-token-service"; + + let message_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + let msg = Message { + message_id: message_id.clone(), + source_address: its_address.to_string(), + destination_chain: "stacks".parse().unwrap(), + destination_address: its_address.to_string(), + payload_hash: "0x8488259c3537e21e92750cc757a4b99377c5149ea986e2eff7716fdaf8c4ace8" + .parse() + .unwrap(), + }; + + let wrong_event = TransactionEvents { + event_index: 0, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: None, + }; + + /* + payload is: + { + type: 'verify-token-manager', + token-manager-address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token-manager + } + */ + let event = TransactionEvents { + event_index: 1, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: Some(ContractLog { + contract_id: gateway_address.to_string(), + topic: "print".to_string(), + value: ContractLogValue { + hex: "0x0c000000061164657374696e6174696f6e2d636861696e0d00000006737461636b731c64657374696e6174696f6e2d636f6e74726163742d616464726573730d00000042535431505148514b5630524a585a465931444758384d4e534e5956453356475a4a53525450475a474d2e696e746572636861696e2d746f6b656e2d73657276696365077061796c6f616402000000da0c000000050d746f6b656e2d61646472657373061a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce0e73616d706c652d7369702d30313008746f6b656e2d69640200000020289df9e77347122b6306bc2db1fa9387bb8b851d685ff3ee92d18335abd1c10c15746f6b656e2d6d616e616765722d61646472657373061a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce0d746f6b656e2d6d616e616765720a746f6b656e2d74797065010000000000000000000000000000000204747970650d000000147665726966792d746f6b656e2d6d616e616765720c7061796c6f61642d6861736802000000208488259c3537e21e92750cc757a4b99377c5149ea986e2eff7716fdaf8c4ace80673656e646572061a6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce18696e746572636861696e2d746f6b656e2d7365727669636504747970650d0000000d636f6e74726163742d63616c6c".to_string(), + } + }), + }; + + let transaction = Transaction { + tx_id: message_id.tx_hash.into(), + nonce: 1, + sender_address: "whatever".to_string(), + tx_status: "success".to_string(), + events: vec![wrong_event, event], + }; + + ( + source_chain.parse().unwrap(), + gateway_address.to_string(), + its_address.to_string(), + transaction, + msg, + ) + } +} diff --git a/ampd/src/stacks/mod.rs b/ampd/src/stacks/mod.rs new file mode 100644 index 000000000..cfc89248c --- /dev/null +++ b/ampd/src/stacks/mod.rs @@ -0,0 +1,141 @@ +use axelar_wasm_std::hash::Hash; +use clarity::types::StacksEpochId; +use clarity::vm::types::{ + BufferLength, ListTypeData, SequenceSubtype, TupleData, TupleTypeSignature, TypeSignature, +}; +use clarity::vm::{ClarityName, Value}; +use cosmwasm_std::Uint256; +use error_stack::{Report, ResultExt}; +use multisig::key::PublicKey; +use multisig::msg::Signer; +use multisig::verifier_set::VerifierSet; +use sha3::{Digest, Keccak256}; + +use crate::stacks::error::Error; + +mod error; +pub(crate) mod http_client; +mod its_verifier; +pub(crate) mod verifier; + +pub struct WeightedSigner { + pub signer: Vec, + pub weight: u128, +} + +pub struct WeightedSigners { + pub signers: Vec, + pub threshold: Value, + pub nonce: Value, +} + +impl TryFrom<&Signer> for WeightedSigner { + type Error = Error; + + fn try_from(signer: &Signer) -> Result { + Ok(WeightedSigner { + signer: ecdsa_key(&signer.pub_key)?, + weight: signer.weight.into(), + }) + } +} + +impl TryFrom<&VerifierSet> for WeightedSigners { + type Error = Report; + + fn try_from(verifier_set: &VerifierSet) -> Result { + let mut signers: Vec = verifier_set + .signers + .values() + .map(WeightedSigner::try_from) + .collect::>()?; + + signers.sort_by(|signer1, signer2| signer1.signer.cmp(&signer2.signer)); + + Ok(WeightedSigners { + signers, + threshold: Value::UInt(verifier_set.threshold.into()), + nonce: Value::buff_from( + Uint256::from(verifier_set.created_at) + .to_be_bytes() + .to_vec(), + ) + .change_context(Error::InvalidEncoding)?, + }) + } +} + +impl WeightedSigner { + fn try_into_value(self) -> Result> { + Ok(Value::from( + TupleData::from_data(vec![ + ( + ClarityName::from("signer"), + Value::buff_from(self.signer).change_context(Error::InvalidEncoding)?, + ), + (ClarityName::from("weight"), Value::UInt(self.weight)), + ]) + .change_context(Error::InvalidEncoding)?, + )) + } +} + +impl WeightedSigners { + pub fn hash(self) -> Result> { + let value = self + .try_into_value() + .change_context(Error::InvalidEncoding)?; + + Ok(Keccak256::digest( + value + .serialize_to_vec() + .map_err(|_| Error::InvalidEncoding)?, + ) + .into()) + } + + pub fn try_into_value(self) -> Result> { + let weighted_signers: Vec = self + .signers + .into_iter() + .map(|weighted_signer| weighted_signer.try_into_value()) + .collect::>() + .change_context(Error::InvalidEncoding)?; + + let signer_type_signature = TupleTypeSignature::try_from(vec![ + ( + ClarityName::from("signer"), + TypeSignature::SequenceType(SequenceSubtype::BufferType( + BufferLength::try_from(33u32).change_context(Error::InvalidEncoding)?, + )), + ), + (ClarityName::from("weight"), TypeSignature::UIntType), + ]) + .change_context(Error::InvalidEncoding)?; + + let tuple_data = TupleData::from_data(vec![ + ( + ClarityName::from("signers"), + Value::list_with_type( + &StacksEpochId::latest(), + weighted_signers, + ListTypeData::new_list(TypeSignature::from(signer_type_signature), 100) + .change_context(Error::InvalidEncoding)?, + ) + .map_err(|_| Error::InvalidEncoding)?, + ), + (ClarityName::from("threshold"), self.threshold), + (ClarityName::from("nonce"), self.nonce), + ]) + .change_context(Error::InvalidEncoding)?; + + Ok(Value::from(tuple_data)) + } +} + +pub fn ecdsa_key(pub_key: &PublicKey) -> Result, Error> { + match pub_key { + PublicKey::Ecdsa(ecdsa_key) => Ok(ecdsa_key.to_vec()), + _ => Err(Error::NotEcdsaKey), + } +} diff --git a/ampd/src/stacks/verifier.rs b/ampd/src/stacks/verifier.rs new file mode 100644 index 000000000..851fe35c3 --- /dev/null +++ b/ampd/src/stacks/verifier.rs @@ -0,0 +1,772 @@ +use axelar_wasm_std::voting::Vote; +use clarity::vm::types::{ + BufferLength, PrincipalData, SequenceSubtype, StringSubtype, TupleTypeSignature, TypeSignature, + Value, +}; +use clarity::vm::ClarityName; +use router_api::ChainName; + +use crate::handlers::stacks_verify_msg::Message; +use crate::handlers::stacks_verify_verifier_set::VerifierSetConfirmation; +use crate::stacks::error::Error; +use crate::stacks::http_client::{Client, Transaction, TransactionEvents}; +use crate::stacks::its_verifier::{get_its_hub_payload_hash, its_verify_contract_code}; +use crate::stacks::WeightedSigners; +use crate::types::Hash; + +pub const PRINT_TOPIC: &str = "print"; + +pub const CONTRACT_CALL_TYPE: &str = "contract-call"; +const SIGNERS_ROTATED_TYPE: &str = "signers-rotated"; + +impl Message { + pub fn eq_event( + &self, + event: &TransactionEvents, + new_payload_hash: Option, + ) -> Result> { + let contract_log = event.contract_log.as_ref().ok_or(Error::PropertyEmpty)?; + + if contract_log.topic != PRINT_TOPIC { + return Ok(false); + } + + let tuple_type_signature = TupleTypeSignature::try_from(vec![ + ( + ClarityName::from("type"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(13u32)?, + ))), + ), + (ClarityName::from("sender"), TypeSignature::PrincipalType), + ( + ClarityName::from("destination-chain"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(20u32)?, + ))), + ), + ( + ClarityName::from("destination-contract-address"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(128u32)?, + ))), + ), + ( + ClarityName::from("payload-hash"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 32u32, + )?)), + ), + ])?; + + let hex = contract_log + .value + .hex + .strip_prefix("0x") + .ok_or(Error::PropertyEmpty)?; + + let value = + Value::try_deserialize_hex(hex, &TypeSignature::TupleType(tuple_type_signature), true)?; + + if let Value::Tuple(data) = value { + if !data.get("type")?.eq(&Value::string_ascii_from_bytes( + CONTRACT_CALL_TYPE.as_bytes().to_vec(), + )?) { + return Ok(false); + } + + if !data.get("sender")?.eq(&Value::from(PrincipalData::parse( + self.source_address.as_str(), + )?)) { + return Ok(false); + } + + if !data + .get("destination-chain")? + .eq(&Value::string_ascii_from_bytes( + self.destination_chain.as_ref().as_bytes().to_vec(), + )?) + { + return Ok(false); + } + + if !data + .get("destination-contract-address")? + .eq(&Value::string_ascii_from_bytes( + self.destination_address.as_bytes().to_vec(), + )?) + { + return Ok(false); + } + + if let Some(new_payload_hash) = new_payload_hash { + if new_payload_hash != self.payload_hash { + return Ok(false); + } + } else if !data + .get("payload-hash")? + .eq(&Value::buff_from(self.payload_hash.as_bytes().to_vec())?) + { + return Ok(false); + } + + return Ok(true); + } + + Ok(false) + } +} + +impl VerifierSetConfirmation { + fn eq_event(&self, event: &TransactionEvents) -> Result> { + let contract_log = event.contract_log.as_ref().ok_or(Error::PropertyEmpty)?; + + if contract_log.topic != PRINT_TOPIC { + return Ok(false); + } + + let tuple_type_signature = TupleTypeSignature::try_from(vec![ + ( + ClarityName::from("type"), + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(15u32)?, + ))), + ), + ( + ClarityName::from("signers-hash"), + TypeSignature::SequenceType(SequenceSubtype::BufferType(BufferLength::try_from( + 32u32, + )?)), + ), + ])?; + + let hex = contract_log + .value + .hex + .strip_prefix("0x") + .ok_or(Error::PropertyEmpty)?; + + let value = + Value::try_deserialize_hex(hex, &TypeSignature::TupleType(tuple_type_signature), true)?; + + if let Value::Tuple(data) = value { + if !data.get("type")?.eq(&Value::string_ascii_from_bytes( + SIGNERS_ROTATED_TYPE.as_bytes().to_vec(), + )?) { + return Ok(false); + } + + let weighted_signers = WeightedSigners::try_from(&self.verifier_set)?; + + let hash = weighted_signers.hash(); + + if !data + .get("signers-hash")? + .eq(&Value::buff_from(hash?.to_vec())?) + { + return Ok(false); + } + + return Ok(true); + } + + Ok(false) + } +} + +fn find_event<'a>( + transaction: &'a Transaction, + gateway_address: &String, + log_index: u64, +) -> Option<&'a TransactionEvents> { + let event = transaction + .events + .iter() + .find(|el| el.event_index == log_index)?; + + if !event.contract_log.as_ref()?.contract_id.eq(gateway_address) { + return None; + } + + Some(event) +} + +pub async fn verify_message( + source_chain: &ChainName, + gateway_address: &String, + its_address: &String, + transaction: &Transaction, + message: &Message, + http_client: &Client, + reference_native_interchain_token_code: &String, + reference_token_manager_code: &String, +) -> Vote { + if message.message_id.tx_hash != transaction.tx_id.as_bytes() { + return Vote::NotFound; + } + + match find_event(transaction, gateway_address, message.message_id.event_index) { + Some(event) => { + // In case message is not from ITS + if &message.source_address != its_address { + if message.eq_event(event, None).unwrap_or(false) { + return Vote::SucceededOnChain; + } + + return Vote::NotFound; + } + + // In case messages is from Stacks -> Stacks and from ITS -> ITS, use custom logic + // for confirming contract deployments + if &message.destination_chain == source_chain + && &message.destination_address == its_address + { + if message.eq_event(event, None).unwrap_or(false) + && its_verify_contract_code( + event, + http_client, + reference_native_interchain_token_code, + reference_token_manager_code, + ) + .await + .unwrap_or(false) + { + return Vote::SucceededOnChain; + } + + return Vote::NotFound; + } + + // In other case, abi encode payload coming from Stacks ITS + if let Ok(payload_hash) = get_its_hub_payload_hash(event) { + if message.eq_event(event, Some(payload_hash)).unwrap_or(false) { + return Vote::SucceededOnChain; + } + } + + Vote::NotFound + } + _ => Vote::NotFound, + } +} + +pub fn verify_verifier_set( + gateway_address: &String, + transaction: &Transaction, + verifier_set: VerifierSetConfirmation, +) -> Vote { + if verifier_set.message_id.tx_hash != transaction.tx_id.as_bytes() { + return Vote::NotFound; + } + + match find_event( + transaction, + gateway_address, + verifier_set.message_id.event_index, + ) { + Some(event) if verifier_set.eq_event(event).unwrap_or(false) => Vote::SucceededOnChain, + _ => Vote::NotFound, + } +} + +#[cfg(test)] +mod tests { + use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; + use axelar_wasm_std::voting::Vote; + use clarity::vm::types::TupleData; + use clarity::vm::{ClarityName, Value}; + use cosmwasm_std::{HexBinary, Uint128}; + use multisig::key::KeyType; + use multisig::test::common::{build_verifier_set, ecdsa_test_data}; + use router_api::ChainName; + use tokio::test as async_test; + + use crate::handlers::stacks_verify_msg::Message; + use crate::handlers::stacks_verify_verifier_set::VerifierSetConfirmation; + use crate::stacks::http_client::{ + Client, ContractLog, ContractLogValue, Transaction, TransactionEvents, + }; + use crate::stacks::verifier::{verify_message, verify_verifier_set, SIGNERS_ROTATED_TYPE}; + use crate::types::Hash; + + // test verify message + #[async_test] + async fn should_not_verify_tx_id_does_not_match() { + let (source_chain, gateway_address, its_address, tx, mut msg) = get_matching_msg_and_tx(); + + msg.message_id.tx_hash = Hash::random().into(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_no_log_for_event_index() { + let (source_chain, gateway_address, its_address, tx, mut msg) = get_matching_msg_and_tx(); + + msg.message_id.event_index = 2; + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_event_index_does_not_match() { + let (source_chain, gateway_address, its_address, tx, mut msg) = get_matching_msg_and_tx(); + + msg.message_id.event_index = 0; + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_not_gateway() { + let (source_chain, gateway_address, its_address, mut tx, msg) = get_matching_msg_and_tx(); + + let transaction_events = tx.events.get_mut(1).unwrap(); + let contract_call = transaction_events.contract_log.as_mut().unwrap(); + + contract_call.contract_id = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".to_string(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_invalid_topic() { + let (source_chain, gateway_address, its_address, mut tx, msg) = get_matching_msg_and_tx(); + + let transaction_events = tx.events.get_mut(1).unwrap(); + let contract_call = transaction_events.contract_log.as_mut().unwrap(); + + contract_call.topic = "other".to_string(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_invalid_type() { + let (source_chain, gateway_address, its_address, mut tx, msg) = get_matching_msg_and_tx(); + + let transaction_events = tx.events.get_mut(1).unwrap(); + let contract_call = transaction_events.contract_log.as_mut().unwrap(); + + // Remove 'call' as hex from `contract-call` data + contract_call.value.hex = contract_call + .value + .hex + .strip_suffix("63616c6c") + .unwrap() + .to_string(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_invalid_sender() { + let (source_chain, gateway_address, its_address, tx, mut msg) = get_matching_msg_and_tx(); + + msg.source_address = "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway".to_string(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_invalid_destination_chain() { + let (source_chain, gateway_address, its_address, tx, mut msg) = get_matching_msg_and_tx(); + + msg.destination_chain = "other".parse().unwrap(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_invalid_destination_address() { + let (source_chain, gateway_address, its_address, tx, mut msg) = get_matching_msg_and_tx(); + + msg.destination_address = "other".parse().unwrap(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_not_verify_invalid_payload_hash() { + let (source_chain, gateway_address, its_address, tx, mut msg) = get_matching_msg_and_tx(); + + msg.payload_hash = "0xaa38573718f5cd6d7e5a90adcdebd28b097f99574ad6febffea9a40adb17f4aa" + .parse() + .unwrap(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::NotFound + ); + } + + #[async_test] + async fn should_verify_msg() { + let (source_chain, gateway_address, its_address, tx, msg) = get_matching_msg_and_tx(); + + assert_eq!( + verify_message( + &source_chain, + &gateway_address, + &its_address, + &tx, + &msg, + &Client::faux(), + &"native_interchain_token_code".to_string(), + &"token_manager_code".to_string() + ) + .await, + Vote::SucceededOnChain + ); + } + + // test verify worker set + #[test] + fn should_not_verify_verifier_set_if_tx_id_does_not_match() { + let (gateway_address, tx, mut verifier_set) = get_matching_verifier_set_and_tx(); + + verifier_set.message_id.tx_hash = Hash::random().into(); + + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_verifier_set_if_no_log_for_event_index() { + let (gateway_address, tx, mut verifier_set) = get_matching_verifier_set_and_tx(); + + verifier_set.message_id.event_index = 2; + + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_verifier_set_if_event_index_does_not_match() { + let (gateway_address, tx, mut verifier_set) = get_matching_verifier_set_and_tx(); + + verifier_set.message_id.event_index = 0; + + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_verifier_set_if_not_from_gateway() { + let (gateway_address, mut tx, verifier_set) = get_matching_verifier_set_and_tx(); + + let transaction_events = tx.events.get_mut(1).unwrap(); + let contract_call = transaction_events.contract_log.as_mut().unwrap(); + + contract_call.contract_id = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM".to_string(); + + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_verifier_set_invalid_topic() { + let (gateway_address, mut tx, verifier_set) = get_matching_verifier_set_and_tx(); + + let transaction_events = tx.events.get_mut(1).unwrap(); + let contract_call = transaction_events.contract_log.as_mut().unwrap(); + + contract_call.topic = "other".to_string(); + + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_verifier_set_invalid_type() { + let (gateway_address, mut tx, verifier_set) = get_matching_verifier_set_and_tx(); + + let transaction_events = tx.events.get_mut(1).unwrap(); + let signers_rotated = transaction_events.contract_log.as_mut().unwrap(); + + // Remove 'rotated' as hex from `signers-rotated` data + signers_rotated.value.hex = signers_rotated + .value + .hex + .strip_suffix("726f7461746564") + .unwrap() + .to_string(); + + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_worker_set_if_verifier_set_does_not_match() { + let (gateway_address, tx, mut verifier_set) = get_matching_verifier_set_and_tx(); + + verifier_set.verifier_set.threshold = Uint128::from(10u128); + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::NotFound + ); + } + + #[test] + fn should_verify_verifier_set() { + let (gateway_address, tx, verifier_set) = get_matching_verifier_set_and_tx(); + + assert_eq!( + verify_verifier_set(&gateway_address, &tx, verifier_set), + Vote::SucceededOnChain + ); + } + + fn get_matching_msg_and_tx() -> (ChainName, String, String, Transaction, Message) { + let source_chain = "stacks"; + let gateway_address = "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway"; + let its_address = "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B.its"; + + let message_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + let msg = Message { + message_id: message_id.clone(), + source_address: "ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG".to_string(), + destination_chain: "Destination".parse().unwrap(), + destination_address: "0x123abc".to_string(), + payload_hash: "0x9ed02951dbf029855b46b102cc960362732569e83d00a49a7575d7aed229890e" + .parse() + .unwrap(), + }; + + let wrong_event = TransactionEvents { + event_index: 0, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: None, + }; + + let event = TransactionEvents { + event_index: 1, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: Some(ContractLog { + contract_id: gateway_address.to_string(), + topic: "print".to_string(), + value: ContractLogValue { + hex: "0x0c000000061164657374696e6174696f6e2d636861696e0d0000000b64657374696e6174696f6e1c64657374696e6174696f6e2d636f6e74726163742d616464726573730d000000083078313233616263077061796c6f61640200000029535431534a3344544535444e375835345944483544363452334243423641324147325a5138595044350c7061796c6f61642d6861736802000000209ed02951dbf029855b46b102cc960362732569e83d00a49a7575d7aed229890e0673656e646572051a99e2ec69ac5b6e67b4e26edd0e2c1c1a6b9bbd2304747970650d0000000d636f6e74726163742d63616c6c".to_string(), + } + }), + }; + + let transaction = Transaction { + tx_id: message_id.tx_hash.into(), + nonce: 1, + sender_address: "whatever".to_string(), + tx_status: "success".to_string(), + events: vec![wrong_event, event], + }; + + ( + source_chain.parse().unwrap(), + gateway_address.to_string(), + its_address.to_string(), + transaction, + msg, + ) + } + + fn get_matching_verifier_set_and_tx() -> (String, Transaction, VerifierSetConfirmation) { + let gateway_address = "SP2N959SER36FZ5QT1CX9BR63W3E8X35WQCMBYYWC.axelar-gateway"; + let message_id = HexTxHashAndEventIndex::new(Hash::random(), 1u64); + + let mut verifier_set_confirmation = VerifierSetConfirmation { + message_id: message_id.clone(), + verifier_set: build_verifier_set(KeyType::Ecdsa, &ecdsa_test_data::signers()), + }; + verifier_set_confirmation.verifier_set.created_at = 5; + + let wrong_event = TransactionEvents { + event_index: 0, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: None, + }; + + let signers_hash = + HexBinary::from_hex("6925aafa48d1c99f0fd9bdd98b00fc319462a3ecbf2bbb8379c975a26a0c0c46") + .unwrap(); + + let value = Value::from( + TupleData::from_data(vec![ + ( + ClarityName::from("signers-hash"), + Value::buff_from(signers_hash.to_vec()).unwrap(), + ), + ( + ClarityName::from("type"), + Value::string_ascii_from_bytes(SIGNERS_ROTATED_TYPE.as_bytes().to_vec()) + .unwrap(), + ), + ]) + .unwrap(), + ); + + let event = TransactionEvents { + event_index: 1, + tx_id: message_id.tx_hash_as_hex().to_string(), + contract_log: Some(ContractLog { + contract_id: gateway_address.to_string(), + topic: "print".to_string(), + value: ContractLogValue { + hex: format!("0x{}", value.serialize_to_hex().unwrap()), + }, + }), + }; + + let transaction = Transaction { + tx_id: message_id.tx_hash.into(), + nonce: 1, + sender_address: "whatever".to_string(), + tx_status: "success".to_string(), + events: vec![wrong_event, event], + }; + + ( + gateway_address.to_string(), + transaction, + verifier_set_confirmation, + ) + } +} diff --git a/ampd/src/tests/config_template.toml b/ampd/src/tests/config_template.toml index ec99f3de7..55ce13f51 100644 --- a/ampd/src/tests/config_template.toml +++ b/ampd/src/tests/config_template.toml @@ -82,6 +82,19 @@ type = 'StellarVerifierSetVerifier' cosmwasm_contract = 'axelar1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqecnww6' rpc_url = 'http://127.0.0.1/' +[[handlers]] +type = 'StacksMsgVerifier' +cosmwasm_contract = 'axelar1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqecnww6' +http_url = 'http://127.0.0.1/' +its_address = 'its_address' +reference_native_interchain_token_address = 'interchain_token_address' +reference_token_manager_address = 'token_manager_address' + +[[handlers]] +type = 'StacksVerifierSetVerifier' +cosmwasm_contract = 'axelar1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqecnww6' +http_url = 'http://127.0.0.1/' + [tofnd_config] url = 'http://localhost:50051/' party_uid = 'ampd'