diff --git a/kms-connector/Cargo.lock b/kms-connector/Cargo.lock index 54b7f1bcda..ec0b695590 100644 --- a/kms-connector/Cargo.lock +++ b/kms-connector/Cargo.lock @@ -276,9 +276,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17c19591d57add4f0c47922877a48aae1f47074e3433436545f8948353b3bbb" +checksum = "9e15860af634cad451f598712c24ca7fd9b45d84fff68ab8d4967567fa996c64" dependencies = [ "alloy-consensus", "alloy-contract", @@ -289,7 +289,6 @@ dependencies = [ "alloy-network", "alloy-node-bindings", "alloy-provider", - "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", "alloy-serde", @@ -298,7 +297,6 @@ dependencies = [ "alloy-signer-local", "alloy-transport", "alloy-transport-http", - "alloy-transport-ws", "alloy-trie", ] @@ -315,9 +313,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a0dd3ed764953a6b20458b2b7abbfdc93d20d14b38babe1a70fe631a443a9f1" +checksum = "8b6440213a22df93a87ed512d2f668e7dc1d62a05642d107f82d61edc9e12370" dependencies = [ "alloy-eips", "alloy-primitives", @@ -326,6 +324,7 @@ dependencies = [ "alloy-trie", "alloy-tx-macros", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -341,9 +340,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9556182afa73cddffa91e64a5aa9508d5e8c912b3a15f26998d2388a824d2c7b" +checksum = "15d0bea09287942405c4f9d2a4f22d1e07611c2dbd9d5bf94b75366340f9e6e0" dependencies = [ "alloy-consensus", "alloy-eips", @@ -355,9 +354,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b19d7092c96defc3d132ee0d8969ca1b79ef512b5eda5c66e3065266b253adf2" +checksum = "d69af404f1d00ddb42f2419788fa87746a4cd13bab271916d7726fda6c792d94" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -366,7 +365,6 @@ dependencies = [ "alloy-network-primitives", "alloy-primitives", "alloy-provider", - "alloy-pubsub", "alloy-rpc-types-eth", "alloy-sol-types", "alloy-transport", @@ -420,32 +418,34 @@ dependencies = [ [[package]] name = "alloy-eip2930" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", "thiserror 2.0.12", ] [[package]] name = "alloy-eips" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fa99b538ca7006b0c03cfed24ec6d82beda67aac857ef4714be24231d15e6" +checksum = "4bd2c7ae05abcab4483ce821f12f285e01c0b33804e6883dd9ca1569a87ee2be" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -454,6 +454,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -465,14 +466,15 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a272533715aefc900f89d51db00c96e6fd4f517ea081a12fea482a352c8c815c" +checksum = "fc47eaae86488b07ea8e20236184944072a78784a1f4993f8ec17b3aa5d08c21" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-serde", "alloy-trie", + "borsh", "serde", "serde_with", ] @@ -504,9 +506,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91676d242c0ced99c0dd6d0096d7337babe9457cc43407d26aa6367fcf90553" +checksum = "003f46c54f22854a32b9cc7972660a476968008ad505427eabab49225309ec40" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -519,9 +521,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f82150116b30ba92f588b87f08fa97a46a1bd5ffc0d0597efdf0843d36bfda" +checksum = "4f4029954d9406a40979f3a3b46950928a0fdcfe3ea8a9b0c17490d57e8aa0e3" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -545,9 +547,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223612259a080160ce839a4e5df0125ca403a1d5e7206cc911cea54af5d769aa" +checksum = "7805124ad69e57bbae7731c9c344571700b2a18d351bda9e0eba521c991d1bcb" dependencies = [ "alloy-consensus", "alloy-eips", @@ -558,9 +560,9 @@ dependencies = [ [[package]] name = "alloy-node-bindings" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3652a65bacfba0a169755090d4ecd7d3c63fa534b21d09b8e604dc2609760da6" +checksum = "b03d35475a02d2a8b76209cb4a1336cb7d85331d10a0f6ec329ee42151695c19" dependencies = [ "alloy-genesis", "alloy-hardforks", @@ -606,9 +608,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7283b81b6f136100b152e699171bc7ed8184a58802accbc91a7df4ebb944445" +checksum = "d369e12c92870d069e0c9dc5350377067af8a056e29e3badf8446099d7e00889" dependencies = [ "alloy-chains", "alloy-consensus", @@ -618,7 +620,6 @@ dependencies = [ "alloy-network-primitives", "alloy-node-bindings", "alloy-primitives", - "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types-anvil", "alloy-rpc-types-debug", @@ -628,7 +629,6 @@ dependencies = [ "alloy-sol-types", "alloy-transport", "alloy-transport-http", - "alloy-transport-ws", "async-stream", "async-trait", "auto_impl", @@ -649,28 +649,6 @@ dependencies = [ "wasmtimer", ] -[[package]] -name = "alloy-pubsub" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee7e3d343814ec0dfea69bd1820042a133a9d0b9ac5faf1e6eb133b43366315" -dependencies = [ - "alloy-json-rpc", - "alloy-primitives", - "alloy-transport", - "auto_impl", - "bimap", - "futures", - "parking_lot", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tower", - "tracing", - "wasmtimer", -] - [[package]] name = "alloy-rlp" version = "0.3.12" @@ -695,16 +673,14 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1154b12d470bef59951c62676e106f4ce5de73b987d86b9faa935acebb138ded" +checksum = "31c89883fe6b7381744cbe80fef638ac488ead4f1956a4278956a1362c71cd2e" dependencies = [ "alloy-json-rpc", "alloy-primitives", - "alloy-pubsub", "alloy-transport", "alloy-transport-http", - "alloy-transport-ws", "futures", "pin-project", "reqwest", @@ -720,11 +696,12 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ab76bf97648a1c6ad8fb00f0d594618942b5a9e008afbfb5c8a8fca800d574" +checksum = "64e279e6d40ee40fe8f76753b678d8d5d260cb276dc6c8a8026099b16d2b43f4" dependencies = [ "alloy-primitives", + "alloy-rpc-types-anvil", "alloy-rpc-types-debug", "alloy-rpc-types-eth", "alloy-rpc-types-trace", @@ -734,9 +711,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456cfc2c1677260edbd7ce3eddb7de419cb46de0e9826c43401f42b0286a779a" +checksum = "5e176c26fdd87893b6afeb5d92099d8f7e7a1fe11d6f4fe0883d6e33ac5f31ba" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -746,9 +723,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cc57ee0c1ac9fb14854195fc249494da7416591dc4a4d981ddfd5dd93b9bce" +checksum = "b43c1622aac2508d528743fd4cfdac1dea92d5a8fa894038488ff7edd0af0b32" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -757,9 +734,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ac29dd005c33e3f7e09087accc80843315303685c3f7a1b888002cd27785b" +checksum = "1b2ca3a434a6d49910a7e8e51797eb25db42ef8a5578c52d877fcb26d0afe7bc" dependencies = [ "alloy-primitives", "derive_more", @@ -769,9 +746,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7d47bca1a2a1541e4404aa38b7e262bb4dffd9ac23b4f178729a4ddc5a5caa" +checksum = "ed5fafb741c19b3cca4cdd04fa215c89413491f9695a3e928dee2ae5657f607e" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -790,9 +767,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c331c8e48665607682e8a9549a2347c13674d4fbcbdc342e7032834eba2424f4" +checksum = "c55324323aa634b01bdecb2d47462a8dce05f5505b14a6e5db361eef16eda476" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -804,9 +781,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8468f1a7f9ee3bae73c24eead0239abea720dbf7779384b9c7e20d51bfb6b0" +checksum = "a6f180c399ca7c1e2fe17ea58343910cad0090878a696ff5a50241aee12fc529" dependencies = [ "alloy-primitives", "serde", @@ -815,9 +792,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33387c90b0a5021f45a5a77c2ce6c49b8f6980e66a318181468fb24cea771670" +checksum = "ecc39ad2c0a3d2da8891f4081565780703a593f090f768f884049aa3aa929cbc" dependencies = [ "alloy-primitives", "async-trait", @@ -830,9 +807,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bf90f2355769ad93f790b930434b8d3d2948317f3e484de458010409024462" +checksum = "75411104af460ca0b306ae998f0a00b5159457780487630f4b24722beae6b690" dependencies = [ "alloy-consensus", "alloy-network", @@ -849,9 +826,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55d9e795c85e36dcea08786d2e7ae9b73cb554b6bea6ac4c212def24e1b4d03" +checksum = "930e17cb1e46446a193a593a3bfff8d0ecee4e510b802575ebe300ae2e43ef75" dependencies = [ "alloy-consensus", "alloy-network", @@ -938,12 +915,11 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702002659778d89a94cd4ff2044f6b505460df6c162e2f47d1857573845b0ace" +checksum = "cae82426d98f8bc18f53c5223862907cac30ab8fc5e4cd2bb50808e6d3ab43d8" dependencies = [ "alloy-json-rpc", - "alloy-primitives", "auto_impl", "base64 0.22.1", "derive_more", @@ -962,9 +938,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6bdc0830e5e8f08a4c70a4c791d400a86679c694a3b4b986caf26fad680438" +checksum = "90aa6825760905898c106aba9c804b131816a15041523e80b6d4fe7af6380ada" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -975,24 +951,6 @@ dependencies = [ "url", ] -[[package]] -name = "alloy-transport-ws" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686219dcef201655763bd3d4eabe42388d9368bfbf6f1c8016d14e739ec53aac" -dependencies = [ - "alloy-pubsub", - "alloy-transport", - "futures", - "http 1.3.1", - "rustls 0.23.31", - "serde_json", - "tokio", - "tokio-tungstenite", - "tracing", - "ws_stream_wasm", -] - [[package]] name = "alloy-trie" version = "0.9.1" @@ -1011,11 +969,10 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.38" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bf39928a5e70c9755d6811a2928131b53ba785ad37c8bf85c90175b5d43b818" +checksum = "ae109e33814b49fc0a62f2528993aa8a2dd346c26959b151f05441dc0b9da292" dependencies = [ - "alloy-primitives", "darling", "proc-macro2", "quote", @@ -1436,17 +1393,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version 0.4.1", -] - [[package]] name = "atoi" version = "2.0.0" @@ -1959,12 +1905,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bimap" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" - [[package]] name = "bincode" version = "1.3.3" @@ -2156,6 +2096,29 @@ dependencies = [ "serde_with", ] +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "brotli" version = "8.0.2" @@ -2652,12 +2615,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - [[package]] name = "der" version = "0.7.10" @@ -4755,16 +4712,6 @@ dependencies = [ "indexmap 2.11.4", ] -[[package]] -name = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version 0.4.1", -] - [[package]] name = "pin-project" version = "1.1.10" @@ -5855,12 +5802,6 @@ dependencies = [ "pest", ] -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - [[package]] name = "serde" version = "1.0.226" @@ -6901,22 +6842,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "tokio-tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "rustls 0.23.31", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tungstenite", - "webpki-roots 0.26.11", -] - [[package]] name = "tokio-util" version = "0.7.16" @@ -7194,25 +7119,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" -dependencies = [ - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "rustls 0.23.31", - "rustls-pki-types", - "sha1", - "thiserror 2.0.12", - "utf-8", -] - [[package]] name = "tx-sender" version = "0.10.0" @@ -7333,12 +7239,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -7910,25 +7810,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" -[[package]] -name = "ws_stream_wasm" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "log", - "pharos", - "rustc_version 0.4.1", - "send_wrapper", - "thiserror 2.0.12", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wyz" version = "0.5.1" diff --git a/kms-connector/Cargo.toml b/kms-connector/Cargo.toml index 973347636c..e33ed7b839 100644 --- a/kms-connector/Cargo.toml +++ b/kms-connector/Cargo.toml @@ -27,10 +27,10 @@ tfhe = "=1.4.0-alpha.3" # External dependencies # ##################################################################### actix-web = "=4.11.0" -alloy = { version = "=1.0.38", default-features = false, features = [ +alloy = { version = "=1.1.2", default-features = false, features = [ "essentials", + "json-rpc", "provider-debug-api", - "provider-ws", "reqwest-rustls-tls", "signer-aws", "std", diff --git a/kms-connector/crates/tx-sender/Cargo.toml b/kms-connector/crates/tx-sender/Cargo.toml index d77a60fea4..f04c93b971 100644 --- a/kms-connector/crates/tx-sender/Cargo.toml +++ b/kms-connector/crates/tx-sender/Cargo.toml @@ -23,7 +23,6 @@ tracing.workspace = true tracing-opentelemetry.workspace = true [dev-dependencies] -alloy = { workspace = true, features = ["json-rpc"] } bc2wrap.workspace = true connector-utils = { workspace = true, features = ["tests"] } rstest.workspace = true diff --git a/kms-connector/crates/tx-sender/src/core/tx_sender.rs b/kms-connector/crates/tx-sender/src/core/tx_sender.rs index 5510fd332c..6125b3a211 100644 --- a/kms-connector/crates/tx-sender/src/core/tx_sender.rs +++ b/kms-connector/crates/tx-sender/src/core/tx_sender.rs @@ -7,9 +7,7 @@ use crate::{ }; use alloy::{ hex, - providers::{ - PendingTransactionError, Provider, RootProvider, ext::DebugApi, fillers::TxFiller, - }, + providers::{PendingTransactionError, Provider, ext::DebugApi}, rpc::types::{ TransactionReceipt, TransactionRequest, trace::geth::{CallConfig, GethDebugTracingOptions}, @@ -18,8 +16,7 @@ use alloy::{ }; use anyhow::anyhow; use connector_utils::{ - conn::{WalletGatewayProviderFillers, connect_to_db, connect_to_gateway_with_wallet}, - provider::NonceManagedProvider, + conn::{WalletGatewayProvider, connect_to_db, connect_to_gateway_with_wallet}, tasks::spawn_with_limit, types::{ CrsgenResponse, KeygenResponse, KmsResponse, KmsResponseKind, PrepKeygenResponse, @@ -39,31 +36,29 @@ use tracing::{debug, error, info, warn}; use tracing_opentelemetry::OpenTelemetrySpanExt; /// Struct sending stored KMS Core's responses to the Gateway. -pub struct TransactionSender +pub struct TransactionSender where - F: TxFiller, P: Provider, { /// The entity used to collect stored KMS Core's responses. response_picker: L, /// The entity responsible to send transaction to the Gateway. - inner: TransactionSenderInner, + inner: TransactionSenderInner

, /// The database pool for where the KMS Core's responses are stored. db_pool: Pool, } -impl TransactionSender +impl TransactionSender where L: KmsResponsePicker, - F: TxFiller + 'static, P: Provider + Clone + 'static, { /// Creates a new `TransactionSender` instance. pub fn new( response_picker: L, - inner: TransactionSenderInner, + inner: TransactionSenderInner

, db_pool: Pool, ) -> Self { Self { @@ -116,7 +111,7 @@ where /// Handles a response coming from the KMS Core. #[tracing::instrument(skip(inner, db_pool, cancel_token), fields(response = %response.kind))] async fn forward_response( - inner: TransactionSenderInner, + inner: TransactionSenderInner

, db_pool: Pool, response: KmsResponse, cancel_token: CancellationToken, @@ -135,7 +130,7 @@ where } } -impl TransactionSender { +impl TransactionSender { /// Creates a new `TransactionSender` instance from a valid `Config`. pub async fn from_config(config: Config) -> anyhow::Result<(Self, State)> { let db_pool = connect_to_db(&config.database_url, config.database_pool_size).await?; @@ -168,14 +163,13 @@ impl TransactionSender +pub struct TransactionSenderInner

where - F: TxFiller, P: Provider, { - provider: NonceManagedProvider, - decryption_contract: DecryptionInstance>, - kms_generation_contract: KMSGenerationInstance>, + provider: P, + decryption_contract: DecryptionInstance

, + kms_generation_contract: KMSGenerationInstance

, config: TransactionSenderInnerConfig, } @@ -187,15 +181,14 @@ pub struct TransactionSenderInnerConfig { pub gas_multiplier_percent: usize, } -impl TransactionSenderInner +impl

TransactionSenderInner

where - F: TxFiller, P: Provider, { pub fn new( - provider: NonceManagedProvider, - decryption_contract: DecryptionInstance>, - kms_generation_contract: KMSGenerationInstance>, + provider: P, + decryption_contract: DecryptionInstance

, + kms_generation_contract: KMSGenerationInstance

, inner_config: TransactionSenderInnerConfig, ) -> Self { Self { @@ -419,9 +412,8 @@ where } } -impl Clone for TransactionSenderInner +impl

Clone for TransactionSenderInner

where - F: TxFiller, P: Provider + Clone, { fn clone(&self) -> Self { @@ -481,20 +473,11 @@ impl From for Error { mod tests { use super::*; use alloy::{ - network::{Ethereum, IntoWallet, Network, TransactionBuilder}, primitives::Address, - providers::{ - Identity, ProviderBuilder, SendableTx, - fillers::{FillProvider, FillerControlFlow}, - mock::Asserter, - }, + providers::{Identity, ProviderBuilder, fillers::FillProvider, mock::Asserter}, rpc::{json_rpc::ErrorPayload, types::trace::geth::GethTrace}, - transports::TransportResult, - }; - use connector_utils::{ - config::KmsWallet, - tests::rand::{rand_signature, rand_u256}, }; + use connector_utils::tests::rand::{rand_signature, rand_u256}; use serde::de::DeserializeOwned; use serde_json::value::RawValue; use std::fs::File; @@ -504,15 +487,9 @@ mod tests { async fn test_send_tx_out_of_gas() -> anyhow::Result<()> { // Create a mocked `alloy::Provider` let asserter = Asserter::new(); - let mock_provider = NonceManagedProvider::new( - FillProvider::new( - ProviderBuilder::new() - .disable_recommended_fillers() - .connect_mocked_client(asserter.clone()), - MockFiller {}, - ), - Address::default(), - ); + let mock_provider = ProviderBuilder::new() + .disable_recommended_fillers() + .connect_mocked_client(asserter.clone()); // Used to mock all RPC responses of transaction sending operation let test_data_dir = test_data_dir(); @@ -555,10 +532,7 @@ mod tests { #[tokio::test] async fn test_disable_reverted_tx_tracing() { let asserter = Asserter::new(); - let mock_provider = NonceManagedProvider::new( - ProviderBuilder::new().connect_mocked_client(asserter.clone()), - Address::default(), - ); + let mock_provider = ProviderBuilder::new().connect_mocked_client(asserter.clone()); let inner_sender = TransactionSenderInner::new( mock_provider.clone(), DecryptionInstance::new(Address::default(), mock_provider.clone()), @@ -586,15 +560,9 @@ mod tests { async fn test_error_decryption_not_requested() -> anyhow::Result<()> { // Create a mocked `alloy::Provider` let asserter = Asserter::new(); - let mock_provider = NonceManagedProvider::new( - FillProvider::new( - ProviderBuilder::new() - .disable_recommended_fillers() - .connect_mocked_client(asserter.clone()), - MockFiller {}, - ), - Address::default(), - ); + let mock_provider = ProviderBuilder::new() + .disable_recommended_fillers() + .connect_mocked_client(asserter.clone()); // Used to mock all RPC responses of transaction sending operation let estimate_gas: usize = 21000; @@ -640,15 +608,9 @@ mod tests { async fn test_error_not_kms_tx_sender() -> anyhow::Result<()> { // Create a mocked `alloy::Provider` let asserter = Asserter::new(); - let mock_provider = NonceManagedProvider::new( - FillProvider::new( - ProviderBuilder::new() - .disable_recommended_fillers() - .connect_mocked_client(asserter.clone()), - MockFiller {}, - ), - Address::default(), - ); + let mock_provider = ProviderBuilder::new() + .disable_recommended_fillers() + .connect_mocked_client(asserter.clone()); // Used to mock all RPC responses of transaction sending operation let estimate_gas: usize = 21000; @@ -694,14 +656,11 @@ mod tests { async fn test_error_not_kms_signer() -> anyhow::Result<()> { // Create a mocked `alloy::Provider` let asserter = Asserter::new(); - let mock_provider = NonceManagedProvider::new( - FillProvider::new( - ProviderBuilder::new() - .disable_recommended_fillers() - .connect_mocked_client(asserter.clone()), - Identity, - ), - Address::default(), + let mock_provider = FillProvider::new( + ProviderBuilder::new() + .disable_recommended_fillers() + .connect_mocked_client(asserter.clone()), + Identity, ); // Used to mock all RPC responses of transaction sending operation @@ -750,66 +709,4 @@ mod tests { fn test_data_dir() -> String { format!("{}/tests/data/tx_out_of_gas", env!("CARGO_MANIFEST_DIR")) } - - /// A filler that mocks gas estimation and signing of the transactions - #[derive(Clone, Debug)] - struct MockFiller; - - impl TxFiller for MockFiller { - type Fillable = (); - - fn status(&self, tx: &::TransactionRequest) -> FillerControlFlow { - if tx.from().is_none() { - return FillerControlFlow::Ready; - } - - match tx.complete_preferred() { - Ok(_) => FillerControlFlow::Ready, - Err(e) => FillerControlFlow::Missing(vec![("Wallet", e)]), - } - } - - fn fill_sync(&self, _tx: &mut SendableTx) {} - - async fn prepare

( - &self, - _provider: &P, - _tx: &::TransactionRequest, - ) -> TransportResult - where - P: Provider, - { - Ok(()) - } - - async fn fill( - &self, - _fillable: Self::Fillable, - tx: SendableTx, - ) -> TransportResult> { - let mut builder = match tx { - SendableTx::Builder(builder) => builder, - _ => return Ok(tx), - }; - - let chain_id = 54321; - let wallet = KmsWallet::from_private_key_str( - "0x3f45b129a7fd099146e9fe63851a71646231f7743c712695f3b2d2bf0e41c774", - Some(chain_id), - ) - .unwrap() - .into_wallet(); - builder.set_gas_limit(21000); - builder.set_max_fee_per_gas(10); - builder.set_max_priority_fee_per_gas(10); - builder.set_chain_id(chain_id); - builder.set_nonce(0); - let envelope = builder - .build(&wallet) - .await - .map_err(RpcError::local_usage)?; - - Ok(SendableTx::Envelope(envelope)) - } - } } diff --git a/kms-connector/crates/utils/Cargo.toml b/kms-connector/crates/utils/Cargo.toml index 5bf3817e65..371c3d3250 100644 --- a/kms-connector/crates/utils/Cargo.toml +++ b/kms-connector/crates/utils/Cargo.toml @@ -43,6 +43,7 @@ testcontainers = { workspace = true, optional = true } toml = { workspace = true, optional = true } [dev-dependencies] +alloy = { workspace = true, features = ["provider-anvil-node"] } serial_test.workspace = true tempfile.workspace = true toml.workspace = true diff --git a/kms-connector/crates/utils/src/conn.rs b/kms-connector/crates/utils/src/conn.rs index ce28e15a20..dac46d8ea2 100644 --- a/kms-connector/crates/utils/src/conn.rs +++ b/kms-connector/crates/utils/src/conn.rs @@ -11,7 +11,7 @@ use alloy::{ WalletFiller, }, }, - transports::http::reqwest::Url, + transports::http::reqwest::{self, Url}, }; use anyhow::anyhow; use sqlx::{Pool, Postgres, postgres::PgPoolOptions}; @@ -24,6 +24,9 @@ pub const CONNECTION_RETRY_NUMBER: usize = 5; /// The delay between two connection attempts. pub const CONNECTION_RETRY_DELAY: Duration = Duration::from_secs(2); +/// Timeout for requests to the Gateway's RPC node. +const REQUEST_TIMEOUT: Duration = Duration::from_mins(1); + /// Tries to establish the connection with Postgres database. pub async fn connect_to_db(db_url: &str, db_pool_size: u32) -> anyhow::Result> { for i in 1..=CONNECTION_RETRY_NUMBER { @@ -55,7 +58,8 @@ type DefaultFillers = JoinFill< pub type GatewayProvider = FillProvider, RootProvider>; /// The default `alloy::Provider` used to interact with the Gateway using a wallet. -pub type WalletGatewayProvider = NonceManagedProvider; +pub type WalletGatewayProvider = + NonceManagedProvider>; pub type WalletGatewayProviderFillers = JoinFill< JoinFill, FillersWithoutNonceManagement>, WalletFiller, @@ -107,7 +111,11 @@ where let gateway_url = Url::from_str(gateway_url).map_err(|e| anyhow!("Invalid Gateway URL: {e}"))?; - let provider = provider_builder_new().connect_http(gateway_url); + let client = reqwest::ClientBuilder::new() + .timeout(REQUEST_TIMEOUT) + .build()?; + let provider = provider_builder_new().connect_reqwest(client, gateway_url); + info!("Connected to Gateway's RPC node successfully"); Ok(provider) } diff --git a/kms-connector/crates/utils/src/provider.rs b/kms-connector/crates/utils/src/provider/mod.rs similarity index 77% rename from kms-connector/crates/utils/src/provider.rs rename to kms-connector/crates/utils/src/provider/mod.rs index a2970e7ccc..97504c3911 100644 --- a/kms-connector/crates/utils/src/provider.rs +++ b/kms-connector/crates/utils/src/provider/mod.rs @@ -1,35 +1,34 @@ +mod nonce_manager; + +use crate::provider::nonce_manager::ZamaNonceManager; use alloy::{ consensus::Account, - eips::{BlockId, BlockNumberOrTag, Encodable2718}, - network::{Ethereum, Network, TransactionBuilder}, + eips::{BlockId, BlockNumberOrTag}, + network::{Network, TransactionBuilder}, primitives::{ Address, B256, BlockHash, BlockNumber, Bytes, StorageKey, StorageValue, TxHash, U64, U128, U256, }, providers::{ - EthCall, EthCallMany, EthGetBlock, FilterPollerBuilder, GetSubscription, - PendingTransaction, PendingTransactionBuilder, PendingTransactionConfig, - PendingTransactionError, Provider, ProviderCall, RootProvider, RpcWithBlock, SendableTx, - fillers::{ - BlobGasFiller, CachedNonceManager, FillProvider, GasFiller, JoinFill, NonceManager, - TxFiller, - }, + EthCall, EthCallMany, EthGetBlock, FilterPollerBuilder, PendingTransaction, + PendingTransactionBuilder, PendingTransactionConfig, PendingTransactionError, Provider, + ProviderCall, RootProvider, RpcWithBlock, SendableTx, + fillers::{BlobGasFiller, GasFiller, JoinFill, NonceManager}, }, rpc::{ client::NoParams, + json_rpc::ErrorPayload, types::{ AccessListResult, Bundle, EIP1186AccountProofResponse, EthCallResponse, FeeHistory, - Filter, FilterChanges, Index, Log, SyncStatus, TransactionReceipt, TransactionRequest, + Filter, FilterChanges, Index, Log, SyncStatus, erc4337::TransactionConditional, - pubsub::{Params, SubscriptionKind}, simulate::{SimulatePayload, SimulatedBlock}, }, }, - transports::{TransportError, TransportResult}, + transports::{RpcError, TransportErrorKind, TransportResult}, }; -use futures::lock::Mutex; use serde_json::value::RawValue; -use std::{borrow::Cow, sync::Arc}; +use std::borrow::Cow; pub type FillersWithoutNonceManagement = JoinFill; @@ -38,68 +37,23 @@ pub type FillersWithoutNonceManagement = JoinFill; /// Note that the provider given by the user must not have nonce management enabled, as this /// is done by the `NonceManagedProvider` itself. /// Users can use the default `FillersWithoutNonceManagement` to create a provider. -pub struct NonceManagedProvider -where - N: Network, - F: TxFiller, - P: Provider, -{ - inner: FillProvider, +pub struct NonceManagedProvider

{ + inner: P, signer_address: Address, - nonce_manager: Arc>, + nonce_manager: ZamaNonceManager, } -impl NonceManagedProvider -where - F: TxFiller, - P: Provider, -{ - pub fn new(provider: FillProvider, signer_address: Address) -> Self { +impl

NonceManagedProvider

{ + pub fn new(provider: P, signer_address: Address) -> Self { Self { inner: provider, signer_address, - nonce_manager: Default::default(), - } - } - - pub async fn send_transaction_sync( - &self, - mut tx: TransactionRequest, - ) -> TransportResult { - let nonce = self - .nonce_manager - .lock() - .await - .get_next_nonce(&self.inner, self.signer_address) - .await?; - tx.set_nonce(nonce); - - let mut tx_bytes = Vec::new(); - self.inner - .fill(tx) - .await? - .try_into_envelope() - .map_err(|e| TransportError::LocalUsageError(Box::new(e)))? - .encode_2718(&mut tx_bytes); - - let res = self - .client() - .request("eth_sendRawTransactionSync", (Bytes::from(tx_bytes),)) - .await; - if res.is_err() { - // Reset the nonce manager if the transaction sending failed. - *self.nonce_manager.lock().await = Default::default(); + nonce_manager: ZamaNonceManager::new(), } - res } } -impl Clone for NonceManagedProvider -where - N: Network, - F: TxFiller, - P: Provider + Clone, -{ +impl Clone for NonceManagedProvider

{ fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -109,34 +63,82 @@ where } } +// See https://ethereum-json-rpc.com/errors +const ETH_INVALID_INPUT_RPC_ERROR_CODE: i64 = -32000; + +/// Returns `true` if the RPC error is "nonce too low" or "already known", `false` otherwise. +fn is_nonce_too_low(error: &RpcError) -> bool { + match error { + RpcError::ErrorResp(ErrorPayload { code, message, .. }) => { + if *code == ETH_INVALID_INPUT_RPC_ERROR_CODE { + let lowercase_msg = message.to_lowercase(); + lowercase_msg.starts_with("nonce too low") + || lowercase_msg.starts_with("already known") + } else { + false + } + } + _ => false, + } +} + #[async_trait::async_trait] -impl Provider for NonceManagedProvider +impl Provider for NonceManagedProvider

where - N: Network, - F: TxFiller, P: Provider, { fn root(&self) -> &RootProvider { self.inner.root() } + async fn send_transaction_sync( + &self, + mut tx: N::TransactionRequest, + ) -> TransportResult { + let signer_addr = self.signer_address; + let nonce = self + .nonce_manager + .get_next_nonce(&self.inner, signer_addr) + .await?; + tx.set_nonce(nonce); + + let send_tx_result = self.inner.send_transaction_sync(tx).await; + match &send_tx_result { + Err(e) if is_nonce_too_low(e) => { + self.nonce_manager.confirm_nonce(signer_addr, nonce).await; + } + Err(_) => { + self.nonce_manager.release_nonce(signer_addr, nonce).await; + } + Ok(_) => self.nonce_manager.confirm_nonce(signer_addr, nonce).await, + } + + send_tx_result + } + async fn send_transaction( &self, mut tx: N::TransactionRequest, ) -> TransportResult> { + let signer_addr = self.signer_address; let nonce = self .nonce_manager - .lock() - .await - .get_next_nonce(&self.inner, self.signer_address) + .get_next_nonce(&self.inner, signer_addr) .await?; tx.set_nonce(nonce); - let res = self.inner.send_transaction(tx).await; - if res.is_err() { - // Reset the nonce manager if the transaction sending failed. - *self.nonce_manager.lock().await = Default::default(); + let send_tx_result = self.inner.send_transaction(tx).await; + + match &send_tx_result { + Err(e) if is_nonce_too_low(e) => { + self.nonce_manager.confirm_nonce(signer_addr, nonce).await; + } + Err(_) => { + self.nonce_manager.release_nonce(signer_addr, nonce).await; + } + Ok(_) => self.nonce_manager.confirm_nonce(signer_addr, nonce).await, } - res + + send_tx_result } fn get_accounts(&self) -> ProviderCall> { @@ -416,28 +418,6 @@ where self.inner.sign_transaction(tx).await } - fn subscribe_blocks(&self) -> GetSubscription<(SubscriptionKind,), N::HeaderResponse> { - self.inner.subscribe_blocks() - } - - fn subscribe_pending_transactions(&self) -> GetSubscription<(SubscriptionKind,), B256> { - self.inner.subscribe_pending_transactions() - } - - fn subscribe_full_pending_transactions( - &self, - ) -> GetSubscription<(SubscriptionKind, Params), N::TransactionResponse> { - self.inner.subscribe_full_pending_transactions() - } - - fn subscribe_logs(&self, filter: &Filter) -> GetSubscription<(SubscriptionKind, Params), Log> { - self.inner.subscribe_logs(filter) - } - - async fn unsubscribe(&self, id: B256) -> TransportResult<()> { - self.inner.unsubscribe(id).await - } - fn syncing(&self) -> ProviderCall { self.inner.syncing() } diff --git a/kms-connector/crates/utils/src/provider/nonce_manager.rs b/kms-connector/crates/utils/src/provider/nonce_manager.rs new file mode 100644 index 0000000000..49fb17accf --- /dev/null +++ b/kms-connector/crates/utils/src/provider/nonce_manager.rs @@ -0,0 +1,522 @@ +use alloy::{ + network::Network, + primitives::Address, + providers::{Provider, fillers::NonceManager}, + transports::TransportResult, +}; +use async_trait::async_trait; +use futures::lock::{Mutex, MutexGuard}; +use std::collections::{BTreeSet, HashMap, hash_map::Entry}; +use std::sync::Arc; +use tracing::debug; + +/// A robust, in-memory nonce manager for a scalable transaction engine. +#[derive(Clone, Debug, Default)] +pub struct ZamaNonceManager { + /// Nonce state for each account, shared across all tasks/threads using the nonce manager. + accounts: Arc>>, +} + +/// Represents the complete nonce state for a single account. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct AccountState { + /// The "high-water mark" nonce. Used only when no gaps are available. + pub next_nonce: u64, + /// Nonces that have been dispatched but not yet confirmed or rejected. + pub locked_nonces: BTreeSet, + /// Nonces that were previously locked but have been released, creating gaps. + pub available_nonces: BTreeSet, +} + +impl ZamaNonceManager { + pub fn new() -> Self { + Self::default() + } + + /// The primary logic for acquiring and locking the next valid nonce. + /// + /// The logic prioritizes filling gaps from `available_nonces` before incrementing the main + /// `next_nonce` counter. + pub async fn get_and_lock_nonce( + &self, + provider: &P, + address: Address, + ) -> TransportResult + where + P: Provider, + N: Network, + { + let mut accounts_guard = self.accounts.lock().await; + let account = + Self::get_or_init_account_state(&mut accounts_guard, provider, address).await?; + let nonce_to_use = + if let Some(available_nonce) = account.available_nonces.iter().next().copied() { + account.available_nonces.remove(&available_nonce); + debug!(%address, nonce = available_nonce, "Reusing available nonce"); + available_nonce + } else { + let next = account.next_nonce; + account.next_nonce += 1; + debug!(%address, nonce = next, "Using next sequential nonce"); + next + }; + + account.locked_nonces.insert(nonce_to_use); + Ok(nonce_to_use) + } + + /// Releases a locked nonce, making it available for reuse. + pub async fn release_nonce(&self, address: Address, nonce: u64) { + let mut accounts = self.accounts.lock().await; + if let Some(account) = accounts.get_mut(&address) + && account.locked_nonces.remove(&nonce) + { + account.available_nonces.insert(nonce); + } + } + + /// Confirms a nonce has been used on-chain, removing it permanently. + pub async fn confirm_nonce(&self, address: Address, nonce: u64) { + let mut accounts = self.accounts.lock().await; + if let Some(account) = accounts.get_mut(&address) { + account.locked_nonces.remove(&nonce); + + // Should not be required as a confirmed nonce should always be in a locked state, but + // it might be safer to remove it from the available nonces as well + account.available_nonces.remove(&nonce); + } + } + + /// Helper to retrieve or initialize the `AccountState` for an address. + async fn get_or_init_account_state<'a, P, N>( + accounts_guard: &'a mut MutexGuard<'_, HashMap>, + provider: &P, + address: Address, + ) -> TransportResult<&'a mut AccountState> + where + P: Provider, + N: Network, + { + let account = match accounts_guard.entry(address) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let initial_nonce = provider.get_transaction_count(address).await?; + entry.insert(AccountState { + next_nonce: initial_nonce, + ..Default::default() + }) + } + }; + Ok(account) + } +} + +// Implements the `NonceManager` trait for seamless integration with Alloy's provider stack. +#[async_trait] +impl NonceManager for ZamaNonceManager { + async fn get_next_nonce(&self, provider: &P, address: Address) -> TransportResult + where + P: Provider, + N: Network, + { + self.get_and_lock_nonce(provider, address).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::providers::ProviderBuilder; + use std::{collections::HashSet, str::FromStr}; + use tokio::time::{Duration, sleep}; + + async fn get_test_address(provider: &P) -> Address { + provider.get_accounts().await.unwrap()[0] + } + + #[tokio::test] + async fn test_initialization_matches_live_chain_nonce() { + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let manager = ZamaNonceManager::new(); + let address = get_test_address(&provider).await; + + let on_chain_nonce = provider.get_transaction_count(address).await.unwrap(); + + // Trigger initialization by getting the first nonce + let first_nonce = manager.get_next_nonce(&provider, address).await.unwrap(); + + assert_eq!( + first_nonce, on_chain_nonce, + "First fetched nonce should match the on-chain nonce" + ); + + let details = manager.get_account_details(address).await.unwrap(); + assert_eq!( + details.next_nonce, + on_chain_nonce + 1, + "High-water mark should be incremented" + ); + assert!(details.locked_nonces.contains(&on_chain_nonce)); + } + + #[tokio::test] + async fn test_sequential_nonces_are_dispensed_correctly() { + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let manager = ZamaNonceManager::new(); + let address = get_test_address(&provider).await; + + let nonce0 = manager.get_next_nonce(&provider, address).await.unwrap(); + assert_eq!(nonce0, 0); + let nonce1 = manager.get_next_nonce(&provider, address).await.unwrap(); + assert_eq!(nonce1, 1); + let nonce2 = manager.get_next_nonce(&provider, address).await.unwrap(); + assert_eq!(nonce2, 2); + + let details = manager.get_account_details(address).await.unwrap(); + assert_eq!(details.next_nonce, 3); + assert_eq!(details.locked_nonces, BTreeSet::from([0, 1, 2])); + assert!(details.available_nonces.is_empty()); + } + + #[tokio::test] + async fn test_get_next_nonce_prioritizes_available_gaps_over_sequential() { + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let manager = ZamaNonceManager::new(); + let address = get_test_address(&provider).await; + + // Manually set up a state with a high `next_nonce` and some available gaps. + let initial_state = AccountState { + next_nonce: 100, + locked_nonces: BTreeSet::new(), + available_nonces: BTreeSet::from([5, 2, 8]), // Intentionally unsorted + }; + manager.accounts.lock().await.insert(address, initial_state); + + // The manager should dispense the LOWEST available nonces first. + assert_eq!(manager.get_next_nonce(&provider, address).await.unwrap(), 2); + assert_eq!(manager.get_next_nonce(&provider, address).await.unwrap(), 5); + assert_eq!(manager.get_next_nonce(&provider, address).await.unwrap(), 8); + + // Now that the available pool is empty, it should use the high-water mark. + assert_eq!( + manager.get_next_nonce(&provider, address).await.unwrap(), + 100 + ); + + let details = manager.get_account_details(address).await.unwrap(); + assert!(details.available_nonces.is_empty()); + assert_eq!(details.locked_nonces, BTreeSet::from([2, 5, 8, 100])); + assert_eq!(details.next_nonce, 101); + } + + #[tokio::test] + async fn scenario_initialization_and_sequential_dispatch() { + println!("SCENARIO: Initialization and Sequential Dispatch"); + + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let manager = ZamaNonceManager::new(); + let address = get_test_address(&provider).await; + + let nonce0 = manager.get_next_nonce(&provider, address).await.unwrap(); + assert_eq!(nonce0, 0, "First nonce should be 0 for a new account"); + let nonce1 = manager.get_next_nonce(&provider, address).await.unwrap(); + assert_eq!(nonce1, 1, "Second nonce should be 1"); + let nonce2 = manager.get_next_nonce(&provider, address).await.unwrap(); + assert_eq!(nonce2, 2, "Third nonce should be 2"); + + // Final State Verification + let details = manager.get_account_details(address).await.unwrap(); + assert_eq!(details.next_nonce, 3, "High-water mark should now be 3"); + assert_eq!( + details.locked_nonces, + BTreeSet::from([0, 1, 2]), + "All dispatched nonces should be locked" + ); + assert!( + details.available_nonces.is_empty(), + "Available pool should be empty" + ); + // Validating transaction: + manager.confirm_nonce(address, 0).await; + manager.confirm_nonce(address, 1).await; + let details1 = manager.get_account_details(address).await.unwrap(); + assert_eq!( + details1.locked_nonces, + BTreeSet::from([2]), + "Only the transaction with nonce 2 is still pending." + ); + + manager.confirm_nonce(address, 2).await; + let details2 = manager.get_account_details(address).await.unwrap(); + assert_eq!( + details2.locked_nonces, + BTreeSet::from([]), + "All locked nonces should be released." + ); + assert_eq!( + details2.next_nonce, 3, + "High-water mark should now still be 3" + ); + } + + #[tokio::test] + async fn scenario_stuck_transaction_is_released_and_reused() { + println!("SCENARIO: Stuck Transaction is Released and Reused"); + + // 1. Arrange: Dispatch 3 nonces (0, 1, 2) + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let manager = ZamaNonceManager::new(); + let address = get_test_address(&provider).await; + manager.get_next_nonce(&provider, address).await.unwrap(); // Nonce 0 + manager.get_next_nonce(&provider, address).await.unwrap(); // Nonce 1 + manager.get_next_nonce(&provider, address).await.unwrap(); // Nonce 2 + + // 2. Act: Simulate nonce 1 getting stuck and being released. + println!("Releasing stuck nonce 1..."); + manager.release_nonce(address, 1).await; + + // 3. Assert Intermediate State + let details1 = manager.get_account_details(address).await.unwrap(); + assert_eq!( + details1.locked_nonces, + BTreeSet::from([0, 2]), + "Nonces 0 and 2 should remain locked" + ); + assert_eq!( + details1.available_nonces, + BTreeSet::from([1]), + "Nonce 1 should now be available" + ); + + // 4. Act: Request a new nonce. + println!("Requesting a new nonce, expecting it to be the released one..."); + let reused_nonce = manager.get_next_nonce(&provider, address).await.unwrap(); + + // 5. Assert Final State + assert_eq!( + reused_nonce, 1, + "The manager MUST reuse the released nonce 1 to fill the gap" + ); + let details2 = manager.get_account_details(address).await.unwrap(); + assert_eq!( + details2.locked_nonces, + BTreeSet::from([0, 1, 2]), + "All nonces are now locked again" + ); + assert!( + details2.available_nonces.is_empty(), + "Available pool should be empty after reuse" + ); + } + + #[tokio::test] + async fn scenario_concurrent_requests_for_same_address_are_safe() { + // This is the most critical test. It proves that the manager is thread-safe + // by simulating many concurrent requests for nonces for the SAME address. + // It must dispense unique, sequential nonces with no duplicates. + println!("SCENARIO: Concurrent Requests are Safe"); + + // 1. Arrange + let provider = Arc::new(ProviderBuilder::new().connect_anvil_with_wallet()); + let manager = Arc::new(ZamaNonceManager::new()); + let address = get_test_address(&provider).await; + + let initial_nonce = provider.get_transaction_count(address).await.unwrap(); + let num_tasks = 20; + let mut tasks = Vec::new(); + + // 2. Act: Spawn N tasks that all request a nonce at the same time. + for _ in 0..num_tasks { + let manager_clone = Arc::clone(&manager); + let provider_clone = Arc::clone(&provider); + tasks.push(tokio::spawn(async move { + manager_clone + .get_next_nonce(&*provider_clone, address) + .await + })); + } + let results = futures::future::join_all(tasks).await; + + // 3. Assert + let mut received_nonces = HashSet::new(); + for res in results { + let nonce_result = res.unwrap(); // Panics on task failure + assert!(nonce_result.is_ok()); + let nonce = nonce_result.unwrap(); + // The core assertion: was this nonce already given to another task? + assert!( + received_nonces.insert(nonce), + "FATAL: Duplicate nonce {nonce} dispensed under contention!", + ); + } + + assert_eq!( + received_nonces.len() as u64, + num_tasks, + "Should have received exactly {num_tasks} unique nonces", + ); + println!("✅ All {num_tasks} dispensed nonces were unique."); + + let final_details = manager.get_account_details(address).await.unwrap(); + assert_eq!( + final_details.next_nonce, + initial_nonce + num_tasks, + "High-water mark should be advanced by the number of tasks" + ); + } + + #[tokio::test] + async fn scenario_mixed_lifecycle_operations() { + // This test simulates a complex, realistic sequence of events. + println!("SCENARIO: Mixed Lifecycle Operations"); + + // 1. Arrange: Get 5 nonces (0-4) + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + + let manager = ZamaNonceManager::new(); + let address = Address::from_str("0x4444444444444444444444444444444444444444").unwrap(); + for i in 0..5 { + assert_eq!(manager.get_next_nonce(&provider, address).await.unwrap(), i); + } + + let details1 = manager.get_account_details(address).await.unwrap(); + assert_eq!(details1.locked_nonces, BTreeSet::from([0, 1, 2, 3, 4])); + + // 2. Act: A series of events happens out of order. + println!("Locked nonce 2, Releasing nonce 1, Confirming nonce 0..."); + manager.release_nonce(address, 1).await; // Got stuck + manager.confirm_nonce(address, 0).await; // Mined successfully + + // 3. Assert Intermediate State + let details2 = manager.get_account_details(address).await.unwrap(); + assert_eq!( + details2.locked_nonces, + BTreeSet::from([2, 3, 4]), + "Only nonces 2, 3 and 4 should be locked" + ); + assert_eq!( + details2.available_nonces, + BTreeSet::from([1]), + "Only nonce 1 should be available" + ); + + // 4. Act: Get two more nonces. + println!("Requesting two more nonces..."); + let nonce_a = manager.get_next_nonce(&provider, address).await.unwrap(); + let nonce_b = manager.get_next_nonce(&provider, address).await.unwrap(); + + // 5. Assert Final State + assert_eq!(nonce_a, 1, "Should have reused the available nonce 1 first"); + assert_eq!( + nonce_b, 5, + "Should have used the high-water mark nonce 5 after filling the gap" + ); + + let details3 = manager.get_account_details(address).await.unwrap(); + assert_eq!(details3.locked_nonces, BTreeSet::from([1, 2, 3, 4, 5])); + assert_eq!(details3.next_nonce, 6); + + // Then txs 1 and 2 passes ! And transaction 4 is dropped by the mempool! + manager.confirm_nonce(address, 1).await; // Mined successfully + manager.confirm_nonce(address, 2).await; // Mined successfully + manager.release_nonce(address, 4).await; + + let details4 = manager.get_account_details(address).await.unwrap(); + + assert_eq!(details4.locked_nonces, BTreeSet::from([3, 5])); + + let released = manager.get_next_nonce(&provider, address).await.unwrap(); + let details5 = manager.get_account_details(address).await.unwrap(); + assert_eq!( + released, 4, + "Should have reused the available nonce 4 first" + ); + assert_eq!(details5.locked_nonces, BTreeSet::from([3, 4, 5])); + assert_eq!(details5.next_nonce, 6); + } + + #[tokio::test] + async fn scenario_concurrent_requests_with_mixed_lifecycle() { + // This test doesn't care about real scenario. + println!("SCENARIO: Concurrent Requests with Mixed Lifecycle ('Chaos Test')"); + + // 1. Arrange + let provider = Arc::new(ProviderBuilder::new().connect_anvil_with_wallet()); + let manager = Arc::new(ZamaNonceManager::new()); + // Use a fresh address for a predictable state. + let address = Address::from_str("0x5555555555555555555555555555555555555555").unwrap(); + + // Pre-warm the manager: get the first 10 nonces (0-9) and lock them. + for i in 0..10 { + assert_eq!( + manager.get_next_nonce(&*provider, address).await.unwrap(), + i + ); + } + + let mut tasks = Vec::new(); + + // - Release nonce 2 and 5 (simulating stuck txs) + let manager_clone = Arc::clone(&manager); + tasks.push(tokio::spawn(async move { + println!("Task A: Releasing nonces 2 and 5"); + manager_clone.release_nonce(address, 2).await; + sleep(Duration::from_millis(10)).await; // small delay to allow interleaving + manager_clone.release_nonce(address, 5).await; + })); + + // - Confirm nonce 3 and 7 (simulating mined txs) + let manager_clone = Arc::clone(&manager); + tasks.push(tokio::spawn(async move { + println!("Task B: Confirming nonces 3 and 7"); + manager_clone.confirm_nonce(address, 3).await; + sleep(Duration::from_millis(5)).await; + manager_clone.confirm_nonce(address, 7).await; + })); + + // - Request 5 new nonces + let mut nonce_requester_tasks = Vec::new(); + for i in 0..5 { + let manager_clone = Arc::clone(&manager); + let provider_clone = Arc::clone(&provider); + nonce_requester_tasks.push(tokio::spawn(async move { + let nonce = manager_clone + .get_next_nonce(&*provider_clone, address) + .await + .unwrap(); + println!("Task C.{i}: Requested and received nonce {nonce}"); + nonce + })); + } + + // Wait for the release/confirm tasks to finish first. + futures::future::join_all(tasks).await; + // Then get the results from the nonce requesters. + futures::future::join_all(nonce_requester_tasks).await; + let state1 = manager.get_account_details(address).await.unwrap(); + assert_eq!( + state1.locked_nonces, + BTreeSet::from([0, 1, 2, 4, 6, 8, 9, 10, 11, 12, 13]) + ); + // Because of milliseconds waiting before release. + assert_eq!(state1.available_nonces, BTreeSet::from([5])); + assert_eq!(state1.next_nonce, 14); + manager.get_next_nonce(&provider, address).await.unwrap(); + let state2 = manager.get_account_details(address).await.unwrap(); + + assert_eq!( + state2.locked_nonces, + BTreeSet::from([0, 1, 2, 4, 5, 6, 8, 9, 10, 11, 12, 13]) + ); + // Because of milliseconds waiting before release. + assert_eq!(state2.available_nonces, BTreeSet::from([])); + assert_eq!(state2.next_nonce, 14); + } + + impl ZamaNonceManager { + /// Returns a snapshot of the current nonce state for a given address. + async fn get_account_details(&self, address: Address) -> Option { + self.accounts.lock().await.get(&address).cloned() + } + } +}