From a31338556d24beeab090c2f78053d969902458e6 Mon Sep 17 00:00:00 2001 From: benluelo Date: Fri, 7 Nov 2025 16:42:57 +0000 Subject: [PATCH 01/12] wip --- Cargo.lock | 625 ++++++++++++++++++++++- Cargo.toml | 2 + voyager/plugins/attestor/evm/Cargo.toml | 31 ++ voyager/plugins/attestor/evm/src/call.rs | 15 + voyager/plugins/attestor/evm/src/main.rs | 291 +++++++++++ 5 files changed, 951 insertions(+), 13 deletions(-) create mode 100644 voyager/plugins/attestor/evm/Cargo.toml create mode 100644 voyager/plugins/attestor/evm/src/call.rs create mode 100644 voyager/plugins/attestor/evm/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 68a63b0d12..d0a5b2cfe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,65 @@ dependencies = [ "zeroize", ] +[[package]] +name = "age" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc171f4874fa10887e47088f81a55fcf030cd421aa31ec2b370cafebcc608a" +dependencies = [ + "aes", + "aes-gcm", + "age-core", + "base64 0.21.7", + "bcrypt-pbkdf", + "bech32 0.9.1", + "cbc", + "chacha20poly1305", + "cipher", + "console", + "cookie-factory", + "ctr", + "curve25519-dalek", + "hmac 0.12.1", + "i18n-embed", + "i18n-embed-fl", + "is-terminal", + "lazy_static", + "nom", + "num-traits", + "pin-project", + "pinentry", + "rand 0.8.5", + "rpassword", + "rsa 0.9.8", + "rust-embed", + "scrypt", + "sha2 0.10.9", + "subtle 2.6.1", + "which", + "wsl", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "io_tee", + "nom", + "rand 0.8.5", + "secrecy", + "sha2 0.10.9", + "tempfile", +] + [[package]] name = "ahash" version = "0.8.11" @@ -2178,6 +2237,26 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2 0.12.2", + "sha2 0.10.9", +] + [[package]] name = "bcs" version = "0.1.6" @@ -2362,7 +2441,7 @@ dependencies = [ "hmac 0.12.1", "k256 0.11.6", "once_cell", - "pbkdf2", + "pbkdf2 0.11.0", "rand_core 0.6.4", "ripemd", "sha2 0.10.9", @@ -2559,6 +2638,12 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.7.3" @@ -3053,6 +3138,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chain-kitchen" version = "0.0.0" @@ -3110,6 +3219,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -3135,6 +3245,15 @@ dependencies = [ "terminal_size", ] +[[package]] +name = "clap_complete" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.32" @@ -3153,6 +3272,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clap_mangen" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "cliclack" version = "0.2.5" @@ -3543,6 +3672,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -5455,6 +5593,19 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -5960,6 +6111,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "fixed-hash" version = "0.7.0" @@ -6024,6 +6184,50 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "flume" version = "0.11.1" @@ -7024,6 +7228,73 @@ dependencies = [ "tracing", ] +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "locale_config", + "log", + "parking_lot", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.101", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -7185,7 +7456,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -7196,9 +7467,9 @@ checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", - "tinystr", + "tinystr 0.7.6", "writeable", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -7211,8 +7482,8 @@ dependencies = [ "icu_locid", "icu_locid_transform_data", "icu_provider", - "tinystr", - "zerovec", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] @@ -7236,7 +7507,7 @@ dependencies = [ "utf16_iter", "utf8_iter", "write16", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -7256,8 +7527,8 @@ dependencies = [ "icu_locid_transform", "icu_properties_data", "icu_provider", - "tinystr", - "zerovec", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] @@ -7276,11 +7547,11 @@ dependencies = [ "icu_locid", "icu_provider_macros", "stable_deref_trait", - "tinystr", + "tinystr 0.7.6", "writeable", "yoke", "zerofrom", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -7481,6 +7752,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "inventory" version = "0.3.20" @@ -7501,6 +7791,12 @@ dependencies = [ "libc", ] +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + [[package]] name = "ipnet" version = "2.11.0" @@ -8309,6 +8605,19 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -8545,6 +8854,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "maplit" version = "1.0.2" @@ -9676,6 +9994,35 @@ dependencies = [ "smallvec", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.36.7" @@ -10247,6 +10594,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "pem" version = "3.0.5" @@ -10513,6 +10870,20 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinentry" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ecb857a7b11a03e8872c704d0a1ae1efc20533b3be98338008527a1928ffa6" +dependencies = [ + "log", + "nom", + "percent-encoding", + "secrecy", + "which", + "zeroize", +] + [[package]] name = "pkcs1" version = "0.4.1" @@ -10562,6 +10933,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug 0.3.1", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -11052,6 +11434,28 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rage" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b8bd06353dd574797ae7fdb520db5f8773e253bc68b38684c7d3823d23a8726" +dependencies = [ + "age", + "chrono", + "clap", + "clap_complete", + "clap_mangen", + "console", + "env_logger", + "flate2", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "log", + "pinentry", + "rust-embed", +] + [[package]] name = "rand" version = "0.6.5" @@ -11685,12 +12089,29 @@ dependencies = [ "byteorder", ] +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "route-recognizer" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" +[[package]] +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + [[package]] name = "rsa" version = "0.8.2" @@ -11732,6 +12153,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rtoolbox" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "ruint" version = "1.17.0" @@ -11780,6 +12211,40 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.101", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +dependencies = [ + "sha2 0.10.9", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -12049,6 +12514,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -12200,6 +12674,17 @@ dependencies = [ "unionlabs", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2 0.12.2", + "salsa20", + "sha2 0.10.9", +] + [[package]] name = "sct" version = "0.7.1" @@ -12279,6 +12764,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -12315,6 +12809,21 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.1", +] + +[[package]] +name = "self_cell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" + [[package]] name = "semver" version = "0.11.0" @@ -14540,7 +15049,7 @@ dependencies = [ "anyhow", "hmac 0.12.1", "once_cell", - "pbkdf2", + "pbkdf2 0.11.0", "rand 0.8.5", "rustc-hash 1.1.0", "sha2 0.10.9", @@ -14579,7 +15088,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", - "zerovec", + "zerovec 0.10.4", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec 0.11.5", ] [[package]] @@ -15207,6 +15727,15 @@ dependencies = [ "static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + [[package]] name = "typed-arena" version = "2.0.2" @@ -15460,6 +15989,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr 0.8.2", +] + [[package]] name = "unicase" version = "2.8.1" @@ -17052,6 +17600,29 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-plugin-attestor-evm" +version = "0.0.0" +dependencies = [ + "alloy", + "clap", + "embed-commit", + "enumorph", + "ibc-solidity", + "ibc-union-spec", + "jsonrpsee 0.25.1", + "macros", + "rage", + "serde", + "serde-utils", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "unionlabs", + "voyager-sdk", +] + [[package]] name = "voyager-plugin-packet-batch" version = "0.0.0" @@ -18138,6 +18709,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "whoami" version = "1.6.0" @@ -18672,6 +19255,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wsl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4" + [[package]] name = "wyz" version = "0.2.0" @@ -18851,6 +19440,16 @@ dependencies = [ "zerovec-derive", ] +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "zerofrom", +] + [[package]] name = "zerovec-derive" version = "0.10.3" diff --git a/Cargo.toml b/Cargo.toml index 4cbde99f64..8f118e4292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -227,6 +227,8 @@ members = [ "voyager/plugins/sui-ibc-app", "voyager/plugins/sui-packet-timeout", + "voyager/plugins/attestor/evm", + "drip", # "lib/aptos-verifier", diff --git a/voyager/plugins/attestor/evm/Cargo.toml b/voyager/plugins/attestor/evm/Cargo.toml new file mode 100644 index 0000000000..1c300f5c71 --- /dev/null +++ b/voyager/plugins/attestor/evm/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "voyager-plugin-attestor-evm" +version = "0.0.0" + +authors = { workspace = true } +edition = { workspace = true } +license-file = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } + +[lints] +workspace = true + +[dependencies] +alloy = { workspace = true, features = ["contract", "network", "providers", "rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "reqwest-rustls-tls", "provider-ws"] } +clap = { workspace = true, features = ["default", "derive", "env", "error-context", "color"] } +embed-commit = { workspace = true } +enumorph = { workspace = true } +ibc-solidity = { workspace = true, features = ["rpc"] } +ibc-union-spec = { workspace = true, features = ["serde", "ethabi"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +macros = { workspace = true } +rage = "0.11.1" +serde = { workspace = true, features = ["derive"] } +serde-utils = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-sdk = { workspace = true } diff --git a/voyager/plugins/attestor/evm/src/call.rs b/voyager/plugins/attestor/evm/src/call.rs new file mode 100644 index 0000000000..3c2705b947 --- /dev/null +++ b/voyager/plugins/attestor/evm/src/call.rs @@ -0,0 +1,15 @@ +use macros::model; +use unionlabs::primitives::H256; + +#[model] +pub enum ModuleCall { + VerifyState(VerifyState), + SubmitAttestation(SubmitAttestation), +} + +#[model] +pub struct SubmitAttestation { + pub event: ibc_union_spec::event::FullEvent, + pub tx_hash: H256, + pub height: u64, +} diff --git a/voyager/plugins/attestor/evm/src/main.rs b/voyager/plugins/attestor/evm/src/main.rs new file mode 100644 index 0000000000..7614e1d188 --- /dev/null +++ b/voyager/plugins/attestor/evm/src/main.rs @@ -0,0 +1,291 @@ +use std::{collections::VecDeque, ops::Deref, panic::AssertUnwindSafe, path::PathBuf, sync::Arc}; + +use alloy::{ + network::AnyNetwork, + providers::{DynProvider, Provider, ProviderBuilder}, +}; +use clap::Subcommand; +use ibc_solidity::Ibc::{self, IbcInstance}; +use ibc_union_spec::{ + IbcUnion, + event::FullEvent, + path::{ConnectionPath, StorePath}, +}; +use jsonrpsee::{ + Extensions, + core::{RpcResult, async_trait}, + types::ErrorObject, +}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use tracing::instrument; +use unionlabs::{ + ErrorReporter, + never::Never, + primitives::{H160, H256}, +}; +use voyager_sdk::{ + anyhow::{self, bail}, + hook::{SubmitTxHook, simple_take_filter}, + message::{PluginMessage, VoyagerMessage, data::Data}, + plugin::Plugin, + primitives::{ChainId, IbcSpec}, + rpc::{FATAL_JSONRPC_ERROR_CODE, PluginServer, types::PluginInfo}, + vm::{Op, Visit, call, defer, now, pass::PassResult, seq}, +}; + +use crate::call::{ModuleCall, SubmitAttestation}; + +pub mod call; + +#[tokio::main] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module(Arc); + +impl Deref for Module { + type Target = ModuleInner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug)] +pub struct ModuleInner { + pub chain_id: ChainId, + + /// The address of the `IBCHandler` smart contract. + pub ibc_handler_address: H160, + + pub provider: DynProvider, + + pub attestation_key: (), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub chain_id: ChainId, + #[serde(default)] + pub additional_chain_ids: Vec, + + /// The address of the `IBCHandler` smart contract. + pub ibc_handler_address: H160, + + /// The RPC endpoint for the execution chain. + pub rpc_url: String, + + pub attestation_key_path: PathBuf, +} + +#[derive(Subcommand)] +pub enum Cmd {} + +impl Plugin for Module { + type Call = ModuleCall; + type Callback = Never; + + type Config = Config; + type Cmd = Cmd; + + async fn new(config: Self::Config) -> anyhow::Result { + let provider = DynProvider::new( + ProviderBuilder::new() + .network::() + .connect(&config.rpc_url) + .await?, + ); + + let raw_chain_id = provider.get_chain_id().await?; + let chain_id = ChainId::new(raw_chain_id.to_string()); + + if chain_id != config.chain_id { + bail!( + "incorrect chain id: expected `{}`, but found `{}`", + config.chain_id, + chain_id + ); + } + + Ok(Self(Arc::new(ModuleInner { + chain_id, + ibc_handler_address: config.ibc_handler_address, + provider, + attestation_key: (), + }))) + } + + fn info(config: Self::Config) -> PluginInfo { + PluginInfo { + name: plugin_name(&config.chain_id), + interest_filter: simple_take_filter(format!( + r#" +if ."@type" == "data" then + ."@value" as $data | + + # pull all ibc events from this chain, they will be verified and attested to + + $data."@type" == "ibc_event" and $data."@value".chain_id == "{chain_id}" and $data."@value".ibc_spec_id == "{ibc_union_id}" +else + false +end +"#, + chain_id = config.chain_id, + ibc_union_id = IbcUnion::ID, + )), + } + } + + async fn cmd(config: Self::Config, cmd: Self::Cmd) { + let plugin = Self::new(config).await.unwrap(); + + match cmd {} + } +} + +fn plugin_name(chain_id: &ChainId) -> String { + pub const PLUGIN_NAME: &str = env!("CARGO_PKG_NAME"); + + format!("{PLUGIN_NAME}/{}", chain_id) +} + +impl Module { + fn plugin_name(&self) -> String { + plugin_name(&self.chain_id) + } +} + +#[async_trait] +impl PluginServer for Module { + async fn run_pass( + &self, + _: &Extensions, + msgs: Vec>, + ) -> RpcResult> { + Ok(PassResult { + optimize_further: vec![], + ready: msgs + .into_iter() + .enumerate() + .map(|(idx, mut op)| { + let op = match op.into_data().unwrap() { + Data::IbcEvent(chain_event) => call(PluginMessage::new( + plugin_name(&self.chain_id), + ModuleCall::SubmitAttestation(SubmitAttestation { + event: chain_event.decode_event::().unwrap().unwrap(), + tx_hash: chain_event.tx_hash, + height: chain_event.provable_height.height().height(), + }), + )), + _ => todo!(), + }; + + (vec![idx], op) + }) + .collect(), + }) + } + + #[instrument(skip_all, fields(chain_id = %self.chain_id))] + async fn call(&self, _: &Extensions, msg: ModuleCall) -> RpcResult> { + match msg { + ModuleCall::SubmitAttestation(SubmitAttestation { + event, + tx_hash, + height, + }) => { + self.submit_attestation(event, tx_hash, height).await?; + + Ok(noop()) + } + } + } + + #[instrument(skip_all, fields(chain_id = %self.chain_id))] + async fn callback( + &self, + _: &Extensions, + cb: Never, + _data: VecDeque, + ) -> RpcResult> { + match cb {} + } +} + +impl Module { + fn ibc_handler(&self) -> IbcInstance, AnyNetwork> { + Ibc::new::<_, AnyNetwork>(self.ibc_handler_address.get().into(), self.provider.clone()) + } + + async fn submit_attestation( + &self, + event: FullEvent, + tx_hash: H256, + height: u64, + ) -> RpcResult<()> { + let (k, v) = match event { + FullEvent::CreateClient(event) => return Ok(()), + FullEvent::UpdateClient(event) => return Ok(()), + FullEvent::ConnectionOpenInit(event) => { + // ConnectionPath { + // connection_id: event.connection_id, + // } + // .key() + + let connection = self + .ibc_handler() + .connections(event.connection_id.raw()) + .call() + .await?; + } + FullEvent::ConnectionOpenTry(event) => todo!(), + FullEvent::ConnectionOpenAck(event) => todo!(), + FullEvent::ConnectionOpenConfirm(event) => todo!(), + FullEvent::ChannelOpenInit(event) => todo!(), + FullEvent::ChannelOpenTry(event) => todo!(), + FullEvent::ChannelOpenAck(event) => todo!(), + FullEvent::ChannelOpenConfirm(event) => todo!(), + FullEvent::ChannelCloseInit(event) => todo!(), + FullEvent::ChannelCloseConfirm(event) => todo!(), + FullEvent::PacketSend(event) => todo!(), + FullEvent::BatchSend(event) => todo!(), + FullEvent::PacketRecv(event) => todo!(), + FullEvent::IntentPacketRecv(event) => todo!(), + FullEvent::WriteAck(event) => todo!(), + FullEvent::PacketAck(event) => todo!(), + FullEvent::PacketTimeout(event) => todo!(), + }; + + let tx = self + .provider + .get_transaction_by_hash(tx_hash.get().into()) + .await + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching source transaction"), + None::<()>, + ) + })? + .ok_or_else(|| ErrorObject::owned(-1, format!("tx {tx_hash} not found"), None::<()>))?; + + let tx_height = tx.block_number.unwrap(); + + if tx_height != height { + return Err(ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!( + "block number is inconsistent; event height \ + is {height} but tx height is {tx_height}" + ), + None::<()>, + )); + } + + Ok(()) + } +} From 38eeda860d15e658ef3ddcfa25b6aad1a037eefd Mon Sep 17 00:00:00 2001 From: benluelo Date: Fri, 7 Nov 2025 16:43:12 +0000 Subject: [PATCH 02/12] wip --- voyager/plugins/attestor/evm/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voyager/plugins/attestor/evm/src/main.rs b/voyager/plugins/attestor/evm/src/main.rs index 7614e1d188..8b1789ff5c 100644 --- a/voyager/plugins/attestor/evm/src/main.rs +++ b/voyager/plugins/attestor/evm/src/main.rs @@ -240,7 +240,8 @@ impl Module { .ibc_handler() .connections(event.connection_id.raw()) .call() - .await?; + .await + .map_err(|e| ErrorObject::owned(-1))?; } FullEvent::ConnectionOpenTry(event) => todo!(), FullEvent::ConnectionOpenAck(event) => todo!(), From 2601ea9b53fb21309d56b375d0797a3e23bd48ec Mon Sep 17 00:00:00 2001 From: benluelo Date: Sun, 9 Nov 2025 13:36:48 +0000 Subject: [PATCH 03/12] wip --- Cargo.lock | 609 +---------------------- Cargo.toml | 1 + voyager/plugins/attestor/evm/Cargo.toml | 39 +- voyager/plugins/attestor/evm/src/call.rs | 1 - voyager/plugins/attestor/evm/src/main.rs | 326 ++++++++++-- 5 files changed, 314 insertions(+), 662 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0a5b2cfe0..dcb67d3f79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,65 +220,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "age" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fc171f4874fa10887e47088f81a55fcf030cd421aa31ec2b370cafebcc608a" -dependencies = [ - "aes", - "aes-gcm", - "age-core", - "base64 0.21.7", - "bcrypt-pbkdf", - "bech32 0.9.1", - "cbc", - "chacha20poly1305", - "cipher", - "console", - "cookie-factory", - "ctr", - "curve25519-dalek", - "hmac 0.12.1", - "i18n-embed", - "i18n-embed-fl", - "is-terminal", - "lazy_static", - "nom", - "num-traits", - "pin-project", - "pinentry", - "rand 0.8.5", - "rpassword", - "rsa 0.9.8", - "rust-embed", - "scrypt", - "sha2 0.10.9", - "subtle 2.6.1", - "which", - "wsl", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "age-core" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" -dependencies = [ - "base64 0.21.7", - "chacha20poly1305", - "cookie-factory", - "hkdf", - "io_tee", - "nom", - "rand 0.8.5", - "secrecy", - "sha2 0.10.9", - "tempfile", -] - [[package]] name = "ahash" version = "0.8.11" @@ -2237,26 +2178,6 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - -[[package]] -name = "bcrypt-pbkdf" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" -dependencies = [ - "blowfish", - "pbkdf2 0.12.2", - "sha2 0.10.9", -] - [[package]] name = "bcs" version = "0.1.6" @@ -2441,7 +2362,7 @@ dependencies = [ "hmac 0.12.1", "k256 0.11.6", "once_cell", - "pbkdf2 0.11.0", + "pbkdf2", "rand_core 0.6.4", "ripemd", "sha2 0.10.9", @@ -2638,12 +2559,6 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block-buffer" version = "0.7.3" @@ -3138,30 +3053,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chain-kitchen" version = "0.0.0" @@ -3219,7 +3110,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", - "zeroize", ] [[package]] @@ -3245,15 +3135,6 @@ dependencies = [ "terminal_size", ] -[[package]] -name = "clap_complete" -version = "4.5.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" -dependencies = [ - "clap", -] - [[package]] name = "clap_derive" version = "4.5.32" @@ -3272,16 +3153,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "clap_mangen" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" -dependencies = [ - "clap", - "roff", -] - [[package]] name = "cliclack" version = "0.2.5" @@ -3672,15 +3543,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -5593,19 +5455,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -6111,15 +5960,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "find-crate" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml 0.5.11", -] - [[package]] name = "fixed-hash" version = "0.7.0" @@ -6184,50 +6024,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fluent" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" -dependencies = [ - "fluent-bundle", - "unic-langid", -] - -[[package]] -name = "fluent-bundle" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" -dependencies = [ - "fluent-langneg", - "fluent-syntax", - "intl-memoizer", - "intl_pluralrules", - "rustc-hash 1.1.0", - "self_cell 0.10.3", - "smallvec", - "unic-langid", -] - -[[package]] -name = "fluent-langneg" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" -dependencies = [ - "unic-langid", -] - -[[package]] -name = "fluent-syntax" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "flume" version = "0.11.1" @@ -7228,73 +7024,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "i18n-config" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" -dependencies = [ - "basic-toml", - "log", - "serde", - "serde_derive", - "thiserror 1.0.69", - "unic-langid", -] - -[[package]] -name = "i18n-embed" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" -dependencies = [ - "arc-swap", - "fluent", - "fluent-langneg", - "fluent-syntax", - "i18n-embed-impl", - "intl-memoizer", - "locale_config", - "log", - "parking_lot", - "rust-embed", - "thiserror 1.0.69", - "unic-langid", - "walkdir", -] - -[[package]] -name = "i18n-embed-fl" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" -dependencies = [ - "find-crate", - "fluent", - "fluent-syntax", - "i18n-config", - "i18n-embed", - "proc-macro-error2", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.101", - "unic-langid", -] - -[[package]] -name = "i18n-embed-impl" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" -dependencies = [ - "find-crate", - "i18n-config", - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "iana-time-zone" version = "0.1.63" @@ -7456,7 +7185,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", - "zerovec 0.10.4", + "zerovec", ] [[package]] @@ -7467,9 +7196,9 @@ checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", - "tinystr 0.7.6", + "tinystr", "writeable", - "zerovec 0.10.4", + "zerovec", ] [[package]] @@ -7482,8 +7211,8 @@ dependencies = [ "icu_locid", "icu_locid_transform_data", "icu_provider", - "tinystr 0.7.6", - "zerovec 0.10.4", + "tinystr", + "zerovec", ] [[package]] @@ -7507,7 +7236,7 @@ dependencies = [ "utf16_iter", "utf8_iter", "write16", - "zerovec 0.10.4", + "zerovec", ] [[package]] @@ -7527,8 +7256,8 @@ dependencies = [ "icu_locid_transform", "icu_properties_data", "icu_provider", - "tinystr 0.7.6", - "zerovec 0.10.4", + "tinystr", + "zerovec", ] [[package]] @@ -7547,11 +7276,11 @@ dependencies = [ "icu_locid", "icu_provider_macros", "stable_deref_trait", - "tinystr 0.7.6", + "tinystr", "writeable", "yoke", "zerofrom", - "zerovec 0.10.4", + "zerovec", ] [[package]] @@ -7752,25 +7481,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "intl-memoizer" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" -dependencies = [ - "type-map", - "unic-langid", -] - -[[package]] -name = "intl_pluralrules" -version = "7.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" -dependencies = [ - "unic-langid", -] - [[package]] name = "inventory" version = "0.3.20" @@ -7791,12 +7501,6 @@ dependencies = [ "libc", ] -[[package]] -name = "io_tee" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" - [[package]] name = "ipnet" version = "2.11.0" @@ -8605,19 +8309,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" -[[package]] -name = "locale_config" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" -dependencies = [ - "lazy_static", - "objc", - "objc-foundation", - "regex", - "winapi", -] - [[package]] name = "lock_api" version = "0.4.12" @@ -8854,15 +8545,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "maplit" version = "1.0.2" @@ -9994,35 +9676,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.36.7" @@ -10594,16 +10247,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pem" version = "3.0.5" @@ -10870,20 +10513,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pinentry" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ecb857a7b11a03e8872c704d0a1ae1efc20533b3be98338008527a1928ffa6" -dependencies = [ - "log", - "nom", - "percent-encoding", - "secrecy", - "which", - "zeroize", -] - [[package]] name = "pkcs1" version = "0.4.1" @@ -10933,17 +10562,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug 0.3.1", - "universal-hash", -] - [[package]] name = "polyval" version = "0.6.2" @@ -11434,28 +11052,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rage" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8bd06353dd574797ae7fdb520db5f8773e253bc68b38684c7d3823d23a8726" -dependencies = [ - "age", - "chrono", - "clap", - "clap_complete", - "clap_mangen", - "console", - "env_logger", - "flate2", - "i18n-embed", - "i18n-embed-fl", - "lazy_static", - "log", - "pinentry", - "rust-embed", -] - [[package]] name = "rand" version = "0.6.5" @@ -12089,29 +11685,12 @@ dependencies = [ "byteorder", ] -[[package]] -name = "roff" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" - [[package]] name = "route-recognizer" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" -[[package]] -name = "rpassword" -version = "7.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" -dependencies = [ - "libc", - "rtoolbox", - "windows-sys 0.59.0", -] - [[package]] name = "rsa" version = "0.8.2" @@ -12153,16 +11732,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rtoolbox" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "ruint" version = "1.17.0" @@ -12211,40 +11780,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rust-embed" -version = "8.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.101", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" -dependencies = [ - "sha2 0.10.9", - "walkdir", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -12514,15 +12049,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -12674,17 +12200,6 @@ dependencies = [ "unionlabs", ] -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2 0.12.2", - "salsa20", - "sha2 0.10.9", -] - [[package]] name = "sct" version = "0.7.1" @@ -12764,15 +12279,6 @@ dependencies = [ "cc", ] -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "zeroize", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -12809,21 +12315,6 @@ dependencies = [ "libc", ] -[[package]] -name = "self_cell" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" -dependencies = [ - "self_cell 1.2.1", -] - -[[package]] -name = "self_cell" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" - [[package]] name = "semver" version = "0.11.0" @@ -15049,7 +14540,7 @@ dependencies = [ "anyhow", "hmac 0.12.1", "once_cell", - "pbkdf2 0.11.0", + "pbkdf2", "rand 0.8.5", "rustc-hash 1.1.0", "sha2 0.10.9", @@ -15088,18 +14579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", - "zerovec 0.10.4", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec 0.11.5", + "zerovec", ] [[package]] @@ -15727,15 +15207,6 @@ dependencies = [ "static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash 2.1.1", -] - [[package]] name = "typed-arena" version = "2.0.2" @@ -15989,25 +15460,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" -[[package]] -name = "unic-langid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" -dependencies = [ - "unic-langid-impl", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" -dependencies = [ - "serde", - "tinystr 0.8.2", -] - [[package]] name = "unicase" version = "2.8.1" @@ -17605,14 +17057,19 @@ name = "voyager-plugin-attestor-evm" version = "0.0.0" dependencies = [ "alloy", + "attested-light-client", "clap", + "cometbft-rpc", + "concurrent-keyring", + "cosmos-client", + "ed25519-dalek", "embed-commit", "enumorph", "ibc-solidity", "ibc-union-spec", "jsonrpsee 0.25.1", "macros", - "rage", + "protos", "serde", "serde-utils", "serde_json", @@ -18709,18 +18166,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "whoami" version = "1.6.0" @@ -19255,12 +18700,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wsl" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4" - [[package]] name = "wyz" version = "0.2.0" @@ -19440,16 +18879,6 @@ dependencies = [ "zerovec-derive", ] -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "serde", - "zerofrom", -] - [[package]] name = "zerovec-derive" version = "0.10.3" diff --git a/Cargo.toml b/Cargo.toml index 8f118e4292..c79a955de6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -348,6 +348,7 @@ arbitrum-client = { path = "lib/arbitrum-client", defa arbitrum-light-client-types = { path = "lib/arbitrum-light-client-types", default-features = false } arbitrum-types = { path = "lib/arbitrum-types", default-features = false } arbitrum-verifier = { path = "lib/arbitrum-verifier", default-features = false } +attested-light-client = { path = "cosmwasm/ibc-union/lightclient/attested", default-features = false } attested-light-client-types = { path = "lib/attested-light-client-types", default-features = false } base-client = { path = "lib/base-client", default-features = false } base-light-client-types = { path = "lib/base-light-client-types", default-features = false } diff --git a/voyager/plugins/attestor/evm/Cargo.toml b/voyager/plugins/attestor/evm/Cargo.toml index 1c300f5c71..42c5f411a9 100644 --- a/voyager/plugins/attestor/evm/Cargo.toml +++ b/voyager/plugins/attestor/evm/Cargo.toml @@ -12,20 +12,25 @@ repository = { workspace = true } workspace = true [dependencies] -alloy = { workspace = true, features = ["contract", "network", "providers", "rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "reqwest-rustls-tls", "provider-ws"] } -clap = { workspace = true, features = ["default", "derive", "env", "error-context", "color"] } -embed-commit = { workspace = true } -enumorph = { workspace = true } -ibc-solidity = { workspace = true, features = ["rpc"] } -ibc-union-spec = { workspace = true, features = ["serde", "ethabi"] } -jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } -macros = { workspace = true } -rage = "0.11.1" -serde = { workspace = true, features = ["derive"] } -serde-utils = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -unionlabs = { workspace = true } -voyager-sdk = { workspace = true } +alloy = { workspace = true, features = ["contract", "network", "providers", "rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "reqwest-rustls-tls", "provider-ws"] } +attested-light-client = { workspace = true } +clap = { workspace = true, features = ["default", "derive", "env", "error-context", "color"] } +cometbft-rpc = { workspace = true } +concurrent-keyring = { workspace = true } +cosmos-client = { workspace = true } +ed25519-dalek = { version = "2.2.0", features = ["pkcs8", "pem"] } +embed-commit = { workspace = true } +enumorph = { workspace = true } +ibc-solidity = { workspace = true, features = ["rpc"] } +ibc-union-spec = { workspace = true, features = ["serde", "ethabi"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +macros = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde-utils = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-sdk = { workspace = true } diff --git a/voyager/plugins/attestor/evm/src/call.rs b/voyager/plugins/attestor/evm/src/call.rs index 3c2705b947..de88975eda 100644 --- a/voyager/plugins/attestor/evm/src/call.rs +++ b/voyager/plugins/attestor/evm/src/call.rs @@ -3,7 +3,6 @@ use unionlabs::primitives::H256; #[model] pub enum ModuleCall { - VerifyState(VerifyState), SubmitAttestation(SubmitAttestation), } diff --git a/voyager/plugins/attestor/evm/src/main.rs b/voyager/plugins/attestor/evm/src/main.rs index 8b1789ff5c..6d98ac6be3 100644 --- a/voyager/plugins/attestor/evm/src/main.rs +++ b/voyager/plugins/attestor/evm/src/main.rs @@ -2,14 +2,26 @@ use std::{collections::VecDeque, ops::Deref, panic::AssertUnwindSafe, path::Path use alloy::{ network::AnyNetwork, + primitives::U256, providers::{DynProvider, Provider, ProviderBuilder}, }; +use attested_light_client::types::{Attestation, AttestationValue}; use clap::Subcommand; -use ibc_solidity::Ibc::{self, IbcInstance}; +use concurrent_keyring::{ConcurrentKeyring, KeyringConfig, KeyringEntry}; +use cosmos_client::{ + BroadcastTxCommitError, TxClient, TxError, + gas::{any, feemarket, fixed, osmosis_eip1559_feemarket}, + rpc::{Rpc, RpcT}, + wallet::{LocalSigner, WalletT}, +}; +use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut, pkcs8::DecodePrivateKey}; use ibc_union_spec::{ - IbcUnion, - event::FullEvent, - path::{ConnectionPath, StorePath}, + IbcUnion, Timestamp, + event::{ + ChannelOpenAck, ChannelOpenConfirm, ChannelOpenInit, ChannelOpenTry, ConnectionOpenAck, + ConnectionOpenConfirm, ConnectionOpenInit, ConnectionOpenTry, FullEvent, + }, + path::{BatchPacketsPath, BatchReceiptsPath, ChannelPath, ConnectionPath}, }; use jsonrpsee::{ Extensions, @@ -18,20 +30,22 @@ use jsonrpsee::{ }; use serde::{Deserialize, Serialize}; use serde_json::json; -use tracing::instrument; +use tracing::{error, info, info_span, instrument, warn}; use unionlabs::{ ErrorReporter, + cosmwasm::wasm::msg_execute_contract::MsgExecuteContract, + encoding::{Bincode, EncodeAs}, never::Never, - primitives::{H160, H256}, + primitives::{Bech32, H160, H256}, }; use voyager_sdk::{ anyhow::{self, bail}, - hook::{SubmitTxHook, simple_take_filter}, + hook::simple_take_filter, message::{PluginMessage, VoyagerMessage, data::Data}, plugin::Plugin, primitives::{ChainId, IbcSpec}, rpc::{FATAL_JSONRPC_ERROR_CODE, PluginServer, types::PluginInfo}, - vm::{Op, Visit, call, defer, now, pass::PassResult, seq}, + vm::{Op, call, noop, pass::PassResult}, }; use crate::call::{ModuleCall, SubmitAttestation}; @@ -63,7 +77,15 @@ pub struct ModuleInner { pub provider: DynProvider, - pub attestation_key: (), + pub attestation_key: SigningKey, + + pub keyring: ConcurrentKeyring, LocalSigner>, + + pub cosmos_client: cosmos_client::rpc::Rpc, + + pub gas_config: any::GasFiller, + + pub attestation_client_address: Bech32, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -76,10 +98,78 @@ pub struct Config { /// The address of the `IBCHandler` smart contract. pub ibc_handler_address: H160, - /// The RPC endpoint for the execution chain. - pub rpc_url: String, + /// The RPC endpoint for the EVM chain being attested to. + pub eth_rpc_url: String, + + /// The RPC endpoint for the cosmos chain to submit the attestations to. + pub cosmos_rpc_url: String, + + pub gas_config: GasFillerConfig, + pub attestation_client_address: Bech32, + + /// The path to the PKCS#8 encoded private key to sign the attestations with. pub attestation_key_path: PathBuf, + + pub keyring: KeyringConfig, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "config")] +pub enum GasFillerConfig { + // fixed gas filler is it's own config + Fixed(fixed::GasFiller), + Feemarket(FeemarketConfig), + OsmosisEip1559Feemarket(OsmosisEip1559FeemarketConfig), +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct FeemarketConfig { + pub max_gas: u64, + #[serde(with = "::serde_utils::string_opt")] + pub gas_multiplier: Option, + pub denom: Option, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct OsmosisEip1559FeemarketConfig { + pub max_gas: u64, + #[serde(with = "::serde_utils::string_opt")] + pub gas_multiplier: Option, + #[serde(with = "::serde_utils::string_opt")] + pub base_fee_multiplier: Option, + pub denom: Option, +} + +impl GasFillerConfig { + async fn into_gas_filler(self, rpc_url: String) -> anyhow::Result { + Ok(match self { + GasFillerConfig::Fixed(config) => any::GasFiller::Fixed(config), + GasFillerConfig::Feemarket(config) => any::GasFiller::Feemarket( + feemarket::GasFiller::new(feemarket::Config { + rpc_url, + max_gas: config.max_gas, + gas_multiplier: config.gas_multiplier, + denom: config.denom, + }) + .await?, + ), + GasFillerConfig::OsmosisEip1559Feemarket(config) => { + any::GasFiller::OsmosisEip1559Feemarket( + osmosis_eip1559_feemarket::GasFiller::new(osmosis_eip1559_feemarket::Config { + rpc_url, + max_gas: config.max_gas, + gas_multiplier: config.gas_multiplier, + base_fee_multiplier: config.base_fee_multiplier, + denom: config.denom, + }) + .await?, + ) + } + }) + } } #[derive(Subcommand)] @@ -93,10 +183,12 @@ impl Plugin for Module { type Cmd = Cmd; async fn new(config: Self::Config) -> anyhow::Result { + let rpc = Rpc::new(config.cosmos_rpc_url.clone()).await?; + let provider = DynProvider::new( ProviderBuilder::new() .network::() - .connect(&config.rpc_url) + .connect(&config.eth_rpc_url) .await?, ); @@ -111,11 +203,44 @@ impl Plugin for Module { ); } + let attestation_key = SigningKey::read_pkcs8_pem_file(config.attestation_key_path)?; + + let bech32_prefix = rpc + .client() + .grpc_abci_query::<_, protos::cosmos::auth::v1beta1::Bech32PrefixResponse>( + "/cosmos.auth.v1beta1.Query/Bech32Prefix", + &protos::cosmos::auth::v1beta1::Bech32PrefixRequest {}, + None, + false, + ) + .await? + .into_result()? + .unwrap() + .bech32_prefix; + Ok(Self(Arc::new(ModuleInner { chain_id, ibc_handler_address: config.ibc_handler_address, provider, - attestation_key: (), + attestation_key, + keyring: ConcurrentKeyring::new( + config.keyring.name, + config.keyring.keys.into_iter().map(|entry| { + let signer = + LocalSigner::new(entry.value().try_into().unwrap(), bech32_prefix.clone()); + + KeyringEntry { + address: signer.address(), + signer, + } + }), + ), + cosmos_client: rpc, + attestation_client_address: config.attestation_client_address, + gas_config: config + .gas_config + .into_gas_filler(config.cosmos_rpc_url) + .await?, }))) } @@ -140,9 +265,7 @@ end } } - async fn cmd(config: Self::Config, cmd: Self::Cmd) { - let plugin = Self::new(config).await.unwrap(); - + async fn cmd(_: Self::Config, cmd: Self::Cmd) { match cmd {} } } @@ -171,7 +294,7 @@ impl PluginServer for Module { ready: msgs .into_iter() .enumerate() - .map(|(idx, mut op)| { + .map(|(idx, op)| { let op = match op.into_data().unwrap() { Data::IbcEvent(chain_event) => call(PluginMessage::new( plugin_name(&self.chain_id), @@ -217,50 +340,57 @@ impl PluginServer for Module { } impl Module { - fn ibc_handler(&self) -> IbcInstance, AnyNetwork> { - Ibc::new::<_, AnyNetwork>(self.ibc_handler_address.get().into(), self.provider.clone()) - } - async fn submit_attestation( &self, event: FullEvent, tx_hash: H256, height: u64, - ) -> RpcResult<()> { - let (k, v) = match event { - FullEvent::CreateClient(event) => return Ok(()), - FullEvent::UpdateClient(event) => return Ok(()), - FullEvent::ConnectionOpenInit(event) => { - // ConnectionPath { - // connection_id: event.connection_id, - // } - // .key() - - let connection = self - .ibc_handler() - .connections(event.connection_id.raw()) - .call() - .await - .map_err(|e| ErrorObject::owned(-1))?; + ) -> RpcResult> { + let key = match &event { + FullEvent::ConnectionOpenInit(ConnectionOpenInit { connection_id, .. }) + | FullEvent::ConnectionOpenTry(ConnectionOpenTry { connection_id, .. }) + | FullEvent::ConnectionOpenAck(ConnectionOpenAck { connection_id, .. }) + | FullEvent::ConnectionOpenConfirm(ConnectionOpenConfirm { connection_id, .. }) => { + ConnectionPath { + connection_id: *connection_id, + } + .key() + } + FullEvent::ChannelOpenInit(ChannelOpenInit { channel_id, .. }) + | FullEvent::ChannelOpenTry(ChannelOpenTry { channel_id, .. }) + | FullEvent::ChannelOpenAck(ChannelOpenAck { channel_id, .. }) + | FullEvent::ChannelOpenConfirm(ChannelOpenConfirm { channel_id, .. }) => ChannelPath { + channel_id: *channel_id, + } + .key(), + FullEvent::PacketSend(event) => BatchPacketsPath::from_packets(&[event.packet()]).key(), + FullEvent::BatchSend(event) => BatchPacketsPath { + batch_hash: event.batch_hash, + } + .key(), + FullEvent::WriteAck(event) => BatchReceiptsPath::from_packets(&[event.packet()]).key(), + _ => { + info!(event = %event.name(), "nothing to attest for event"); + return Ok(noop()); } - FullEvent::ConnectionOpenTry(event) => todo!(), - FullEvent::ConnectionOpenAck(event) => todo!(), - FullEvent::ConnectionOpenConfirm(event) => todo!(), - FullEvent::ChannelOpenInit(event) => todo!(), - FullEvent::ChannelOpenTry(event) => todo!(), - FullEvent::ChannelOpenAck(event) => todo!(), - FullEvent::ChannelOpenConfirm(event) => todo!(), - FullEvent::ChannelCloseInit(event) => todo!(), - FullEvent::ChannelCloseConfirm(event) => todo!(), - FullEvent::PacketSend(event) => todo!(), - FullEvent::BatchSend(event) => todo!(), - FullEvent::PacketRecv(event) => todo!(), - FullEvent::IntentPacketRecv(event) => todo!(), - FullEvent::WriteAck(event) => todo!(), - FullEvent::PacketAck(event) => todo!(), - FullEvent::PacketTimeout(event) => todo!(), }; + let value = self + .provider + .get_storage_at( + self.ibc_handler_address.get().into(), + U256::from_be_bytes(*key.get()), + ) + .block_id(height.into()) + .await + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching storage"), + None::<()>, + ) + })?; + let tx = self .provider .get_transaction_by_hash(tx_hash.get().into()) @@ -287,6 +417,94 @@ impl Module { )); } - Ok(()) + let timestamp = self + .provider + .get_block_by_number(height.into()) + .await + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching block"), + Some(json!({ "height": height })), + ) + })? + .ok_or_else(|| ErrorObject::owned(-1, format!("block {height} not found"), None::<()>))? + .header + .timestamp; + + info_span!("attesting to state", %key, %value, %height, %timestamp) + .in_scope(|| { + self.keyring.with(|signer| { + let tx_client = TxClient::new(signer, &self.cosmos_client, &self.gas_config); + + let attestation = Attestation { + height, + timestamp: Timestamp::from_secs(timestamp), + key: key.into(), + value: AttestationValue::Existence(value.to_be_bytes::<32>().into()), + }; + + let signature = self + .attestation_key.clone() + .sign(&(&attestation).encode_as::()); + + AssertUnwindSafe(async move { + let res = tx_client + .tx( + MsgExecuteContract { + sender: tx_client.wallet().address().map_data(Into::into), + contract: self.attestation_client_address.clone(), + msg: serde_json::to_vec( + &attested_light_client::msg::ExecuteMsg::Attest { + attestation: attestation.clone(), + attestor: self + .attestation_key + .verifying_key() + .to_bytes() + .into(), + signature: signature.to_bytes().into(), + }, + ) + .unwrap() + .into(), + funds: vec![], + }, + "", + true, + ) + .await; + + match res { + Ok((tx_hash, _)) => { + info!(%tx_hash, "submitted attestation"); + Ok(noop()) + } + Err(TxError::BroadcastTxCommit(BroadcastTxCommitError::TxFailed { + codespace, + error_code, + log, + })) => { + error!(%codespace, %error_code, %log, "tx failed"); + Ok(noop()) + } + Err(err) => { + warn!(err = %ErrorReporter(&err), "error when submitting tx, will be retried"); + Err(ErrorObject::owned( + -1, + ErrorReporter(err).with_message("error when submitting attestation"), + Some(json!(attestation)), + )) + } + } + }) + }) + }) + .await + .unwrap_or_else(|| { + Ok(call(PluginMessage::new( + self.plugin_name(), + ModuleCall::SubmitAttestation(SubmitAttestation { event, tx_hash, height }), + ))) + }) } } From a21f80ebc321ce8c57daa606270dc45406b59d10 Mon Sep 17 00:00:00 2001 From: benluelo Date: Sun, 9 Nov 2025 21:15:57 +0000 Subject: [PATCH 04/12] wip --- cosmwasm/cosmwasm.nix | 8 +- .../ibc-union/lightclient/attested/Cargo.toml | 3 +- .../lightclient/attested/src/contract.rs | 166 ++++------------ .../attested/src/contract/execute.rs | 187 ++++++++++++++++++ .../attested/src/contract/query.rs | 42 ++++ .../lightclient/attested/src/errors.rs | 15 +- .../ibc-union/lightclient/attested/src/msg.rs | 57 ++++-- .../lightclient/attested/src/tests.rs | 47 ++++- .../lightclient/attested/src/types.rs | 35 ++++ voyager/config.jsonc | 6 +- voyager/plugins/attestor/evm/src/main.rs | 2 - 11 files changed, 404 insertions(+), 164 deletions(-) create mode 100644 cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs create mode 100644 cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs diff --git a/cosmwasm/cosmwasm.nix b/cosmwasm/cosmwasm.nix index d77b5aea76..b83fcf2690 100644 --- a/cosmwasm/cosmwasm.nix +++ b/cosmwasm/cosmwasm.nix @@ -108,8 +108,9 @@ _: { }; # lightclients = pkgs.lib.lists.remove "cometbls" (builtins.attrNames all-lightclients); lightclients = [ - "trusted-mpt" + # "trusted-mpt" # "sui" + "attested" ]; } { @@ -491,6 +492,11 @@ _: { dir = "parlia"; client-type = "parlia"; } + { + name = "attested"; + dir = "attested"; + client-type = "attested"; + } ]; # client type => package name diff --git a/cosmwasm/ibc-union/lightclient/attested/Cargo.toml b/cosmwasm/ibc-union/lightclient/attested/Cargo.toml index 9abd95e279..7f26da2bbb 100644 --- a/cosmwasm/ibc-union/lightclient/attested/Cargo.toml +++ b/cosmwasm/ibc-union/lightclient/attested/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["cdylib", "rlib"] attested-light-client-types = { workspace = true, features = ["serde", "ethabi", "bincode"] } bincode = { workspace = true, features = ["derive"] } cosmwasm-std = { workspace = true, features = ["abort"] } -depolama = { workspace = true } +depolama = { workspace = true, features = ["iterator"] } embed-commit = { workspace = true } frissitheto = { workspace = true } ibc-union-light-client = { workspace = true } @@ -33,6 +33,7 @@ ed25519-dalek = { workspace = true, features = ["default"] } hex = { workspace = true } hex-literal = { workspace = true } serde_json = { workspace = true } +unionlabs = { workspace = true, features = ["test-utils"] } [features] library = [] diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs index 78c1d78323..34bb11ec17 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs @@ -1,25 +1,25 @@ use cosmwasm_std::{ - Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdError, StdResult, ensure, - entry_point, + Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, entry_point, to_json_binary, }; -use depolama::StorageExt; use frissitheto::UpgradeMsg; -use ibc_union_light_client::{IbcClientError, default_reply, msg::QueryMsg}; +use ibc_union_light_client::{IbcClientError, default_migrate, default_reply, msg::QueryMsg}; use serde::{Deserialize, Serialize}; -use unionlabs::encoding::{Bincode, EncodeAs}; use crate::{ client::AttestedLightClient, - errors::Error, - msg::{ExecuteMsg, InitMsg}, - state::{ - AttestationAttestors, Attestations, Attestors, HeightTimestamps, PendingAttestations, - Quorum, + contract::{ + execute::{add_attestor, attest, confirm_attestation, remove_attestor, set_quorum}, + query::{attested_value, attestors, quorum, timestamp_at_height}, }, - types::AttestationKey, + errors::Error, + msg::{ExecuteMsg, QueryMsg, RestrictedExecuteMsg}, }; default_reply!(); +defualt_migrate!(); + +pub mod execute; +pub mod query; #[entry_point] pub fn execute( @@ -33,129 +33,33 @@ pub fn execute( attestation, attestor, signature, - } => { - deps.storage - .maybe_read::(&attestor)? - .ok_or(Error::InvalidAttestor { attestor })?; - - if let Some(previously_attested_timestamp) = deps - .storage - .maybe_read::(&attestation.height)? - && previously_attested_timestamp != attestation.timestamp - { - return Err(Error::InconsistentTimestamp { - height: attestation.height, - timestamp: attestation.timestamp, - previously_attested_timestamp, - }); - } - - let attestation_key = AttestationKey { - height: attestation.height, - key: attestation.key.clone(), - }; - - if let Some(value) = deps.storage.maybe_read::(&attestation_key)? { - return Err(Error::AlreadyAttested { - height: attestation.height, - timestamp: attestation.timestamp, - key: attestation.key, - value, - }); + } => attest(deps, attestation, attestor, signature), + ExecuteMsg::ConfirmAttestation { attestation } => confirm_attestation(deps, attestation), + ExecuteMsg::Restricted(msg) => match msg { + RestrictedExecuteMsg::SetQuorum { new_quorum } => set_quorum(deps, new_quorum), + RestrictedExecuteMsg::AddAttestor { new_attestor } => add_attestor(deps, new_attestor), + RestrictedExecuteMsg::RemoveAttestor { old_attestor } => { + remove_attestor(deps, old_attestor) } - - ensure!( - deps.api - .ed25519_verify( - &(&attestation).encode_as::(), - signature.as_ref(), - attestor.as_ref() - ) - .map_err(StdError::from)?, - Error::InvalidSignature - ); - - let mut signatures = deps - .storage - .maybe_read::(&attestation)? - .unwrap_or_default(); - - if signatures.insert(attestor, signature).is_some() { - return Err(Error::AttestationAlreadyReceived); - } - - let quorum = deps.storage.read_item::()?; - - let mut res = Response::new().add_event( - Event::new("attestation_submitted") - .add_attribute("height", attestation.height.to_string()) - .add_attribute("timestamp", attestation.timestamp.to_string()) - .add_attribute("key", attestation.key.to_string()) - .add_attribute("value", attestation.value.to_string()) - .add_attribute("attestor", attestor.to_string()) - .add_attribute("signature", signature.to_string()), - ); - - if signatures.len() >= quorum.get().into() { - deps.storage.delete::(&attestation); - - deps.storage - .write::(&attestation_key, &attestation.value); - deps.storage - .write::(&attestation, &signatures); - - deps.storage - .upsert::(&attestation.height, |maybe_timestamp| { - Ok(maybe_timestamp.unwrap_or(attestation.timestamp)) - })?; - - res = res.add_event( - Event::new("quorum_reached") - .add_attribute("height", attestation.height.to_string()) - .add_attribute("timestamp", attestation.timestamp.to_string()) - .add_attribute("key", attestation.key.to_string()) - .add_attribute("value", attestation.value.to_string()) - .add_attribute("quorum", quorum.to_string()), - ); - } else { - deps.storage - .write::(&attestation, &signatures); - } - - Ok(res) - } + }, } } #[entry_point] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - // TODO: Add more queries for the attested light client - ibc_union_light_client::query::(deps, env, msg).map_err(Into::into) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MigrateMsg {} - -#[entry_point] -pub fn migrate( - deps: DepsMut, - _env: Env, - msg: UpgradeMsg, -) -> Result> { - msg.run( - deps, - |deps, init_msg| { - // TODO: Pull this out into an `init` function - for key in init_msg.attestors { - deps.storage.write::(&key, &()); - } - - deps.storage.write_item::(&init_msg.quorum); - - let res = ibc_union_light_client::init(deps, init_msg.ibc_union_light_client_init_msg)?; - - Ok((res, None)) - }, - |_deps, _migrate_msg, _current_version| Ok((Response::default(), None)), - ) +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Quorum {} => Ok(to_json_binary(&quorum(deps)?)?), + QueryMsg::Attestors {} => Ok(to_json_binary(&attestors(deps)?)?), + QueryMsg::AttestedValue { height, key } => { + Ok(to_json_binary(&attested_value(deps, height, key)?)?) + } + QueryMsg::TimestampAtHeight { height } => { + Ok(to_json_binary(×tamp_at_height(deps, height)?)?) + } + QueryMsg::LightClient(msg) => { + ibc_union_light_client::query::(deps, env, msg) + .map_err(StdError::from) + .map_err(Into::into) + } + } } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs new file mode 100644 index 0000000000..9980b0acc7 --- /dev/null +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs @@ -0,0 +1,187 @@ +use std::{collections::BTreeMap, num::NonZero}; + +use cosmwasm_std::{DepsMut, Event, Response, StdError, ensure}; +use depolama::StorageExt; +use unionlabs::{ + encoding::{Bincode, EncodeAs}, + primitives::{H256, H512}, +}; + +use crate::{ + errors::Error, + state::{ + AttestationAttestors, Attestations, Attestors, HeightTimestamps, PendingAttestations, + Quorum, + }, + types::{Attestation, AttestationKey}, +}; + +pub fn attest( + mut deps: DepsMut, + attestation: Attestation, + attestor: H256, + signature: H512, +) -> Result { + { + deps.storage + .maybe_read::(&attestor)? + .ok_or(Error::InvalidAttestor { attestor })?; + + if let Some(previously_attested_timestamp) = deps + .storage + .maybe_read::(&attestation.height)? + && previously_attested_timestamp != attestation.timestamp + { + return Err(Error::InconsistentTimestamp { + height: attestation.height, + timestamp: attestation.timestamp, + previously_attested_timestamp, + }); + } + + let attestation_key = AttestationKey { + height: attestation.height, + key: attestation.key.clone(), + }; + + if let Some(value) = deps.storage.maybe_read::(&attestation_key)? { + return Err(Error::AlreadyAttested { + height: attestation.height, + timestamp: attestation.timestamp, + key: attestation.key, + value, + }); + } + + ensure!( + deps.api + .ed25519_verify( + &(&attestation).encode_as::(), + signature.as_ref(), + attestor.as_ref() + ) + .map_err(StdError::from)?, + Error::InvalidSignature + ); + + let mut signatures = deps + .storage + .maybe_read::(&attestation)? + .unwrap_or_default(); + + if signatures.insert(attestor, signature).is_some() { + return Err(Error::AttestationAlreadyReceived); + } + + let mut res = Response::new().add_event( + Event::new("attestation_submitted") + .add_attribute("height", attestation.height.to_string()) + .add_attribute("timestamp", attestation.timestamp.to_string()) + .add_attribute("key", attestation.key.to_string()) + .add_attribute("value", attestation.value.to_string()) + .add_attribute("attestor", attestor.to_string()) + .add_attribute("signature", signature.to_string()), + ); + + if let Some(event) = check_quorum(deps.branch(), &signatures, &attestation)? { + res = res.add_event(event); + } else { + deps.storage + .write::(&attestation, &signatures); + } + + Ok(res) + } +} + +pub fn confirm_attestation(deps: DepsMut, attestation: Attestation) -> Result { + let signatures = deps + .storage + .maybe_read::(&attestation)? + .unwrap_or_default(); + + let event = check_quorum(deps, &signatures, &attestation)?.ok_or(Error::QuorumNotReached)?; + + Ok(Response::new().add_event(event)) +} + +fn check_quorum( + deps: DepsMut, + signatures: &BTreeMap, + attestation: &Attestation, +) -> Result, Error> { + let quorum = deps.storage.read_item::()?; + + let total_valid_signatures = signatures.iter().try_fold(0, |total, (attestor, _)| { + deps.storage + .maybe_read::(attestor) + .map(|exists| total + (exists.is_some() as usize)) + })?; + + if total_valid_signatures >= quorum.get().into() { + deps.storage.delete::(attestation); + + deps.storage.write::( + &AttestationKey { + height: attestation.height, + key: attestation.key.clone(), + }, + &attestation.value, + ); + deps.storage + .write::(attestation, signatures); + + deps.storage + .upsert::(&attestation.height, |maybe_timestamp| { + Ok(maybe_timestamp.unwrap_or(attestation.timestamp)) + })?; + + Ok(Some( + Event::new("quorum_reached") + .add_attribute("height", attestation.height.to_string()) + .add_attribute("timestamp", attestation.timestamp.to_string()) + .add_attribute("key", attestation.key.to_string()) + .add_attribute("value", attestation.value.to_string()) + .add_attribute("quorum", quorum.to_string()), + )) + } else { + Ok(None) + } +} + +pub fn set_quorum(deps: DepsMut, new_quorum: NonZero) -> Result { + deps.storage.write_item::(&new_quorum); + + Ok(Response::new() + .add_event(Event::new("quorum_updated").add_attribute("quorum", new_quorum.to_string()))) +} + +pub fn add_attestor(deps: DepsMut, new_attestor: H256) -> Result { + if deps + .storage + .maybe_read::(&new_attestor)? + .is_some() + { + Err(Error::AttestorAlreadyExists { + attestor: new_attestor, + }) + } else { + deps.storage.write::(&new_attestor, &()); + + Ok(Response::new().add_event( + Event::new("attestor_added").add_attribute("attestor", new_attestor.to_string()), + )) + } +} + +pub fn remove_attestor(deps: DepsMut, old_attestor: H256) -> Result { + if deps.storage.take::(&old_attestor)?.is_some() { + Ok(Response::new().add_event( + Event::new("attestor_removed").add_attribute("attestor", old_attestor.to_string()), + )) + } else { + Err(Error::InvalidAttestor { + attestor: old_attestor, + }) + } +} diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs new file mode 100644 index 0000000000..4ffceb7973 --- /dev/null +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs @@ -0,0 +1,42 @@ +use std::{collections::BTreeSet, num::NonZero}; + +use cosmwasm_std::{Deps, Order}; +use depolama::{Bytes, StorageExt}; +use ibc_union_light_client::spec::Timestamp; +use unionlabs::primitives::H256; + +use crate::{ + errors::Error, + state::{Attestations, Attestors, HeightTimestamps, Quorum}, + types::{AttestationKey, AttestationValue}, +}; + +pub fn quorum(deps: Deps) -> Result, Error> { + deps.storage + .maybe_read_item::()? + .ok_or(Error::QuorumNotSet) +} + +pub fn attestors(deps: Deps) -> Result, Error> { + deps.storage + .iter::(Order::Ascending) + .map(|r| r.map(|(attestor, ())| attestor)) + .collect::>() + .map_err(Into::into) +} + +pub fn attested_value( + deps: Deps, + height: u64, + key: Bytes, +) -> Result, Error> { + deps.storage + .maybe_read::(&AttestationKey { height, key }) + .map_err(Into::into) +} + +pub fn timestamp_at_height(deps: Deps, height: u64) -> Result, Error> { + deps.storage + .maybe_read::(&height) + .map_err(Into::into) +} diff --git a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs index 04dd0c0edb..61fec88828 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs @@ -12,12 +12,6 @@ pub enum Error { #[error("no misbehaviour in an attested client")] NoMisbehaviourInAttestedClient, - #[error("unauthorized call")] - Unauthorized, - - #[error("key {key} has not been attested to at height {height}")] - KeyNotAttested { height: u64, key: Bytes }, - #[error( "key {key} was attested to at height {height} with value {value} but \ attempted to verify against value {value}" @@ -71,6 +65,15 @@ pub enum Error { #[error("no attestation found for height {height}, key {key}")] AttestationNotFound { height: u64, key: Bytes }, + + #[error("attestor {attestor} is already in the attestation set")] + AttestorAlreadyExists { attestor: H256 }, + + #[error("the quorum has not yet been set")] + QuorumNotSet, + + #[error("the quorum has not been reached for this attestation")] + QuorumNotReached, } impl From for IbcClientError { diff --git a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs index fb850524c0..8969ccc80b 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs @@ -1,24 +1,59 @@ -use std::{collections::BTreeSet, num::NonZero}; +use std::num::NonZero; +use ibc_union_light_client::access_managed::Restricted; use serde::{Deserialize, Serialize}; -use unionlabs::primitives::{H256, H512}; +use unionlabs::primitives::{Bytes, H256, H512}; use crate::types::Attestation; +pub enum ExecuteMsg { + /// Attest to a key/value state. + /// + /// `attestor` must be a valid attestor and must have signed the `attestation` payload. + Attest { + attestation: Attestation, + attestor: H256, + signature: H512, + }, + /// Confirm an attestation. + /// + /// Attestations are typically confirmed in [`ExecuteMsg::Attest`] upon receiving the attestation that pushes the total attestations over the quorum. However, if the quorum is decreased while there are still pending attestations, then this can called to confirm those attestations if they have already hit the new quorum without requiring another attestation to be submitted (which may not be possible if there are not enough attestors in the set). + ConfirmAttestation { attestation: Attestation }, + #[serde(untagged)] + Restricted(Restricted), +} + #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "snake_case")] -pub struct InitMsg { - pub attestors: BTreeSet, - pub quorum: NonZero, - pub ibc_union_light_client_init_msg: ibc_union_light_client::msg::InitMsg, +pub enum RestrictedExecuteMsg { + /// Set a new quorum for the attestations to be considered valid. + /// + /// If the new quorum is larger than the currently configuured quorum, any existing attestations that have already hit the quorum will still be considered valid, but any current pending attestations will need to reach the new quorum in order to be confirmed. + SetQuorum { new_quorum: NonZero }, + /// Add a new attestor to the attestation set. + AddAttestor { new_attestor: H256 }, + /// Add an existing attestor from the attestation set. + RemoveAttestor { old_attestor: H256 }, } #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "snake_case")] -pub enum ExecuteMsg { - Attest { - attestation: Attestation, - attestor: H256, - signature: H512, +pub enum QueryMsg { + /// Returns the currently configured quorum. + Quorum {}, + /// Returns the current attestation set. + Attestors {}, + /// Returns the value attested to under `key` at `height`. + AttestedValue { + // #[serde(with = "serde_utils::string")] + height: u64, + key: Bytes, + }, + /// Returns the timestamp attested to at `height`. + TimestampAtHeight { + // #[serde(with = "serde_utils::string")] + height: u64, }, + #[serde(untagged)] + LightClient(ibc_union_light_client::msg::QueryMsg), } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs index 548abee851..6fc2cc0135 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs @@ -2,7 +2,7 @@ use std::{num::NonZero, sync::LazyLock}; use attested_light_client_types::{ClientState, ClientStateV1, ConsensusState, Header}; use cosmwasm_std::{ - Addr, Api, OwnedDeps, + Addr, Api, Event, OwnedDeps, Response, testing::{MockApi, MockQuerier, MockStorage, message_info, mock_dependencies, mock_env}, }; use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut}; @@ -10,6 +10,7 @@ use frissitheto::UpgradeMsg; use hex_literal::hex; use ibc_union_light_client::{ access_managed, + msg::InitMsg, spec::{Duration, Timestamp}, }; use unionlabs::{ @@ -21,7 +22,7 @@ use crate::{ client::{verify_attestation, verify_header}, contract::{execute, migrate}, errors::Error, - msg::{ExecuteMsg, InitMsg}, + msg::{ExecuteMsg, RestrictedExecuteMsg}, types::{Attestation, AttestationValue}, }; @@ -72,20 +73,46 @@ fn setup() -> OwnedDeps { migrate( deps.as_mut(), - env, + env.clone(), UpgradeMsg::Init(InitMsg { - ibc_union_light_client_init_msg: ibc_union_light_client::msg::InitMsg { - ibc_host: ibc_host.into_string(), - access_managed_init_msg: access_managed::InitMsg { - initial_authority: Addr::unchecked("manager"), - }, + ibc_host: ibc_host.into_string(), + access_managed_init_msg: access_managed::InitMsg { + initial_authority: Addr::unchecked("manager"), }, - attestors: attestors().map(vk).collect(), - quorum: const { >::new(2).unwrap() }, }), ) .unwrap(); + assert_eq!( + execute( + deps.as_mut(), + env.clone(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + new_quorum: const { NonZero::new(2).unwrap() }, + }), + ) + .unwrap(), + Response::new().add_event(Event::new("quorum_updated").add_attribute("quorum", "2")) + ); + + for attestor in attestors() { + assert_eq!( + execute( + deps.as_mut(), + env.clone(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + new_attestor: vk(attestor) + }), + ) + .unwrap(), + Response::new().add_event( + Event::new("attestor_added").add_attribute("attestor", vk(attestor).to_string()) + ) + ); + } + deps } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/types.rs b/cosmwasm/ibc-union/lightclient/attested/src/types.rs index 7c47d7469b..94112659f3 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/types.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/types.rs @@ -36,3 +36,38 @@ impl fmt::Display for AttestationValue { } } } + +#[cfg(test)] +mod tests { + use hex_literal::hex; + use unionlabs::{ + encoding::{Bincode, EncodeAs, Json}, + test_utils::assert_codec_iso_bytes, + }; + + use super::*; + + #[test] + fn attestation_value_json() { + assert_codec_iso_bytes::<_, Json>(&AttestationValue::NonExistence, br#""non_existence""#); + + assert_codec_iso_bytes::<_, Json>( + &AttestationValue::Existence([0x00].into()), + br#"{"existence":"0x00"}"#, + ); + } + + #[test] + fn attestation_value_bincode() { + assert_codec_iso_bytes::<_, Bincode>(&AttestationValue::NonExistence, &hex!("00000000")); + + assert_codec_iso_bytes::<_, Bincode>( + &AttestationValue::Existence([0x00].into()), + &hex!( + "01000000" // variant + "0100000000000000" // byte length + "00" // bytes + ), + ); + } +} diff --git a/voyager/config.jsonc b/voyager/config.jsonc index 8e2c591bab..352ff6d4f5 100644 --- a/voyager/config.jsonc +++ b/voyager/config.jsonc @@ -305,7 +305,9 @@ "config": { "chain_id": "union-devnet-1", "rpc_url": "http://localhost:26657", - "prover_endpoints": ["https://galois.testnet-9.union.build:443"] + "prover_endpoints": [ + "https://galois.testnet-9.union.build:443" + ] } }, { @@ -354,4 +356,4 @@ }, "optimizer_delay_milliseconds": 100 } -} +} \ No newline at end of file diff --git a/voyager/plugins/attestor/evm/src/main.rs b/voyager/plugins/attestor/evm/src/main.rs index 6d98ac6be3..78cbd0062d 100644 --- a/voyager/plugins/attestor/evm/src/main.rs +++ b/voyager/plugins/attestor/evm/src/main.rs @@ -92,8 +92,6 @@ pub struct ModuleInner { #[serde(deny_unknown_fields)] pub struct Config { pub chain_id: ChainId, - #[serde(default)] - pub additional_chain_ids: Vec, /// The address of the `IBCHandler` smart contract. pub ibc_handler_address: H160, From 21808326913117f73061c45785847c2e77f43e5a Mon Sep 17 00:00:00 2001 From: benluelo Date: Mon, 10 Nov 2025 08:35:44 +0000 Subject: [PATCH 05/12] wip --- .../attested/src/contract/execute.rs | 23 +- .../lightclient/attested/src/errors.rs | 6 +- .../lightclient/attested/src/tests.rs | 421 +++++++++++++++--- .../lightclient/attested/src/types.rs | 82 +++- voyager/config.jsonc | 6 +- 5 files changed, 452 insertions(+), 86 deletions(-) diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs index 9980b0acc7..81ccb4eed2 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs @@ -83,7 +83,7 @@ pub fn attest( .add_attribute("signature", signature.to_string()), ); - if let Some(event) = check_quorum(deps.branch(), &signatures, &attestation)? { + if let Ok(event) = check_quorum(deps.branch(), &signatures, &attestation)? { res = res.add_event(event); } else { deps.storage @@ -100,7 +100,8 @@ pub fn confirm_attestation(deps: DepsMut, attestation: Attestation) -> Result(&attestation)? .unwrap_or_default(); - let event = check_quorum(deps, &signatures, &attestation)?.ok_or(Error::QuorumNotReached)?; + let event = check_quorum(deps, &signatures, &attestation)? + .map_err(|(quorum, current)| Error::QuorumNotReached { quorum, current })?; Ok(Response::new().add_event(event)) } @@ -109,7 +110,7 @@ fn check_quorum( deps: DepsMut, signatures: &BTreeMap, attestation: &Attestation, -) -> Result, Error> { +) -> Result, u8)>, Error> { let quorum = deps.storage.read_item::()?; let total_valid_signatures = signatures.iter().try_fold(0, |total, (attestor, _)| { @@ -136,16 +137,14 @@ fn check_quorum( Ok(maybe_timestamp.unwrap_or(attestation.timestamp)) })?; - Ok(Some( - Event::new("quorum_reached") - .add_attribute("height", attestation.height.to_string()) - .add_attribute("timestamp", attestation.timestamp.to_string()) - .add_attribute("key", attestation.key.to_string()) - .add_attribute("value", attestation.value.to_string()) - .add_attribute("quorum", quorum.to_string()), - )) + Ok(Ok(Event::new("quorum_reached") + .add_attribute("height", attestation.height.to_string()) + .add_attribute("timestamp", attestation.timestamp.to_string()) + .add_attribute("key", attestation.key.to_string()) + .add_attribute("value", attestation.value.to_string()) + .add_attribute("quorum", quorum.to_string()))) } else { - Ok(None) + Ok(Err((quorum, total_valid_signatures as u8))) } } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs index 61fec88828..f2eb981f2a 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs @@ -1,3 +1,5 @@ +use std::num::NonZero; + use cosmwasm_std::StdError; use ibc_union_light_client::{IbcClientError, spec::Timestamp}; use unionlabs::primitives::{Bytes, H256}; @@ -72,8 +74,8 @@ pub enum Error { #[error("the quorum has not yet been set")] QuorumNotSet, - #[error("the quorum has not been reached for this attestation")] - QuorumNotReached, + #[error("the quorum has not been reached for this attestation ({current}/{quorum})")] + QuorumNotReached { quorum: NonZero, current: u8 }, } impl From for IbcClientError { diff --git a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs index 6fc2cc0135..8e984d14b4 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs @@ -1,18 +1,19 @@ -use std::{num::NonZero, sync::LazyLock}; +use std::{collections::BTreeSet, fmt::Debug, num::NonZero, sync::LazyLock}; use attested_light_client_types::{ClientState, ClientStateV1, ConsensusState, Header}; use cosmwasm_std::{ - Addr, Api, Event, OwnedDeps, Response, + Addr, Api, Deps, Env, Event, OwnedDeps, Response, from_json, testing::{MockApi, MockQuerier, MockStorage, message_info, mock_dependencies, mock_env}, }; use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut}; use frissitheto::UpgradeMsg; use hex_literal::hex; use ibc_union_light_client::{ - access_managed, + StateUpdate, access_managed, msg::InitMsg, spec::{Duration, Timestamp}, }; +use serde::de::DeserializeOwned; use unionlabs::{ encoding::{Bincode, EncodeAs}, primitives::{H256, H512}, @@ -20,30 +21,36 @@ use unionlabs::{ use crate::{ client::{verify_attestation, verify_header}, - contract::{execute, migrate}, + contract::{execute, migrate, query}, errors::Error, - msg::{ExecuteMsg, RestrictedExecuteMsg}, + msg::{ExecuteMsg, QueryMsg, RestrictedExecuteMsg}, types::{Attestation, AttestationValue}, }; -// sha256(1) +// sha256(0x01) static ATTESTOR_1: LazyLock = LazyLock::new(|| { SigningKey::from_bytes(&hex!( "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a" )) }); -// sha256(2) +// sha256(0x02) static ATTESTOR_2: LazyLock = LazyLock::new(|| { SigningKey::from_bytes(&hex!( "dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986" )) }); -// sha256(3) +// sha256(0x03) static ATTESTOR_3: LazyLock = LazyLock::new(|| { SigningKey::from_bytes(&hex!( "084fed08b978af4d7d196a7446a86b58009e636b611db16211b65a9aadff29c5" )) }); +// sha256(0x04) +static ATTESTOR_4: LazyLock = LazyLock::new(|| { + SigningKey::from_bytes(&hex!( + "e52d9c508c502347344d8c07ad91cbd6068afc75ff6292f062a09ca381c89e71" + )) +}); fn attestors() -> impl Iterator { [&ATTESTOR_1, &ATTESTOR_2, &ATTESTOR_3] @@ -62,7 +69,18 @@ fn vk(sk: &SigningKey) -> H256 { sk.verifying_key().to_bytes().into() } -fn setup() -> OwnedDeps { +#[track_caller] +pub(crate) fn assert_query_result( + deps: Deps, + env: &Env, + msg: QueryMsg, + expected: &T, +) { + let res = query(deps, env.clone(), msg).unwrap(); + assert_eq!(&from_json::(res).unwrap(), expected); +} + +fn setup() -> (OwnedDeps, Env) { let mut deps = mock_dependencies(); let env = mock_env(); @@ -96,6 +114,8 @@ fn setup() -> OwnedDeps { Response::new().add_event(Event::new("quorum_updated").add_attribute("quorum", "2")) ); + assert_query_result(deps.as_ref(), &env, QueryMsg::Quorum {}, &2); + for attestor in attestors() { assert_eq!( execute( @@ -113,10 +133,22 @@ fn setup() -> OwnedDeps { ); } - deps + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::Attestors {}, + &attestors().map(vk).collect::>(), + ); + + (deps, env) } -fn reach_quorum(deps: &mut OwnedDeps, attestation: Attestation) { +fn reach_quorum<'a>( + deps: &mut OwnedDeps, + env: &Env, + attestation: Attestation, + attestors: impl IntoIterator, +) { assert_eq!( verify_attestation( deps.as_ref(), @@ -131,34 +163,44 @@ fn reach_quorum(deps: &mut OwnedDeps, attesta }, ); - execute( - deps.as_mut(), - mock_env(), - message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Attest { - attestation: attestation.clone(), - attestor: vk(&ATTESTOR_1), - signature: sign(&ATTESTOR_1, &attestation), - }, - ) - .unwrap(); + let mut res = Response::new(); + for attestor in attestors { + res = execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Attest { + attestation: attestation.clone(), + attestor: vk(attestor), + signature: sign(attestor, &attestation), + }, + ) + .unwrap(); - let res = execute( - deps.as_mut(), - mock_env(), - message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Attest { - attestation: attestation.clone(), - attestor: vk(&ATTESTOR_2), - signature: sign(&ATTESTOR_2, &attestation), - }, - ) - .unwrap(); + assert_eq!(res.events[0].ty, "attestation_submitted"); + } - assert_eq!(res.events.len(), 2); - assert_eq!(res.events[0].ty, "attestation_submitted"); assert_eq!(res.events[1].ty, "quorum_reached"); + assert_query_result( + deps.as_ref(), + env, + QueryMsg::AttestedValue { + height: attestation.height, + key: attestation.key.clone(), + }, + &attestation.value, + ); + + assert_query_result( + deps.as_ref(), + env, + QueryMsg::TimestampAtHeight { + height: attestation.height, + }, + &attestation.timestamp, + ); + // quorum reached, attestation should verify verify_attestation( deps.as_ref(), @@ -192,7 +234,7 @@ fn reach_quorum(deps: &mut OwnedDeps, attesta #[test] fn attest() { - let mut deps = setup(); + let (mut deps, _) = setup(); let attestation = Attestation { height: 1, @@ -232,7 +274,7 @@ fn attest() { #[test] fn verify_header_works() { - let mut deps = setup(); + let (mut deps, env) = setup(); let attestation = Attestation { height: 2, @@ -257,7 +299,12 @@ fn verify_header_works() { .is_err() ); - reach_quorum(&mut deps, attestation.clone()); + reach_quorum( + &mut deps, + &env, + attestation.clone(), + [&*ATTESTOR_1, &*ATTESTOR_2], + ); // timestamp is checked assert_eq!( @@ -281,7 +328,12 @@ fn verify_header_works() { } ); - let res = verify_header( + let StateUpdate { + height, + client_state, + consensus_state, + storage_writes, + } = verify_header( deps.as_ref(), ClientState::V1(ClientStateV1 { chain_id: "999".to_owned(), @@ -294,26 +346,26 @@ fn verify_header_works() { ) .unwrap(); - assert_eq!(res.height, 2); + assert_eq!(height, 2); assert_eq!( - res.client_state, + client_state, Some(ClientState::V1(ClientStateV1 { chain_id: "999".to_owned(), latest_height: 2, })), ); assert_eq!( - res.consensus_state, + consensus_state, ConsensusState { timestamp: attestation.timestamp } ); - assert!(res.storage_writes.is_empty()); + assert!(storage_writes.is_empty()); } #[test] fn quorum() { - let mut deps = setup(); + let (mut deps, env) = setup(); let attestation = Attestation { height: 1, @@ -322,7 +374,12 @@ fn quorum() { value: AttestationValue::Existence(b"value-1".into()), }; - reach_quorum(&mut deps, attestation.clone()); + reach_quorum( + &mut deps, + &env, + attestation.clone(), + [&*ATTESTOR_1, &*ATTESTOR_2], + ); // membership, proof value is non-existence assert_eq!( @@ -358,6 +415,14 @@ fn quorum() { }, ); + verify_attestation( + deps.as_ref(), + attestation.height, + attestation.key.clone(), + AttestationValue::Existence(b"value-1".into()), + ) + .unwrap(); + let attestation = Attestation { height: 2, timestamp: Timestamp::from_nanos(100), @@ -365,7 +430,12 @@ fn quorum() { value: AttestationValue::NonExistence, }; - reach_quorum(&mut deps, attestation.clone()); + reach_quorum( + &mut deps, + &env, + attestation.clone(), + [&*ATTESTOR_1, &*ATTESTOR_2], + ); // non-membership, proof value is existence assert_eq!( @@ -383,11 +453,19 @@ fn quorum() { attested: attestation.value.clone(), }, ); + + verify_attestation( + deps.as_ref(), + attestation.height, + attestation.key.clone(), + AttestationValue::NonExistence, + ) + .unwrap(); } #[test] fn invalid_signature() { - let mut deps = setup(); + let (mut deps, _) = setup(); let attestation = Attestation { height: 1, @@ -415,7 +493,7 @@ fn invalid_signature() { #[test] fn inconsistent_timestamp() { - let mut deps = setup(); + let (mut deps, env) = setup(); let mut attestation = Attestation { height: 1, @@ -424,7 +502,128 @@ fn inconsistent_timestamp() { value: AttestationValue::Existence(b"value-1".into()), }; + // no attestations exist at this height yet + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::TimestampAtHeight { + height: attestation.height, + }, + &None::, + ); + // reach quorum first + reach_quorum( + &mut deps, + &env, + attestation.clone(), + [&*ATTESTOR_1, &*ATTESTOR_2], + ); + + // attesting to data at the same height but with a different timestamp should fail + attestation.timestamp = attestation + .timestamp + .plus_duration(Duration::from_nanos(1)) + .unwrap(); + + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Attest { + attestation: attestation.clone(), + attestor: vk(&ATTESTOR_1), + signature: sign(&ATTESTOR_1, &attestation), + }, + ) + .unwrap_err(), + Error::InconsistentTimestamp { + height: 1, + timestamp: Timestamp::from_nanos(101), + previously_attested_timestamp: Timestamp::from_nanos(100), + } + ); +} + +#[test] +fn add_attestor() { + let (mut deps, env) = setup(); + + // can't add an attestor that's already in the attestation set + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + new_attestor: vk(&ATTESTOR_3) + }), + ) + .unwrap_err(), + Error::AttestorAlreadyExists { + attestor: vk(&ATTESTOR_3) + } + ); + + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + new_attestor: vk(&ATTESTOR_4) + }), + ) + .unwrap(), + Response::new().add_event( + Event::new("attestor_added").add_attribute("attestor", vk(&ATTESTOR_4).to_string()) + ), + ); + + // the new attestor can now attest + reach_quorum( + &mut deps, + &env, + Attestation { + height: 1, + timestamp: Timestamp::from_secs(1), + key: b"key".into(), + value: AttestationValue::NonExistence, + }, + [&*ATTESTOR_4, &*ATTESTOR_3], + ); +} + +#[test] +fn remove_attestor() { + let (mut deps, env) = setup(); + + // can't remove an attestor that isn't in the attestation set + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::RemoveAttestor { + old_attestor: vk(&ATTESTOR_4) + }), + ) + .unwrap_err(), + Error::InvalidAttestor { + attestor: vk(&ATTESTOR_4) + } + ); + + let attestation = Attestation { + height: 1, + timestamp: Timestamp::from_secs(1), + key: b"key".into(), + value: AttestationValue::NonExistence, + }; + + // begin an attestation with a signature from the attestor that will be removed + execute( deps.as_mut(), mock_env(), @@ -437,40 +636,134 @@ fn inconsistent_timestamp() { ) .unwrap(); + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::RemoveAttestor { + old_attestor: vk(&ATTESTOR_1) + }), + ) + .unwrap(), + Response::new().add_event( + Event::new("attestor_removed").add_attribute("attestor", vk(&ATTESTOR_1).to_string()) + ), + ); + + // 2 signatures are required now, since the signature from attestor-1 is no longer valid + reach_quorum(&mut deps, &env, attestation, [&*ATTESTOR_2, &*ATTESTOR_3]); + + let attestation = Attestation { + height: 1, + timestamp: Timestamp::from_secs(1), + key: b"key2".into(), + value: AttestationValue::NonExistence, + }; + + // removed attestor can't attest to any new attestations + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Attest { + attestation: attestation.clone(), + attestor: vk(&ATTESTOR_1), + signature: sign(&ATTESTOR_1, &attestation) + }, + ) + .unwrap_err(), + Error::InvalidAttestor { + attestor: vk(&ATTESTOR_1) + } + ); +} + +#[test] +fn confirm_attestation() { + let (mut deps, _) = setup(); + + let attestation = Attestation { + height: 1, + timestamp: Timestamp::from_secs(1), + key: b"key2".into(), + value: AttestationValue::NonExistence, + }; + + // can't confirm non-existent attestation + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::ConfirmAttestation { + attestation: attestation.clone() + }, + ) + .unwrap_err(), + Error::QuorumNotReached { + quorum: const { NonZero::new(2).unwrap() }, + current: 0, + } + ); + execute( deps.as_mut(), mock_env(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Attest { attestation: attestation.clone(), - attestor: vk(&ATTESTOR_2), - signature: sign(&ATTESTOR_2, &attestation), + attestor: vk(&ATTESTOR_1), + signature: sign(&ATTESTOR_1, &attestation), }, ) .unwrap(); - // attesting to data at the same height but with a different timestamp should fail - attestation.timestamp = attestation - .timestamp - .plus_duration(Duration::from_nanos(1)) - .unwrap(); - + // can't confirm attestation that hasn't reached quorum assert_eq!( execute( deps.as_mut(), mock_env(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Attest { - attestation: attestation.clone(), - attestor: vk(&ATTESTOR_1), - signature: sign(&ATTESTOR_1, &attestation), + ExecuteMsg::ConfirmAttestation { + attestation: attestation.clone() }, ) .unwrap_err(), - Error::InconsistentTimestamp { - height: 1, - timestamp: Timestamp::from_nanos(101), - previously_attested_timestamp: Timestamp::from_nanos(100), + Error::QuorumNotReached { + quorum: const { NonZero::new(2).unwrap() }, + current: 1, } ); + + // lower the quorum to 1, making the current pending attestation valid + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + new_quorum: const { NonZero::new(1).unwrap() } + }), + ) + .unwrap(), + Response::new().add_event(Event::new("quorum_updated").add_attribute("quorum", "1")) + ); + + // the attestation has hit the new quorum, so it can be confirmed + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::ConfirmAttestation { + attestation: attestation.clone() + }, + ) + .unwrap() + .events[0] + .ty, + "quorum_reached", + ); } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/types.rs b/cosmwasm/ibc-union/lightclient/attested/src/types.rs index 94112659f3..4ab1371b7d 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/types.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/types.rs @@ -4,7 +4,7 @@ use ibc_union_light_client::spec::Timestamp; use serde::{Deserialize, Serialize}; use unionlabs::primitives::Bytes; -#[derive(Debug, Clone, Serialize, Deserialize, bincode::Encode, bincode::Decode)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bincode::Encode, bincode::Decode)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub struct Attestation { pub height: u64, @@ -13,8 +13,7 @@ pub struct Attestation { pub value: AttestationValue, } -#[derive(Debug, Serialize, Deserialize, bincode::Encode, bincode::Decode)] -#[serde(deny_unknown_fields, rename_all = "snake_case")] +#[derive(Debug, Clone, PartialEq, bincode::Encode, bincode::Decode)] pub struct AttestationKey { pub height: u64, pub key: Bytes, @@ -41,7 +40,7 @@ impl fmt::Display for AttestationValue { mod tests { use hex_literal::hex; use unionlabs::{ - encoding::{Bincode, EncodeAs, Json}, + encoding::{Bincode, Json}, test_utils::assert_codec_iso_bytes, }; @@ -70,4 +69,79 @@ mod tests { ), ); } + + #[test] + fn attestation_key_bincode() { + assert_codec_iso_bytes::<_, Bincode>( + &AttestationKey { + height: 1, + key: b"key".into(), + }, + &hex!( + "0100000000000000" // height + "0300000000000000" // key length + "6b6579" // b"key" + ), + ); + } + + #[test] + fn attestation_json() { + assert_codec_iso_bytes::<_, Json>( + &Attestation { + height: 1, + timestamp: Timestamp::from_nanos(2), + key: b"key".into(), + value: AttestationValue::Existence([0x00].into()), + }, + br#"{"height":1,"timestamp":2,"key":"0x6b6579","value":{"existence":"0x00"}}"#, + ); + + assert_codec_iso_bytes::<_, Json>( + &Attestation { + height: 1, + timestamp: Timestamp::from_nanos(2), + key: b"key".into(), + value: AttestationValue::NonExistence, + }, + br#"{"height":1,"timestamp":2,"key":"0x6b6579","value":"non_existence"}"#, + ); + } + + #[test] + fn attestation_bincode() { + assert_codec_iso_bytes::<_, Bincode>( + &Attestation { + height: 1, + timestamp: Timestamp::from_nanos(2), + key: b"key".into(), + value: AttestationValue::Existence([0x00].into()), + }, + &hex!( + "0100000000000000" // height + "0200000000000000" // timestamp + "0300000000000000" // key length + "6b6579" // b"key" + "01000000" // variant + "0100000000000000" // byte length + "00" // bytes + ), + ); + + assert_codec_iso_bytes::<_, Bincode>( + &Attestation { + height: 1, + timestamp: Timestamp::from_nanos(2), + key: b"key".into(), + value: AttestationValue::NonExistence, + }, + &hex!( + "0100000000000000" // height + "0200000000000000" // timestamp + "0300000000000000" // key length + "6b6579" // b"key" + "00000000" // variant + ), + ); + } } diff --git a/voyager/config.jsonc b/voyager/config.jsonc index 352ff6d4f5..8e2c591bab 100644 --- a/voyager/config.jsonc +++ b/voyager/config.jsonc @@ -305,9 +305,7 @@ "config": { "chain_id": "union-devnet-1", "rpc_url": "http://localhost:26657", - "prover_endpoints": [ - "https://galois.testnet-9.union.build:443" - ] + "prover_endpoints": ["https://galois.testnet-9.union.build:443"] } }, { @@ -356,4 +354,4 @@ }, "optimizer_delay_milliseconds": 100 } -} \ No newline at end of file +} From 2c81d10ef3549d69df148d2929c5cd273bb3941a Mon Sep 17 00:00:00 2001 From: benluelo Date: Mon, 10 Nov 2025 11:51:30 +0000 Subject: [PATCH 06/12] wip: key by chain id --- Cargo.lock | 20 ++ Cargo.toml | 1 + .../lightclient/attested/src/client.rs | 25 +- .../lightclient/attested/src/contract.rs | 37 ++- .../attested/src/contract/execute.rs | 201 ++++++++------ .../attested/src/contract/query.rs | 30 ++- .../lightclient/attested/src/errors.rs | 59 ++-- .../ibc-union/lightclient/attested/src/msg.rs | 29 +- .../lightclient/attested/src/state.rs | 53 +++- .../lightclient/attested/src/tests.rs | 255 +++++++++++++++++- .../lightclient/attested/src/types.rs | 17 +- lib/depolama/src/lib.rs | 1 + lib/depolama/src/tests.rs | 75 +++++- voyager/modules/proof/attested/Cargo.toml | 28 ++ voyager/modules/proof/attested/src/main.rs | 189 +++++++++++++ 15 files changed, 846 insertions(+), 174 deletions(-) create mode 100644 voyager/modules/proof/attested/Cargo.toml create mode 100644 voyager/modules/proof/attested/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index dcb67d3f79..e8698b6be5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17258,6 +17258,26 @@ dependencies = [ "unionlabs", ] +[[package]] +name = "voyager-proof-module-attested" +version = "0.0.0" +dependencies = [ + "clap", + "cometbft-rpc", + "embed-commit", + "ibc-union-spec", + "jsonrpsee 0.25.1", + "prost 0.12.6", + "protos", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "unionlabs", + "voyager-sdk", +] + [[package]] name = "voyager-proof-module-cosmos-sdk" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index c79a955de6..791f51bf55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ members = [ # "voyager/modules/state/movement", "voyager/modules/state/sui", + "voyager/modules/proof/attested", "voyager/modules/proof/cosmos-sdk", "voyager/modules/proof/cosmos-sdk-union", "voyager/modules/proof/ethermint", diff --git a/cosmwasm/ibc-union/lightclient/attested/src/client.rs b/cosmwasm/ibc-union/lightclient/attested/src/client.rs index 4ca295c434..d0670fca71 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/client.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/client.rs @@ -39,8 +39,11 @@ impl IbcClient for AttestedLightClient { StorageProof {}: Self::StorageProof, value: Vec, ) -> Result<(), IbcClientError> { + let ClientState::V1(client_state) = ctx.read_self_client_state()?; + verify_attestation( ctx.deps, + client_state.chain_id, height, key.into(), AttestationValue::Existence(value.into()), @@ -54,8 +57,16 @@ impl IbcClient for AttestedLightClient { key: Vec, StorageProof {}: Self::StorageProof, ) -> Result<(), IbcClientError> { - verify_attestation(ctx.deps, height, key.into(), AttestationValue::NonExistence) - .map_err(Into::into) + let ClientState::V1(client_state) = ctx.read_self_client_state()?; + + verify_attestation( + ctx.deps, + client_state.chain_id, + height, + key.into(), + AttestationValue::NonExistence, + ) + .map_err(Into::into) } fn verify_header( @@ -115,11 +126,14 @@ pub fn verify_header( let Header { height, timestamp } = header; - let attested_timestamp = deps.storage.read::(&height)?; + let attested_timestamp = deps + .storage + .read::(&(client_state.chain_id.clone(), height))?; ensure!( attested_timestamp == timestamp, Error::InvalidTimestamp { + chain_id: client_state.chain_id, height, attested_timestamp, timestamp @@ -138,6 +152,7 @@ pub fn verify_header( pub fn verify_attestation( deps: Deps, + chain_id: String, height: u64, key: Bytes, value: AttestationValue, @@ -147,10 +162,12 @@ pub fn verify_attestation( let attested = deps .storage .maybe_read::(&AttestationKey { + chain_id: chain_id.clone(), height, key: key.clone(), })? .ok_or_else(|| Error::AttestationNotFound { + chain_id: chain_id.clone(), height, key: key.clone(), })?; @@ -161,6 +178,7 @@ pub fn verify_attestation( ensure!( value == attested, Error::InvalidAttestedValue { + chain_id, height, key, attested: Existence(attested), @@ -177,6 +195,7 @@ pub fn verify_attestation( // invalid (attested @ Existence(_), value @ NonExistence) | (attested @ NonExistence, value @ Existence(_)) => Err(Error::InvalidAttestedValue { + chain_id, height, key, attested, diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs index 34bb11ec17..17cd81fa47 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs @@ -36,11 +36,18 @@ pub fn execute( } => attest(deps, attestation, attestor, signature), ExecuteMsg::ConfirmAttestation { attestation } => confirm_attestation(deps, attestation), ExecuteMsg::Restricted(msg) => match msg { - RestrictedExecuteMsg::SetQuorum { new_quorum } => set_quorum(deps, new_quorum), - RestrictedExecuteMsg::AddAttestor { new_attestor } => add_attestor(deps, new_attestor), - RestrictedExecuteMsg::RemoveAttestor { old_attestor } => { - remove_attestor(deps, old_attestor) - } + RestrictedExecuteMsg::SetQuorum { + chain_id, + new_quorum, + } => set_quorum(deps, chain_id, new_quorum), + RestrictedExecuteMsg::AddAttestor { + chain_id, + new_attestor, + } => add_attestor(deps, chain_id, new_attestor), + RestrictedExecuteMsg::RemoveAttestor { + chain_id, + old_attestor, + } => remove_attestor(deps, chain_id, old_attestor), }, } } @@ -48,14 +55,18 @@ pub fn execute( #[entry_point] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::Quorum {} => Ok(to_json_binary(&quorum(deps)?)?), - QueryMsg::Attestors {} => Ok(to_json_binary(&attestors(deps)?)?), - QueryMsg::AttestedValue { height, key } => { - Ok(to_json_binary(&attested_value(deps, height, key)?)?) - } - QueryMsg::TimestampAtHeight { height } => { - Ok(to_json_binary(×tamp_at_height(deps, height)?)?) - } + QueryMsg::Quorum { chain_id } => Ok(to_json_binary(&quorum(deps, chain_id)?)?), + QueryMsg::Attestors { chain_id } => Ok(to_json_binary(&attestors(deps, chain_id)?)?), + QueryMsg::AttestedValue { + chain_id, + height, + key, + } => Ok(to_json_binary(&attested_value( + deps, chain_id, height, key, + )?)?), + QueryMsg::TimestampAtHeight { chain_id, height } => Ok(to_json_binary( + ×tamp_at_height(deps, chain_id, height)?, + )?), QueryMsg::LightClient(msg) => { ibc_union_light_client::query::(deps, env, msg) .map_err(StdError::from) diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs index 81ccb4eed2..edc3b8030e 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs @@ -22,76 +22,83 @@ pub fn attest( attestor: H256, signature: H512, ) -> Result { + deps.storage + .maybe_read::(&(attestation.chain_id.clone(), attestor))? + .ok_or_else(|| Error::InvalidAttestor { + chain_id: attestation.chain_id.clone(), + attestor, + })?; + + if let Some(previously_attested_timestamp) = deps + .storage + .maybe_read::(&(attestation.chain_id.clone(), attestation.height))? + && previously_attested_timestamp != attestation.timestamp { - deps.storage - .maybe_read::(&attestor)? - .ok_or(Error::InvalidAttestor { attestor })?; - - if let Some(previously_attested_timestamp) = deps - .storage - .maybe_read::(&attestation.height)? - && previously_attested_timestamp != attestation.timestamp - { - return Err(Error::InconsistentTimestamp { - height: attestation.height, - timestamp: attestation.timestamp, - previously_attested_timestamp, - }); - } - - let attestation_key = AttestationKey { + return Err(Error::InconsistentTimestamp { + chain_id: attestation.chain_id.clone(), height: attestation.height, - key: attestation.key.clone(), - }; - - if let Some(value) = deps.storage.maybe_read::(&attestation_key)? { - return Err(Error::AlreadyAttested { - height: attestation.height, - timestamp: attestation.timestamp, - key: attestation.key, - value, - }); - } + timestamp: attestation.timestamp, + previously_attested_timestamp, + }); + } - ensure!( - deps.api - .ed25519_verify( - &(&attestation).encode_as::(), - signature.as_ref(), - attestor.as_ref() - ) - .map_err(StdError::from)?, - Error::InvalidSignature - ); + let attestation_key = AttestationKey { + chain_id: attestation.chain_id.clone(), + height: attestation.height, + key: attestation.key.clone(), + }; - let mut signatures = deps - .storage - .maybe_read::(&attestation)? - .unwrap_or_default(); + if let Some(value) = deps.storage.maybe_read::(&attestation_key)? { + return Err(Error::AlreadyAttested { + chain_id: attestation.chain_id.clone(), + height: attestation.height, + timestamp: attestation.timestamp, + key: attestation.key, + value, + }); + } - if signatures.insert(attestor, signature).is_some() { - return Err(Error::AttestationAlreadyReceived); - } + ensure!( + deps.api + .ed25519_verify( + &(&attestation).encode_as::(), + signature.as_ref(), + attestor.as_ref() + ) + .map_err(StdError::from)?, + Error::InvalidSignature + ); + + let mut signatures = deps + .storage + .maybe_read::(&attestation)? + .unwrap_or_default(); - let mut res = Response::new().add_event( - Event::new("attestation_submitted") - .add_attribute("height", attestation.height.to_string()) - .add_attribute("timestamp", attestation.timestamp.to_string()) - .add_attribute("key", attestation.key.to_string()) - .add_attribute("value", attestation.value.to_string()) - .add_attribute("attestor", attestor.to_string()) - .add_attribute("signature", signature.to_string()), - ); + if signatures.insert(attestor, signature).is_some() { + return Err(Error::AttestationAlreadyReceived { + chain_id: attestation.chain_id.clone(), + }); + } - if let Ok(event) = check_quorum(deps.branch(), &signatures, &attestation)? { - res = res.add_event(event); - } else { - deps.storage - .write::(&attestation, &signatures); - } + let mut res = Response::new().add_event( + Event::new("attestation_submitted") + .add_attribute("chain_id", attestation.chain_id.clone()) + .add_attribute("height", attestation.height.to_string()) + .add_attribute("timestamp", attestation.timestamp.to_string()) + .add_attribute("key", attestation.key.to_string()) + .add_attribute("value", attestation.value.to_string()) + .add_attribute("attestor", attestor.to_string()) + .add_attribute("signature", signature.to_string()), + ); - Ok(res) + if let Ok(event) = check_quorum(deps.branch(), &signatures, &attestation)? { + res = res.add_event(event); + } else { + deps.storage + .write::(&attestation, &signatures); } + + Ok(res) } pub fn confirm_attestation(deps: DepsMut, attestation: Attestation) -> Result { @@ -100,8 +107,13 @@ pub fn confirm_attestation(deps: DepsMut, attestation: Attestation) -> Result(&attestation)? .unwrap_or_default(); - let event = check_quorum(deps, &signatures, &attestation)? - .map_err(|(quorum, current)| Error::QuorumNotReached { quorum, current })?; + let event = check_quorum(deps, &signatures, &attestation)?.map_err(|(quorum, current)| { + Error::QuorumNotReached { + chain_id: attestation.chain_id, + quorum, + current, + } + })?; Ok(Response::new().add_event(event)) } @@ -111,11 +123,11 @@ fn check_quorum( signatures: &BTreeMap, attestation: &Attestation, ) -> Result, u8)>, Error> { - let quorum = deps.storage.read_item::()?; + let quorum = deps.storage.read::(&attestation.chain_id)?; let total_valid_signatures = signatures.iter().try_fold(0, |total, (attestor, _)| { deps.storage - .maybe_read::(attestor) + .maybe_read::(&(attestation.chain_id.clone(), *attestor)) .map(|exists| total + (exists.is_some() as usize)) })?; @@ -124,6 +136,7 @@ fn check_quorum( deps.storage.write::( &AttestationKey { + chain_id: attestation.chain_id.clone(), height: attestation.height, key: attestation.key.clone(), }, @@ -132,12 +145,13 @@ fn check_quorum( deps.storage .write::(attestation, signatures); - deps.storage - .upsert::(&attestation.height, |maybe_timestamp| { - Ok(maybe_timestamp.unwrap_or(attestation.timestamp)) - })?; + deps.storage.upsert::( + &(attestation.chain_id.clone(), attestation.height), + |maybe_timestamp| Ok(maybe_timestamp.unwrap_or(attestation.timestamp)), + )?; Ok(Ok(Event::new("quorum_reached") + .add_attribute("chain_id", attestation.chain_id.clone()) .add_attribute("height", attestation.height.to_string()) .add_attribute("timestamp", attestation.timestamp.to_string()) .add_attribute("key", attestation.key.to_string()) @@ -148,38 +162,65 @@ fn check_quorum( } } -pub fn set_quorum(deps: DepsMut, new_quorum: NonZero) -> Result { - deps.storage.write_item::(&new_quorum); +pub fn set_quorum( + deps: DepsMut, + chain_id: String, + new_quorum: NonZero, +) -> Result { + deps.storage.write::(&chain_id, &new_quorum); - Ok(Response::new() - .add_event(Event::new("quorum_updated").add_attribute("quorum", new_quorum.to_string()))) + Ok(Response::new().add_event( + Event::new("quorum_updated") + .add_attribute("chain_id", chain_id) + .add_attribute("quorum", new_quorum.to_string()), + )) } -pub fn add_attestor(deps: DepsMut, new_attestor: H256) -> Result { +pub fn add_attestor( + deps: DepsMut, + chain_id: String, + new_attestor: H256, +) -> Result { + let attestor_key = (chain_id.clone(), new_attestor); + if deps .storage - .maybe_read::(&new_attestor)? + .maybe_read::(&attestor_key)? .is_some() { Err(Error::AttestorAlreadyExists { + chain_id, attestor: new_attestor, }) } else { - deps.storage.write::(&new_attestor, &()); + deps.storage.write::(&attestor_key, &()); Ok(Response::new().add_event( - Event::new("attestor_added").add_attribute("attestor", new_attestor.to_string()), + Event::new("attestor_added") + .add_attribute("chain_id", chain_id) + .add_attribute("attestor", new_attestor.to_string()), )) } } -pub fn remove_attestor(deps: DepsMut, old_attestor: H256) -> Result { - if deps.storage.take::(&old_attestor)?.is_some() { +pub fn remove_attestor( + deps: DepsMut, + chain_id: String, + old_attestor: H256, +) -> Result { + if deps + .storage + .take::(&(chain_id.clone(), old_attestor))? + .is_some() + { Ok(Response::new().add_event( - Event::new("attestor_removed").add_attribute("attestor", old_attestor.to_string()), + Event::new("attestor_removed") + .add_attribute("chain_id", chain_id) + .add_attribute("attestor", old_attestor.to_string()), )) } else { Err(Error::InvalidAttestor { + chain_id, attestor: old_attestor, }) } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs index 4ffceb7973..4405e68490 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs @@ -11,32 +11,44 @@ use crate::{ types::{AttestationKey, AttestationValue}, }; -pub fn quorum(deps: Deps) -> Result, Error> { +pub fn quorum(deps: Deps, chain_id: String) -> Result, Error> { deps.storage - .maybe_read_item::()? - .ok_or(Error::QuorumNotSet) + .maybe_read::(&chain_id)? + .ok_or_else(|| Error::QuorumNotSet { chain_id }) } -pub fn attestors(deps: Deps) -> Result, Error> { +pub fn attestors(deps: Deps, chain_id: String) -> Result, Error> { deps.storage - .iter::(Order::Ascending) - .map(|r| r.map(|(attestor, ())| attestor)) + .iter_range::( + Order::Ascending, + (chain_id.clone(), H256::MIN)..=(chain_id, H256::MAX), + ) + .map(|r| r.map(|((_, attestor), ())| attestor)) .collect::>() .map_err(Into::into) } pub fn attested_value( deps: Deps, + chain_id: String, height: u64, key: Bytes, ) -> Result, Error> { deps.storage - .maybe_read::(&AttestationKey { height, key }) + .maybe_read::(&AttestationKey { + chain_id, + height, + key, + }) .map_err(Into::into) } -pub fn timestamp_at_height(deps: Deps, height: u64) -> Result, Error> { +pub fn timestamp_at_height( + deps: Deps, + chain_id: String, + height: u64, +) -> Result, Error> { deps.storage - .maybe_read::(&height) + .maybe_read::(&(chain_id, height)) .map_err(Into::into) } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs index f2eb981f2a..f807e2f645 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs @@ -19,6 +19,7 @@ pub enum Error { attempted to verify against value {value}" )] InvalidAttestedValue { + chain_id: String, height: u64, key: Bytes, value: AttestationValue, @@ -26,10 +27,11 @@ pub enum Error { }, #[error( - "height {height} was attested to with timestamp {attested_timestamp}, \ - but attempted to update with timestamp {timestamp}" + "height {height} on chain {chain_id} was attested to with timestamp \ + {attested_timestamp}, but attempted to update with timestamp {timestamp}" )] InvalidTimestamp { + chain_id: String, height: u64, attested_timestamp: Timestamp, timestamp: Timestamp, @@ -37,45 +39,58 @@ pub enum Error { #[error( "(height: {height}, timestamp: {timestamp}, key: {key}) has already been \ - attested to be {value}" + attested to be {value} on chain {chain_id}" )] AlreadyAttested { + chain_id: String, height: u64, timestamp: Timestamp, key: Bytes, value: AttestationValue, }, - #[error("invalid attestation signature")] - InvalidSignature, - - #[error("attestation already received")] - AttestationAlreadyReceived, - - #[error("{attestor} is not a valid attestor")] - InvalidAttestor { attestor: H256 }, - #[error( - "height {height} was previously attested to timestamp {previously_attested_timestamp}, \ - but this attestation is for timestamp {timestamp}" + "height {height} on chain {chain_id} was previously attested to timestamp \ + {previously_attested_timestamp}, but this attestation is for timestamp {timestamp}" )] InconsistentTimestamp { + chain_id: String, height: u64, timestamp: Timestamp, previously_attested_timestamp: Timestamp, }, - #[error("no attestation found for height {height}, key {key}")] - AttestationNotFound { height: u64, key: Bytes }, + #[error("no attestation found for height {height}, key {key} on chain {chain_id}")] + AttestationNotFound { + chain_id: String, + height: u64, + key: Bytes, + }, + + #[error("invalid attestation signature")] + InvalidSignature, + + #[error("attestation already received for chain {chain_id}")] + AttestationAlreadyReceived { chain_id: String }, - #[error("attestor {attestor} is already in the attestation set")] - AttestorAlreadyExists { attestor: H256 }, + #[error("{attestor} is not a valid attestor for chain {chain_id}")] + InvalidAttestor { chain_id: String, attestor: H256 }, - #[error("the quorum has not yet been set")] - QuorumNotSet, + #[error("attestor {attestor} is already in the attestation set for chain {chain_id}")] + AttestorAlreadyExists { chain_id: String, attestor: H256 }, - #[error("the quorum has not been reached for this attestation ({current}/{quorum})")] - QuorumNotReached { quorum: NonZero, current: u8 }, + #[error("the quorum has not yet been set for {chain_id}")] + QuorumNotSet { chain_id: String }, + + #[error( + "the attestation has not yet hit the quorum required for \ + chain {chain_id}: {current}/{quorum}" + )] + QuorumNotReached { + chain_id: String, + quorum: NonZero, + current: u8, + }, } impl From for IbcClientError { diff --git a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs index 8969ccc80b..7390adaf7a 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs @@ -29,28 +29,39 @@ pub enum RestrictedExecuteMsg { /// Set a new quorum for the attestations to be considered valid. /// /// If the new quorum is larger than the currently configuured quorum, any existing attestations that have already hit the quorum will still be considered valid, but any current pending attestations will need to reach the new quorum in order to be confirmed. - SetQuorum { new_quorum: NonZero }, + SetQuorum { + chain_id: String, + new_quorum: NonZero, + }, /// Add a new attestor to the attestation set. - AddAttestor { new_attestor: H256 }, + AddAttestor { + chain_id: String, + new_attestor: H256, + }, /// Add an existing attestor from the attestation set. - RemoveAttestor { old_attestor: H256 }, + RemoveAttestor { + chain_id: String, + old_attestor: H256, + }, } #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub enum QueryMsg { - /// Returns the currently configured quorum. - Quorum {}, - /// Returns the current attestation set. - Attestors {}, - /// Returns the value attested to under `key` at `height`. + /// Returns the currently configured quorum for `chain_id`. + Quorum { chain_id: String }, + /// Returns the current attestation set for `chain_id`. + Attestors { chain_id: String }, + /// Returns the value attested to under `key` at `height` on `chain_id`. AttestedValue { + chain_id: String, // #[serde(with = "serde_utils::string")] height: u64, key: Bytes, }, - /// Returns the timestamp attested to at `height`. + /// Returns the timestamp attested to at `height` on `chain_id`. TimestampAtHeight { + chain_id: String, // #[serde(with = "serde_utils::string")] height: u64, }, diff --git a/cosmwasm/ibc-union/lightclient/attested/src/state.rs b/cosmwasm/ibc-union/lightclient/attested/src/state.rs index 9494767c82..92226640db 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/state.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/state.rs @@ -21,6 +21,7 @@ impl Store for PendingAttestations { type Key = Attestation; type Value = BTreeMap; } +// not intended to be iterable in any meaningful way, so bincode is fine impl KeyCodecViaEncoding for PendingAttestations { type Encoding = Bincode; } @@ -35,6 +36,7 @@ impl Store for Attestations { type Key = AttestationKey; type Value = AttestationValue; } +// not intended to be iterable in any meaningful way, so bincode is fine impl KeyCodecViaEncoding for Attestations { type Encoding = Bincode; } @@ -42,13 +44,16 @@ impl ValueCodecViaEncoding for Attestations { type Encoding = Bincode; } -/// `Set` +/// `Map>` pub enum Attestors {} impl Store for Attestors { const PREFIX: Prefix = Prefix::new(b"attestors"); - type Key = H256; + type Key = (String, H256); type Value = (); } +// not intended to be iterable in any meaningful way, so bincode is fine +// note that this storage *is* iterated, but only ever all attestors under +// a specific chain id impl KeyCodecViaEncoding for Attestors { type Encoding = Bincode; } @@ -63,6 +68,7 @@ impl Store for AttestationAttestors { type Key = Attestation; type Value = BTreeMap; } +// not intended to be iterable in any meaningful way, so bincode is fine impl KeyCodecViaEncoding for AttestationAttestors { type Encoding = Bincode; } @@ -70,26 +76,42 @@ impl ValueCodecViaEncoding for AttestationAttestors { type Encoding = Bincode; } -/// `Map` +/// `Map<(ChainId, Height), Timestamp>` pub enum HeightTimestamps {} impl Store for HeightTimestamps { const PREFIX: Prefix = Prefix::new(b"height_timestamps"); - type Key = u64; + type Key = (String, u64); type Value = Timestamp; } // implement manually since bincode uses LE but we need BE for iteration -impl KeyCodec for HeightTimestamps { - fn encode_key(key: &u64) -> Bytes { - key.to_be_bytes().into() +impl KeyCodec<(String, u64)> for HeightTimestamps { + fn encode_key((chain_id, height): &(String, u64)) -> Bytes { + chain_id + .as_bytes() + .iter() + .copied() + .chain(height.to_be_bytes()) + .collect() } - fn decode_key(raw: &Bytes) -> StdResult { - raw.try_into().map(u64::from_be_bytes).map_err(|_| { - StdError::generic_err(format!( - "invalid key: expected 8 bytes, found {} (raw: {raw})", + fn decode_key(raw: &Bytes) -> StdResult<(String, u64)> { + if raw.len() < 8 { + return Err(StdError::generic_err(format!( + "invalid key: expected at least 8 bytes, found {} (raw: {raw})", raw.len() - )) - }) + ))); + } else { + let height = raw[raw.len() - 8..] + .try_into() + .map(u64::from_be_bytes) + .expect("8 bytes; qed;"); + + let chain_id = str::from_utf8(&raw[..raw.len() - 8]) + .map_err(|e| StdError::generic_err(format!("invalid chain id: {e}",)))? + .to_owned(); + + Ok((chain_id, height)) + } } } impl ValueCodecViaEncoding for HeightTimestamps { @@ -100,9 +122,12 @@ impl ValueCodecViaEncoding for HeightTimestamps { pub enum Quorum {} impl Store for Quorum { const PREFIX: Prefix = Prefix::new(b"quorum"); - type Key = (); + type Key = String; type Value = NonZero; } +impl KeyCodecViaEncoding for Quorum { + type Encoding = Bincode; +} impl ValueCodecViaEncoding for Quorum { type Encoding = Bincode; } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs index 8e984d14b4..9866e88b4f 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs @@ -52,6 +52,8 @@ static ATTESTOR_4: LazyLock = LazyLock::new(|| { )) }); +const CHAIN_ID: &str = "999"; + fn attestors() -> impl Iterator { [&ATTESTOR_1, &ATTESTOR_2, &ATTESTOR_3] .into_iter() @@ -107,14 +109,26 @@ fn setup() -> (OwnedDeps, Env) { env.clone(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + chain_id: CHAIN_ID.to_owned(), new_quorum: const { NonZero::new(2).unwrap() }, }), ) .unwrap(), - Response::new().add_event(Event::new("quorum_updated").add_attribute("quorum", "2")) + Response::new().add_event( + Event::new("quorum_updated") + .add_attribute("chain_id", CHAIN_ID) + .add_attribute("quorum", "2") + ) ); - assert_query_result(deps.as_ref(), &env, QueryMsg::Quorum {}, &2); + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::Quorum { + chain_id: CHAIN_ID.to_owned(), + }, + &2, + ); for attestor in attestors() { assert_eq!( @@ -123,12 +137,15 @@ fn setup() -> (OwnedDeps, Env) { env.clone(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + chain_id: CHAIN_ID.to_owned(), new_attestor: vk(attestor) }), ) .unwrap(), Response::new().add_event( - Event::new("attestor_added").add_attribute("attestor", vk(attestor).to_string()) + Event::new("attestor_added") + .add_attribute("chain_id", CHAIN_ID) + .add_attribute("attestor", vk(attestor).to_string()) ) ); } @@ -136,7 +153,9 @@ fn setup() -> (OwnedDeps, Env) { assert_query_result( deps.as_ref(), &env, - QueryMsg::Attestors {}, + QueryMsg::Attestors { + chain_id: CHAIN_ID.to_owned(), + }, &attestors().map(vk).collect::>(), ); @@ -152,15 +171,28 @@ fn reach_quorum<'a>( assert_eq!( verify_attestation( deps.as_ref(), + attestation.chain_id.clone(), attestation.height, attestation.key.clone(), attestation.value.clone(), ) .unwrap_err(), Error::AttestationNotFound { + chain_id: CHAIN_ID.to_owned(), + height: attestation.height, + key: attestation.key.clone(), + }, + ); + + assert_query_result( + deps.as_ref(), + env, + QueryMsg::AttestedValue { + chain_id: attestation.chain_id.clone(), height: attestation.height, key: attestation.key.clone(), }, + &None::, ); let mut res = Response::new(); @@ -186,6 +218,7 @@ fn reach_quorum<'a>( deps.as_ref(), env, QueryMsg::AttestedValue { + chain_id: CHAIN_ID.to_owned(), height: attestation.height, key: attestation.key.clone(), }, @@ -196,6 +229,7 @@ fn reach_quorum<'a>( deps.as_ref(), env, QueryMsg::TimestampAtHeight { + chain_id: CHAIN_ID.to_owned(), height: attestation.height, }, &attestation.timestamp, @@ -204,6 +238,7 @@ fn reach_quorum<'a>( // quorum reached, attestation should verify verify_attestation( deps.as_ref(), + attestation.chain_id.clone(), attestation.height, attestation.key.clone(), attestation.value.clone(), @@ -224,6 +259,7 @@ fn reach_quorum<'a>( ) .unwrap_err(), Error::AlreadyAttested { + chain_id: CHAIN_ID.to_owned(), height: attestation.height, timestamp: attestation.timestamp, key: attestation.key, @@ -237,6 +273,7 @@ fn attest() { let (mut deps, _) = setup(); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_nanos(100), key: b"key-1".into(), @@ -268,7 +305,9 @@ fn attest() { }, ) .unwrap_err(), - Error::AttestationAlreadyReceived, + Error::AttestationAlreadyReceived { + chain_id: CHAIN_ID.to_owned() + }, ); } @@ -277,6 +316,7 @@ fn verify_header_works() { let (mut deps, env) = setup(); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 2, timestamp: Timestamp::from_nanos(100), key: b"key-1".into(), @@ -288,7 +328,7 @@ fn verify_header_works() { verify_header( deps.as_ref(), ClientState::V1(ClientStateV1 { - chain_id: "999".to_owned(), + chain_id: CHAIN_ID.to_owned(), latest_height: 1, }), Header { @@ -311,7 +351,7 @@ fn verify_header_works() { verify_header( deps.as_ref(), ClientState::V1(ClientStateV1 { - chain_id: "999".to_owned(), + chain_id: CHAIN_ID.to_owned(), latest_height: 1, }), Header { @@ -322,6 +362,7 @@ fn verify_header_works() { .err() .unwrap(), Error::InvalidTimestamp { + chain_id: CHAIN_ID.to_owned(), height: 2, attested_timestamp: Timestamp::from_nanos(100), timestamp: Timestamp::from_nanos(101) @@ -336,7 +377,7 @@ fn verify_header_works() { } = verify_header( deps.as_ref(), ClientState::V1(ClientStateV1 { - chain_id: "999".to_owned(), + chain_id: CHAIN_ID.to_owned(), latest_height: 1, }), Header { @@ -350,7 +391,7 @@ fn verify_header_works() { assert_eq!( client_state, Some(ClientState::V1(ClientStateV1 { - chain_id: "999".to_owned(), + chain_id: CHAIN_ID.to_owned(), latest_height: 2, })), ); @@ -368,6 +409,7 @@ fn quorum() { let (mut deps, env) = setup(); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_nanos(100), key: b"key-1".into(), @@ -385,12 +427,14 @@ fn quorum() { assert_eq!( verify_attestation( deps.as_ref(), + attestation.chain_id.clone(), attestation.height, attestation.key.clone(), AttestationValue::NonExistence, ) .unwrap_err(), Error::InvalidAttestedValue { + chain_id: CHAIN_ID.to_owned(), height: attestation.height, key: attestation.key.clone(), value: AttestationValue::NonExistence, @@ -402,12 +446,14 @@ fn quorum() { assert_eq!( verify_attestation( deps.as_ref(), + attestation.chain_id.clone(), attestation.height, attestation.key.clone(), AttestationValue::Existence(b"invalid value".into()), ) .unwrap_err(), Error::InvalidAttestedValue { + chain_id: CHAIN_ID.to_owned(), height: attestation.height, key: attestation.key.clone(), value: AttestationValue::Existence(b"invalid value".into()), @@ -417,6 +463,7 @@ fn quorum() { verify_attestation( deps.as_ref(), + attestation.chain_id.clone(), attestation.height, attestation.key.clone(), AttestationValue::Existence(b"value-1".into()), @@ -424,6 +471,7 @@ fn quorum() { .unwrap(); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 2, timestamp: Timestamp::from_nanos(100), key: b"key-1".into(), @@ -441,12 +489,14 @@ fn quorum() { assert_eq!( verify_attestation( deps.as_ref(), + attestation.chain_id.clone(), attestation.height, attestation.key.clone(), AttestationValue::Existence(b"unexpected existence".into()), ) .unwrap_err(), Error::InvalidAttestedValue { + chain_id: CHAIN_ID.to_owned(), height: attestation.height, key: attestation.key.clone(), value: AttestationValue::Existence(b"unexpected existence".into()), @@ -456,6 +506,7 @@ fn quorum() { verify_attestation( deps.as_ref(), + attestation.chain_id.clone(), attestation.height, attestation.key.clone(), AttestationValue::NonExistence, @@ -468,6 +519,7 @@ fn invalid_signature() { let (mut deps, _) = setup(); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_nanos(100), key: b"key-1".into(), @@ -496,6 +548,7 @@ fn inconsistent_timestamp() { let (mut deps, env) = setup(); let mut attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_nanos(100), key: b"key-1".into(), @@ -507,6 +560,7 @@ fn inconsistent_timestamp() { deps.as_ref(), &env, QueryMsg::TimestampAtHeight { + chain_id: CHAIN_ID.to_owned(), height: attestation.height, }, &None::, @@ -539,6 +593,7 @@ fn inconsistent_timestamp() { ) .unwrap_err(), Error::InconsistentTimestamp { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_nanos(101), previously_attested_timestamp: Timestamp::from_nanos(100), @@ -557,11 +612,13 @@ fn add_attestor() { mock_env(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + chain_id: CHAIN_ID.to_owned(), new_attestor: vk(&ATTESTOR_3) }), ) .unwrap_err(), Error::AttestorAlreadyExists { + chain_id: CHAIN_ID.to_owned(), attestor: vk(&ATTESTOR_3) } ); @@ -572,12 +629,15 @@ fn add_attestor() { mock_env(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + chain_id: CHAIN_ID.to_owned(), new_attestor: vk(&ATTESTOR_4) }), ) .unwrap(), Response::new().add_event( - Event::new("attestor_added").add_attribute("attestor", vk(&ATTESTOR_4).to_string()) + Event::new("attestor_added") + .add_attribute("chain_id", CHAIN_ID) + .add_attribute("attestor", vk(&ATTESTOR_4).to_string()) ), ); @@ -586,6 +646,7 @@ fn add_attestor() { &mut deps, &env, Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_secs(1), key: b"key".into(), @@ -606,16 +667,19 @@ fn remove_attestor() { mock_env(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Restricted(RestrictedExecuteMsg::RemoveAttestor { + chain_id: CHAIN_ID.to_owned(), old_attestor: vk(&ATTESTOR_4) }), ) .unwrap_err(), Error::InvalidAttestor { + chain_id: CHAIN_ID.to_owned(), attestor: vk(&ATTESTOR_4) } ); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_secs(1), key: b"key".into(), @@ -642,12 +706,15 @@ fn remove_attestor() { mock_env(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Restricted(RestrictedExecuteMsg::RemoveAttestor { + chain_id: CHAIN_ID.to_owned(), old_attestor: vk(&ATTESTOR_1) }), ) .unwrap(), Response::new().add_event( - Event::new("attestor_removed").add_attribute("attestor", vk(&ATTESTOR_1).to_string()) + Event::new("attestor_removed") + .add_attribute("chain_id", CHAIN_ID) + .add_attribute("attestor", vk(&ATTESTOR_1).to_string()) ), ); @@ -655,6 +722,7 @@ fn remove_attestor() { reach_quorum(&mut deps, &env, attestation, [&*ATTESTOR_2, &*ATTESTOR_3]); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_secs(1), key: b"key2".into(), @@ -675,6 +743,7 @@ fn remove_attestor() { ) .unwrap_err(), Error::InvalidAttestor { + chain_id: CHAIN_ID.to_owned(), attestor: vk(&ATTESTOR_1) } ); @@ -685,6 +754,7 @@ fn confirm_attestation() { let (mut deps, _) = setup(); let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), height: 1, timestamp: Timestamp::from_secs(1), key: b"key2".into(), @@ -703,6 +773,7 @@ fn confirm_attestation() { ) .unwrap_err(), Error::QuorumNotReached { + chain_id: CHAIN_ID.to_owned(), quorum: const { NonZero::new(2).unwrap() }, current: 0, } @@ -732,6 +803,7 @@ fn confirm_attestation() { ) .unwrap_err(), Error::QuorumNotReached { + chain_id: CHAIN_ID.to_owned(), quorum: const { NonZero::new(2).unwrap() }, current: 1, } @@ -744,11 +816,16 @@ fn confirm_attestation() { mock_env(), message_info(&Addr::unchecked(""), &[]), ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + chain_id: CHAIN_ID.to_owned(), new_quorum: const { NonZero::new(1).unwrap() } }), ) .unwrap(), - Response::new().add_event(Event::new("quorum_updated").add_attribute("quorum", "1")) + Response::new().add_event( + Event::new("quorum_updated") + .add_attribute("chain_id", CHAIN_ID) + .add_attribute("quorum", "1") + ) ); // the attestation has hit the new quorum, so it can be confirmed @@ -767,3 +844,157 @@ fn confirm_attestation() { "quorum_reached", ); } + +#[test] +fn attestations_unique_per_chain() { + let (mut deps, env) = setup(); + + let attestation = Attestation { + chain_id: CHAIN_ID.to_owned(), + height: 1, + timestamp: Timestamp::from_nanos(100), + key: b"key-1".into(), + value: AttestationValue::Existence(b"value-1".into()), + }; + + reach_quorum( + &mut deps, + &env, + attestation.clone(), + [&*ATTESTOR_1, &*ATTESTOR_2], + ); + + // quorum reached, attestation should verify + verify_attestation( + deps.as_ref(), + attestation.chain_id.clone(), + attestation.height, + attestation.key.clone(), + attestation.value.clone(), + ) + .unwrap(); + + // attestation should not verify for a different chain id + assert_eq!( + verify_attestation( + deps.as_ref(), + "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG".to_owned(), + attestation.height, + attestation.key.clone(), + attestation.value.clone(), + ) + .unwrap_err(), + Error::AttestationNotFound { + chain_id: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG".to_owned(), + height: attestation.height, + key: attestation.key.clone(), + }, + ); +} + +#[test] +fn quorum_unique_per_chain() { + let (mut deps, env) = setup(); + + assert_eq!( + query( + deps.as_ref(), + env.clone(), + QueryMsg::Quorum { + chain_id: "998".to_owned(), + }, + ) + .unwrap_err(), + Error::QuorumNotSet { + chain_id: "998".to_owned() + } + ); + + assert_eq!( + execute( + deps.as_mut(), + env.clone(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + chain_id: "998".to_owned(), + new_quorum: const { NonZero::new(3).unwrap() }, + }), + ) + .unwrap(), + Response::new().add_event( + Event::new("quorum_updated") + .add_attribute("chain_id", "998") + .add_attribute("quorum", "3") + ) + ); + + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::Quorum { + chain_id: "998".to_owned(), + }, + &3, + ); +} + +#[test] +fn attestors_unique_per_chain() { + let (mut deps, env) = setup(); + + assert_query_result::>( + deps.as_ref(), + &env, + QueryMsg::Attestors { + chain_id: "998".to_owned(), + }, + &BTreeSet::default(), + ); + + assert_eq!( + execute( + deps.as_mut(), + env.clone(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + chain_id: "998".to_owned(), + new_attestor: H256::MIN, + }), + ) + .unwrap(), + Response::new().add_event( + Event::new("attestor_added") + .add_attribute("chain_id", "998") + .add_attribute("attestor", ::MIN.to_string()) + ) + ); + + assert_eq!( + execute( + deps.as_mut(), + env.clone(), + message_info(&Addr::unchecked(""), &[]), + ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + chain_id: "998".to_owned(), + new_attestor: H256::MAX, + }), + ) + .unwrap(), + Response::new().add_event( + Event::new("attestor_added") + .add_attribute("chain_id", "998") + .add_attribute("attestor", ::MAX.to_string()) + ) + ); + + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::Attestors { + chain_id: "998".to_owned(), + }, + &[::MIN, ::MAX] + .into_iter() + .collect::>(), + ); +} diff --git a/cosmwasm/ibc-union/lightclient/attested/src/types.rs b/cosmwasm/ibc-union/lightclient/attested/src/types.rs index 4ab1371b7d..4fc2f7c0f8 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/types.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/types.rs @@ -7,6 +7,7 @@ use unionlabs::primitives::Bytes; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, bincode::Encode, bincode::Decode)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub struct Attestation { + pub chain_id: String, pub height: u64, pub timestamp: Timestamp, pub key: Bytes, @@ -15,6 +16,7 @@ pub struct Attestation { #[derive(Debug, Clone, PartialEq, bincode::Encode, bincode::Decode)] pub struct AttestationKey { + pub chain_id: String, pub height: u64, pub key: Bytes, } @@ -74,10 +76,13 @@ mod tests { fn attestation_key_bincode() { assert_codec_iso_bytes::<_, Bincode>( &AttestationKey { + chain_id: "999".to_owned(), height: 1, key: b"key".into(), }, &hex!( + "0300000000000000" // chain id length + "393939" // chain id "0100000000000000" // height "0300000000000000" // key length "6b6579" // b"key" @@ -89,22 +94,24 @@ mod tests { fn attestation_json() { assert_codec_iso_bytes::<_, Json>( &Attestation { + chain_id: "999".to_owned(), height: 1, timestamp: Timestamp::from_nanos(2), key: b"key".into(), value: AttestationValue::Existence([0x00].into()), }, - br#"{"height":1,"timestamp":2,"key":"0x6b6579","value":{"existence":"0x00"}}"#, + br#"{"chain_id":"999","height":1,"timestamp":2,"key":"0x6b6579","value":{"existence":"0x00"}}"#, ); assert_codec_iso_bytes::<_, Json>( &Attestation { + chain_id: "999".to_owned(), height: 1, timestamp: Timestamp::from_nanos(2), key: b"key".into(), value: AttestationValue::NonExistence, }, - br#"{"height":1,"timestamp":2,"key":"0x6b6579","value":"non_existence"}"#, + br#"{"chain_id":"999","height":1,"timestamp":2,"key":"0x6b6579","value":"non_existence"}"#, ); } @@ -112,12 +119,15 @@ mod tests { fn attestation_bincode() { assert_codec_iso_bytes::<_, Bincode>( &Attestation { + chain_id: "999".to_owned(), height: 1, timestamp: Timestamp::from_nanos(2), key: b"key".into(), value: AttestationValue::Existence([0x00].into()), }, &hex!( + "0300000000000000" // chain id length + "393939" // chain id "0100000000000000" // height "0200000000000000" // timestamp "0300000000000000" // key length @@ -130,12 +140,15 @@ mod tests { assert_codec_iso_bytes::<_, Bincode>( &Attestation { + chain_id: "999".to_owned(), height: 1, timestamp: Timestamp::from_nanos(2), key: b"key".into(), value: AttestationValue::NonExistence, }, &hex!( + "0300000000000000" // chain id length + "393939" // chain id "0100000000000000" // height "0200000000000000" // timestamp "0300000000000000" // key length diff --git a/lib/depolama/src/lib.rs b/lib/depolama/src/lib.rs index c13a9a4c97..c8664633dd 100644 --- a/lib/depolama/src/lib.rs +++ b/lib/depolama/src/lib.rs @@ -504,6 +504,7 @@ impl StorageExt for dyn Storage + '_ { 'block: { for byte in raw_key.iter_mut().rev() { if *byte == u8::MAX { + *byte = 0; continue; } diff --git a/lib/depolama/src/tests.rs b/lib/depolama/src/tests.rs index b37a4f82f4..519a181e71 100644 --- a/lib/depolama/src/tests.rs +++ b/lib/depolama/src/tests.rs @@ -1,9 +1,14 @@ use std::ops::Bound; -use cosmwasm_std::{Order, testing::MockStorage}; +use cosmwasm_std::{ + Order, + testing::{MockStorage, mock_dependencies}, +}; +use num_traits::ToBytes; use unionlabs::primitives::ByteArrayExt; use super::*; +use crate::value::{ValueCodecViaEncoding, ValueUnitEncoding}; enum TestStore {} @@ -71,10 +76,10 @@ mod iterator { use super::*; #[allow(clippy::type_complexity)] - fn init() -> (MockStorage, [(u64, (u64, u64)); 3]) { + fn init() -> (MockStorage, [(u64, (u64, u64)); 4]) { let mut storage = MockStorage::new(); - let kvs = [(1, (1, 1)), (2, (1, 2)), (3, (1, 3))]; + let kvs = [(1, (1, 1)), (2, (1, 2)), (3, (1, 3)), (u64::MAX, (0, 0))]; // write additional values to storage to ensure only the prefixed store is iterated @@ -119,7 +124,7 @@ mod iterator { let (storage, kvs) = init(); let res = storage - .iter_range::(Order::Ascending, 1..=3) + .iter_range::(Order::Ascending, 0..=u64::MAX) .collect::, _>>() .unwrap(); @@ -155,7 +160,7 @@ mod iterator { let (storage, kvs) = init(); let res = storage - .iter_range::(Order::Ascending, ..=3) + .iter_range::(Order::Ascending, ..=u64::MAX) .collect::, _>>() .unwrap(); @@ -183,7 +188,7 @@ mod iterator { .collect::, _>>() .unwrap(); - assert_eq!(res, [(2, (1, 2)), (3, (1, 3))]); + assert_eq!(res, [(2, (1, 2)), (3, (1, 3)), (u64::MAX, (0, 0))]); } #[test] @@ -191,11 +196,14 @@ mod iterator { let (storage, _kvs) = init(); let res = storage - .iter_range::(Order::Ascending, (Bound::Excluded(1), Bound::Included(3))) + .iter_range::( + Order::Ascending, + (Bound::Excluded(1), Bound::Included(u64::MAX)), + ) .collect::, _>>() .unwrap(); - assert_eq!(res, [(2, (1, 2)), (3, (1, 3))]); + assert_eq!(res, [(2, (1, 2)), (3, (1, 3)), (u64::MAX, (0, 0))]); } #[test] @@ -203,12 +211,59 @@ mod iterator { let (storage, _kvs) = init(); let res = storage - .iter_range::(Order::Ascending, (Bound::Excluded(1), Bound::Excluded(3))) + .iter_range::( + Order::Ascending, + (Bound::Excluded(1), Bound::Excluded(u64::MAX)), + ) .collect::, _>>() .unwrap(); - assert_eq!(res, [(2, (1, 2))]); + assert_eq!(res, [(2, (1, 2)), (3, (1, 3))]); + } +} + +#[test] +fn compound_key_iter_range() { + enum A {} + impl Store for A { + const PREFIX: Prefix = Prefix::new(&[1]); + type Key = (Bytes, u32); + type Value = (); + } + impl KeyCodec<(Bytes, u32)> for A { + fn encode_key((bz, n): &(Bytes, u32)) -> Bytes { + bz.iter().copied().chain(n.to_be_bytes()).collect() + } + + fn decode_key(raw: &Bytes) -> StdResult<(Bytes, u32)> { + let (bz, n) = raw.split_at(raw.len() - 4); + Ok(( + bz.into(), + u32::from_be_bytes(n.try_into().expect("is 4 bytes; qed;")), + )) + } } + impl ValueCodecViaEncoding for A { + type Encoding = ValueUnitEncoding; + } + + let mut deps = mock_dependencies(); + + deps.storage.write::(&([0x00].into(), u32::MAX), &()); + deps.storage.write::(&([0x01].into(), 0), &()); + + let range = deps + .storage + .iter_range::( + Order::Ascending, + ([0x00].into(), 0)..=([0x00].into(), u32::MAX), + ) + .map(|r| r.map(|(k, _)| k)) + .collect::, _>>() + .unwrap(); + + // this does NOT include ([0x01], 0) + assert_eq!(range, [([0x00].into(), u32::MAX)]); } #[test] diff --git a/voyager/modules/proof/attested/Cargo.toml b/voyager/modules/proof/attested/Cargo.toml new file mode 100644 index 0000000000..7724f7af81 --- /dev/null +++ b/voyager/modules/proof/attested/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "voyager-proof-module-attested" +version = "0.0.0" + +authors = { workspace = true } +edition = { workspace = true } +license-file = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } + +[lints] +workspace = true + +[dependencies] +clap = { workspace = true, features = ["derive"] } +cometbft-rpc = { workspace = true } +embed-commit = { workspace = true } +ibc-union-spec = { workspace = true, features = ["serde"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +prost = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-sdk = { workspace = true } diff --git a/voyager/modules/proof/attested/src/main.rs b/voyager/modules/proof/attested/src/main.rs new file mode 100644 index 0000000000..79dc8041c3 --- /dev/null +++ b/voyager/modules/proof/attested/src/main.rs @@ -0,0 +1,189 @@ +#![warn(clippy::unwrap_used)] + +use std::num::ParseIntError; + +use ibc_union_spec::{ + IbcUnion, + path::{IBC_UNION_COSMWASM_COMMITMENT_PREFIX, StorePath}, +}; +use jsonrpsee::{ + Extensions, + core::{RpcResult, async_trait}, + types::ErrorObject, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use tracing::{error, instrument, warn}; +use unionlabs::{ + ErrorReporter, + bounded::BoundedI64, + cosmos::ics23::commitment_proof::CommitmentProof, + ibc::core::{client::height::Height, commitment::merkle_proof::MerkleProof}, + primitives::{Bech32, H256}, +}; +use voyager_sdk::{ + anyhow, into_value, + plugin::ProofModule, + primitives::ChainId, + rpc::{FATAL_JSONRPC_ERROR_CODE, ProofModuleServer, rpc_error, types::ProofModuleInfo}, + types::ProofType, +}; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + >::run().await; +} + +#[derive(clap::Subcommand)] +pub enum Cmd { + ChainId, + LatestHeight, +} + +#[derive(Debug, Clone)] +pub struct Module { + pub chain_id: ChainId, + pub chain_revision: u64, + + pub cometbft_client: cometbft_rpc::Client, + + pub attestation_client_address: Bech32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub rpc_url: String, + pub attestation_client_address: Bech32, +} + +impl ProofModule for Module { + type Config = Config; + + async fn new(config: Self::Config, info: ProofModuleInfo) -> anyhow::Result { + let tm_client = cometbft_rpc::Client::new(config.rpc_url).await?; + + let chain_id = tm_client.status().await?.node_info.network; + + info.ensure_chain_id(&chain_id)?; + + let chain_revision = chain_id + .split('-') + .next_back() + .ok_or_else(|| ChainIdParseError { + found: chain_id.clone(), + source: None, + })? + .parse() + .map_err(|err| ChainIdParseError { + found: chain_id.clone(), + source: Some(err), + })?; + + Ok(Self { + cometbft_client: tm_client, + chain_id: ChainId::new(chain_id), + chain_revision, + attestation_client_address: config.attestation_client_address, + }) + } +} + +impl Module { + #[must_use] + pub fn make_height(&self, height: u64) -> Height { + Height::new_with_revision(self.chain_revision, height) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("unable to parse chain id: expected format `-`, found `{found}`")] +pub struct ChainIdParseError { + found: String, + #[source] + source: Option, +} + +#[async_trait] +impl ProofModuleServer for Module { + #[instrument(skip_all, fields(chain_id = %self.chain_id))] + async fn query_ibc_proof( + &self, + _: &Extensions, + at: Height, + path: StorePath, + ) -> RpcResult> { + // TODO: Extract this into a function somewhere, reuse in lightclients + let data = [0x03] + .into_iter() + .chain(*self.ibc_host_contract_address.data()) + .chain(IBC_UNION_COSMWASM_COMMITMENT_PREFIX) + .chain(path.key()) + .collect::>(); + + let query_result = self + .cometbft_client + .abci_query( + "store/wasm/key", + data, + // THIS -1 IS VERY IMPORTANT!!! + // + // a proof at height H is provable at height H + 1 + // we assume that the height passed in to this function is the intended height to prove against, thus we have to query the height - 1 + Some(BoundedI64::new(at.height() - 1).map_err(|e| { + let message = format!("invalid height value: {}", ErrorReporter(e)); + error!(%message); + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + message, + Some(json!({ "height": at })), + ) + })?), + true, + ) + .await + .map_err(rpc_error("error querying ibc proof", None))?; + + // if this field is none, the proof is not available at this height + let Some(proofs) = query_result.response.proof_ops else { + return Ok(None); + }; + + let proofs = proofs + .ops + .into_iter() + .map(|op| { + ::decode(&*op.data) + .map_err(|e| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("invalid height value: {}", ErrorReporter(e)), + Some(json!({ "height": at })), + ) + }) + }) + .collect::, _>>()?; + + let proof = + MerkleProof::try_from(protos::ibc::core::commitment::v1::MerkleProof { proofs }) + .map_err(|e| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("invalid merkle proof value: {}", ErrorReporter(e)), + Some(json!({ "height": at })), + ) + })?; + + let proof_type = if proof + .proofs + .iter() + .any(|p| matches!(&p, CommitmentProof::Nonexist(_))) + { + ProofType::NonMembership + } else { + ProofType::Membership + }; + + Ok(Some((into_value(proof), proof_type))) + } +} From 659e9183fc2558e1232c2987961c60e32ce90305 Mon Sep 17 00:00:00 2001 From: benluelo Date: Mon, 10 Nov 2025 14:57:43 +0000 Subject: [PATCH 07/12] wip --- Cargo.lock | 1 + cosmwasm/ibc-union/lightclient/attested/Cargo.toml | 1 + cosmwasm/ibc-union/lightclient/attested/src/msg.rs | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8698b6be5..124c04d6c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1892,6 +1892,7 @@ dependencies = [ "ibc-union-light-client", "ibc-union-msg", "serde", + "serde-utils", "serde_json", "thiserror 2.0.12", "unionlabs", diff --git a/cosmwasm/ibc-union/lightclient/attested/Cargo.toml b/cosmwasm/ibc-union/lightclient/attested/Cargo.toml index 7f26da2bbb..a46f83ea23 100644 --- a/cosmwasm/ibc-union/lightclient/attested/Cargo.toml +++ b/cosmwasm/ibc-union/lightclient/attested/Cargo.toml @@ -24,6 +24,7 @@ frissitheto = { workspace = true } ibc-union-light-client = { workspace = true } ibc-union-msg = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde-utils = { workspace = true } thiserror = { workspace = true } unionlabs = { workspace = true, features = ["ethabi"] } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs index 7390adaf7a..5862c0f006 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs @@ -28,7 +28,7 @@ pub enum ExecuteMsg { pub enum RestrictedExecuteMsg { /// Set a new quorum for the attestations to be considered valid. /// - /// If the new quorum is larger than the currently configuured quorum, any existing attestations that have already hit the quorum will still be considered valid, but any current pending attestations will need to reach the new quorum in order to be confirmed. + /// If the new quorum is larger than the currently configured quorum, any existing attestations that have already hit the quorum will still be considered valid, but any current pending attestations will need to reach the new quorum in order to be confirmed. SetQuorum { chain_id: String, new_quorum: NonZero, @@ -55,14 +55,14 @@ pub enum QueryMsg { /// Returns the value attested to under `key` at `height` on `chain_id`. AttestedValue { chain_id: String, - // #[serde(with = "serde_utils::string")] + #[serde(with = "serde_utils::string")] height: u64, key: Bytes, }, /// Returns the timestamp attested to at `height` on `chain_id`. TimestampAtHeight { chain_id: String, - // #[serde(with = "serde_utils::string")] + #[serde(with = "serde_utils::string")] height: u64, }, #[serde(untagged)] From 20ff159a9f82ac41c21a9c9bff39a54b3ac361c2 Mon Sep 17 00:00:00 2001 From: benluelo Date: Mon, 10 Nov 2025 17:00:02 +0000 Subject: [PATCH 08/12] wip --- Cargo.lock | 26 ++- Cargo.toml | 1 + .../lightclient/attested/src/contract.rs | 3 +- .../attested/src/contract/query.rs | 20 ++ .../ibc-union/lightclient/attested/src/msg.rs | 2 + .../lightclient/attested/src/tests.rs | 92 ++++++++- lib/voyager-primitives/src/lib.rs | 3 + .../modules/finality/attested-evm/Cargo.toml | 31 +++ .../modules/finality/attested-evm/src/main.rs | 164 +++++++++++++++ voyager/modules/proof/attested/Cargo.toml | 29 +-- voyager/modules/proof/attested/src/main.rs | 192 ++++++------------ voyager/plugins/attestor/evm/src/main.rs | 1 + 12 files changed, 419 insertions(+), 145 deletions(-) create mode 100644 voyager/modules/finality/attested-evm/Cargo.toml create mode 100644 voyager/modules/finality/attested-evm/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 124c04d6c3..1c7e7b0d30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16827,6 +16827,29 @@ dependencies = [ "voyager-sdk", ] +[[package]] +name = "voyager-finality-module-attested-evm" +version = "0.0.0" +dependencies = [ + "alloy", + "attested-light-client", + "attested-light-client-types", + "clap", + "cometbft-rpc", + "embed-commit", + "ibc-union-spec", + "jsonrpsee 0.25.1", + "prost 0.12.6", + "protos", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "unionlabs", + "voyager-sdk", +] + [[package]] name = "voyager-finality-module-base" version = "0.0.0" @@ -17263,7 +17286,8 @@ dependencies = [ name = "voyager-proof-module-attested" version = "0.0.0" dependencies = [ - "clap", + "attested-light-client", + "attested-light-client-types", "cometbft-rpc", "embed-commit", "ibc-union-spec", diff --git a/Cargo.toml b/Cargo.toml index 791f51bf55..852206adca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,6 +192,7 @@ members = [ "voyager/modules/finality/tendermint", "voyager/modules/finality/trusted-evm", "voyager/modules/finality/sui", + "voyager/modules/finality/attested-evm", "voyager/plugins/client-update/base", "voyager/plugins/client-update/bob", diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs index 17cd81fa47..6b764a976e 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs @@ -9,7 +9,7 @@ use crate::{ client::AttestedLightClient, contract::{ execute::{add_attestor, attest, confirm_attestation, remove_attestor, set_quorum}, - query::{attested_value, attestors, quorum, timestamp_at_height}, + query::{attested_value, attestors, latest_height, quorum, timestamp_at_height}, }, errors::Error, msg::{ExecuteMsg, QueryMsg, RestrictedExecuteMsg}, @@ -67,6 +67,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { QueryMsg::TimestampAtHeight { chain_id, height } => Ok(to_json_binary( ×tamp_at_height(deps, chain_id, height)?, )?), + QueryMsg::LatestHeight { chain_id } => Ok(to_json_binary(&latest_height(deps, chain_id)?)?), QueryMsg::LightClient(msg) => { ibc_union_light_client::query::(deps, env, msg) .map_err(StdError::from) diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs index 4405e68490..3a1cb0aa84 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs @@ -3,6 +3,7 @@ use std::{collections::BTreeSet, num::NonZero}; use cosmwasm_std::{Deps, Order}; use depolama::{Bytes, StorageExt}; use ibc_union_light_client::spec::Timestamp; +use serde::{Deserialize, Serialize}; use unionlabs::primitives::H256; use crate::{ @@ -52,3 +53,22 @@ pub fn timestamp_at_height( .maybe_read::(&(chain_id, height)) .map_err(Into::into) } + +pub fn latest_height(deps: Deps, chain_id: String) -> Result, Error> { + deps.storage + .iter_range::( + Order::Descending, + (chain_id.clone(), 0)..=(chain_id.clone(), u64::MAX), + ) + .next() + .map(|r| r.map(|((_, height), timestamp)| LatestHeight { height, timestamp })) + .transpose() + .map_err(Into::into) +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub struct LatestHeight { + pub height: u64, + pub timestamp: Timestamp, +} diff --git a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs index 5862c0f006..037cab8a24 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs @@ -65,6 +65,8 @@ pub enum QueryMsg { #[serde(with = "serde_utils::string")] height: u64, }, + /// Returns the latest height and timestamp attested to for `chain_id`. + LatestHeight { chain_id: String }, #[serde(untagged)] LightClient(ibc_union_light_client::msg::QueryMsg), } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs index 9866e88b4f..c50a37b3be 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs @@ -21,7 +21,7 @@ use unionlabs::{ use crate::{ client::{verify_attestation, verify_header}, - contract::{execute, migrate, query}, + contract::{execute, migrate, query, query::LatestHeight}, errors::Error, msg::{ExecuteMsg, QueryMsg, RestrictedExecuteMsg}, types::{Attestation, AttestationValue}, @@ -998,3 +998,93 @@ fn attestors_unique_per_chain() { .collect::>(), ); } + +#[test] +fn latest_height() { + let (mut deps, env) = setup(); + + reach_quorum( + &mut deps, + &env, + Attestation { + chain_id: CHAIN_ID.to_owned(), + height: 1, + timestamp: Timestamp::from_secs(1), + key: b"key".into(), + value: AttestationValue::NonExistence, + }, + [&*ATTESTOR_1, &*ATTESTOR_2], + ); + + // no heights attested to for this chain + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::LatestHeight { + chain_id: "998".to_owned(), + }, + &None::, + ); + + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::LatestHeight { + chain_id: CHAIN_ID.to_owned(), + }, + &Some(LatestHeight { + height: 1, + timestamp: Timestamp::from_secs(1), + }), + ); + + reach_quorum( + &mut deps, + &env, + Attestation { + chain_id: CHAIN_ID.to_owned(), + height: 2, + timestamp: Timestamp::from_secs(1), + key: b"key".into(), + value: AttestationValue::NonExistence, + }, + [&*ATTESTOR_1, &*ATTESTOR_2], + ); + + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::LatestHeight { + chain_id: CHAIN_ID.to_owned(), + }, + &Some(LatestHeight { + height: 2, + timestamp: Timestamp::from_secs(1), + }), + ); + + reach_quorum( + &mut deps, + &env, + Attestation { + chain_id: CHAIN_ID.to_owned(), + height: u64::MAX, + timestamp: Timestamp::from_secs(1), + key: b"key".into(), + value: AttestationValue::NonExistence, + }, + [&*ATTESTOR_1, &*ATTESTOR_2], + ); + + assert_query_result( + deps.as_ref(), + &env, + QueryMsg::LatestHeight { + chain_id: CHAIN_ID.to_owned(), + }, + &Some(LatestHeight { + height: u64::MAX, + timestamp: Timestamp::from_secs(1), + }), + ); +} diff --git a/lib/voyager-primitives/src/lib.rs b/lib/voyager-primitives/src/lib.rs index df34c431e0..5d87b7c632 100644 --- a/lib/voyager-primitives/src/lib.rs +++ b/lib/voyager-primitives/src/lib.rs @@ -257,6 +257,9 @@ impl ConsensusType { /// [L2 settlement]: https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock pub const OPTIMISM: &'static str = "optimism"; + /// An attested client. + pub const ATTESTED: &'static str = "attested"; + // lots more to come - near, linea, polygon - stay tuned } diff --git a/voyager/modules/finality/attested-evm/Cargo.toml b/voyager/modules/finality/attested-evm/Cargo.toml new file mode 100644 index 0000000000..7c0aefab14 --- /dev/null +++ b/voyager/modules/finality/attested-evm/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "voyager-finality-module-attested-evm" +version = "0.0.0" + +authors = { workspace = true } +edition = { workspace = true } +license-file = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } + +[lints] +workspace = true + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "reqwest-rustls-tls", "provider-ws"] } +attested-light-client = { workspace = true } +attested-light-client-types = { workspace = true, features = ["serde"] } +clap = { workspace = true, features = ["derive"] } +cometbft-rpc = { workspace = true } +embed-commit = { workspace = true } +ibc-union-spec = { workspace = true, features = ["serde"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +prost = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-sdk = { workspace = true } diff --git a/voyager/modules/finality/attested-evm/src/main.rs b/voyager/modules/finality/attested-evm/src/main.rs new file mode 100644 index 0000000000..870e6899ed --- /dev/null +++ b/voyager/modules/finality/attested-evm/src/main.rs @@ -0,0 +1,164 @@ +use alloy::{ + eips::BlockNumberOrTag, + network::AnyNetwork, + providers::{DynProvider, Provider, ProviderBuilder}, +}; +use attested_light_client::{contract::query::LatestHeight, types::AttestationValue}; +use ibc_union_spec::{IbcUnion, Timestamp, path::StorePath}; +use jsonrpsee::{ + Extensions, + core::{RpcResult, async_trait}, + types::ErrorObject, +}; +use protos::cosmwasm::wasm::v1::{QuerySmartContractStateRequest, QuerySmartContractStateResponse}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use tracing::instrument; +use unionlabs::{ + ErrorReporter, + ibc::core::client::height::Height, + primitives::{Bech32, H256}, +}; +use voyager_sdk::{ + anyhow, into_value, + plugin::FinalityModule, + primitives::{ChainId, ConsensusType}, + rpc::{FinalityModuleServer, types::FinalityModuleInfo}, +}; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await; +} + +#[derive(Debug, Clone)] +pub struct Module { + pub chain_id: ChainId, + pub attestation_client_address: Bech32, + pub cometbft_client: cometbft_rpc::Client, + pub provider: DynProvider, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub attestation_client_address: Bech32, + pub cometbft_rpc_url: String, + pub eth_rpc_url: String, +} + +impl FinalityModule for Module { + type Config = Config; + + async fn new(config: Self::Config, info: FinalityModuleInfo) -> anyhow::Result { + let provider = DynProvider::new( + ProviderBuilder::new() + .network::() + .connect(&config.eth_rpc_url) + .await?, + ); + + info.ensure_chain_id(provider.get_chain_id().await?.to_string())?; + info.ensure_consensus_type(ConsensusType::ATTESTED)?; + + Ok(Self { + chain_id: info.chain_id, + attestation_client_address: config.attestation_client_address, + cometbft_client: cometbft_rpc::Client::new(config.cometbft_rpc_url).await?, + provider, + }) + } +} + +impl Module { + async fn query_latest_attested_height(&self) -> RpcResult { + let req = QuerySmartContractStateRequest { + address: self.attestation_client_address.to_string(), + query_data: serde_json::to_vec(&attested_light_client::msg::QueryMsg::LatestHeight { + chain_id: self.chain_id.to_string(), + }) + .unwrap(), + }; + + let raw = self + .cometbft_client + .grpc_abci_query::<_, QuerySmartContractStateResponse>( + "/cosmwasm.wasm.v1.Query/SmartContractState", + &req, + None, + false, + ) + .await + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching latest attested height"), + None::<()>, + ) + })? + .into_result() + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching latest attested height"), + None::<()>, + ) + })? + .unwrap() + .data; + + Ok(serde_json::from_slice::>(&raw) + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching latest attested height"), + None::<()>, + ) + })? + .unwrap()) + } +} + +#[async_trait] +impl FinalityModuleServer for Module { + /// Query the latest finalized height of this chain. + #[instrument(skip_all, fields(chain_id = %self.chain_id, finalized))] + async fn query_latest_height(&self, _: &Extensions, finalized: bool) -> RpcResult { + if finalized { + Ok(Height::new( + self.query_latest_attested_height().await?.height, + )) + } else { + self.provider + .get_block_number() + .await + .map(Height::new) + .map_err(|err| ErrorObject::owned(-1, ErrorReporter(err).to_string(), None::<()>)) + } + } + + /// Query the latest finalized timestamp of this chain. + #[instrument(skip_all, fields(chain_id = %self.chain_id, finalized))] + async fn query_latest_timestamp( + &self, + _: &Extensions, + finalized: bool, + ) -> RpcResult { + if finalized { + Ok(self.query_latest_attested_height().await?.timestamp) + } else { + Ok(Timestamp::from_secs( + self.provider + .get_block(BlockNumberOrTag::Latest.into()) + .hashes() + .await + .map_err(|err| { + ErrorObject::owned(-1, ErrorReporter(err).to_string(), None::<()>) + })? + .ok_or_else(|| ErrorObject::owned(-1, "latest block not found", None::<()>))? + .header + .timestamp, + )) + } + } +} diff --git a/voyager/modules/proof/attested/Cargo.toml b/voyager/modules/proof/attested/Cargo.toml index 7724f7af81..5e6fa092b0 100644 --- a/voyager/modules/proof/attested/Cargo.toml +++ b/voyager/modules/proof/attested/Cargo.toml @@ -12,17 +12,18 @@ repository = { workspace = true } workspace = true [dependencies] -clap = { workspace = true, features = ["derive"] } -cometbft-rpc = { workspace = true } -embed-commit = { workspace = true } -ibc-union-spec = { workspace = true, features = ["serde"] } -jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } -prost = { workspace = true } -protos = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -unionlabs = { workspace = true } -voyager-sdk = { workspace = true } +attested-light-client = { workspace = true } +attested-light-client-types = { workspace = true, features = ["serde"] } +cometbft-rpc = { workspace = true } +embed-commit = { workspace = true } +ibc-union-spec = { workspace = true, features = ["serde"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +prost = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-sdk = { workspace = true } diff --git a/voyager/modules/proof/attested/src/main.rs b/voyager/modules/proof/attested/src/main.rs index 79dc8041c3..7f211ab434 100644 --- a/voyager/modules/proof/attested/src/main.rs +++ b/voyager/modules/proof/attested/src/main.rs @@ -1,31 +1,25 @@ -#![warn(clippy::unwrap_used)] - -use std::num::ParseIntError; - -use ibc_union_spec::{ - IbcUnion, - path::{IBC_UNION_COSMWASM_COMMITMENT_PREFIX, StorePath}, -}; +use attested_light_client::types::AttestationValue; +use attested_light_client_types::StorageProof; +use ibc_union_spec::{IbcUnion, path::StorePath}; use jsonrpsee::{ Extensions, core::{RpcResult, async_trait}, types::ErrorObject, }; +use protos::cosmwasm::wasm::v1::{QuerySmartContractStateRequest, QuerySmartContractStateResponse}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use tracing::{error, instrument, warn}; +use tracing::instrument; use unionlabs::{ ErrorReporter, - bounded::BoundedI64, - cosmos::ics23::commitment_proof::CommitmentProof, - ibc::core::{client::height::Height, commitment::merkle_proof::MerkleProof}, + ibc::core::client::height::Height, primitives::{Bech32, H256}, }; use voyager_sdk::{ anyhow, into_value, plugin::ProofModule, primitives::ChainId, - rpc::{FATAL_JSONRPC_ERROR_CODE, ProofModuleServer, rpc_error, types::ProofModuleInfo}, + rpc::{ProofModuleServer, types::ProofModuleInfo}, types::ProofType, }; @@ -34,76 +28,35 @@ async fn main() { >::run().await; } -#[derive(clap::Subcommand)] -pub enum Cmd { - ChainId, - LatestHeight, -} - #[derive(Debug, Clone)] pub struct Module { pub chain_id: ChainId, - pub chain_revision: u64, - - pub cometbft_client: cometbft_rpc::Client, - pub attestation_client_address: Bech32, + pub cometbft_client: cometbft_rpc::Client, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { - pub rpc_url: String, + pub chain_id: ChainId, pub attestation_client_address: Bech32, + pub rpc_url: String, } impl ProofModule for Module { type Config = Config; async fn new(config: Self::Config, info: ProofModuleInfo) -> anyhow::Result { - let tm_client = cometbft_rpc::Client::new(config.rpc_url).await?; - - let chain_id = tm_client.status().await?.node_info.network; - - info.ensure_chain_id(&chain_id)?; - - let chain_revision = chain_id - .split('-') - .next_back() - .ok_or_else(|| ChainIdParseError { - found: chain_id.clone(), - source: None, - })? - .parse() - .map_err(|err| ChainIdParseError { - found: chain_id.clone(), - source: Some(err), - })?; + info.ensure_chain_id(config.chain_id.as_str())?; Ok(Self { - cometbft_client: tm_client, - chain_id: ChainId::new(chain_id), - chain_revision, + chain_id: config.chain_id, attestation_client_address: config.attestation_client_address, + cometbft_client: cometbft_rpc::Client::new(config.rpc_url).await?, }) } } -impl Module { - #[must_use] - pub fn make_height(&self, height: u64) -> Height { - Height::new_with_revision(self.chain_revision, height) - } -} - -#[derive(Debug, thiserror::Error)] -#[error("unable to parse chain id: expected format `-`, found `{found}`")] -pub struct ChainIdParseError { - found: String, - #[source] - source: Option, -} - #[async_trait] impl ProofModuleServer for Module { #[instrument(skip_all, fields(chain_id = %self.chain_id))] @@ -113,77 +66,60 @@ impl ProofModuleServer for Module { at: Height, path: StorePath, ) -> RpcResult> { - // TODO: Extract this into a function somewhere, reuse in lightclients - let data = [0x03] - .into_iter() - .chain(*self.ibc_host_contract_address.data()) - .chain(IBC_UNION_COSMWASM_COMMITMENT_PREFIX) - .chain(path.key()) - .collect::>(); + let req = QuerySmartContractStateRequest { + address: self.attestation_client_address.to_string(), + query_data: serde_json::to_vec(&attested_light_client::msg::QueryMsg::AttestedValue { + chain_id: self.chain_id.to_string(), + height: at.height(), + key: path.key().into(), + }) + .unwrap(), + }; - let query_result = self + let raw = self .cometbft_client - .abci_query( - "store/wasm/key", - data, - // THIS -1 IS VERY IMPORTANT!!! - // - // a proof at height H is provable at height H + 1 - // we assume that the height passed in to this function is the intended height to prove against, thus we have to query the height - 1 - Some(BoundedI64::new(at.height() - 1).map_err(|e| { - let message = format!("invalid height value: {}", ErrorReporter(e)); - error!(%message); - ErrorObject::owned( - FATAL_JSONRPC_ERROR_CODE, - message, - Some(json!({ "height": at })), - ) - })?), - true, + .grpc_abci_query::<_, QuerySmartContractStateResponse>( + "/cosmwasm.wasm.v1.Query/SmartContractState", + &req, + None, + false, ) .await - .map_err(rpc_error("error querying ibc proof", None))?; - - // if this field is none, the proof is not available at this height - let Some(proofs) = query_result.response.proof_ops else { - return Ok(None); - }; - - let proofs = proofs - .ops - .into_iter() - .map(|op| { - ::decode(&*op.data) - .map_err(|e| { - ErrorObject::owned( - FATAL_JSONRPC_ERROR_CODE, - format!("invalid height value: {}", ErrorReporter(e)), - Some(json!({ "height": at })), - ) - }) - }) - .collect::, _>>()?; - - let proof = - MerkleProof::try_from(protos::ibc::core::commitment::v1::MerkleProof { proofs }) - .map_err(|e| { - ErrorObject::owned( - FATAL_JSONRPC_ERROR_CODE, - format!("invalid merkle proof value: {}", ErrorReporter(e)), - Some(json!({ "height": at })), - ) - })?; - - let proof_type = if proof - .proofs - .iter() - .any(|p| matches!(&p, CommitmentProof::Nonexist(_))) - { - ProofType::NonMembership - } else { - ProofType::Membership - }; - - Ok(Some((into_value(proof), proof_type))) + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching attested value"), + Some(json!({ + "chain_id": self.chain_id.to_string(), + "height": at.height(), + "key": path.key() + })), + ) + })? + .into_result() + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching attested value"), + Some(json!({ + "chain_id": self.chain_id.to_string(), + "height": at.height(), + "key": path.key() + })), + ) + })? + .unwrap() + .data; + + Ok( + match serde_json::from_slice::(&raw).unwrap() { + AttestationValue::NonExistence => { + Some((into_value(StorageProof {}), ProofType::Membership)) + } + AttestationValue::Existence(_) => { + Some((into_value(StorageProof {}), ProofType::NonMembership)) + } + }, + ) } } diff --git a/voyager/plugins/attestor/evm/src/main.rs b/voyager/plugins/attestor/evm/src/main.rs index 78cbd0062d..5de90c40f7 100644 --- a/voyager/plugins/attestor/evm/src/main.rs +++ b/voyager/plugins/attestor/evm/src/main.rs @@ -436,6 +436,7 @@ impl Module { let tx_client = TxClient::new(signer, &self.cosmos_client, &self.gas_config); let attestation = Attestation { + chain_id: self.chain_id.clone().to_string(), height, timestamp: Timestamp::from_secs(timestamp), key: key.into(), From adfef1c08c91af076b4aef3ad61376879338824e Mon Sep 17 00:00:00 2001 From: benluelo Date: Tue, 11 Nov 2025 09:49:32 +0000 Subject: [PATCH 09/12] wip --- Cargo.lock | 21 ++ Cargo.toml | 1 + .../lightclient/attested/src/state.rs | 4 +- lib/voyager-primitives/src/lib.rs | 3 + voyager/plugins/attestor/evm/src/main.rs | 2 +- .../plugins/client-update/attested/Cargo.toml | 29 +++ .../client-update/attested/src/main.rs | 226 ++++++++++++++++++ 7 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 voyager/plugins/client-update/attested/Cargo.toml create mode 100644 voyager/plugins/client-update/attested/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 1c7e7b0d30..71ac1b8819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16443,6 +16443,27 @@ dependencies = [ "voyager-sdk", ] +[[package]] +name = "voyager-client-update-plugin-attested" +version = "0.0.0" +dependencies = [ + "attested-light-client", + "attested-light-client-types", + "cometbft-rpc", + "embed-commit", + "ibc-union-spec", + "jsonrpsee 0.25.1", + "prost 0.12.6", + "protos", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "unionlabs", + "voyager-sdk", +] + [[package]] name = "voyager-client-update-plugin-base" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 852206adca..5a1127beb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -207,6 +207,7 @@ members = [ "voyager/plugins/client-update/state-lens", "voyager/plugins/client-update/sui", "voyager/plugins/client-update/trusted-mpt", + "voyager/plugins/client-update/attested", "voyager/plugins/periodic-client-update", diff --git a/cosmwasm/ibc-union/lightclient/attested/src/state.rs b/cosmwasm/ibc-union/lightclient/attested/src/state.rs index 92226640db..27e5af88c7 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/state.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/state.rs @@ -96,10 +96,10 @@ impl KeyCodec<(String, u64)> for HeightTimestamps { fn decode_key(raw: &Bytes) -> StdResult<(String, u64)> { if raw.len() < 8 { - return Err(StdError::generic_err(format!( + Err(StdError::generic_err(format!( "invalid key: expected at least 8 bytes, found {} (raw: {raw})", raw.len() - ))); + ))) } else { let height = raw[raw.len() - 8..] .try_into() diff --git a/lib/voyager-primitives/src/lib.rs b/lib/voyager-primitives/src/lib.rs index 5d87b7c632..e0bcf82d95 100644 --- a/lib/voyager-primitives/src/lib.rs +++ b/lib/voyager-primitives/src/lib.rs @@ -178,6 +178,9 @@ impl ClientType { /// [L2 settlement]: https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock pub const OPTIMISM: &'static str = "optimism"; + /// An attested client. + pub const ATTESTED: &'static str = "attested"; + // lots more to come - near, linea, polygon - stay tuned } diff --git a/voyager/plugins/attestor/evm/src/main.rs b/voyager/plugins/attestor/evm/src/main.rs index 5de90c40f7..7f5e2d57ec 100644 --- a/voyager/plugins/attestor/evm/src/main.rs +++ b/voyager/plugins/attestor/evm/src/main.rs @@ -436,7 +436,7 @@ impl Module { let tx_client = TxClient::new(signer, &self.cosmos_client, &self.gas_config); let attestation = Attestation { - chain_id: self.chain_id.clone().to_string(), + chain_id: self.chain_id.to_string(), height, timestamp: Timestamp::from_secs(timestamp), key: key.into(), diff --git a/voyager/plugins/client-update/attested/Cargo.toml b/voyager/plugins/client-update/attested/Cargo.toml new file mode 100644 index 0000000000..29bb3c61c9 --- /dev/null +++ b/voyager/plugins/client-update/attested/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "voyager-client-update-plugin-attested" +version = "0.0.0" + +authors = { workspace = true } +edition = { workspace = true } +license-file = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } + +[lints] +workspace = true + +[dependencies] +attested-light-client = { workspace = true } +attested-light-client-types = { workspace = true, features = ["serde"] } +cometbft-rpc = { workspace = true } +embed-commit = { workspace = true } +ibc-union-spec = { workspace = true, features = ["serde"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +prost = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-sdk = { workspace = true } diff --git a/voyager/plugins/client-update/attested/src/main.rs b/voyager/plugins/client-update/attested/src/main.rs new file mode 100644 index 0000000000..51cdc21529 --- /dev/null +++ b/voyager/plugins/client-update/attested/src/main.rs @@ -0,0 +1,226 @@ +use std::collections::VecDeque; + +use attested_light_client::msg::QueryMsg; +use attested_light_client_types::Header; +use ibc_union_spec::Timestamp; +use jsonrpsee::{ + Extensions, + core::{RpcResult, async_trait}, + types::ErrorObject, +}; +use protos::cosmwasm::wasm::v1::{QuerySmartContractStateRequest, QuerySmartContractStateResponse}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; +use unionlabs::{ + ErrorReporter, + ibc::core::client::height::Height, + never::Never, + primitives::{Bech32, H256}, +}; +use voyager_sdk::{ + DefaultCmd, anyhow, + hook::UpdateHook, + into_value, + message::{ + PluginMessage, VoyagerMessage, + call::Call, + data::{Data, DecodedHeaderMeta, OrderedHeaders}, + }, + plugin::Plugin, + primitives::{ChainId, ClientType}, + rpc::{FATAL_JSONRPC_ERROR_CODE, PluginServer, types::PluginInfo}, + vm::{Op, Visit, data, pass::PassResult}, +}; + +use crate::call::{FetchUpdate, ModuleCall}; + +pub mod call { + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub enum ModuleCall { + FetchUpdate(FetchUpdate), + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct FetchUpdate { + pub to: u64, + } +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await; +} + +#[derive(Debug, Clone)] +pub struct Module { + pub chain_id: ChainId, + pub attestation_client_address: Bech32, + pub cometbft_client: cometbft_rpc::Client, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub chain_id: ChainId, + pub attestation_client_address: Bech32, + pub rpc_url: String, +} + +impl Plugin for Module { + type Call = ModuleCall; + type Callback = Never; + + type Config = Config; + type Cmd = DefaultCmd; + + async fn new(config: Self::Config) -> anyhow::Result { + Ok(Self { + chain_id: config.chain_id, + attestation_client_address: config.attestation_client_address, + cometbft_client: cometbft_rpc::Client::new(config.rpc_url).await?, + }) + } + + fn info(config: Self::Config) -> PluginInfo { + PluginInfo { + name: plugin_name(&config.chain_id), + interest_filter: UpdateHook::filter( + &config.chain_id, + &ClientType::new(ClientType::ATTESTED), + ), + } + } + + async fn cmd(_: Self::Config, cmd: Self::Cmd) { + match cmd {} + } +} + +#[async_trait] +impl PluginServer for Module { + #[instrument(skip_all, fields(chain_id = %self.chain_id))] + async fn run_pass( + &self, + _: &Extensions, + msgs: Vec>, + ) -> RpcResult> { + Ok(PassResult { + optimize_further: vec![], + ready: msgs + .into_iter() + .map(|mut op| { + UpdateHook::new( + &self.chain_id, + &ClientType::new(ClientType::ATTESTED), + |fetch| { + Call::Plugin(PluginMessage::new( + self.plugin_name(), + ModuleCall::FetchUpdate(FetchUpdate { + to: fetch.update_to.height(), + }), + )) + }, + ) + .visit_op(&mut op); + + op + }) + .enumerate() + .map(|(i, op)| (vec![i], op)) + .collect(), + }) + } + + #[instrument(skip_all, fields(chain_id = %self.chain_id))] + async fn call(&self, _: &Extensions, msg: ModuleCall) -> RpcResult> { + match msg { + ModuleCall::FetchUpdate(FetchUpdate { to }) => { + let timestamp = self.query_attested_timestamp_at_height(to).await?; + + Ok(data(OrderedHeaders { + headers: vec![( + DecodedHeaderMeta { + height: Height::new(to), + }, + into_value(Header { + height: to, + timestamp, + }), + )], + })) + } + } + } + + #[instrument(skip_all, fields(chain_id = %self.chain_id))] + async fn callback( + &self, + _: &Extensions, + cb: Never, + _: VecDeque, + ) -> RpcResult> { + match cb {} + } +} + +fn plugin_name(chain_id: &ChainId) -> String { + pub const PLUGIN_NAME: &str = env!("CARGO_PKG_NAME"); + + format!("{PLUGIN_NAME}/{}", chain_id) +} + +impl Module { + fn plugin_name(&self) -> String { + plugin_name(&self.chain_id) + } + + async fn query_attested_timestamp_at_height(&self, height: u64) -> RpcResult { + let req = QuerySmartContractStateRequest { + address: self.attestation_client_address.to_string(), + query_data: serde_json::to_vec(&QueryMsg::TimestampAtHeight { + chain_id: self.chain_id.to_string(), + height, + }) + .unwrap(), + }; + + let raw = self + .cometbft_client + .grpc_abci_query::<_, QuerySmartContractStateResponse>( + "/cosmwasm.wasm.v1.Query/SmartContractState", + &req, + None, + false, + ) + .await + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching attested timestamp at height"), + None::<()>, + ) + })? + .into_result() + .map_err(|e| { + ErrorObject::owned( + -1, + ErrorReporter(e).with_message("error fetching attested timestamp at height"), + None::<()>, + ) + })? + .unwrap() + .data; + + Ok(serde_json::from_slice::>(&raw) + .map_err(|e| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + ErrorReporter(e).with_message("height {height} has not been attested to"), + None::<()>, + ) + })? + .unwrap()) + } +} From 7306b503a3f24ea79908072d4df4c4f77f854cff Mon Sep 17 00:00:00 2001 From: benluelo Date: Tue, 11 Nov 2025 12:08:47 +0000 Subject: [PATCH 10/12] wip --- Cargo.lock | 36 ++++ Cargo.toml | 2 + lib/voyager-plugin/src/lib.rs | 6 +- .../client-bootstrap/attested/Cargo.toml | 29 +++ .../client-bootstrap/attested/src/main.rs | 101 ++++++++++ voyager/modules/client/attested/Cargo.toml | 23 +++ voyager/modules/client/attested/src/main.rs | 189 ++++++++++++++++++ .../modules/finality/attested-evm/src/main.rs | 7 +- voyager/plugins/attestor/evm/src/main.rs | 6 +- 9 files changed, 390 insertions(+), 9 deletions(-) create mode 100644 voyager/modules/client-bootstrap/attested/Cargo.toml create mode 100644 voyager/modules/client-bootstrap/attested/src/main.rs create mode 100644 voyager/modules/client/attested/Cargo.toml create mode 100644 voyager/modules/client/attested/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 71ac1b8819..674c76d133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15946,6 +15946,27 @@ dependencies = [ "voyager-sdk", ] +[[package]] +name = "voyager-client-bootstrap-module-attested" +version = "0.0.0" +dependencies = [ + "alloy", + "attested-light-client", + "attested-light-client-types", + "embed-commit", + "ibc-union-spec", + "jsonrpsee 0.25.1", + "prost 0.12.6", + "protos", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "unionlabs", + "voyager-sdk", +] + [[package]] name = "voyager-client-bootstrap-module-base" version = "0.0.0" @@ -16197,6 +16218,21 @@ dependencies = [ "voyager-sdk", ] +[[package]] +name = "voyager-client-module-attested" +version = "0.0.0" +dependencies = [ + "attested-light-client-types", + "embed-commit", + "jsonrpsee 0.25.1", + "serde", + "serde_json", + "tokio", + "tracing", + "unionlabs", + "voyager-sdk", +] + [[package]] name = "voyager-client-module-base" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5a1127beb0..5218a6db67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,6 +165,7 @@ members = [ "voyager/modules/client/state-lens/ics23-smt", "voyager/modules/client/sui", "voyager/modules/client/trusted-mpt", + "voyager/modules/client/attested", "voyager/modules/client-bootstrap/base", "voyager/modules/client-bootstrap/bob", @@ -180,6 +181,7 @@ members = [ "voyager/modules/client-bootstrap/state-lens/ics23-smt", "voyager/modules/client-bootstrap/state-lens/ics23-ics23", "voyager/modules/client-bootstrap/sui", + "voyager/modules/client-bootstrap/attested", "voyager/modules/finality/base", "voyager/modules/finality/bob", diff --git a/lib/voyager-plugin/src/lib.rs b/lib/voyager-plugin/src/lib.rs index ca460c8c77..7f30d49f91 100644 --- a/lib/voyager-plugin/src/lib.rs +++ b/lib/voyager-plugin/src/lib.rs @@ -3,7 +3,7 @@ use std::{env::VarError, time::Duration}; use opentelemetry::KeyValue; use opentelemetry_otlp::WithExportConfig; use serde::de::DeserializeOwned; -use tracing::{Instrument, debug_span, instrument}; +use tracing::{Instrument, debug_span, error, instrument}; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; use unionlabs::ErrorReporter; pub use voyager_plugin_protocol as protocol; @@ -344,12 +344,12 @@ fn init(metrics_endpoint: Option) { } } -#[instrument(level = "debug", fields(%config_str))] +#[instrument(level = "info", fields(%config_str, current_exe = ?std::env::current_exe().unwrap_or_default()))] fn must_parse(config_str: &str) -> T { match serde_json::from_str::(config_str) { Ok(ok) => ok, Err(err) => { - eprintln!("invalid config: {}", ErrorReporter(err)); + error!("invalid config: {}", ErrorReporter(err)); std::process::exit(INVALID_CONFIG_EXIT_CODE as i32); } } diff --git a/voyager/modules/client-bootstrap/attested/Cargo.toml b/voyager/modules/client-bootstrap/attested/Cargo.toml new file mode 100644 index 0000000000..190fd66ab2 --- /dev/null +++ b/voyager/modules/client-bootstrap/attested/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "voyager-client-bootstrap-module-attested" +version = "0.0.0" + +authors = { workspace = true } +edition = { workspace = true } +license-file = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } + +[lints] +workspace = true + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "reqwest-rustls-tls", "provider-ws"] } +attested-light-client = { workspace = true } +attested-light-client-types = { workspace = true, features = ["serde"] } +embed-commit = { workspace = true } +ibc-union-spec = { workspace = true, features = ["serde"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +prost = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-sdk = { workspace = true } diff --git a/voyager/modules/client-bootstrap/attested/src/main.rs b/voyager/modules/client-bootstrap/attested/src/main.rs new file mode 100644 index 0000000000..3f8b3f5b6b --- /dev/null +++ b/voyager/modules/client-bootstrap/attested/src/main.rs @@ -0,0 +1,101 @@ +use alloy::{ + network::AnyNetwork, + providers::{DynProvider, Provider, ProviderBuilder}, +}; +use attested_light_client_types::{ClientState, ClientStateV1, ConsensusState}; +use ibc_union_spec::Timestamp; +use jsonrpsee::{ + Extensions, + core::{RpcResult, async_trait}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::instrument; +use unionlabs::ibc::core::client::height::Height; +use voyager_sdk::{ + anyhow, ensure_null, into_value, + plugin::ClientBootstrapModule, + primitives::ChainId, + rpc::{ClientBootstrapModuleServer, types::ClientBootstrapModuleInfo}, +}; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await; +} + +#[derive(Debug, Clone)] +pub struct Module { + pub chain_id: ChainId, + pub provider: DynProvider, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub rpc_url: String, +} + +impl ClientBootstrapModule for Module { + type Config = Config; + + async fn new(config: Self::Config, info: ClientBootstrapModuleInfo) -> anyhow::Result { + let provider = DynProvider::new( + ProviderBuilder::new() + .network::() + .connect(&config.rpc_url) + .await?, + ); + + let chain_id = provider.get_chain_id().await?.to_string(); + + info.ensure_chain_id(&chain_id)?; + + Ok(Self { + chain_id: ChainId::new(chain_id), + provider, + }) + } +} + +#[async_trait] +impl ClientBootstrapModuleServer for Module { + #[instrument(skip_all, fields(chain_id = %self.chain_id, %height))] + async fn self_client_state( + &self, + _: &Extensions, + height: Height, + config: Value, + ) -> RpcResult { + ensure_null(config)?; + + Ok(serde_json::to_value(ClientState::V1(ClientStateV1 { + chain_id: self.chain_id.to_string(), + latest_height: height.height(), + })) + .expect("infallible")) + } + + #[instrument(skip_all, fields(chain_id = %self.chain_id, %height))] + async fn self_consensus_state( + &self, + _: &Extensions, + height: Height, + config: Value, + ) -> RpcResult { + ensure_null(config)?; + + let timestamp = self + .provider + .get_block_by_number(height.height().into()) + .await + .unwrap() + .unwrap() + .header + .timestamp; + + Ok(into_value(ConsensusState { + timestamp: Timestamp::from_secs(timestamp), + })) + } +} diff --git a/voyager/modules/client/attested/Cargo.toml b/voyager/modules/client/attested/Cargo.toml new file mode 100644 index 0000000000..ca75c6bda2 --- /dev/null +++ b/voyager/modules/client/attested/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "voyager-client-module-attested" +version = "0.0.0" + +authors = { workspace = true } +edition = { workspace = true } +license-file = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } + +[lints] +workspace = true + +[dependencies] +attested-light-client-types = { workspace = true, features = ["serde", "bincode", "ethabi"] } +embed-commit = { workspace = true } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true, features = ["bincode"] } +voyager-sdk = { workspace = true } diff --git a/voyager/modules/client/attested/src/main.rs b/voyager/modules/client/attested/src/main.rs new file mode 100644 index 0000000000..95d0ad63c7 --- /dev/null +++ b/voyager/modules/client/attested/src/main.rs @@ -0,0 +1,189 @@ +use attested_light_client_types::{ClientState, ConsensusState, Header, StorageProof}; +use jsonrpsee::{ + Extensions, + core::{RpcResult, async_trait}, + types::ErrorObject, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::instrument; +use unionlabs::{ + self, ErrorReporter, + encoding::{Bincode, DecodeAs, EncodeAs, EthAbi}, + ibc::core::client::height::Height, + primitives::Bytes, +}; +use voyager_sdk::{ + anyhow, ensure_null, into_value, + plugin::ClientModule, + primitives::{ + ChainId, ClientStateMeta, ClientType, ConsensusStateMeta, ConsensusType, IbcInterface, + }, + rpc::{ClientModuleServer, FATAL_JSONRPC_ERROR_CODE, types::ClientModuleInfo}, +}; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config {} + +impl ClientModule for Module { + type Config = Config; + + async fn new(Config {}: Self::Config, info: ClientModuleInfo) -> anyhow::Result { + info.ensure_client_type(ClientType::ATTESTED)?; + info.ensure_consensus_type(ConsensusType::ATTESTED)?; + info.ensure_ibc_interface(IbcInterface::IBC_COSMWASM)?; + + Ok(Self) + } +} + +impl Module { + pub fn decode_consensus_state(consensus_state: &[u8]) -> RpcResult { + ConsensusState::decode_as::(consensus_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode consensus state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + } + + pub fn decode_client_state(client_state: &[u8]) -> RpcResult { + ClientState::decode_as::(client_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode client state: {err}"), + None::<()>, + ) + }) + } + + pub fn make_height(revision_height: u64) -> Height { + Height::new(revision_height) + } +} + +#[async_trait] +impl ClientModuleServer for Module { + #[instrument] + async fn decode_client_state_meta( + &self, + _: &Extensions, + client_state: Bytes, + ) -> RpcResult { + match Module::decode_client_state(&client_state)? { + ClientState::V1(v1) => Ok(ClientStateMeta { + counterparty_chain_id: ChainId::new(v1.chain_id.to_string()), + counterparty_height: Module::make_height(v1.latest_height), + }), + } + } + + #[instrument] + async fn decode_consensus_state_meta( + &self, + _: &Extensions, + consensus_state: Bytes, + ) -> RpcResult { + let cs = Module::decode_consensus_state(&consensus_state)?; + + Ok(ConsensusStateMeta { + timestamp: cs.timestamp, + }) + } + + #[instrument] + async fn decode_client_state(&self, _: &Extensions, client_state: Bytes) -> RpcResult { + Ok(into_value(Module::decode_client_state(&client_state)?)) + } + + #[instrument] + async fn decode_consensus_state( + &self, + _: &Extensions, + consensus_state: Bytes, + ) -> RpcResult { + Ok(into_value(Module::decode_consensus_state( + &consensus_state, + )?)) + } + + #[instrument] + async fn encode_client_state( + &self, + _: &Extensions, + client_state: Value, + metadata: Value, + ) -> RpcResult { + ensure_null(metadata)?; + + serde_json::from_value::(client_state) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize client state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|cs| cs.encode_as::()) + .map(Into::into) + } + + #[instrument] + async fn encode_consensus_state( + &self, + _: &Extensions, + consensus_state: Value, + ) -> RpcResult { + serde_json::from_value::(consensus_state) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!( + "unable to deserialize consensus state: {}", + ErrorReporter(err) + ), + None::<()>, + ) + }) + .map(|cs| cs.encode_as::()) + .map(Into::into) + } + + #[instrument] + async fn encode_header(&self, _: &Extensions, header: Value) -> RpcResult { + serde_json::from_value::
(header) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize header: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|header| header.encode_as::()) + .map(Into::into) + } + + #[instrument] + async fn encode_proof(&self, _: &Extensions, proof: Value) -> RpcResult { + serde_json::from_value::(proof) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize proof: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|storage_proof| storage_proof.encode_as::()) + .map(Into::into) + } +} diff --git a/voyager/modules/finality/attested-evm/src/main.rs b/voyager/modules/finality/attested-evm/src/main.rs index 870e6899ed..c99e382889 100644 --- a/voyager/modules/finality/attested-evm/src/main.rs +++ b/voyager/modules/finality/attested-evm/src/main.rs @@ -3,8 +3,8 @@ use alloy::{ network::AnyNetwork, providers::{DynProvider, Provider, ProviderBuilder}, }; -use attested_light_client::{contract::query::LatestHeight, types::AttestationValue}; -use ibc_union_spec::{IbcUnion, Timestamp, path::StorePath}; +use attested_light_client::contract::query::LatestHeight; +use ibc_union_spec::Timestamp; use jsonrpsee::{ Extensions, core::{RpcResult, async_trait}, @@ -12,7 +12,6 @@ use jsonrpsee::{ }; use protos::cosmwasm::wasm::v1::{QuerySmartContractStateRequest, QuerySmartContractStateResponse}; use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; use tracing::instrument; use unionlabs::{ ErrorReporter, @@ -20,7 +19,7 @@ use unionlabs::{ primitives::{Bech32, H256}, }; use voyager_sdk::{ - anyhow, into_value, + anyhow, plugin::FinalityModule, primitives::{ChainId, ConsensusType}, rpc::{FinalityModuleServer, types::FinalityModuleInfo}, diff --git a/voyager/plugins/attestor/evm/src/main.rs b/voyager/plugins/attestor/evm/src/main.rs index 7f5e2d57ec..5d12bd0b1e 100644 --- a/voyager/plugins/attestor/evm/src/main.rs +++ b/voyager/plugins/attestor/evm/src/main.rs @@ -2,7 +2,6 @@ use std::{collections::VecDeque, ops::Deref, panic::AssertUnwindSafe, path::Path use alloy::{ network::AnyNetwork, - primitives::U256, providers::{DynProvider, Provider, ProviderBuilder}, }; use attested_light_client::types::{Attestation, AttestationValue}; @@ -35,6 +34,7 @@ use unionlabs::{ ErrorReporter, cosmwasm::wasm::msg_execute_contract::MsgExecuteContract, encoding::{Bincode, EncodeAs}, + ethereum::ibc_commitment_key, never::Never, primitives::{Bech32, H160, H256}, }; @@ -203,6 +203,8 @@ impl Plugin for Module { let attestation_key = SigningKey::read_pkcs8_pem_file(config.attestation_key_path)?; + info!(attestation_key = %::new(attestation_key.verifying_key().to_bytes())); + let bech32_prefix = rpc .client() .grpc_abci_query::<_, protos::cosmos::auth::v1beta1::Bech32PrefixResponse>( @@ -377,7 +379,7 @@ impl Module { .provider .get_storage_at( self.ibc_handler_address.get().into(), - U256::from_be_bytes(*key.get()), + ibc_commitment_key(key).into(), ) .block_id(height.into()) .await From 8c0933633132d2f9d673cce13a82f0cb2a5a6fec Mon Sep 17 00:00:00 2001 From: benluelo Date: Wed, 19 Nov 2025 15:19:30 +0000 Subject: [PATCH 11/12] wip --- .../lightclient/attested/src/contract.rs | 44 +++++++++++-------- .../ibc-union/lightclient/attested/src/msg.rs | 2 + 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs index 6b764a976e..1836dc206e 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs @@ -1,8 +1,7 @@ use cosmwasm_std::{ Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, entry_point, to_json_binary, }; -use frissitheto::UpgradeMsg; -use ibc_union_light_client::{IbcClientError, default_migrate, default_reply, msg::QueryMsg}; +use ibc_union_light_client::{access_managed::state::Authority, default_migrate, default_reply}; use serde::{Deserialize, Serialize}; use crate::{ @@ -24,8 +23,8 @@ pub mod query; #[entry_point] pub fn execute( deps: DepsMut, - _env: Env, - _info: MessageInfo, + env: Env, + info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { @@ -35,20 +34,29 @@ pub fn execute( signature, } => attest(deps, attestation, attestor, signature), ExecuteMsg::ConfirmAttestation { attestation } => confirm_attestation(deps, attestation), - ExecuteMsg::Restricted(msg) => match msg { - RestrictedExecuteMsg::SetQuorum { - chain_id, - new_quorum, - } => set_quorum(deps, chain_id, new_quorum), - RestrictedExecuteMsg::AddAttestor { - chain_id, - new_attestor, - } => add_attestor(deps, chain_id, new_attestor), - RestrictedExecuteMsg::RemoveAttestor { - chain_id, - old_attestor, - } => remove_attestor(deps, chain_id, old_attestor), - }, + ExecuteMsg::Restricted(msg) => { + let msg = match msg.ensure_can_call::(deps.branch(), &env, &info)? { + EnsureCanCallResult::Msg(msg) => msg, + EnsureCanCallResult::Scheduled(sub_msgs) => { + return Ok(Response::new().add_submessages(sub_msgs)); + } + }; + + match msg { + RestrictedExecuteMsg::SetQuorum { + chain_id, + new_quorum, + } => set_quorum(deps, chain_id, new_quorum), + RestrictedExecuteMsg::AddAttestor { + chain_id, + new_attestor, + } => add_attestor(deps, chain_id, new_attestor), + RestrictedExecuteMsg::RemoveAttestor { + chain_id, + old_attestor, + } => remove_attestor(deps, chain_id, old_attestor), + } + } } } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs index 037cab8a24..d7e4d6ed1b 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs @@ -6,6 +6,8 @@ use unionlabs::primitives::{Bytes, H256, H512}; use crate::types::Attestation; +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] pub enum ExecuteMsg { /// Attest to a key/value state. /// From e7f774481a59ff5708c3029cf775a8982d3f65f0 Mon Sep 17 00:00:00 2001 From: benluelo Date: Wed, 19 Nov 2025 15:51:42 +0000 Subject: [PATCH 12/12] wip --- Cargo.lock | 1 + .../ibc-union/lightclient/attested/Cargo.toml | 1 + .../lightclient/attested/src/contract.rs | 24 ++++---- .../lightclient/attested/src/errors.rs | 5 +- .../attested/src/{contract => }/execute.rs | 0 .../ibc-union/lightclient/attested/src/lib.rs | 2 + .../ibc-union/lightclient/attested/src/msg.rs | 2 + .../attested/src/{contract => }/query.rs | 0 .../lightclient/attested/src/tests.rs | 58 +++++++++++-------- .../modules/finality/attested-evm/src/main.rs | 2 +- 10 files changed, 59 insertions(+), 36 deletions(-) rename cosmwasm/ibc-union/lightclient/attested/src/{contract => }/execute.rs (100%) rename cosmwasm/ibc-union/lightclient/attested/src/{contract => }/query.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 674c76d133..a723e90e0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1879,6 +1879,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" name = "attested-light-client" version = "0.0.0" dependencies = [ + "access-manager-types", "attested-light-client-types", "base64 0.22.1", "bincode 2.0.1", diff --git a/cosmwasm/ibc-union/lightclient/attested/Cargo.toml b/cosmwasm/ibc-union/lightclient/attested/Cargo.toml index a46f83ea23..22e66fad99 100644 --- a/cosmwasm/ibc-union/lightclient/attested/Cargo.toml +++ b/cosmwasm/ibc-union/lightclient/attested/Cargo.toml @@ -15,6 +15,7 @@ workspace = true crate-type = ["cdylib", "rlib"] [dependencies] +access-manager-types = { workspace = true } attested-light-client-types = { workspace = true, features = ["serde", "ethabi", "bincode"] } bincode = { workspace = true, features = ["derive"] } cosmwasm-std = { workspace = true, features = ["abort"] } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs index 1836dc206e..99bdc1a02e 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/contract.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/contract.rs @@ -1,28 +1,26 @@ use cosmwasm_std::{ Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, entry_point, to_json_binary, }; -use ibc_union_light_client::{access_managed::state::Authority, default_migrate, default_reply}; -use serde::{Deserialize, Serialize}; +use ibc_union_light_client::{ + access_managed::{EnsureCanCallResult, state::Authority}, + default_migrate, default_reply, +}; +use unionlabs::ErrorReporter; use crate::{ client::AttestedLightClient, - contract::{ - execute::{add_attestor, attest, confirm_attestation, remove_attestor, set_quorum}, - query::{attested_value, attestors, latest_height, quorum, timestamp_at_height}, - }, errors::Error, + execute::{add_attestor, attest, confirm_attestation, remove_attestor, set_quorum}, msg::{ExecuteMsg, QueryMsg, RestrictedExecuteMsg}, + query::{attested_value, attestors, latest_height, quorum, timestamp_at_height}, }; default_reply!(); -defualt_migrate!(); - -pub mod execute; -pub mod query; +default_migrate!(AttestedLightClient); #[entry_point] pub fn execute( - deps: DepsMut, + mut deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, @@ -57,6 +55,10 @@ pub fn execute( } => remove_attestor(deps, chain_id, old_attestor), } } + ExecuteMsg::LightClient(msg) => { + ibc_union_light_client::execute::(deps, env, info, msg) + .map_err(|e| StdError::generic_err(ErrorReporter(e).to_string()).into()) + } } } diff --git a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs index f807e2f645..7301d1d038 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/errors.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/errors.rs @@ -1,7 +1,7 @@ use std::num::NonZero; use cosmwasm_std::StdError; -use ibc_union_light_client::{IbcClientError, spec::Timestamp}; +use ibc_union_light_client::{IbcClientError, access_managed, spec::Timestamp}; use unionlabs::primitives::{Bytes, H256}; use crate::{client::AttestedLightClient, types::AttestationValue}; @@ -11,6 +11,9 @@ pub enum Error { #[error(transparent)] Std(#[from] StdError), + #[error(transparent)] + AccessManaged(#[from] access_managed::error::ContractError), + #[error("no misbehaviour in an attested client")] NoMisbehaviourInAttestedClient, diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs b/cosmwasm/ibc-union/lightclient/attested/src/execute.rs similarity index 100% rename from cosmwasm/ibc-union/lightclient/attested/src/contract/execute.rs rename to cosmwasm/ibc-union/lightclient/attested/src/execute.rs diff --git a/cosmwasm/ibc-union/lightclient/attested/src/lib.rs b/cosmwasm/ibc-union/lightclient/attested/src/lib.rs index aa080f8aa5..6884afc6bd 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/lib.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/lib.rs @@ -2,7 +2,9 @@ pub mod client; #[cfg(any(test, not(feature = "library")))] pub mod contract; pub mod errors; +pub mod execute; pub mod msg; +pub mod query; pub mod state; pub mod types; diff --git a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs index d7e4d6ed1b..1d227aa73d 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/msg.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/msg.rs @@ -23,6 +23,8 @@ pub enum ExecuteMsg { ConfirmAttestation { attestation: Attestation }, #[serde(untagged)] Restricted(Restricted), + #[serde(untagged)] + LightClient(ibc_union_light_client::msg::ExecuteMsg), } #[derive(Debug, Serialize, Deserialize)] diff --git a/cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs b/cosmwasm/ibc-union/lightclient/attested/src/query.rs similarity index 100% rename from cosmwasm/ibc-union/lightclient/attested/src/contract/query.rs rename to cosmwasm/ibc-union/lightclient/attested/src/query.rs diff --git a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs index c50a37b3be..76388bc8ea 100644 --- a/cosmwasm/ibc-union/lightclient/attested/src/tests.rs +++ b/cosmwasm/ibc-union/lightclient/attested/src/tests.rs @@ -1,15 +1,18 @@ use std::{collections::BTreeSet, fmt::Debug, num::NonZero, sync::LazyLock}; +use access_manager_types::CanCall; use attested_light_client_types::{ClientState, ClientStateV1, ConsensusState, Header}; use cosmwasm_std::{ - Addr, Api, Deps, Env, Event, OwnedDeps, Response, from_json, + Addr, Api, ContractResult, Deps, Env, Event, OwnedDeps, Response, SystemResult, from_json, testing::{MockApi, MockQuerier, MockStorage, message_info, mock_dependencies, mock_env}, + to_json_binary, }; use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut}; use frissitheto::UpgradeMsg; use hex_literal::hex; use ibc_union_light_client::{ - StateUpdate, access_managed, + StateUpdate, + access_managed::{self, EnsureCanCallResult, Restricted}, msg::InitMsg, spec::{Duration, Timestamp}, }; @@ -21,9 +24,10 @@ use unionlabs::{ use crate::{ client::{verify_attestation, verify_header}, - contract::{execute, migrate, query, query::LatestHeight}, + contract::{execute, migrate, query}, errors::Error, msg::{ExecuteMsg, QueryMsg, RestrictedExecuteMsg}, + query::LatestHeight, types::{Attestation, AttestationValue}, }; @@ -103,15 +107,23 @@ fn setup() -> (OwnedDeps, Env) { ) .unwrap(); + deps.querier.update_wasm({ + move |_| { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&CanCall::Immediate {}).unwrap(), + )) + } + }); + assert_eq!( execute( deps.as_mut(), env.clone(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::SetQuorum { chain_id: CHAIN_ID.to_owned(), new_quorum: const { NonZero::new(2).unwrap() }, - }), + })), ) .unwrap(), Response::new().add_event( @@ -136,10 +148,10 @@ fn setup() -> (OwnedDeps, Env) { deps.as_mut(), env.clone(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::AddAttestor { chain_id: CHAIN_ID.to_owned(), new_attestor: vk(attestor) - }), + })), ) .unwrap(), Response::new().add_event( @@ -611,10 +623,10 @@ fn add_attestor() { deps.as_mut(), mock_env(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::AddAttestor { chain_id: CHAIN_ID.to_owned(), new_attestor: vk(&ATTESTOR_3) - }), + })), ) .unwrap_err(), Error::AttestorAlreadyExists { @@ -628,10 +640,10 @@ fn add_attestor() { deps.as_mut(), mock_env(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::AddAttestor { chain_id: CHAIN_ID.to_owned(), new_attestor: vk(&ATTESTOR_4) - }), + })), ) .unwrap(), Response::new().add_event( @@ -666,10 +678,10 @@ fn remove_attestor() { deps.as_mut(), mock_env(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::RemoveAttestor { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::RemoveAttestor { chain_id: CHAIN_ID.to_owned(), old_attestor: vk(&ATTESTOR_4) - }), + })), ) .unwrap_err(), Error::InvalidAttestor { @@ -705,10 +717,10 @@ fn remove_attestor() { deps.as_mut(), mock_env(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::RemoveAttestor { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::RemoveAttestor { chain_id: CHAIN_ID.to_owned(), old_attestor: vk(&ATTESTOR_1) - }), + })), ) .unwrap(), Response::new().add_event( @@ -815,10 +827,10 @@ fn confirm_attestation() { deps.as_mut(), mock_env(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::SetQuorum { chain_id: CHAIN_ID.to_owned(), new_quorum: const { NonZero::new(1).unwrap() } - }), + })), ) .unwrap(), Response::new().add_event( @@ -915,10 +927,10 @@ fn quorum_unique_per_chain() { deps.as_mut(), env.clone(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::SetQuorum { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::SetQuorum { chain_id: "998".to_owned(), new_quorum: const { NonZero::new(3).unwrap() }, - }), + })), ) .unwrap(), Response::new().add_event( @@ -956,10 +968,10 @@ fn attestors_unique_per_chain() { deps.as_mut(), env.clone(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::AddAttestor { chain_id: "998".to_owned(), new_attestor: H256::MIN, - }), + })), ) .unwrap(), Response::new().add_event( @@ -974,10 +986,10 @@ fn attestors_unique_per_chain() { deps.as_mut(), env.clone(), message_info(&Addr::unchecked(""), &[]), - ExecuteMsg::Restricted(RestrictedExecuteMsg::AddAttestor { + ExecuteMsg::Restricted(Restricted::wrap(RestrictedExecuteMsg::AddAttestor { chain_id: "998".to_owned(), new_attestor: H256::MAX, - }), + })), ) .unwrap(), Response::new().add_event( diff --git a/voyager/modules/finality/attested-evm/src/main.rs b/voyager/modules/finality/attested-evm/src/main.rs index c99e382889..16b51cb144 100644 --- a/voyager/modules/finality/attested-evm/src/main.rs +++ b/voyager/modules/finality/attested-evm/src/main.rs @@ -3,7 +3,7 @@ use alloy::{ network::AnyNetwork, providers::{DynProvider, Provider, ProviderBuilder}, }; -use attested_light_client::contract::query::LatestHeight; +use attested_light_client::query::LatestHeight; use ibc_union_spec::Timestamp; use jsonrpsee::{ Extensions,