From 3169950bae07c9a87f87b19704b6bd1a80092396 Mon Sep 17 00:00:00 2001 From: Haiyi Zhong Date: Tue, 3 Sep 2024 09:59:23 -0400 Subject: [PATCH 1/2] feat(minor-ampd): stellar verify messages --- Cargo.lock | 352 +++++++----------------- Cargo.toml | 1 + ampd/Cargo.toml | 12 +- ampd/src/config.rs | 14 +- ampd/src/handlers/config.rs | 21 ++ ampd/src/handlers/mod.rs | 9 + ampd/src/handlers/stellar_verify_msg.rs | 334 ++++++++++++++++++++++ ampd/src/lib.rs | 17 ++ ampd/src/stellar/http_client.rs | 108 ++++++++ ampd/src/stellar/mod.rs | 2 + ampd/src/stellar/verifier.rs | 224 +++++++++++++++ ampd/src/tests/config_template.toml | 5 + external-gateways/stellar/Cargo.toml | 2 +- 13 files changed, 839 insertions(+), 262 deletions(-) create mode 100644 ampd/src/handlers/stellar_verify_msg.rs create mode 100644 ampd/src/stellar/http_client.rs create mode 100644 ampd/src/stellar/mod.rs create mode 100644 ampd/src/stellar/verifier.rs diff --git a/Cargo.lock b/Cargo.lock index 96b7aa119..031773a98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,7 @@ dependencies = [ "dirs", "ecdsa", "ed25519 2.2.3", + "ed25519-dalek", "elliptic-curve", "enum-display-derive", "error-stack", @@ -245,6 +246,7 @@ dependencies = [ "events", "events-derive", "evm-gateway", + "faux", "futures", "generic-array", "hex", @@ -268,6 +270,8 @@ dependencies = [ "serde_with 3.8.1", "service-registry", "sha3", + "stellar-rs", + "stellar-xdr", "sui-gateway", "sui-json-rpc-types", "sui-types 0.1.0", @@ -688,7 +692,7 @@ dependencies = [ "proc-macro2 1.0.85", "quote 1.0.36", "syn 1.0.109", - "synstructure 0.12.6", + "synstructure", ] [[package]] @@ -2247,6 +2251,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-getters" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6433aac097572ea8ccc60b3f2e756c661c9aeed9225cdd4d0cb119cb7ff6ba" +dependencies = [ + "proc-macro2 1.0.85", + "quote 1.0.36", + "syn 2.0.68", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -2950,6 +2965,29 @@ dependencies = [ "bytes", ] +[[package]] +name = "faux" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b55a7f424e532314115b5cdc6d9711b15ac453bfe0dcfa212baebc5efacd60" +dependencies = [ + "faux_macros", + "paste", +] + +[[package]] +name = "faux_macros" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15473d7f83b54a44826907af16ae5727eaacaf6e53b51474016d3efd9aa35d5" +dependencies = [ + "darling 0.20.9", + "proc-macro2 1.0.85", + "quote 1.0.36", + "syn 2.0.68", + "uuid 1.8.0", +] + [[package]] name = "ff" version = "0.13.0" @@ -3811,124 +3849,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2 1.0.85", - "quote 1.0.36", - "syn 2.0.68", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -3937,14 +3857,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -4415,11 +4333,11 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin 0.9.8", ] [[package]] @@ -4462,12 +4380,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -5002,7 +4914,7 @@ dependencies = [ "proc-macro2 1.0.85", "quote 1.0.36", "syn 1.0.109", - "synstructure 0.12.6", + "synstructure", ] [[package]] @@ -5179,7 +5091,7 @@ source = "git+https://github.com/mystenlabs/sui?tag=mainnet-v1.26.2#f531168c7452 dependencies = [ "proc-macro2 1.0.85", "syn 1.0.109", - "synstructure 0.12.6", + "synstructure", ] [[package]] @@ -7403,12 +7315,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "indexmap 2.2.6", "itoa", + "memchr", "ryu", "serde", ] @@ -7793,12 +7706,6 @@ dependencies = [ "der 0.7.9", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stacker" version = "0.1.15" @@ -7838,6 +7745,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "stellar-rs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0099336b1e8824b731cc0138bffbaefb2b9013eb4ced9098c7ee7fef282220" +dependencies = [ + "base64 0.22.1", + "chrono", + "derive-getters", + "hex", + "lazy_static", + "reqwest 0.12.5", + "serde", + "serde_json", + "stellar-xdr", + "stellar_rust_sdk_derive", + "tokio", + "url", +] + [[package]] name = "stellar-strkey" version = "0.0.8" @@ -7855,12 +7782,27 @@ version = "21.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" dependencies = [ + "base64 0.13.1", "crate-git-revision", "escape-bytes", "hex", + "serde", + "serde_json", + "serde_with 3.8.1", "stellar-strkey", ] +[[package]] +name = "stellar_rust_sdk_derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc686a17ad5f5a958273870bb1ace955221ef5fbfcf02d5ddb7eb0ec91ea4b" +dependencies = [ + "proc-macro2 1.0.85", + "quote 1.0.36", + "syn 2.0.68", +] + [[package]] name = "strsim" version = "0.10.0" @@ -8270,17 +8212,6 @@ dependencies = [ "unicode-xid 0.2.4", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2 1.0.85", - "quote 1.0.36", - "syn 2.0.68", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -8589,16 +8520,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -9157,6 +9078,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -9231,9 +9158,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -9246,18 +9173,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -9748,18 +9663,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "ws_stream_wasm" version = "0.7.4" @@ -9842,30 +9745,6 @@ dependencies = [ "time", ] -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2 1.0.85", - "quote 1.0.36", - "syn 2.0.68", - "synstructure 0.13.1", -] - [[package]] name = "zerocopy" version = "0.7.34" @@ -9886,27 +9765,6 @@ dependencies = [ "syn 2.0.68", ] -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2 1.0.85", - "quote 1.0.36", - "syn 2.0.68", - "synstructure 0.13.1", -] - [[package]] name = "zeroize" version = "1.8.1" @@ -9926,25 +9784,3 @@ dependencies = [ "quote 1.0.36", "syn 2.0.68", ] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2 1.0.85", - "quote 1.0.36", - "syn 2.0.68", -] diff --git a/Cargo.toml b/Cargo.toml index 1743b395a..1b2732bd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ interchain-token-service = { version = "^0.1.0", path = "interchain-token-servic goldie = { version = "0.5" } axelarnet-gateway = { version = "^0.1.0", path = "contracts/axelarnet-gateway" } cw-multi-test = "1.2.0" +stellar-xdr = { version = "21.2.0" } [workspace.lints.clippy] arithmetic_side_effects = "deny" diff --git a/ampd/Cargo.toml b/ampd/Cargo.toml index 6dd31cc18..fb7bf6fdd 100644 --- a/ampd/Cargo.toml +++ b/ampd/Cargo.toml @@ -24,7 +24,9 @@ enum-display-derive = "0.1.1" error-stack = { workspace = true } ethers-contract = { workspace = true } ethers-core = { workspace = true } -ethers-providers = { version = "2.0.13", default-features = false, features = ["rustls"] } +ethers-providers = { version = "2.0.13", default-features = false, features = [ + "rustls", +] } events = { workspace = true } events-derive = { workspace = true } evm-gateway = { workspace = true } @@ -48,9 +50,13 @@ serde_json = { workspace = true } serde_with = "3.2.0" service-registry = { workspace = true } sha3 = { workspace = true } +stellar-rs = "0.3.2" +stellar-xdr = { workspace = true, features = ["serde_json"] } sui-gateway = { workspace = true } sui-json-rpc-types = { git = "https://github.com/mystenlabs/sui", tag = "mainnet-v1.26.2" } -sui-types = { git = "https://github.com/mystenlabs/sui", features = ["test-utils"], tag = "mainnet-v1.26.2" } +sui-types = { git = "https://github.com/mystenlabs/sui", features = [ + "test-utils", +], tag = "mainnet-v1.26.2" } # Need to switch to our own fork of tendermint and tendermint-rpc due to event attribute value being nullable. # Can switch back once https://github.com/informalsystems/tendermint-rs/issues/1216 is resolved. # The fix for the issue is at https://github.com/axelarnetwork/tendermint-rs/commit/e97033e20e660a7e707ea86db174ec047bbba50d. @@ -74,7 +80,9 @@ valuable-serde = { version = "0.1.0", features = ["std"] } voting-verifier = { workspace = true } [dev-dependencies] +ed25519-dalek = { workspace = true, features = ["rand_core"] } elliptic-curve = "0.13.5" +faux = "0.1.10" generic-array = "0.14.7" multisig = { workspace = true, features = ["test", "library"] } rand = "0.8.5" diff --git a/ampd/src/config.rs b/ampd/src/config.rs index 0a1609945..076688b61 100644 --- a/ampd/src/config.rs +++ b/ampd/src/config.rs @@ -120,6 +120,11 @@ mod tests { type = 'MvxVerifierSetVerifier' cosmwasm_contract = '{}' proxy_url = 'http://localhost:7545' + + [[handlers]] + type = 'StellarMsgVerifier' + cosmwasm_contract = '{}' + http_url = 'http://localhost:8000' ", TMAddress::random(PREFIX), TMAddress::random(PREFIX), @@ -129,10 +134,11 @@ mod tests { 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(), 8); + assert_eq!(cfg.handlers.len(), 9); } #[test] @@ -324,6 +330,12 @@ mod tests { ), proxy_url: Url::from_str("http://127.0.0.1").unwrap(), }, + HandlerConfig::StellarMsgVerifier { + 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 6befec34e..a923782e2 100644 --- a/ampd/src/handlers/config.rs +++ b/ampd/src/handlers/config.rs @@ -55,6 +55,10 @@ pub enum Config { cosmwasm_contract: TMAddress, proxy_url: Url, }, + StellarMsgVerifier { + cosmwasm_contract: TMAddress, + http_url: Url, + }, } fn validate_multisig_signer_config<'de, D>(configs: &[Config]) -> Result<(), D::Error> @@ -182,6 +186,22 @@ where } } +fn validate_stellar_msg_verifier_config<'de, D>(configs: &[Config]) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + match configs + .iter() + .filter(|config| matches!(config, Config::StellarMsgVerifier { .. })) + .count() + { + count if count > 1 => Err(de::Error::custom( + "only one Stellar msg verifier config is allowed", + )), + _ => Ok(()), + } +} + pub fn deserialize_handler_configs<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -195,6 +215,7 @@ where validate_sui_verifier_set_verifier_config::(&configs)?; validate_mvx_msg_verifier_config::(&configs)?; validate_mvx_worker_set_verifier_config::(&configs)?; + validate_stellar_msg_verifier_config::(&configs)?; Ok(configs) } diff --git a/ampd/src/handlers/mod.rs b/ampd/src/handlers/mod.rs index 1a01f197b..72276b63e 100644 --- a/ampd/src/handlers/mod.rs +++ b/ampd/src/handlers/mod.rs @@ -5,6 +5,7 @@ pub mod evm_verify_verifier_set; pub mod multisig; pub mod mvx_verify_msg; pub mod mvx_verify_verifier_set; +pub(crate) mod stellar_verify_msg; pub mod sui_verify_msg; pub mod sui_verify_verifier_set; @@ -18,6 +19,7 @@ mod tests { use tendermint::abci; use crate::types::TMAddress; + use crate::PREFIX; /// Convert a CosmWasm event into an ABCI event pub fn into_structured_event( @@ -41,4 +43,11 @@ mod tests { .try_into() .expect("should convert to ABCI event") } + + pub fn participants(n: u8, verifier: Option) -> Vec { + (0..n) + .map(|_| TMAddress::random(PREFIX)) + .chain(verifier) + .collect() + } } diff --git a/ampd/src/handlers/stellar_verify_msg.rs b/ampd/src/handlers/stellar_verify_msg.rs new file mode 100644 index 000000000..8532e0275 --- /dev/null +++ b/ampd/src/handlers/stellar_verify_msg.rs @@ -0,0 +1,334 @@ +use std::collections::HashSet; +use std::convert::TryInto; + +use async_trait::async_trait; +use axelar_wasm_std::voting::{PollId, Vote}; +use cosmrs::cosmwasm::MsgExecuteContract; +use cosmrs::tx::Msg; +use error_stack::ResultExt; +use events::Error::EventTypeMismatch; +use events::Event; +use events_derive::try_from; +use prost_types::Any; +use router_api::ChainName; +use serde::Deserialize; +use serde_with::{serde_as, DisplayFromStr}; +use stellar_xdr::curr::{ScAddress, ScBytes, ScString}; +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::handlers::errors::Error::DeserializeEvent; +use crate::stellar::http_client::Client; +use crate::stellar::verifier::verify_message; +use crate::types::TMAddress; + +#[serde_as] +#[derive(Deserialize, Debug, Clone)] +pub struct Message { + pub tx_id: String, + pub event_index: u32, + pub destination_address: ScString, + pub destination_chain: ScString, + #[serde_as(as = "DisplayFromStr")] + pub source_address: ScAddress, + pub payload_hash: ScBytes, +} + +#[serde_as] +#[derive(Deserialize, Debug)] +#[try_from("wasm-messages_poll_started")] +struct PollStartedEvent { + poll_id: PollId, + source_chain: ChainName, + #[serde_as(as = "DisplayFromStr")] + source_gateway_address: ScAddress, + expires_at: u64, + messages: Vec, + participants: Vec, +} + +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, 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) -> error_stack::Result, Self::Err> { + if !event.is_from_contract(self.voting_verifier_contract.as_ref()) { + return Ok(vec![]); + } + + let PollStartedEvent { + poll_id, + source_chain, + source_gateway_address, + messages, + expires_at, + participants, + } = match event.try_into() as error_stack::Result<_, _> { + Err(report) if matches!(report.current_context(), EventTypeMismatch(_)) => { + return Ok(vec![]) + } + event => event.change_context(DeserializeEvent)?, + }; + + if !participants.contains(&self.verifier) { + return Ok(vec![]); + } + + if *self.latest_block_height.borrow() >= expires_at { + info!(poll_id = poll_id.to_string(), "skipping expired poll"); + return Ok(vec![]); + } + + let tx_hashes: HashSet<_> = messages + .iter() + .map(|message| message.tx_id.clone()) + .collect(); + + let transaction_responses = self + .http_client + .transaction_responses(tx_hashes) + .await + .change_context(Error::TxReceipts)?; + + let message_ids = messages + .iter() + .map(|message| format!("{}-{}", message.tx_id, message.event_index)) + .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(|| { + info!("ready to verify messages in poll",); + + let votes: Vec<_> = messages + .iter() + .map(|msg| { + transaction_responses + .get(&msg.tx_id) + .map_or(Vote::NotFound, |tx_response| { + verify_message(&source_gateway_address, tx_response, msg) + }) + }) + .collect(); + info!( + votes = votes.as_value(), + "ready to vote for messages in poll" + ); + + votes + }); + + 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 cosmrs::cosmwasm::MsgExecuteContract; + use cosmrs::tx::Msg; + use error_stack::Result; + use events::Error::{DeserializationFailed, EventTypeMismatch}; + use events::Event; + use stellar_xdr::curr::ScAddress; + use tokio::sync::watch; + use tokio::test as async_test; + use voting_verifier::events::{PollMetadata, PollStarted, TxEventConfirmation}; + + use super::PollStartedEvent; + use crate::event_processor::EventHandler; + use crate::handlers::tests::{into_structured_event, participants}; + use crate::stellar::http_client::Client; + use crate::types::{EVMAddress, Hash, TMAddress}; + use crate::PREFIX; + + #[test] + fn should_not_deserialize_incorrect_event() { + // incorrect event type + let mut event: Event = into_structured_event( + poll_started_event(participants(5, None), 100), + &TMAddress::random(PREFIX), + ); + match event { + Event::Abci { + ref mut event_type, .. + } => { + *event_type = "incorrect".into(); + } + _ => panic!("incorrect event type"), + } + let event: Result = (&event).try_into(); + + assert!(matches!( + event.unwrap_err().current_context(), + EventTypeMismatch(_) + )); + + // invalid field + let mut event: Event = into_structured_event( + poll_started_event(participants(5, None), 100), + &TMAddress::random(PREFIX), + ); + match event { + Event::Abci { + ref mut attributes, .. + } => { + attributes.insert("source_gateway_address".into(), "invalid".into()); + } + _ => panic!("incorrect event type"), + } + + let event: Result = (&event).try_into(); + + assert!(matches!( + event.unwrap_err().current_context(), + DeserializationFailed(_, _) + )); + } + + #[test] + fn should_deserialize_correct_event() { + let event: Event = into_structured_event( + poll_started_event(participants(5, None), 100), + &TMAddress::random(PREFIX), + ); + let event: Result = event.try_into(); + assert!(event.is_ok()); + } + + #[async_test] + async fn contract_is_not_voting_verifier() { + let event = into_structured_event( + 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( + 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_vote_correctly() { + let mut client = Client::faux(); + faux::when!(client.transaction_responses).then(|_| Ok(HashMap::new())); + + let voting_verifier = TMAddress::random(PREFIX); + let verifier = TMAddress::random(PREFIX); + let event = into_structured_event( + poll_started_event(participants(5, Some(verifier.clone())), 100), + &voting_verifier, + ); + + let handler = super::Handler::new(verifier, 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 poll_started_event(participants: Vec, expires_at: u64) -> PollStarted { + PollStarted::Messages { + metadata: PollMetadata { + poll_id: "100".parse().unwrap(), + source_chain: "stellar".parse().unwrap(), + source_gateway_address: ScAddress::Contract(stellar_xdr::curr::Hash::from( + Hash::random().0, + )) + .to_string() + .try_into() + .unwrap(), + confirmation_height: 15, + expires_at, + participants: participants + .into_iter() + .map(|addr| cosmwasm_std::Addr::unchecked(addr.to_string())) + .collect(), + }, + messages: (0..2) + .map(|i| TxEventConfirmation { + tx_id: format!("{:x}", Hash::random()).parse().unwrap(), + event_index: i, + source_address: ScAddress::Contract(stellar_xdr::curr::Hash::from( + Hash::random().0, + )) + .to_string() + .try_into() + .unwrap(), + destination_chain: "ethereum".parse().unwrap(), + destination_address: format!("0x{:x}", EVMAddress::random()).parse().unwrap(), + payload_hash: Hash::random().to_fixed_bytes(), + }) + .collect::>(), + } + } +} diff --git a/ampd/src/lib.rs b/ampd/src/lib.rs index 89ca29a83..35bf89267 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 stellar; mod sui; mod tm_client; mod tofnd; @@ -365,6 +366,22 @@ where ), event_processor_config.clone(), ), + handlers::config::Config::StellarMsgVerifier { + cosmwasm_contract, + http_url, + } => self.create_handler_task( + "stellar-msg-verifier", + handlers::stellar_verify_msg::Handler::new( + verifier.clone(), + cosmwasm_contract, + stellar::http_client::Client::new( + http_url.to_string().trim_end_matches('/').into(), + ) + .change_context(Error::Connection)?, + 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/stellar/http_client.rs b/ampd/src/stellar/http_client.rs new file mode 100644 index 000000000..d900a3e2c --- /dev/null +++ b/ampd/src/stellar/http_client.rs @@ -0,0 +1,108 @@ +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +use error_stack::{report, Result}; +use futures::future::join_all; +use num_traits::cast; +use stellar_rs::horizon_client::HorizonClient; +use stellar_rs::transactions::prelude::{SingleTransactionRequest, TransactionResponse}; +use stellar_xdr::curr::{ContractEvent, Limits, ReadXdr, ScAddress, TransactionMeta, VecM}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to create client")] + Client, + #[error("invalid tx hash")] + TxHash, +} + +/// TxResponse parses XDR encoded TransactionMeta to ContractEvent type, and only contains necessary fields for verification +#[derive(Debug)] +pub struct TxResponse { + pub transaction_hash: String, + pub source_address: ScAddress, + pub successful: bool, + pub contract_events: Option>, +} + +impl From for TxResponse { + fn from(response: TransactionResponse) -> Self { + let events = + match TransactionMeta::from_xdr_base64(response.result_meta_xdr(), Limits::none()) { + Ok(TransactionMeta::V3(data)) => match data.soroban_meta { + Some(meta) => meta.events, + None => VecM::default(), + }, + _ => VecM::default(), + }; + + Self { + transaction_hash: response.id().to_owned(), + successful: *response.successful(), + source_address: ScAddress::from_str(response.source_account()) + .expect("must convert to Stellar address"), + contract_events: Some(events), + } + } +} + +impl TxResponse { + pub fn has_failed(&self) -> bool { + !self.successful + } + + pub fn event(&self, index: u32) -> Option<&ContractEvent> { + match self.contract_events { + Some(ref events) => { + let log_index: usize = cast(index).expect("event index must be a valid usize"); + events.get(log_index) + } + None => None, + } + } + + pub fn tx_hash(&self) -> String { + self.transaction_hash.clone() + } +} + +#[cfg_attr(test, faux::create)] +pub struct Client(HorizonClient); + +#[cfg_attr(test, faux::methods)] +impl Client { + pub fn new(url: String) -> Result { + Ok(Self(HorizonClient::new(url).map_err(|err_str| { + report!(Error::Client).attach_printable(err_str) + })?)) + } + + pub async fn transaction_responses( + &self, + tx_hashes: HashSet, + ) -> Result, Error> { + let tx_hashes: Vec<_> = tx_hashes + .into_iter() + .map(|tx_hash| { + SingleTransactionRequest::new() + .set_transaction_hash(tx_hash) + .map_err(|err_str| report!(Error::TxHash).attach_printable(err_str)) + }) + .collect::, _>>()?; + + Ok(join_all( + tx_hashes + .iter() + .map(|tx_hash| self.0.get_single_transaction(tx_hash)), + ) + .await + .into_iter() + .map(|tx_response| tx_response.map(TxResponse::from)) + .filter_map(|tx_response| match tx_response { + Ok(tx_response) => Some((tx_response.tx_hash(), tx_response)), + Err(_) => None, + }) + .collect::>()) + } +} diff --git a/ampd/src/stellar/mod.rs b/ampd/src/stellar/mod.rs new file mode 100644 index 000000000..be0cc859c --- /dev/null +++ b/ampd/src/stellar/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod http_client; +pub(crate) mod verifier; diff --git a/ampd/src/stellar/verifier.rs b/ampd/src/stellar/verifier.rs new file mode 100644 index 000000000..906768e9a --- /dev/null +++ b/ampd/src/stellar/verifier.rs @@ -0,0 +1,224 @@ +use std::str::FromStr; + +use axelar_wasm_std::voting::Vote; +use stellar_xdr::curr::{ContractEventBody, ScAddress, ScSymbol, ScVal, StringM}; + +use crate::handlers::stellar_verify_msg::Message; +use crate::stellar::http_client::TxResponse; + +const TOPIC_CALLED: &str = "called"; + +impl PartialEq<&ContractEventBody> for &Message { + fn eq(&self, event: &&ContractEventBody) -> bool { + let ContractEventBody::V0(body) = event; + + if body.topics.len() != 3 { + return false; + } + + let [symbol, source_address, payload_hash] = &body.topics[..] else { + return false; + }; + + let expected_topic: ScVal = + ScSymbol(StringM::from_str(TOPIC_CALLED).expect("must convert str to ScSymbol")).into(); + + symbol == &expected_topic + && (source_address == &ScVal::Address(self.source_address.clone())) + && (payload_hash == &ScVal::Bytes(self.payload_hash.clone())) + && match &body.data { + ScVal::Vec(Some(data)) if data.len() == 3 => { + let [dest_chain, dest_address, _] = &data[..] else { + return false; + }; + (ScVal::String(self.destination_chain.clone()) == *dest_chain) + && (ScVal::String(self.destination_address.clone()) == *dest_address) + } + _ => false, + } + } +} + +pub fn verify_message(gateway_address: &ScAddress, tx_receipt: &TxResponse, msg: &Message) -> Vote { + if tx_receipt.has_failed() { + return Vote::FailedOnChain; + } + + if msg.tx_id != tx_receipt.transaction_hash { + return Vote::NotFound; + } + + match tx_receipt.event(msg.event_index) { + Some(event) + if event + .clone() + .contract_id + .is_some_and(|hash| ScAddress::Contract(hash) == *gateway_address) + && msg == &event.body => + { + Vote::SucceededOnChain + } + _ => Vote::NotFound, + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use axelar_wasm_std::voting::Vote; + use cosmrs::tx::MessageExt; + use ed25519_dalek::SigningKey; + use rand::rngs::OsRng; + use stellar_xdr::curr::{ + AccountId, BytesM, ContractEvent, ContractEventBody, ContractEventType, ContractEventV0, + PublicKey, ScAddress, ScBytes, ScString, ScSymbol, ScVal, StringM, Uint256, + }; + + use crate::handlers::stellar_verify_msg::Message; + use crate::stellar::http_client::TxResponse; + use crate::stellar::verifier::{verify_message, TOPIC_CALLED}; + use crate::types::{EVMAddress, Hash}; + + #[test] + fn should_not_verify_msg_if_tx_id_does_not_match() { + let (gateway_address, tx_response, mut msg) = matching_msg_and_tx_block(); + msg.tx_id = "different_tx_hash".to_string(); + + assert_eq!( + verify_message(&gateway_address, &tx_response, &msg), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_msg_if_event_index_does_not_match() { + let (gateway_address, tx_response, mut msg) = matching_msg_and_tx_block(); + msg.event_index = 1; + + assert_eq!( + verify_message(&gateway_address, &tx_response, &msg), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_msg_if_source_address_does_not_match() { + let (gateway_address, tx_response, mut msg) = matching_msg_and_tx_block(); + + // Generate a different source address + let mut csprng = OsRng; + let signing_key = SigningKey::generate(&mut csprng); + let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256::from( + signing_key.verifying_key().to_bytes(), + ))); + msg.source_address = ScAddress::Account(account_id); + + assert_eq!( + verify_message(&gateway_address, &tx_response, &msg), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_msg_if_destination_chain_does_not_match() { + let (gateway_address, tx_response, mut msg) = matching_msg_and_tx_block(); + msg.destination_chain = ScString::from(StringM::from_str("different-chain").unwrap()); + + assert_eq!( + verify_message(&gateway_address, &tx_response, &msg), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_msg_if_destination_address_does_not_match() { + let (gateway_address, tx_response, mut msg) = matching_msg_and_tx_block(); + msg.destination_address = ScString::from( + StringM::try_from(format!("0x{:x}", EVMAddress::random()).to_bytes().unwrap()).unwrap(), + ); + + assert_eq!( + verify_message(&gateway_address, &tx_response, &msg), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_msg_if_payload_hash_does_not_match() { + let (gateway_address, tx_response, mut msg) = matching_msg_and_tx_block(); + msg.payload_hash = ScBytes(BytesM::try_from(Hash::random().to_fixed_bytes()).unwrap()); + + assert_eq!( + verify_message(&gateway_address, &tx_response, &msg), + Vote::NotFound + ); + } + + #[test] + fn should_verify_msg_if_correct() { + let (gateway_address, tx_response, msg) = matching_msg_and_tx_block(); + + assert_eq!( + verify_message(&gateway_address, &tx_response, &msg), + Vote::SucceededOnChain + ); + } + + fn matching_msg_and_tx_block() -> (ScAddress, TxResponse, Message) { + let account_id = stellar_xdr::curr::Hash::from(Hash::random().0); + let gateway_address = ScAddress::Contract(account_id.clone()); + + let mut csprng = OsRng; + let signing_key = SigningKey::generate(&mut csprng); + + let msg = Message { + tx_id: Hash::random().to_string(), + event_index: 0, + source_address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519( + Uint256::from(signing_key.verifying_key().to_bytes()), + ))), + destination_chain: ScString::from(StringM::from_str("ethereum").unwrap()), + destination_address: ScString::from( + StringM::try_from(format!("0x{:x}", EVMAddress::random()).to_bytes().unwrap()) + .unwrap(), + ), + payload_hash: ScBytes(BytesM::try_from(Hash::random().to_fixed_bytes()).unwrap()), + }; + + let event_body = ContractEventBody::V0(ContractEventV0 { + topics: vec![ + ScVal::Symbol(ScSymbol(StringM::from_str(TOPIC_CALLED).unwrap())), + ScVal::Address(msg.source_address.clone()), + ScVal::Bytes(msg.payload_hash.clone()), + ] + .try_into() + .unwrap(), + data: ScVal::Vec(Some( + vec![ + ScVal::String(msg.destination_chain.clone()), + ScVal::String(msg.destination_address.clone()), + ScVal::String(StringM::from_str("payload").unwrap().into()), + ] + .try_into() + .unwrap(), + )), + }); + + let event = ContractEvent { + ext: stellar_xdr::curr::ExtensionPoint::V0, + contract_id: Some(account_id), + type_: ContractEventType::Contract, + body: event_body, + }; + + let tx_response = TxResponse { + transaction_hash: msg.tx_id.clone(), + source_address: msg.source_address.clone(), + successful: true, + contract_events: Some(vec![event].try_into().unwrap()), + }; + + (gateway_address, tx_response, msg) + } +} diff --git a/ampd/src/tests/config_template.toml b/ampd/src/tests/config_template.toml index c50c2b107..4eac79264 100644 --- a/ampd/src/tests/config_template.toml +++ b/ampd/src/tests/config_template.toml @@ -72,6 +72,11 @@ type = 'MvxVerifierSetVerifier' cosmwasm_contract = 'axelar1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqecnww6' proxy_url = 'http://127.0.0.1/' +[[handlers]] +type = 'StellarMsgVerifier' +cosmwasm_contract = 'axelar1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqecnww6' +http_url = 'http://127.0.0.1/' + [tofnd_config] url = 'http://localhost:50051/' party_uid = 'ampd' diff --git a/external-gateways/stellar/Cargo.toml b/external-gateways/stellar/Cargo.toml index 3efc2cb84..3584af721 100644 --- a/external-gateways/stellar/Cargo.toml +++ b/external-gateways/stellar/Cargo.toml @@ -15,7 +15,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha3 = { workspace = true } stellar-strkey = { version = "0.0.8" } -stellar-xdr = { version = "21.2.0" } +stellar-xdr = { workspace = true } thiserror = { workspace = true } [lints] From 74ff448b6179fedd2984bd48ba74fe48bf13c9cb Mon Sep 17 00:00:00 2001 From: Haiyi Zhong Date: Tue, 10 Sep 2024 17:31:39 -0400 Subject: [PATCH 2/2] feat(minor-ampd): stellar verifier set verifier --- Cargo.lock | 1 + ampd/Cargo.toml | 1 + ampd/src/config.rs | 14 +- ampd/src/handlers/config.rs | 21 ++ ampd/src/handlers/mod.rs | 1 + .../handlers/stellar_verify_verifier_set.rs | 37 ++- ampd/src/lib.rs | 16 ++ ampd/src/stellar/http_client.rs | 13 ++ ampd/src/stellar/verifier.rs | 212 +++++++++++++++++- ampd/src/tests/config_template.toml | 5 + 10 files changed, 285 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d8d6a688..accb37a14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -270,6 +270,7 @@ dependencies = [ "serde_with 3.8.1", "service-registry", "sha3", + "stellar", "stellar-rs", "stellar-xdr", "sui-gateway", diff --git a/ampd/Cargo.toml b/ampd/Cargo.toml index fb7bf6fdd..840134453 100644 --- a/ampd/Cargo.toml +++ b/ampd/Cargo.toml @@ -50,6 +50,7 @@ serde_json = { workspace = true } serde_with = "3.2.0" service-registry = { workspace = true } sha3 = { workspace = true } +stellar = { workspace = true } stellar-rs = "0.3.2" stellar-xdr = { workspace = true, features = ["serde_json"] } sui-gateway = { workspace = true } diff --git a/ampd/src/config.rs b/ampd/src/config.rs index 076688b61..8bc2a3c42 100644 --- a/ampd/src/config.rs +++ b/ampd/src/config.rs @@ -125,6 +125,11 @@ mod tests { type = 'StellarMsgVerifier' cosmwasm_contract = '{}' http_url = 'http://localhost:8000' + + [[handlers]] + type = 'StellarVerifierSetVerifier' + cosmwasm_contract = '{}' + http_url = 'http://localhost:8000' ", TMAddress::random(PREFIX), TMAddress::random(PREFIX), @@ -135,10 +140,11 @@ mod tests { 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(), 9); + assert_eq!(cfg.handlers.len(), 10); } #[test] @@ -336,6 +342,12 @@ mod tests { ), http_url: Url::from_str("http://127.0.0.1").unwrap(), }, + HandlerConfig::StellarVerifierSetVerifier { + 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 a923782e2..14510ba83 100644 --- a/ampd/src/handlers/config.rs +++ b/ampd/src/handlers/config.rs @@ -59,6 +59,10 @@ pub enum Config { cosmwasm_contract: TMAddress, http_url: Url, }, + StellarVerifierSetVerifier { + cosmwasm_contract: TMAddress, + http_url: Url, + }, } fn validate_multisig_signer_config<'de, D>(configs: &[Config]) -> Result<(), D::Error> @@ -202,6 +206,22 @@ where } } +fn validate_stellar_verifier_set_verifier_config<'de, D>(configs: &[Config]) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + match configs + .iter() + .filter(|config| matches!(config, Config::StellarMsgVerifier { .. })) + .count() + { + count if count > 1 => Err(de::Error::custom( + "only one Stellar verifier set verifier config is allowed", + )), + _ => Ok(()), + } +} + pub fn deserialize_handler_configs<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -216,6 +236,7 @@ where validate_mvx_msg_verifier_config::(&configs)?; validate_mvx_worker_set_verifier_config::(&configs)?; validate_stellar_msg_verifier_config::(&configs)?; + validate_stellar_verifier_set_verifier_config::(&configs)?; Ok(configs) } diff --git a/ampd/src/handlers/mod.rs b/ampd/src/handlers/mod.rs index 72276b63e..1f8868164 100644 --- a/ampd/src/handlers/mod.rs +++ b/ampd/src/handlers/mod.rs @@ -6,6 +6,7 @@ pub mod multisig; pub mod mvx_verify_msg; pub mod mvx_verify_verifier_set; pub(crate) mod stellar_verify_msg; +pub(crate) mod stellar_verify_verifier_set; pub mod sui_verify_msg; pub mod sui_verify_verifier_set; diff --git a/ampd/src/handlers/stellar_verify_verifier_set.rs b/ampd/src/handlers/stellar_verify_verifier_set.rs index ceed406b1..82b4ff4a5 100644 --- a/ampd/src/handlers/stellar_verify_verifier_set.rs +++ b/ampd/src/handlers/stellar_verify_verifier_set.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use std::convert::TryInto; use async_trait::async_trait; @@ -11,10 +10,9 @@ use events::Event; use events_derive::try_from; use multisig::verifier_set::VerifierSet; use prost_types::Any; -use router_api::ChainName; use serde::Deserialize; use serde_with::{serde_as, DisplayFromStr}; -use stellar_xdr::curr::{ScAddress, ScBytes, ScString}; +use stellar_xdr::curr::ScAddress; use tokio::sync::watch::Receiver; use tracing::{info, info_span}; use valuable::Valuable; @@ -112,7 +110,7 @@ impl EventHandler for Handler { let transaction_response = self .http_client - .transaction_response(verifier_set.tx_id) + .transaction_response(verifier_set.tx_id.clone()) .await .change_context(Error::TxReceipts)?; @@ -145,7 +143,6 @@ impl EventHandler for Handler { #[cfg(test)] mod tests { - use std::collections::HashMap; use std::convert::TryInto; use cosmrs::cosmwasm::MsgExecuteContract; @@ -153,16 +150,18 @@ mod tests { use error_stack::Result; use events::Error::{DeserializationFailed, EventTypeMismatch}; use events::Event; + use multisig::key::KeyType; + use multisig::test::common::{build_verifier_set, ed25519_test_data}; use stellar_xdr::curr::ScAddress; use tokio::sync::watch; use tokio::test as async_test; - use voting_verifier::events::{PollMetadata, PollStarted, TxEventConfirmation}; + use voting_verifier::events::{PollMetadata, PollStarted, VerifierSetConfirmation}; use super::PollStartedEvent; use crate::event_processor::EventHandler; use crate::handlers::tests::{into_structured_event, participants}; use crate::stellar::http_client::Client; - use crate::types::{EVMAddress, Hash, TMAddress}; + use crate::types::{Hash, TMAddress}; use crate::PREFIX; #[test] @@ -257,7 +256,7 @@ mod tests { #[async_test] async fn should_vote_correctly() { let mut client = Client::faux(); - faux::when!(client.transaction_responses).then(|_| Ok(HashMap::new())); + faux::when!(client.transaction_response).then(|_| Ok(None)); let voting_verifier = TMAddress::random(PREFIX); let verifier = TMAddress::random(PREFIX); @@ -274,7 +273,7 @@ mod tests { } fn poll_started_event(participants: Vec, expires_at: u64) -> PollStarted { - PollStarted::Messages { + PollStarted::VerifierSet { metadata: PollMetadata { poll_id: "100".parse().unwrap(), source_chain: "stellar".parse().unwrap(), @@ -291,21 +290,11 @@ mod tests { .map(|addr| cosmwasm_std::Addr::unchecked(addr.to_string())) .collect(), }, - messages: (0..2) - .map(|i| TxEventConfirmation { - tx_id: format!("{:x}", Hash::random()).parse().unwrap(), - event_index: i, - source_address: ScAddress::Contract(stellar_xdr::curr::Hash::from( - Hash::random().0, - )) - .to_string() - .try_into() - .unwrap(), - destination_chain: "ethereum".parse().unwrap(), - destination_address: format!("0x{:x}", EVMAddress::random()).parse().unwrap(), - payload_hash: Hash::random().to_fixed_bytes(), - }) - .collect::>(), + verifier_set: VerifierSetConfirmation { + tx_id: format!("{:x}", Hash::random()).parse().unwrap(), + event_index: 0, + verifier_set: build_verifier_set(KeyType::Ed25519, &ed25519_test_data::signers()), + }, } } } diff --git a/ampd/src/lib.rs b/ampd/src/lib.rs index 0a5842fb4..b9183adac 100644 --- a/ampd/src/lib.rs +++ b/ampd/src/lib.rs @@ -371,6 +371,22 @@ where ), event_processor_config.clone(), ), + handlers::config::Config::StellarVerifierSetVerifier { + cosmwasm_contract, + http_url, + } => self.create_handler_task( + "stellar-verifier-set-verifier", + handlers::stellar_verify_verifier_set::Handler::new( + verifier.clone(), + cosmwasm_contract, + stellar::http_client::Client::new( + http_url.to_string().trim_end_matches('/').into(), + ) + .change_context(Error::Connection)?, + 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/stellar/http_client.rs b/ampd/src/stellar/http_client.rs index d900a3e2c..8711c2970 100644 --- a/ampd/src/stellar/http_client.rs +++ b/ampd/src/stellar/http_client.rs @@ -105,4 +105,17 @@ impl Client { }) .collect::>()) } + + pub async fn transaction_response(&self, tx_hash: String) -> Result, Error> { + let tx_hash = SingleTransactionRequest::new() + .set_transaction_hash(tx_hash) + .map_err(|err_str| report!(Error::TxHash).attach_printable(err_str))?; + + Ok(self + .0 + .get_single_transaction(&tx_hash) + .await + .map(|tx_response| Some(tx_response.into())) + .unwrap_or_default()) + } } diff --git a/ampd/src/stellar/verifier.rs b/ampd/src/stellar/verifier.rs index 409d01534..fa4280bb7 100644 --- a/ampd/src/stellar/verifier.rs +++ b/ampd/src/stellar/verifier.rs @@ -1,12 +1,15 @@ use std::str::FromStr; use axelar_wasm_std::voting::Vote; +use stellar::WeightedSigners; use stellar_xdr::curr::{ContractEventBody, ScAddress, ScSymbol, ScVal, StringM}; use crate::handlers::stellar_verify_msg::Message; +use crate::handlers::stellar_verify_verifier_set::VerifierSetConfirmation; use crate::stellar::http_client::TxResponse; const TOPIC_CALLED: &str = "called"; +const TOPIC_ROTATED: &str = "rotated"; impl PartialEq for Message { fn eq(&self, event: &ContractEventBody) -> bool { @@ -41,22 +44,78 @@ impl PartialEq for Message { } } -pub fn verify_message(gateway_address: &ScAddress, tx_receipt: &TxResponse, msg: &Message) -> Vote { - if tx_receipt.has_failed() { - return Vote::FailedOnChain; +impl PartialEq for VerifierSetConfirmation { + fn eq(&self, event: &ContractEventBody) -> bool { + let ContractEventBody::V0(body) = event; + + if body.topics.len() != 3 { + return false; + } + + let [symbol, _, signer_hash] = &body.topics[..] else { + return false; + }; + + let expected_topic: ScVal = + ScSymbol(StringM::from_str(TOPIC_ROTATED).expect("must convert str to ScSymbol")) + .into(); + + WeightedSigners::try_from(&self.verifier_set) + .ok() + .and_then(|signers| signers.hash().ok()) + .and_then(|hash| ScVal::try_from(hash).ok()) + .map_or(false, |hash| { + symbol == &expected_topic && signer_hash == &hash + }) } +} + +pub fn verify_message(gateway_address: &ScAddress, tx_receipt: &TxResponse, msg: &Message) -> Vote { + verify( + gateway_address, + tx_receipt, + msg, + msg.tx_id.clone(), + msg.event_index, + ) +} - if msg.tx_id != tx_receipt.transaction_hash { +pub fn verify_verifier_set( + gateway_address: &ScAddress, + tx_receipt: &TxResponse, + verifier_set_confirmation: &VerifierSetConfirmation, +) -> Vote { + verify( + gateway_address, + tx_receipt, + verifier_set_confirmation, + verifier_set_confirmation.tx_id.clone(), + verifier_set_confirmation.event_index, + ) +} + +fn verify<'a>( + gateway_address: &ScAddress, + tx_receipt: &'a TxResponse, + to_verify: impl PartialEq<&'a ContractEventBody>, + expected_tx_id: String, + expected_event_index: u32, +) -> Vote { + if expected_tx_id != tx_receipt.transaction_hash { return Vote::NotFound; } - match tx_receipt.event(msg.event_index) { + if tx_receipt.has_failed() { + return Vote::FailedOnChain; + } + + match tx_receipt.event(expected_event_index) { Some(event) if event .clone() .contract_id .is_some_and(|hash| ScAddress::Contract(hash) == *gateway_address) - && msg == &event.body => + && to_verify == &event.body => { Vote::SucceededOnChain } @@ -70,17 +129,26 @@ mod test { use axelar_wasm_std::voting::Vote; use cosmrs::tx::MessageExt; + use cosmwasm_std::{Addr, HexBinary, Uint128}; use ed25519_dalek::SigningKey; + use multisig::key::KeyType; + use multisig::msg::Signer; + use multisig::verifier_set::VerifierSet; use rand::rngs::OsRng; + use stellar::WeightedSigners; use stellar_xdr::curr::{ AccountId, BytesM, ContractEvent, ContractEventBody, ContractEventType, ContractEventV0, PublicKey, ScAddress, ScBytes, ScString, ScSymbol, ScVal, StringM, Uint256, }; use crate::handlers::stellar_verify_msg::Message; + use crate::handlers::stellar_verify_verifier_set::VerifierSetConfirmation; use crate::stellar::http_client::TxResponse; - use crate::stellar::verifier::{verify_message, TOPIC_CALLED}; + use crate::stellar::verifier::{ + verify_message, verify_verifier_set, TOPIC_CALLED, TOPIC_ROTATED, + }; use crate::types::{EVMAddress, Hash}; + use crate::PREFIX; #[test] fn should_not_verify_msg_if_tx_id_does_not_match() { @@ -109,8 +177,7 @@ mod test { let (gateway_address, tx_response, mut msg) = matching_msg_and_tx_block(); // Generate a different source address - let mut csprng = OsRng; - let signing_key = SigningKey::generate(&mut csprng); + let signing_key = SigningKey::generate(&mut OsRng); let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256::from( signing_key.verifying_key().to_bytes(), ))); @@ -167,12 +234,63 @@ mod test { ); } + #[test] + fn should_not_verify_verifier_set_if_tx_id_does_not_match() { + let (gateway_address, tx_response, mut confirmation) = matching_verifier_set_and_tx_block(); + confirmation.tx_id = "different_tx_hash".to_string(); + + assert_eq!( + verify_verifier_set(&gateway_address, &tx_response, &confirmation), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_verifier_set_if_event_index_does_not_match() { + let (gateway_address, tx_response, mut confirmation) = matching_verifier_set_and_tx_block(); + confirmation.event_index = 1; + + assert_eq!( + verify_verifier_set(&gateway_address, &tx_response, &confirmation), + Vote::NotFound + ); + } + + #[test] + fn should_not_verify_verifier_set_if_signer_hash_does_not_match() { + let (gateway_address, tx_response, mut confirmation) = matching_verifier_set_and_tx_block(); + + let signers = vec![random_signer(), random_signer(), random_signer()]; + confirmation.verifier_set = VerifierSet { + signers: signers + .iter() + .map(|signer| (signer.address.to_string(), signer.clone())) + .collect(), + threshold: Uint128::new(2u128), + created_at: rand::random(), + }; + + assert_eq!( + verify_verifier_set(&gateway_address, &tx_response, &confirmation), + Vote::NotFound + ); + } + + #[test] + fn should_verify_verifier_set_if_correct() { + let (gateway_address, tx_response, confirmation) = matching_verifier_set_and_tx_block(); + + assert_eq!( + verify_verifier_set(&gateway_address, &tx_response, &confirmation), + Vote::SucceededOnChain + ); + } + fn matching_msg_and_tx_block() -> (ScAddress, TxResponse, Message) { let account_id = stellar_xdr::curr::Hash::from(Hash::random().0); let gateway_address = ScAddress::Contract(account_id.clone()); - let mut csprng = OsRng; - let signing_key = SigningKey::generate(&mut csprng); + let signing_key = SigningKey::generate(&mut OsRng); let msg = Message { tx_id: Hash::random().to_string(), @@ -223,4 +341,76 @@ mod test { (gateway_address, tx_response, msg) } + + fn matching_verifier_set_and_tx_block() -> (ScAddress, TxResponse, VerifierSetConfirmation) { + let account_id = stellar_xdr::curr::Hash::from(Hash::random().0); + let gateway_address = ScAddress::Contract(account_id.clone()); + + let signers = vec![random_signer(), random_signer(), random_signer()]; + let created_at = rand::random(); + let threshold = Uint128::new(2u128); + + let verifier_set_confirmation = VerifierSetConfirmation { + tx_id: Hash::random().to_string(), + event_index: 0, + verifier_set: VerifierSet { + signers: signers + .iter() + .map(|signer| (signer.address.to_string(), signer.clone())) + .collect(), + threshold, + created_at, + }, + }; + + let weighted_signers = + WeightedSigners::try_from(&verifier_set_confirmation.verifier_set).unwrap(); + let signer_hash = weighted_signers.hash().unwrap(); + + let event_body = ContractEventBody::V0(ContractEventV0 { + topics: vec![ + ScVal::Symbol(ScSymbol(StringM::from_str(TOPIC_ROTATED).unwrap())), + ScVal::Bytes(ScBytes( + BytesM::try_from(Hash::random().to_fixed_bytes()).unwrap(), + )), + ScVal::try_from(signer_hash).unwrap(), + ] + .try_into() + .unwrap(), + data: ScVal::Vec(None), + }); + + let event = ContractEvent { + ext: stellar_xdr::curr::ExtensionPoint::V0, + contract_id: Some(account_id), + type_: ContractEventType::Contract, + body: event_body, + }; + + let tx_response = TxResponse { + transaction_hash: verifier_set_confirmation.tx_id.clone(), + source_address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519( + Uint256::from(SigningKey::generate(&mut OsRng).verifying_key().to_bytes()), + ))), + successful: true, + contract_events: Some(vec![event].try_into().unwrap()), + }; + + (gateway_address, tx_response, verifier_set_confirmation) + } + + pub fn random_signer() -> Signer { + let priv_key: ecdsa::SigningKey = ecdsa::SigningKey::random(&mut OsRng); + let pub_key: cosmrs::crypto::PublicKey = priv_key.verifying_key().into(); + + let ed25519_pub_key = SigningKey::generate(&mut OsRng).verifying_key().to_bytes(); + + Signer { + address: Addr::unchecked(pub_key.account_id(PREFIX).unwrap()), + weight: Uint128::one(), + pub_key: (KeyType::Ed25519, HexBinary::from(ed25519_pub_key)) + .try_into() + .unwrap(), + } + } } diff --git a/ampd/src/tests/config_template.toml b/ampd/src/tests/config_template.toml index 4eac79264..4e6828eb6 100644 --- a/ampd/src/tests/config_template.toml +++ b/ampd/src/tests/config_template.toml @@ -77,6 +77,11 @@ type = 'StellarMsgVerifier' cosmwasm_contract = 'axelar1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqecnww6' http_url = 'http://127.0.0.1/' +[[handlers]] +type = 'StellarVerifierSetVerifier' +cosmwasm_contract = 'axelar1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqecnww6' +http_url = 'http://127.0.0.1/' + [tofnd_config] url = 'http://localhost:50051/' party_uid = 'ampd'