diff --git a/Cargo.lock b/Cargo.lock index f70dc814233..202a2792d87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -527,6 +527,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "auto-future" version = "1.0.0" @@ -924,6 +935,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "cfg_aliases", +] + [[package]] name = "brotli" version = "8.0.1" @@ -1007,6 +1027,34 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo-edit" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e499ddb9fd2cbcfc16a6624f53bc81acf8835ed26c7d13467e65ab220896dc44" +dependencies = [ + "anyhow", + "cargo_metadata 0.21.0", + "clap", + "clap-cargo", + "clap-verbosity-flag", + "concolor-control", + "dunce", + "env_logger", + "home", + "indexmap 2.12.1", + "log", + "pathdiff", + "semver 1.0.27", + "serde", + "serde_derive", + "tame-index", + "termcolor", + "toml 0.9.8", + "toml_edit 0.23.9", + "url", +] + [[package]] name = "cargo-platform" version = "0.1.9" @@ -1016,6 +1064,31 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo-platform" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-util-schemas" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc1a6f7b5651af85774ae5a34b4e8be397d9cf4bc063b7e6dbd99a841837830" +dependencies = [ + "semver 1.0.27", + "serde", + "serde-untagged", + "serde-value", + "thiserror 2.0.17", + "toml 0.8.23", + "unicode-xid", + "url", +] + [[package]] name = "cargo_metadata" version = "0.18.1" @@ -1023,8 +1096,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", - "cargo-platform", - "semver 1.0.26", + "cargo-platform 0.1.9", + "semver 1.0.27", "serde", "serde_json", "thiserror 1.0.69", @@ -1037,8 +1110,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", - "cargo-platform", - "semver 1.0.26", + "cargo-platform 0.1.9", + "semver 1.0.27", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cargo_metadata" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cfca2aaa699835ba88faf58a06342a314a950d2b9686165e038286c30316868" +dependencies = [ + "camino", + "cargo-platform 0.2.0", + "cargo-util-schemas", + "semver 1.0.27", "serde", "serde_json", "thiserror 2.0.17", @@ -1178,24 +1266,45 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", ] +[[package]] +name = "clap-cargo" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6affd9fc8702a94172345c11fa913aa84601cd05e187af166dcd48deff27b8d" +dependencies = [ + "anstyle", + "clap", +] + +[[package]] +name = "clap-verbosity-flag" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -1219,9 +1328,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1259,7 +1368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1273,6 +1382,23 @@ dependencies = [ "unicode-width 0.2.1", ] +[[package]] +name = "concolor-control" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7104119c2f80d887239879d0c50e033cd40eac9a3f3561e0684ba7d5d654f4da" +dependencies = [ + "atty", + "bitflags 1.3.2", + "concolor-query", +] + +[[package]] +name = "concolor-query" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad159cc964ac8f9d407cbc0aa44b02436c054b541f2b4b5f06972e1efdc54bc7" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1402,6 +1528,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1937,7 +2073,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "schemars 0.8.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "thiserror 1.0.69", ] @@ -2293,6 +2429,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.19" @@ -2499,6 +2641,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.13" @@ -2694,9 +2847,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -3037,7 +3190,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -3056,7 +3209,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -3123,6 +3276,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -3181,6 +3340,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -3316,11 +3484,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3509,6 +3677,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls 0.23.29", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -3704,9 +3873,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3816,13 +3985,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -3968,9 +4138,9 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4260,6 +4430,52 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "localnet-orchestrator" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "bytes", + "cargo-edit", + "cfg-if", + "clap", + "console", + "cw-utils", + "dkg-bypass-contract", + "futures", + "humantime", + "indicatif", + "itertools 0.14.0", + "nym-bin-common", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-crypto", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-pemstore", + "nym-performance-contract-common", + "nym-validator-client", + "nym-vesting-contract-common", + "rand 0.8.5", + "reqwest 0.12.22", + "serde", + "serde_json", + "sqlx", + "strum_macros", + "tempfile", + "time", + "tokio", + "toml 0.8.23", + "tracing", + "url", + "zeroize", +] + [[package]] name = "lock_api" version = "0.4.13" @@ -4272,9 +4488,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -4824,7 +5040,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", ] @@ -4897,7 +5113,7 @@ dependencies = [ "rand_chacha 0.3.1", "reqwest 0.12.22", "schemars 0.8.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "sha2 0.10.9", @@ -4983,7 +5199,7 @@ dependencies = [ "nym-service-provider-requests-common", "nym-validator-client", "nym-wireguard-types", - "semver 1.0.26", + "semver 1.0.27", "thiserror 2.0.17", "tokio", "tokio-util", @@ -5005,7 +5221,7 @@ dependencies = [ "nym-test-utils", "nym-wireguard-types", "rand 0.8.5", - "semver 1.0.26", + "semver 1.0.27", "serde", "sha2 0.10.9", "strum_macros", @@ -5789,7 +6005,7 @@ dependencies = [ "nym-http-api-client", "nym-network-defaults", "nym-validator-client", - "semver 1.0.26", + "semver 1.0.27", "thiserror 2.0.17", "tokio", "tracing", @@ -5802,7 +6018,7 @@ version = "0.1.0" dependencies = [ "nym-coconut-dkg-common", "nym-crypto", - "semver 1.0.26", + "semver 1.0.27", "serde", "thiserror 2.0.17", "time", @@ -6307,7 +6523,7 @@ dependencies = [ "nym-contracts-common", "rand_chacha 0.3.1", "schemars 0.8.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_repr", "thiserror 2.0.17", @@ -6639,7 +6855,7 @@ dependencies = [ "rand_chacha 0.3.1", "regex", "reqwest 0.12.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "serde_json_path", @@ -8100,6 +8316,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "peg" version = "0.8.5" @@ -8149,9 +8371,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" @@ -8204,7 +8426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.12.1", ] [[package]] @@ -8532,7 +8754,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.5", + "toml_edit 0.23.9", ] [[package]] @@ -8725,7 +8947,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8957,8 +9179,10 @@ dependencies = [ "async-compression", "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -8971,6 +9195,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.29", + "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", "serde_json", @@ -9146,6 +9371,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc-stable-hash" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08" + [[package]] name = "rustc_version" version = "0.2.3" @@ -9161,7 +9392,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.26", + "semver 1.0.27", ] [[package]] @@ -9174,7 +9405,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9240,7 +9471,7 @@ dependencies = [ "openssl-probe", "rustls-pemfile 1.0.4", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] @@ -9253,7 +9484,19 @@ dependencies = [ "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", ] [[package]] @@ -9489,7 +9732,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -9516,11 +9772,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -9548,6 +9805,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.5.0" @@ -9723,6 +10002,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -9745,7 +10033,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.12.1", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -9773,7 +10061,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "itoa", "ryu", "serde", @@ -9946,6 +10234,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smol_str" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "snafu" version = "0.7.5" @@ -10081,7 +10379,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.4", "hashlink", - "indexmap 2.10.0", + "indexmap 2.12.1", "log", "memchr", "once_cell", @@ -10454,7 +10752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -10474,6 +10772,33 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tame-index" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b180c2c9076c23d5588cb2fde0fdd012ff2bfcd97b1fdcb97e62903af2e44c7" +dependencies = [ + "bytes", + "camino", + "crossbeam-channel", + "home", + "http 1.3.1", + "libc", + "memchr", + "rayon", + "reqwest 0.12.22", + "rustc-stable-hash", + "semver 1.0.27", + "serde", + "serde_json", + "sha2 0.10.9", + "smol_str", + "thiserror 2.0.17", + "tokio", + "toml-span", + "twox-hash", +] + [[package]] name = "tap" version = "1.0.1" @@ -10501,7 +10826,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10579,7 +10904,7 @@ dependencies = [ "pin-project", "rand 0.8.5", "reqwest 0.11.27", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_bytes", "serde_json", @@ -10617,6 +10942,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix 1.0.8", + "windows-sys 0.60.2", +] + [[package]] name = "test-with" version = "0.15.4" @@ -10630,48 +10965,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "testnet-manager" -version = "0.1.0" -dependencies = [ - "anyhow", - "bip39", - "bs58", - "clap", - "console", - "cw-utils", - "dkg-bypass-contract", - "humantime", - "indicatif", - "nym-bin-common", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", - "nym-contracts-common", - "nym-crypto", - "nym-ecash-contract-common", - "nym-group-contract-common", - "nym-http-api-client", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-pemstore", - "nym-performance-contract-common", - "nym-validator-client", - "nym-vesting-contract-common", - "rand 0.8.5", - "serde", - "serde_json", - "sqlx", - "tempfile", - "thiserror 2.0.17", - "time", - "tokio", - "toml 0.8.23", - "tracing", - "url", - "zeroize", -] - [[package]] name = "textwrap" version = "0.16.2" @@ -11015,11 +11308,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.1", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml-span" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d36acfca70d66f9b5f9c4786fec60096c3594169bf77b8d4207174dc862e6a4" +dependencies = [ + "smallvec", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -11044,9 +11361,9 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", "winnow", @@ -11054,21 +11371,22 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.5" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "toml_datetime 0.7.3", "toml_parser", + "toml_writer", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -11079,6 +11397,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tonic" version = "0.12.3" @@ -11165,7 +11489,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.10.0", + "indexmap 2.12.1", "pin-project-lite", "slab", "sync_wrapper 1.0.2", @@ -11500,6 +11824,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typed-builder" version = "0.23.0" @@ -11520,6 +11850,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" @@ -11620,7 +11956,7 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.12.1", "once_cell", "serde", "tempfile", @@ -11662,7 +11998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f64bec2f3a33f2f08df8150e67fa45ba59a2ca740bf20c1beb010d4d791f9a1b" dependencies = [ "anyhow", - "indexmap 2.10.0", + "indexmap 2.12.1", "proc-macro2", "quote", "syn 2.0.104", @@ -11705,7 +12041,7 @@ checksum = "b925b6421df15cf4bedee27714022cd9626fb4d7eee0923522a608b274ba4371" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.12.1", "tempfile", "uniffi_internal_macros", ] @@ -11752,9 +12088,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -11792,7 +12128,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "serde", "serde_json", "utoipa-gen", @@ -12355,7 +12691,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -12779,9 +13115,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -12977,7 +13313,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.10.0", + "indexmap 2.12.1", "memchr", "thiserror 2.0.17", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index 85e43d76e2b..6c10b7f24a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,10 +153,10 @@ members = [ "tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-contract", "tools/internal/mixnet-connectivity-check", -# "tools/internal/sdk-version-bump", + # "tools/internal/sdk-version-bump", "tools/internal/ssl-inject", - "tools/internal/testnet-manager", - "tools/internal/testnet-manager/dkg-bypass-contract", + "tools/internal/localnet-orchestrator", + "tools/internal/localnet-orchestrator/dkg-bypass-contract", "tools/internal/validator-status-check", "tools/nym-cli", "tools/nym-id-cli", @@ -186,6 +186,7 @@ default-members = [ "service-providers/ip-packet-router", "service-providers/network-requester", "tools/nymvisor", + "tools/internal/localnet-orchestrator" ] exclude = ["contracts", "nym-wallet", "cpu-cycles"] @@ -225,6 +226,7 @@ bloomfilter = "3.0.1" bs58 = "0.5.1" bytecodec = "0.4.15" bytes = "1.10.1" +cargo-edit = "0.13.8" cargo_metadata = "0.19.2" celes = "2.6.0" cfg-if = "1.0.0" diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 2f6d87dbc41..3c7ea04e0ed 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -2,7 +2,7 @@ name = "nym-client-core" version = "1.1.15" authors = ["Dave Hrycyszyn "] -edition = "2021" +edition = "2024" rust-version = "1.85" license.workspace = true diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 8f0f1002063..5af519cabf8 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -32,6 +32,7 @@ const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50; const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50; const DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead +const DEFAULT_MAX_STARTUP_TOPOLOGY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead // Set this to a high value for now, so that we don't risk sporadic timeouts that might cause // bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the @@ -555,6 +556,11 @@ pub struct Topology { #[serde(with = "humantime_serde")] pub max_startup_gateway_waiting_period: Duration, + /// Defines how long the client is going to wait on startup for minimal topology to become online, + /// before abandoning the procedure. + #[serde(with = "humantime_serde")] + pub max_startup_network_waiting_period: Duration, + /// Specifies a minimum performance of a mixnode that is used on route construction. /// This setting is only applicable when `NymApi` topology is used. pub minimum_mixnode_performance: u8, @@ -583,6 +589,7 @@ impl Default for Topology { topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, disable_refreshing: false, max_startup_gateway_waiting_period: DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD, + max_startup_network_waiting_period: DEFAULT_MAX_STARTUP_TOPOLOGY_WAITING_PERIOD, minimum_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE, minimum_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE, use_extended_topology: false, diff --git a/common/client-core/config-types/src/old/v6.rs b/common/client-core/config-types/src/old/v6.rs index 704de10e528..14c9bab61b8 100644 --- a/common/client-core/config-types/src/old/v6.rs +++ b/common/client-core/config-types/src/old/v6.rs @@ -159,6 +159,7 @@ impl From for Config { use_extended_topology: value.debug.topology.use_extended_topology, ignore_egress_epoch_role: value.debug.topology.ignore_egress_epoch_role, ignore_ingress_epoch_role: value.debug.topology.ignore_ingress_epoch_role, + ..Default::default() }, reply_surbs: ReplySurbs { minimum_reply_surb_storage_threshold: value diff --git a/common/client-core/src/cli_helpers/client_add_gateway.rs b/common/client-core/src/cli_helpers/client_add_gateway.rs index 5a1950a35aa..ae2a611a0f5 100644 --- a/common/client-core/src/cli_helpers/client_add_gateway.rs +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -160,7 +160,10 @@ where ) .await?; } else { - info!("registered with new gateway {} (under address {address}), but this will not be our default address", gateway_details.gateway_id); + info!( + "registered with new gateway {} (under address {address}), but this will not be our default address", + gateway_details.gateway_id + ); } Ok(GatewayInfo { diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 600085b4cf8..3aa80cf5e5c 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -4,13 +4,13 @@ use super::mix_traffic::ClientRequestSender; use super::received_buffer::ReceivedBufferMessage; use super::statistics_control::StatisticsControl; -use crate::client::base_client::storage::helpers::store_client_keys; use crate::client::base_client::storage::MixnetClientStorage; +use crate::client::base_client::storage::helpers::store_client_keys; use crate::client::cover_traffic_stream::LoopCoverTrafficStream; use crate::client::event_control::EventControl; use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; -use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ClientKeys; +use crate::client::key_manager::persistence::KeyStore; use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway}; use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent}; use crate::client::real_messages_control; @@ -52,12 +52,12 @@ use nym_sphinx::addressing::nodes::NodeIdentity; use nym_sphinx::receiver::{ReconstructedMessage, SphinxMessageReceiver}; use nym_statistics_common::clients::ClientStatsSender; use nym_statistics_common::generate_client_stats_id; -use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; use nym_task::ShutdownTracker; -use nym_topology::provider_trait::TopologyProvider; +use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; use nym_topology::HardcodedTopologyProvider; +use nym_topology::provider_trait::TopologyProvider; use nym_validator_client::nym_api::NymApiClientExt; -use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent}; +use nym_validator_client::{UserAgent, nyxd::contract_traits::DkgQueryClient}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; use rand::thread_rng; @@ -220,6 +220,7 @@ pub struct BaseClientBuilder { nym_api_urls: Option>, wait_for_gateway: bool, + wait_for_initial_topology: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, shutdown: Option, @@ -250,6 +251,7 @@ where dkg_query_client, nym_api_urls: None, wait_for_gateway: false, + wait_for_initial_topology: false, custom_topology_provider: None, custom_gateway_transceiver: None, shutdown: None, @@ -305,6 +307,12 @@ where self } + #[must_use] + pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self { + self.wait_for_initial_topology = wait_for_initial_topology; + self + } + #[must_use] pub fn with_topology_provider( mut self, @@ -674,6 +682,7 @@ where topology_accessor: TopologyAccessor, local_gateway: NodeIdentity, wait_for_gateway: bool, + wait_for_initial_topology: bool, shutdown_tracker: &ShutdownTracker, ) -> Result<(), ClientCoreError> { let topology_refresher_config = @@ -694,6 +703,46 @@ where tracing::info!("Obtaining initial network topology"); topology_refresher.try_refresh().await; + // 1. wait for the minimum topology (if applicable) + if topology_refresher + .ensure_topology_is_routable() + .await + .is_err() + && wait_for_initial_topology + { + if let Err(err) = topology_refresher + .wait_for_initial_network(topology_config.max_startup_network_waiting_period) + .await + { + tracing::error!( + "the network did not come become online within the specified timeout: {err}" + ); + return Err(err.into()); + } + } + + // 2. wait for our gateway (if applicable) + if topology_refresher + .ensure_contains_routable_egress(local_gateway) + .await + .is_err() + && wait_for_gateway + { + if let Err(err) = topology_refresher + .wait_for_gateway( + local_gateway, + topology_config.max_startup_gateway_waiting_period, + ) + .await + { + tracing::error!( + "the gateway did not come back online within the specified timeout: {err}" + ); + return Err(err.into()); + } + } + + // 3. check if the topology is routable (in case we were NOT waiting for it) if let Err(err) = topology_refresher.ensure_topology_is_routable().await { tracing::error!( "The current network topology seem to be insufficient to route any packets through \ @@ -702,30 +751,15 @@ where return Err(ClientCoreError::InsufficientNetworkTopology(err)); } - let gateway_wait_timeout = if wait_for_gateway { - Some(topology_config.max_startup_gateway_waiting_period) - } else { - None - }; - + // 4. check if the gateway exists (in case we were NOT waiting for it) if let Err(err) = topology_refresher .ensure_contains_routable_egress(local_gateway) .await { - if let Some(waiting_timeout) = gateway_wait_timeout { - if let Err(err) = topology_refresher - .wait_for_gateway(local_gateway, waiting_timeout) - .await - { - tracing::error!( - "the gateway did not come back online within the specified timeout: {err}" - ); - return Err(err.into()); - } - } else { - tracing::error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}"); - return Err(err.into()); - } + tracing::error!( + "the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}" + ); + return Err(err.into()); } if !topology_config.disable_refreshing { @@ -1024,6 +1058,7 @@ where shared_topology_accessor.clone(), self_address.gateway(), self.wait_for_gateway, + self.wait_for_initial_topology, &shutdown_tracker.clone(), ) .await?; @@ -1195,9 +1230,11 @@ mod tests { ]); assert_eq!(network_details.nym_api_urls.as_ref().unwrap().len(), 2); - assert!(network_details.nym_api_urls.as_ref().unwrap()[1] - .front_hosts - .is_some()); + assert!( + network_details.nym_api_urls.as_ref().unwrap()[1] + .front_hosts + .is_some() + ); } #[test] @@ -1210,11 +1247,13 @@ mod tests { assert_eq!(api_url.url, "https://nym-frontdoor.vercel.app/api/"); assert_eq!(api_url.front_hosts.as_ref().unwrap().len(), 2); - assert!(api_url - .front_hosts - .as_ref() - .unwrap() - .contains(&"vercel.app".to_string())); + assert!( + api_url + .front_hosts + .as_ref() + .unwrap() + .contains(&"vercel.app".to_string()) + ); } #[test] diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index 365aabd0e87..335ca8c522e 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend}, + client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend, fs_backend}, config, config::Config, error::ClientCoreError, @@ -10,7 +10,7 @@ use crate::{ use nym_bandwidth_controller::BandwidthController; use nym_client_core_gateways_storage::OnDiskGatewaysDetails; use nym_credential_storage::storage::Storage as CredentialStorage; -use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; +use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd}; use std::{io, path::Path}; use time::OffsetDateTime; use tracing::{error, info, trace}; @@ -24,7 +24,9 @@ async fn setup_fresh_backend>( let mut storage_backend = match fs_backend::Backend::init(db_path).await { Ok(backend) => backend, Err(err) => { - error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}"); + error!( + "setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}" + ); return Err(ClientCoreError::SurbStorageError { source: Box::new(err), }); @@ -93,7 +95,9 @@ pub async fn setup_fs_reply_surb_backend>( match fs_backend::Backend::try_load(db_path).await { Ok(backend) => Ok(backend), Err(err) => { - error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); + error!( + "setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future" + ); archive_corrupted_database(db_path).await?; setup_fresh_backend(db_path, surb_config).await } diff --git a/common/client-core/src/client/base_client/storage/helpers.rs b/common/client-core/src/client/base_client/storage/helpers.rs index 922402e9e2e..02c54d9214a 100644 --- a/common/client-core/src/client/base_client/storage/helpers.rs +++ b/common/client-core/src/client/base_client/storage/helpers.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ClientKeys; +use crate::client::key_manager::persistence::KeyStore; use crate::error::ClientCoreError; use nym_client_core_gateways_storage::{ ActiveGateway, GatewayPublishedData, GatewayRegistration, GatewaysDetailsStore, diff --git a/common/client-core/src/client/base_client/storage/migration_helpers.rs b/common/client-core/src/client/base_client/storage/migration_helpers.rs index b115b23f972..788e25f7954 100644 --- a/common/client-core/src/client/base_client/storage/migration_helpers.rs +++ b/common/client-core/src/client/base_client/storage/migration_helpers.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 pub mod v1_1_33 { - use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; use crate::config::disk_persistence::CommonClientPaths; + use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; use crate::error::ClientCoreError; diff --git a/common/client-core/src/client/cover_traffic_stream.rs b/common/client-core/src/client/cover_traffic_stream.rs index 9d5d46f1d51..7beaeeaa63a 100644 --- a/common/client-core/src/client/cover_traffic_stream.rs +++ b/common/client-core/src/client/cover_traffic_stream.rs @@ -11,8 +11,8 @@ use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::cover::generate_loop_cover_packet; use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::utils::sample_poisson_duration; -use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; -use rand::{rngs::OsRng, CryptoRng, Rng}; +use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent}; +use rand::{CryptoRng, Rng, rngs::OsRng}; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; @@ -20,10 +20,10 @@ use tokio::sync::mpsc::error::TrySendError; use tracing::*; #[cfg(not(target_arch = "wasm32"))] -use tokio::time::{sleep, Sleep}; +use tokio::time::{Sleep, sleep}; #[cfg(target_arch = "wasm32")] -use wasmtimer::tokio::{sleep, Sleep}; +use wasmtimer::tokio::{Sleep, sleep}; pub struct LoopCoverTrafficStream where @@ -179,7 +179,9 @@ impl LoopCoverTrafficStream { ) { Ok(topology) => topology, Err(err) => { - warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"); + warn!( + "We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}" + ); return; } }; diff --git a/common/client-core/src/client/key_manager/persistence.rs b/common/client-core/src/client/key_manager/persistence.rs index 6395e07da6b..7a0f4957289 100644 --- a/common/client-core/src/client/key_manager/persistence.rs +++ b/common/client-core/src/client/key_manager/persistence.rs @@ -13,10 +13,10 @@ use crate::config::disk_persistence::ClientKeysPaths; #[cfg(not(target_arch = "wasm32"))] use nym_crypto::asymmetric::{ed25519, x25519}; #[cfg(not(target_arch = "wasm32"))] -use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; -#[cfg(not(target_arch = "wasm32"))] use nym_pemstore::KeyPairPath; #[cfg(not(target_arch = "wasm32"))] +use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +#[cfg(not(target_arch = "wasm32"))] use nym_sphinx::acknowledgements::AckKey; // we have to define it as an async trait since wasm storage is async diff --git a/common/client-core/src/client/mix_traffic/transceiver.rs b/common/client-core/src/client/mix_traffic/transceiver.rs index f749700ab2a..8737346836c 100644 --- a/common/client-core/src/client/mix_traffic/transceiver.rs +++ b/common/client-core/src/client/mix_traffic/transceiver.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::ed25519; -use nym_gateway_client::error::GatewayClientError; use nym_gateway_client::GatewayClient; +use nym_gateway_client::error::GatewayClientError; pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter}; use nym_gateway_requests::ClientRequest; use nym_sphinx::forwarding::packet::MixPacket; diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 2167718e57a..fe25bf24faf 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use super::action_controller::{AckActionSender, Action}; -use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; +use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent}; use futures::StreamExt; use nym_gateway_client::AcknowledgementReceiver; use nym_sphinx::{ - acknowledgements::{identifier::recover_identifier, AckKey}, - chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}, + acknowledgements::{AckKey, identifier::recover_identifier}, + chunking::fragment::{COVER_FRAG_ID, FragmentIdentifier}, }; use nym_task::ShutdownToken; use std::sync::Arc; diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 6262a37e234..4e1d41bf871 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -3,11 +3,11 @@ use super::PendingAcknowledgement; use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender; -use futures::channel::mpsc; use futures::StreamExt; +use futures::channel::mpsc; use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey}; -use nym_sphinx::chunking::fragment::FragmentIdentifier; use nym_sphinx::Delay as SphinxDelay; +use nym_sphinx::chunking::fragment::FragmentIdentifier; use nym_task::ShutdownToken; use std::collections::HashMap; use std::sync::Arc; diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 69ca92709fc..db4a48baeb0 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -9,8 +9,8 @@ use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::PacketType; -use nym_task::connections::TransmissionLane; use nym_task::ShutdownToken; +use nym_task::connections::TransmissionLane; use rand::{CryptoRng, Rng}; use tracing::*; diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index fd17348ee95..ecbd01109c4 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -16,10 +16,10 @@ use nym_gateway_client::AcknowledgementReceiver; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::{ + Delay as SphinxDelay, acknowledgements::AckKey, addressing::clients::Recipient, chunking::fragment::{Fragment, FragmentIdentifier}, - Delay as SphinxDelay, }; use nym_statistics_common::clients::ClientStatsSender; use rand::{CryptoRng, Rng}; diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 597bfecf567..2172923f7e5 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use super::{ - action_controller::{AckActionSender, Action}, PendingAcknowledgement, RetransmissionRequestReceiver, + action_controller::{AckActionSender, Action}, }; use crate::client::real_messages_control::acknowledgement_control::PacketDestination; use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError}; @@ -13,7 +13,7 @@ use futures::StreamExt; use nym_sphinx::chunking::fragment::Fragment; use nym_sphinx::preparer::PreparedFragment; use nym_sphinx::{addressing::clients::Recipient, params::PacketType}; -use nym_task::{connections::TransmissionLane, ShutdownToken}; +use nym_task::{ShutdownToken, connections::TransmissionLane}; use rand::{CryptoRng, Rng}; use std::sync::{Arc, Weak}; use tracing::*; diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs index 02560805a8e..80d4986c3ed 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs @@ -1,10 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::action_controller::{AckActionSender, Action}; use super::SentPacketNotificationReceiver; +use super::action_controller::{AckActionSender, Action}; use futures::StreamExt; -use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; +use nym_sphinx::chunking::fragment::{COVER_FRAG_ID, FragmentIdentifier}; use tracing::*; /// Module responsible for starting up retransmission timers. diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 366b2660901..77677b749ee 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -10,17 +10,17 @@ use crate::client::replies::reply_controller::MaxRetransmissions; use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit}; use nym_client_core_surb_storage::RetrievedReplySurb; +use nym_sphinx::Delay; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; -use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage}; use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; +use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage}; use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier}; use nym_sphinx::message::NymMessage; use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::preparer::{MessagePreparer, PreparedFragment}; -use nym_sphinx::Delay; -use nym_task::connections::TransmissionLane; use nym_task::ShutdownToken; +use nym_task::connections::TransmissionLane; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, Rng}; use std::collections::HashMap; @@ -272,7 +272,9 @@ where let primary_count = msg.required_packets(self.config.primary_packet_size); let secondary_count = msg.required_packets(secondary_packet); - trace!("This message would require: {primary_count} primary packets or {secondary_count} secondary packets..."); + trace!( + "This message would require: {primary_count} primary packets or {secondary_count} secondary packets..." + ); // if there would be no benefit in using the secondary packet - use the primary (duh) if primary_count <= secondary_count { trace!("so choosing primary for this message"); diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index 9b852535fa9..809088f4346 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -25,9 +25,9 @@ use nym_gateway_client::AcknowledgementReceiver; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_statistics_common::clients::ClientStatsSender; -use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths}; use nym_task::ShutdownToken; -use rand::{rngs::OsRng, CryptoRng, Rng}; +use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths}; +use rand::{CryptoRng, Rng, rngs::OsRng}; use std::sync::Arc; use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig; diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index 1b90208b6dd..0e826633004 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -17,11 +17,11 @@ use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::PacketSize; use nym_sphinx::preparer::PreparedFragment; use nym_sphinx::utils::sample_poisson_duration; -use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; +use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent}; +use nym_task::ShutdownToken; use nym_task::connections::{ ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, }; -use nym_task::ShutdownToken; use rand::{CryptoRng, Rng}; use std::pin::Pin; use std::sync::Arc; @@ -29,11 +29,11 @@ use std::time::Duration; use tracing::*; #[cfg(not(target_arch = "wasm32"))] -use tokio::time::{sleep, Sleep}; +use tokio::time::{Sleep, sleep}; // use wasm_utils::console_log; #[cfg(target_arch = "wasm32")] -use wasmtimer::tokio::{sleep, Sleep}; +use wasmtimer::tokio::{Sleep, sleep}; mod sending_delay_controller; /// Configurable parameters of the `OutQueueControl` @@ -230,7 +230,9 @@ where let (next_message, fragment_id, packet_size) = match next_message { StreamMessage::Cover => { let cover_traffic_packet_size = self.loop_cover_message_size(); - trace!("the next loop cover message will be put in a {cover_traffic_packet_size} packet"); + trace!( + "the next loop cover message will be put in a {cover_traffic_packet_size} packet" + ); // TODO for way down the line: in very rare cases (during topology update) we might have // to wait a really tiny bit before actually obtaining the permit hence messing with our @@ -244,7 +246,9 @@ where ) { Ok(topology) => topology, Err(err) => { - warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"); + warn!( + "We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}" + ); return; } }; @@ -436,7 +440,7 @@ where } } - if let Some(ref mut next_delay) = &mut self.next_delay { + if let Some(next_delay) = &mut self.next_delay { // it is not yet time to return a message if next_delay.as_mut().poll(cx).is_pending() { return Poll::Pending; diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index fa9898e42bd..4b36ad7c318 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::helpers::{get_time_now, Instant}; +use crate::client::helpers::{Instant, get_time_now}; use std::time::Duration; // The minimum time between increasing the average delay between packets. If we hit the ceiling in diff --git a/common/client-core/src/client/received_buffer.rs b/common/client-core/src/client/received_buffer.rs index b31cccb4198..51e6b537b97 100644 --- a/common/client-core/src/client/received_buffer.rs +++ b/common/client-core/src/client/received_buffer.rs @@ -5,20 +5,20 @@ use crate::client::helpers::get_time_now; use crate::client::replies::{ reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys, }; +use futures::StreamExt; use futures::channel::mpsc; use futures::lock::Mutex; -use futures::StreamExt; -use nym_crypto::asymmetric::x25519; use nym_crypto::Digest; +use nym_crypto::asymmetric::x25519; use nym_gateway_client::MixnetMessageReceiver; use nym_sphinx::anonymous_replies::requests::{ RepliableMessage, RepliableMessageContent, ReplyMessage, ReplyMessageContent, }; -use nym_sphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey}; +use nym_sphinx::anonymous_replies::{SurbEncryptionKey, encryption_key::EncryptionKeyDigest}; use nym_sphinx::message::{NymMessage, PlainMessage}; use nym_sphinx::params::ReplySurbKeyDigestAlgorithm; use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage}; -use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; +use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent}; use nym_task::ShutdownToken; use std::collections::HashSet; use std::sync::Arc; @@ -78,14 +78,19 @@ impl ReceivedMessagesBufferInner { let fragment = match self.message_receiver.recover_fragment(fragment_data) { Err(err) => { - warn!("failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!"); + warn!( + "failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!" + ); return None; } Ok(frag) => frag, }; if self.recently_reconstructed.contains(&fragment.id()) { - debug!("Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost", fragment.id()); + debug!( + "Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost", + fragment.id() + ); return None; } @@ -93,7 +98,9 @@ impl ReceivedMessagesBufferInner { match self.message_receiver.insert_new_fragment(fragment) { Err(err) => match err { MessageRecoveryError::MalformedReconstructedMessage { source, used_sets } => { - error!("message reconstruction failed - {source}. Attempting to re-use the message sets..."); + error!( + "message reconstruction failed - {source}. Attempting to re-use the message sets..." + ); // TODO: should we really insert reconstructed sets? could this be abused for some attack? for set_id in used_sets { if !self.recently_reconstructed.insert(set_id) { @@ -144,7 +151,9 @@ impl ReceivedMessagesBufferInner { &mut raw_fragment, ) { Err(err) => { - warn!("failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!"); + warn!( + "failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!" + ); return None; } Ok(frag_data) => frag_data, @@ -275,7 +284,9 @@ impl ReceivedMessagesBuffer { } RepliableMessageContent::Heartbeat(content) => { let additional_reply_surbs = content.additional_reply_surbs; - error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"); + error!( + "received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)" + ); (additional_reply_surbs, false) } RepliableMessageContent::DataV2(content) => { @@ -304,7 +315,9 @@ impl ReceivedMessagesBuffer { } RepliableMessageContent::HeartbeatV2(content) => { let additional_reply_surbs = content.additional_reply_surbs; - error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"); + error!( + "received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)" + ); (additional_reply_surbs, false) } }; @@ -380,7 +393,9 @@ impl ReceivedMessagesBuffer { if let Some(sender) = &inner_guard.message_sender { trace!("Sending reconstructed messages to announced sender"); if let Err(err) = sender.unbounded_send(reconstructed_messages) { - warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {err})"); + warn!( + "The reconstructed message receiver went offline without explicit notification (relevant error: - {err})" + ); inner_guard.message_sender = None; inner_guard.messages.extend(err.into_inner()); } diff --git a/common/client-core/src/client/replies/reply_controller/receiver_controller.rs b/common/client-core/src/client/replies/reply_controller/receiver_controller.rs index 11905c3a85b..0398787681b 100644 --- a/common/client-core/src/client/replies/reply_controller/receiver_controller.rs +++ b/common/client-core/src/client/replies/reply_controller/receiver_controller.rs @@ -5,15 +5,15 @@ use crate::client::real_messages_control::acknowledgement_control::PendingAcknow use crate::client::real_messages_control::message_handler::{ FragmentWithMaxRetransmissions, MessageHandler, PreparationError, }; -use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState; use crate::client::replies::reply_controller::Config; +use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState; use crate::client::topology_control::TopologyAccessor; use crate::client::transmission_buffer::TransmissionBuffer; use futures::channel::oneshot; use nym_client_core_surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap}; use nym_crypto::aes::cipher::crypto_common::rand_core::CryptoRng; -use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; +use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::chunking::fragment::FragmentIdentifier; use nym_task::connections::{ConnectionId, TransmissionLane}; use nym_topology::NymTopologyMetadata; @@ -50,7 +50,9 @@ impl SenderData { let pending_retransmissions = self.pending_retransmissions.len(); let total_pending = pending_retransmissions + pending_replies; - debug!("total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}"); + debug!( + "total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}" + ); total_pending } @@ -200,7 +202,9 @@ where let total_required_surbs = total_queue + target_surbs_after_clearing_queue; let total_available_surbs = pending_surbs + available_surbs; - debug!("available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}"); + debug!( + "available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}" + ); // We should request more surbs if: // 1. We haven't hit the maximum surb threshold, and @@ -225,9 +229,13 @@ where .is_none() { // don't report it every single time - warn!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"); + warn!( + "received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!" + ); } else { - trace!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"); + trace!( + "received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!" + ); } return; } @@ -383,7 +391,9 @@ where let (surbs_for_reply, _) = self.surbs_storage.get_reply_surbs(&target, to_take.len()); let Some(surbs_for_reply) = surbs_for_reply else { - error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + error!( + "somehow different task has stolen our reply surbs! - this should have been impossible" + ); self.re_insert_pending_retransmission(&target, to_take); return; }; @@ -459,7 +469,9 @@ where .get_reply_surbs(&target, to_send_clone.len()); let Some(surbs_for_reply) = surbs_for_reply else { - error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + error!( + "somehow different task has stolen our reply surbs! - this should have been impossible" + ); self.re_insert_pending_replies(&target, to_send); return; }; @@ -543,7 +555,9 @@ where let ack_ref = match timed_out_ack.upgrade() { Some(ack) => ack, None => { - debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue"); + debug!( + "we received the ack for one of the reply packets as we were putting it in the retransmission queue" + ); return; } }; @@ -657,9 +671,13 @@ where // only log at higher level if it's the first time this error has occurred in a while if now - last_failure > time::Duration::seconds(30) { - warn!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}") + warn!( + "failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}" + ) } else { - debug!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}") + debug!( + "failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}" + ) } } } @@ -681,7 +699,10 @@ where .surbs_storage .surbs_last_received_at(pending_reply_target) else { - error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", retransmission_buf.total_size()); + error!( + "we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", + retransmission_buf.total_size() + ); to_remove.push(*pending_reply_target); continue; }; @@ -702,7 +723,9 @@ where // if client is offline) if vals.current_clear_rerequest_counter > max_rerequests { to_remove.push(*pending_reply_target); - debug!("we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender"); + debug!( + "we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender" + ); continue; } @@ -710,7 +733,10 @@ where if diff > max_drop_wait { to_remove.push(*pending_reply_target) } else { - debug!("We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more", humantime::format_duration(diff.unsigned_abs())); + debug!( + "We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more", + humantime::format_duration(diff.unsigned_abs()) + ); vals.increment_current_clear_rerequest_counter(); to_request.push(*pending_reply_target); } diff --git a/common/client-core/src/client/replies/reply_controller/requests.rs b/common/client-core/src/client/replies/reply_controller/requests.rs index d4a7014065b..310e9f9a243 100644 --- a/common/client-core/src/client/replies/reply_controller/requests.rs +++ b/common/client-core/src/client/replies/reply_controller/requests.rs @@ -4,8 +4,8 @@ use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; use futures::channel::{mpsc, oneshot}; use nym_sphinx::addressing::clients::Recipient; -use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; +use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_task::connections::{ConnectionId, TransmissionLane}; use std::sync::Weak; use tracing::error; diff --git a/common/client-core/src/client/replies/reply_controller/sender_controller.rs b/common/client-core/src/client/replies/reply_controller/sender_controller.rs index 4d5aac999ba..0dd71f4623f 100644 --- a/common/client-core/src/client/replies/reply_controller/sender_controller.rs +++ b/common/client-core/src/client/replies/reply_controller/sender_controller.rs @@ -43,7 +43,9 @@ where // 1. check whether we sent any surbs in the past to this recipient, otherwise // they have no business in asking for more if !self.tags_storage.exists(&recipient) { - warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!"); + warn!( + "{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!" + ); return; } @@ -54,7 +56,12 @@ where .reply_surbs .maximum_allowed_reply_surb_request_size { - warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.reply_surbs.maximum_allowed_reply_surb_request_size); + warn!( + "The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", + self.config + .reply_surbs + .maximum_allowed_reply_surb_request_size + ); amount = self .config .reply_surbs diff --git a/common/client-core/src/client/statistics_control.rs b/common/client-core/src/client/statistics_control.rs index dcfbd2e19c5..30e2737a772 100644 --- a/common/client-core/src/client/statistics_control.rs +++ b/common/client-core/src/client/statistics_control.rs @@ -23,7 +23,7 @@ use nym_sphinx::addressing::Recipient; use nym_statistics_common::clients::{ ClientStatsController, ClientStatsReceiver, ClientStatsSender, }; -use nym_task::{connections::TransmissionLane, ShutdownToken, ShutdownTracker}; +use nym_task::{ShutdownToken, ShutdownTracker, connections::TransmissionLane}; use std::time::Duration; /// Time interval between reporting statistics locally (logging/shutdown_token) diff --git a/common/client-core/src/client/topology_control/accessor.rs b/common/client-core/src/client/topology_control/accessor.rs index 9841127b524..5d66f60c568 100644 --- a/common/client-core/src/client/topology_control/accessor.rs +++ b/common/client-core/src/client/topology_control/accessor.rs @@ -5,8 +5,8 @@ use nym_sphinx::addressing::clients::Recipient; use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError, NymTopologyMetadata}; use nym_validator_client::models::KeyRotationId; use std::ops::Deref; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use tokio::sync::{Notify, RwLock, RwLockReadGuard}; #[derive(Debug)] diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index c083aa73788..9eb2079f82e 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -63,7 +63,9 @@ impl TopologyRefresher { trace!("Refreshing the topology"); if self.topology_accessor.controlled_manually() { - info!("topology is being controlled manually - we're going to wait until the control is released..."); + info!( + "topology is being controlled manually - we're going to wait until the control is released..." + ); self.topology_accessor .wait_for_released_manual_control() .await; @@ -138,6 +140,35 @@ impl TopologyRefresher { } } + pub async fn wait_for_initial_network( + &mut self, + timeout_duration: Duration, + ) -> Result<(), NymTopologyError> { + info!( + "going to wait for at most {timeout_duration:?} for initial network to become online" + ); + + let deadline = sleep(timeout_duration); + tokio::pin!(deadline); + + loop { + tokio::select! { + _ = &mut deadline => { + return Err(NymTopologyError::TimedOutWaitingForTopology) + } + _ = self.try_refresh() => { + if let Err(err) = self.ensure_topology_is_routable().await { + info!("network is still not routable...: {err}"); + } else { + return Ok(()) + } + + sleep(self.refresh_rate).await + } + } + } + } + // it's perfectly fine if task is interrupted mid-refresh // there's no data to persist or send over pub async fn run(&mut self) { diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index a81118665c1..4898521df42 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -3,8 +3,8 @@ use async_trait::async_trait; use nym_mixnet_contract_common::EpochRewardedSet; -use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; use nym_topology::NymTopology; +use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; use nym_validator_client::nym_api::NymApiClientExt; use rand::prelude::SliceRandom; use rand::thread_rng; @@ -82,7 +82,9 @@ impl NymApiTopologyProvider { fn use_next_nym_api(&mut self) { if self.nym_api_urls.len() == 1 { - warn!("There's only a single nym API available - it won't be possible to use a different one"); + warn!( + "There's only a single nym API available - it won't be possible to use a different one" + ); return; } @@ -155,7 +157,10 @@ impl NymApiTopologyProvider { let mixnodes = mixnodes_res.nodes; if !gateways_res.metadata.consistency_check(&metadata) { - warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata); + warn!( + "inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", + gateways_res.metadata + ); return None; } diff --git a/common/client-core/src/client/transmission_buffer.rs b/common/client-core/src/client/transmission_buffer.rs index ee15210d148..12bb7ec59eb 100644 --- a/common/client-core/src/client/transmission_buffer.rs +++ b/common/client-core/src/client/transmission_buffer.rs @@ -1,11 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::helpers::{get_time_now, Instant}; +use crate::client::helpers::{Instant, get_time_now}; use crate::client::real_messages_control::real_traffic_stream::RealMessage; use nym_sphinx::chunking::fragment::Fragment; use nym_task::connections::TransmissionLane; -use rand::{seq::SliceRandom, Rng}; +use rand::{Rng, seq::SliceRandom}; use std::{ collections::{HashMap, HashSet, VecDeque}, time::Duration, diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 44eca8ac0bb..59c4b2b540e 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -7,9 +7,9 @@ use nym_gateway_client::error::GatewayClientError; use nym_task::RegistryAccessError; use nym_topology::node::RoutingNodeError; use nym_topology::{NodeId, NymTopologyError}; +use nym_validator_client::ValidatorClientError; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::nyxd::error::NyxdError; -use nym_validator_client::ValidatorClientError; use rand::distributions::WeightedError; use std::error::Error; use std::path::PathBuf; @@ -56,7 +56,9 @@ pub enum ClientCoreError { #[error("no gateways on network")] NoGatewaysOnNetwork, - #[error("there are no more new gateways on the network - it seems this client has already registered with all nodes it could have")] + #[error( + "there are no more new gateways on the network - it seems this client has already registered with all nodes it could have" + )] NoNewGatewaysAvailable, #[error("list of nym apis is empty")] @@ -127,7 +129,9 @@ pub enum ClientCoreError { #[error("unexpected exit")] UnexpectedExit, - #[error("this operation would have resulted in the gateway {gateway_id:?} key being overwritten without permission")] + #[error( + "this operation would have resulted in the gateway {gateway_id:?} key being overwritten without permission" + )] ForbiddenGatewayKeyOverwrite { gateway_id: String }, #[error( @@ -151,7 +155,9 @@ pub enum ClientCoreError { #[error("attempted to obtain fresh gateway details whilst already knowing about one")] UnexpectedGatewayDetails, - #[error("the provided gateway details (for gateway {gateway_id}) do not correspond to the shared keys")] + #[error( + "the provided gateway details (for gateway {gateway_id}) do not correspond to the shared keys" + )] MismatchedGatewayDetails { gateway_id: String }, #[error("unable to upgrade config file from `{current_version}`")] @@ -227,7 +233,9 @@ pub enum ClientCoreError { source: url::ParseError, }, - #[error("this client (id: '{client_id}') has already been initialised before. If you want to add additional gateway, use `add-gateway` command")] + #[error( + "this client (id: '{client_id}') has already been initialised before. If you want to add additional gateway, use `add-gateway` command" + )] AlreadyInitialised { client_id: String }, #[error("this client has already registered with gateway {gateway_id}")] diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 8cb926fe555..ce63903f8fb 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -5,13 +5,13 @@ use crate::error::ClientCoreError; use crate::init::types::RegistrationResult; use futures::{SinkExt, StreamExt}; use nym_crypto::asymmetric::ed25519; -use nym_gateway_client::client::GatewayListeners; use nym_gateway_client::GatewayClient; +use nym_gateway_client::client::GatewayListeners; use nym_topology::node::RoutingNode; +use nym_validator_client::UserAgent; use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt}; use nym_validator_client::nym_nodes::SkimmedNodesWithMetadata; -use nym_validator_client::UserAgent; -use rand::{seq::SliceRandom, Rng}; +use rand::{Rng, seq::SliceRandom}; #[cfg(unix)] use std::os::fd::RawFd; use std::{sync::Arc, time::Duration}; @@ -26,10 +26,10 @@ use nym_topology::NodeId; #[cfg(not(target_arch = "wasm32"))] use tokio::net::TcpStream; #[cfg(not(target_arch = "wasm32"))] -use tokio::time::sleep; -#[cfg(not(target_arch = "wasm32"))] use tokio::time::Instant; #[cfg(not(target_arch = "wasm32"))] +use tokio::time::sleep; +#[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 9a6910e3271..8d0d1eb8ad8 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -7,8 +7,8 @@ use crate::client::base_client::storage::helpers::{ has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details, store_gateway_details, update_stored_published_data_gateway, }; -use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ClientKeys; +use crate::client::key_manager::persistence::KeyStore; use crate::error::ClientCoreError; use crate::init::helpers::{ choose_gateway_by_latency, get_specified_gateway, uniformly_random_gateway, diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 75ee6a6a311..deafd830c51 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -1,8 +1,8 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ClientKeys; +use crate::client::key_manager::persistence::KeyStore; use crate::config::Config; use crate::error::ClientCoreError; use crate::init::{setup_gateway, use_loaded_gateway_details}; @@ -10,8 +10,8 @@ use nym_client_core_gateways_storage::{ GatewayRegistration, GatewaysDetailsStore, RemoteGatewayDetails, }; use nym_crypto::asymmetric::ed25519; -use nym_gateway_client::client::{GatewayListeners, InitGatewayClient}; use nym_gateway_client::SharedSymmetricKey; +use nym_gateway_client::client::{GatewayListeners, InitGatewayClient}; use nym_sphinx::addressing::clients::Recipient; use nym_topology::node::RoutingNode; use nym_validator_client::client::IdentityKey; diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 80af295644a..d9916328ea0 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -130,7 +130,7 @@ pub trait CosmWasmClient: TendermintRpcClient { let req = QueryBalanceRequest { address: address.to_string(), - denom: search_denom.to_string(), + denom: search_denom, }; let res = self diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index d7768b4bfd4..0c931c2bd5b 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -199,6 +199,18 @@ impl NyxdClient { let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?; Ok(Self::connect_with_signer(config, client, wallet)) } + + pub fn connect_with_mnemonic_and_network_details( + endpoint: U, + network_details: NymNetworkDetails, + mnemonic: bip39::Mnemonic, + ) -> Result + where + U: TryInto, + { + let config = Config::try_from_nym_network_details(&network_details)?; + Self::connect_with_mnemonic(config, endpoint, mnemonic) + } } #[allow(deprecated)] diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 23ca16dddfa..2a8dff99fb4 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -15,6 +15,16 @@ pub struct ChainDetails { pub stake_denom: DenomDetailsOwned, } +impl ChainDetails { + pub fn mainnet() -> Self { + ChainDetails { + bech32_account_prefix: mainnet::BECH32_PREFIX.into(), + mix_denom: mainnet::MIX_DENOM.into(), + stake_denom: mainnet::STAKE_DENOM.into(), + } + } +} + #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct NymContracts { @@ -175,11 +185,7 @@ impl NymNetworkDetails { // Consider caching this process (lazy static) NymNetworkDetails { network_name: mainnet::NETWORK_NAME.into(), - chain_details: ChainDetails { - bech32_account_prefix: mainnet::BECH32_PREFIX.into(), - mix_denom: mainnet::MIX_DENOM.into(), - stake_denom: mainnet::STAKE_DENOM.into(), - }, + chain_details: ChainDetails::mainnet(), endpoints: mainnet::validators(), contracts: NymContracts { mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), diff --git a/common/topology/src/error.rs b/common/topology/src/error.rs index 8350baaa942..91d01456600 100644 --- a/common/topology/src/error.rs +++ b/common/topology/src/error.rs @@ -38,6 +38,9 @@ pub enum NymTopologyError { #[error("timed out while waiting for gateway '{identity_key}' to come online")] TimedOutWaitingForGateway { identity_key: String }, + #[error("timed out while waiting for minimum network topology to become online")] + TimedOutWaitingForTopology, + #[error( "Wanted to create a mix route with {requested} hops, while only {available} layers are available" )] diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index cf7ff7f32ff..e56bf761cf4 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -159,6 +159,7 @@ impl WireguardGatewayData { pub struct WireguardData { pub inner: WireguardGatewayData, pub peer_rx: Receiver, + pub use_userspace: bool, } /// Start wireguard device @@ -170,6 +171,7 @@ pub async fn start_wireguard( upgrade_mode_status: nym_credential_verification::upgrade_mode::UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, wireguard_data: WireguardData, + use_userspace: bool, ) -> Result, Box> { use base64::{Engine, prelude::BASE64_STANDARD}; use defguard_wireguard_rs::{InterfaceConfiguration, WireguardInterfaceApi}; @@ -181,7 +183,11 @@ pub async fn start_wireguard( use tracing::info; let ifname = String::from(WG_TUN_BASE_NAME); - let wg_api = defguard_wireguard_rs::WGApi::new(ifname.clone(), false)?; + info!( + "Initializing WireGuard interface '{}' with use_userspace={}", + ifname, use_userspace + ); + let wg_api = defguard_wireguard_rs::WGApi::new(ifname.clone(), use_userspace)?; let mut peer_bandwidth_managers = HashMap::with_capacity(peers.len()); for peer in peers.iter() { @@ -212,7 +218,13 @@ pub async fn start_wireguard( interface_config.address, interface_config.port ); - wg_api.configure_interface(&interface_config)?; + info!("Configuring WireGuard interface..."); + wg_api.configure_interface(&interface_config).map_err(|e| { + tracing::error!("Failed to configure WireGuard interface: {:?}", e); + e + })?; + + info!("Adding IPv6 address to interface..."); std::process::Command::new("ip") .args([ "-6", @@ -226,7 +238,11 @@ pub async fn start_wireguard( "dev", (&ifname), ]) - .output()?; + .output() + .map_err(|e| { + tracing::error!("Failed to add IPv6 address: {:?}", e); + e + })?; // Use a dummy peer to create routing rule for the entire network space let mut catch_all_peer = Peer::new(Key::new([0; 32])); diff --git a/docker/localnet/README.md b/docker/localnet/README.md new file mode 100644 index 00000000000..19941bab3cb --- /dev/null +++ b/docker/localnet/README.md @@ -0,0 +1,608 @@ +# Nym Localnet for Kata Container Runtimes + +A complete Nym mixnet test environment running on Apple's container runtime for macOS (for now). + +## Overview + +This localnet setup provides a fully functional Nym mixnet for local development and testing: +- **3 mixnodes** (layer 1, 2, 3) +- **1 gateway** (entry + exit mode) +- **1 network-requester** (service provider) +- **1 SOCKS5 client** + +All components run in isolated containers with proper networking and dynamic IP resolution. + +## Prerequisites + +### Required +- **macOS** (tested on macOS Sequoia 15.0+) +- **Apple Container Runtime** - Built into macOS +- **Docker Desktop** (for building images only) +- **Python 3** with `base58` library + +### Installation +```bash +# Install Python dependencies +pip3 install --break-system-packages base58 + +# Verify container runtime is available +container --version + +# Verify Docker is installed (for building) +docker --version +``` + +## Quick Start + +```bash +# Navigate to the localnet directory +cd docker/localnet + +# Build the container image +./localnet.sh build + +# Start the localnet +./localnet.sh start + +# Test the SOCKS5 proxy +curl -L --socks5 localhost:1080 https://nymtech.net + +# View logs +./localnet.sh logs gateway +./localnet.sh logs socks5 + +# Stop the localnet +./localnet.sh stop + +# Clean up everything +./localnet.sh clean +``` + +## Architecture + +### Container Network + +All containers run on a custom bridge network (`nym-localnet-network`) with dynamic IP assignment: + +``` +Host Machine (macOS) +โ”œโ”€โ”€ nym-localnet-network (bridge) +โ”‚ โ”œโ”€โ”€ nym-mixnode1 (192.168.66.3) +โ”‚ โ”œโ”€โ”€ nym-mixnode2 (192.168.66.4) +โ”‚ โ”œโ”€โ”€ nym-mixnode3 (192.168.66.5) +โ”‚ โ”œโ”€โ”€ nym-gateway (192.168.66.6) +โ”‚ โ”œโ”€โ”€ nym-network-requester (192.168.66.7) +โ”‚ โ””โ”€โ”€ nym-socks5-client (192.168.66.8) +``` + +Ports published to host: +- 1080 โ†’ SOCKS5 proxy +- 9000 โ†’ Gateway entry +- 10001-10004 โ†’ Mixnet ports +- 20001-20004 โ†’ Verloc ports +- 30001-30004 โ†’ HTTP APIs + +### Startup Flow + +1. **Container Initialization** (parallel) + - Each container starts and gets a dynamic IP + - Each node runs `nym-node run --init-only` with its container IP + - Bonding JSON files are written to shared volume + +2. **Topology Generation** (sequential) + - Wait for all 4 bonding JSON files + - Get container IPs dynamically + - Run `build_topology.py` with container IPs + - Generate `network.json` with correct addresses + +3. **Node Startup** (parallel) + - Each container starts its node with `--local` flag + - Nodes read configuration from init phase + - Clients use custom topology file + +4. **Service Providers** (sequential) + - Network requester initializes and starts + - SOCKS5 client initializes with requester address + +### Network Topology + +The `network.json` file contains the complete network topology: + +```json +{ + "metadata": { + "key_rotation_id": 0, + "absolute_epoch_id": 0, + "refreshed_at": "2025-11-03T..." + }, + "rewarded_set": { + "epoch_id": 0, + "entry_gateways": [4], + "exit_gateways": [4], + "layer1": [1], + "layer2": [2], + "layer3": [3], + "standby": [] + }, + "node_details": { + "1": { "mix_host": "192.168.66.3:10001", ... }, + "2": { "mix_host": "192.168.66.4:10002", ... }, + "3": { "mix_host": "192.168.66.5:10003", ... }, + "4": { "mix_host": "192.168.66.6:10004", ... } + } +} +``` + +## Commands + +### Build +```bash +./localnet.sh build +``` +Builds the Docker image and loads it into Apple container runtime. + +**Note**: First build takes ~5-10 minutes to compile all components. + +### Start +```bash +./localnet.sh start +``` +Starts all containers, generates topology, and launches the complete network. + +**Expected output**: +``` +[INFO] Starting Nym Localnet... +[SUCCESS] Network created: nym-localnet-network +[INFO] Starting nym-mixnode1... +[SUCCESS] nym-mixnode1 started +... +[INFO] Building network topology with container IPs... +[SUCCESS] Network topology created successfully +[SUCCESS] Nym Localnet is running! + +Test with: + curl -x socks5h://127.0.0.1:1080 https://nymtech.net +``` + +### Stop +```bash +./localnet.sh stop +``` +Stops and removes all running containers. + +### Clean +```bash +./localnet.sh clean +``` +Complete cleanup: removes containers, volumes, network, and temporary files. + +### Logs +```bash +# View logs for a specific container +./localnet.sh logs + +# Container names: +# - mix1, mix2, mix3 +# - gateway +# - requester +# - socks5 + +# Examples: +./localnet.sh logs gateway +./localnet.sh logs socks5 +container logs nym-gateway --follow +``` + +### Status +```bash +# List all containers +container list + +# Check specific container +container logs nym-gateway + +# Inspect network +container network inspect nym-localnet-network +``` + +## Testing + +### Basic SOCKS5 Test +```bash +# Simple HTTP request with redirect following +curl -L --socks5 localhost:1080 http://example.com + +# HTTPS request +curl -L --socks5 localhost:1080 https://nymtech.net + +# Download a file +curl -L --socks5 localhost:1080 \ + https://test-download-files-nym.s3.amazonaws.com/download-files/1MB.zip \ + --output /tmp/test.zip +``` + +### Verify Network Topology +```bash +# View the generated topology +container exec nym-gateway cat /localnet/network.json | jq . + +# Check container IPs +container list | grep nym- + +# Verify all bonding files exist +container exec nym-gateway ls -la /localnet/ +``` + +### Test Mixnet Routing +```bash +# All traffic flows through: client โ†’ mix1 โ†’ mix2 โ†’ mix3 โ†’ gateway โ†’ internet +# Watch logs to verify routing: +container logs nym-mixnode1 --follow & +container logs nym-mixnode2 --follow & +container logs nym-mixnode3 --follow & +container logs nym-gateway --follow & + +# Make a request +curl -L --socks5 localhost:1080 https://nymtech.com +``` + +## File Structure + +``` +docker/localnet/ +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ localnet.sh # Main orchestration script +โ”œโ”€โ”€ Dockerfile.localnet # Docker image definition +โ””โ”€โ”€ build_topology.py # Topology generator +``` + +## How It Works + +### Node Initialization + +Each node initializes itself at runtime inside its container: + +```bash +# Get container IP +CONTAINER_IP=$(hostname -i) + +# Initialize with container IP +nym-node run --id mix1-localnet --init-only \ + --unsafe-disable-replay-protection \ + --local \ + --mixnet-bind-address=0.0.0.0:10001 \ + --verloc-bind-address=0.0.0.0:20001 \ + --http-bind-address=0.0.0.0:30001 \ + --http-access-token=lala \ + --public-ips $CONTAINER_IP \ + --output=json \ + --bonding-information-output="/localnet/mix1.json" +``` + +**Key flags**: +- `--local`: Accept private IPs for local development +- `--public-ips`: Announce the container's IP address +- `--unsafe-disable-replay-protection`: Disable bloomfilter to save memory + +### Dynamic Topology + +The topology is built **after** containers start: + +```bash +# Get container IPs +MIX1_IP=$(container exec nym-mixnode1 hostname -i) +MIX2_IP=$(container exec nym-mixnode2 hostname -i) +MIX3_IP=$(container exec nym-mixnode3 hostname -i) +GATEWAY_IP=$(container exec nym-gateway hostname -i) + +# Build topology with actual IPs +python3 build_topology.py /localnet localnet \ + $MIX1_IP $MIX2_IP $MIX3_IP $GATEWAY_IP +``` + +This ensures the topology contains reachable container addresses. + +### Client Configuration + +Clients use `--custom-mixnet` to read the local topology: + +```bash +# Network requester +nym-network-requester init \ + --id "network-requester-$SUFFIX" \ + --open-proxy=true \ + --custom-mixnet /localnet/network.json + +# SOCKS5 client +nym-socks5-client init \ + --id "socks5-client-$SUFFIX" \ + --provider "$REQUESTER_ADDRESS" \ + --custom-mixnet /localnet/network.json \ + --host 0.0.0.0 +``` + +The `--custom-mixnet` flag tells clients to use our local topology instead of fetching from nym-api. + +## Troubleshooting + +### Container Build Issues + +**Problem**: Docker build fails +```bash +# Check Docker is running +docker info + +# Clean Docker cache +docker system prune -a + +# Rebuild with no cache +./localnet.sh build +``` + +**Problem**: Container image load fails +```bash +# Verify temp file was created +ls -lh /tmp/nym-localnet-image-* + +# Check container runtime +container image list + +# Manually load if needed +docker save -o /tmp/nym-image.tar nym-localnet:latest +container image load --input /tmp/nym-image.tar +``` + +### Network Issues + +**Problem**: Containers can't communicate +```bash +# Check network exists +container network list | grep nym-localnet + +# Inspect network +container network inspect nym-localnet-network + +# Verify containers are on the network +container list | grep nym- +``` + +**Problem**: SOCKS5 connection refused +```bash +# Check SOCKS5 is listening +container logs nym-socks5-client | grep "Listening on" + +# Verify port mapping +container list | grep socks5 + +# Test from host +nc -zv localhost 1080 +``` + +### Node Issues + +**Problem**: "No valid public addresses" error +- Ensure `--local` flag is present in both init and run commands +- Check container can resolve its own IP: `container exec nym-mixnode1 hostname -i` +- Verify `--public-ips` is using `$CONTAINER_IP` variable + +**Problem**: "TUN device error" +- The gateway needs TUN device support for exit functionality +- Verify `iproute2` is installed in the image (adds `ip` command) +- Check gateway logs: `container logs nym-gateway` +- The gateway should show: "Created TUN device: nymtun0" + +**Problem**: "Noise handshake" warnings +- These are warnings, not errors - nodes fall back to TCP +- Does not affect functionality in local development +- Safe to ignore for testing purposes + +### Topology Issues + +**Problem**: Network.json not created +```bash +# Check all bonding files exist +container exec nym-gateway ls -la /localnet/ + +# Verify build_topology.py ran +container logs nym-gateway | grep "Building network topology" + +# Check Python dependencies +container exec nym-gateway python3 -c "import base58" +``` + +**Problem**: Clients can't connect to nodes +```bash +# Verify IPs in topology match container IPs +container exec nym-gateway cat /localnet/network.json | jq '.node_details' +container list | grep nym- + +# Check containers can reach each other +container exec nym-socks5-client ping -c 1 192.168.66.6 +``` + +### Startup Issues + +**Problem**: Containers exit immediately +```bash +# Check logs for errors +container logs nym-mixnode1 + +# Common issues: +# - Missing network.json: Wait for topology to be built +# - Port already in use: Check for conflicting services +# - Init failed: Check for correct container IP +``` + +**Problem**: Topology build times out +```bash +# Verify all containers initialized +container exec nym-gateway ls -la /localnet/*.json + +# Check for init errors +container logs nym-mixnode1 | grep -i error + +# Manual cleanup and restart +./localnet.sh clean +./localnet.sh start +``` + +## Performance Notes + +### Memory Usage +- Each mixnode: ~200MB +- Gateway: ~300MB (includes TUN device) +- Network requester: ~150MB +- SOCKS5 client: ~150MB +- **Total**: ~1.2GB + overhead + +**Recommended**: 4GB+ system memory + +### Startup Time +- Image build: ~5-10 minutes (first time) +- Network start: ~20-30 seconds +- Node initialization: ~5-10 seconds per node (parallel) + +### Latency +Mixnet adds latency by design for privacy: +- ~1-3 seconds for SOCKS5 requests +- Cover traffic adds random delays +- Local testing may show variable timing + +This is **expected behavior** - the mixnet provides privacy through traffic mixing. + +## Advanced Configuration + +### Custom Node Configuration + +Edit node init commands in `localnet.sh` (search for `nym-node run --init-only`): + +```bash +# Example: Change mixnode ports +--mixnet-bind-address=0.0.0.0:11001 \ +--verloc-bind-address=0.0.0.0:21001 \ +--http-bind-address=0.0.0.0:31001 \ +``` + +Remember to update port mappings in the `container run` command as well. + +### Enable Replay Protection + +Remove `--unsafe-disable-replay-protection` flags (requires more memory): + +```bash +# In start_mixnode() and start_gateway() functions +nym-node run --id mix1-localnet --init-only \ + --local \ + --mixnet-bind-address=0.0.0.0:10001 \ + # ... other flags (without --unsafe-disable-replay-protection) +``` + +**Note**: Each node will require an additional ~1.5GB memory for bloomfilter. + +### API Access + +Each node exposes an HTTP API: + +```bash +# Get gateway info +curl -H "Authorization: Bearer lala" http://localhost:30004/api/v1/gateway + +# Get mixnode stats +curl -H "Authorization: Bearer lala" http://localhost:30001/api/v1/stats + +# Get node description +curl -H "Authorization: Bearer lala" http://localhost:30001/api/v1/description +``` + +Access token is `lala` (configured with `--http-access-token=lala`). + +### Add More Mixnodes + +To add a 4th mixnode: + +1. **Update constants** in `localnet.sh`: +```bash +MIXNODE4_CONTAINER="nym-mixnode4" +``` + +2. **Add start call** in `start_all()`: +```bash +start_mixnode 4 "$MIXNODE4_CONTAINER" +``` + +3. **Update topology builder** to include the new node + +4. **Rebuild and restart**: +```bash +./localnet.sh clean +./localnet.sh build +./localnet.sh start +``` + +## Technical Details + +### Container Runtime + +Apple's container runtime is a native macOS container system: +- Uses Virtualization.framework for isolation +- Lightweight VMs for each container +- Native macOS integration +- Separate image store from Docker +- Natively uses [Kata Containers](https://github.com/kata-containers/kata-containers) images + +### Initial setup for [Container Runtime](https://github.com/apple/container) + +- **MUST** have MacOS Tahoe for inter-container networking +- `brew install --cask container` +- Download Kata Containers 3.20, this one can be loaded by `container` and has `CONFIG_TUN=y` kernel flag + - `https://github.com/kata-containers/kata-containers/releases/download/3.20.0/kata-static-3.20.0-arm64.tar.xz` +- Load new kernel + - `container system kernel set --tar kata-static-3.20.0-arm64.tar.xz --binary opt/kata/share/kata-containers/vmlinux-6.12.42-162` +- Validate kernel version once you have container running + - `uname -r` should return `6.12.42` + - `cat /proc/config.gz | grep CONFIG_TUN` should return `CONFIG_TUN=y` + +### Image Building + +Images are built with Docker then transferred: +1. `docker build` creates the image +2. `docker save` exports to tar file +3. `container image load` imports into container runtime +4. Temporary file is cleaned up + +This approach allows using Docker's build cache while running on Apple's runtime. + +### Network Architecture + +The custom bridge network (`nym-localnet-network`): +- Provides container-to-container communication +- Assigns dynamic IPs from 192.168.66.0/24 +- NAT for outbound internet access +- Port publishing for host access + +### Volumes + +Two types of volumes: +1. **Shared data** (`/tmp/nym-localnet-*`): Bonding files and topology +2. **Node configs** (`/tmp/nym-localnet-home-*`): Node configurations + +Both are ephemeral by default (cleaned up on stop). + +## Known Limitations + +- **macOS only**: Apple container runtime requires macOS +- **No Docker Compose**: Uses custom orchestration script +- **Dynamic IPs**: Container IPs may change between restarts +- **Port conflicts**: Cannot run alongside services using same ports +- **TUN device**: Gateway requires `ip` command for network interfaces + +## Support + +For issues and questions: +- **GitHub Issues**: https://github.com/nymtech/nym/issues +- **Documentation**: https://nymtech.net/docs +- **Discord**: https://discord.gg/nym + +## License + +This localnet setup is part of the Nym project and follows the same license. diff --git a/docker/localnet/build_topology.py b/docker/localnet/build_topology.py new file mode 100644 index 00000000000..88c2bfe8139 --- /dev/null +++ b/docker/localnet/build_topology.py @@ -0,0 +1,287 @@ +import json +import os +import subprocess +import sys +from datetime import datetime +from functools import lru_cache +from pathlib import Path + +import base58 + +DEFAULT_OWNER = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf" +DEFAULT_SUFFIX = os.environ.get("NYM_NODE_SUFFIX", "localnet") +NYM_NODES_ROOT = Path.home() / ".nym" / "nym-nodes" + + +def debug(msg): + """Print debug message to stderr""" + print(f"[DEBUG] {msg}", file=sys.stderr, flush=True) + + +def error(msg): + """Print error message to stderr""" + print(f"[ERROR] {msg}", file=sys.stderr, flush=True) + + +def maybe_assign(target, key, value): + if value is not None: + target[key] = value + + +@lru_cache(maxsize=None) +def get_nym_node_version(): + try: + result = subprocess.run( + ["nym-node", "--version"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + return None + + version_line = result.stdout.strip() + if not version_line: + return None + + parts = version_line.split() + for token in reversed(parts): + if token and token[0].isdigit(): + return token + return version_line + + +def node_config_path(prefix, suffix): + path = NYM_NODES_ROOT / f"{prefix}-{suffix}" / "config" / "config.toml" + debug(f"Looking for config at: {path}") + if path.exists(): + debug(f" โœ“ Config found") + return path + else: + error(f" โœ— Config NOT found at {path}") + return None + + +def read_node_details(prefix, suffix): + config_path = node_config_path(prefix, suffix) + if config_path is None: + error(f"Cannot read node details for {prefix}-{suffix}: config not found") + return {} + + debug(f"Running: nym-node node-details --config-file {config_path}") + try: + result = subprocess.run( + [ + "nym-node", + "node-details", + "--config-file", + str(config_path), + "--output=json", + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + debug(f" โœ“ node-details command succeeded") + except subprocess.CalledProcessError as e: + error(f"node-details command failed for {prefix}-{suffix}: {e}") + error(f" stdout: {e.stdout}") + error(f" stderr: {e.stderr}") + return {} + except FileNotFoundError: + error("nym-node command not found in PATH") + return {} + + try: + details = json.loads(result.stdout) + debug(f" โœ“ Parsed node-details JSON") + except json.JSONDecodeError as e: + error(f"Failed to parse node-details JSON: {e}") + error(f" Output was: {result.stdout[:200]}") + return {} + + info = {} + + # Get sphinx key and decode from Base58 to byte array + sphinx_data = details.get("x25519_primary_sphinx_key") + if isinstance(sphinx_data, dict): + sphinx_key_b58 = sphinx_data.get("public_key") + if sphinx_key_b58: + debug(f" Got sphinx_key (Base58): {sphinx_key_b58[:20]}...") + try: + # Decode Base58 to byte array + sphinx_bytes = base58.b58decode(sphinx_key_b58) + info["sphinx_key"] = list(sphinx_bytes) + debug(f" โœ“ Decoded to {len(sphinx_bytes)} bytes") + except Exception as e: + error(f" Failed to decode sphinx_key: {e}") + + version = get_nym_node_version() + if version: + info["version"] = version + + return info + + +def resolve_host(data): + # For localnet, always use 127.0.0.1 unless explicitly overridden + env_host = os.environ.get("LOCALNET_PUBLIC_IP") or os.environ.get("NYMNODE_PUBLIC_IP") + if env_host: + return env_host.split(",")[0].strip() + + # Default to localhost for localnet (containers can reach each other via published ports) + return "127.0.0.1" + + +def create_mixnode_entry(base_dir, mix_id, port_delta, suffix, host_ip): + """Create a node_details entry for a mixnode""" + debug(f"\n=== Creating mixnode{mix_id} entry ===") + mix_file = Path(base_dir) / f"mix{mix_id}.json" + debug(f"Reading bonding JSON from: {mix_file}") + with mix_file.open("r") as json_blob: + mix_data = json.load(json_blob) + + node_details = read_node_details(f"mix{mix_id}", suffix) + + # Get identity key from bonding JSON (already byte array) + identity = mix_data.get("identity_key") + if not identity: + raise RuntimeError(f"Missing identity_key in {mix_file}") + debug(f" โœ“ Got identity_key from bonding JSON: {len(identity)} bytes") + + # Get sphinx key from node-details (decoded from Base58) + sphinx_key = node_details.get("sphinx_key") + if not sphinx_key: + raise RuntimeError(f"Missing sphinx_key from node-details for mix{mix_id}") + + host = host_ip + port = 10000 + port_delta + debug(f" Using host: {host}:{port}") + + entry = { + "node_id": mix_id, + "mix_host": f"{host}:{port}", + "entry": None, + "identity_key": identity, + "sphinx_key": sphinx_key, + "supported_roles": { + "mixnode": True, + "mixnet_entry": False, + "mixnet_exit": False + } + } + + maybe_assign(entry, "version", node_details.get("version") or mix_data.get("version")) + + return entry + + +def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip): + """Create a node_details entry for a gateway""" + debug(f"\n=== Creating gateway entry ===") + gateway_file = Path(base_dir) / "gateway.json" + debug(f"Reading bonding JSON from: {gateway_file}") + with gateway_file.open("r") as json_blob: + gateway_data = json.load(json_blob) + + node_details = read_node_details("gateway", suffix) + + # Get identity key from bonding JSON (already byte array) + identity = gateway_data.get("identity_key") + if not identity: + raise RuntimeError("Missing identity_key in gateway.json") + debug(f" โœ“ Got identity_key from bonding JSON: {len(identity)} bytes") + + # Get sphinx key from node-details (decoded from Base58) + sphinx_key = node_details.get("sphinx_key") + if not sphinx_key: + raise RuntimeError("Missing sphinx_key from node-details for gateway") + + host = host_ip + mix_port = 10000 + port_delta + clients_port = 9000 + debug(f" Using host: {host} (mix:{mix_port}, clients:{clients_port})") + + entry = { + "node_id": node_id, + "mix_host": f"{host}:{mix_port}", + "entry": { + "ip_addresses": [host], + "clients_ws_port": clients_port, + "hostname": None, + "clients_wss_port": None + }, + "identity_key": identity, + "sphinx_key": sphinx_key, + "supported_roles": { + "mixnode": False, + "mixnet_entry": True, + "mixnet_exit": True + } + } + + maybe_assign(entry, "version", node_details.get("version") or gateway_data.get("version")) + + return entry + + +def main(args): + if not args: + raise SystemExit("Usage: build_topology.py [node_suffix] [mix1_ip] [mix2_ip] [mix3_ip] [gateway_ip]") + + base_dir = args[0] + suffix = args[1] if len(args) > 1 and args[1] else DEFAULT_SUFFIX + + # Get container IPs from arguments (or use 127.0.0.1 as fallback) + mix1_ip = args[2] if len(args) > 2 else "127.0.0.1" + mix2_ip = args[3] if len(args) > 3 else "127.0.0.1" + mix3_ip = args[4] if len(args) > 4 else "127.0.0.1" + gateway_ip = args[5] if len(args) > 5 else "127.0.0.1" + + debug(f"\n=== Starting topology generation ===") + debug(f"Output directory: {base_dir}") + debug(f"Node suffix: {suffix}") + debug(f"Container IPs: mix1={mix1_ip}, mix2={mix2_ip}, mix3={mix3_ip}, gateway={gateway_ip}") + + # Create node_details entries with integer keys + node_details = { + 1: create_mixnode_entry(base_dir, 1, 1, suffix, mix1_ip), + 2: create_mixnode_entry(base_dir, 2, 2, suffix, mix2_ip), + 3: create_mixnode_entry(base_dir, 3, 3, suffix, mix3_ip), + 4: create_gateway_entry(base_dir, 4, 4, suffix, gateway_ip) + } + + # Create the NymTopology structure + topology = { + "metadata": { + "key_rotation_id": 0, + "absolute_epoch_id": 0, + "refreshed_at": datetime.utcnow().isoformat() + "Z" + }, + "rewarded_set": { + "epoch_id": 0, + "entry_gateways": [4], + "exit_gateways": [4], + "layer1": [1], + "layer2": [2], + "layer3": [3], + "standby": [] + }, + "node_details": node_details + } + + output_path = Path(base_dir) / "network.json" + debug(f"\nWriting topology to: {output_path}") + with output_path.open("w") as out: + json.dump(topology, out, indent=2) + + print(f"โœ“ Generated topology with {len(node_details)} nodes") + print(f" - 3 mixnodes (layers 1, 2, 3)") + print(f" - 1 gateway (entry + exit)") + debug(f"\n=== Topology generation complete ===\n") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/docker/localnet/localnet-logs.sh b/docker/localnet/localnet-logs.sh new file mode 100755 index 00000000000..3347943e096 --- /dev/null +++ b/docker/localnet/localnet-logs.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Tmux-based log viewer for Nym Localnet containers +# Shows all container logs in a multi-pane layout + +SESSION_NAME="nym-localnet-logs" + +# Container names +CONTAINERS=( + "nym-mixnode1" + "nym-mixnode2" + "nym-mixnode3" + "nym-gateway" + "nym-network-requester" + "nym-socks5-client" +) + +# Check if containers are running +running_containers=() +for container in "${CONTAINERS[@]}"; do + if container inspect "$container" &>/dev/null; then + running_containers+=("$container") + fi +done + +if [ ${#running_containers[@]} -eq 0 ]; then + echo "Error: No containers are running" + echo "Start the localnet first: ./localnet.sh start" + exit 1 +fi + +# Check if we're already in tmux +if [ -n "$TMUX" ]; then + # Inside tmux - create new window + tmux new-window -n "logs" "container logs -f ${running_containers[0]}" + + # Split for remaining containers + for ((i=1; i<${#running_containers[@]}; i++)); do + tmux split-window -t logs "container logs -f ${running_containers[$i]}" + tmux select-layout -t logs tiled + done + + tmux select-layout -t logs tiled +else + # Not in tmux - check if session exists + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + # Session exists - attach to it + exec tmux attach-session -t "$SESSION_NAME" + else + # Create new session + tmux new-session -d -s "$SESSION_NAME" -n "logs" "container logs -f ${running_containers[0]}" + + # Split for remaining containers + for ((i=1; i<${#running_containers[@]}; i++)); do + tmux split-window -t "$SESSION_NAME:logs" "container logs -f ${running_containers[$i]}" + tmux select-layout -t "$SESSION_NAME:logs" tiled + done + + tmux select-layout -t "$SESSION_NAME:logs" tiled + + # Attach to the session + exec tmux attach-session -t "$SESSION_NAME" + fi +fi diff --git a/docker/localnet/localnet.sh b/docker/localnet/localnet.sh new file mode 100755 index 00000000000..a8bde191f82 --- /dev/null +++ b/docker/localnet/localnet.sh @@ -0,0 +1,597 @@ +#!/bin/bash + +set -ex + +# Nym Localnet Orchestration Script for Apple Container Runtime +# Emulates docker-compose functionality + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +IMAGE_NAME="nym-localnet:latest" +VOLUME_NAME="nym-localnet-data" +VOLUME_PATH="/tmp/nym-localnet-$$" +NYM_VOLUME_PATH="/tmp/nym-localnet-home-$$" + +SUFFIX=${NYM_NODE_SUFFIX:-localnet} + +# Container names +INIT_CONTAINER="nym-localnet-init" +MIXNODE1_CONTAINER="nym-mixnode1" +MIXNODE2_CONTAINER="nym-mixnode2" +MIXNODE3_CONTAINER="nym-mixnode3" +GATEWAY_CONTAINER="nym-gateway" +REQUESTER_CONTAINER="nym-network-requester" +SOCKS5_CONTAINER="nym-socks5-client" + +ALL_CONTAINERS=( + "$MIXNODE1_CONTAINER" + "$MIXNODE2_CONTAINER" + "$MIXNODE3_CONTAINER" + "$GATEWAY_CONTAINER" + "$REQUESTER_CONTAINER" + "$SOCKS5_CONTAINER" +) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${BLUE}[INFO]${NC} $*" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $*" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $*" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $*" +} + +cleanup_host_state() { + log_info "Cleaning local nym-node state for suffix ${SUFFIX}" + for node in mix1 mix2 mix3 gateway; do + rm -rf "$HOME/.nym/nym-nodes/${node}-${SUFFIX}" + done +} + +# Check if container command exists +check_prerequisites() { + if ! command -v container &> /dev/null; then + log_error "Apple 'container' command not found" + log_error "Install from: https://github.com/apple/container" + exit 1 + fi +} + +# Build the Docker image +build_image() { + log_info "Building image: $IMAGE_NAME" + log_warn "This will take 15-30 minutes on first build..." + + cd "$PROJECT_ROOT" + + # Build with Docker + log_info "Building with Docker..." + if ! docker build \ + -f "$SCRIPT_DIR/Dockerfile.localnet" \ + -t "$IMAGE_NAME" \ + "$PROJECT_ROOT"; then + log_error "Docker build failed" + exit 1 + fi + + # Transfer image to container runtime + log_info "Transferring image to container runtime..." + + # Save to temporary file (container image load doesn't support stdin) + TEMP_IMAGE="/tmp/nym-localnet-image-$$.tar" + if ! docker save -o "$TEMP_IMAGE" "$IMAGE_NAME"; then + log_error "Failed to save Docker image" + exit 1 + fi + + # Load into container runtime from file + if ! container image load --input "$TEMP_IMAGE"; then + rm -f "$TEMP_IMAGE" + log_error "Failed to load image into container runtime" + exit 1 + fi + + # Clean up temporary file + rm -f "$TEMP_IMAGE" + + # Verify image is available + if ! container image inspect "$IMAGE_NAME" &>/dev/null; then + log_error "Image not found in container runtime after load" + exit 1 + fi + + log_success "Image built and loaded: $IMAGE_NAME" +} + +# Create shared volume directory +create_volume() { + log_info "Creating shared volume at: $VOLUME_PATH" + mkdir -p "$VOLUME_PATH" + chmod 777 "$VOLUME_PATH" + log_success "Volume created" +} + +# Create shared nym home directory +create_nym_volume() { + log_info "Creating shared nym home volume at: $NYM_VOLUME_PATH" + mkdir -p "$NYM_VOLUME_PATH" + chmod 777 "$NYM_VOLUME_PATH" + log_success "Nym home volume created" +} + +# Remove shared volume directory +remove_volume() { + if [ -d "$VOLUME_PATH" ]; then + log_info "Removing volume: $VOLUME_PATH" + rm -rf "$VOLUME_PATH" + log_success "Volume removed" + fi + if [ -d "$NYM_VOLUME_PATH" ]; then + log_info "Removing nym home volume: $NYM_VOLUME_PATH" + rm -rf "$NYM_VOLUME_PATH" + log_success "Nym home volume removed" + fi +} + +# Network name +NETWORK_NAME="nym-localnet-network" + +# Create container network +create_network() { + log_info "Creating container network: $NETWORK_NAME" + if container network create "$NETWORK_NAME" 2>/dev/null; then + log_success "Network created: $NETWORK_NAME" + else + log_info "Network $NETWORK_NAME already exists or creation failed" + fi +} + +# Remove container network +remove_network() { + if container network list | grep -q "$NETWORK_NAME"; then + log_info "Removing network: $NETWORK_NAME" + container network rm "$NETWORK_NAME" 2>/dev/null || true + log_success "Network removed" + fi +} + +# Start a mixnode +start_mixnode() { + local node_id=$1 + local container_name=$2 + + log_info "Starting $container_name..." + + # Calculate port numbers based on node_id + local mixnet_port="1000${node_id}" + local verloc_port="2000${node_id}" + local http_port="3000${node_id}" + + container run \ + --name "$container_name" \ + --dns 1.1.1.1 \ + -m 2G \ + --network "$NETWORK_NAME" \ + -p "${mixnet_port}:${mixnet_port}" \ + -p "${verloc_port}:${verloc_port}" \ + -p "${http_port}:${http_port}" \ + -v "$VOLUME_PATH:/localnet" \ + -v "$NYM_VOLUME_PATH:/root/.nym" \ + -d \ + -e "NYM_NODE_SUFFIX=$SUFFIX" \ + "$IMAGE_NAME" \ + sh -c ' + CONTAINER_IP=$(hostname -i); + echo "Container IP: $CONTAINER_IP"; + echo "Initializing mix'"${node_id}"'..."; + nym-node run --id mix'"${node_id}"'-localnet --init-only \ + --unsafe-disable-replay-protection \ + --local \ + --mixnet-bind-address=0.0.0.0:'"${mixnet_port}"' \ + --verloc-bind-address=0.0.0.0:'"${verloc_port}"' \ + --http-bind-address=0.0.0.0:'"${http_port}"' \ + --http-access-token=lala \ + --public-ips $CONTAINER_IP \ + --output=json \ + --bonding-information-output="/localnet/mix'"${node_id}"'.json"; + + echo "Waiting for network.json..."; + while [ ! -f /localnet/network.json ]; do + sleep 2; + done; + echo "Starting mix'"${node_id}"'..."; + exec nym-node run --id mix'"${node_id}"'-localnet --unsafe-disable-replay-protection --local + ' + + log_success "$container_name started" +} +# Start gateway +start_gateway() { + log_info "Starting $GATEWAY_CONTAINER..." + + container run \ + --name "$GATEWAY_CONTAINER" \ + --dns 1.1.1.1 \ + -m 2G \ + --network "$NETWORK_NAME" \ + -p 9000:9000 \ + -p 10004:10004 \ + -p 20004:20004 \ + -p 30004:30004 \ + -v "$VOLUME_PATH:/localnet" \ + -v "$NYM_VOLUME_PATH:/root/.nym" \ + -d \ + -e "NYM_NODE_SUFFIX=$SUFFIX" \ + "$IMAGE_NAME" \ + sh -c ' + CONTAINER_IP=$(hostname -i); + echo "Container IP: $CONTAINER_IP"; + echo "Initializing gateway..."; + nym-node run --id gateway-localnet --init-only \ + --unsafe-disable-replay-protection \ + --local \ + --mode entry-gateway \ + --mode exit-gateway \ + --mixnet-bind-address=0.0.0.0:10004 \ + --entry-bind-address=0.0.0.0:9000 \ + --verloc-bind-address=0.0.0.0:20004 \ + --http-bind-address=0.0.0.0:30004 \ + --http-access-token=lala \ + --public-ips $CONTAINER_IP \ + --output=json \ + --wireguard-enabled true \ + --wireguard-userspace true \ + --bonding-information-output="/localnet/gateway.json"; + + echo "Waiting for network.json..."; + while [ ! -f /localnet/network.json ]; do + sleep 2; + done; + echo "Starting gateway..."; + exec nym-node run --id gateway-localnet --unsafe-disable-replay-protection --local --wireguard-enabled true --wireguard-userspace true --lp-use-mock-ecash true + ' + + log_success "$GATEWAY_CONTAINER started" + + # Wait for gateway to be ready + log_info "Waiting for gateway to listen on port 9000..." + local retries=0 + local max_retries=30 + while ! nc -z 127.0.0.1 9000 2>/dev/null; do + sleep 2 + retries=$((retries + 1)) + if [ $retries -ge $max_retries ]; then + log_error "Gateway failed to start on port 9000" + return 1 + fi + done + log_success "Gateway is ready on port 9000" +} +# Start network requester +start_network_requester() { + log_info "Starting $REQUESTER_CONTAINER..." + + # Get gateway IP address + log_info "Getting gateway IP address..." + GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i) + log_info "Gateway IP: $GATEWAY_IP" + + container run \ + --name "$REQUESTER_CONTAINER" \ + --dns 1.1.1.1 \ + --network "$NETWORK_NAME" \ + -v "$VOLUME_PATH:/localnet" \ + -v "$NYM_VOLUME_PATH:/root/.nym" \ + -e "GATEWAY_IP=$GATEWAY_IP" \ + -d \ + "$IMAGE_NAME" \ + sh -c ' + while [ ! -f /localnet/network.json ]; do + echo "Waiting for network.json..."; + sleep 2; + done; + while ! nc -z $GATEWAY_IP 9000 2>/dev/null; do + echo "Waiting for gateway on port 9000 ($GATEWAY_IP)..."; + sleep 2; + done; + SUFFIX=$(date +%s); + nym-network-requester init \ + --id "network-requester-$SUFFIX" \ + --open-proxy=true \ + --custom-mixnet /localnet/network.json \ + --output=json > /localnet/network_requester.json; + exec nym-network-requester run \ + --id "network-requester-$SUFFIX" \ + --custom-mixnet /localnet/network.json + ' + + log_success "$REQUESTER_CONTAINER started" +} + +# Start SOCKS5 client +start_socks5_client() { + log_info "Starting $SOCKS5_CONTAINER..." + + container run \ + --name "$SOCKS5_CONTAINER" \ + --dns 1.1.1.1 \ + --network "$NETWORK_NAME" \ + -p 1080:1080 \ + -v "$VOLUME_PATH:/localnet:ro" \ + -v "$NYM_VOLUME_PATH:/root/.nym" \ + -d \ + "$IMAGE_NAME" \ + sh -c ' + while [ ! -f /localnet/network_requester.json ]; do + echo "Waiting for network requester..."; + sleep 2; + done; + SUFFIX=$(date +%s); + PROVIDER=$(cat /localnet/network_requester.json | grep -o "\"client_address\":\"[^\"]*\"" | cut -d\" -f4); + if [ -z "$PROVIDER" ]; then + echo "Error: Could not extract provider address"; + exit 1; + fi; + nym-socks5-client init \ + --id "socks5-client-$SUFFIX" \ + --provider "$PROVIDER" \ + --custom-mixnet /localnet/network.json \ + --no-cover; + exec nym-socks5-client run \ + --id "socks5-client-$SUFFIX" \ + --custom-mixnet /localnet/network.json \ + --host 0.0.0.0 + ' + + log_success "$SOCKS5_CONTAINER started" + + # Wait for SOCKS5 to be ready + log_info "Waiting for SOCKS5 proxy on port 1080..." + sleep 5 + local retries=0 + local max_retries=15 + while ! nc -z 127.0.0.1 1080 2>/dev/null; do + sleep 2 + retries=$((retries + 1)) + if [ $retries -ge $max_retries ]; then + log_warn "SOCKS5 proxy not responding on port 1080 yet" + return 0 + fi + done + log_success "SOCKS5 proxy is ready on port 1080" +} + +# Stop all containers +stop_containers() { + log_info "Stopping all containers..." + + for container_name in "${ALL_CONTAINERS[@]}"; do + if container inspect "$container_name" &>/dev/null; then + log_info "Stopping $container_name" + container stop "$container_name" 2>/dev/null || true + container rm "$container_name" 2>/dev/null || true + fi + done + + # Also clean up init container if it exists + container rm "$INIT_CONTAINER" 2>/dev/null || true + + log_success "All containers stopped" + + cleanup_host_state + remove_network +} + +# Show container logs +show_logs() { + local container_name=${1:-} + + if [ -z "$container_name" ]; then + # No container specified - launch tmux log viewer + log_info "Launching tmux log viewer for all containers..." + exec "$SCRIPT_DIR/localnet-logs.sh" + fi + + # Show logs for specific container + if container inspect "$container_name" &>/dev/null; then + container logs -f "$container_name" + else + log_error "Container not found: $container_name" + log_info "Available containers:" + for name in "${ALL_CONTAINERS[@]}"; do + echo " - $name" + done + exit 1 + fi +} + +# Show container status +show_status() { + log_info "Container status:" + echo "" + + for container_name in "${ALL_CONTAINERS[@]}"; do + if container inspect "$container_name" &>/dev/null; then + local status=$(container inspect "$container_name" 2>/dev/null | grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || echo "unknown") + echo -e " ${GREEN}โ—${NC} $container_name - $status" + else + echo -e " ${RED}โ—‹${NC} $container_name - not running" + fi + done + + echo "" + log_info "Port status:" + for port in 9000 1080 10001 10002 10003 10004; do + if nc -z 127.0.0.1 $port 2>/dev/null; then + echo -e " ${GREEN}โ—${NC} Port $port - listening" + else + echo -e " ${RED}โ—‹${NC} Port $port - not listening" + fi + done +} + +# Build network topology with container IPs +build_topology() { + log_info "Building network topology with container IPs..." + + # Wait for all bonding JSON files to be created + log_info "Waiting for all nodes to complete initialization..." + for file in mix1.json mix2.json mix3.json gateway.json; do + while [ ! -f "$VOLUME_PATH/$file" ]; do + echo " Waiting for $file..." + sleep 1 + done + log_success " $file created" + done + + # Get container IPs + log_info "Getting container IP addresses..." + MIX1_IP=$(container exec "$MIXNODE1_CONTAINER" hostname -i) + MIX2_IP=$(container exec "$MIXNODE2_CONTAINER" hostname -i) + MIX3_IP=$(container exec "$MIXNODE3_CONTAINER" hostname -i) + GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i) + + log_info "Container IPs:" + echo " mix1: $MIX1_IP" + echo " mix2: $MIX2_IP" + echo " mix3: $MIX3_IP" + echo " gateway: $GATEWAY_IP" + + # Run build_topology.py in a container with access to the volumes + container run \ + --name "nym-localnet-topology-builder" \ + --dns 1.1.1.1 \ + --network "$NETWORK_NAME" \ + -v "$VOLUME_PATH:/localnet" \ + -v "$NYM_VOLUME_PATH:/root/.nym" \ + --rm \ + "$IMAGE_NAME" \ + python3 /usr/local/bin/build_topology.py \ + /localnet \ + "$SUFFIX" \ + "$MIX1_IP" \ + "$MIX2_IP" \ + "$MIX3_IP" \ + "$GATEWAY_IP" + + # Verify network.json was created + if [ -f "$VOLUME_PATH/network.json" ]; then + log_success "Network topology created successfully" + else + log_error "Failed to create network topology" + exit 1 + fi +} + +# Start all services +start_all() { + log_info "Starting Nym Localnet..." + + cleanup_host_state + create_network + create_volume + create_nym_volume + + start_mixnode 1 "$MIXNODE1_CONTAINER" + start_mixnode 2 "$MIXNODE2_CONTAINER" + start_mixnode 3 "$MIXNODE3_CONTAINER" + start_gateway + build_topology + start_network_requester + start_socks5_client + + echo "" + log_success "Nym Localnet is running!" + echo "" + echo "Test with:" + echo " curl -x socks5h://127.0.0.1:1080 https://nymtech.net" + echo "" + echo "View logs:" + echo " $0 logs # All containers in tmux" + echo " $0 logs gateway # Single container" + echo "" + echo "Stop:" + echo " $0 down" + echo "" +} + +# Main command handler +main() { + check_prerequisites + + local command=${1:-help} + shift || true + + case "$command" in + build) + build_image + ;; + up) + build_image + start_all + ;; + start) + start_all + ;; + down|stop) + stop_containers + remove_volume + ;; + restart) + stop_containers + start_all + ;; + logs) + show_logs "$@" + ;; + status|ps) + show_status + ;; + help|--help|-h) + cat < [options] + +Commands: + build Build the localnet image + up Build image and start all services + start Start all services (requires built image) + down, stop Stop all services and clean up + restart Restart all services + logs [name] Show logs (no args = tmux overlay, with name = single container) + status, ps Show status of all containers and ports + help Show this help message + +Examples: + $0 up # Build and start everything + $0 logs # View all logs in tmux overlay + $0 logs gateway # View gateway logs only + $0 status # Check what's running + $0 down # Stop and clean up + +EOF + ;; + *) + log_error "Unknown command: $command" + echo "Run '$0 help' for usage information" + exit 1 + ;; + esac +} + +main "$@" diff --git a/docker/localnet/nym-binaries-localnet.Dockerfile b/docker/localnet/nym-binaries-localnet.Dockerfile new file mode 100644 index 00000000000..df4cc29b5a1 --- /dev/null +++ b/docker/localnet/nym-binaries-localnet.Dockerfile @@ -0,0 +1,107 @@ +# Single-stage Dockerfile for Nym localnet +# Builds: nym-node, nym-nym-api +# Target: Apple Container Runtime with host networking + +# syntax=docker/dockerfile:1.4 +FROM rust:1.91.1 AS builder + +# Install runtime dependencies including Go for wireguard-go +#RUN apt update && apt install -y \ +# python3 \ +# python3-pip \ +# netcat-openbsd \ +# jq \ +# iproute2 \ +# net-tools \ +# wireguard-tools \ +# golang-go \ +# git \ +# && rm -rf /var/lib/apt/lists/* + +RUN apt update && apt install -y \ + iproute2 \ + iptables \ + netcat-openbsd \ + net-tools \ + wireguard-tools \ + golang-go \ + git \ + jq \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Install wireguard-go (userspace WireGuard implementation) +RUN git clone https://git.zx2c4.com/wireguard-go && \ + cd wireguard-go && \ + make && \ + cp wireguard-go /usr/local/bin/ && \ + cd .. && \ + rm -rf wireguard-go + +WORKDIR /usr/src/nym + +############################################################### +# 1. Copy only top-level manifests (always stable) +############################################################### +COPY Cargo.toml Cargo.lock ./ + +############################################################### +# 2. Copy *full workspace* into a temp folder +############################################################### +COPY . /tmp/fullsrc + + +############################################################### +# 3. Recreate directory structure for all workspace crates +# by finding Cargo.toml files inside the container (for dependency caching) +############################################################### +RUN set -eux; \ + cd /usr/src/nym; \ + mkdir -p /usr/src/nym; \ + # find every Cargo.toml except the top-level one \ + find /tmp/fullsrc -name Cargo.toml ! -path "/tmp/fullsrc/Cargo.toml" | while read path; do \ + rel="${path#/tmp/fullsrc/}"; \ + dir="$(dirname "$rel")"; \ + mkdir -p "$dir"; \ + cp "$path" "$dir/"; \ + done + +############################################################### +# 4. Dummy build (dependencies only) +############################################################### +RUN echo "fn main() {}" > dummy.rs + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/usr/src/nym/target \ + cargo build --release --locked || true + +############################################################### +# 5. Copy REAL workspace sources into place +############################################################### +RUN cp -a /tmp/fullsrc/. /usr/src/nym/ + +############################################################### +# 6. Final build +############################################################### +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/usr/src/nym/target \ + cargo build --release --locked \ + -p nym-node \ + -p nym-api \ + -p nym-gateway-probe + +# Move binaries to /usr/local/bin for easy access +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/usr/src/nym/target \ + mv /usr/src/nym/target/release/nym-node /usr/local/bin/ && \ + mv /usr/src/nym/target/release/nym-api /usr/local/bin/ && \ + mv /usr/src/nym/target/release/nym-gateway-probe /usr/local/bin/ + + +WORKDIR /nym + +# Default command +CMD ["nym-node", "--help"] diff --git a/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs index 136e1d27087..2591ab4f517 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs @@ -19,6 +19,7 @@ pub async fn create_mixnet_client( custom_transceiver: Option>, custom_topology_provider: Option>, wait_for_gateway: bool, + wait_for_topology: bool, paths: &CommonClientPaths, ) -> Result { let debug_config = config.debug; @@ -34,7 +35,8 @@ pub async fn create_mixnet_client( .network_details(NymNetworkDetails::new_from_env()) .debug_config(debug_config) .custom_shutdown(shutdown) - .with_wait_for_gateway(wait_for_gateway); + .with_wait_for_gateway(wait_for_gateway) + .with_wait_for_initial_topology(wait_for_topology); if !config.get_disabled_credentials_mode() { client_builder = client_builder.enable_credentials_mode(); } diff --git a/gateway/src/node/internal_service_providers/authenticator/mod.rs b/gateway/src/node/internal_service_providers/authenticator/mod.rs index f63a86fcc2d..bed29982c5c 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -37,6 +37,7 @@ pub struct Authenticator { config: Config, upgrade_mode_state: UpgradeModeDetails, wait_for_gateway: bool, + wait_for_topology: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, wireguard_gateway_data: WireguardGatewayData, @@ -59,6 +60,7 @@ impl Authenticator { config, upgrade_mode_state, wait_for_gateway: false, + wait_for_topology: false, custom_topology_provider: None, custom_gateway_transceiver: None, ecash_verifier, @@ -76,6 +78,13 @@ impl Authenticator { self } + #[must_use] + #[allow(unused)] + pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self { + self.wait_for_topology = wait_for_initial_topology; + self + } + #[must_use] pub fn with_minimum_gateway_performance(mut self, minimum_gateway_performance: u8) -> Self { self.config.base.debug.topology.minimum_gateway_performance = minimum_gateway_performance; @@ -128,6 +137,7 @@ impl Authenticator { self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, + self.wait_for_topology, &self.config.storage_paths.common_paths, ) .await?; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index ba891bd7165..99b546f7ac3 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -307,6 +307,7 @@ impl GatewayTasksBuilder { NRServiceProviderBuilder::new(nr_opts.config.clone(), self.shutdown_tracker.clone()) .with_custom_gateway_transceiver(transceiver) .with_wait_for_gateway(true) + .with_wait_for_initial_topology(true) .with_minimum_gateway_performance(0) .with_custom_topology_provider(topology_provider) .with_on_start(on_start_tx); @@ -342,6 +343,7 @@ impl GatewayTasksBuilder { IpPacketRouter::new(ip_opts.config.clone(), self.shutdown_tracker.clone()) .with_custom_gateway_transceiver(Box::new(transceiver)) .with_wait_for_gateway(true) + .with_wait_for_initial_topology(true) .with_minimum_gateway_performance(0) .with_custom_topology_provider(topology_provider) .with_on_start(on_start_tx); @@ -456,6 +458,7 @@ impl GatewayTasksBuilder { ) .with_custom_gateway_transceiver(transceiver) .with_wait_for_gateway(true) + .with_wait_for_initial_topology(true) .with_minimum_gateway_performance(0) .with_custom_topology_provider(topology_provider) .with_on_start(on_start_tx); @@ -562,6 +565,7 @@ impl GatewayTasksBuilder { wireguard_data.inner.config().announced_metadata_port, ); + let use_userspace = wireguard_data.use_userspace; let wg_handle = nym_wireguard::start_wireguard( ecash_manager, self.metrics.clone(), @@ -569,6 +573,7 @@ impl GatewayTasksBuilder { self.upgrade_mode_state.upgrade_mode_status(), self.shutdown_tracker.clone_shutdown_token(), wireguard_data, + use_userspace, ) .await?; diff --git a/nym-api/nym-api-requests/src/models/mod.rs b/nym-api/nym-api-requests/src/models/mod.rs index 6d7ab23f962..75d41012a97 100644 --- a/nym-api/nym-api-requests/src/models/mod.rs +++ b/nym-api/nym-api-requests/src/models/mod.rs @@ -17,6 +17,7 @@ pub mod network; pub mod network_monitor; pub mod node_status; pub mod schema_helpers; +pub mod utility; // don't break existing imports pub use api_status::*; diff --git a/nym-api/nym-api-requests/src/models/utility.rs b/nym-api/nym-api-requests/src/models/utility.rs new file mode 100644 index 00000000000..4ea1e5bc853 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/utility.rs @@ -0,0 +1,40 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct RefreshMixnetContractCacheRequestBody { + // for now no additional data is needed, but keep the struct for easier changes down the line +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct RefreshMixnetContractCacheResponse { + pub success: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct MixnetContractCacheTimestampResponse { + #[serde(with = "time::serde::rfc3339")] + #[schema(value_type = String)] + pub timestamp: OffsetDateTime, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct RefreshNodeStatusCacheRequestBody { + // for now no additional data is needed, but keep the struct for easier changes down the line +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct RefreshNodeStatusCacheResponse { + pub success: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct NodeStatusCacheTimestampResponse { + #[serde(with = "time::serde::rfc3339")] + #[schema(value_type = String)] + pub timestamp: OffsetDateTime, +} diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index a9cf2e87f8d..047342f6b35 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1285,7 +1285,7 @@ impl TestFixture { address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000), forced_refresh: ForcedRefresh::new(true), mixnet_contract_cache: MixnetContractCache::new(), - node_status_cache: NodeStatusCache::new(), + node_annotations_cache: NodeStatusCache::new(), storage, described_nodes_cache: SharedCache::::new(), network_details: NetworkDetails::new( diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index 5f1e5a2f6b4..a353dc0d29f 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::EpochAdvancer; -use crate::support::caching::Cache; +use crate::support::caching::cache::UninitialisedCache; use cosmwasm_std::{Decimal, Fraction}; use nym_api_requests::models::NodeAnnotation; use nym_mixnet_contract_common::helpers::IntoBaseDecimal; @@ -210,10 +210,13 @@ fn determine_per_node_work( impl EpochAdvancer { fn load_performance( - status_cache: &Option>>>, + status_cache: &Result< + RwLockReadGuard<'_, HashMap>, + UninitialisedCache, + >, node_id: NodeId, ) -> NodeWithPerformance { - let Some(status_cache) = status_cache.as_ref() else { + let Ok(status_cache) = status_cache.as_ref() else { return NodeWithPerformance::new_zero(node_id); }; @@ -239,7 +242,7 @@ impl EpochAdvancer { let standby_node_work_factor = nodes_work.standby; let status_cache = self.status_cache.node_annotations().await; - if status_cache.is_none() { + if status_cache.is_err() { error!("there are no node annotations available"); }; diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index d513579ef2c..a1f315da697 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -214,7 +214,7 @@ impl EpochAdvancer { #[allow(clippy::unwrap_used)] let described_cache = self.described_cache.get().await.unwrap(); - let Some(status_cache) = self.status_cache.node_annotations().await else { + let Ok(status_cache) = self.status_cache.node_annotations().await else { warn!("there are no node annotations available"); return Vec::new(); }; diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 96c7cb22d77..b2f52320e06 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -24,6 +24,7 @@ mod signers_cache; mod status; pub(crate) mod support; mod unstable_routes; +pub(crate) mod utility_routes; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 074a4f612f7..00219a883df 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -174,6 +174,33 @@ impl PacketPreparer { layer_choices.choose(rng).copied().unwrap() } + fn naive_rearrange( + &self, + nodes: HashMap>, + ) -> HashMap> { + let all_nodes = nodes + .into_values() + .flat_map(|nodes| nodes.into_iter()) + .collect::>(); + + let mut layered = HashMap::new(); + + for (i, node) in all_nodes.into_iter().enumerate() { + let layer = match i % 3 { + 0 => LegacyMixLayer::One, + 1 => LegacyMixLayer::Two, + 2 => LegacyMixLayer::Three, + // this is literally impossible to reach + #[allow(clippy::unreachable)] + _ => unreachable!(), + }; + let layer_mixes = layered.entry(layer).or_insert_with(Vec::new); + layer_mixes.push(node) + } + + layered + } + fn to_legacy_layered_mixes<'a, R: Rng>( &self, rng: &mut R, @@ -199,7 +226,20 @@ impl PacketPreparer { layer_mixes.push((parsed_node, weight)) } - layered_mixes + // if some layers are empty, fallback to naive assignment + // (for small localnets/testnets) + let layers = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + + if layers.into_iter().any(|l| layered_mixes.get(&l).is_none()) { + info!("insufficient number of nodes on layers - attempting to fallback to naive assignment"); + self.naive_rearrange(layered_mixes) + } else { + layered_mixes + } } fn to_legacy_gateway_nodes<'a>( @@ -233,7 +273,7 @@ impl PacketPreparer { // routes so that they wouldn't be reused pub(crate) async fn prepare_test_routes(&self, n: usize) -> Option> { let descriptions = self.described_cache.get().await.ok()?; - let statuses = self.node_status_cache.node_annotations().await?; + let statuses = self.node_status_cache.node_annotations().await.ok()?; let mixing_nym_nodes = descriptions.mixing_nym_nodes(); // last I checked `gatewaying` wasn't a word : ) diff --git a/nym-api/src/node_status_api/cache/data.rs b/nym-api/src/node_status_api/cache/data.rs index 1a174eae6b8..c13e0ad05af 100644 --- a/nym-api/src/node_status_api/cache/data.rs +++ b/nym-api/src/node_status_api/cache/data.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::support::caching::Cache; use nym_api_requests::models::NodeAnnotation; use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; @@ -10,11 +9,11 @@ use std::collections::HashMap; #[allow(deprecated)] pub(crate) struct NodeStatusCacheData { /// Basic annotation for nym-nodes - pub(crate) node_annotations: Cache>, + pub(crate) node_annotations: HashMap, } -impl NodeStatusCacheData { - pub fn new() -> Self { - Self::default() +impl From> for NodeStatusCacheData { + fn from(node_annotations: HashMap) -> Self { + NodeStatusCacheData { node_annotations } } } diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 32fd552f18d..21c073247e4 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -3,19 +3,16 @@ use self::data::NodeStatusCacheData; use crate::node_performance::provider::PerformanceRetrievalFailure; -use crate::support::caching::cache::UninitialisedCache; +use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::Cache; use nym_api_requests::models::NodeAnnotation; use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; -use std::{sync::Arc, time::Duration}; use thiserror::Error; +use time::OffsetDateTime; use tokio::sync::RwLockReadGuard; -use tokio::{sync::RwLock, time}; use tracing::error; -const CACHE_TIMEOUT_MS: u64 = 100; - mod config_score; pub mod data; pub mod refresher; @@ -44,43 +41,48 @@ impl From for NodeStatusCacheError { /// The cache can be triggered to update on contract cache changes, and/or periodically on a timer. #[derive(Clone)] pub struct NodeStatusCache { - inner: Arc>, + inner: SharedCache, } impl NodeStatusCache { /// Creates a new cache with no data. pub(crate) fn new() -> NodeStatusCache { NodeStatusCache { - inner: Arc::new(RwLock::new(NodeStatusCacheData::new())), + inner: SharedCache::new_with_value(HashMap::new().into()), } } + pub async fn cache_timestamp(&self) -> OffsetDateTime { + let Ok(cache) = self.inner.get().await else { + return OffsetDateTime::UNIX_EPOCH; + }; + + cache.timestamp() + } + /// Updates the cache with the latest data. async fn update(&self, node_annotations: HashMap) { - match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.write()).await { - Ok(mut cache) => { - cache.node_annotations.unchecked_update(node_annotations); - } - Err(e) => error!("{e}"), + if self + .inner + .try_overwrite_old_value(node_annotations, "node-status") + .await + .is_err() + { + error!("failed to update node status cache!") } } async fn get<'a, T: 'a>( &'a self, - fn_arg: impl FnOnce(&NodeStatusCacheData) -> &Cache, - ) -> Option>> { - match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { - Ok(cache) => Some(RwLockReadGuard::map(cache, |item| fn_arg(item))), - Err(e) => { - error!("{e}"); - None - } - } + fn_arg: impl FnOnce(&Cache) -> &T, + ) -> Result, UninitialisedCache> { + let guard = self.inner.get().await?; + Ok(RwLockReadGuard::map(guard, fn_arg)) } pub(crate) async fn node_annotations( &self, - ) -> Option>>> { + ) -> Result>, UninitialisedCache> { self.get(|c| &c.node_annotations).await } } diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index 0da2748846e..b1d93b78236 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -8,6 +8,7 @@ use crate::node_performance::provider::{NodePerformanceProvider, NodesRoutingSco use crate::node_status_api::cache::config_score::calculate_config_score; use crate::node_status_api::models::Uptime; use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::RefreshRequester; use crate::{ mixnet_contract_cache::cache::MixnetContractCache, node_status_api::cache::NodeStatusCacheError, support::caching::CacheNotification, @@ -32,9 +33,18 @@ pub struct NodeStatusCacheRefresher { // Sources for when refreshing data mixnet_contract_cache: MixnetContractCache, described_cache: SharedCache, + + /// channel notifying us when mixnet cache has been refreshed, + /// so that this cache could also be recreated mixnet_contract_cache_listener: watch::Receiver, + + /// channel notifying us when the describe cache has been refreshed, + /// so that this cache could also be recreated describe_cache_listener: watch::Receiver, + /// channel explicitly requesting cache refresh. it does not follow the usual rate limiting + refresh_requester: RefreshRequester, + performance_provider: Box, } @@ -55,10 +65,15 @@ impl NodeStatusCacheRefresher { described_cache, mixnet_contract_cache_listener: contract_cache_listener, describe_cache_listener, + refresh_requester: Default::default(), performance_provider, } } + pub(crate) fn refresh_requester(&self) -> RefreshRequester { + self.refresh_requester.clone() + } + /// Runs the node status cache refresher task. pub async fn run(&mut self, shutdown_token: ShutdownToken) { let mut last_update = OffsetDateTime::now_utc(); @@ -89,6 +104,23 @@ impl NodeStatusCacheRefresher { } } } + // note: `Notify` is not cancellation safe, HOWEVER, there's only one listener, + // so it doesn't matter if we lose our queue position + _ = self.refresh_requester.notified() => { + tokio::select! { + // perform full refresh regardless of the rates + _ = self.refresh() => { + last_update = OffsetDateTime::now_utc(); + fallback_interval.reset(); + }, + _ = shutdown_token.cancelled() => { + trace!("NodeStatusCacheRefresher: Received shutdown"); + break; + } + } + } + + // ... however, if we don't receive any notifications we fall back to periodic // refreshes _ = fallback_interval.tick() => { diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 7820d9a9536..140d09603f6 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -5,6 +5,7 @@ use self::cache::refresher::NodeStatusCacheRefresher; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_performance::provider::NodePerformanceProvider; use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::RefreshRequester; use crate::support::config; use crate::{ mixnet_contract_cache::cache::MixnetContractCache, @@ -40,7 +41,7 @@ pub(crate) fn start_cache_refresh( nym_contract_cache_listener: watch::Receiver, described_cache_cache_listener: watch::Receiver, shutdown_manager: &ShutdownManager, -) { +) -> RefreshRequester { let mut nym_api_cache_refresher = NodeStatusCacheRefresher::new( node_status_cache_state.to_owned(), config.debug.caching_interval, @@ -50,6 +51,8 @@ pub(crate) fn start_cache_refresh( described_cache_cache_listener, performance_provider, ); + let refresh_requester = nym_api_cache_refresher.refresh_requester(); let shutdown_listener = shutdown_manager.clone_shutdown_token(); tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await }); + refresh_requester } diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 76a9d0199ba..2ddda00b378 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -346,13 +346,6 @@ impl AxumErrorResponse { } } - pub(crate) fn internal() -> Self { - Self { - message: RequestError::new("Internal server error"), - status: StatusCode::INTERNAL_SERVER_ERROR, - } - } - pub(crate) fn not_implemented() -> Self { Self { message: RequestError::empty(), diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 954349c03a3..0c7e8ed922e 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -272,11 +272,7 @@ async fn get_node_annotation( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let annotations = state - .node_status_cache - .node_annotations() - .await - .ok_or_else(AxumErrorResponse::internal)?; + let annotations = state.node_status_cache().node_annotations().await?; Ok(output.to_response(AnnotationResponse { node_id, @@ -305,11 +301,7 @@ async fn get_current_node_performance( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let annotations = state - .node_status_cache - .node_annotations() - .await - .ok_or_else(AxumErrorResponse::internal)?; + let annotations = state.node_status_cache().node_annotations().await?; Ok(output.to_response(NodePerformanceResponse { node_id, diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 2b21397c891..3883e1a8bad 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -7,6 +7,7 @@ use async_trait::async_trait; use nym_task::ShutdownToken; use std::sync::Arc; use std::time::Duration; +use tokio::sync::futures::Notified; use tokio::sync::{watch, Notify}; use tokio::time::interval; use tracing::{debug, error, info, trace, warn}; @@ -20,6 +21,10 @@ impl RefreshRequester { pub(crate) fn request_cache_refresh(&self) { self.0.notify_waiters() } + + pub(crate) fn notified(&self) -> Notified<'_> { + self.0.notified() + } } impl Default for RefreshRequester { @@ -249,7 +254,7 @@ where _ = refresh_interval.tick() => self.refresh(&shutdown_token).await, // note: `Notify` is not cancellation safe, HOWEVER, there's only one listener, // so it doesn't matter if we lose our queue position - _ = self.refresh_requester.0.notified() => { + _ = self.refresh_requester.notified() => { self.refresh(&shutdown_token).await; // since we just performed the full request, we can reset our existing interval refresh_interval.reset(); diff --git a/nym-api/src/support/cli/init.rs b/nym-api/src/support/cli/init.rs index d93e90545c0..01be826603d 100644 --- a/nym-api/src/support/cli/init.rs +++ b/nym-api/src/support/cli/init.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::support::cli::run::initialise_storage; use crate::support::config::default_config_filepath; use crate::support::config::helpers::initialise_new; use anyhow::bail; @@ -71,8 +72,10 @@ pub(crate) struct Args { #[clap(hide = true, long, default_value_t = false)] pub(crate) allow_illegal_ips: bool, - // #[clap(short, long, default_value_t = OutputFormat::default())] - // output: OutputFormat, + + /// Bearer token for exposing and accessing additional utility routes + #[clap(long, env = "NYMAPI_UTILITY_ROUTES_BEARER_ARG")] + pub(crate) utility_routes_bearer: Option, } pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { @@ -88,12 +91,15 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { bail!("there already exists a configuration file at '{}'. If you intend to replace it, you need to manually remove it first. Make sure to make backup of any keys and datastores first.", config_path.display()) } - let config = initialise_new(&args.id)?; // args take precedence over env - config + let config = initialise_new(&args.id)? .override_with_env() - .override_with_args(args) - .try_save()?; + .override_with_args(args); + + config.try_save()?; + + // create the initial database file + initialise_storage(&config).await?; Ok(()) } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 6c8750149ad..cff0f328461 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -26,6 +26,8 @@ use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL}; use crate::support::http::state::chain_status::ChainStatusCache; use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; +use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState; +use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache; use crate::support::http::state::AppState; use crate::support::http::{RouterBuilder, TASK_MANAGER_TIMEOUT_S}; use crate::support::nyxd; @@ -117,20 +119,33 @@ pub(crate) struct Args { #[clap(hide = true, long, default_value_t = false)] pub(crate) allow_illegal_ips: bool, + + /// Bearer token for exposing and accessing additional utility routes + #[clap(long, env = "NYMAPI_UTILITY_ROUTES_BEARER_ARG")] + pub(crate) utility_routes_bearer: Option, +} + +pub(crate) async fn initialise_storage(config: &Config) -> anyhow::Result { + let nyxd_client = nyxd::Client::new(config)?; + let storage = NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; + + // try to perform any needed migrations of the storage + migrate_to_directory_services_v2_1(&storage, &nyxd_client).await?; + Ok(storage) } -async fn start_nym_api_tasks(config: &Config) -> anyhow::Result { +async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result { let shutdown_manager = ShutdownManager::build_new_default()? .with_shutdown_duration(Duration::from_secs(TASK_MANAGER_TIMEOUT_S)); - let nyxd_client = nyxd::Client::new(config)?; + let nyxd_client = nyxd::Client::new(&config)?; let connected_nyxd = config.get_nyxd_url(); let nym_network_details = NymNetworkDetails::new_from_env(); let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); let ecash_keypair_wrapper = ecash::keys::KeyPair::new(); - // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen + // if the keypair doesn't exist (because say this API is running in the caching mode), nothing will happen if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.ecash_signer)? { let issued_for = loaded_keys.issued_for_epoch; ecash_keypair_wrapper.set(loaded_keys).await; @@ -140,16 +155,11 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result } } - let storage = NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; - - // try to perform any needed migrations of the storage - migrate_to_directory_services_v2_1(&storage, &nyxd_client).await?; + let storage = initialise_storage(&config).await?; let identity_keypair = config.base.storage_paths.load_identity()?; let identity_public_key = *identity_keypair.public_key(); - let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - let mixnet_contract_cache_state = MixnetContractCache::new(); let node_status_cache_state = NodeStatusCache::new(); let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); @@ -173,7 +183,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result let encoded_identity = identity_keypair.public_key().to_base58_string(); let mut ecash_state = EcashState::new( - config, + &config, ecash_contract, nyxd_client.clone(), identity_keypair, @@ -223,25 +233,6 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result }; ecash_state.spawn_background_cleaner(); - let router = router.with_state(AppState { - nyxd_client: nyxd_client.clone(), - chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL), - ecash_signers_cache, - address_info_cache: AddressInfoCache::new( - config.address_cache.time_to_live, - config.address_cache.capacity, - ), - forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips), - mixnet_contract_cache: mixnet_contract_cache_state.clone(), - node_status_cache: node_status_cache_state.clone(), - storage: storage.clone(), - described_nodes_cache: described_nodes_cache.clone(), - network_details: network_details.clone(), - node_info_cache, - contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live), - api_status: ApiStatusState::new(signer_information), - ecash_state: Arc::new(ecash_state), - }); // start note describe cache refresher // we should be doing the below, but can't due to our current startup structure @@ -292,7 +283,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result let contract_cache_watcher = mixnet_contract_cache_refresher.start_with_watcher(shutdown_manager.clone_shutdown_token()); - node_status_api::start_cache_refresh( + let node_status_cache_refresh_requester = node_status_api::start_cache_refresh( &config.node_status_api, &mixnet_contract_cache_state, &described_nodes_cache, @@ -325,7 +316,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result // if the monitoring is enabled if config.network_monitor.enabled { network_monitor::start::( - config, + &config, &mixnet_contract_cache_state, described_nodes_cache.clone(), node_status_cache_state.clone(), @@ -342,9 +333,9 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result if config.rewarding.enabled && has_performance_data { epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; EpochAdvancer::start( - nyxd_client, + nyxd_client.clone(), &mixnet_contract_cache_state, - mixnet_contract_cache_refresh_requester, + mixnet_contract_cache_refresh_requester.clone(), &node_status_cache_state, described_nodes_cache.clone(), &storage, @@ -357,10 +348,41 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result KeyRotationController::new( describe_cache_refresh_requester, contract_cache_watcher, - mixnet_contract_cache_state, + mixnet_contract_cache_state.clone(), ) .start(shutdown_manager.clone_shutdown_token()); + let mixnet_contract_cache = MixnetContractCacheState::new( + mixnet_contract_cache_state, + mixnet_contract_cache_refresh_requester, + ); + let node_annotations_cache = + NodeAnnotationsCache::new(node_status_cache_state, node_status_cache_refresh_requester); + + let router = RouterBuilder::with_default_routes( + config.network_monitor.enabled, + config.base.utility_routes_bearer.take(), + ) + .with_state(AppState { + nyxd_client, + chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL), + ecash_signers_cache, + address_info_cache: AddressInfoCache::new( + config.address_cache.time_to_live, + config.address_cache.capacity, + ), + forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips), + mixnet_contract_cache, + node_annotations_cache, + storage, + described_nodes_cache, + network_details, + node_info_cache, + contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live), + api_status: ApiStatusState::new(signer_information), + ecash_state: Arc::new(ecash_state), + }); + let bind_address = config.base.bind_address.to_owned(); let server = router.build_server(&bind_address).await?; @@ -385,7 +407,7 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - let mut shutdown_manager = start_nym_api_tasks(&config).await?; + let mut shutdown_manager = start_nym_api_tasks(config).await?; shutdown_manager.run_until_shutdown().await; Ok(()) diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 6bea4240b31..d9ac4250bb1 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -194,6 +194,9 @@ impl Config { if let Some(nyxd_upstream) = args.nyxd_validator { self.base.local_validator = nyxd_upstream; } + if let Some(bearer) = args.utility_routes_bearer { + self.base.utility_routes_bearer = Some(bearer) + } if let Some(mnemonic) = args.mnemonic { self.base.mnemonic = Some(mnemonic) } @@ -301,6 +304,11 @@ pub struct Base { #[serde(default = "default_http_socket_addr")] pub bind_address: SocketAddr, + /// Bearer token for exposing and accessing additional utility routes + #[serde(default)] + #[serde(deserialize_with = "de_maybe_stringified")] + pub utility_routes_bearer: Option, + /// Mnemonic used for rewarding and/or multisig operations // TODO: similarly to the note in gateway, this should get moved to a separate file #[serde(deserialize_with = "de_maybe_stringified")] @@ -326,6 +334,7 @@ impl Base { id, local_validator: default_validator, bind_address: default_http_socket_addr(), + utility_routes_bearer: None, mnemonic: None, } } diff --git a/nym-api/src/support/config/override.rs b/nym-api/src/support/config/override.rs index 4f8b96db313..d755082f6ff 100644 --- a/nym-api/src/support/config/override.rs +++ b/nym-api/src/support/config/override.rs @@ -15,6 +15,9 @@ pub(crate) struct OverrideConfig { /// Endpoint to nyxd instance used for contract information. pub(crate) nyxd_validator: Option, + /// Bearer token for exposing and accessing additional utility routes + pub(crate) utility_routes_bearer: Option, + /// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions pub(crate) mnemonic: Option, @@ -33,6 +36,7 @@ pub(crate) struct OverrideConfig { pub(crate) bind_address: Option, pub(crate) address_cache_ttl: Option, + pub(crate) address_cache_capacity: Option, pub(crate) allow_illegal_ips: bool, @@ -44,6 +48,7 @@ impl From for OverrideConfig { enable_monitor: Some(args.enable_monitor), enable_rewarding: Some(args.enable_rewarding), nyxd_validator: args.nyxd_validator, + utility_routes_bearer: args.utility_routes_bearer, mnemonic: args.mnemonic, enable_zk_nym: Some(args.enable_zk_nym), announce_address: args.announce_address, @@ -63,6 +68,7 @@ impl From for OverrideConfig { enable_monitor: args.enable_monitor, enable_rewarding: args.enable_rewarding, nyxd_validator: args.nyxd_validator, + utility_routes_bearer: args.utility_routes_bearer, mnemonic: args.mnemonic, enable_zk_nym: args.enable_zk_nym, announce_address: args.announce_address, diff --git a/nym-api/src/support/config/template.rs b/nym-api/src/support/config/template.rs index ab947530474..a316b3e1414 100644 --- a/nym-api/src/support/config/template.rs +++ b/nym-api/src/support/config/template.rs @@ -18,6 +18,9 @@ local_validator = '{{ base.local_validator }}' # Socket address this api will use for binding its http API. bind_address = '{{ base.bind_address }}' +# Bearer token for exposing and accessing additional utility routes +utility_routes_bearer = '{{ base.utility_routes_bearer }}' + # Mnemonic used for rewarding and validator interaction mnemonic = '{{ base.mnemonic }}' diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 56a89509680..e9b7bbf2972 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -12,17 +12,21 @@ use crate::support::http::openapi::ApiDoc; use crate::support::http::state::AppState; use crate::unstable_routes::v1::unstable_routes_v1; use crate::unstable_routes::v2::unstable_routes_v2; +use crate::utility_routes::utility_routes; use anyhow::anyhow; use axum::response::Redirect; use axum::routing::get; use axum::Router; use core::net::SocketAddr; +use nym_http_api_common::middleware::bearer_auth::AuthLayer; use nym_http_api_common::middleware::logging::log_request_info; use nym_task::ShutdownToken; +use std::sync::Arc; use tokio::net::TcpListener; use tower_http::cors::CorsLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use zeroize::Zeroizing; /// Wrapper around `axum::Router` which ensures correct [order of layers][order]. /// Add new routes as if you were working directly with `axum`. @@ -36,9 +40,34 @@ pub(crate) struct RouterBuilder { } impl RouterBuilder { + fn v1_routes(network_monitor: bool, bearer_token: Option) -> Router { + let base = Router::new() + // unfortunately some routes didn't use correct prefix and were attached to the root + .nest("/epoch", epoch_routes()) + .nest("/circulating-supply", circulating_supply_routes()) + .nest("/status", status_routes(network_monitor)) + .nest("/network", nym_network_routes()) + .nest("/api-status", status::handlers::api_status_routes()) + .nest("/nym-nodes", nym_node_routes()) + .nest("/ecash", ecash_routes()) + .nest("/unstable", unstable_routes_v1()) + .nest("/legacy", legacy_nodes_routes()); // CORS layer needs to be "outside" of routes + + if let Some(bearer_token) = bearer_token { + let auth_middleware = AuthLayer::new(Arc::new(Zeroizing::new(bearer_token))); + base.nest("/utility", utility_routes().route_layer(auth_middleware)) + } else { + base + } + } + + fn v2_routes() -> Router { + Router::new().nest("/unstable", unstable_routes_v2()) + } + /// All routes should be, if possible, added here. Exceptions are e.g. /// routes which are added conditionally in other places based on some `if`. - pub(crate) fn with_default_routes(network_monitor: bool) -> Self { + pub(crate) fn with_default_routes(network_monitor: bool, bearer_token: Option) -> Self { // https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html // TODO rocket use tracing instead of env_logger // https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs @@ -52,21 +81,8 @@ impl RouterBuilder { let default_routes = Router::new() .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) .route("/", get(|| async { Redirect::to("/swagger") })) - .nest( - "/v1", - Router::new() - // unfortunately some routes didn't use correct prefix and were attached to the root - .nest("/epoch", epoch_routes()) - .nest("/circulating-supply", circulating_supply_routes()) - .nest("/status", status_routes(network_monitor)) - .nest("/network", nym_network_routes()) - .nest("/api-status", status::handlers::api_status_routes()) - .nest("/nym-nodes", nym_node_routes()) - .nest("/ecash", ecash_routes()) - .nest("/unstable", unstable_routes_v1()) - .nest("/legacy", legacy_nodes_routes()), // CORS layer needs to be "outside" of routes - ) - .nest("/v2", Router::new().nest("/unstable", unstable_routes_v2())); + .nest("/v1", Self::v1_routes(network_monitor, bearer_token)) + .nest("/v2", Self::v2_routes()); Self { unfinished_router: default_routes, diff --git a/nym-api/src/support/http/state/helpers.rs b/nym-api/src/support/http/state/helpers.rs new file mode 100644 index 00000000000..07581ae53d0 --- /dev/null +++ b/nym-api/src/support/http/state/helpers.rs @@ -0,0 +1,38 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::caching::refresher::RefreshRequester; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::Arc; +use time::OffsetDateTime; + +#[derive(Clone)] +pub(crate) struct Refreshing { + handle: RefreshRequester, + last_requested: Arc, // unix timestamp +} + +impl Refreshing { + pub(crate) fn new(handle: RefreshRequester) -> Self { + Refreshing { + handle, + last_requested: Arc::new(Default::default()), + } + } + + pub(crate) fn last_requested(&self) -> OffsetDateTime { + // SAFETY: this value is always populated with valid timestamps + #[allow(clippy::unwrap_used)] + OffsetDateTime::from_unix_timestamp(self.last_requested.load(Ordering::SeqCst)).unwrap() + } + + fn update_last_requested(&self, now: OffsetDateTime) { + self.last_requested + .store(now.unix_timestamp(), Ordering::SeqCst); + } + + pub(crate) fn request_refresh(&self, now: OffsetDateTime) { + self.update_last_requested(now); + self.handle.request_cache_refresh(); + } +} diff --git a/nym-api/src/support/http/state/mixnet_contract_cache.rs b/nym-api/src/support/http/state/mixnet_contract_cache.rs new file mode 100644 index 00000000000..658abb5f79f --- /dev/null +++ b/nym-api/src/support/http/state/mixnet_contract_cache.rs @@ -0,0 +1,21 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::support::caching::refresher::RefreshRequester; +use crate::support::http::state::helpers::Refreshing; + +#[derive(Clone)] +pub(crate) struct MixnetContractCacheState { + pub(crate) inner_cache: MixnetContractCache, + pub(crate) refresh_handle: Refreshing, +} + +impl MixnetContractCacheState { + pub(crate) fn new(inner_cache: MixnetContractCache, refresh_handle: RefreshRequester) -> Self { + MixnetContractCacheState { + inner_cache, + refresh_handle: Refreshing::new(refresh_handle), + } + } +} diff --git a/nym-api/src/support/http/state/mod.rs b/nym-api/src/support/http/state/mod.rs index f0ac2d121bc..35c2416f857 100644 --- a/nym-api/src/support/http/state/mod.rs +++ b/nym-api/src/support/http/state/mod.rs @@ -15,22 +15,24 @@ use crate::support::caching::Cache; use crate::support::http::state::chain_status::ChainStatusCache; use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; +use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState; +use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache; use crate::support::nyxd::Client; use crate::support::storage; use crate::unstable_routes::v1::account::cache::AddressInfoCache; use crate::unstable_routes::v1::account::models::NyxAccountDetails; use axum::extract::FromRef; -use nym_api_requests::models::NodeAnnotation; use nym_crypto::asymmetric::ed25519; -use nym_mixnet_contract_common::NodeId; use nym_topology::CachedEpochRewardedSet; -use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLockReadGuard; pub(crate) mod chain_status; pub(crate) mod contract_details; pub(crate) mod force_refresh; +pub(crate) mod helpers; +pub(crate) mod mixnet_contract_cache; +pub(crate) mod node_annotations_cache; #[derive(Clone)] pub(crate) struct AppState { @@ -55,11 +57,11 @@ pub(crate) struct AppState { pub(crate) forced_refresh: ForcedRefresh, /// Holds cached state of the Nym Mixnet contract, e.g. bonded nym-nodes, rewarded set, current interval. - pub(crate) mixnet_contract_cache: MixnetContractCache, + pub(crate) mixnet_contract_cache: MixnetContractCacheState, /// Holds processed information on network nodes, i.e. performance, config scores, etc. // TODO: also perhaps redundant? - pub(crate) node_status_cache: NodeStatusCache, + pub(crate) node_annotations_cache: NodeAnnotationsCache, /// Holds reference to the persistent storage of this nym-api. pub(crate) storage: storage::NymApiStorage, @@ -101,11 +103,29 @@ impl FromRef for Arc { } impl FromRef for MixnetContractCache { + fn from_ref(app_state: &AppState) -> Self { + app_state.mixnet_contract_cache.inner_cache.clone() + } +} + +impl FromRef for MixnetContractCacheState { fn from_ref(app_state: &AppState) -> Self { app_state.mixnet_contract_cache.clone() } } +impl FromRef for NodeAnnotationsCache { + fn from_ref(app_state: &AppState) -> Self { + app_state.node_annotations_cache.clone() + } +} + +impl FromRef for NodeStatusCache { + fn from_ref(app_state: &AppState) -> Self { + app_state.node_annotations_cache.inner_cache.clone() + } +} + impl FromRef for SharedCache { fn from_ref(app_state: &AppState) -> Self { app_state.ecash_signers_cache.clone() @@ -120,11 +140,11 @@ impl AppState { } pub(crate) fn nym_contract_cache(&self) -> &MixnetContractCache { - &self.mixnet_contract_cache + &self.mixnet_contract_cache.inner_cache } pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { - &self.node_status_cache + &self.node_annotations_cache.inner_cache } pub(crate) fn network_details(&self) -> &NetworkDetails { @@ -158,16 +178,6 @@ impl AppState { Ok(self.nym_contract_cache().cached_rewarded_set().await?) } - pub(crate) async fn node_annotations( - &self, - ) -> Result>>, AxumErrorResponse> - { - self.node_status_cache() - .node_annotations() - .await - .ok_or_else(AxumErrorResponse::internal) - } - pub(crate) async fn get_address_info( self, account_id: nym_validator_client::nyxd::AccountId, @@ -186,7 +196,7 @@ impl AppState { .address_info_cache .collect_balances( self.nyxd_client.clone(), - self.mixnet_contract_cache.clone(), + self.mixnet_contract_cache.inner_cache.clone(), self.network_details() .network .chain_details diff --git a/nym-api/src/support/http/state/node_annotations_cache.rs b/nym-api/src/support/http/state/node_annotations_cache.rs new file mode 100644 index 00000000000..ff731821b1d --- /dev/null +++ b/nym-api/src/support/http/state/node_annotations_cache.rs @@ -0,0 +1,21 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::NodeStatusCache; +use crate::support::caching::refresher::RefreshRequester; +use crate::support::http::state::helpers::Refreshing; + +#[derive(Clone)] +pub(crate) struct NodeAnnotationsCache { + pub(crate) inner_cache: NodeStatusCache, + pub(crate) refresh_handle: Refreshing, +} + +impl NodeAnnotationsCache { + pub(crate) fn new(inner_cache: NodeStatusCache, refresh_handle: RefreshRequester) -> Self { + NodeAnnotationsCache { + inner_cache, + refresh_handle: Refreshing::new(refresh_handle), + } + } +} diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs index d86003c091a..3f1cd33e3fa 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs @@ -90,7 +90,8 @@ pub(super) async fn nodes_expanded( let describe_cache = state.describe_nodes_cache_data().await?; let all_nym_nodes = describe_cache.all_nym_nodes(); - let annotations = state.node_annotations().await?; + let status_cache = &state.node_status_cache(); + let annotations = status_cache.node_annotations().await?; let contract_cache = state.nym_contract_cache(); let current_key_rotation = contract_cache.current_key_rotation_id().await?; @@ -107,7 +108,7 @@ pub(super) async fn nodes_expanded( // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), - annotations.timestamp(), + status_cache.cache_timestamp().await, describe_cache.timestamp(), ]); diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs index 56f96198037..ead99f73908 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs @@ -101,7 +101,8 @@ where let rewarded_set = state.rewarded_set().await?; // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; + let status_cache = &state.node_status_cache(); + let annotations = status_cache.node_annotations().await?; // 3. implicitly grab the relevant described nodes // (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler) @@ -138,7 +139,7 @@ where // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), - annotations.timestamp(), + status_cache.cache_timestamp().await, describe_cache.timestamp(), ]); @@ -155,7 +156,7 @@ where // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), - annotations.timestamp(), + status_cache.cache_timestamp().await, describe_cache.timestamp(), ]); @@ -183,7 +184,8 @@ pub(crate) async fn nodes_basic( let describe_cache = state.describe_nodes_cache_data().await?; let all_nym_nodes = describe_cache.all_nym_nodes(); - let annotations = state.node_annotations().await?; + let status_cache = &state.node_status_cache(); + let annotations = status_cache.node_annotations().await?; let interval = state.nym_contract_cache().current_interval().await?; let current_key_rotation = state.nym_contract_cache().current_key_rotation_id().await?; @@ -199,7 +201,7 @@ pub(crate) async fn nodes_basic( // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), - annotations.timestamp(), + status_cache.cache_timestamp().await, describe_cache.timestamp(), ]); diff --git a/nym-api/src/utility_routes.rs b/nym-api/src/utility_routes.rs new file mode 100644 index 00000000000..71f5e28d706 --- /dev/null +++ b/nym-api/src/utility_routes.rs @@ -0,0 +1,170 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::node_status_api::NodeStatusCache; +use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState; +use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::routing::{get, post}; +use axum::Router; +use nym_api_requests::models::utility::{ + MixnetContractCacheTimestampResponse, NodeStatusCacheTimestampResponse, + RefreshMixnetContractCacheRequestBody, RefreshMixnetContractCacheResponse, + RefreshNodeStatusCacheRequestBody, RefreshNodeStatusCacheResponse, +}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use std::time::Duration; +use time::OffsetDateTime; + +pub(crate) fn utility_routes() -> Router { + Router::new() + .route("/refresh-mixnet-cache", post(refresh_mixnet_cache)) + .route("/mixnet-cache-timestamp", get(mixnet_cache_timestamp)) + .route( + "/refresh-node-annotations-cache", + post(refresh_node_annotations_cache), + ) + .route( + "/node-annotations-cache-timestamp", + get(node_annotations_cache_timestamp), + ) +} + +/// Allow to request to refresh the cache of all mixnet nodes on the network. +/// Note that this endpoint enforces high global rate limiting and realistically +/// should not be used outside very special scenarios. +#[utoipa::path( + tag = "Utility", + post, + request_body = RefreshMixnetContractCacheRequestBody, + path = "/refresh-mixnet-cache", + context_path = "/v1/utility", + responses( + (status = 200, content( + (RefreshMixnetContractCacheResponse = "application/json"), + (RefreshMixnetContractCacheResponse = "application/yaml"), + (RefreshMixnetContractCacheResponse = "application/bincode") + )) + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +async fn refresh_mixnet_cache( + Query(output): Query, + State(cache): State, +) -> AxumResult> { + let output = output.get_output(); + let now = OffsetDateTime::now_utc(); + + // max 1 refresh every 5min (TODO: make it configurable) + let cutoff = now - Duration::from_secs(5 * 60); + let last = cache.refresh_handle.last_requested(); + if last > cutoff { + return Err(AxumErrorResponse::too_many( + "already refreshed contract cache in the last 5 minutes", + )); + } + cache.refresh_handle.request_refresh(now); + + Ok(output.to_response(RefreshMixnetContractCacheResponse { success: true })) +} + +/// Return information on when the mixnet cache has last been refreshed. +#[utoipa::path( + tag = "Utility", + get, + path = "/mixnet-cache-timestamp", + context_path = "/v1/utility", + responses( + (status = 200, content( + (MixnetContractCacheTimestampResponse = "application/json"), + (MixnetContractCacheTimestampResponse = "application/yaml"), + (MixnetContractCacheTimestampResponse = "application/bincode") + )) + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +async fn mixnet_cache_timestamp( + Query(output): Query, + State(cache): State, +) -> FormattedResponse { + let output = output.get_output(); + let timestamp = cache.cache_timestamp().await; + output.to_response(MixnetContractCacheTimestampResponse { timestamp }) +} + +/// Allow to request to refresh the cache of all mixnet nodes on the network. +/// Note that this endpoint enforces high global rate limiting and realistically +/// should not be used outside very special scenarios. +#[utoipa::path( + tag = "Utility", + post, + request_body = RefreshNodeStatusCacheRequestBody, + path = "/refresh-node-annotations-cache", + context_path = "/v1/utility", + responses( + (status = 200, content( + (RefreshNodeStatusCacheResponse = "application/json"), + (RefreshNodeStatusCacheResponse = "application/yaml"), + (RefreshNodeStatusCacheResponse = "application/bincode") + )) + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +async fn refresh_node_annotations_cache( + Query(output): Query, + State(cache): State, +) -> AxumResult> { + let output = output.get_output(); + let now = OffsetDateTime::now_utc(); + + // max 1 refresh every 5min (TODO: make it configurable) + let cutoff = now - Duration::from_secs(5 * 60); + let last = cache.refresh_handle.last_requested(); + if last > cutoff { + return Err(AxumErrorResponse::too_many( + "already refreshed contract cache in the last 5 minutes", + )); + } + cache.refresh_handle.request_refresh(now); + + Ok(output.to_response(RefreshNodeStatusCacheResponse { success: true })) +} + +/// Return information on when the mixnet cache has last been refreshed. +#[utoipa::path( + tag = "Utility", + get, + path = "/node-annotations-cache-timestamp", + context_path = "/v1/utility", + responses( + (status = 200, content( + (NodeStatusCacheTimestampResponse = "application/json"), + (NodeStatusCacheTimestampResponse = "application/yaml"), + (NodeStatusCacheTimestampResponse = "application/bincode") + )) + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +async fn node_annotations_cache_timestamp( + Query(output): Query, + State(cache): State, +) -> FormattedResponse { + let output = output.get_output(); + let timestamp = cache.cache_timestamp().await; + output.to_response(NodeStatusCacheTimestampResponse { timestamp }) +} diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 08ccef0857b..7a6b9816d60 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -293,6 +293,14 @@ pub(crate) struct WireguardArgs { env = NYMNODE_WG_PRIVATE_NETWORK_PREFIX_ARG )] pub(crate) wireguard_private_network_prefix: Option, + + /// Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. + /// Useful in containerized environments without kernel WireGuard support. + #[clap( + long, + env = NYMNODE_WG_USERSPACE_ARG + )] + pub(crate) wireguard_userspace: Option, } impl WireguardArgs { @@ -321,6 +329,10 @@ impl WireguardArgs { section.private_network_prefix_v4 = private_network_prefix } + if let Some(userspace) = self.wireguard_userspace { + section.use_userspace = userspace + } + section } } diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index ca47c155386..75f5eec8c4a 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -64,8 +64,13 @@ pub struct Debug { /// of the services providers pub minimum_mix_performance: u8, + /// Specifies the maximum time this node will wait for its initial valid topology + #[serde(with = "humantime_serde")] + pub maximum_initial_topology_waiting_time: Duration, + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. #[serde(alias = "maximum_auth_request_age")] + #[serde(with = "humantime_serde")] pub max_request_timestamp_skew: Duration, pub stale_messages: StaleMessageDebug, @@ -85,6 +90,8 @@ impl Debug { pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; pub const DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK: Duration = Duration::from_secs(30); + pub const DEFAULT_MAXIMUM_INITIAL_TOPOLOGY_WAITING_TIME: Duration = + Duration::from_secs(15 * 60); } impl Default for Debug { @@ -98,6 +105,8 @@ impl Default for Debug { client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), upgrade_mode_min_staleness_recheck: Self::DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK, + maximum_initial_topology_waiting_time: + Self::DEFAULT_MAXIMUM_INITIAL_TOPOLOGY_WAITING_TIME, } } } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 9605302aa20..88f225fc937 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -149,6 +149,7 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { debug: config.service_providers.ip_packet_router.debug.client_debug, }, ip_packet_router: nym_ip_packet_router::config::IpPacketRouter { + open_proxy: config.service_providers.open_proxy, disable_poisson_rate: config .service_providers .ip_packet_router @@ -212,6 +213,7 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { announced_metadata_port: config.wireguard.announced_metadata_port, private_network_prefix_v4: config.wireguard.private_network_prefix_v4, private_network_prefix_v6: config.wireguard.private_network_prefix_v6, + use_userspace: config.wireguard.use_userspace, storage_paths: config.wireguard.storage_paths.clone(), }, custom_mixnet_path: None, diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 08b578760ef..064efc8b6a1 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -958,6 +958,12 @@ pub struct Wireguard { /// The maximum value for IPv6 is 128 pub private_network_prefix_v6: u8, + /// Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. + /// Useful in containerized environments without kernel WireGuard support. + /// default: `false` + #[serde(default)] + pub use_userspace: bool, + /// Paths for wireguard keys, client registries, etc. pub storage_paths: persistence::WireguardPaths, } @@ -973,6 +979,7 @@ impl Wireguard { announced_metadata_port: WG_METADATA_PORT, private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, + use_userspace: false, storage_paths: persistence::WireguardPaths::new(data_dir), } } diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs index e45cca8dd21..8a64a1cc98e 100644 --- a/nym-node/src/config/old_configs/old_config_v10.rs +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -1324,6 +1324,7 @@ pub async fn try_upgrade_config_v10>( announced_metadata_port: WG_METADATA_PORT, private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + use_userspace: false, storage_paths: WireguardPaths { private_diffie_hellman_key_file: old_cfg .wireguard diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 1564d087a43..64cdedfa25f 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -47,6 +47,7 @@ pub mod vars { pub const NYMNODE_WG_BIND_ADDRESS_ARG: &str = "NYMNODE_WG_BIND_ADDRESS"; pub const NYMNODE_WG_ANNOUNCED_PORT_ARG: &str = "NYMNODE_WG_ANNOUNCED_PORT"; pub const NYMNODE_WG_PRIVATE_NETWORK_PREFIX_ARG: &str = "NYMNODE_WG_PRIVATE_NETWORK_PREFIX"; + pub const NYMNODE_WG_USERSPACE_ARG: &str = "NYMNODE_WG_USERSPACE"; // verloc: pub const NYMNODE_VERLOC_BIND_ADDRESS_ARG: &str = "NYMNODE_VERLOC_BIND_ADDRESS"; diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index abe336dc449..bb19c161404 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -204,6 +204,9 @@ pub enum NymNodeError { )] InitialTopologyQueryFailure { source: ValidatorClientError }, + #[error("failed to retrieve initial valid topology within the specified deadline")] + InitialTopologyTimeout, + #[error("experienced critical failure with the replay detection bloomfilter: {message}")] BloomfilterFailure { message: &'static str }, diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 8ba968ca29f..9b320e5a186 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -325,6 +325,7 @@ impl ServiceProvidersData { pub struct WireguardData { inner: WireguardGatewayData, peer_rx: mpsc::Receiver, + use_userspace: bool, } impl WireguardData { @@ -335,7 +336,11 @@ impl WireguardData { &config.storage_paths.x25519_wireguard_storage_paths(), )?), ); - Ok(WireguardData { inner, peer_rx }) + Ok(WireguardData { + inner, + peer_rx, + use_userspace: config.use_userspace, + }) } pub(crate) fn initialise(config: &Wireguard) -> Result<(), ServiceProvidersError> { @@ -357,6 +362,7 @@ impl From for nym_wireguard::WireguardData { nym_wireguard::WireguardData { inner: value.inner, peer_rx: value.peer_rx, + use_userspace: value.use_userspace, } } } @@ -569,6 +575,11 @@ impl NymNode { self.config.mixnet.nym_api_urls.clone(), self.config.debug.topology_cache_ttl, self.config.debug.routing_nodes_check_interval, + self.config + .gateway_tasks + .debug + .maximum_initial_topology_waiting_time, + self.config.gateway_tasks.debug.minimum_mix_performance, self.shutdown_manager.clone_shutdown_token(), ) .await @@ -1242,8 +1253,11 @@ impl NymNode { active_egress_mixnet_connections, ); + let network = network_refresher.cached_network(); + network_refresher.start(); + self.start_gateway_tasks( - network_refresher.cached_network(), + network, metrics_sender, active_clients_store, mix_packet_sender, @@ -1253,7 +1267,6 @@ impl NymNode { self.setup_key_rotation(nym_apis_client, bloomfilters_manager) .await?; - network_refresher.start(); self.shutdown_manager.close_tracker(); Ok(self.shutdown_manager) diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index da0ad973726..12bccce0468 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -28,7 +28,7 @@ use std::ops::Deref; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; -use tokio::time::interval; +use tokio::time::{Instant, interval, sleep}; use tracing::log::error; use tracing::{debug, trace, warn}; use url::Url; @@ -148,27 +148,12 @@ impl CachedTopologyProvider { #[async_trait] impl TopologyProvider for CachedTopologyProvider { async fn get_new_topology(&mut self) -> Option { - let network_guard = self.cached_network.inner.read().await; let self_node = self.gateway_node.identity_key; - let mut topology = NymTopology::new( - network_guard.topology_metadata, - network_guard.rewarded_set.clone(), - Vec::new(), - ) - .with_additional_nodes( - network_guard - .network_nodes - .iter() - .map(|node| &node.basic) - .filter(|node| { - if node.supported_roles.mixnode { - node.performance.round_to_integer() >= self.min_mix_performance - } else { - true - } - }), - ); + let mut topology = self + .cached_network + .network_topology(self.min_mix_performance) + .await; if !topology.has_node(self.gateway_node.identity_key) { debug!("{self_node} didn't exist in topology. inserting it.",); @@ -195,6 +180,29 @@ impl CachedNetwork { })), } } + + async fn network_topology(&self, min_mix_performance: u8) -> NymTopology { + let network_guard = self.inner.read().await; + + NymTopology::new( + network_guard.topology_metadata, + network_guard.rewarded_set.clone(), + Vec::new(), + ) + .with_additional_nodes( + network_guard + .network_nodes + .iter() + .map(|node| &node.basic) + .filter(|node| { + if node.supported_roles.mixnode { + node.performance.round_to_integer() >= min_mix_performance + } else { + true + } + }), + ) + } } struct CachedNetworkInner { @@ -215,12 +223,15 @@ pub struct NetworkRefresher { } impl NetworkRefresher { + #[allow(clippy::too_many_arguments)] pub(crate) async fn initialise_new( testnet: bool, user_agent: UserAgent, nym_api_urls: Vec, full_refresh_interval: Duration, pending_check_interval: Duration, + max_startup_waiting_period: Duration, + min_mix_performance: u8, shutdown_token: ShutdownToken, ) -> Result { let nym_api = nym_http_api_client::Client::builder(nym_api_urls[0].clone())? @@ -242,7 +253,8 @@ impl NetworkRefresher { noise_view: NoiseNetworkView::new_empty(), }; - this.obtain_initial_network().await?; + this.obtain_initial_network(max_startup_waiting_period, min_mix_performance) + .await?; Ok(this) } @@ -319,7 +331,7 @@ impl NetworkRefresher { self.routing_filter.resolved.swap_denied(current_denied); self.routing_filter.pending.clear().await; - //update noise Noise Nodes + //update noise Nodes let noise_nodes = nodes .iter() .filter(|n| n.x25519_noise_versioned_key.is_some()) @@ -355,10 +367,32 @@ impl NetworkRefresher { } } - pub(crate) async fn obtain_initial_network(&mut self) -> Result<(), NymNodeError> { - self.refresh_network_nodes_inner() - .await - .map_err(|source| NymNodeError::InitialTopologyQueryFailure { source }) + pub(crate) async fn obtain_initial_network( + &mut self, + max_startup_waiting_period: Duration, + min_mix_performance: u8, + ) -> Result<(), NymNodeError> { + // make it configurable + const STARTUP_REFRESH_INTERVAL: Duration = Duration::from_secs(30); + + let start = Instant::now(); + + loop { + self.refresh_network_nodes_inner() + .await + .map_err(|source| NymNodeError::InitialTopologyQueryFailure { source })?; + + let topology = self.network.network_topology(min_mix_performance).await; + if topology.is_minimally_routable() { + return Ok(()); + } + + if start.elapsed() > max_startup_waiting_period { + return Err(NymNodeError::InitialTopologyTimeout); + } + + sleep(STARTUP_REFRESH_INTERVAL).await; + } } pub(crate) fn routing_filter(&self) -> NetworkRoutingFilter { diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 5020c1bd5c8..9e89c112c83 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -52,6 +52,7 @@ pub struct MixnetClientBuilder { socks5_config: Option, wait_for_gateway: bool, + wait_for_initial_topology: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, custom_shutdown: Option, @@ -94,6 +95,7 @@ impl MixnetClientBuilder { storage_paths: None, socks5_config: None, wait_for_gateway: false, + wait_for_initial_topology: false, custom_topology_provider: None, storage: storage_paths .initialise_default_persistent_storage() @@ -132,6 +134,7 @@ where storage_paths: None, socks5_config: None, wait_for_gateway: false, + wait_for_initial_topology: false, custom_topology_provider: None, custom_gateway_transceiver: None, custom_shutdown: None, @@ -157,6 +160,7 @@ where storage_paths: self.storage_paths, socks5_config: self.socks5_config, wait_for_gateway: self.wait_for_gateway, + wait_for_initial_topology: self.wait_for_initial_topology, custom_topology_provider: self.custom_topology_provider, custom_gateway_transceiver: self.custom_gateway_transceiver, custom_shutdown: self.custom_shutdown, @@ -293,13 +297,21 @@ where self } - /// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded. + /// Attempt to wait for the selected gateway (if applicable) to come online if it's currently not bonded. #[must_use] pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self { self.wait_for_gateway = wait_for_gateway; self } + /// Attempt to wait for initial network topology to become online before finalizing client setup + /// this is useful during network bootstrapping phases + #[must_use] + pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self { + self.wait_for_initial_topology = wait_for_initial_topology; + self + } + #[must_use] pub fn with_user_agent(mut self, user_agent: UserAgent) -> Self { self.user_agent = Some(user_agent); @@ -352,6 +364,7 @@ where client.custom_topology_provider = self.custom_topology_provider; client.custom_shutdown = self.custom_shutdown; client.wait_for_gateway = self.wait_for_gateway; + client.wait_for_initial_topology = self.wait_for_initial_topology; client.force_tls = self.force_tls; client.no_hostname = self.no_hostname; client.user_agent = self.user_agent; @@ -400,9 +413,13 @@ where /// advanced usage of custom gateways custom_gateway_transceiver: Option>, - /// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded. + /// Attempt to wait for the selected gateway (if applicable) to come online if it's currently not bonded. wait_for_gateway: bool, + /// Attempt to wait for initial network topology to become online before finalizing client setup + /// this is useful during network bootstrapping phases + wait_for_initial_topology: bool, + /// Force the client to connect using wss protocol with the gateway. force_tls: bool, @@ -476,6 +493,7 @@ where custom_topology_provider: None, custom_gateway_transceiver: None, wait_for_gateway: false, + wait_for_initial_topology: false, force_tls: false, no_hostname: false, custom_shutdown: None, @@ -758,6 +776,7 @@ where let mut base_builder: BaseClientBuilder<_, _> = BaseClientBuilder::new(base_config, self.storage, self.dkg_query_client) .with_wait_for_gateway(self.wait_for_gateway) + .with_wait_for_initial_topology(self.wait_for_initial_topology) .with_forget_me(&self.forget_me) .with_remember_me(&self.remember_me) .with_derivation_material(self.derivation_material); diff --git a/service-providers/ip-packet-router/src/config/mod.rs b/service-providers/ip-packet-router/src/config/mod.rs index 30e53c6e6f7..3b39fa9706c 100644 --- a/service-providers/ip-packet-router/src/config/mod.rs +++ b/service-providers/ip-packet-router/src/config/mod.rs @@ -188,6 +188,10 @@ impl Config { #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(default, deny_unknown_fields)] pub struct IpPacketRouter { + /// specifies whether this IP Router should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + /// Disable Poisson sending rate. pub disable_poisson_rate: bool, @@ -199,6 +203,7 @@ pub struct IpPacketRouter { impl Default for IpPacketRouter { fn default() -> Self { IpPacketRouter { + open_proxy: false, disable_poisson_rate: true, #[allow(clippy::expect_used)] upstream_exit_policy_url: Some( diff --git a/service-providers/ip-packet-router/src/config/old_config_v1.rs b/service-providers/ip-packet-router/src/config/old_config_v1.rs index 8a5ffd86359..1352812a240 100644 --- a/service-providers/ip-packet-router/src/config/old_config_v1.rs +++ b/service-providers/ip-packet-router/src/config/old_config_v1.rs @@ -94,6 +94,7 @@ impl Default for IpPacketRouterV1 { impl From for IpPacketRouter { fn from(value: IpPacketRouterV1) -> Self { IpPacketRouter { + open_proxy: false, disable_poisson_rate: value.disable_poisson_rate, upstream_exit_policy_url: value.upstream_exit_policy_url, } diff --git a/service-providers/ip-packet-router/src/ip_packet_router.rs b/service-providers/ip-packet-router/src/ip_packet_router.rs index 052636dae3c..13d5a7f6a88 100644 --- a/service-providers/ip-packet-router/src/ip_packet_router.rs +++ b/service-providers/ip-packet-router/src/ip_packet_router.rs @@ -37,6 +37,7 @@ pub struct IpPacketRouter { config: Config, wait_for_gateway: bool, + wait_for_topology: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, shutdown: ShutdownTracker, @@ -48,6 +49,7 @@ impl IpPacketRouter { Self { config, wait_for_gateway: false, + wait_for_topology: false, custom_topology_provider: None, custom_gateway_transceiver: None, shutdown, @@ -72,6 +74,13 @@ impl IpPacketRouter { self } + #[must_use] + #[allow(unused)] + pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self { + self.wait_for_topology = wait_for_initial_topology; + self + } + #[must_use] pub fn with_minimum_gateway_performance(mut self, minimum_gateway_performance: u8) -> Self { self.config.base.debug.topology.minimum_gateway_performance = minimum_gateway_performance; @@ -131,6 +140,7 @@ impl IpPacketRouter { self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, + self.wait_for_topology, &self.config.storage_paths.common_paths, ) .await?; diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs index 5f3c655205f..56b6680500d 100644 --- a/service-providers/ip-packet-router/src/messages/response.rs +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -19,6 +19,7 @@ use crate::{ use super::ClientVersion; +#[derive(Debug)] pub(crate) struct VersionedResponse { pub(crate) version: ClientVersion, pub(crate) reply_to: ConnectedClientId, diff --git a/service-providers/ip-packet-router/src/mixnet_client.rs b/service-providers/ip-packet-router/src/mixnet_client.rs index f3cd81a7c59..0fe7c517731 100644 --- a/service-providers/ip-packet-router/src/mixnet_client.rs +++ b/service-providers/ip-packet-router/src/mixnet_client.rs @@ -17,6 +17,7 @@ pub(crate) async fn create_mixnet_client( custom_transceiver: Option>, custom_topology_provider: Option>, wait_for_gateway: bool, + wait_for_topology: bool, paths: &CommonClientPaths, ) -> Result { let debug_config = config.debug; @@ -34,7 +35,8 @@ pub(crate) async fn create_mixnet_client( .debug_config(debug_config) .custom_shutdown(shutdown) .with_user_agent(user_agent) - .with_wait_for_gateway(wait_for_gateway); + .with_wait_for_gateway(wait_for_gateway) + .with_wait_for_initial_topology(wait_for_topology); if !config.get_disabled_credentials_mode() { client_builder = client_builder.enable_credentials_mode(); } diff --git a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs index 7baca7b5c58..133df560c79 100644 --- a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs @@ -35,6 +35,13 @@ impl ExitPolicyRequestFilter { } } + pub fn new_from_policy(policy: ExitPolicy) -> Self { + ExitPolicyRequestFilter { + upstream: None, + policy, + } + } + #[allow(unused)] pub fn policy(&self) -> &ExitPolicy { &self.policy diff --git a/service-providers/ip-packet-router/src/request_filter/mod.rs b/service-providers/ip-packet-router/src/request_filter/mod.rs index 5e2f3595700..2c724af9eb6 100644 --- a/service-providers/ip-packet-router/src/request_filter/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/mod.rs @@ -5,6 +5,7 @@ use crate::config::Config; use crate::error::IpPacketRouterError; use crate::request_filter::exit_policy::ExitPolicyRequestFilter; use log::{info, warn}; +use nym_exit_policy::ExitPolicy; use std::{net::SocketAddr, sync::Arc}; pub mod exit_policy; @@ -42,12 +43,17 @@ impl RequestFilter { } async fn new_exit_policy_filter(config: &Config) -> Result { - let upstream_url = config - .ip_packet_router - .upstream_exit_policy_url - .as_ref() - .ok_or(IpPacketRouterError::NoUpstreamExitPolicy)?; - let policy_filter = ExitPolicyRequestFilter::new_upstream(upstream_url.clone()).await?; + let policy_filter = if config.ip_packet_router.open_proxy { + ExitPolicyRequestFilter::new_from_policy(ExitPolicy::new_open()) + } else { + let upstream_url = config + .ip_packet_router + .upstream_exit_policy_url + .as_ref() + .ok_or(IpPacketRouterError::NoUpstreamExitPolicy)?; + ExitPolicyRequestFilter::new_upstream(upstream_url.clone()).await? + }; + Ok(RequestFilter { inner: Arc::new(RequestFilterInner::ExitPolicy { policy_filter }), }) diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 6ca9b3b588c..ad9623dabab 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -64,6 +64,7 @@ pub struct NRServiceProviderBuilder { config: Config, wait_for_gateway: bool, + wait_for_topology: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, shutdown: ShutdownTracker, @@ -153,6 +154,7 @@ impl NRServiceProviderBuilder { NRServiceProviderBuilder { config, wait_for_gateway: false, + wait_for_topology: false, custom_topology_provider: None, custom_gateway_transceiver: None, shutdown, @@ -181,6 +183,15 @@ impl NRServiceProviderBuilder { self } + #[must_use] + // this is a false positive, this method is actually called when used as a library + // but clippy complains about it when building the binary + #[allow(unused)] + pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self { + self.wait_for_topology = wait_for_initial_topology; + self + } + #[must_use] // this is a false positive, this method is actually called when used as a library // but clippy complains about it when building the binary @@ -232,6 +243,7 @@ impl NRServiceProviderBuilder { self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, + self.wait_for_topology, &self.config.storage_paths.common_paths, ) .await?; @@ -554,6 +566,7 @@ async fn create_mixnet_client( custom_transceiver: Option>, custom_topology_provider: Option>, wait_for_gateway: bool, + wait_for_topology: bool, paths: &CommonClientPaths, ) -> Result { let debug_config = config.debug; @@ -569,7 +582,8 @@ async fn create_mixnet_client( .network_details(NymNetworkDetails::new_from_env()) .debug_config(debug_config) .custom_shutdown(shutdown) - .with_wait_for_gateway(wait_for_gateway); + .with_wait_for_gateway(wait_for_gateway) + .with_wait_for_initial_topology(wait_for_topology); if !config.get_disabled_credentials_mode() { client_builder = client_builder.enable_credentials_mode(); } diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/localnet-orchestrator/Cargo.toml similarity index 80% rename from tools/internal/testnet-manager/Cargo.toml rename to tools/internal/localnet-orchestrator/Cargo.toml index 6f799d84a43..b25a52f1202 100644 --- a/tools/internal/testnet-manager/Cargo.toml +++ b/tools/internal/localnet-orchestrator/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "testnet-manager" +name = "localnet-orchestrator" version = "0.1.0" authors.workspace = true repository.workspace = true @@ -8,37 +8,41 @@ documentation.workspace = true edition.workspace = true license.workspace = true rust-version.workspace = true +readme.workspace = true [dependencies] -anyhow.workspace = true -bip39.workspace = true -bs58.workspace = true +anyhow = { workspace = true } +bip39 = { workspace = true } +bytes = { workspace = true } +cargo-edit = { workspace = true } +cfg-if = { workspace = true } console = { workspace = true } -cw-utils.workspace = true +cw-utils = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"] } +futures = { workspace = true } indicatif = { workspace = true } +itertools = { workspace = true } humantime = { workspace = true } -rand.workspace = true +rand = { workspace = true } +reqwest = { workspace = true, features = ["json", "stream", "rustls-tls"] } serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true +serde_json = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +strum_macros = { workspace = true } tempfile = { workspace = true } -thiserror.workspace = true time = { workspace = true, features = ["parsing", "formatting"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process"] } toml = { workspace = true } -tracing.workspace = true -url.workspace = true +tracing = { workspace = true } +url = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } - nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } nym-crypto = { path = "../../../common/crypto", features = ["asymmetric", "rand", "serde"] } +nym-compact-ecash = { path = "../../../common/nym_offline_compact_ecash" } nym-config = { path = "../../../common/config" } nym-validator-client = { path = "../../../common/client-libs/validator-client" } -nym-compact-ecash = { path = "../../../common/nym_offline_compact_ecash" } -nym-http-api-client = { path = "../../../common/http-api-client" } -dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false } +nym-pemstore = { path = "../../../common/pemstore" } # contracts: nym-mixnet-contract-common = { path = "../../../common/cosmwasm-smart-contracts/mixnet-contract" } @@ -49,10 +53,13 @@ nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/e nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" } nym-performance-contract-common = { path = "../../../common/cosmwasm-smart-contracts/nym-performance-contract" } -nym-pemstore = { path = "../../../common/pemstore" } - +dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false } [build-dependencies] anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + + +[lints] +workspace = true diff --git a/tools/internal/localnet-orchestrator/README.md b/tools/internal/localnet-orchestrator/README.md new file mode 100644 index 00000000000..064b55fec1a --- /dev/null +++ b/tools/internal/localnet-orchestrator/README.md @@ -0,0 +1,3 @@ +# Localnet Orchestrator + +based off the testnet manager (to be deprecated) \ No newline at end of file diff --git a/tools/internal/testnet-manager/build.rs b/tools/internal/localnet-orchestrator/build.rs similarity index 67% rename from tools/internal/testnet-manager/build.rs rename to tools/internal/localnet-orchestrator/build.rs index be394b3c41e..72a8e21b80b 100644 --- a/tools/internal/testnet-manager/build.rs +++ b/tools/internal/localnet-orchestrator/build.rs @@ -5,7 +5,7 @@ use std::env; #[tokio::main] async fn main() -> anyhow::Result<()> { let out_dir = env::var("OUT_DIR")?; - let database_path = format!("{out_dir}/nym-api-example.sqlite"); + let database_path = format!("{out_dir}/localnet-example.sqlite"); // remove the db file if it already existed from previous build // in case it was from a different branch @@ -22,13 +22,7 @@ async fn main() -> anyhow::Result<()> { .await .context("Failed to perform SQLx migrations")?; - #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); - #[cfg(target_family = "windows")] - // for some strange reason we need to add a leading `/` to the windows path even though it's - // not a valid windows path... but hey, it works... - println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); - Ok(()) } diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml b/tools/internal/localnet-orchestrator/dkg-bypass-contract/Cargo.toml similarity index 100% rename from tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml rename to tools/internal/localnet-orchestrator/dkg-bypass-contract/Cargo.toml diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile b/tools/internal/localnet-orchestrator/dkg-bypass-contract/Makefile similarity index 100% rename from tools/internal/testnet-manager/dkg-bypass-contract/Makefile rename to tools/internal/localnet-orchestrator/dkg-bypass-contract/Makefile diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs b/tools/internal/localnet-orchestrator/dkg-bypass-contract/src/contract.rs similarity index 89% rename from tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs rename to tools/internal/localnet-orchestrator/dkg-bypass-contract/src/contract.rs index 3c25a60f630..574c329f660 100644 --- a/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs +++ b/tools/internal/localnet-orchestrator/dkg-bypass-contract/src/contract.rs @@ -7,7 +7,9 @@ use cosmwasm_std::{ Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError, StdResult, Storage, entry_point, }; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cw_storage_plus::{ + Index, IndexList, IndexedMap, Item, Map, MultiIndex, SnapshotItem, Strategy, +}; use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, NodeIndex}; use nym_coconut_dkg_common::verification_key::ContractVKShare; @@ -15,6 +17,18 @@ use nym_coconut_dkg_common::verification_key::ContractVKShare; pub(crate) type Dealer<'a> = &'a Addr; pub(crate) const CURRENT_EPOCH: Item = Item::new("current_epoch"); +pub const HISTORICAL_EPOCH: SnapshotItem = SnapshotItem::new( + "historical_epoch", + "historical_epoch__checkpoints", + "historical_epoch__changelog", + Strategy::EveryBlock, +); + +#[allow(deprecated)] +pub fn save_epoch(storage: &mut dyn Storage, height: u64, epoch: &Epoch) -> StdResult<()> { + CURRENT_EPOCH.save(storage, epoch)?; + HISTORICAL_EPOCH.save(storage, epoch, height) +} pub const THRESHOLD: Item = Item::new("threshold"); @@ -104,8 +118,9 @@ pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result + * SPDX-License-Identifier: Apache-2.0 + */ + + +CREATE TABLE account +( + address TEXT NOT NULL PRIMARY KEY UNIQUE, + mnemonic TEXT NOT NULL +); + +CREATE TABLE nyxd +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + rpc_endpoint TEXT NOT NULL, + master_address TEXT NOT NULL REFERENCES account (address) +); + +CREATE table nym_api +( + network_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id), + endpoint TEXT NOT NULL +); + +CREATE TABLE contract +( + -- note: I'm purposely not using contract address as primary key, + -- as you can have the same addresses for different contracts (on different instances of localnets) + -- as addressing is semi-kinda deterministic-ish + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address TEXT NOT NULL, + admin_address TEXT NOT NULL REFERENCES account (address) +); + +CREATE TABLE localnet_metadata +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + -- human-readable name associated with the localnet (to have some unique prefix for containers) + name TEXT NOT NULL UNIQUE, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE localnet_contracts +( + metadata_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id), + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), + performance_contract_id INTEGER NOT NULL REFERENCES contract (id) +); + +CREATE TABLE localnet_auxiliary_accounts +( + network_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id), + + rewarder_address TEXT NOT NULL REFERENCES account (address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) +); + +CREATE TABLE localnet +( + metadata_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id), + nyxd_id INTEGER NOT NULL REFERENCES nyxd (id) +); + + +-- keep it separate to more easily support testing having multiple network monitors +CREATE TABLE authorised_network_monitor +( + address TEXT NOT NULL PRIMARY KEY REFERENCES account (address), + network_id INTEGER NOT NULL REFERENCES localnet_metadata (id) +); + +CREATE TABLE metadata +( + id INTEGER PRIMARY KEY CHECK (id = 0), + latest_network_id INTEGER REFERENCES localnet_metadata (id), + latest_nyxd_id INTEGER REFERENCES nyxd (id) +); + +CREATE TABLE nym_node +( + node_id INTEGER NOT NULL, + identity_key TEXT NOT NULL PRIMARY KEY, + private_identity_key TEXT NOT NULL, + network_id INTEGER NOT NULL REFERENCES localnet_metadata (id), + owner_address TEXT NOT NULL REFERENCES account (address), + gateway BOOL NOT NULL +); + +INSERT OR IGNORE INTO metadata(id) +VALUES (0); diff --git a/tools/internal/localnet-orchestrator/src/README.md b/tools/internal/localnet-orchestrator/src/README.md new file mode 100644 index 00000000000..ea0c8a09f2c --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/README.md @@ -0,0 +1,221 @@ +## Localnet: + +Result of marrying the dedicated `localnet.sh` script and the old `Testnet Manager`. +It allows to run a complete Nym mixnet test environment on Apple's `container` runtime or on Linux `containerd` (via +`nerdctl` and kata shim). + +It results in creation of the following containers: + +- `nyxd` +- `nym-api` +- `nym-node-1` (gateway) +- `nym-node-2` (mixnode) +- `nym-node-3` (mixnode) +- `nym-node-4` (mixnode) + +which run on a custom brige network (`nym-localnet-network`) with dynamic IP assignment: + +``` +Host Machine (macOS) +โ”œโ”€โ”€ nym-localnet-network (bridge) +โ”‚ โ”œโ”€โ”€ nyxd (192.168.66.3) +โ”‚ โ”œโ”€โ”€ nym-api (192.168.66.4) +โ”‚ โ”œโ”€โ”€ nym-node-1 (192.168.66.5) +โ”‚ โ”œโ”€โ”€ nym-node-2 (192.168.66.6) +โ”‚ โ”œโ”€โ”€ nym-node-3 (192.168.66.7) +โ”‚ โ””โ”€โ”€ nym-node-4 (192.168.66.8) +``` + +it also embeddeds `nym-gateway-probe` binary in the container image for easy testing. + +### Prerequisites + +#### MacOS + +- **MUST** have MacOS Tahoe for inter-container networking +- `brew install --cask container` +- Download Kata Containers 3.20, this one can be loaded by `container` and has `CONFIG_TUN=y` kernel flag + - `https://github.com/kata-containers/kata-containers/releases/download/3.20.0/kata-static-3.20.0-arm64.tar.xz` +- Load new kernel + - + `container system kernel set --tar kata-static-3.20.0-arm64.tar.xz --binary opt/kata/share/kata-containers/vmlinux-6.12.42-162` +- Validate kernel version once you have container running + +#### Linux + +The following dependencies must be installed: + +- `newuidmap` and `newgidmap` which can be installed via `uidmap` package +- `containerd` which will probably come with your distro +- `nerdctl`, `kata-runtime` and `containerd-shim-kata-v2`. they can be either installed manually or via + `kata-manager.sh` script: https://github.com/kata-containers/kata-containers/blob/main/utils/README.md#kata-manager. + it is recommended to run it with the `-N` flag to install it alongside `nerdctl` + +### Quick Start + +```bash +# navigate to the root of the nym monorepo +# (exact command will depend on the relative location of the directory on your machine) +cd nym + +# build the orchestrator binary +cargo run --release --bin localnet-orchestrator + +# run the orchestrator binary to startup the network +target/release/nym-localnet-orchestrator up + +# run the gateway probe test +target/release/nym-localnet-orchestrator run-gateway-probe-test + +# purge all the containers and build data +target/release/nym-localnet-orchestrator purge +``` + +### Startup flow + +The startup is separated into 4 main steps (which can also be run individually as separate commands) + +1. `initialise-nyxd` + - builds `nyxd` docker image from https://github.com/nymtech/nyxd.git and imports it into the `container` runtime + - initialises the `genesis.json` of the localnet chain and saves it to a shared volume + - starts up `nyxd` container using the shared volume data + +2. `initialise-contracts` + - either downloads nym contracts or builds all of them fresh using `cosmwasm/optimizer` image + - uploads and initialises all the contracts onto the chain + - fixes up state inconsistencies (the bootstrap problem) by performing additional contract migrations + +3. `initialise-nym-api` + - builds `nym-binaries` docker image and imports it into the `container` runtime. note: its version tag is based on + the current version of the `nym-node` binary + - generates DKG keys to allow future zk-nym issuance and injects those into a shared volume to be used by the + `nym-api` + - initialises `nym-api` data and starts its container using a shared volume + - overwrites the states of the `dkg` and `group` contracts by forcing the just created `nym-api` instance to be a + valid zk-nym issuer + +4. `initialise-nym-nodes` + - initialises data of 4 nym-nodes `nym-node --init-only`: 3 mixnodes and 1 gateway + - bonds all of them into previously created mixnet contract + - force assigns them to the active set by performing additional admin-only contract shenanigans + - force refreshes nym-api caches to make the nodes appear in the relevant endpoints immediately + - injects fake "100%" network monitor scores for each node in the `nym-api` container to make sure all nodes have + valid performance metrics and force refreshes the relevant cache + +### Commands + +#### `build-info` + +Show build information of the localnet orchestrator binary + +#### `initialise-nyxd` + +Initialise new nyxd instance as described above + +##### Relevant arguments: + +- `nyxd-tag` to allow using non-default nyxd repo branch + +#### `initialise-contracts` + +Upload and initialise all Nym cosmwasm contracts as described above + +##### Relevant arguments: + +- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root +- `reproducible-builds` - ensure contract builds are fully reproducible by removing additional source of + non-determinism. note that this slows down the build process significantly +- `ci-build-branch` - use prebuilt contracts from the `build.ci.nymte.ch` server +- `cosmwasm-optimizer-image` - cosmwasm optimizer image used for building and optimising the contracts +- `allow-cached-build` - allow using pre-built contracts from previous localnet runs + +#### `initialise-nym-api` + +Initialise instance of nym api and adjust the DKG contract to allow it to immediately start issuing zk-nyms as described +above + +##### Relevant arguments: + +- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root +- `cosmwasm-optimizer-image` - cosmwasm optimizer image used for building and optimising the contracts +- `allow-cached-build` - allow using pre-built contracts from previous localnet runs +- `custom-dns` - allows specifying custom nameserver to be used by all spawned containers + +#### `initialise-nym-nodes` + +Initialise nym nodes to start serving mixnet (and wireguard) traffic. this involves bonding them in the contract and +starting the containers as described above + +##### Relevant arguments: + +- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root +- `open-proxy` - allow internal service providers to run in open proxy mode +- `custom-dns` - allows specifying custom nameserver to be used by all spawned containers + +#### `run-gateway-probe-test` + +Run a gateway probe against the running localnet + +##### Relevant arguments: + +- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root +- `prove-args` - allows specifying additional flags to be passed to the gateway probe + +#### `rebuild-binaries-image` + +Rebuild the docker and container image used for running the nym binaries + +##### Relevant arguments: + +- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root +- `custom-tag` - custom image tag for the new image + +#### `up` + +Single command to start up localnet with minimal configuration + +##### Relevant arguments: + +refer to arguments of `initialise-nyxd`, `initialise-contracts`, `initialise-nym-api` and `initialise-nym-nodes` as the +same ones are available + +#### `down` + +Stop the localnet (stops and removes all containers using `localnet-*` image + +#### `purge` + +Remove all localnet information, including any containers and images + +##### Relevant arguments: + +- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root +- `remove-cache` (default: true) - specify whether the cache data should be removed +- `remove-images` (default: true) - specify whether the built images should be removed + +### Storage + +All the localnet data is saved, by default, under `~/.nym/localnet-orchestrator/` directory and further split into the +following: + +- `network-data.sqlite` (by default `~/.nym/localnet-orchestrator/network-data.sqlite`) which contains basic network + metadata - it was easier than jugling random .json files around +- each container has its volume stored in: + - $NETWORK_NAME/nym-api (e.g. `~/.nym/localnet-orchestrator/group-key/nym-api`) + - $NETWORK_NAME/nyxd (e.g. `~/.nym/localnet-orchestrator/group-key/nyxd`) + - $NETWORK_NAME/nym-node-1 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-1`) + - $NETWORK_NAME/nym-node-2 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-2`) + - $NETWORK_NAME/nym-node-3 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-3`) + - $NETWORK_NAME/nym-node-4 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-4`) +- `~/.nym/localnet-orchestrator/.cache` which contains intermediate build data that can be reused between runs to speed + up the deployment process. currently it only contains `contracts` directory for built cosmwasm contracts + +### Current Limitations: + +- `nyxd` instance exposes port `26657` to the host. this was to speed up development to allow easier chain interaction + by being able to use rust client directly from the orchestrator host. in the future this should get modified +- no windows support +- no docker compose - custom orchestrator is used instead +- dynamic ips - container ip addresses may change between restarts, thus there's a lot of inflexibility with a network + setup. once created it cannot be modified + diff --git a/tools/internal/testnet-manager/src/cli/build_info.rs b/tools/internal/localnet-orchestrator/src/cli/build_info.rs similarity index 59% rename from tools/internal/testnet-manager/src/cli/build_info.rs rename to tools/internal/localnet-orchestrator/src/cli/build_info.rs index ed3d7ecadb5..2abae16aee2 100644 --- a/tools/internal/testnet-manager/src/cli/build_info.rs +++ b/tools/internal/localnet-orchestrator/src/cli/build_info.rs @@ -1,9 +1,9 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use crate::error::NetworkManagerError; use nym_bin_common::bin_info_owned; use nym_bin_common::output_format::OutputFormat; +use tracing::debug; #[derive(clap::Args, Debug)] pub(crate) struct Args { @@ -11,7 +11,8 @@ pub(crate) struct Args { output: OutputFormat, } -pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { +pub(crate) fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); println!("{}", args.output.format(&bin_info_owned!())); Ok(()) } diff --git a/tools/internal/localnet-orchestrator/src/cli/check_prerequisites.rs b/tools/internal/localnet-orchestrator/src/cli/check_prerequisites.rs new file mode 100644 index 00000000000..e8b6c008c7a --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/check_prerequisites.rs @@ -0,0 +1,20 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use tracing::debug; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + // during the initial setup the prerequisites are checked + LocalnetOrchestrator::new(&args.common).await?; + Ok(()) +} diff --git a/tools/internal/localnet-orchestrator/src/cli/down.rs b/tools/internal/localnet-orchestrator/src/cli/down.rs new file mode 100644 index 00000000000..0976b50b35b --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/down.rs @@ -0,0 +1,21 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use tracing::debug; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + LocalnetOrchestrator::new(&args.common) + .await? + .stop_localnet() + .await +} diff --git a/tools/internal/localnet-orchestrator/src/cli/initialise_contracts.rs b/tools/internal/localnet-orchestrator/src/cli/initialise_contracts.rs new file mode 100644 index 00000000000..967e7431e37 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/initialise_contracts.rs @@ -0,0 +1,79 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::cosmwasm_contracts; +use crate::orchestrator::state::LocalnetState; +use anyhow::bail; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use tracing::debug; + +#[derive(clap::Args, Debug)] +#[clap(group(clap::ArgGroup::new("built-contracts").required(false)))] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Point to on-disk director containing .wasm files of all required Nym contracts + #[clap(long, group = "built-contracts")] + contracts_directory: Option, + + /// Provide a branch name to be used for attempting to retrieve .wasm files from the ci build server, + /// e.g. for branch `feature/my-amazing-feature`, the following urls will be used: + /// - `https://builds.ci.nymte.ch/feature/my-amazing-feature/mixnet_contract.wasm` + /// - `https://builds.ci.nymte.ch/feature/my-amazing-feature/nym_performance_contract.wasm` + /// - ... + /// - etc. + #[clap(long, group = "built-contracts")] + ci_build_branch: Option, + + /// Ensure contracts wasm code is fully reproducible by building those them + /// with linux/amd64 platform and forcing some additional cargo build flags. + /// Note: it will cause significant (build-time) overhead for M1 Macs + #[clap(long)] + reproducible_builds: bool, + + /// Cosmwasm optimizer image used for building and optimising the contracts + #[clap(long, default_value = "cosmwasm/optimizer:0.17.0")] + cosmwasm_optimizer_image: String, + + /// Custom path to root of the monorepo in case this binary has been executed from a different location. + /// If not provided, it is going to get assumed that the current directory is the monorepo root + #[clap(long)] + monorepo_root: Option, + + /// Specify whether the orchestrator can attempt to retrieve previously built cached contracts. + #[clap(long, conflicts_with = "reproducible_builds")] + allow_cached_build: bool, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?; + + if orchestrator.state != LocalnetState::RunningNyxd { + bail!( + "can't initialise cosmwasm contracts - nyxd is not running or the contracts have already been initialised. the localnet is in {} state.", + orchestrator.state + ) + } + + orchestrator + .initialise_contracts(cosmwasm_contracts::Config { + reproducible_builds: args.reproducible_builds, + cosmwasm_optimizer_image: args.cosmwasm_optimizer_image, + explicit_contracts_directory: args.contracts_directory, + ci_build_branch: args.ci_build_branch, + monorepo_root: args.monorepo_root, + allow_cached_build: args.allow_cached_build, + }) + .await?; + + Ok(()) +} diff --git a/tools/internal/localnet-orchestrator/src/cli/initialise_nym_api.rs b/tools/internal/localnet-orchestrator/src/cli/initialise_nym_api.rs new file mode 100644 index 00000000000..3178f437b90 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/initialise_nym_api.rs @@ -0,0 +1,57 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::nym_api; +use crate::orchestrator::state::LocalnetState; +use anyhow::bail; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use tracing::debug; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Cosmwasm optimizer image used for building and optimising the contracts + #[clap(long, default_value = "cosmwasm/optimizer:0.17.0")] + cosmwasm_optimizer_image: String, + + /// Custom path to root of the monorepo in case this binary has been executed from a different location. + /// If not provided, it is going to get assumed that the current directory is the monorepo root + #[clap(long)] + monorepo_root: Option, + + /// Specify whether the orchestrator can attempt to retrieve previously built cached contracts. + #[clap(long)] + allow_cached_build: bool, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?; + + if orchestrator.state != LocalnetState::DeployedNymContracts { + bail!( + "can't initialise nym api - nym contracts have not already been initialised or nym api is already running. the localnet is in {} state.", + orchestrator.state + ) + } + + orchestrator + .initialise_nym_api(nym_api::Config { + cosmwasm_optimizer_image: args.cosmwasm_optimizer_image, + monorepo_root: args.monorepo_root, + custom_dns: args.common.custom_dns, + allow_cached_build: args.allow_cached_build, + }) + .await?; + + Ok(()) +} diff --git a/tools/internal/localnet-orchestrator/src/cli/initialise_nym_nodes.rs b/tools/internal/localnet-orchestrator/src/cli/initialise_nym_nodes.rs new file mode 100644 index 00000000000..4014fcfc9b5 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/initialise_nym_nodes.rs @@ -0,0 +1,52 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::nym_nodes; +use crate::orchestrator::state::LocalnetState; +use anyhow::bail; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use tracing::debug; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Custom path to root of the monorepo in case this binary has been executed from a different location. + /// If not provided, it is going to get assumed that the current directory is the monorepo root + #[clap(long)] + monorepo_root: Option, + + /// Specify whether internal service providers should run in open proxy mode + #[clap(long)] + open_proxy: bool, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?; + + if orchestrator.state != LocalnetState::RunningNymApi { + bail!( + "can't initialise nym nodes - nym api has not already been initialised or nym nodes are already running. the localnet is in {} state.", + orchestrator.state + ) + } + + orchestrator + .initialise_nym_nodes(nym_nodes::Config { + monorepo_root: args.monorepo_root, + custom_dns: args.common.custom_dns, + open_proxy: args.open_proxy, + }) + .await?; + + Ok(()) +} diff --git a/tools/internal/localnet-orchestrator/src/cli/initialise_nyxd.rs b/tools/internal/localnet-orchestrator/src/cli/initialise_nyxd.rs new file mode 100644 index 00000000000..58c177271ad --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/initialise_nyxd.rs @@ -0,0 +1,53 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::nyxd; +use crate::orchestrator::state::LocalnetState; +use anyhow::bail; +use nym_bin_common::output_format::OutputFormat; +use tracing::debug; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + #[clap(long, default_value = "https://github.com/nymtech/nyxd.git")] + nyxd_repo: Url, + + /// Absolute path (from the repo root) to the location of the Dockerfile used for building nyxd + #[clap(long, default_value = "Dockerfile.dev")] + nyxd_dockerfile_path: String, + + #[clap(long, default_value = "v0.60.1")] + nyxd_tag: String, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?; + if orchestrator.state != LocalnetState::Uninitialised { + bail!( + "can't initialise nyxd as it appears to have already been initialised. the localnet is in {} state.", + orchestrator.state + ) + } + + orchestrator + .initialise_nyxd(nyxd::Config { + nyxd_repo: args.nyxd_repo, + nyxd_dockerfile_path: args.nyxd_dockerfile_path, + custom_dns: args.common.custom_dns, + nyxd_tag: args.nyxd_tag, + }) + .await?; + + Ok(()) +} diff --git a/tools/internal/localnet-orchestrator/src/cli/mod.rs b/tools/internal/localnet-orchestrator/src/cli/mod.rs new file mode 100644 index 00000000000..9b9e959c45b --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/mod.rs @@ -0,0 +1,122 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::path::PathBuf; +use std::sync::OnceLock; + +pub(crate) mod build_info; +pub(crate) mod check_prerequisites; +pub(crate) mod down; +pub(crate) mod initialise_contracts; +pub(crate) mod initialise_nym_api; +pub(crate) mod initialise_nym_nodes; +pub(crate) mod initialise_nyxd; +pub(crate) mod purge; +pub(crate) mod rebuild_binaries_image; +pub(crate) mod run_gateway_probe_test; +pub(crate) mod up; + +#[derive(clap::Args, Debug)] +pub(crate) struct CommonArgs { + #[clap(long, group = "storage")] + pub(crate) localnet_storage_path: Option, + + #[clap(long)] + pub(crate) orchestrator_db: Option, + + #[clap(long)] + pub(crate) existing_network: Option, + + /// Custom DNS flag ('--dns') to pass to all spawned containers + #[clap(long)] + pub(crate) custom_dns: Option, + + /// Specify whether all the data should be cleaned-up after use + #[clap(long, group = "storage")] + pub(crate) ephemeral: bool, +} + +impl CommonArgs { + // +} + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub(crate) async fn execute(self) -> anyhow::Result<()> { + match self.command { + Commands::BuildInfo(args) => build_info::execute(args), + Commands::InitialiseNyxd(args) => initialise_nyxd::execute(args).await, + Commands::InitialiseContracts(args) => initialise_contracts::execute(args).await, + Commands::InitialiseNymApi(args) => initialise_nym_api::execute(args).await, + Commands::InitialiseNymNodes(args) => initialise_nym_nodes::execute(args).await, + Commands::RunGatewayProbeTest(args) => run_gateway_probe_test::execute(args).await, + Commands::RebuildBinariesImage(args) => rebuild_binaries_image::execute(args).await, + Commands::CheckPrerequisites(args) => check_prerequisites::execute(args).await, + Commands::Up(args) => up::execute(args).await, + Commands::Down(args) => down::execute(args).await, + Commands::Purge(args) => purge::execute(args).await, + } + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Show build information of this binary + BuildInfo(build_info::Args), + + /// Initialise new nyxd instance + InitialiseNyxd(initialise_nyxd::Args), + + /// Upload and initialise all Nym cosmwasm contracts + InitialiseContracts(initialise_contracts::Args), + + /// Initialise instance of nym api and adjust the DKG contract + /// to allow it to immediately start issuing zk-nyms + InitialiseNymApi(initialise_nym_api::Args), + + /// Initialise nym nodes to start serving mixnet (and wireguard) traffic. + /// this involves bonding them in the contract and starting the containers + InitialiseNymNodes(initialise_nym_nodes::Args), + + /// Run a gateway probe against the running localnet + RunGatewayProbeTest(run_gateway_probe_test::Args), + + /// Rebuild the docker and container image used for running the nym binaries + RebuildBinariesImage(rebuild_binaries_image::Args), + + /// Performs basic prerequisites check for running the orchestrator + CheckPrerequisites(check_prerequisites::Args), + + /// Single command to start up localnet with minimal configuration + Up(up::Args), + + /// Stop the localnet (stops and removes all containers using `localnet-*` image + Down(down::Args), + + /// Remove all localnet information, including any containers and images + Purge(purge::Args), +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/tools/internal/localnet-orchestrator/src/cli/purge.rs b/tools/internal/localnet-orchestrator/src/cli/purge.rs new file mode 100644 index 00000000000..c0dc2f19dc3 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/purge.rs @@ -0,0 +1,41 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::purge; +use clap::ArgAction; +use std::path::PathBuf; +use tracing::debug; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Remove any built docker and container images + #[clap(long, action = ArgAction::Set, default_value_t = true)] + remove_images: bool, + + /// Remove any cached build data + #[clap(long, action = ArgAction::Set, default_value_t = true)] + remove_cache: bool, + + /// Custom path to root of the monorepo in case this binary has been executed from a different location. + /// If not provided, it is going to get assumed that the current directory is the monorepo root + #[clap(long)] + monorepo_root: Option, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + LocalnetOrchestrator::new(&args.common) + .await? + .purge_localnet(purge::Config { + remove_images: args.remove_images, + remove_cache: args.remove_cache, + monorepo_root: args.monorepo_root, + }) + .await +} diff --git a/tools/internal/localnet-orchestrator/src/cli/rebuild_binaries_image.rs b/tools/internal/localnet-orchestrator/src/cli/rebuild_binaries_image.rs new file mode 100644 index 00000000000..0dc8b0a41ae --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/rebuild_binaries_image.rs @@ -0,0 +1,38 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::rebuild_binaries_image; +use std::path::PathBuf; +use tracing::debug; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Custom tag for the new images + #[clap(long)] + custom_tag: Option, + + /// Custom path to root of the monorepo in case this binary has been executed from a different location. + /// If not provided, it is going to get assumed that the current directory is the monorepo root + #[clap(long)] + monorepo_root: Option, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + let orchestrator = LocalnetOrchestrator::new(&args.common).await?; + + orchestrator + .rebuild_binaries_image(rebuild_binaries_image::Config { + custom_tag: args.custom_tag, + monorepo_root: args.monorepo_root, + }) + .await?; + + Ok(()) +} diff --git a/tools/internal/localnet-orchestrator/src/cli/run_gateway_probe_test.rs b/tools/internal/localnet-orchestrator/src/cli/run_gateway_probe_test.rs new file mode 100644 index 00000000000..cb824fbf424 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/run_gateway_probe_test.rs @@ -0,0 +1,43 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::state::LocalnetState; +use anyhow::bail; +use std::path::PathBuf; +use tracing::debug; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Custom path to root of the monorepo in case this binary has been executed from a different location. + /// If not provided, it is going to get assumed that the current directory is the monorepo root + #[clap(long)] + monorepo_root: Option, + + /// additional, optional flags to pass when starting the gateway probe + /// e.g. "--ignore-egress-epoch-role --netstack-args='...'" + #[clap(long)] + probe_args: Option, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + let orchestrator = LocalnetOrchestrator::new(&args.common).await?; + if orchestrator.state != LocalnetState::RunningNymNodes { + bail!( + "can't test the gateway probe as the localnet does not appear to be running. the localnet is in {} state.", + orchestrator.state + ) + } + + orchestrator + .run_gateway_probe(args.monorepo_root, args.probe_args) + .await?; + + Ok(()) +} diff --git a/tools/internal/localnet-orchestrator/src/cli/up.rs b/tools/internal/localnet-orchestrator/src/cli/up.rs new file mode 100644 index 00000000000..4738f85ebd0 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/cli/up.rs @@ -0,0 +1,104 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::{cosmwasm_contracts, nym_api, nym_nodes, nyxd, up}; +use crate::orchestrator::state::LocalnetState; +use anyhow::bail; +use std::path::PathBuf; +use tracing::debug; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + #[clap(long, default_value = "https://github.com/nymtech/nyxd.git")] + nyxd_repo: Url, + + /// Absolute path (from the repo root) to the location of the Dockerfile used for building nyxd + #[clap(long, default_value = "Dockerfile.dev")] + nyxd_dockerfile_path: String, + + #[clap(long, default_value = "v0.60.1")] + nyxd_tag: String, + + /// Cosmwasm optimizer image used for building and optimising the contracts + #[clap(long, default_value = "cosmwasm/optimizer:0.17.0")] + cosmwasm_optimizer_image: String, + + /// Custom path to root of the monorepo in case this binary has been executed from a different location. + /// If not provided, it is going to get assumed that the current directory is the monorepo root + #[clap(long)] + monorepo_root: Option, + + /// Point to on-disk director containing .wasm files of all required Nym contracts + #[clap(long, group = "built-contracts")] + contracts_directory: Option, + + /// Provide a branch name to be used for attempting to retrieve .wasm files from the ci build server, + /// e.g. for branch `feature/my-amazing-feature`, the following urls will be used: + /// - `https://builds.ci.nymte.ch/feature/my-amazing-feature/mixnet_contract.wasm` + /// - `https://builds.ci.nymte.ch/feature/my-amazing-feature/nym_performance_contract.wasm` + /// - ... + /// - etc. + #[clap(long, group = "built-contracts")] + ci_build_branch: Option, + + /// Ensure contracts wasm code is fully reproducible by building those them + /// with linux/amd64 platform and forcing some additional cargo build flags. + /// Note: it will cause significant (build-time) overhead for M1 Macs + #[clap(long)] + reproducible_builds: bool, + + /// Specify whether the orchestrator can attempt to retrieve previously built cached contracts. + #[clap(long, conflicts_with = "reproducible_builds")] + allow_cached_build: bool, + + /// Specify whether internal service providers should run in open proxy mode + #[clap(long)] + open_proxy: bool, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + debug!("args: {args:#?}"); + + let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?; + + // TODO: allow non-fresh state + if orchestrator.state != LocalnetState::Uninitialised { + bail!("orchestrator is not in a fresh state") + } + + orchestrator + .start_localnet(up::Config { + nyxd_setup: nyxd::Config { + nyxd_repo: args.nyxd_repo, + nyxd_dockerfile_path: args.nyxd_dockerfile_path, + custom_dns: args.common.custom_dns.clone(), + nyxd_tag: args.nyxd_tag, + }, + contracts_setup: cosmwasm_contracts::Config { + reproducible_builds: args.reproducible_builds, + cosmwasm_optimizer_image: args.cosmwasm_optimizer_image.clone(), + explicit_contracts_directory: args.contracts_directory, + ci_build_branch: args.ci_build_branch, + monorepo_root: args.monorepo_root.clone(), + allow_cached_build: args.allow_cached_build, + }, + nym_api_setup: nym_api::Config { + cosmwasm_optimizer_image: args.cosmwasm_optimizer_image, + monorepo_root: args.monorepo_root.clone(), + custom_dns: args.common.custom_dns.clone(), + allow_cached_build: args.allow_cached_build, + }, + nym_nodes_setup: nym_nodes::Config { + monorepo_root: args.monorepo_root, + custom_dns: args.common.custom_dns, + open_proxy: args.open_proxy, + }, + }) + .await +} diff --git a/tools/internal/localnet-orchestrator/src/constants.rs b/tools/internal/localnet-orchestrator/src/constants.rs new file mode 100644 index 00000000000..c3e70de6618 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/constants.rs @@ -0,0 +1,36 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const LOCALNET_NYXD_IMAGE_NAME: &str = "localnet-nyxd"; +pub const LOCALNET_NYM_BINARIES_IMAGE_NAME: &str = "localnet-nym-binaries"; + +pub const LOCALNET_NYXD_CONTAINER_NAME_SUFFIX: &str = "localnet-nyxd"; +pub const LOCALNET_NYM_API_CONTAINER_NAME_SUFFIX: &str = "localnet-nym-api"; +pub const LOCALNET_NYM_NODE_CONTAINER_NAME_SUFFIX: &str = "localnet-nym-node"; + +pub const NYM_NODE_HTTP_BEARER: &str = "dQw4w9WgXcQ"; +pub const NYM_API_UTILITY_BEARER: &str = "dQw4w9WgXcQ"; + +pub const CONTAINER_NETWORK_NAME: &str = "nym-localnet"; + +// this value is quite arbitrary +pub const MIN_MASTER_UNYM_BALANCE: u128 = 10_000_000_000; + +pub const CI_BUILD_SERVER: &str = "https://builds.ci.nymte.ch"; + +pub const CARGO_REGISTRY_CACHE_VOLUME: &str = "registry_cache"; +pub const CONTRACTS_CACHE_VOLUME: &str = "nym_contracts_cache"; + +// filenames as created by our build pipeline as of 24.11.25 +pub mod contract_build_names { + pub const MULTISIG: &str = "cw3_flex_multisig.wasm"; + pub const GROUP: &str = "cw4_group.wasm"; + pub const MIXNET: &str = "mixnet_contract.wasm"; + pub const VESTING: &str = "vesting_contract.wasm"; + pub const DKG: &str = "nym_coconut_dkg.wasm"; + pub const ECASH: &str = "nym_ecash.wasm"; + pub const PERFORMANCE: &str = "nym_performance_contract.wasm"; + pub const NYM_POOL: &str = "nym_pool_contract.wasm"; + + pub const DKG_BYPASS_CONTRACT: &str = "dkg_bypass_contract.wasm"; +} diff --git a/tools/internal/localnet-orchestrator/src/helpers.rs b/tools/internal/localnet-orchestrator/src/helpers.rs new file mode 100644 index 00000000000..3c9ab55c245 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/helpers.rs @@ -0,0 +1,316 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{CI_BUILD_SERVER, NYM_API_UTILITY_BEARER, contract_build_names}; +use anyhow::{Context, bail}; +use bytes::Buf; +use futures::stream::StreamExt; +use indicatif::ProgressBar; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use std::env::current_dir; +use std::ffi::{OsStr, OsString}; +use std::fs::create_dir_all; +use std::future::Future; +use std::io::{BufWriter, Read}; +use std::path::{Path, PathBuf}; +use std::process::{Output, Stdio}; +use std::time::Duration; +use std::{fs, io}; +use tokio::pin; +use tokio::process::Command; +use tokio::time::interval; +use tracing::{debug, error}; +use url::Url; + +pub(crate) async fn async_with_progress(fut: F, pb: &ProgressBar) -> T +where + F: Future, +{ + pb.tick(); + pin!(fut); + let mut update_interval = interval(Duration::from_millis(50)); + + loop { + tokio::select! { + _ = update_interval.tick() => { + pb.tick() + } + res = &mut fut => { + return res + } + } + } +} + +pub(crate) fn wasm_code>(path: P) -> anyhow::Result> { + let path = path.as_ref(); + assert!(path.exists()); + let mut file = std::fs::File::open(path).context("failed to open wasm code")?; + let mut data = Vec::new(); + + file.read_to_end(&mut data) + .context("failed to read wasm code")?; + Ok(data) +} + +pub(crate) async fn download_cosmwasm_contract( + output_directory: impl AsRef, + ci_build_branch: &str, + contract_filename: &str, +) -> anyhow::Result<()> { + let output_directory = output_directory.as_ref(); + let download_target = output_directory.join(contract_filename); + + create_dir_all(output_directory)?; + + let download_url = format!("{CI_BUILD_SERVER}/{ci_build_branch}/{contract_filename}"); + let response = reqwest::get(download_url).await?; + + let mut source = response.bytes_stream(); + + let output_binary = fs::File::create(download_target)?; + let mut out = BufWriter::new(output_binary); + + while let Some(chunk) = source.next().await { + let mut bytes = chunk?.reader(); + io::copy(&mut bytes, &mut out)?; + } + + Ok(()) +} + +/// Does not explicitly return an `Err` for exit code != 0 +pub(crate) async fn exec_fallible_cmd_with_output( + cmd: S1, + args: I, +) -> anyhow::Result +where + I: IntoIterator, + S1: AsRef, + S2: AsRef, +{ + let (cmd, cmd_args) = debug_args(cmd, args); + + let output = Command::new(cmd.clone()) + .args(cmd_args.clone()) + .stdin(Stdio::null()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn()? + .wait_with_output() + .await + .inspect_err(|err| error!("{cmd:?} {cmd_args:?} FAILED WITH {err}"))?; + + Ok(output) +} + +pub(crate) async fn exec_inherit_output(cmd: S1, args: I) -> anyhow::Result +where + I: IntoIterator, + S1: AsRef, + S2: AsRef, +{ + let (cmd, cmd_args) = debug_args(cmd, args); + + let output = Command::new(cmd.clone()) + .args(cmd_args.clone()) + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()) + .kill_on_drop(true) + .spawn()? + .wait_with_output() + .await + .inspect_err(|err| error!("{cmd:?} {cmd_args:?} FAILED WITH {err}"))?; + + Ok(output) +} + +/// Does explicitly return an `Err` for exit code != 0 +pub(crate) async fn exec_cmd_with_output(cmd: S1, args: I) -> anyhow::Result +where + I: IntoIterator, + S1: AsRef, + S2: AsRef, +{ + let cmd = cmd.as_ref(); + let output = exec_fallible_cmd_with_output(cmd, args).await?; + + if !output.status.success() { + error!( + "'{}' exited with status {}", + cmd.to_string_lossy(), + output.status + ); + if !output.stderr.is_empty() { + error!("{}", String::from_utf8_lossy(&output.stderr)); + } + + bail!( + "'{}' exited with status {}", + cmd.to_string_lossy(), + output.status + ); + } + Ok(output) +} + +pub(crate) fn generate_network_name() -> String { + let mut rng = thread_rng(); + + let words = bip39::Language::English.word_list(); + // SAFETY: this list is not empty + #[allow(clippy::unwrap_used)] + let first = words.choose(&mut rng).unwrap(); + #[allow(clippy::unwrap_used)] + let second = words.choose(&mut rng).unwrap(); + format!("{first}-{second}") +} + +// ordering doesn't matter for the purposes of this function +pub(crate) fn nym_cosmwasm_contract_names() -> Vec<&'static str> { + vec![ + contract_build_names::MIXNET, + contract_build_names::VESTING, + contract_build_names::ECASH, + contract_build_names::DKG, + contract_build_names::GROUP, + contract_build_names::MULTISIG, + contract_build_names::PERFORMANCE, + ] +} + +// this is beyond hacky, but it works, for now* +// and is easier than attempting to retrieve the data from a running node +// (and node can't run without mixnet contract, for which we need the version) +// *assuming nym-node image is built from fresh +pub(crate) fn retrieve_current_nymnode_version>( + monorepo_root: P, +) -> anyhow::Result { + let nym_node_cargo_toml = monorepo_root.as_ref().join("nym-node/Cargo.toml"); + let manifest = cargo_edit::LocalManifest::find(Some(&nym_node_cargo_toml))?; + Ok(manifest + .data + .get("package") + .context("malformed nym-node Cargo.toml file - no 'package' section")? + .get("version") + .context("malformed nym-node Cargo.toml file - no [package].version set")? + .as_str() + .context("malformed nym-node Cargo.toml file - [package].version is not a string!")? + .to_string()) +} + +pub(crate) fn monorepo_root_path(arg: Option) -> anyhow::Result { + let maybe_path = match arg { + Some(path) => path, + None => { + // ASSUMPTION: we're being run from the root of the nym repo + current_dir()? + } + }; + + if !maybe_path.exists() { + bail!("'{}' does not exist", maybe_path.display()); + } + + let maybe_path_canon = maybe_path.canonicalize()?; + + // don't allow such degenerative cases + let dir = maybe_path_canon + .components() + .next_back() + .context("attempted to execute orchestrator from the root of the filesystem")?; + if dir.as_os_str().to_string_lossy() != "nym" { + bail!( + "localnet-orchestrator must be executed from the root of the nym repo! the path is {maybe_path_canon:?}" + ); + } + + Ok(maybe_path) +} + +pub(crate) fn nym_api_cache_refresh_script( + cache_timestamp_route: Url, + cache_refresh_route: Url, +) -> String { + // I prefer inlining the scripts over putting them in dedicated files (for the localnet purpose) + // to the better flexibility in being able to modify them more easily + format!( + r#" +set -euo pipefail + +# initial ts +initial_ts=$(curl --fail-with-body -s \ + -H "Authorization: Bearer {NYM_API_UTILITY_BEARER}" \ + {cache_timestamp_route} | jq -r '.timestamp') + +# refresh cache +curl --fail-with-body -s -X POST {cache_refresh_route} \ + -H "Authorization: Bearer {NYM_API_UTILITY_BEARER}" \ + -H "Content-Type: application/json" \ + -d '{{}}' > /dev/null + +# wait for the cache to actually get refreshed +while true; do + current_ts=$(curl --fail-with-body -s \ + -H "Authorization: Bearer {NYM_API_UTILITY_BEARER}" \ + {cache_timestamp_route} | jq -r '.timestamp') + + if [ "$(date -d "$current_ts" +%s%N)" -gt "$(date -d "$initial_ts" +%s%N)" ]; then + break + fi + + sleep 0.2 +done + "#, + ) +} + +fn debug_args(cmd: S1, args: I) -> (OsString, Vec) +where + I: IntoIterator, + S1: AsRef, + S2: AsRef, +{ + let mut cmd_args = Vec::new(); + let mut args_debug = Vec::new(); + for arg in args { + let arg = arg.as_ref(); + args_debug.push(arg.to_string_lossy().to_string()); + cmd_args.push(arg.to_os_string()); + } + + let cmd = cmd.as_ref().to_os_string(); + let cmd_debug = cmd.to_string_lossy(); + + debug!("executing: {cmd_debug} {}", args_debug.join(" ")); + + (cmd, cmd_args) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::orchestrator::cosmwasm_contract::ContractBeingInitialised; + use crate::orchestrator::network::NymContractsBeingInitialised; + + #[test] + fn all_contracts_are_included() { + let contracts = NymContractsBeingInitialised { + mixnet: ContractBeingInitialised::new("mixnet"), + vesting: ContractBeingInitialised::new("vesting"), + ecash: ContractBeingInitialised::new("ecash"), + cw3_multisig: ContractBeingInitialised::new("cw3-multisig"), + cw4_group: ContractBeingInitialised::new("cw4-group"), + dkg: ContractBeingInitialised::new("dkg"), + performance: ContractBeingInitialised::new("performance"), + }; + + assert_eq!( + nym_cosmwasm_contract_names().len(), + NymContractsBeingInitialised::COUNT + ); + } +} diff --git a/tools/internal/testnet-manager/src/main.rs b/tools/internal/localnet-orchestrator/src/main.rs similarity index 73% rename from tools/internal/testnet-manager/src/main.rs rename to tools/internal/localnet-orchestrator/src/main.rs index 49d93a46dce..15552b6c2e4 100644 --- a/tools/internal/testnet-manager/src/main.rs +++ b/tools/internal/localnet-orchestrator/src/main.rs @@ -1,17 +1,15 @@ -// Copyright 2024 - Nym Technologies SA +// Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only // Allow dead code for not(unix) #![cfg_attr(not(unix), allow(dead_code))] +extern crate core; -#[cfg(unix)] -pub(crate) mod cli; -#[cfg(unix)] -pub(crate) mod error; -#[cfg(unix)] -mod helpers; -#[cfg(unix)] -mod manager; +pub mod cli; +pub mod constants; +pub mod helpers; +pub mod orchestrator; +pub mod serde_helpers; #[cfg(unix)] #[tokio::main] @@ -35,5 +33,5 @@ async fn main() -> anyhow::Result<()> { #[cfg(not(unix))] fn main() { - println!("This binary is only supported on Unix systems"); + eprintln!("This binary is only supported on Unix systems"); } diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/account.rs b/tools/internal/localnet-orchestrator/src/orchestrator/account.rs new file mode 100644 index 00000000000..c1a715c7e38 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/account.rs @@ -0,0 +1,34 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_validator_client::DirectSecp256k1HdWallet; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::signing::signer::OfflineSigner; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub(crate) struct Account { + /// n1 address, e.g. 'n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy' + pub(crate) address: AccountId, + + /// mnemonic associated with the account + pub(crate) mnemonic: bip39::Mnemonic, +} + +impl Account { + // SAFETY: we're using valid constants + #[allow(clippy::unwrap_used)] + pub(crate) fn new() -> Account { + let mnemonic = bip39::Mnemonic::generate(24).unwrap(); + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic("n", mnemonic.clone()).unwrap(); + let acc = wallet.get_accounts().first().unwrap(); + Account { + address: acc.address.clone(), + mnemonic, + } + } + + pub(crate) fn address(&self) -> AccountId { + self.address.clone() + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/linux.rs b/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/linux.rs new file mode 100644 index 00000000000..e58d576c7bc --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/linux.rs @@ -0,0 +1,75 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::CONTAINER_NETWORK_NAME; +use crate::helpers::exec_fallible_cmd_with_output; +use crate::orchestrator::container_helpers::container_binary; +use crate::orchestrator::context::LocalnetContext; +use crate::serde_helpers::linux::container_network_inspect::NetworkInspect; +use crate::serde_helpers::{ContainerInspect, ContainersList, linux}; +use anyhow::Context; + +pub(crate) async fn try_inspect_container_network() -> anyhow::Result> { + let container_bin = container_binary(); + + let output = exec_fallible_cmd_with_output( + container_bin, + ["network", "inspect", CONTAINER_NETWORK_NAME], + ) + .await?; + if !output.status.success() { + return Ok(None); + } + let network_details: NetworkInspect = serde_json::from_slice(&output.stdout) + .context("failed to deserialise network information")?; + Ok(Some(network_details)) +} + +pub(crate) async fn is_container_network_running() -> anyhow::Result { + let Some(network_details) = try_inspect_container_network().await? else { + return Ok(false); + }; + Ok(network_details.is_running()) +} + +pub(crate) async fn inspect_container( + ctx: &LocalnetContext, + container_name: &str, +) -> anyhow::Result { + let container_bin = container_binary(); + + let output = ctx + .exec_fallible_cmd_with_output(container_bin, ["inspect", container_name]) + .await?; + if !output.status.success() { + return Ok(ContainerInspect::new_empty_container()); + } + + let inspect_info: linux::ContainerInspect = serde_json::from_slice(&output.stdout) + .context("failed to deserialise container information")?; + inspect_info.try_into() +} + +pub(crate) async fn list_containers(ctx: &LocalnetContext) -> anyhow::Result { + let container_bin = container_binary(); + + let output = ctx + .exec_fallible_cmd_with_output(container_bin, ["container", "ls", "-a", "--format", "json"]) + .await?; + if !output.status.success() { + return Ok(ContainersList::new_empty()); + } + // the output is per container so we need to split it + let output_str = String::from_utf8(output.stdout)?; + let containers = output_str + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|l| { + serde_json::from_str::(l) + .context("container info deserialisation failure") + }) + .collect::>>()?; + + let containers_list = linux::ContainersList(containers); + containers_list.try_into() +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/macos.rs b/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/macos.rs new file mode 100644 index 00000000000..1365d2a0ab1 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/macos.rs @@ -0,0 +1,53 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::CONTAINER_NETWORK_NAME; +use crate::helpers::exec_cmd_with_output; +use crate::orchestrator::container_helpers::container_binary; +use crate::orchestrator::context::LocalnetContext; +use crate::serde_helpers::macos::{self, container_network_inspect::NetworkInspect}; +use crate::serde_helpers::{ContainerInspect, ContainersList}; +use anyhow::Context; + +pub(crate) async fn inspect_container_network() -> anyhow::Result { + let container_bin = container_binary(); + + let output = exec_cmd_with_output( + container_bin, + ["network", "inspect", CONTAINER_NETWORK_NAME], + ) + .await?; + let network_details: NetworkInspect = serde_json::from_slice(&output.stdout) + .context("failed to deserialise network information")?; + Ok(network_details) +} + +pub(crate) async fn is_container_network_running() -> anyhow::Result { + let network_details = inspect_container_network().await?; + Ok(network_details.is_running()) +} + +pub(crate) async fn inspect_container( + ctx: &LocalnetContext, + container_name: &str, +) -> anyhow::Result { + let container_bin = container_binary(); + + let stdout = ctx + .execute_cmd_with_stdout(container_bin, ["inspect", container_name]) + .await?; + let inspect_info: macos::ContainerInspect = + serde_json::from_slice(&stdout).context("failed to deserialise container information")?; + inspect_info.try_into() +} + +pub(crate) async fn list_containers(ctx: &LocalnetContext) -> anyhow::Result { + let container_bin = container_binary(); + + let stdout = ctx + .execute_cmd_with_stdout(container_bin, ["ls", "-a", "--format", "json"]) + .await?; + let containers_list: macos::ContainersList = + serde_json::from_slice(&stdout).context("failed to deserialise containers list")?; + containers_list.try_into() +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/mod.rs b/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/mod.rs new file mode 100644 index 00000000000..7de2f7532c9 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/mod.rs @@ -0,0 +1,358 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + CONTAINER_NETWORK_NAME, LOCALNET_NYM_API_CONTAINER_NAME_SUFFIX, + LOCALNET_NYM_BINARIES_IMAGE_NAME, LOCALNET_NYM_NODE_CONTAINER_NAME_SUFFIX, + LOCALNET_NYXD_CONTAINER_NAME_SUFFIX, +}; +use crate::helpers::{exec_cmd_with_output, retrieve_current_nymnode_version}; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::context::LocalnetContext; +use nym_mixnet_contract_common::NodeId; +use std::ffi::{OsStr, OsString}; +use std::net::IpAddr; +use std::path::Path; +use std::process::ExitStatus; +use tracing::info; + +#[cfg(target_os = "linux")] +pub(crate) use linux::*; + +#[cfg(target_os = "macos")] +pub(crate) use macos::*; + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "macos")] +mod macos; + +impl LocalnetOrchestrator { + pub(crate) fn nyxd_container_name(&self) -> String { + format!( + "{}-{}", + self.localnet_details.human_name, LOCALNET_NYXD_CONTAINER_NAME_SUFFIX + ) + } + + pub(crate) fn nym_api_container_name(&self) -> String { + format!( + "{}-{}", + self.localnet_details.human_name, LOCALNET_NYM_API_CONTAINER_NAME_SUFFIX + ) + } + + pub(crate) fn nym_node_container_name(&self, id: NodeId) -> String { + self.nym_node_name(id) + } + + pub(crate) fn nym_node_name(&self, id: NodeId) -> String { + format!( + "{}-{}-{id}", + self.localnet_details.human_name, LOCALNET_NYM_NODE_CONTAINER_NAME_SUFFIX + ) + } + + #[allow(clippy::unwrap_used)] + pub(crate) fn nyxd_volume(&self) -> String { + // SAFETY: directory had been sanitised before getting here + format!( + "{}:/root/.nyxd", + self.storage + .nyxd_container_data_directory() + .canonicalize() + .unwrap() + .to_string_lossy() + ) + } + + #[allow(clippy::unwrap_used)] + pub(crate) fn nym_api_volume(&self) -> String { + // SAFETY: directory had been sanitised before getting here + format!( + "{}:/root/.nym/nym-api/default", + self.storage + .nym_api_container_data_directory() + .canonicalize() + .unwrap() + .to_string_lossy() + ) + } + + #[allow(clippy::unwrap_used)] + pub(crate) fn nym_node_volume(&self, id: NodeId) -> String { + // SAFETY: directory had been sanitised before getting here + format!( + "{}:/root/.nym/nym-nodes/default-nym-node", + self.storage + .nym_node_container_data_directory(id) + .canonicalize() + .unwrap() + .to_string_lossy() + ) + } + + #[allow(clippy::unwrap_used)] + pub(crate) fn kernel_configs_volume(&self) -> String { + // SAFETY: directory had been sanitised before getting here + format!( + "{}:/root/kernel-configs", + self.storage + .data_cache() + .kernel_configs_directory() + .canonicalize() + .unwrap() + .to_string_lossy() + ) + } +} + +#[allow(clippy::panic)] +pub(crate) fn container_binary() -> &'static str { + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + "container" + } else if #[cfg(target_os = "linux")] { + "nerdctl" + } else { + panic!("unsupported platform") + } + } +} + +pub(crate) async fn save_docker_image( + ctx: &mut LocalnetContext, + output_path: &str, + image_tag: &str, +) -> anyhow::Result<()> { + ctx.begin_next_step("saving the docker image to a temporary file...", "๐Ÿ’พ๏ธ"); + + ctx.execute_cmd_with_exit_status("docker", ["save", "-o", output_path, image_tag]) + .await?; + Ok(()) +} + +pub(crate) async fn load_image_into_container_runtime( + ctx: &mut LocalnetContext, + saved_image_path: &str, +) -> anyhow::Result<()> { + let container_bin = container_binary(); + ctx.begin_next_step("inserting docker image into the container runtime...", "๐Ÿ“ฉ"); + + ctx.execute_cmd_with_exit_status( + container_bin, + ["image", "load", "--input", saved_image_path], + ) + .await?; + + Ok(()) +} + +pub(crate) async fn remove_container_image( + ctx: &LocalnetContext, + image_tag: &str, +) -> anyhow::Result<()> { + let container_bin = container_binary(); + + ctx.execute_cmd_with_stdout(container_bin, ["image", "rm", image_tag]) + .await?; + Ok(()) +} + +pub(crate) async fn check_container_image_exists( + ctx: &LocalnetContext, + image_tag: &str, +) -> anyhow::Result { + let container_bin = container_binary(); + + let status = ctx + .exec_fallible_cmd_with_exit_status(container_bin, ["image", "inspect", image_tag]) + .await?; + + Ok(status.success()) +} + +pub(crate) async fn stop_container( + ctx: &LocalnetContext, + container_name: &str, +) -> anyhow::Result<()> { + let container_bin = container_binary(); + + ctx.execute_cmd_with_stdout(container_bin, ["stop", container_name]) + .await?; + Ok(()) +} + +pub(crate) async fn remove_container( + ctx: &LocalnetContext, + container_name: &str, +) -> anyhow::Result<()> { + let container_bin = container_binary(); + + ctx.execute_cmd_with_stdout(container_bin, ["rm", container_name]) + .await?; + Ok(()) +} + +pub(crate) async fn check_container_is_running( + ctx: &LocalnetContext, + container_name: &str, +) -> anyhow::Result { + let container_info = inspect_container(ctx, container_name).await?; + Ok(container_info.is_running()) +} + +pub(crate) async fn get_container_ip_address( + ctx: &LocalnetContext, + container_name: &str, +) -> anyhow::Result { + let container_info = inspect_container(ctx, container_name).await?; + container_info.container_ip() +} + +pub(crate) async fn create_container_network() -> anyhow::Result<()> { + let container_bin = container_binary(); + + info!("creating {CONTAINER_NETWORK_NAME} network"); + exec_cmd_with_output(container_bin, ["network", "create", CONTAINER_NETWORK_NAME]).await?; + Ok(()) +} + +async fn run_container_cmd( + ctx: &LocalnetContext, + sub_cmd: OsString, + mut args: Vec, +) -> anyhow::Result> { + let container_bin = container_binary(); + args.insert(0, sub_cmd); + + ctx.execute_cmd_with_stdout(container_bin, args).await +} + +async fn run_container_cmd_fallible( + ctx: &LocalnetContext, + sub_cmd: OsString, + mut args: Vec, +) -> anyhow::Result { + let container_bin = container_binary(); + args.insert(0, sub_cmd); + + ctx.exec_fallible_cmd_with_exit_status(container_bin, args) + .await +} + +pub(crate) fn attach_run_container_args(base_args: I) -> Vec +where + I: IntoIterator, + S: AsRef, +{ + let mut cmd_args: Vec = Vec::new(); + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + cmd_args.push("--arch".into()); + cmd_args.push("amd64".into()); + } else if #[cfg(target_os = "linux")] { + cmd_args.push("--runtime".into()); + cmd_args.push("io.containerd.kata.v2".into()); + cmd_args.push("--device".into()); + cmd_args.push("/dev/net/tun".into()); + cmd_args.push("--privileged".into()); + cmd_args.push("--security-opt".into()); + cmd_args.push("privileged-without-host-devices".into()); + } + } + + for arg in base_args { + cmd_args.push(arg.as_ref().into()); + } + cmd_args +} + +pub(crate) async fn run_container( + ctx: &LocalnetContext, + args: I, + dns: Option, +) -> anyhow::Result> +where + I: IntoIterator, + S: AsRef, +{ + let mut cmd_args = attach_run_container_args(args); + if let Some(dns) = dns { + // --dns $DNS + cmd_args.insert(0, "--dns".into()); + cmd_args.insert(1, dns.into()); + } + + run_container_cmd(ctx, "run".into(), cmd_args).await +} + +// no progress bar +pub(crate) async fn run_container_fut(args: I) -> anyhow::Result<()> +where + I: IntoIterator, + S: AsRef, +{ + let container_bin = container_binary(); + + let mut cmd_args: Vec = Vec::new(); + cmd_args.push("run".into()); + + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + cmd_args.push("--arch".into()); + cmd_args.push("amd64".into()); + } + } + + for arg in args { + cmd_args.push(arg.as_ref().into()); + } + + exec_cmd_with_output(container_bin, cmd_args).await?; + Ok(()) +} + +pub(crate) async fn run_container_fallible( + ctx: &LocalnetContext, + args: I, +) -> anyhow::Result +where + I: IntoIterator, + S: AsRef, +{ + run_container_cmd_fallible( + ctx, + "run".into(), + args.into_iter() + .map(|a| a.as_ref().to_os_string()) + .collect(), + ) + .await +} + +pub(crate) async fn exec_container( + ctx: &LocalnetContext, + args: I, +) -> anyhow::Result> +where + I: IntoIterator, + S: AsRef, +{ + run_container_cmd( + ctx, + "exec".into(), + args.into_iter() + .map(|a| a.as_ref().to_os_string()) + .collect(), + ) + .await +} + +pub(crate) fn default_nym_binaries_image_tag( + monorepo_root: impl AsRef, +) -> anyhow::Result { + let version = retrieve_current_nymnode_version(monorepo_root)?; + Ok(format!("{LOCALNET_NYM_BINARIES_IMAGE_NAME}:{version}")) +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/context.rs b/tools/internal/localnet-orchestrator/src/orchestrator/context.rs new file mode 100644 index 00000000000..25743f1d867 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/context.rs @@ -0,0 +1,218 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::{async_with_progress, exec_cmd_with_output, exec_fallible_cmd_with_output}; +use console::{Emoji, style}; +use indicatif::{HumanDuration, ProgressBar, ProgressStyle}; +use nym_validator_client::nyxd::Coin; +use std::borrow::Cow; +use std::ffi::OsStr; +use std::io::IsTerminal; +use std::process::{ExitStatus, Output}; +use std::time::Instant; +use tracing::info; + +#[derive(Default)] +pub(crate) struct Empty; + +pub(crate) struct LocalnetContext { + pub(crate) data: T, + progress_tracker: ProgressTracker, + current_step: usize, + steps: usize, +} + +pub(crate) fn ephemeral_context(msg: impl AsRef) -> LocalnetContext { + LocalnetContext::ephemeral(msg) +} + +impl LocalnetContext { + pub(crate) fn ephemeral(msg: impl AsRef) -> Self { + LocalnetContext::new(Empty, 1, msg) + } +} + +impl LocalnetContext { + pub(crate) fn new(data: T, steps: usize, msg: impl AsRef) -> Self { + LocalnetContext { + data, + progress_tracker: ProgressTracker::new(msg), + current_step: 0, + steps, + } + } + + pub(crate) fn skip_steps(&mut self, steps: usize) { + self.current_step += steps; + } + + pub(crate) fn begin_next_step( + &mut self, + msg: impl AsRef, + emoji: impl Into>, + ) { + self.current_step += 1; + + let emoji = match emoji.into() { + Some(emoji) => Emoji::new(emoji, ">"), + None => Emoji(">", ">"), + }; + let msg = msg.as_ref(); + + let progress = format!("{}/{}", self.current_step, self.steps); + self.println(format!("{emoji} {} {msg}", style(progress).bold().dim())); + self.set_pb_prefix(""); + self.set_pb_message(format!("{emoji} {msg}")) + } + + pub(crate) fn println>(&self, msg: I) { + self.progress_tracker.println(msg) + } + + pub(crate) fn println_with_emoji>(&self, msg: I, emoji: &str) { + self.progress_tracker.println_with_emoji(msg, emoji) + } + + pub(crate) fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_tracker.set_pb_prefix(prefix) + } + + pub(crate) fn set_pb_message(&self, msg: impl Into>) { + self.progress_tracker.set_pb_message(msg) + } + + pub(crate) async fn async_with_progress(&self, fut: F) -> O + where + F: Future, + { + async_with_progress(fut, &self.progress_tracker.progress_bar).await + } + + /// Does not explicitly return an `Err` for exit code != 0 + pub(crate) async fn exec_fallible_cmd_with_exit_status( + &self, + cmd: S1, + args: I, + ) -> anyhow::Result + where + I: IntoIterator, + S1: AsRef, + S2: AsRef, + { + let fut = exec_fallible_cmd_with_output(cmd, args); + Ok(self.async_with_progress(fut).await?.status) + } + + /// Does explicitly return an `Err` for exit code != 0 + pub(crate) async fn execute_cmd_with_exit_status( + &self, + cmd: S1, + args: I, + ) -> anyhow::Result + where + I: IntoIterator, + S1: AsRef, + S2: AsRef, + { + let fut = exec_cmd_with_output(cmd, args); + Ok(self.async_with_progress(fut).await?.status) + } + + // depends on target + #[allow(dead_code)] + pub(crate) async fn exec_fallible_cmd_with_output( + &self, + cmd: S1, + args: I, + ) -> anyhow::Result + where + I: IntoIterator, + S1: AsRef, + S2: AsRef, + { + let fut = exec_fallible_cmd_with_output(cmd, args); + self.async_with_progress(fut).await + } + + pub(crate) async fn execute_cmd_with_stdout( + &self, + cmd: S1, + args: I, + ) -> anyhow::Result> + where + I: IntoIterator, + S1: AsRef, + S2: AsRef, + { + let fut = exec_cmd_with_output(cmd, args); + Ok(self.async_with_progress(fut).await?.stdout) + } + + pub(crate) fn unyms(&self, amount: u128) -> Vec { + vec![self.unym(amount)] + } + + pub(crate) fn unym(&self, amount: u128) -> Coin { + Coin::new(amount, "unym") + } +} + +pub(crate) struct ProgressTracker { + start: Instant, + pub(crate) progress_bar: ProgressBar, +} + +impl ProgressTracker { + pub(crate) fn new>(msg: I) -> Self { + // SAFETY: this is a valid template + let progress_bar = ProgressBar::new_spinner(); + + #[allow(clippy::unwrap_used)] + progress_bar.set_style(ProgressStyle::with_template("{spinner} {prefix} {msg}").unwrap()); + progress_bar.println(style(msg.as_ref()).bold().to_string()); + + ProgressTracker { + start: Instant::now(), + progress_bar, + } + } + + pub(crate) fn println>(&self, msg: I) { + if std::io::stdout().is_terminal() { + self.progress_bar.println(msg) + } else { + info!("{}", msg.as_ref()); + } + } + + pub(crate) fn println_with_emoji>(&self, msg: I, emoji: &str) { + self.println(format!("{} {}", Emoji::new(emoji, ""), msg.as_ref())); + } + + pub(crate) fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_bar.set_prefix(prefix) + } + + pub(crate) fn set_pb_message(&self, msg: impl Into>) { + self.progress_bar.set_message(msg) + } +} + +impl Default for ProgressTracker { + fn default() -> Self { + ProgressTracker { + start: Instant::now(), + progress_bar: ProgressBar::new_spinner(), + } + } +} + +impl Drop for ProgressTracker { + fn drop(&mut self) { + self.println_with_emoji( + format!("Done in {}", HumanDuration(self.start.elapsed())), + "โœจ", + ); + self.progress_bar.finish_and_clear(); + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/cosmwasm_contract.rs b/tools/internal/localnet-orchestrator/src/orchestrator/cosmwasm_contract.rs new file mode 100644 index 00000000000..154a7c0664b --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/cosmwasm_contract.rs @@ -0,0 +1,138 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::account::Account; +use anyhow::Context; +use nym_contracts_common::ContractBuildInformation; +use nym_validator_client::nyxd::cosmwasm_client::types::{ + ContractCodeId, InstantiateResult, MigrateResult, UploadResult, +}; +use nym_validator_client::nyxd::{AccountId, Hash}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct CosmwasmContract { + /// Name associated with the contract, e.g. 'mixnet', 'performance', etc. + pub(crate) name: String, + + /// n1 address of the contract + pub(crate) address: AccountId, + + /// n1 address and mnemonic of the contract admin (i.e. wallet that is allowed to perform migrations) + pub(crate) admin: Account, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct ContractBeingInitialised { + pub(crate) name: String, + pub(crate) wasm_path: Option, + pub(crate) upload_info: Option, + pub(crate) admin: Option, + pub(crate) init_info: Option, + pub(crate) migrate_info: Option, + pub(crate) build_info: Option, +} + +impl ContractBeingInitialised { + pub(crate) fn new>(name: S) -> Self { + ContractBeingInitialised { + name: name.into(), + wasm_path: None, + upload_info: None, + admin: None, + init_info: None, + migrate_info: None, + build_info: None, + } + } + + pub(crate) fn wasm_path(&self) -> anyhow::Result<&PathBuf> { + self.wasm_path.as_ref().context(format!( + "could not find .wasm file for {} contract under the provided directory", + self.name + )) + } + + pub(crate) fn upload_info(&self) -> anyhow::Result<&MinimalUploadInfo> { + self.upload_info + .as_ref() + .context(format!("could not find code_id for {} contract", self.name)) + } + + pub(crate) fn code_id(&self) -> anyhow::Result { + Ok(self.upload_info()?.code_id) + } + + pub(crate) fn admin(&self) -> anyhow::Result<&Account> { + self.admin.as_ref().context(format!( + "could not find contract admin for {} contract", + self.name + )) + } + + pub(crate) fn admin_address(&self) -> anyhow::Result { + Ok(self.admin()?.address.clone()) + } + + pub(crate) fn init_info(&self) -> anyhow::Result<&MinimalInitInfo> { + self.init_info + .as_ref() + .context(format!("could not find address for {} contract", self.name)) + } + + #[allow(dead_code)] + pub(crate) fn build_info(&self) -> anyhow::Result<&ContractBuildInformation> { + self.build_info.as_ref().context(format!( + "could not find build information for {} contract", + self.name + )) + } + + pub(crate) fn address(&self) -> anyhow::Result<&AccountId> { + self.init_info().map(|info| &info.contract_address) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub(crate) struct MinimalUploadInfo { + pub transaction_hash: Hash, + pub code_id: ContractCodeId, +} + +impl From for MinimalUploadInfo { + fn from(value: UploadResult) -> Self { + MinimalUploadInfo { + transaction_hash: value.transaction_hash, + code_id: value.code_id, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub(crate) struct MinimalInitInfo { + pub transaction_hash: Hash, + pub contract_address: AccountId, +} + +impl From for MinimalInitInfo { + fn from(value: InstantiateResult) -> Self { + MinimalInitInfo { + transaction_hash: value.transaction_hash, + contract_address: value.contract_address, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub(crate) struct MinimalMigrateInfo { + pub transaction_hash: Hash, +} + +impl From for MinimalMigrateInfo { + fn from(value: MigrateResult) -> Self { + MinimalMigrateInfo { + transaction_hash: value.transaction_hash, + } + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/helpers.rs b/tools/internal/localnet-orchestrator/src/orchestrator/helpers.rs new file mode 100644 index 00000000000..1b5f85c4749 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/helpers.rs @@ -0,0 +1,176 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::MIN_MASTER_UNYM_BALANCE; +use crate::helpers::wasm_code; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::container_helpers::check_container_is_running; +use crate::orchestrator::context::LocalnetContext; +use crate::orchestrator::setup::nym_nodes::{GATEWAYS, MIXNODES}; +use anyhow::{Context, bail}; +use nym_mixnet_contract_common::NodeId; +use nym_validator_client::nyxd::CosmWasmClient; +use nym_validator_client::nyxd::cosmwasm_client::types::UploadResult; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use std::path::Path; + +impl LocalnetOrchestrator { + pub(crate) fn rpc_query_client(&self) -> anyhow::Result { + let rpc_endpoint = self.localnet_details.localhost_rpc_endpoint()?; + let network_details = self.localnet_details.nym_network_details()?; + + QueryHttpRpcNyxdClient::connect_with_network_details(rpc_endpoint.as_str(), network_details) + .context("nyxd query client creation failure") + } + + pub(crate) fn signing_client( + &self, + mnemonic: &bip39::Mnemonic, + ) -> anyhow::Result { + let rpc_endpoint = self.localnet_details.localhost_rpc_endpoint()?; + let network_details = self.localnet_details.nym_network_details()?; + let mnemonic = mnemonic.clone(); + DirectSigningHttpRpcNyxdClient::connect_with_mnemonic_and_network_details( + rpc_endpoint.as_str(), + network_details, + mnemonic, + ) + .context("nyxd signing client creation failure") + } + + pub(crate) fn master_signing_client(&self) -> anyhow::Result { + let mnemonic = &self + .localnet_details + .nyxd_details()? + .master_account + .mnemonic; + self.signing_client(mnemonic) + } + + pub(crate) fn mixnet_rewarder_signing_client( + &self, + ) -> anyhow::Result { + let mnemonic = &self + .localnet_details + .auxiliary_accounts()? + .mixnet_rewarder + .mnemonic; + self.signing_client(mnemonic) + } + + pub(crate) async fn check_nyxd_container_is_running( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + check_container_is_running(ctx, &self.nyxd_container_name()).await + } + + pub(crate) async fn check_nym_api_container_is_running( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + check_container_is_running(ctx, &self.nym_api_container_name()).await + } + + pub(crate) async fn check_nym_node_containers_are_running( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + let mut running = 0; + for id in 1..=GATEWAYS + MIXNODES { + if check_container_is_running(ctx, &self.nym_node_container_name(id as NodeId)).await? { + running += 1; + } + } + // either ALL containers must be running or NONE of them. we must not be in a zombie state + if running == 0 { + return Ok(false); + } + if running == GATEWAYS + MIXNODES { + return Ok(true); + } + bail!("only a subset of nym node containers is running! this is not allowed ({running}/4") + } + + pub(crate) async fn verify_master_account( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result<()> { + // essentially perform two checks in one: + // 1. is the rpc node running at the expected address + // 2. is the master account really the main one? - we don't need to be incredibly restrictive, + // i.e. whether it has staked on validators and whatnot. we only care it has sufficient + // amount of tokens + let client = self.rpc_query_client()?; + let address = self + .localnet_details + .nyxd_details()? + .master_account + .address(); + + let balance_fut = client.get_balance(&address, "unym".to_string()); + let balance = ctx + .async_with_progress(balance_fut) + .await + .context(format!("failed to retrieve unym balance of {address}"))? + .context(format!("{address} does not have any unym"))?; + + if balance.amount < MIN_MASTER_UNYM_BALANCE { + bail!( + "the unym balance of {address} ({balance}) is smaller than the minimum value of {MIN_MASTER_UNYM_BALANCE}" + ) + } + + Ok(()) + } + + pub(crate) async fn upload_contract, T>( + &self, + ctx: &LocalnetContext, + path: P, + ) -> anyhow::Result { + let wasm = wasm_code(path)?; + let admin = self.master_signing_client()?; + let upload_future = admin.upload(wasm, "localnet contract upload", None); + + ctx.async_with_progress(upload_future) + .await + .context("contract upload failure") + } + + pub(crate) async fn try_build_nym_binaries_docker_image( + &self, + ctx: &mut LocalnetContext, + dockerfile_path: impl AsRef, + monorepo_path: impl AsRef, + image_tag: &str, + ) -> anyhow::Result<()> { + ctx.begin_next_step( + "building localnet-nym-binaries docker image... this might take few minutes...", + "๐Ÿ—๏ธ", + ); + let dockerfile_path = dockerfile_path.as_ref().to_path_buf(); + let dockerfile_path_arg = dockerfile_path + .to_str() + .context("invalid Dockerfile path")?; + + let monorepo_path = monorepo_path.as_ref().to_path_buf(); + let monorepo_path_arg = monorepo_path.to_str().context("invalid monorepo path")?; + + ctx.execute_cmd_with_exit_status( + "docker", + [ + "build", + "--platform", + "linux/amd64", + "-f", + dockerfile_path_arg, + "-t", + image_tag, + monorepo_path_arg, + ], + ) + .await?; + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/mod.rs b/tools/internal/localnet-orchestrator/src/orchestrator/mod.rs new file mode 100644 index 00000000000..80f115e9b50 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/mod.rs @@ -0,0 +1,299 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::constants::CONTAINER_NETWORK_NAME; +use crate::helpers::{exec_fallible_cmd_with_output, generate_network_name}; +use crate::orchestrator::container_helpers::{ + create_container_network, is_container_network_running, run_container, +}; +use crate::orchestrator::context::{LocalnetContext, ephemeral_context}; +use crate::orchestrator::network::Localnet; +use crate::orchestrator::state::LocalnetState; +use crate::orchestrator::storage::orchestrator::LocalnetOrchestratorStorage; +use crate::orchestrator::storage::{ + LocalnetStorage, default_cache_dir, default_orchestrator_db_file, default_storage_dir, +}; +use anyhow::{Context, bail}; +use std::collections::HashMap; +use std::env::temp_dir; +use std::fs; +use tracing::info; + +pub mod account; +pub(crate) mod container_helpers; +pub(crate) mod context; +pub(crate) mod cosmwasm_contract; +pub(crate) mod helpers; +pub(crate) mod network; +pub(crate) mod nym_node; +pub(crate) mod setup; +pub(crate) mod state; +pub(crate) mod storage; +pub(crate) mod test_cmds; + +pub(crate) struct LocalnetOrchestrator { + pub(crate) state: LocalnetState, + + pub(crate) localnet_details: Localnet, + pub(crate) storage: LocalnetStorage, +} + +impl LocalnetOrchestrator { + pub(crate) async fn new(args: &CommonArgs) -> anyhow::Result { + let orchestrator_data = args + .orchestrator_db + .clone() + .unwrap_or_else(default_orchestrator_db_file); + let orchestrator_storage = LocalnetOrchestratorStorage::init(orchestrator_data).await?; + + // if network name has not been explicitly provided, we use the latest one created + // or if this is the first one, we generate a new one + let network_name = match args.existing_network.clone() { + // name provided => see if it existed + Some(network_name) => { + // sanity check: try to load metadata (it will fail if entry does not exist) + let _ = orchestrator_storage + .get_localnet_metadata_by_name(&network_name) + .await?; + network_name + } + // name not provided + None => { + let metadata = orchestrator_storage.get_last_created().await?; + // have we initialised anything before? + match metadata.latest_network_id { + // no => create new entry + None => { + let network_name = generate_network_name(); + orchestrator_storage + .save_new_localnet_metadata(&network_name) + .await?; + network_name + } + // yes => attempt to retrieve it + Some(localnet_id) => { + orchestrator_storage + .get_localnet_metadata(localnet_id) + .await? + .name + } + } + } + }; + + let localnet_directory = match args.localnet_storage_path.clone() { + Some(localnet_storage_path) => localnet_storage_path, + None => { + if args.ephemeral { + temp_dir().join(&network_name) + } else { + default_storage_dir().join(&network_name) + } + } + }; + + info!("setting up network '{network_name}'"); + info!("main storage directory: '{}'", localnet_directory.display()); + + let cache_dir = default_cache_dir(); + + let mut this = LocalnetOrchestrator { + state: Default::default(), + storage: LocalnetStorage::new(localnet_directory, cache_dir, orchestrator_storage)?, + localnet_details: Localnet::new(network_name), + }; + let ctx = ephemeral_context("performing initial state check..."); + + this.check_system_deps().await?; + this.check_kernel_config(&ctx).await?; + this.resync_state(&ctx).await?; + + info!("initial state: {}", this.state); + + // pre-requirements for any subsequent command + this.create_localnet_network_if_doesnt_exist().await?; + Ok(this) + } + + async fn check_kernel_config(&self, ctx: &LocalnetContext) -> anyhow::Result<()> { + // NOTE: this is incomplete, I haven't yet determined full set of required config values + const REQUIRED_CONFIG: &[(&str, &str)] = &[("CONFIG_TUN", "y"), ("CONFIG_NF_TABLES", "y")]; + + let stdout = run_container( + ctx, + [ + "--rm", + "-v", + &self.kernel_configs_volume(), + "busybox:latest", + "sh", + "-c", + r#" + mkdir /root/kernel-configs + cat /proc/config.gz | gunzip > /root/kernel-configs/"$(uname -r)" + uname -r + "#, + ], + None, + ) + .await?; + let maybe_kernel = String::from_utf8(stdout).context("malformed kernel version")?; + info!("found kernel version: {maybe_kernel}"); + + // sure, it's easier to check it directly on the machine, + // but persisting the file locally makes it easier to debug + let config_values = fs::read_to_string( + self.storage + .data_cache() + .kernel_configs_directory() + .join(maybe_kernel.trim()), + ) + .context("failed to read retrieved kernel config")?; + + let mut enabled_configs = HashMap::new(); + + for config in config_values.lines().filter(|l| { + let trimmed = l.trim(); + !trimmed.is_empty() && !trimmed.starts_with('#') + }) { + let (key, value) = config + .split_once('=') + .context(format!("malformed kernel config entry: '{config}'"))?; + enabled_configs.insert(key, value); + } + + for (expected_key, expected_value) in REQUIRED_CONFIG { + let Some(value) = enabled_configs.get(expected_key) else { + bail!( + "{expected_key} not set in the kernel - please either recompile it or obtain a valid image" + ); + }; + if value != expected_value { + bail!( + "{expected_key} does not have the expected value. we need it to be set to '{expected_value}' but it's set to '{value}'" + ); + } + ctx.println_with_emoji( + format!("{expected_key}={expected_value} present in the kernel"), + "โœ…", + ) + } + + Ok(()) + } + + async fn create_localnet_network_if_doesnt_exist(&self) -> anyhow::Result<()> { + info!("checking if {CONTAINER_NETWORK_NAME} network exists"); + + if !is_container_network_running().await? { + create_container_network().await?; + } + + Ok(()) + } + + /// Inspects the current network state and resyncs initial state + /// for example if there's already a nyxd running, there's no point in redeploying it + /// (unless forced by the cli) + async fn resync_state(&mut self, ctx: &LocalnetContext) -> anyhow::Result<()> { + let latest_nyxd_id = self + .storage + .orchestrator() + .get_last_created() + .await? + .latest_nyxd_id; + + if self.check_nyxd_container_is_running(ctx).await? { + // ASSUMPTION: if container is running it is using the latest initialised nyxd instance + let latest_nyxd_id = latest_nyxd_id + .context("nyxd container running, but no known nyxd instances initialised")?; + + let nyxd_details = self + .storage + .orchestrator() + .get_nyxd_details(latest_nyxd_id) + .await?; + self.localnet_details.set_nyxd_details(nyxd_details); + + self.state = LocalnetState::RunningNyxd + } else { + return Ok(()); + } + + let metadata = self + .storage + .orchestrator() + .get_localnet_metadata_by_name(&self.localnet_details.human_name) + .await?; + + let maybe_contracts = self + .storage + .orchestrator() + .load_localnet_contracts(metadata.id) + .await; + let auxiliary_accounts = self + .storage + .orchestrator() + .load_auxiliary_accounts(metadata.id) + .await; + + match (maybe_contracts, auxiliary_accounts) { + (Ok(contracts), Ok(auxiliary_accounts)) => { + self.localnet_details + .set_auxiliary_accounts(auxiliary_accounts) + .set_contracts(contracts); + self.state = LocalnetState::DeployedNymContracts; + } + _ => return Ok(()), + } + + // at this point there is no restarting containers due to changing ips + if self.check_nym_api_container_is_running(ctx).await? { + let nym_api = self + .storage + .orchestrator() + .get_nym_api_details(metadata.id) + .await?; + self.localnet_details.set_nym_api_endpoint(nym_api); + self.state = LocalnetState::RunningNymApi; + } else { + return Ok(()); + } + + if self.check_nym_node_containers_are_running(ctx).await? { + self.state = LocalnetState::RunningNymNodes; + } + + Ok(()) + } + + async fn check_dep_exists(&self, name: &str) -> anyhow::Result<()> { + if !exec_fallible_cmd_with_output("which", [name]) + .await? + .status + .success() + { + bail!("'{}' installation not found", name) + } + Ok(()) + } + + async fn check_system_deps(&self) -> anyhow::Result<()> { + self.check_dep_exists("docker").await?; + + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + self.check_dep_exists("container").await?; + } else if #[cfg(target_os = "linux")] { + self.check_dep_exists("newuidmap").await?; + self.check_dep_exists("newgidmap").await?; + self.check_dep_exists("containerd").await?; + self.check_dep_exists("nerdctl").await?; + self.check_dep_exists("kata-runtime").await?; + self.check_dep_exists("containerd-shim-kata-v2").await?; + } + } + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/network.rs b/tools/internal/localnet-orchestrator/src/orchestrator/network.rs new file mode 100644 index 00000000000..825b97d9b04 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/network.rs @@ -0,0 +1,413 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::contract_build_names; +use crate::orchestrator::account::Account; +use crate::orchestrator::cosmwasm_contract::{ContractBeingInitialised, CosmwasmContract}; +use anyhow::{Context, bail}; +use nym_config::defaults::{ApiUrl, ChainDetails, NymNetworkDetails, ValidatorDetails}; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use url::Url; + +pub(crate) struct Localnet { + pub(crate) human_name: String, + + pub(crate) nyxd: Option, + + pub(crate) nym_api_endpoint: Option, + + pub(crate) contracts: Option, + + pub(crate) auxiliary_accounts: Option, +} + +impl Localnet { + pub(crate) fn new(human_name: String) -> Self { + Localnet { + human_name, + nyxd: None, + nym_api_endpoint: None, + contracts: None, + auxiliary_accounts: None, + } + } + + /// Best effort conversion of `Localnet` information into `NymNetworkDetails` + /// The result will depend on the current state of localnet setup, e.g. + /// if contracts have not yet been initialised, the relevant addresses will not be set. + pub(crate) fn nym_network_details(&self) -> anyhow::Result { + let mut details = NymNetworkDetails::new_empty(); + details.network_name = "localnet".to_string(); + + let mut validator_details = + ValidatorDetails::new_nyxd_only(self.localhost_rpc_endpoint()?.to_string()); + + // localnet uses the same chain-details (i.e. denoms, prefixes) as mainnet + details.chain_details = ChainDetails::mainnet(); + + if let Some(contracts) = self.contracts.as_ref() { + details.contracts.mixnet_contract_address = Some(contracts.mixnet.address.to_string()); + details.contracts.vesting_contract_address = + Some(contracts.vesting.address.to_string()); + details.contracts.performance_contract_address = + Some(contracts.performance.address.to_string()); + details.contracts.ecash_contract_address = Some(contracts.ecash.address.to_string()); + details.contracts.group_contract_address = + Some(contracts.cw4_group.address.to_string()); + details.contracts.multisig_contract_address = + Some(contracts.cw3_multisig.address.to_string()); + details.contracts.coconut_dkg_contract_address = + Some(contracts.dkg.address.to_string()); + } + + if let Some(nym_api) = self.nym_api_endpoint.as_ref() { + validator_details.api_url = Some(nym_api.to_string()); + details.nym_api_urls = Some(vec![ApiUrl { + url: nym_api.to_string(), + front_hosts: None, + }]) + } + + details.endpoints = vec![validator_details]; + Ok(details) + } + + pub(crate) fn env_file_content(&self) -> anyhow::Result { + let mut env_content = r#" +CONFIGURED=true + +RUST_LOG=info +RUST_BACKTRACE=1 +NETWORK_NAME=localnet +BECH32_PREFIX=n +MIX_DENOM=unym +MIX_DENOM_DISPLAY=nym +STAKE_DENOM=unyx +STAKE_DENOM_DISPLAY=nyx +DENOMS_EXPONENT=6 + +"# + .to_string(); + + if let Some(contracts) = &self.contracts { + // if contracts are defined so must be the addresses + let aux = self.auxiliary_accounts()?; + + env_content.push_str(&format!( + r#"REWARDING_VALIDATOR_ADDRESS={} +MIXNET_CONTRACT_ADDRESS={} +VESTING_CONTRACT_ADDRESS={} +GROUP_CONTRACT_ADDRESS={} +MULTISIG_CONTRACT_ADDRESS={} +COCONUT_DKG_CONTRACT_ADDRESS={} +ECASH_CONTRACT_ADDRESS={} +PERFORMANCE_CONTRACT_ADDRESS={} + +"#, + aux.mixnet_rewarder.address, + contracts.mixnet.address, + contracts.vesting.address, + contracts.cw4_group.address, + contracts.cw3_multisig.address, + contracts.dkg.address, + contracts.ecash.address, + contracts.performance.address, + )) + } + + let nyxd = self.nyxd_details()?; + + env_content.push_str(&format!("NYXD={}\n\n", nyxd.rpc_endpoint)); + + if let Ok(nym_api) = self.nym_api_endpoint() { + env_content.push_str(&format!("NYM_API={nym_api}\n\n")); + } + + Ok(env_content) + } + + pub(crate) fn nyxd_details(&self) -> anyhow::Result<&NyxdDetails> { + self.nyxd.as_ref().context("nyxd details not set") + } + + pub(crate) fn set_nyxd_details(&mut self, account: NyxdDetails) -> &mut Self { + self.nyxd = Some(account); + self + } + + pub(crate) fn localhost_rpc_endpoint(&self) -> anyhow::Result { + let _ = self.nyxd_details()?; + Ok("http://127.0.0.1:26657".parse()?) + } + + /// Returns address of the nyxd rpc endpoint on the localnet container network + #[allow(dead_code)] + pub(crate) fn rpc_endpoint(&self) -> anyhow::Result<&Url> { + Ok(&self.nyxd_details()?.rpc_endpoint) + } + + pub(crate) fn nym_api_endpoint(&self) -> anyhow::Result<&Url> { + self.nym_api_endpoint + .as_ref() + .context("nym api endpoint has not been set") + } + + pub(crate) fn set_nym_api_endpoint(&mut self, nym_api_endpoint: Url) -> &mut Self { + self.nym_api_endpoint = Some(nym_api_endpoint); + self + } + + pub(crate) fn auxiliary_accounts(&self) -> anyhow::Result<&AuxiliaryAccounts> { + self.auxiliary_accounts + .as_ref() + .context("auxiliary accounts have not been set") + } + + pub(crate) fn set_auxiliary_accounts( + &mut self, + auxiliary_accounts: AuxiliaryAccounts, + ) -> &mut Self { + self.auxiliary_accounts = Some(auxiliary_accounts); + self + } + + pub(crate) fn contracts(&self) -> anyhow::Result<&NymContracts> { + self.contracts + .as_ref() + .context("cosmwasm contracts have not been initialised") + } + + pub(crate) fn set_contracts(&mut self, contracts: NymContracts) -> &mut Self { + self.contracts = Some(contracts); + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct AuxiliaryAccounts { + pub(crate) mixnet_rewarder: Account, + pub(crate) network_monitor: Vec, + pub(crate) ecash_holding_account: Account, +} + +impl AuxiliaryAccounts { + pub(crate) fn new() -> Self { + AuxiliaryAccounts { + mixnet_rewarder: Account::new(), + network_monitor: vec![Account::new()], + ecash_holding_account: Account::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct NyxdDetails { + pub(crate) rpc_endpoint: Url, + pub(crate) master_account: Account, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct NymContracts { + pub(crate) mixnet: CosmwasmContract, + pub(crate) vesting: CosmwasmContract, + pub(crate) ecash: CosmwasmContract, + pub(crate) cw3_multisig: CosmwasmContract, + pub(crate) cw4_group: CosmwasmContract, + pub(crate) dkg: CosmwasmContract, + pub(crate) performance: CosmwasmContract, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct NymContractsBeingInitialised { + pub(crate) mixnet: ContractBeingInitialised, + pub(crate) vesting: ContractBeingInitialised, + pub(crate) ecash: ContractBeingInitialised, + pub(crate) cw3_multisig: ContractBeingInitialised, + pub(crate) cw4_group: ContractBeingInitialised, + pub(crate) dkg: ContractBeingInitialised, + pub(crate) performance: ContractBeingInitialised, +} + +impl NymContractsBeingInitialised { + pub(crate) const COUNT: usize = 7; + + pub(crate) fn into_built_contracts(self) -> anyhow::Result { + Ok(NymContracts { + mixnet: CosmwasmContract { + address: self.mixnet.address()?.clone(), + admin: self.mixnet.admin()?.clone(), + name: self.mixnet.name, + }, + vesting: CosmwasmContract { + address: self.vesting.address()?.clone(), + admin: self.vesting.admin()?.clone(), + name: self.vesting.name, + }, + ecash: CosmwasmContract { + address: self.ecash.address()?.clone(), + admin: self.ecash.admin()?.clone(), + name: self.ecash.name, + }, + cw3_multisig: CosmwasmContract { + address: self.cw3_multisig.address()?.clone(), + admin: self.cw3_multisig.admin()?.clone(), + name: self.cw3_multisig.name, + }, + cw4_group: CosmwasmContract { + address: self.cw4_group.address()?.clone(), + admin: self.cw4_group.admin()?.clone(), + name: self.cw4_group.name, + }, + dkg: CosmwasmContract { + address: self.dkg.address()?.clone(), + admin: self.dkg.admin()?.clone(), + name: self.dkg.name, + }, + performance: CosmwasmContract { + address: self.performance.address()?.clone(), + admin: self.performance.admin()?.clone(), + name: self.performance.name, + }, + }) + } + + pub(crate) fn all(&self) -> Vec<&ContractBeingInitialised> { + vec![ + &self.mixnet, + &self.vesting, + &self.ecash, + &self.cw3_multisig, + &self.cw4_group, + &self.dkg, + &self.performance, + ] + } + + pub(crate) fn all_mut(&mut self) -> Vec<&mut ContractBeingInitialised> { + vec![ + &mut self.mixnet, + &mut self.vesting, + &mut self.ecash, + &mut self.cw3_multisig, + &mut self.cw4_group, + &mut self.dkg, + &mut self.performance, + ] + } + + pub(crate) fn by_filename(&self, filename: &str) -> anyhow::Result<&ContractBeingInitialised> { + if filename == contract_build_names::MIXNET { + return Ok(&self.mixnet); + } + if filename == contract_build_names::VESTING { + return Ok(&self.vesting); + } + if filename == contract_build_names::ECASH { + return Ok(&self.ecash); + } + if filename == contract_build_names::DKG { + return Ok(&self.dkg); + } + if filename == contract_build_names::GROUP { + return Ok(&self.cw4_group); + } + if filename == contract_build_names::MULTISIG { + return Ok(&self.cw3_multisig); + } + if filename == contract_build_names::PERFORMANCE { + return Ok(&self.performance); + } + + bail!("no known contract with name {filename}") + } + + pub(crate) fn by_filename_mut( + &mut self, + filename: &str, + ) -> anyhow::Result<&mut ContractBeingInitialised> { + if filename == contract_build_names::MIXNET { + return Ok(&mut self.mixnet); + } + if filename == contract_build_names::VESTING { + return Ok(&mut self.vesting); + } + if filename == contract_build_names::ECASH { + return Ok(&mut self.ecash); + } + if filename == contract_build_names::DKG { + return Ok(&mut self.dkg); + } + if filename == contract_build_names::GROUP { + return Ok(&mut self.cw4_group); + } + if filename == contract_build_names::MULTISIG { + return Ok(&mut self.cw3_multisig); + } + if filename == contract_build_names::PERFORMANCE { + return Ok(&mut self.performance); + } + + bail!("no known contract with name {filename}") + } + + pub(crate) fn discover_paths>(&mut self, base_path: P) -> anyhow::Result<()> { + // just look in the base path, don't traverse + for entry_res in base_path.as_ref().read_dir()? { + let entry = entry_res?; + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + + if let Ok(contract) = self.by_filename_mut(&name) { + contract.wasm_path = Some(entry.path()); + } + } + + if let Some(no_path) = self.all().iter().find(|c| c.wasm_path.is_none()) { + bail!( + "could not find .wasm file for {} contract under the provided directory", + no_path.name + ) + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn count_is_up_to_date() { + let contracts = NymContractsBeingInitialised { + mixnet: ContractBeingInitialised::new("mixnet"), + vesting: ContractBeingInitialised::new("vesting"), + ecash: ContractBeingInitialised::new("ecash"), + cw3_multisig: ContractBeingInitialised::new("cw3-multisig"), + cw4_group: ContractBeingInitialised::new("cw4-group"), + dkg: ContractBeingInitialised::new("dkg"), + performance: ContractBeingInitialised::new("performance"), + }; + assert_eq!(contracts.all().len(), NymContractsBeingInitialised::COUNT); + } + + #[test] + fn all_and_all_mut_have_the_same_order() { + let contracts = NymContractsBeingInitialised { + mixnet: ContractBeingInitialised::new("mixnet"), + vesting: ContractBeingInitialised::new("vesting"), + ecash: ContractBeingInitialised::new("ecash"), + cw3_multisig: ContractBeingInitialised::new("cw3-multisig"), + cw4_group: ContractBeingInitialised::new("cw4-group"), + dkg: ContractBeingInitialised::new("dkg"), + performance: ContractBeingInitialised::new("performance"), + }; + let mut contracts_clone = contracts.clone(); + + for (c1, c2) in contracts.all().into_iter().zip(contracts_clone.all_mut()) { + assert_eq!(c1, c2); + } + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/nym_node.rs b/tools/internal/localnet-orchestrator/src/orchestrator/nym_node.rs new file mode 100644 index 00000000000..b96eb332399 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/nym_node.rs @@ -0,0 +1,54 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::account::Account; +use nym_coconut_dkg_common::types::Addr; +use nym_contracts_common::Percent; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::{NodeCostParams, NodeId, construct_nym_node_bonding_sign_payload}; +use nym_validator_client::nyxd::CosmWasmCoin; +use std::net::IpAddr; + +pub(crate) struct LocalnetNymNode { + pub(crate) id: NodeId, + + pub(crate) gateway: bool, + pub(crate) identity: ed25519::KeyPair, + pub(crate) owner: Account, +} + +impl LocalnetNymNode { + pub(crate) fn pledge(&self) -> CosmWasmCoin { + CosmWasmCoin::new(100_000000u32, "unym") + } + + pub(crate) fn bonding_nym_node(&self, node_ip: IpAddr) -> nym_mixnet_contract_common::NymNode { + nym_mixnet_contract_common::NymNode { + host: node_ip.to_string(), + custom_http_port: None, + identity_key: self.identity.public_key().to_base58_string(), + } + } + + pub(crate) fn cost_params(&self) -> NodeCostParams { + // SAFETY: we're using valid value + #[allow(clippy::unwrap_used)] + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: CosmWasmCoin::new(40_000000u32, "unym"), + } + } + + pub(crate) fn node_bonding_payload(&self, node_ip: IpAddr) -> String { + let payload = construct_nym_node_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + self.pledge(), + self.bonding_nym_node(node_ip), + self.cost_params(), + ); + // SAFETY: we're using valid encoding + #[allow(clippy::unwrap_used)] + payload.to_base58_string().unwrap() + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/cosmwasm_contracts.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/cosmwasm_contracts.rs new file mode 100644 index 00000000000..5962a36b7c9 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/cosmwasm_contracts.rs @@ -0,0 +1,869 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::contract_build_names; +use crate::constants::{CARGO_REGISTRY_CACHE_VOLUME, CI_BUILD_SERVER, CONTRACTS_CACHE_VOLUME}; +use crate::helpers::{ + download_cosmwasm_contract, monorepo_root_path, nym_cosmwasm_contract_names, + retrieve_current_nymnode_version, +}; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::account::Account; +use crate::orchestrator::context::LocalnetContext; +use crate::orchestrator::cosmwasm_contract::ContractBeingInitialised; +use crate::orchestrator::network::{AuxiliaryAccounts, NymContractsBeingInitialised}; +use crate::orchestrator::state::LocalnetState; +use anyhow::{Context, bail}; +use cw_utils::Threshold; +use nym_coconut_dkg_common::types::TimeConfiguration; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; +use nym_mixnet_contract_common::{Decimal, InitialRewardingParams}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; +use serde::Serialize; +use std::collections::VecDeque; +use std::fs; +use std::path::PathBuf; +use std::time::Duration; +use time::OffsetDateTime; +use time::format_description::well_known::Rfc3339; +use tracing::{debug, info}; + +pub(crate) struct Config { + pub(crate) reproducible_builds: bool, + pub(crate) cosmwasm_optimizer_image: String, + pub(crate) explicit_contracts_directory: Option, + pub(crate) ci_build_branch: Option, + pub(crate) monorepo_root: Option, + pub(crate) allow_cached_build: bool, +} + +pub(crate) struct ContractsSetup { + reproducible_builds: bool, + cosmwasm_optimizer_image: String, + allow_cached_build: bool, + + contracts_wasm_dir: Option, + ci_build_branch: Option, + monorepo_root: PathBuf, + contracts: NymContractsBeingInitialised, + auxiliary_accounts: AuxiliaryAccounts, +} + +impl ContractsSetup { + pub(crate) fn new(config: Config) -> anyhow::Result { + let monorepo_root = monorepo_root_path(config.monorepo_root)?; + + Ok(ContractsSetup { + reproducible_builds: config.reproducible_builds, + cosmwasm_optimizer_image: config.cosmwasm_optimizer_image, + allow_cached_build: config.allow_cached_build, + contracts_wasm_dir: config.explicit_contracts_directory, + ci_build_branch: config.ci_build_branch, + contracts: NymContractsBeingInitialised { + mixnet: ContractBeingInitialised::new("mixnet"), + vesting: ContractBeingInitialised::new("vesting"), + ecash: ContractBeingInitialised::new("ecash"), + cw3_multisig: ContractBeingInitialised::new("cw3-multisig"), + cw4_group: ContractBeingInitialised::new("cw4-group"), + dkg: ContractBeingInitialised::new("dkg"), + performance: ContractBeingInitialised::new("performance"), + }, + monorepo_root, + auxiliary_accounts: AuxiliaryAccounts::new(), + }) + } +} + +impl LocalnetOrchestrator { + fn contract_signer( + &self, + contract: &ContractBeingInitialised, + ) -> anyhow::Result { + let mnemonic = &contract.admin()?.mnemonic; + self.signing_client(mnemonic) + } + + fn mixnet_migrate_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_mixnet_contract_common::MigrateMsg { + vesting_contract_address: Some(ctx.data.contracts.vesting.address()?.to_string()), + unsafe_skip_state_updates: Some(true), + }) + } + + fn multisig_migrate_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_multisig_contract_common::msg::MigrateMsg { + coconut_bandwidth_address: ctx.data.contracts.ecash.address()?.to_string(), + coconut_dkg_address: ctx.data.contracts.dkg.address()?.to_string(), + }) + } + + fn mixnet_init_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_mixnet_contract_common::InstantiateMsg { + rewarding_validator_address: ctx + .data + .auxiliary_accounts + .mixnet_rewarder + .address() + .to_string(), + // PLACEHOLDER \/ + vesting_contract_address: ctx + .data + .auxiliary_accounts + .mixnet_rewarder + .address() + .to_string(), + // PLACEHOLDER /\ + rewarding_denom: "unym".to_string(), + epochs_in_interval: 720, + epoch_duration: Duration::from_secs(60 * 60), + initial_rewarding_params: InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0)?, + initial_staking_supply: Decimal::from_atomics(100_000_000_000_000u128, 0)?, + staking_supply_scale_factor: Percent::from_percentage_value(50)?, + sybil_resistance: Percent::from_percentage_value(30)?, + active_set_work_factor: Decimal::from_atomics(10u32, 0)?, + interval_pool_emission: Percent::from_percentage_value(2)?, + rewarded_set_params: RewardedSetParams { + entry_gateways: 70, + exit_gateways: 50, + mixnodes: 120, + standby: 0, + }, + }, + current_nym_node_version: retrieve_current_nymnode_version(&ctx.data.monorepo_root)?, + version_score_weights: Default::default(), + version_score_params: Default::default(), + profit_margin: Default::default(), + interval_operating_cost: Default::default(), + key_validity_in_epochs: None, + }) + } + + fn vesting_init_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_vesting_contract_common::InitMsg { + mixnet_contract_address: ctx.data.contracts.mixnet.address()?.to_string(), + mix_denom: "unym".to_string(), + }) + } + + fn dkg_init_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_coconut_dkg_common::msg::InstantiateMsg { + group_addr: ctx.data.contracts.cw4_group.address()?.to_string(), + multisig_addr: ctx.data.contracts.cw3_multisig.address()?.to_string(), + time_configuration: Some(TimeConfiguration { + public_key_submission_time_secs: 3600, + dealing_exchange_time_secs: 3600, + verification_key_submission_time_secs: 3600, + verification_key_validation_time_secs: 3600, + verification_key_finalization_time_secs: 3600, + in_progress_time_secs: 10000000000, + }), + mix_denom: "unym".to_string(), + key_size: 5, + }) + } + + fn ecash_init_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_ecash_contract_common::msg::InstantiateMsg { + holding_account: ctx + .data + .auxiliary_accounts + .ecash_holding_account + .address + .to_string(), + multisig_addr: ctx.data.contracts.cw3_multisig.address()?.to_string(), + group_addr: ctx.data.contracts.cw4_group.address()?.to_string(), + deposit_amount: ctx.unym(75_000_000).into(), + }) + } + + fn cw3_multisig_init_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_multisig_contract_common::msg::InstantiateMsg { + group_addr: ctx.data.contracts.cw4_group.address()?.to_string(), + + // PLACEHOLDER \/ + coconut_bandwidth_contract_address: ctx.data.contracts.cw4_group.address()?.to_string(), + coconut_dkg_contract_address: ctx.data.contracts.cw4_group.address()?.to_string(), + // PLACEHOLDER /\ + threshold: Threshold::AbsolutePercentage { + percentage: "0.67".parse()?, + }, + max_voting_period: cw_utils::Duration::Time(3600), + executor: None, + proposal_deposit: None, + }) + } + + fn cw4_group_init_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_group_contract_common::msg::InstantiateMsg { + admin: Some(ctx.data.contracts.cw4_group.admin()?.address().to_string()), + // TODO: prepopulate + members: vec![], + }) + } + + fn performance_init_message( + &self, + ctx: &LocalnetContext, + ) -> anyhow::Result { + Ok(nym_performance_contract_common::msg::InstantiateMsg { + mixnet_contract_address: ctx.data.contracts.mixnet.address()?.to_string(), + authorised_network_monitors: vec![ + ctx.data + .auxiliary_accounts + .network_monitor + .iter() + .map(|nm| nm.address.to_string()) + .collect(), + ], + }) + } + + fn contracts_wasm_dir(&self, ctx: &LocalnetContext) -> PathBuf { + if let Some(explicit) = &ctx.data.contracts_wasm_dir { + return explicit.clone(); + } + self.storage.cosmwasm_contracts_directory() + } + + async fn download_cosmwasm_contracts( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + let Some(ci_build_branch) = ctx.data.ci_build_branch.clone() else { + bail!("no CI branch specified for downloading pre-built contracts") + }; + + ctx.begin_next_step( + format!("downloading cosmwasm contracts from {CI_BUILD_SERVER}/{ci_build_branch}/..."), + "โฌ‡๏ธ", + ); + let out_dir = self.contracts_wasm_dir(ctx); + fs::create_dir_all(&out_dir)?; + info!("downloading cosmwasm contracts to {}", out_dir.display()); + + ctx.set_pb_prefix("[1/7]"); + ctx.set_pb_message("downloading mixnet contract..."); + download_cosmwasm_contract(&out_dir, &ci_build_branch, contract_build_names::MIXNET) + .await?; + + ctx.set_pb_prefix("[2/7]"); + ctx.set_pb_message("downloading vesting contract..."); + download_cosmwasm_contract(&out_dir, &ci_build_branch, contract_build_names::VESTING) + .await?; + + ctx.set_pb_prefix("[3/7]"); + ctx.set_pb_message("downloading ecash contract..."); + download_cosmwasm_contract(&out_dir, &ci_build_branch, contract_build_names::ECASH).await?; + + ctx.set_pb_prefix("[4/7]"); + ctx.set_pb_message("downloading dkg contract..."); + download_cosmwasm_contract(&out_dir, &ci_build_branch, contract_build_names::DKG).await?; + + ctx.set_pb_prefix("[5/7]"); + ctx.set_pb_message("downloading cw4-group contract..."); + download_cosmwasm_contract(&out_dir, &ci_build_branch, contract_build_names::GROUP).await?; + + ctx.set_pb_prefix("[6/7]"); + ctx.set_pb_message("downloading cw3-multisig contract..."); + download_cosmwasm_contract(&out_dir, &ci_build_branch, contract_build_names::MULTISIG) + .await?; + + ctx.set_pb_prefix("[7/7]"); + ctx.set_pb_message("downloading performance contract..."); + download_cosmwasm_contract( + &out_dir, + &ci_build_branch, + contract_build_names::PERFORMANCE, + ) + .await?; + + Ok(()) + } + + async fn build_contract( + &self, + ctx: &mut LocalnetContext, + contract_relative_path: &str, + ) -> anyhow::Result<()> { + let code_volume = format!("{}:/code", ctx.data.monorepo_root.to_string_lossy()); + let target_volume = format!("type=volume,source={CONTRACTS_CACHE_VOLUME},target=/target"); + let registry_volume = format!( + "type=volume,source={CARGO_REGISTRY_CACHE_VOLUME},target=/usr/local/cargo/registry" + ); + + let mut args = vec![ + "run", + "--rm", + "-v", + &code_volume, + "--mount", + &target_volume, + "--mount", + ®istry_volume, + ]; + + if ctx.data.reproducible_builds { + args.push("--platform"); + args.push("linux/amd64"); + args.push("-e"); + args.push("CARGO_BUILD_INCREMENTAL=false"); + args.push("-e"); + args.push(r#"RUSTFLAGS="-C target-cpu=generic -C debuginfo=0""#); + args.push("-e"); + args.push("SOURCE_DATE_EPOCH=1"); + } + + // the final bit with the actual image and args, e.g. cosmwasm/optimizer:0.17.0 contracts/performance + args.push(&ctx.data.cosmwasm_optimizer_image); + args.push(contract_relative_path); + + ctx.execute_cmd_with_exit_status("docker", args).await?; + + Ok(()) + } + + async fn build_cosmwasm_contracts( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step( + "building cosmwasm contracts... this might take up to 20min if using reproducible builds...", + "๐Ÿ—๏ธ", + ); + + ctx.set_pb_prefix("[1/9]"); + ctx.set_pb_message("cleaning up build volumes..."); + ctx.exec_fallible_cmd_with_exit_status("docker", ["volume", "rm", CONTRACTS_CACHE_VOLUME]) + .await?; + ctx.exec_fallible_cmd_with_exit_status( + "docker", + ["volume", "rm", CARGO_REGISTRY_CACHE_VOLUME], + ) + .await?; + + ctx.set_pb_prefix("[2/9]"); + ctx.set_pb_message("building the mixnet contract..."); + self.build_contract(ctx, "contracts/mixnet").await?; + + ctx.set_pb_prefix("[3/9]"); + ctx.set_pb_message("building the vesting contract..."); + self.build_contract(ctx, "contracts/vesting").await?; + + ctx.set_pb_prefix("[4/9]"); + ctx.set_pb_message("building the ecash contract..."); + self.build_contract(ctx, "contracts/ecash").await?; + + ctx.set_pb_prefix("[5/9]"); + ctx.set_pb_message("building the dkg contract..."); + self.build_contract(ctx, "contracts/coconut-dkg").await?; + + ctx.set_pb_prefix("[6/9]"); + ctx.set_pb_message("building the cw4-group contract..."); + self.build_contract(ctx, "contracts/multisig/cw4-group") + .await?; + + ctx.set_pb_prefix("[7/9]"); + ctx.set_pb_message("building the cw3-multisig contract..."); + self.build_contract(ctx, "contracts/multisig/cw3-flex-multisig") + .await?; + + ctx.set_pb_prefix("[8/9]"); + ctx.set_pb_message("building the performance contract..."); + self.build_contract(ctx, "contracts/performance").await?; + + ctx.set_pb_prefix("[9/9]"); + ctx.set_pb_message("moving output .wasm files to the target directory"); + + let out_dir = self.contracts_wasm_dir(ctx); + fs::create_dir_all(&out_dir)?; + + let artifacts_dir = ctx.data.monorepo_root.join("artifacts"); + for dir_entry in artifacts_dir.read_dir()? { + let entry = dir_entry?; + let build_path = entry.path(); + let Some(extension) = build_path.extension() else { + continue; + }; + let Some(filename) = build_path.file_name() else { + continue; + }; + let out = out_dir.join(filename); + if extension.to_string_lossy() == "wasm" { + debug!("moving {} to {}", build_path.display(), out.display()); + std::fs::rename(&build_path, &out)?; + + // copy it to cache as well + let cache_path = self + .storage + .data_cache() + .contracts_directory() + .join(filename); + fs::copy(out, cache_path).context("failed to move built contract to the cache")?; + } + } + + Ok(()) + } + + /// Check if every expected .wasm file exists in the specified directory + fn check_contracts_built(&self, ctx: &LocalnetContext) -> bool { + // check cache if possible + if ctx.data.allow_cached_build { + let cached_exists = nym_cosmwasm_contract_names() + .iter() + .all(|filename| self.storage.data_cache().cached_contract_exists(filename)); + if cached_exists { + return true; + } + } + + // fallback to default + nym_cosmwasm_contract_names().iter().all(|filename| { + let path = self.contracts_wasm_dir(ctx).join(filename); + path.exists() + }) + } + + fn set_contracts_build_paths( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + if ctx.data.allow_cached_build + && ctx + .data + .contracts + .discover_paths(self.storage.data_cache().contracts_directory()) + .is_ok() + { + info!("using cached contracts"); + return Ok(()); + } + + ctx.data + .contracts + .discover_paths(self.contracts_wasm_dir(ctx)) + } + + // SAFETY: we have an entry for each contract + #[allow(clippy::unwrap_used)] + async fn upload_contracts( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("uploading contracts...", "๐Ÿšš"); + + let total = NymContractsBeingInitialised::COUNT as u64; + + let mut upload_results = VecDeque::new(); + for (progress, contract) in ctx.data.contracts.all().into_iter().enumerate() { + ctx.set_pb_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + ctx.set_pb_message(format!("uploading {name} contract...")); + + let upload_res = self.upload_contract(ctx, &contract.wasm_path()?).await?; + ctx.println(format!( + "\t{name} contract uploaded with code: {}. tx: {}", + upload_res.code_id, upload_res.transaction_hash + )); + upload_results.push_back(upload_res.into()); + } + // we have to assign this in separate loop due to borrow checker rules + // (iterating for the second time was the simplest workaround) + for contract in ctx.data.contracts.all_mut() { + contract.upload_info = Some(upload_results.pop_front().unwrap()) + } + + ctx.println("\tโœ… uploaded all the contracts!"); + + Ok(()) + } + + async fn prepare_contract_accounts( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step( + "preparing contract accounts and sending initial tokens...", + "๐Ÿ’ธ", + ); + + // generate contract admins + let mut new_accounts = Vec::new(); + for contract in ctx.data.contracts.all_mut() { + let admin = Account::new(); + debug!( + "\t{} is going to be admin for the {} contract", + admin.address, contract.name + ); + new_accounts.push(admin.address()); + contract.admin = Some(admin); + } + + // apart from contract admins, we need to send tokens to the mixnet rewarder + // and the network monitor + for nm in &ctx.data.auxiliary_accounts.network_monitor { + new_accounts.push(nm.address()) + } + new_accounts.push(ctx.data.auxiliary_accounts.mixnet_rewarder.address()); + + let receivers = new_accounts + .into_iter() + .map(|addr| (addr, ctx.unyms(1000_000000))) + .collect::>(); + + let signing_client = self.master_signing_client()?; + let send_fut = signing_client.send_multiple(receivers, "localnet token seeding", None); + let res = ctx.async_with_progress(send_fut).await?; + ctx.println(format!( + "\tโœ… sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + + Ok(()) + } + + async fn instantiate_contract( + &self, + ctx: &mut LocalnetContext, + contract_name: &'static str, + init_msg: &T, + ) -> anyhow::Result<()> + where + T: ?Sized + Serialize + Sync, + { + let contract = ctx.data.contracts.by_filename(contract_name)?; + let signer = self.contract_signer(contract)?; + + let code_id = contract.code_id()?; + let admin = contract.admin_address()?; + let name = &contract.name; + // send tx + let init_fut = signer.instantiate( + code_id, + init_msg, + format!("{name} contract"), + "localnet contract init", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address} in tx: {}", + res.transaction_hash + )); + + // update init info + let contract_mut = ctx.data.contracts.by_filename_mut(contract_name)?; + contract_mut.init_info = Some(res.into()); + + Ok(()) + } + + async fn migrate_contract( + &self, + ctx: &mut LocalnetContext, + contract_name: &'static str, + migrate_msg: &T, + ) -> anyhow::Result<()> + where + T: ?Sized + Serialize + Sync, + { + let contract = ctx.data.contracts.by_filename(contract_name)?; + let code_id = contract.code_id()?; + let address = contract.address()?; + let admin = contract.admin()?; + let signer = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic_and_network_details( + self.localnet_details.localhost_rpc_endpoint()?.as_str(), + self.localnet_details.nym_network_details()?, + admin.mnemonic.clone(), + )?; + + let name = &contract.name; + // send tx + let init_fut = signer.migrate( + address, + code_id, + migrate_msg, + "localnet contract migrate", + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + ctx.println(format!( + "\t{name} contract migrated in tx: {}", + res.transaction_hash + )); + + // update migrate info + let contract_mut = ctx.data.contracts.by_filename_mut(contract_name)?; + contract_mut.migrate_info = Some(res.into()); + + Ok(()) + } + + // TODO: there are certainly multiple testing scenario where custom contract configuration would be desirable, + // for example shorter epochs, shorter key rotation, smaller active set, etc. + // however, for the time being, this is out of scope + async fn instantiate_contracts( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("instantiating all the contracts...", "๐Ÿ’ฝ"); + + // ===== MIXNET ===== + ctx.set_pb_prefix("[1/7]"); + ctx.set_pb_message("instantiating the mixnet contract..."); + + let init_msg = self.mixnet_init_message(ctx)?; + self.instantiate_contract(ctx, contract_build_names::MIXNET, &init_msg) + .await?; + // ===== MIXNET ===== + + // ===== VESTING ===== + ctx.set_pb_prefix("[2/7]"); + ctx.set_pb_message("instantiating the vesting contract..."); + let init_msg = self.vesting_init_message(ctx)?; + self.instantiate_contract(ctx, contract_build_names::VESTING, &init_msg) + .await?; + // ===== VESTING ===== + + // ===== GROUP ===== + ctx.set_pb_prefix("[3/7]"); + ctx.set_pb_message("instantiating the group contract..."); + let init_msg = self.cw4_group_init_message(ctx)?; + self.instantiate_contract(ctx, contract_build_names::GROUP, &init_msg) + .await?; + // ===== GROUP ===== + + // ===== MULTISIG ===== + ctx.set_pb_prefix("[4/7]"); + ctx.set_pb_message("instantiating the multisig contract..."); + let init_msg = self.cw3_multisig_init_message(ctx)?; + self.instantiate_contract(ctx, contract_build_names::MULTISIG, &init_msg) + .await?; + // ===== MULTISIG ===== + + // ===== DKG ===== + ctx.set_pb_prefix("[5/7]"); + ctx.set_pb_message("instantiating the dkg contract..."); + let init_msg = self.dkg_init_message(ctx)?; + self.instantiate_contract(ctx, contract_build_names::DKG, &init_msg) + .await?; + // ===== DKG ===== + + // ===== ECASH ===== + ctx.set_pb_prefix("[6/7]"); + ctx.set_pb_message("instantiating the ecash contract..."); + let init_msg = self.ecash_init_message(ctx)?; + self.instantiate_contract(ctx, contract_build_names::ECASH, &init_msg) + .await?; + // ===== ECASH ===== + + // ===== PERFORMANCE ===== + ctx.set_pb_prefix("[7/7]"); + ctx.set_pb_message("instantiating the performance contract..."); + let init_msg = self.performance_init_message(ctx)?; + self.instantiate_contract(ctx, contract_build_names::PERFORMANCE, &init_msg) + .await?; + // ===== PERFORMANCE ===== + + Ok(()) + } + + async fn perform_required_migrations( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("performing final migrations and contract cleanup...", "๐Ÿงน"); + + // ===== MIXNET ===== + ctx.set_pb_prefix("[1/2]"); + ctx.set_pb_message("migrating the mixnet contract (fixing up vesting contract address)..."); + let migrate_msg = self.mixnet_migrate_message(ctx)?; + self.migrate_contract(ctx, contract_build_names::MIXNET, &migrate_msg) + .await?; + // ===== MIXNET ===== + + // ===== MULTISIG ===== + ctx.set_pb_prefix("[2/2]"); + ctx.set_pb_message( + "migrating the multisig contract (fixing up ecash and dkg contract addresses)...", + ); + let migrate_msg = self.multisig_migrate_message(ctx)?; + self.migrate_contract(ctx, contract_build_names::MULTISIG, &migrate_msg) + .await?; + // ===== MULTISIG ===== + + ctx.println("\tโœ… performed all the needed migrations!"); + + Ok(()) + } + + // the purpose of this function is two-fold: + // 1. figure out how old are the contracts + // 2. (more important): implicitly verify they have correct structure, i.e. at the very least + // actually DO store the build information + async fn validate_build_information( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("inspecting contracts build information...", "๐Ÿ”"); + + let client = self.rpc_query_client()?; + + let now = OffsetDateTime::now_utc(); + for contract in ctx.data.contracts.all() { + let build_info_fut = client.try_get_contract_build_information(contract.address()?); + let name = &contract.name; + let build_info = ctx + .async_with_progress(build_info_fut) + .await + .context(format!( + "missing contract build information for {name} contract", + ))?; + let built_time = OffsetDateTime::parse(&build_info.build_timestamp, &Rfc3339)?; + let age = now - built_time; + let age_secs = Duration::from_secs(age.whole_seconds() as u64); + let age_human = humantime::format_duration(age_secs); // no need for ns precision in logs + let emoji = if age > time::Duration::days(30) { + "โ˜ ๏ธ๏ธ" + } else if age > time::Duration::days(7) { + "โ—๏ธ" + } else if age > time::Duration::days(1) { + "๏ธ๏ธโš ๏ธ" + } else { + "โ„น๏ธ" + }; + ctx.println_with_emoji( + format!("the {name} contract has been built {age_human} ago",), + emoji, + ); + } + + Ok(()) + } + + async fn finalize_contracts_setup( + &mut self, + mut ctx: LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("persisting cosmwasm contract details", "๐Ÿ“"); + + // update state + self.localnet_details + .set_auxiliary_accounts(ctx.data.auxiliary_accounts) + .set_contracts(ctx.data.contracts.into_built_contracts()?); + + let localnet_name = &self.localnet_details.human_name; + self.storage + .orchestrator() + .save_auxiliary_accounts(localnet_name, self.localnet_details.auxiliary_accounts()?) + .await?; + self.storage + .orchestrator() + .save_localnet_contracts(localnet_name, self.localnet_details.contracts()?) + .await?; + self.state = LocalnetState::DeployedNymContracts; + + Ok(()) + } + + pub(crate) async fn wait_for_first_block( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("waiting for the chain to produce its first block...", "โณ"); + + let client = self.rpc_query_client()?; + tokio::time::timeout(Duration::from_secs(10), async move { + loop { + if let Ok(height) = client.get_current_block_height().await { + if height.value() >= 2 { + return Ok::<_, anyhow::Error>(()); + } + } + } + }) + .await??; + + Ok(()) + } + + pub(crate) async fn initialise_contracts(&mut self, config: Config) -> anyhow::Result<()> { + // 0. establish initial nyxd details + + let setup = ContractsSetup::new(config)?; + let mut ctx = LocalnetContext::new(setup, 9, "\nsetting up cosmwasm contracts"); + + // 1.1 wait for the chain to produce its first block + self.wait_for_first_block(&mut ctx).await?; + + // 1.2 check rpc connection and master account existence + self.verify_master_account(&ctx).await?; + + // 2. if requested, attempt to download the contracts + if ctx.data.ci_build_branch.is_some() { + self.download_cosmwasm_contracts(&mut ctx).await?; + } else { + ctx.skip_steps(1); + } + + // 3.1 check if contracts have already been built + if self.check_contracts_built(&ctx) { + info!("required contracts have already been built - skipping the step"); + ctx.skip_steps(1); + } else { + // 3.2. create .wasm files + self.build_cosmwasm_contracts(&mut ctx).await?; + } + + // 4.1 update internal metadata (internally figure out paths to all .wasm files) + self.set_contracts_build_paths(&mut ctx)?; + + // 4.2 upload the contracts + self.upload_contracts(&mut ctx).await?; + + // 5. create mnemonics + transfer tokens + self.prepare_contract_accounts(&mut ctx).await?; + + // 6 init the contracts + self.instantiate_contracts(&mut ctx).await?; + + // 7. perform state migrations to fix up initial states + self.perform_required_migrations(&mut ctx).await?; + + // 8. verify build info + self.validate_build_information(&mut ctx).await?; + + // 9. persist all information + self.finalize_contracts_setup(ctx).await?; + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/down.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/down.rs new file mode 100644 index 00000000000..0cf461c661b --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/down.rs @@ -0,0 +1,70 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::container_helpers::{list_containers, remove_container, stop_container}; +use crate::orchestrator::context::LocalnetContext; + +#[derive(Default)] +pub(crate) struct LocalnetDown { + container_names: Vec, +} + +impl LocalnetOrchestrator { + async fn get_localnet_container_names( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("establishing list of localnet containers", "๐Ÿ”"); + + let container_list = list_containers(ctx).await?; + for container in container_list.containers { + if container.image.contains("localnet") { + ctx.data.container_names.push(container.name) + } + } + Ok(()) + } + + async fn stop_localnet_containers( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("stopping localnet containers", "๐Ÿ›‘"); + let count = ctx.data.container_names.len(); + + for (i, container_name) in ctx.data.container_names.iter().enumerate() { + ctx.set_pb_prefix(format!("[{}/{count}]", i + 1)); + ctx.set_pb_message(format!("stopping {container_name}")); + stop_container(ctx, container_name).await?; + } + + Ok(()) + } + + async fn remove_localnet_containers( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("removing localnet containers", "๐Ÿ”ฅ"); + let count = ctx.data.container_names.len(); + + for (i, container_name) in ctx.data.container_names.iter().enumerate() { + ctx.set_pb_prefix(format!("[{}/{count}]", i + 1)); + ctx.set_pb_message(format!("removing {container_name}")); + remove_container(ctx, container_name).await?; + } + + Ok(()) + } + + pub(crate) async fn stop_localnet(&self) -> anyhow::Result<()> { + let mut ctx = LocalnetContext::new(LocalnetDown::default(), 3, "\n stopping the localnet"); + + self.get_localnet_container_names(&mut ctx).await?; + self.stop_localnet_containers(&mut ctx).await?; + self.remove_localnet_containers(&mut ctx).await?; + + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/mod.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/mod.rs new file mode 100644 index 00000000000..2d5cf4d15f4 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod cosmwasm_contracts; +pub(crate) mod down; +pub(crate) mod nym_api; +pub(crate) mod nym_nodes; +pub(crate) mod nyxd; +pub(crate) mod purge; +pub(crate) mod rebuild_binaries_image; +pub(crate) mod up; diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_api.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_api.rs new file mode 100644 index 00000000000..260235fa42a --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_api.rs @@ -0,0 +1,687 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + CARGO_REGISTRY_CACHE_VOLUME, CONTAINER_NETWORK_NAME, CONTRACTS_CACHE_VOLUME, + NYM_API_UTILITY_BEARER, contract_build_names, +}; +use crate::helpers::monorepo_root_path; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::container_helpers::{ + check_container_image_exists, default_nym_binaries_image_tag, get_container_ip_address, + load_image_into_container_runtime, run_container, save_docker_image, +}; +use crate::orchestrator::context::LocalnetContext; +use crate::orchestrator::cosmwasm_contract::ContractBeingInitialised; +use crate::orchestrator::state::LocalnetState; +use anyhow::{Context, bail}; +use dkg_bypass_contract::msg::FakeDealerData; +use nym_coconut_dkg_common::types::Addr; +use nym_compact_ecash::{Base58, KeyPairAuth, ttp_keygen}; +use nym_crypto::asymmetric::ed25519; +use nym_pemstore::traits::PemStorableKey; +use nym_pemstore::{KeyPairPath, store_key, store_keypair}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use nym_validator_client::nyxd::CosmWasmClient; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, GroupSigningClient, PagedGroupQueryClient, +}; +use nym_validator_client::nyxd::cw4::Member; +use rand::{CryptoRng, Rng, thread_rng}; +use std::fs; +use std::path::PathBuf; +use tempfile::NamedTempFile; +use tracing::{debug, info}; + +// perform the same serialisation as the nym-api keys +struct FakeDkgKey<'a> { + inner: &'a KeyPairAuth, +} + +impl<'a> FakeDkgKey<'a> { + fn new(inner: &'a KeyPairAuth) -> Self { + FakeDkgKey { inner } + } +} + +impl PemStorableKey for FakeDkgKey<'_> { + type Error = std::io::Error; + + fn pem_type() -> &'static str { + "ECASH KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + // our fake key is ALWAYS issued for epoch 0 + let mut bytes = vec![0u8; 8]; + bytes.append(&mut self.inner.secret_key().to_bytes()); + bytes + } + + #[allow(clippy::unimplemented)] + fn from_bytes(_: &[u8]) -> Result { + unimplemented!("this is not meant to be ever called") + } +} + +pub(crate) struct Config { + pub(crate) cosmwasm_optimizer_image: String, + pub(crate) monorepo_root: Option, + pub(crate) custom_dns: Option, + pub(crate) allow_cached_build: bool, +} + +struct DKGKeys { + ecash_keys: KeyPairAuth, + ed25519_keypair: ed25519::KeyPair, +} + +impl DKGKeys { + pub(crate) fn generate(rng: &mut R) -> anyhow::Result { + let ecash_keys = ttp_keygen(1, 1) + .context("ecash key generation failure")? + .pop() + .context("empty ecash keys")?; + + let ed25519_keypair = ed25519::KeyPair::new(rng); + Ok(DKGKeys { + ed25519_keypair, + ecash_keys, + }) + } +} + +struct NymApiSetup { + allow_cached_build: bool, + cosmwasm_optimizer_image: String, + monorepo_root: PathBuf, + nym_binaries_image_location: NamedTempFile, + dkg_key_location: NamedTempFile, + ed25519_private_key_location: NamedTempFile, + ed25519_public_key_location: NamedTempFile, + dkg_bypass_contract: ContractBeingInitialised, + dkg_keys: Option, + custom_dns: Option, +} + +impl NymApiSetup { + pub(crate) fn new(config: Config) -> anyhow::Result { + let monorepo_root = monorepo_root_path(config.monorepo_root)?; + + Ok(NymApiSetup { + custom_dns: config.custom_dns, + allow_cached_build: config.allow_cached_build, + cosmwasm_optimizer_image: config.cosmwasm_optimizer_image, + monorepo_root, + nym_binaries_image_location: NamedTempFile::new()?, + dkg_key_location: NamedTempFile::new()?, + ed25519_private_key_location: NamedTempFile::new()?, + ed25519_public_key_location: NamedTempFile::new()?, + dkg_bypass_contract: ContractBeingInitialised::new("dkg-bypass-contract"), + dkg_keys: None, + }) + } + + pub(crate) fn image_temp_location_arg(&self) -> anyhow::Result<&str> { + self.nym_binaries_image_location + .path() + .to_str() + .context("invalid temporary file location") + } + + pub(crate) fn nym_binaries_dockerfile_location_canon(&self) -> anyhow::Result { + Ok(self + .monorepo_root + .join("docker") + .join("localnet") + .join("nym-binaries-localnet.Dockerfile") + .canonicalize()?) + } + + pub(crate) fn monorepo_root_canon(&self) -> anyhow::Result { + Ok(self.monorepo_root.canonicalize()?) + } + + pub(crate) fn dkg_keys(&self) -> anyhow::Result<&DKGKeys> { + self.dkg_keys.as_ref().context("missing dkg keys") + } +} + +impl LocalnetOrchestrator { + pub(crate) fn expected_bypass_contract_wasm_path(&self) -> PathBuf { + self.storage + .cosmwasm_contracts_directory() + .join(contract_build_names::DKG_BYPASS_CONTRACT) + } + + async fn build_nym_binaries_docker_image( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + let dockerfile_path = ctx.data.nym_binaries_dockerfile_location_canon()?; + let monorepo_path = ctx.data.monorepo_root_canon()?; + let image_tag = default_nym_binaries_image_tag(&monorepo_path)?; + + self.try_build_nym_binaries_docker_image(ctx, dockerfile_path, monorepo_path, &image_tag) + .await + } + + async fn save_nym_binaries_docker_image( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + let output_path = ctx.data.image_temp_location_arg()?.to_owned(); + let monorepo_path = ctx.data.monorepo_root_canon()?; + let image_tag = default_nym_binaries_image_tag(&monorepo_path)?; + + save_docker_image(ctx, &output_path, &image_tag).await + } + + async fn load_nym_binaries_into_container_runtime( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + let image_path = ctx.data.image_temp_location_arg()?.to_owned(); + load_image_into_container_runtime(ctx, &image_path).await + } + + async fn verify_nym_binaries_image( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("verifying localnet-nym-binaries container image...", "โ”"); + let monorepo_path = ctx.data.monorepo_root_canon()?; + let image_tag = default_nym_binaries_image_tag(&monorepo_path)?; + + if !check_container_image_exists(ctx, &image_tag).await? { + bail!("localnet-nym-binaries image verification failed"); + } + Ok(()) + } + + fn generate_dkg_keys(&self, ctx: &mut LocalnetContext) -> anyhow::Result<()> { + let dkg_keys = DKGKeys::generate(&mut thread_rng())?; + let fake_ecash_key = FakeDkgKey::new(&dkg_keys.ecash_keys); + + let ed25519_paths = KeyPairPath { + private_key_path: ctx.data.ed25519_private_key_location.path().to_owned(), + public_key_path: ctx.data.ed25519_public_key_location.path().to_owned(), + }; + + store_key(&fake_ecash_key, &ctx.data.dkg_key_location)?; + store_keypair(&dkg_keys.ed25519_keypair, &ed25519_paths)?; + ctx.data.dkg_keys = Some(dkg_keys); + + Ok(()) + } + + fn dkg_admin_signer(&self) -> anyhow::Result { + let mnemonic = &self.localnet_details.contracts()?.dkg.admin.mnemonic; + self.signing_client(mnemonic) + } + + fn group_admin_signer(&self) -> anyhow::Result { + let mnemonic = &self.localnet_details.contracts()?.cw4_group.admin.mnemonic; + self.signing_client(mnemonic) + } + + async fn validate_dkg_contracts_state( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("verifying DKG and group contract states...", "๐Ÿค”"); + + let client = self.rpc_query_client()?; + + ctx.set_pb_prefix("[1/2]"); + ctx.set_pb_message("checking DKG epoch data..."); + let epoch_fut = client.get_current_epoch(); + let dkg_epoch = ctx.async_with_progress(epoch_fut).await?; + if dkg_epoch.epoch_id != 0 { + bail!("DKG epoch has already progressed") + } + + if !dkg_epoch.state.is_waiting_initialisation() { + bail!("DKG has already started"); + } + + ctx.set_pb_prefix("[2/2]"); + ctx.set_pb_message("checking cw4 group members data..."); + let members_fut = client.get_all_members(); + let members = ctx.async_with_progress(members_fut).await?; + if !members.is_empty() { + bail!("CW4 multisig group is not empty!") + } + + Ok(()) + } + + fn check_bypass_contract_built(&self, ctx: &LocalnetContext) -> bool { + // check cache if possible + if ctx.data.allow_cached_build + && self + .storage + .data_cache() + .cached_contract_exists(contract_build_names::DKG_BYPASS_CONTRACT) + { + return true; + } + + // fallback to default + self.expected_bypass_contract_wasm_path().exists() + } + + async fn build_dkg_bypass_contract( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("building the DKG bypass contract...", "๐Ÿ—๏ธ"); + + ctx.execute_cmd_with_exit_status("docker", [ + "run", + "--rm", + "-v", + &format!("{}:/code", ctx.data.monorepo_root.to_string_lossy()), + "--mount", + &format!("type=volume,source={CONTRACTS_CACHE_VOLUME},target=/target"), + "--mount", + &format!( + "type=volume,source={CARGO_REGISTRY_CACHE_VOLUME},target=/usr/local/cargo/registry" + ), + &ctx.data.cosmwasm_optimizer_image, + "tools/internal/localnet-orchestrator/dkg-bypass-contract" // relative path to the contract code from the monorepo root + ]).await?; + + let source = ctx + .data + .monorepo_root + .join("artifacts") + .join("dkg_bypass_contract.wasm"); + let target = self.expected_bypass_contract_wasm_path(); + debug!("moving {} to {}", source.display(), target.display()); + + if !source.exists() { + bail!("source ({}) does not exist", source.display()); + } + + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + + std::fs::rename(&source, &target)?; + + // copy it to cache as well + let cache_path = self + .storage + .data_cache() + .cached_contract_path(contract_build_names::DKG_BYPASS_CONTRACT); + fs::copy(&target, &cache_path)?; + + ctx.data.dkg_bypass_contract.wasm_path = Some(target); + Ok(()) + } + + async fn upload_dkg_bypass_contract( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("uploading the DKG bypass contract...", "๐Ÿšš"); + + let cache = self.storage.data_cache(); + + let path = if ctx.data.allow_cached_build + && cache.cached_contract_exists(contract_build_names::DKG_BYPASS_CONTRACT) + { + cache.cached_contract_path(contract_build_names::DKG_BYPASS_CONTRACT) + } else { + self.expected_bypass_contract_wasm_path() + }; + + let upload_res = self.upload_contract(ctx, path).await?; + ctx.println(format!( + "\tdkg bypass contract uploaded with code: {}. tx: {}", + upload_res.code_id, upload_res.transaction_hash + )); + ctx.data.dkg_bypass_contract.upload_info = Some(upload_res.into()); + Ok(()) + } + + async fn migrate_to_bypass_contract( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("migrating into the DKG bypass contract...", "๐Ÿ”€"); + + let keys = ctx.data.dkg_keys()?; + let api_endpoint = self.localnet_details.nym_api_endpoint()?; + let api_address = self + .localnet_details + .auxiliary_accounts()? + .mixnet_rewarder + .address(); + + let migrate_msg = dkg_bypass_contract::MigrateMsg { + dealers: vec![FakeDealerData { + vk: keys.ecash_keys.verification_key().to_bs58(), + ed25519_identity: keys.ed25519_keypair.public_key().to_base58_string(), + announce: api_endpoint.to_string(), + owner: Addr::unchecked(api_address.as_ref()), + }], + }; + + let dkg_contract = &self.localnet_details.contracts()?.dkg; + + let dkg_admin = self.dkg_admin_signer()?; + let migrate_fut = dkg_admin.migrate( + &dkg_contract.address, + ctx.data.dkg_bypass_contract.upload_info()?.code_id, + &migrate_msg, + "migrating bypass DKG contract from localnet orchestrator", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + Ok(()) + } + + async fn restore_dkg_contract( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("restoring the original DKG contract...", "โ†ฉ๏ธ"); + + // retrieve DKG's original code (which will be the penultimate one) + let client = self.rpc_query_client()?; + + let code_fut = + client.get_contract_code_history(&self.localnet_details.contracts()?.dkg.address); + let code_history = ctx.async_with_progress(code_fut).await?; + let entries = code_history.len(); + let code_id = code_history + .get(entries - 2) + .context("dkg contract has not been initialised")? + .code_id; + + let dkg_admin = self.dkg_admin_signer()?; + + let migrate_msg = nym_coconut_dkg_common::msg::MigrateMsg {}; + let migrate_fut = dkg_admin.migrate( + &self.localnet_details.contracts()?.dkg.address, + code_id, + &migrate_msg, + "restoring original DKG contract", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + Ok(()) + } + + async fn add_dkg_group_members( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("adding all the cw4 group members...", "๐Ÿ‘ช๏ธ"); + + ctx.set_pb_message("โ›ฝ creating a new big cw4 family..."); + let admin = self.group_admin_signer()?; + let signer = &self.localnet_details.auxiliary_accounts()?.mixnet_rewarder; + let new_members = vec![Member { + addr: signer.address.to_string(), + weight: 1, + }]; + + let update_fut = admin.update_members(new_members, Vec::new(), None); + + ctx.async_with_progress(update_fut).await?; + ctx.println("\tโœ… new cw4 group members got added"); + Ok(()) + } + + async fn initialise_nym_api_data( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("setting up nym api instance data...", "๐Ÿ–‹๏ธ"); + + let monorepo_path = ctx.data.monorepo_root_canon()?; + let image_tag = default_nym_binaries_image_tag(&monorepo_path)?; + + // 1.1 retrieve nyxd ip + ctx.set_pb_prefix("[1/3]"); + ctx.set_pb_message("retrieving nyxd container ip address..."); + + let nyxd_container_ip = get_container_ip_address(ctx, &self.nyxd_container_name()).await?; + let nyxd_endpoint = format!("http://{nyxd_container_ip}:26657"); + let mnemonic = self + .localnet_details + .auxiliary_accounts()? + .mixnet_rewarder + .mnemonic + .to_string(); + + // 1.2 generate incomplete .env file (but complete enough-ish for the API to start) + let content = self.localnet_details.env_file_content()?; + let env_path = self + .storage + .nym_api_container_data_directory() + .join("localnet.env"); + fs::write(env_path, &content)?; + + // 3. run init + ctx.set_pb_prefix("[2/3]"); + ctx.set_pb_message("initialising nym-api data..."); + + run_container( + ctx, + [ + "--name", + &self.nym_api_container_name(), + "-v", + &self.nym_api_volume(), + "--network", + CONTAINER_NETWORK_NAME, + "--rm", + &image_tag, + "nym-api", + "-c", + "/root/.nym/nym-api/default/localnet.env", + "init", + "--nyxd-validator", + &nyxd_endpoint, + "--mnemonic", + &mnemonic, + "--enable-monitor", + "--enable-rewarding", + "--enable-zk-nym", + "--allow-illegal-ips", + "--utility-routes-bearer", + NYM_API_UTILITY_BEARER, + "--announce-address", + "http://placeholder.nym", + ], + ctx.data.custom_dns.clone(), + ) + .await?; + + // 3. copy keys + ctx.set_pb_prefix("[3/3]"); + ctx.set_pb_message("injecting pre-generated DKG keys..."); + + fs::copy(&ctx.data.dkg_key_location, self.storage.nym_api_ecash_key())?; + fs::copy( + &ctx.data.ed25519_private_key_location, + self.storage.nym_api_ed25519_private_key(), + )?; + fs::copy( + &ctx.data.ed25519_public_key_location, + self.storage.nym_api_ed25519_public_key(), + )?; + + Ok(()) + } + + // quite annoying https://github.com/apple/container/issues/282 is still not resolved + async fn start_nym_api( + &mut self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("starting up nym-api...", "๐Ÿš€"); + + let monorepo_path = ctx.data.monorepo_root_canon()?; + let image_tag = default_nym_binaries_image_tag(&monorepo_path)?; + + // I hate the fact we have to wait for a 'magic file' before the startup + // but that's the best I could think of without redesigning the whole ecash controller + // inside the nym api + let startup_cmd = r#" +CONTAINER_IP=$(hostname -i); +while [ ! -f /root/.nym/nym-api/default/dkg_ready ]; do + sleep 0.5; +done; + +nym-api -c /root/.nym/nym-api/default/localnet.env run --allow-illegal-ips --announce-address http://${CONTAINER_IP}:8000"#; + + // 2. start the api in the background + run_container( + ctx, + [ + "--name", + &self.nym_api_container_name(), + "-v", + &self.nym_api_volume(), + "--network", + CONTAINER_NETWORK_NAME, + "-d", + &image_tag, + "sh", + "-c", + startup_cmd, + ], + ctx.data.custom_dns.clone(), + ) + .await?; + + // 3. retrieve its container ip address + let nym_api_container_ip = + get_container_ip_address(ctx, &self.nym_api_container_name()).await?; + self.localnet_details + .set_nym_api_endpoint(format!("http://{nym_api_container_ip}:8000").parse()?); + + Ok(()) + } + + fn mark_dkg_as_ready(&self, ctx: &mut LocalnetContext) -> anyhow::Result<()> { + ctx.begin_next_step( + "creating magic file to inform nym-api of DKG being completed", + "๐Ÿช„", + ); + + let magic_file_location = self + .storage + .nym_api_container_data_directory() + .join("dkg_ready"); + + fs::write(magic_file_location, "")?; + Ok(()) + } + + async fn finalize_nym_api_setup( + &mut self, + mut ctx: LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("persisting nym api details", "๐Ÿ“"); + + // unfortunately we had to set `self.localnet_details.set_nym_api_endpoint` earlier due to + // non-predictable container ip addresses, so we can't be consistent with other setup steps + let address = self.localnet_details.nym_api_endpoint()?; + self.storage + .orchestrator() + .save_nym_api_details(&self.localnet_details.human_name, address.as_str()) + .await?; + self.state = LocalnetState::RunningNymApi; + + Ok(()) + } + + pub(crate) async fn initialise_nym_api(&mut self, config: Config) -> anyhow::Result<()> { + let setup = NymApiSetup::new(config)?; + let mut ctx = LocalnetContext::new(setup, 15, "\ninitialising nym-api with DKG keys"); + fs::create_dir_all(self.storage.nym_api_container_data_directory()) + .context("failed to create nym-api data directory")?; + + // 0.1 check if we have to do anything + if self.check_nym_api_container_is_running(&ctx).await? { + info!("nym-api instance for this localnet is already running"); + return Ok(()); + } + + // 0.2 check if container had already been built + let monorepo_path = ctx.data.monorepo_root_canon()?; + let image_tag = default_nym_binaries_image_tag(&monorepo_path)?; + if check_container_image_exists(&ctx, &image_tag).await? { + info!( + "'{image_tag}' container image already exists - skipping docker build and import", + ); + ctx.skip_steps(4); + } else { + // 1. docker build + self.build_nym_binaries_docker_image(&mut ctx).await?; + + // 2. docker save + self.save_nym_binaries_docker_image(&mut ctx).await?; + + // 3. container load + self.load_nym_binaries_into_container_runtime(&mut ctx) + .await?; + + // 4. container image inspect + self.verify_nym_binaries_image(&mut ctx).await?; + } + + // 5. generate (and persist) all keys needed for dkg + self.generate_dkg_keys(&mut ctx)?; + + // 6. initialise nym-api configs + self.initialise_nym_api_data(&mut ctx).await?; + + // 7. ensure the current contracts are in the valid state, i.e. DKG hasn't been run, + // the multisig group is empty, etc. + self.validate_dkg_contracts_state(&mut ctx).await?; + + // 8. start nyxd in the background and retrieve the container ip address + // (which is needed for the dkg bypass) + self.start_nym_api(&mut ctx).await?; + + // 9.1 check if the contract has already been build + if self.check_bypass_contract_built(&ctx) { + info!("the dkg bypass contract has already been built - skipping the step"); + ctx.skip_steps(1) + } else { + // 9.2 build it + self.build_dkg_bypass_contract(&mut ctx).await?; + } + + // 10. upload the dkg state bypass contract + // (to overwrite the current DKG state without having to actually perform the exchange) + self.upload_dkg_bypass_contract(&mut ctx).await?; + + // 11. migrate current dkg contract state into the bypass contract + // (keys are set in migrate msg) + self.migrate_to_bypass_contract(&mut ctx).await?; + + // 12. restore the original DKG contract code + self.restore_dkg_contract(&mut ctx).await?; + + // 13. add nym-api to the CW4 DKG group + self.add_dkg_group_members(&mut ctx).await?; + + // 14. create tha magic file for nym-api to trigger its full startup + self.mark_dkg_as_ready(&mut ctx)?; + + // 15. persist relevant information and update local state + self.finalize_nym_api_setup(ctx).await?; + + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_nodes.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_nodes.rs new file mode 100644 index 00000000000..58ebf3002de --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_nodes.rs @@ -0,0 +1,880 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + CONTAINER_NETWORK_NAME, LOCALNET_NYM_BINARIES_IMAGE_NAME, NYM_NODE_HTTP_BEARER, +}; +use crate::helpers::{ + monorepo_root_path, nym_api_cache_refresh_script, retrieve_current_nymnode_version, +}; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::account::Account; +use crate::orchestrator::container_helpers::{ + exec_container, get_container_ip_address, run_container, run_container_fut, +}; +use crate::orchestrator::context::LocalnetContext; +use crate::orchestrator::nym_node::LocalnetNymNode; +use crate::orchestrator::state::LocalnetState; +use anyhow::{Context, bail}; +use itertools::Itertools; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{NodeId, RoleAssignment}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use nym_validator_client::models::NodeRefreshBody; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, MixnetSigningClient, PagedMixnetQueryClient, +}; +use std::collections::BTreeMap; +use std::fs; +use std::ops::Range; +use std::path::PathBuf; +use time::OffsetDateTime; +use tokio::task::JoinSet; +use tracing::info; + +// for now just bond 3 mixnodes and 1 gateway +// in the future this could be made configurable +pub(crate) const GATEWAYS: usize = 1; +pub(crate) const MIXNODES: usize = 3; + +pub(crate) struct Config { + pub(crate) monorepo_root: Option, + pub(crate) custom_dns: Option, + pub(crate) open_proxy: bool, +} + +pub(crate) struct NymNodeSetup { + monorepo_root: PathBuf, + custom_dns: Option, + open_proxy: bool, + + nodes: BTreeMap, +} + +impl NymNodeSetup { + pub(crate) fn new(config: Config) -> anyhow::Result { + let monorepo_root = monorepo_root_path(config.monorepo_root)?; + + Ok(NymNodeSetup { + monorepo_root, + custom_dns: config.custom_dns, + open_proxy: config.open_proxy, + nodes: Default::default(), + }) + } + + pub(crate) fn nym_binaries_image_tag(&self) -> anyhow::Result { + let version = retrieve_current_nymnode_version(&self.monorepo_root)?; + Ok(format!("{LOCALNET_NYM_BINARIES_IMAGE_NAME}:{version}")) + } + + fn next_node_id(&self) -> NodeId { + let last_id = self + .nodes + .last_key_value() + .map(|(k, _)| k.to_owned()) + .unwrap_or_default(); + + // node ids are meant to start from 1 + last_id + 1 + } +} + +impl LocalnetOrchestrator { + fn mixnet_admin_signer(&self) -> anyhow::Result { + let mnemonic = &self.localnet_details.contracts()?.mixnet.admin.mnemonic; + self.signing_client(mnemonic) + } + + fn node_signer( + &self, + node: &LocalnetNymNode, + ) -> anyhow::Result { + self.signing_client(&node.owner.mnemonic) + } + + async fn validate_mixnet_contract_state( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("verifying mixnet contract state...", "๐Ÿค”"); + + let client = self.rpc_query_client()?; + let fut = client.get_all_nymnode_bonds(); + let nym_nodes = ctx.async_with_progress(fut).await?; + + if !nym_nodes.is_empty() { + bail!("attempted to bond nodes on a non-empty network") + } + + // for good measure also check legacy nodes in case some tests were messing with those + let fut = client.get_all_mixnode_bonds(); + let mixnodes = ctx.async_with_progress(fut).await?; + + if !mixnodes.is_empty() { + bail!("attempted to bond nodes on a non-empty network") + } + + let fut = client.get_all_gateways(); + let gateways = ctx.async_with_progress(fut).await?; + + if !gateways.is_empty() { + bail!("attempted to bond nodes on a non-empty network") + } + + Ok(()) + } + + async fn init_nym_node( + &self, + ctx: &mut LocalnetContext, + gateway: bool, + ) -> anyhow::Result<()> { + let account = Account::new(); + let node_id = ctx.data.next_node_id(); + + fs::create_dir_all(self.storage.nym_node_container_data_directory(node_id))?; + + let name = self.nym_node_container_name(node_id); + let nym_api = self.localnet_details.nym_api_endpoint()?; + let nyxd = self.localnet_details.rpc_endpoint()?; + let volume = self.nym_node_volume(node_id); + let image_tag = ctx.data.nym_binaries_image_tag()?; + let mnemonic = account.mnemonic.to_string(); + + let mut args = vec![ + "--name", + &name, + "-v", + &volume, + "--network", + CONTAINER_NETWORK_NAME, + "--rm", + &image_tag, + "nym-node", + "run", + "--init-only", + "--accept-operator-terms-and-conditions", + "--unsafe-disable-replay-protection", + // TODO: try to enable noise + "--unsafe-disable-noise", + // "--local" might actually not be needed. TBD + "--local", + "--http-access-token", + NYM_NODE_HTTP_BEARER, + // NOTE: this is a placeholder that will be changed once container is set to run + // 'properly' + "--public-ips", + "1.2.3.4", + "--mnemonic", + &mnemonic, + "--nym-api-urls", + nym_api.as_str(), + "--nyxd-urls", + nyxd.as_str(), + "--wireguard-userspace", + "true", + ]; + + if gateway { + // gw: --wireguard-enabled, --mode exit + args.push("--wireguard-enabled"); + args.push("true"); + args.push("--mode"); + args.push("exit-gateway"); + } else { + // not strictly needed + args.push("--mode"); + args.push("mixnode"); + } + + run_container(ctx, args, ctx.data.custom_dns.clone()).await?; + + // 2. retrieve current identity key + let private_key_path = self.storage.nym_node_ed25519_private_key_path(node_id); + let private_key: ed25519::PrivateKey = nym_pemstore::load_key(&private_key_path)?; + let keypair: ed25519::KeyPair = private_key.into(); + + let details = LocalnetNymNode { + id: node_id, + gateway, + identity: keypair, + owner: account, + }; + + ctx.data.nodes.insert(node_id, details); + + Ok(()) + } + + async fn init_nym_nodes(&self, ctx: &mut LocalnetContext) -> anyhow::Result<()> { + ctx.begin_next_step("initialising nym-nodes storage data...", "๐Ÿ”"); + + let total = MIXNODES + GATEWAYS; + + for i in 0..GATEWAYS { + ctx.set_pb_prefix(format!("[{}/{total}]", i + 1)); + ctx.set_pb_message("initialising a gateway..."); + self.init_nym_node(ctx, true).await?; + } + + for i in 0..MIXNODES { + ctx.set_pb_prefix(format!("[{}/{total}]", GATEWAYS + i + 1)); + ctx.set_pb_message("initialising a mixnode..."); + self.init_nym_node(ctx, false).await?; + } + + Ok(()) + } + + async fn transfer_bonding_tokens( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("sending initial tokens to node owners...", "๐Ÿ’ธ"); + + let mut receivers = Vec::new(); + + // make sure to send minimum bond (100nym) + minimum amount needed for verifying zk-nyms + for node in ctx.data.nodes.values() { + receivers.push((node.owner.address(), ctx.unyms(1000_000000))); + } + + let signing_client = self.master_signing_client()?; + let send_fut = + signing_client.send_multiple(receivers, "localnet nym-nodes token seeding", None); + let res = ctx.async_with_progress(send_fut).await?; + ctx.println(format!( + "\tโœ… sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + + Ok(()) + } + + async fn start_nym_node_container( + &self, + ctx: &LocalnetContext, + node: &LocalnetNymNode, + ) -> anyhow::Result<()> { + // 1. generate the .env file (we need valid contract addresses which can't be set via cli args) + let content = self.localnet_details.env_file_content()?; + let env_path = self + .storage + .nym_node_container_data_directory(node.id) + .join("localnet.env"); + fs::write(env_path, &content)?; + + let mut args = Vec::new(); + + let mut run_cmd = r#"CONTAINER_IP=$(hostname -i); +nym-node -c /root/.nym/nym-nodes/default-nym-node/localnet.env run --accept-operator-terms-and-conditions --public-ips ${CONTAINER_IP} --local --unsafe-disable-noise --wireguard-userspace true --unsafe-disable-replay-protection"#.to_string(); + + if ctx.data.open_proxy { + run_cmd.push_str(" --open-proxy=true"); + }; + + args.push("--name".to_string()); + args.push(self.nym_node_container_name(node.id)); + args.push("-v".to_string()); + args.push(self.nym_node_volume(node.id)); + args.push("--network".to_string()); + args.push(CONTAINER_NETWORK_NAME.to_string()); + args.push("-d".to_string()); + args.push(ctx.data.nym_binaries_image_tag()?); + args.push("sh".to_string()); + args.push("-c".to_string()); + args.push(run_cmd); + + // 2. start the container + run_container(ctx, args, ctx.data.custom_dns.clone()).await?; + + Ok(()) + } + + async fn start_nym_nodes_containers( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("starting nym-nodes containers...", "๐Ÿš€"); + + let total = ctx.data.nodes.len(); + for (i, node) in ctx.data.nodes.values().enumerate() { + ctx.set_pb_prefix(format!("[{}/{total}]", i + 1)); + ctx.set_pb_message("starting node container..."); + self.start_nym_node_container(ctx, node).await?; + } + + Ok(()) + } + + async fn bond_nym_node( + &self, + ctx: &LocalnetContext, + node: &LocalnetNymNode, + ) -> anyhow::Result<()> { + let container_name = self.nym_node_container_name(node.id); + + // 1. get node container ip + let node_ip = get_container_ip_address(ctx, &container_name).await?; + + // 2. prepare bonding signature + let payload = node.node_bonding_payload(node_ip); + + let stdout = exec_container( + ctx, + [ + &self.nym_node_container_name(node.id), + "nym-node", + "--no-banner", + "sign", + "--contract-msg", + &payload, + "--output", + "json", + ], + ) + .await?; + + let details: serde_json::Value = + serde_json::from_slice(&stdout).context("failed to parse signature details")?; + let signature = details + .get("encoded_signature") + .context("failed to retrieve ed25519 signature")?; + let signature_str = signature + .as_str() + .context("failed to retrieve ed25519 signature - not a string")?; + let parsed_signature = signature_str + .parse() + .context("failed to parse ed25519 signature")?; + + // 3. call the contract with bonding message + let client = self.node_signer(node)?; + + let fut = client.bond_nymnode( + node.bonding_nym_node(node_ip), + node.cost_params(), + parsed_signature, + node.pledge().into(), + None, + ); + let res = ctx.async_with_progress(fut).await?; + ctx.println(format!( + "\t node {} bonded in transaction: {}", + node.identity.public_key(), + res.transaction_hash, + )); + + Ok(()) + } + + async fn bond_nym_nodes(&self, ctx: &mut LocalnetContext) -> anyhow::Result<()> { + ctx.begin_next_step("starting nym-node bonding...", "โ›“๏ธ"); + + let total = ctx.data.nodes.len(); + for (i, node) in ctx.data.nodes.values().enumerate() { + ctx.set_pb_prefix(format!("[{}/{total}]", i + 1)); + ctx.set_pb_message("bonding nym-node..."); + self.bond_nym_node(ctx, node).await?; + } + + Ok(()) + } + + // that step is super flaky as nym-api might potentially pick up epoch changes and interject + // first we reduce the epoch length to 1s to essentially force it to finish immediately + // so that we could send all the rewarding txs to update the active set for the following epoch + // finally we restore the expected epoch duration + async fn assign_to_active_set( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("attempting to assign nodes to the active set...", "๐Ÿ”Œ"); + + let rewarder = self.mixnet_rewarder_signing_client()?; + let mixnet_admin = self.mixnet_admin_signer()?; + + let original_epoch = ctx + .async_with_progress(rewarder.get_current_interval_details()) + .await?; + + // ideally we'd make **all** the changes within a single tx to reduce the time chunk in which + // nym-api could cause us problems, but I'm not sure if we can guarantee correct ordering within + // the mempool, so we spread it throughout 3 blocks instead. + // but given ~1s block times, it should be fine + ctx.println_with_emoji("\treducing epoch length...", "๐Ÿ™ˆ"); + + let fut = mixnet_admin.update_interval_config( + original_epoch.interval.epochs_in_interval(), + 1, + true, + None, + ); + ctx.async_with_progress(fut).await?; + + let exec_msgs = vec![ + // 1. start epoch transition + ( + nym_mixnet_contract_common::ExecuteMsg::BeginEpochTransition {}, + vec![], + ), + // (nothing to reward) + // 2. reconcile events + ( + nym_mixnet_contract_common::ExecuteMsg::ReconcileEpochEvents { limit: None }, + vec![], + ), + // 3. assign (empty) exit + ( + nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::ExitGateway, + nodes: vec![], + }, + }, + vec![], + ), + // 4. assign entry + ( + nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::EntryGateway, + nodes: vec![1], + }, + }, + vec![], + ), + // 5. assign layer1 + ( + nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer1, + nodes: vec![2], + }, + }, + vec![], + ), + // 6. assign layer2 + ( + nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer2, + nodes: vec![3], + }, + }, + vec![], + ), + // 7. assign layer3 + ( + nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer3, + nodes: vec![4], + }, + }, + vec![], + ), + // 8. assign (empty) standby + ( + nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Standby, + nodes: vec![], + }, + }, + vec![], + ), + ]; + + ctx.println_with_emoji("\tadvancing epoch and assigning active set...", "๐Ÿ”Œ"); + let contract = &self.localnet_details.contracts()?.mixnet.address; + let fut = rewarder.execute_multiple( + contract, + exec_msgs, + None, + "hacking our way through the mixnet contract!", + ); + ctx.async_with_progress(fut).await?; + + ctx.println_with_emoji("\trestoring the original epoch length...", "๐Ÿ™ˆ"); + let fut = mixnet_admin.update_interval_config( + original_epoch.interval.epochs_in_interval(), + original_epoch.interval.epoch_length_secs(), + true, + None, + ); + ctx.async_with_progress(fut).await?; + + Ok(()) + } + + async fn force_refresh_nym_api_mixnet_and_describe_caches( + &mut self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("refreshing nym-api state [mixnet/described]...", "โณ"); + + // we need to do the following: + // 1. call `/v1/utility/mixnet-cache-timestamp` to get current cache ts + // 2. call `/v1/utility/refresh-mixnet-cache` to make the api start refreshing the cache + // 3. poll `/v1/utility/mixnet-cache-timestamp` until the timestamp changes + // 4. for each nym-node call `/v1/nym-nodes/refresh-described` + + let nym_api_endpoint = self.localnet_details.nym_api_endpoint()?; + let cache_timestamp_route = nym_api_endpoint.join("/v1/utility/mixnet-cache-timestamp")?; + let cache_refresh_route = nym_api_endpoint.join("/v1/utility/refresh-mixnet-cache")?; + let refresh_described_route = nym_api_endpoint.join("/v1/nym-nodes/refresh-described")?; + + ctx.set_pb_prefix("[1/2]"); + ctx.set_pb_message("trying to force refresh mixnet contract cache..."); + + let refresh_cache = + nym_api_cache_refresh_script(cache_timestamp_route, cache_refresh_route); + + run_container( + ctx, + [ + "--network", + CONTAINER_NETWORK_NAME, + "--rm", + &ctx.data.nym_binaries_image_tag()?, + "sh", + "-c", + &refresh_cache, + ], + ctx.data.custom_dns.clone(), + ) + .await?; + + ctx.set_pb_prefix("[2/2]"); + ctx.set_pb_message("trying to force refresh described cache..."); + + let mut refresh_futures = JoinSet::new(); + for (i, node) in ctx.data.nodes.values().enumerate() { + ctx.set_pb_prefix(format!("[{}/5]", i + 2)); + + let refresh_request = NodeRefreshBody::new(node.identity.private_key()); + let refresh_request_json = serde_json::to_string(&refresh_request)?; + + let refresh_cmd = format!( + r#" +set -euo pipefail + +curl --fail-with-body -s -X POST {refresh_described_route} \ + -H "Content-Type: application/json" \ + -d '{refresh_request_json}' > /dev/null + +"# + ); + let image_tag = ctx.data.nym_binaries_image_tag()?; + + let future = run_container_fut([ + "--network".to_string(), + CONTAINER_NETWORK_NAME.to_string(), + "--rm".to_string(), + image_tag, + "sh".to_string(), + "-c".to_string(), + refresh_cmd, + ]); + + refresh_futures.spawn(future); + } + + for res in ctx.async_with_progress(refresh_futures.join_all()).await { + res.context("cache refresh failure")?; + } + + Ok(()) + } + + async fn insert_fake_network_monitor_runs( + &self, + ctx: &LocalnetContext, + timestamps: Range, + ) -> anyhow::Result<()> { + let mut query = r#" + BEGIN; + + INSERT INTO monitor_run(timestamp) + VALUES + "# + .to_string(); + + let values = timestamps + .map(|result_ts| format!("({result_ts})")) + .join(",\n"); + + query.push_str(&values); + query.push_str(";\nCOMMIT;"); + + exec_container( + ctx, + [ + &self.nym_api_container_name(), + "sqlite3", + "/root/.nym/nym-api/default/data/db.sqlite", + &query, + ], + ) + .await?; + + Ok(()) + } + + async fn insert_fake_network_monitor_results_for_node( + &self, + ctx: &LocalnetContext, + node: &LocalnetNymNode, + timestamps: Range, + ) -> anyhow::Result<()> { + // target result (for node_id = 1, identity = 'DwxvqcjzCfvBWECZcW38Zf767CoFkcqxPKzSJZC4nSG4'): + /* + BEGIN; + + INSERT OR IGNORE INTO gateway_details (node_id, identity) + VALUES (1, 'DwxvqcjzCfvBWECZcW38Zf767CoFkcqxPKzSJZC4nSG4'); + + INSERT INTO gateway_status (gateway_details_id, reliability, timestamp) + VALUES ((SELECT id FROM gateway_details WHERE node_id = 1), 100, 1764782010); + + INSERT INTO gateway_status (gateway_details_id, reliability, timestamp) + VALUES ((SELECT id FROM gateway_details WHERE node_id = 1), 100, 1764782011); + + COMMIT; + */ + + let node_id = node.id; + let identity = node.identity.public_key().to_base58_string(); + + let insert_details = if node.gateway { + "INSERT OR IGNORE INTO gateway_details (node_id, identity)" + } else { + "INSERT OR IGNORE INTO mixnode_details (mix_id, identity_key)" + }; + let id_select = if node.gateway { + format!("SELECT id FROM gateway_details WHERE node_id = {node_id}") + } else { + format!("SELECT id FROM mixnode_details WHERE mix_id = {node_id}") + }; + let insert_status = if node.gateway { + "INSERT INTO gateway_status (gateway_details_id, reliability, timestamp)\n" + } else { + "INSERT INTO mixnode_status (mixnode_details_id, reliability, timestamp)\n" + }; + + let mut query = format!( + r#" +BEGIN; +{insert_details} +VALUES ({node_id}, '{identity}'); + +"# + ); + + for result_ts in timestamps { + query.push_str(insert_status); + query.push_str(&format!("VALUES (({id_select}), 100, {result_ts});\n")) + } + + query.push_str("\nCOMMIT;"); + + exec_container( + ctx, + [ + &self.nym_api_container_name(), + "sqlite3", + "/root/.nym/nym-api/default/data/db.sqlite", + &query, + ], + ) + .await?; + + Ok(()) + } + + async fn insert_fake_network_monitor_results( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("inserting fake network monitor measurements...", "๐Ÿฅท"); + + let now = OffsetDateTime::now_utc().unix_timestamp(); + let start = now - 100; + let ts_range = start..now; + + ctx.set_pb_message("inserting base monitor results..."); + self.insert_fake_network_monitor_runs(ctx, ts_range.clone()) + .await?; + + for node in ctx.data.nodes.values() { + ctx.set_pb_message(format!("inserting fake results for node {}...", node.id)); + self.insert_fake_network_monitor_results_for_node(ctx, node, ts_range.clone()) + .await?; + } + + Ok(()) + } + + async fn force_refresh_nym_api_annotations_cache( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("refreshing nym-api state [annotations]...", "โณ"); + + // we need to do the following: + // 1. call `/v1/utility/node-annotations-cache-timestamp` to get current cache ts + // 2. call `/v1/utility/refresh-node-annotations-cache` to make the api start refreshing the cache + // 3. poll `/v1/utility/node-annotations-cache-timestamp` until the timestamp changes + + let nym_api_endpoint = self.localnet_details.nym_api_endpoint()?; + let cache_timestamp_route = + nym_api_endpoint.join("/v1/utility/node-annotations-cache-timestamp")?; + let cache_refresh_route = + nym_api_endpoint.join("/v1/utility/refresh-node-annotations-cache")?; + + let refresh_cache = + nym_api_cache_refresh_script(cache_timestamp_route, cache_refresh_route); + + run_container( + ctx, + [ + "--network", + CONTAINER_NETWORK_NAME, + "--rm", + &ctx.data.nym_binaries_image_tag()?, + "sh", + "-c", + &refresh_cache, + ], + ctx.data.custom_dns.clone(), + ) + .await?; + + Ok(()) + } + + async fn setup_gateway_forwarding( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("setting up gateway forwarding rules", "๐Ÿ”€"); + + // for now ignore ipv6 - they seem to be having their own set of issues + const IP_RULES: &str = r#" +set -euo pipefail + +# Enable IP forwarding +echo 1 > /proc/sys/net/ipv4/ip_forward +echo 1 > /proc/sys/net/ipv6/conf/all/forwarding + +# Add NAT masquerade for outbound traffic +iptables -t nat -C POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE +ip6tables -t nat -C POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null || ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE + +# nymtun0 +iptables -C FORWARD -i nymtun0 -o eth0 -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -i nymtun0 -o eth0 -j ACCEPT +iptables -C FORWARD -i eth0 -o nymtun0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -I FORWARD 2 -i eth0 -o nymtun0 -m state --state RELATED,ESTABLISHED -j ACCEPT + +ip6tables -C FORWARD -i nymtun0 -o eth0 -j ACCEPT 2>/dev/null || ip6tables -I FORWARD 1 -i nymtun0 -o eth0 -j ACCEPT +ip6tables -C FORWARD -i eth0 -o nymtun0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || ip6tables -I FORWARD 2 -i eth0 -o nymtun0 -m state --state RELATED,ESTABLISHED -j ACCEPT + +# nymwg +iptables -C FORWARD -i nymwg -o eth0 -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -i nymwg -o eth0 -j ACCEPT +iptables -C FORWARD -i eth0 -o nymwg -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -I FORWARD 2 -i eth0 -o nymwg -m state --state RELATED,ESTABLISHED -j ACCEPT + +ip6tables -C FORWARD -i nymwg -o eth0 -j ACCEPT 2>/dev/null || ip6tables -I FORWARD 1 -i nymwg -o eth0 -j ACCEPT +ip6tables -C FORWARD -i eth0 -o nymwg -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || ip6tables -I FORWARD 2 -i eth0 -o nymwg -m state --state RELATED,ESTABLISHED -j ACCEPT + +# DNS + ICMP +iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT +iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT + +iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || iptables -A INPUT -p udp --dport 53 -j ACCEPT +iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || iptables -A INPUT -p tcp --dport 53 -j ACCEPT + "#; + + for node in ctx.data.nodes.values() { + if node.gateway { + exec_container( + ctx, + [&self.nym_node_container_name(node.id), "sh", "-c", IP_RULES], + ) + .await?; + } + } + Ok(()) + } + + async fn finalize_nym_nodes_setup( + &mut self, + mut ctx: LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("persisting nym nodes details", "๐Ÿ“"); + + let network_name = &self.localnet_details.human_name; + + for node in ctx.data.nodes.values() { + self.storage + .orchestrator() + .save_nym_node_details(network_name, node) + .await?; + } + + self.state = LocalnetState::RunningNymNodes; + Ok(()) + } + + pub(crate) async fn initialise_nym_nodes(&mut self, config: Config) -> anyhow::Result<()> { + let setup = NymNodeSetup::new(config)?; + let mut ctx = LocalnetContext::new(setup, 11, "\ninitialising nym nodes"); + + // 0 check if we have to do anything + if self.check_nym_node_containers_are_running(&ctx).await? { + info!("nym node instances for this localnet are already running"); + return Ok(()); + } + + // no need to build containers as we're using the same one as nym-api which MUST be running + + // 1. ensure the current mixnet contract is empty, i.e. no nodes are bonded + self.validate_mixnet_contract_state(&mut ctx).await?; + + // 2. run init on all nodes to create initial data + self.init_nym_nodes(&mut ctx).await?; + + // 3. send tokens needed for bonding for all nodes + self.transfer_bonding_tokens(&mut ctx).await?; + + // 4. start nym-nodes to get their proper container addresses to use for bonding + self.start_nym_nodes_containers(&mut ctx).await?; + + // 5. perform the bonding of all the nodes + self.bond_nym_nodes(&mut ctx).await?; + + // 6. hack the mixnet contract by forcing epoch transition to assign the new nodes to the active set + self.assign_to_active_set(&mut ctx).await?; + + // 7. force refresh state of nym-api to fully recognise new nodes + self.force_refresh_nym_api_mixnet_and_describe_caches(&mut ctx) + .await?; + + // 8. insert some fake monitoring results to bump up nodes performance without waiting + // for NM to go around + self.insert_fake_network_monitor_results(&mut ctx).await?; + + // 9. force refresh node annotations to update node scores served + self.force_refresh_nym_api_annotations_cache(&mut ctx) + .await?; + + // 10. set forwarding rules on gateways (at this point the nodes must have been running + // for sufficiently long for the relevant interfaces to have been created) + self.setup_gateway_forwarding(&mut ctx).await?; + + // 11. persist relevant information and update local state + self.finalize_nym_nodes_setup(ctx).await?; + + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/nyxd.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/nyxd.rs new file mode 100644 index 00000000000..795b35b270f --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/nyxd.rs @@ -0,0 +1,341 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{CONTAINER_NETWORK_NAME, LOCALNET_NYXD_IMAGE_NAME}; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::account::Account; +use crate::orchestrator::container_helpers::{ + check_container_image_exists, get_container_ip_address, load_image_into_container_runtime, + run_container, run_container_fallible, save_docker_image, +}; +use crate::orchestrator::context::LocalnetContext; +use crate::orchestrator::network::NyxdDetails; +use crate::orchestrator::state::LocalnetState; +use anyhow::{Context, bail}; +use std::fs; +use std::net::IpAddr; +use tempfile::NamedTempFile; +use tracing::info; +use url::Url; + +pub(crate) struct Config { + pub(crate) nyxd_repo: Url, + pub(crate) nyxd_dockerfile_path: String, + pub(crate) custom_dns: Option, + pub(crate) nyxd_tag: String, +} + +struct NyxdSetup { + config: Config, + master_account: Account, + nyxd_image_location: NamedTempFile, + nyxd_ip: Option, +} + +impl NyxdSetup { + pub(crate) fn new(config: Config) -> anyhow::Result { + Ok(NyxdSetup { + config, + nyxd_image_location: NamedTempFile::new() + .context("failed to create temporary file for nyxd image")?, + master_account: Account::new(), + nyxd_ip: None, + }) + } + + pub(crate) fn image_tag(&self) -> String { + format!("{LOCALNET_NYXD_IMAGE_NAME}:{}", self.config.nyxd_tag) + } + + pub(crate) fn image_temp_location_arg(&self) -> anyhow::Result<&str> { + self.nyxd_image_location + .path() + .to_str() + .context("invalid temporary file location") + } + + fn into_nyxd_details(self) -> anyhow::Result { + let ip = self.nyxd_ip.context("nyxd ip is not set")?; + // for now the port is not configurable (it's not difficult to change that later) + Ok(NyxdDetails { + rpc_endpoint: format!("http://{ip}:26657").parse()?, + master_account: self.master_account, + }) + } +} + +impl LocalnetOrchestrator { + async fn build_nyxd_docker_image( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step( + "building nyxd docker image... this might take few minutes...", + "๐Ÿ—๏ธ", + ); + let cfg = &ctx.data.config; + ctx.execute_cmd_with_exit_status( + "docker", + [ + "build", + "--platform", + "linux/amd64", + "-f", + &cfg.nyxd_dockerfile_path, + &format!("{}#{}", cfg.nyxd_repo, cfg.nyxd_tag), + "-t", + &ctx.data.image_tag(), + ], + ) + .await?; + Ok(()) + } + + async fn save_nyxd_docker_image( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + let output_path = ctx.data.image_temp_location_arg()?.to_owned(); + let image_tag = ctx.data.image_tag(); + + save_docker_image(ctx, &output_path, &image_tag).await + } + + async fn load_nyxd_into_container_runtime( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + let image_path = ctx.data.image_temp_location_arg()?.to_owned(); + load_image_into_container_runtime(ctx, &image_path).await + } + + async fn verify_nyxd_image(&self, ctx: &mut LocalnetContext) -> anyhow::Result<()> { + ctx.begin_next_step("verifying nyxd container image...", "โ”"); + + if !check_container_image_exists(ctx, &ctx.data.image_tag()).await? { + bail!("nyxd image verification failed"); + } + Ok(()) + } + + async fn check_genesis_exists( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result { + let status = run_container_fallible( + ctx, + [ + "--name", + &self.nyxd_container_name(), + "-v", + &self.nyxd_volume(), + "--rm", + &ctx.data.image_tag(), + "test", + "-f", + "/root/.nyxd/config/genesis.json", + ], + ) + .await?; + Ok(status.success()) + } + + async fn initialise_nyxd_data( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("initialising nyxd data...", "๐Ÿ“"); + + ctx.set_pb_prefix("[1/2]"); + ctx.set_pb_message("generating nyxd config..."); + + // unfortunately we have to do it manually as scripts embedded in the image + // (as of v0.60.1) either do not directly expose the genesis mnemonic + // or assume joining existing consensus as opposed starting from genesis + // (and yes, technically `sed` could have been replaced by just directly modifying the files + // on disk, but why break the tradition?) + // + // and why is it split into 2 commands? + // because it made the whole thing easier due to the interactive prompts for key import + let init_cmd1 = format!( + r#" + nyxd init nyx --chain-id nyx + + sed -i "s/\"stake\"/\"unyx\"/" "/root/.nyxd/config/genesis.json" + sed -i 's/minimum-gas-prices = "0stake"/minimum-gas-prices = "0.025unym"/' "/root/.nyxd/config/app.toml" + sed -i '0,/enable = false/s//enable = true/g' "/root/.nyxd/config/app.toml" + + sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' "/root/.nyxd/config/config.toml" + sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' "/root/.nyxd/config/config.toml" + sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' "/root/.nyxd/config/config.toml" + sed -i 's/address = "tcp:\/\/localhost:1317"/address = "tcp:\/\/0.0.0.0:1317"/' "/root/.nyxd/config/app.toml" + + sed -i 's/timeout_propose = "3s"/timeout_propose = "500ms"/' "/root/.nyxd/config/config.toml" + sed -i 's/timeout_propose_delta = "500ms"/timeout_propose_delta = "50ms"/' "/root/.nyxd/config/config.toml" + sed -i 's/timeout_prevote = "1s"/timeout_prevote = "200ms"/' "/root/.nyxd/config/config.toml" + sed -i 's/timeout_prevote_delta = "500ms"/timeout_prevote_delta = "50ms"/' "/root/.nyxd/config/config.toml" + sed -i 's/timeout_precommit = "1s"/timeout_precommit = "200ms"/' "/root/.nyxd/config/config.toml" + sed -i 's/timeout_precommit_delta = "500ms"/timeout_precommit_delta = "50ms"/' "/root/.nyxd/config/config.toml" + sed -i 's/timeout_commit = "5s"/timeout_commit = "1s"/' "/root/.nyxd/config/config.toml" + + cat << 'EOF' | nyxd keys add -i {}-admin + {} + + password + password + EOF" + "#, + self.localnet_details.human_name, ctx.data.master_account.mnemonic + ); + + run_container( + ctx, + [ + "--name", + &self.nyxd_container_name(), + "-v", + &self.nyxd_volume(), + "--rm", + &ctx.data.image_tag(), + "sh", + "-c", + &init_cmd1, + ], + ctx.data.config.custom_dns.clone(), + ) + .await?; + + ctx.set_pb_prefix("[2/2]"); + ctx.set_pb_message("generating genesis file..."); + + let init_cmd2 = format!( + r#" + yes password | nyxd genesis add-genesis-account {}-admin 1000000000000000unym,1000000000000000unyx + yes password | nyxd genesis gentx {}-admin 100000000000unyx --chain-id nyx + nyxd genesis collect-gentxs + nyxd genesis validate-genesis + "#, + self.localnet_details.human_name, self.localnet_details.human_name, + ); + + run_container( + ctx, + [ + "--name", + &self.nyxd_container_name(), + "-v", + &self.nyxd_volume(), + "--rm", + &ctx.data.image_tag(), + "sh", + "-c", + &init_cmd2, + ], + ctx.data.config.custom_dns.clone(), + ) + .await?; + Ok(()) + } + + async fn start_nyxd(&self, ctx: &mut LocalnetContext) -> anyhow::Result<()> { + ctx.begin_next_step("spawning nyxd container", "๐Ÿš€"); + + run_container( + ctx, + [ + "--name", + &self.nyxd_container_name(), + "-v", + &self.nyxd_volume(), + "--network", + CONTAINER_NETWORK_NAME, + "-p", + // TEMP: expose tendermint rpc port to make our setup life easier + "26657:26657", + "-d", + &ctx.data.image_tag(), + "nyxd", + "start", + ], + ctx.data.config.custom_dns.clone(), + ) + .await?; + + Ok(()) + } + + async fn finalize_nyxd_setup( + &mut self, + mut ctx: LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("persisting nyxd details", "๐Ÿ“"); + + let container_ip = get_container_ip_address(&ctx, &self.nyxd_container_name()).await?; + ctx.data.nyxd_ip = Some(container_ip); + + let nyxd_details = ctx.data.into_nyxd_details()?; + self.storage + .orchestrator() + .save_nyxd_details(&nyxd_details) + .await?; + + self.localnet_details.set_nyxd_details(nyxd_details); + self.state = LocalnetState::RunningNyxd; + + Ok(()) + } + + pub(crate) async fn initialise_nyxd(&mut self, config: Config) -> anyhow::Result<()> { + let setup = NyxdSetup::new(config)?; + let mut ctx = LocalnetContext::new(setup, 7, "\ninitialising new nyxd instance"); + fs::create_dir_all(self.storage.nyxd_container_data_directory()) + .context("failed to create nyxd data directory")?; + + // 0.1 check if we have to do anything + if self.check_nyxd_container_is_running(&ctx).await? { + info!("nyxd instance for this localnet is already running"); + return Ok(()); + } + + // 0.2 check if container had already been built + let image_tag = &ctx.data.image_tag(); + if check_container_image_exists(&ctx, image_tag).await? { + info!( + "'{image_tag}' container image already exists - skipping docker build and import", + ); + ctx.skip_steps(4); + } else { + // 1. docker build + self.build_nyxd_docker_image(&mut ctx).await?; + + // 2. docker save + self.save_nyxd_docker_image(&mut ctx).await?; + + // 3. container load + self.load_nyxd_into_container_runtime(&mut ctx).await?; + + // 4. container image inspect + self.verify_nyxd_image(&mut ctx).await?; + } + + // 5.1 check if genesis.json exists, i.e. chain had been initialised + if self.check_genesis_exists(&mut ctx).await? { + info!( + "'{}' already had its genesis generated - skipping the process", + self.nyxd_container_name() + ); + ctx.skip_steps(1); + } else { + // 5.2 perform nyxd init, gentx, etc. + self.initialise_nyxd_data(&mut ctx).await?; + } + + // 6. start nyxd in the background + self.start_nyxd(&mut ctx).await?; + + // 7. persist relevant information and update local state + self.finalize_nyxd_setup(ctx).await?; + + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/purge.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/purge.rs new file mode 100644 index 00000000000..8ee4ee5f46d --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/purge.rs @@ -0,0 +1,103 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::monorepo_root_path; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::container_helpers::{ + default_nym_binaries_image_tag, remove_container_image, +}; +use crate::orchestrator::context::LocalnetContext; +use anyhow::Context; +use std::path::PathBuf; + +struct LocalnetPurge { + remove_images: bool, + remove_cache: bool, + monorepo_root: PathBuf, +} + +impl LocalnetPurge { + fn new(config: Config) -> anyhow::Result { + let monorepo_root = monorepo_root_path(config.monorepo_root)?; + + Ok(LocalnetPurge { + remove_images: config.remove_images, + remove_cache: config.remove_cache, + monorepo_root, + }) + } +} + +pub(crate) struct Config { + pub(crate) remove_images: bool, + pub(crate) remove_cache: bool, + pub(crate) monorepo_root: Option, +} + +impl LocalnetOrchestrator { + async fn remove_built_images( + &self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("removing all built images", "๐Ÿ”ฅ"); + if !ctx.data.remove_images { + ctx.println("\t NOT ENABLED - SKIPPING"); + return Ok(()); + } + + let nym_binaries_tag = default_nym_binaries_image_tag(&ctx.data.monorepo_root)?; + + // TODO: we need to dynamically determine tag for this + // LOCALNET_NYXD_IMAGE_NAME.to_string() + + for tag in [nym_binaries_tag] { + ctx.execute_cmd_with_stdout("docker", ["image", "rm", &tag]) + .await?; + remove_container_image(ctx, &tag).await?; + } + + Ok(()) + } + + fn remove_build_cache(&self, ctx: &mut LocalnetContext) -> anyhow::Result<()> { + ctx.begin_next_step("removing build cache", "๐Ÿ”ฅ"); + if !ctx.data.remove_cache { + ctx.println("\t NOT ENABLED - SKIPPING"); + return Ok(()); + } + + self.storage.data_cache().clear() + } + + async fn remove_storage_data( + self, + ctx: &mut LocalnetContext, + ) -> anyhow::Result<()> { + ctx.begin_next_step("removing storage data", "๐Ÿ”ฅ"); + + std::fs::remove_dir_all(self.storage.localnet_directory()) + .context("missing main storage directory")?; + let storage_db = self.storage.into_orchestrator_storage(); + let db_path = storage_db.stop().await?; + std::fs::remove_file(db_path).context("missing database path")?; + + Ok(()) + } + + pub(crate) async fn purge_localnet(self, config: Config) -> anyhow::Result<()> { + let purge = LocalnetPurge::new(config)?; + let mut ctx = LocalnetContext::new(purge, 3, "\npurging localnet"); + + // 1. stop the localnet + self.stop_localnet().await?; + + // 2. remove docker (and container) images + self.remove_built_images(&mut ctx).await?; + + // 3. remove build cache + self.remove_build_cache(&mut ctx)?; + + // 4. remove all storage dir + self.remove_storage_data(&mut ctx).await + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/rebuild_binaries_image.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/rebuild_binaries_image.rs new file mode 100644 index 00000000000..307b84d51fb --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/rebuild_binaries_image.rs @@ -0,0 +1,95 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::monorepo_root_path; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::container_helpers::{ + check_container_image_exists, default_nym_binaries_image_tag, + load_image_into_container_runtime, save_docker_image, +}; +use crate::orchestrator::context::LocalnetContext; +use anyhow::{Context, bail}; +use std::path::PathBuf; +use tempfile::NamedTempFile; + +pub(crate) struct Config { + pub(crate) custom_tag: Option, + pub(crate) monorepo_root: Option, +} + +struct ImageRebuild { + monorepo_root: PathBuf, + nym_binaries_image_location: NamedTempFile, + + tag: String, +} + +impl ImageRebuild { + fn new(config: Config) -> anyhow::Result { + let monorepo_root = monorepo_root_path(config.monorepo_root)?; + + let tag = config + .custom_tag + .unwrap_or(default_nym_binaries_image_tag(&monorepo_root)?); + + Ok(ImageRebuild { + monorepo_root, + nym_binaries_image_location: NamedTempFile::new()?, + tag, + }) + } + + fn monorepo_root_canon(&self) -> anyhow::Result { + Ok(self.monorepo_root.canonicalize()?) + } + + fn nym_binaries_dockerfile_location_canon(&self) -> anyhow::Result { + Ok(self + .monorepo_root + .join("docker") + .join("localnet") + .join("nym-binaries-localnet.Dockerfile") + .canonicalize()?) + } + + fn image_temp_location_arg(&self) -> anyhow::Result<&str> { + self.nym_binaries_image_location + .path() + .to_str() + .context("invalid temporary file location") + } +} + +impl LocalnetOrchestrator { + pub(crate) async fn rebuild_binaries_image(&self, config: Config) -> anyhow::Result<()> { + let rebuild = ImageRebuild::new(config)?; + let mut ctx = LocalnetContext::new(rebuild, 4, "\nrebuilding nym-binaries image"); + + let dockerfile_path = ctx.data.nym_binaries_dockerfile_location_canon()?; + let monorepo_path = ctx.data.monorepo_root_canon()?; + let image_location = ctx.data.image_temp_location_arg()?.to_owned(); + let image_tag = ctx.data.tag.clone(); + + // 1. docker build + self.try_build_nym_binaries_docker_image( + &mut ctx, + dockerfile_path, + monorepo_path, + &image_tag, + ) + .await?; + + // 2. docker save + save_docker_image(&mut ctx, &image_location, &image_tag).await?; + + // 3. container load + load_image_into_container_runtime(&mut ctx, &image_location).await?; + + // 4. container image inspect + if !check_container_image_exists(&ctx, &image_tag).await? { + bail!("localnet-nym-binaries image verification failed"); + } + + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/setup/up.rs b/tools/internal/localnet-orchestrator/src/orchestrator/setup/up.rs new file mode 100644 index 00000000000..1092432fe75 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/setup/up.rs @@ -0,0 +1,33 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::setup::{cosmwasm_contracts, nym_api, nym_nodes, nyxd}; + +pub(crate) struct Config { + pub(crate) nyxd_setup: nyxd::Config, + pub(crate) contracts_setup: cosmwasm_contracts::Config, + pub(crate) nym_api_setup: nym_api::Config, + pub(crate) nym_nodes_setup: nym_nodes::Config, +} + +impl LocalnetOrchestrator { + pub(crate) async fn start_localnet(&mut self, config: Config) -> anyhow::Result<()> { + // 1. start nyxd + self.initialise_nyxd(config.nyxd_setup).await?; + + // 2. upload contracts + self.initialise_contracts(config.contracts_setup).await?; + + // 3. start nym-api (and setup DKG) + self.initialise_nym_api(config.nym_api_setup).await?; + + // 4. launch nym-nodes + self.initialise_nym_nodes(config.nym_nodes_setup).await?; + + // ??? + + // 5. profit! + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/state.rs b/tools/internal/localnet-orchestrator/src/orchestrator/state.rs new file mode 100644 index 00000000000..4de70728848 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/state.rs @@ -0,0 +1,23 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, Clone, Copy, PartialEq, Default, strum_macros::Display)] +#[strum(serialize_all = "snake_case")] +pub(crate) enum LocalnetState { + /// Defines brand new network without anything deployed on it + #[default] + Uninitialised, + + /// Defines network that only has a nyxd instance on it + RunningNyxd, + + /// Defines network that has had cosmwasm smart contracts initialised on it + DeployedNymContracts, + + /// Defines network with a functional instance of nym-api that is capable of issuing zk-nyms + RunningNymApi, + + /// Defines network with a functional mixnet + // TODO: might have to split between running and bonding + RunningNymNodes, // more steps could be added later to indicate, for example, deployed credential proxy or vpn api +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/storage/cache.rs b/tools/internal/localnet-orchestrator/src/orchestrator/storage/cache.rs new file mode 100644 index 00000000000..c7f20661687 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/storage/cache.rs @@ -0,0 +1,47 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fs; +use std::path::{Path, PathBuf}; + +pub(crate) struct LocalnetCache { + cache_dir: PathBuf, +} + +impl LocalnetCache { + pub(crate) fn new>(cache_dir: P) -> anyhow::Result { + let cache_dir = cache_dir.as_ref(); + + let this = Self { + cache_dir: cache_dir.to_path_buf(), + }; + + // make sure all paths exist + fs::create_dir_all(cache_dir)?; + fs::create_dir_all(this.contracts_directory())?; + fs::create_dir_all(this.kernel_configs_directory())?; + + Ok(this) + } + + pub(crate) fn contracts_directory(&self) -> PathBuf { + self.cache_dir.join("contracts") + } + + pub(crate) fn kernel_configs_directory(&self) -> PathBuf { + self.cache_dir.join("kernels") + } + + pub(crate) fn cached_contract_path(&self, contract_filename: &str) -> PathBuf { + self.contracts_directory().join(contract_filename) + } + + pub(crate) fn cached_contract_exists(&self, contract_filename: &str) -> bool { + self.cached_contract_path(contract_filename).exists() + } + + pub(crate) fn clear(&self) -> anyhow::Result<()> { + fs::remove_dir_all(&self.cache_dir)?; + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/storage/mod.rs b/tools/internal/localnet-orchestrator/src/orchestrator/storage/mod.rs new file mode 100644 index 00000000000..b45758a7071 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/storage/mod.rs @@ -0,0 +1,121 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::storage::cache::LocalnetCache; +use crate::orchestrator::storage::orchestrator::LocalnetOrchestratorStorage; +use nym_config::{NYM_DIR, must_get_home}; +use nym_mixnet_contract_common::NodeId; +use std::fs; +use std::path::{Path, PathBuf}; + +pub(crate) mod cache; +pub(crate) mod orchestrator; + +const NYXD_CONTAINER_DATA_DIR: &str = "nyxd"; +const NYM_API_CONTAINER_DATA_DIR: &str = "nym-api"; +const NYM_NODE_CONTAINER_DATA_DIR_PREFIX: &str = "nym-node"; + +const COSMWASM_CONTRACTS_DIR: &str = "contracts"; + +pub(crate) fn default_storage_dir() -> PathBuf { + must_get_home().join(NYM_DIR).join("localnet-orchestrator") +} + +pub(crate) fn default_cache_dir() -> PathBuf { + default_storage_dir().join(".cache") +} + +pub(crate) fn default_orchestrator_db_file() -> PathBuf { + default_storage_dir().join("network-data.sqlite") +} + +pub(crate) struct LocalnetStorage { + // db with mnemonics and whatnot (to be copied from testnet manager) + // you may ask wtf is it a sqlite db, isn't it an overkill? + // in a way yes, but I needed some way to persist a bunch of data - mnemonics, addresses, ids, etc. + // and shuffling multiple files around turned to be very annoying, very quickly, + // so instead I grouped it in a single sqlite db file + orchestrator_data: LocalnetOrchestratorStorage, + + data_cache: LocalnetCache, + + localnet_directory: PathBuf, +} + +impl LocalnetStorage { + pub fn new( + localnet_directory: impl AsRef, + cache_dir: impl AsRef, + orchestrator_data: LocalnetOrchestratorStorage, + ) -> anyhow::Result { + let localnet_directory = localnet_directory.as_ref(); + let cache_dir = cache_dir.as_ref(); + + fs::create_dir_all(localnet_directory)?; + + Ok(LocalnetStorage { + orchestrator_data, + data_cache: LocalnetCache::new(cache_dir)?, + localnet_directory: localnet_directory.to_path_buf(), + }) + } + + pub(crate) fn cosmwasm_contracts_directory(&self) -> PathBuf { + self.localnet_directory.join(COSMWASM_CONTRACTS_DIR) + } + + pub(crate) fn nyxd_container_data_directory(&self) -> PathBuf { + self.localnet_directory.join(NYXD_CONTAINER_DATA_DIR) + } + + pub(crate) fn nym_api_container_data_directory(&self) -> PathBuf { + self.localnet_directory.join(NYM_API_CONTAINER_DATA_DIR) + } + + pub(crate) fn global_env_file(&self) -> PathBuf { + self.localnet_directory.join("localnet.env") + } + + pub(crate) fn nym_node_container_data_directory(&self, id: NodeId) -> PathBuf { + self.localnet_directory + .join(format!("{NYM_NODE_CONTAINER_DATA_DIR_PREFIX}-{id}")) + } + + pub(crate) fn nym_node_ed25519_private_key_path(&self, id: NodeId) -> PathBuf { + self.nym_node_container_data_directory(id) + .join("data") + .join("ed25519_identity") + } + + fn nym_api_data_directory(&self) -> PathBuf { + self.nym_api_container_data_directory().join("data") + } + + pub(crate) fn nym_api_ecash_key(&self) -> PathBuf { + self.nym_api_data_directory().join("coconut.pem") + } + + pub(crate) fn nym_api_ed25519_private_key(&self) -> PathBuf { + self.nym_api_data_directory().join("private_identity.pem") + } + + pub(crate) fn nym_api_ed25519_public_key(&self) -> PathBuf { + self.nym_api_data_directory().join("public_identity.pem") + } + + pub(crate) fn orchestrator(&self) -> &LocalnetOrchestratorStorage { + &self.orchestrator_data + } + + pub(crate) fn data_cache(&self) -> &LocalnetCache { + &self.data_cache + } + + pub(crate) fn localnet_directory(&self) -> &Path { + &self.localnet_directory + } + + pub(crate) fn into_orchestrator_storage(self) -> LocalnetOrchestratorStorage { + self.orchestrator_data + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/manager.rs b/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/manager.rs new file mode 100644 index 00000000000..1cef6959274 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/manager.rs @@ -0,0 +1,325 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::storage::orchestrator::models::{ + LocalnetMetadata, RawAccount, RawAuthorisedNetworkMonitor, RawAuxiliaryAccounts, RawContract, + RawLocalnetContracts, RawNymApi, RawNymNode, RawNyxd, StoredMetadata, +}; + +#[derive(Clone)] +pub(crate) struct StorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +#[allow(dead_code)] +impl StorageManager { + pub(crate) fn into_connection_pool(self) -> sqlx::SqlitePool { + self.connection_pool + } + + pub(crate) async fn save_latest_network_id( + &self, + latest_network_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE metadata SET latest_network_id = ?", + latest_network_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn save_latest_nyxd_id(&self, latest_nyxd_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!("UPDATE metadata SET latest_nyxd_id = ?", latest_nyxd_id) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_metadata(&self) -> Result { + sqlx::query_as("SELECT * FROM metadata") + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_localnet_metadata(&self, name: String) -> Result { + let localnet_id = sqlx::query!("INSERT INTO localnet_metadata (name) VALUES (?)", name,) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(localnet_id) + } + + pub(crate) async fn load_localnet_metadata( + &self, + localnet_id: i64, + ) -> Result { + sqlx::query_as("SELECT * FROM localnet_metadata WHERE id = ?") + .bind(localnet_id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn load_localnet_metadata_by_name( + &self, + name: &str, + ) -> Result { + sqlx::query_as("SELECT * FROM localnet_metadata WHERE name = ?") + .bind(name) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_nyxd_details( + &self, + rpc_endpoint: String, + master_address: String, + ) -> Result { + let nyxd_id = sqlx::query!( + "INSERT INTO nyxd (rpc_endpoint, master_address) VALUES (?, ?)", + rpc_endpoint, + master_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(nyxd_id) + } + + pub(crate) async fn load_nyxd_details(&self, nyxd_id: i64) -> Result { + sqlx::query_as("SELECT * FROM nyxd WHERE id = ?") + .bind(nyxd_id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn load_nyxd_by_master_address( + &self, + address: &str, + ) -> Result { + sqlx::query_as("SELECT * FROM nyxd WHERE master_address = ?") + .bind(address) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_account( + &self, + address: &str, + mnemonic: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO account (address, mnemonic) VALUES (?, ?)", + address, + mnemonic + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn load_account(&self, address: &str) -> Result { + sqlx::query_as("SELECT * FROM account WHERE address = ?") + .bind(address) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_contract( + &self, + name: &str, + address: &str, + admin_address: &str, + ) -> Result { + let id = sqlx::query!( + "INSERT INTO contract (name, address, admin_address) VALUES (?, ?, ?)", + name, + address, + admin_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + pub(crate) async fn load_contract(&self, id: i64) -> Result { + sqlx::query_as("SELECT * FROM contract WHERE id = ?") + .bind(id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_authorised_network_monitor( + &self, + network_id: i64, + address: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO authorised_network_monitor (network_id, address) VALUES (?, ?)", + network_id, + address, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn load_authorised_network_monitors( + &self, + network_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM authorised_network_monitor WHERE network_id = ?") + .bind(network_id) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn save_auxiliary_accounts( + &self, + network_id: i64, + rewarder_address: &str, + ecash_holding_account_address: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO localnet_auxiliary_accounts (network_id, rewarder_address, ecash_holding_account_address) VALUES (?, ?, ?)", + network_id, + rewarder_address, + ecash_holding_account_address + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn load_auxiliary_accounts( + &self, + network_id: i64, + ) -> Result { + sqlx::query_as("SELECT * FROM localnet_auxiliary_accounts WHERE network_id = ?") + .bind(network_id) + .fetch_one(&self.connection_pool) + .await + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn save_localnet_contracts( + &self, + metadata_id: i64, + mixnet_id: i64, + vesting_id: i64, + ecash_id: i64, + cw3_id: i64, + cw4_id: i64, + dkg_id: i64, + performance_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO localnet_contracts ( + metadata_id, + mixnet_contract_id, + vesting_contract_id, + ecash_contract_id, + cw3_multisig_contract_id, + cw4_group_contract_id, + dkg_contract_id, + performance_contract_id + + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + "#, + metadata_id, + mixnet_id, + vesting_id, + ecash_id, + cw3_id, + cw4_id, + dkg_id, + performance_id, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn load_localnet_contracts( + &self, + id: i64, + ) -> Result { + sqlx::query_as("SELECT * FROM localnet_contracts WHERE metadata_id = ?") + .bind(id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_nym_api( + &self, + network_id: i64, + endpoint: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO nym_api (network_id, endpoint) VALUES (?, ?)", + network_id, + endpoint + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn load_nym_api(&self, network_id: i64) -> Result { + sqlx::query_as("SELECT * FROM nym_api WHERE network_id = ?") + .bind(network_id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn load_gateway_nym_nodes( + &self, + network_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM nym_node WHERE network_id = ? AND gateway IS FALSE") + .bind(network_id) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn load_mix_nym_nodes( + &self, + network_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM nym_node WHERE network_id = ? AND gateway IS NOT FALSE") + .bind(network_id) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn save_nym_node( + &self, + node_id: i64, + identity_key: &str, + private_identity_key: &str, + network_id: i64, + owner_address: &str, + gateway: bool, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO nym_node (node_id, identity_key, private_identity_key, network_id, owner_address, gateway) VALUES (?, ?, ?, ?, ?, ?)", + node_id, + identity_key, + private_identity_key, + network_id, + owner_address, + gateway, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/mod.rs b/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/mod.rs new file mode 100644 index 00000000000..0a7b4d66c61 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/mod.rs @@ -0,0 +1,370 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::account::Account; +use crate::orchestrator::cosmwasm_contract::CosmwasmContract; +use crate::orchestrator::network::{AuxiliaryAccounts, NymContracts, NyxdDetails}; +use crate::orchestrator::nym_node::LocalnetNymNode; +use crate::orchestrator::storage::orchestrator::manager::StorageManager; +use crate::orchestrator::storage::orchestrator::models::{LocalnetMetadata, StoredMetadata}; +use anyhow::{Context, anyhow}; +use sqlx::ConnectOptions; +use sqlx::sqlite::{SqliteAutoVacuum, SqliteSynchronous}; +use std::fs; +use std::path::{Path, PathBuf}; +use tracing::info; +use zeroize::Zeroizing; + +pub(crate) mod manager; +pub(crate) mod models; + +pub(crate) struct LocalnetOrchestratorStorage { + _storage_path: PathBuf, + manager: StorageManager, +} + +impl LocalnetOrchestratorStorage { + pub async fn init>(database_path: P) -> anyhow::Result { + let database_path = database_path.as_ref(); + info!( + "attempting to initialise storage at {}", + database_path.display() + ); + + if let Some(parent) = database_path.parent() { + fs::create_dir_all(parent)?; + } + + let opts = sqlx::sqlite::SqliteConnectOptions::new() + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .auto_vacuum(SqliteAutoVacuum::Incremental) + .filename(database_path) + .create_if_missing(true) + .disable_statement_logging(); + + let connection_pool = sqlx::SqlitePool::connect_with(opts) + .await + .context("db connection failure")?; + + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .context("db migrations failure")?; + + info!("Database migration finished!"); + + Ok(LocalnetOrchestratorStorage { + _storage_path: database_path.to_path_buf(), + manager: StorageManager { connection_pool }, + }) + } + + pub(crate) async fn stop(self) -> anyhow::Result { + let pool = self.manager.into_connection_pool(); + pool.close().await; + Ok(self._storage_path) + } + + pub(crate) async fn get_last_created(&self) -> anyhow::Result { + Ok(self.manager.get_metadata().await?) + } + + async fn save_account(&self, account: &Account) -> anyhow::Result<()> { + let as_str = Zeroizing::new(account.mnemonic.to_string()); + Ok(self + .manager + .save_account(account.address.as_ref(), as_str.as_str()) + .await?) + } + + async fn load_account(&self, address: &str) -> anyhow::Result { + let raw_account = self.manager.load_account(address).await?; + raw_account.try_into() + } + + pub(crate) async fn save_new_localnet_metadata(&self, name: &str) -> anyhow::Result<()> { + let localnet_id = self + .manager + .save_localnet_metadata(name.to_string()) + .await?; + self.manager.save_latest_network_id(localnet_id).await?; + Ok(()) + } + + pub(crate) async fn get_localnet_metadata( + &self, + db_id: i64, + ) -> anyhow::Result { + Ok(self.manager.load_localnet_metadata(db_id).await?) + } + + pub(crate) async fn get_localnet_metadata_by_name( + &self, + name: &str, + ) -> anyhow::Result { + Ok(self.manager.load_localnet_metadata_by_name(name).await?) + } + + pub(crate) async fn get_nyxd_details(&self, db_id: i64) -> anyhow::Result { + let raw_details = self.manager.load_nyxd_details(db_id).await?; + let raw_account = self + .manager + .load_account(&raw_details.master_address) + .await?; + Ok(NyxdDetails { + rpc_endpoint: raw_details.rpc_endpoint.parse()?, + master_account: raw_account.try_into()?, + }) + } + + #[allow(dead_code)] + pub(crate) async fn get_nyxd_details_by_master_address( + &self, + address: &str, + ) -> anyhow::Result { + let raw_details = self.manager.load_nyxd_by_master_address(address).await?; + let raw_account = self.manager.load_account(address).await?; + Ok(NyxdDetails { + rpc_endpoint: raw_details.rpc_endpoint.parse()?, + master_account: raw_account.try_into()?, + }) + } + + pub(crate) async fn save_nyxd_details( + &self, + nyxd_details: &NyxdDetails, + ) -> anyhow::Result { + // 1. save master mnemonic + self.save_account(&nyxd_details.master_account).await?; + + // 2. save actual nyxd information + let nyxd_id = self + .manager + .save_nyxd_details( + nyxd_details.rpc_endpoint.to_string(), + nyxd_details.master_account.address.to_string(), + ) + .await?; + + // 3. update global metadata + self.manager.save_latest_nyxd_id(nyxd_id).await?; + Ok(nyxd_id) + } + + async fn load_cosmwasm_contract(&self, id: i64) -> anyhow::Result { + let raw = self.manager.load_contract(id).await?; + let admin = self.load_account(&raw.admin_address).await?; + + Ok(CosmwasmContract { + name: raw.name, + address: raw + .address + .parse() + .map_err(|err| anyhow!("malformed address: {err}"))?, + admin, + }) + } + + async fn save_cosmwasm_contract(&self, contract: &CosmwasmContract) -> anyhow::Result { + // 1. save admin details + self.save_account(&contract.admin).await?; + + // 2. persist actual contract information + let contract_id = self + .manager + .save_contract( + &contract.name, + contract.address.as_ref(), + contract.admin.address.as_ref(), + ) + .await?; + + Ok(contract_id) + } + + async fn save_authorised_network_monitor( + &self, + network_id: i64, + account: &Account, + ) -> anyhow::Result<()> { + self.save_account(account).await?; + self.manager + .save_authorised_network_monitor(network_id, account.address.as_ref()) + .await?; + Ok(()) + } + + pub(crate) async fn save_auxiliary_accounts( + &self, + localnet_human_name: &str, + aux: &AuxiliaryAccounts, + ) -> anyhow::Result<()> { + // 1. retrieve associated metadata id based on the network name + let metadata = self + .manager + .load_localnet_metadata_by_name(localnet_human_name) + .await?; + + // 2. save accounts + self.save_account(&aux.mixnet_rewarder).await?; + self.save_account(&aux.ecash_holding_account).await?; + for network_monitor in &aux.network_monitor { + // 3. and network monitors + self.save_authorised_network_monitor(metadata.id, network_monitor) + .await?; + } + + // 4. create the container row + self.manager + .save_auxiliary_accounts( + metadata.id, + aux.mixnet_rewarder.address.as_ref(), + aux.ecash_holding_account.address.as_ref(), + ) + .await?; + Ok(()) + } + + pub(crate) async fn load_auxiliary_accounts( + &self, + localnet_id: i64, + ) -> anyhow::Result { + let raw = self.manager.load_auxiliary_accounts(localnet_id).await?; + let mixnet_rewarder = self.load_account(&raw.rewarder_address).await?; + let ecash_holding_account = self + .load_account(&raw.ecash_holding_account_address) + .await?; + let raw_monitors = self + .manager + .load_authorised_network_monitors(localnet_id) + .await?; + let mut network_monitor = Vec::with_capacity(raw_monitors.len()); + for raw_monitor in raw_monitors { + network_monitor.push(self.load_account(&raw_monitor.address).await?) + } + Ok(AuxiliaryAccounts { + mixnet_rewarder, + network_monitor, + ecash_holding_account, + }) + } + + pub(crate) async fn save_localnet_contracts( + &self, + localnet_human_name: &str, + contracts: &NymContracts, + ) -> anyhow::Result<()> { + // 1. retrieve associated metadata id based on the network name + let metadata = self + .manager + .load_localnet_metadata_by_name(localnet_human_name) + .await?; + + // 2. save contracts data + let mixnet_id = self.save_cosmwasm_contract(&contracts.mixnet).await?; + let vesting_id = self.save_cosmwasm_contract(&contracts.vesting).await?; + let ecash_id = self.save_cosmwasm_contract(&contracts.ecash).await?; + let cw3_multisig_id = self.save_cosmwasm_contract(&contracts.cw3_multisig).await?; + let cw4_group_id = self.save_cosmwasm_contract(&contracts.cw4_group).await?; + let dkg_id = self.save_cosmwasm_contract(&contracts.dkg).await?; + let performance_id = self.save_cosmwasm_contract(&contracts.performance).await?; + + // 3. clump it all together + self.manager + .save_localnet_contracts( + metadata.id, + mixnet_id, + vesting_id, + ecash_id, + cw3_multisig_id, + cw4_group_id, + dkg_id, + performance_id, + ) + .await?; + + Ok(()) + } + + pub(crate) async fn load_localnet_contracts( + &self, + localnet_id: i64, + ) -> anyhow::Result { + let raw = self.manager.load_localnet_contracts(localnet_id).await?; + + let mixnet = self.load_cosmwasm_contract(raw.mixnet_contract_id).await?; + let vesting = self.load_cosmwasm_contract(raw.vesting_contract_id).await?; + let ecash = self.load_cosmwasm_contract(raw.ecash_contract_id).await?; + let cw3_multisig = self + .load_cosmwasm_contract(raw.cw3_multisig_contract_id) + .await?; + let cw4_group = self + .load_cosmwasm_contract(raw.cw4_group_contract_id) + .await?; + let dkg = self.load_cosmwasm_contract(raw.dkg_contract_id).await?; + let performance = self + .load_cosmwasm_contract(raw.performance_contract_id) + .await?; + + Ok(NymContracts { + mixnet, + vesting, + ecash, + cw3_multisig, + cw4_group, + dkg, + performance, + }) + } + + pub(crate) async fn save_nym_api_details( + &self, + localnet_human_name: &str, + nym_api_endpoint: &str, + ) -> anyhow::Result<()> { + // 1. retrieve associated metadata id based on the network name + let metadata = self + .manager + .load_localnet_metadata_by_name(localnet_human_name) + .await?; + + self.manager + .save_nym_api(metadata.id, nym_api_endpoint) + .await?; + Ok(()) + } + + pub(crate) async fn get_nym_api_details(&self, localnet_id: i64) -> anyhow::Result { + let raw = self.manager.load_nym_api(localnet_id).await?; + Ok(raw.endpoint.parse()?) + } + + pub(crate) async fn save_nym_node_details( + &self, + localnet_human_name: &str, + node: &LocalnetNymNode, + ) -> anyhow::Result<()> { + // 1. retrieve associated metadata id based on the network name + let metadata = self + .manager + .load_localnet_metadata_by_name(localnet_human_name) + .await?; + + // 2. save account + self.save_account(&node.owner).await?; + + // 3. save node details + self.manager + .save_nym_node( + node.id as i64, + &node.identity.public_key().to_base58_string(), + &node.identity.private_key().to_base58_string(), + metadata.id, + node.owner.address.as_ref(), + node.gateway, + ) + .await?; + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/models.rs b/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/models.rs new file mode 100644 index 00000000000..f40ae002af5 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/models.rs @@ -0,0 +1,104 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::orchestrator::account::Account; +use anyhow::Context; +use sqlx::FromRow; +use time::OffsetDateTime; + +#[allow(dead_code)] +#[derive(FromRow)] +pub(crate) struct RawLocalnetContracts { + pub(crate) metadata_id: i64, + pub(crate) mixnet_contract_id: i64, + pub(crate) vesting_contract_id: i64, + pub(crate) ecash_contract_id: i64, + pub(crate) cw3_multisig_contract_id: i64, + pub(crate) cw4_group_contract_id: i64, + pub(crate) dkg_contract_id: i64, + pub(crate) performance_contract_id: i64, +} + +#[allow(dead_code)] +#[derive(FromRow)] +pub(crate) struct RawAuthorisedNetworkMonitor { + pub(crate) network_id: i64, + pub(crate) address: String, +} + +#[allow(dead_code)] +#[derive(FromRow)] +pub(crate) struct RawAuxiliaryAccounts { + pub(crate) network_id: i64, + pub(crate) rewarder_address: String, + pub(crate) ecash_holding_account_address: String, +} + +#[derive(FromRow)] +pub(crate) struct RawAccount { + pub(crate) address: String, + pub(crate) mnemonic: String, +} + +impl TryFrom for Account { + type Error = anyhow::Error; + + fn try_from(value: RawAccount) -> Result { + Ok(Account { + address: value + .address + .parse() + .map_err(|err| anyhow::anyhow!("malformed account address: {err}"))?, + mnemonic: value.mnemonic.parse().context("malformed mnemonic")?, + }) + } +} + +#[derive(FromRow)] +pub(crate) struct RawContract { + #[allow(unused)] + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) address: String, + pub(crate) admin_address: String, +} + +#[derive(FromRow)] +pub(crate) struct RawNyxd { + #[allow(unused)] + pub(crate) id: i64, + pub(crate) rpc_endpoint: String, + pub(crate) master_address: String, +} + +#[derive(FromRow)] +#[allow(unused)] +pub(crate) struct LocalnetMetadata { + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) created_at: OffsetDateTime, +} + +#[derive(FromRow)] +pub(crate) struct StoredMetadata { + pub(crate) latest_network_id: Option, + pub(crate) latest_nyxd_id: Option, +} + +#[derive(FromRow)] +pub(crate) struct RawNymApi { + #[allow(unused)] + pub(crate) network_id: i64, + pub(crate) endpoint: String, +} + +#[allow(unused)] +#[derive(FromRow)] +pub(crate) struct RawNymNode { + #[allow(unused)] + pub(crate) network_id: i64, + pub(crate) node_id: i64, + pub(crate) identity_key: String, + pub(crate) private_identity_key: String, + pub(crate) owner_address: String, +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/gateway_probe.rs b/tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/gateway_probe.rs new file mode 100644 index 00000000000..da4786075c6 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/gateway_probe.rs @@ -0,0 +1,98 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::CONTAINER_NETWORK_NAME; +use crate::helpers::{exec_inherit_output, monorepo_root_path}; +use crate::orchestrator::LocalnetOrchestrator; +use crate::orchestrator::container_helpers::{ + attach_run_container_args, container_binary, default_nym_binaries_image_tag, +}; +use anyhow::Context; +use bip39::Mnemonic; +use std::fs; +use std::path::{Path, PathBuf}; +use tracing::info; + +impl LocalnetOrchestrator { + fn make_global_env_file(&self) -> anyhow::Result<()> { + let path = self.storage.global_env_file(); + if path.exists() { + return Ok(()); + } + let content = self.localnet_details.env_file_content()?; + fs::write(path, &content)?; + Ok(()) + } + + async fn start_gateway_probe( + &self, + monorepo_root: &Path, + mnemonic: &Mnemonic, + additional_args: Option, + ) -> anyhow::Result<()> { + // run this instance with piped output so we could see live changes + let bin = container_binary(); + + let monorepo_path = monorepo_root.canonicalize()?; + let image_tag = default_nym_binaries_image_tag(&monorepo_path)?; + + // first we construct the base, common, args + let env_file_volume = format!( + "{}:/root", + self.storage + .global_env_file() + .parent() + .context("invalid storage dir")? + .canonicalize()? + .to_string_lossy() + ); + let mnemonic_string = mnemonic.to_string(); + let mut probe_args = vec![ + "-v".to_string(), + env_file_volume, + "--network".to_string(), + CONTAINER_NETWORK_NAME.to_string(), + "--rm".to_string(), + image_tag, + "nym-gateway-probe".to_string(), + "-c".to_string(), + "/root/localnet.env".to_string(), + "run-local".to_string(), + "--mnemonic".to_string(), + mnemonic_string, + ]; + if let Some(additional_args) = additional_args { + probe_args.push(additional_args) + } + + // then we attach platform specific ones + let mut probe_args = attach_run_container_args(probe_args); + + // finally we insert the "run" at the beginning + probe_args.insert(0, "run".into()); + + info!("๐Ÿš€๐Ÿš€๐Ÿš€ STARTING THE GATEWAY PROBE"); + exec_inherit_output(bin, probe_args).await?; + Ok(()) + } + + pub(crate) async fn run_gateway_probe( + &self, + monorepo_root: Option, + additional_args: Option, + ) -> anyhow::Result<()> { + let monorepo_root = monorepo_root_path(monorepo_root)?; + + // 1. create env file + self.make_global_env_file()?; + + // 2. retrieve admin account (no point in making a new one - this one has plenty of tokens) + let account = &self.localnet_details.nyxd_details()?.master_account; + + // 3. run the actual probe + self.start_gateway_probe(&monorepo_root, &account.mnemonic, additional_args) + .await?; + + Ok(()) + } +} diff --git a/tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/mod.rs b/tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/mod.rs new file mode 100644 index 00000000000..29ff688f7d5 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod gateway_probe; diff --git a/tools/internal/localnet-orchestrator/src/serde_helpers/linux.rs b/tools/internal/localnet-orchestrator/src/serde_helpers/linux.rs new file mode 100644 index 00000000000..c6356aba260 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/serde_helpers/linux.rs @@ -0,0 +1,543 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use anyhow::bail; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub mod container_network_inspect { + use serde::{Deserialize, Serialize}; + use std::net::IpAddr; + + #[derive(Serialize, Deserialize, Debug)] + pub struct NetworkInspect(pub(crate) Vec); + + impl NetworkInspect { + // not sure if it's the best test + // but given existing schema, couldn't think of anything better + pub fn is_running(&self) -> bool { + let Some(inner) = &self.0.first() else { + return false; + }; + // check we actually have defined subnet with a gateway + !inner.ipam.config.is_empty() + } + } + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "PascalCase")] + pub struct NetworkInspectInner { + pub name: String, + pub id: String, + #[serde(alias = "IPAM")] + pub ipam: Ipam, + } + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "PascalCase")] + pub struct Ipam { + pub config: Vec, + } + + #[derive(Serialize, Deserialize, Debug)] + #[serde(rename_all = "PascalCase")] + pub struct IpamConfig { + pub subnet: String, // represented in cidr location + pub gateway: IpAddr, + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainerInspect(pub(crate) Vec); + +impl TryFrom for super::ContainerInspect { + type Error = anyhow::Error; + + fn try_from(mut value: ContainerInspect) -> Result { + if value.0.is_empty() { + return Ok(super::ContainerInspect::new_empty_container()); + } + + if value.0.len() != 1 { + bail!("more than a single container information") + } + + // SAFETY: we just checked we have exactly one element + #[allow(clippy::unwrap_used)] + let info = value.0.pop().unwrap(); + Ok(super::ContainerInspect { + info: Some(info.try_into()?), + }) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainersList(pub(crate) Vec); + +impl TryFrom for super::ContainersList { + type Error = anyhow::Error; + + fn try_from(value: ContainersList) -> Result { + Ok(super::ContainersList { + containers: value + .0 + .into_iter() + .filter(|c| c.names.contains("localnet")) + .map(TryInto::try_into) + .collect::>>()?, + }) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct ContainerListContainer { + pub command: String, + + #[serde(alias = "ID")] + pub id: String, + + pub image: String, + pub names: String, + pub status: String, +} + +impl TryFrom for super::CommonContainerInformation { + type Error = anyhow::Error; + + fn try_from(value: ContainerListContainer) -> Result { + Ok(super::CommonContainerInformation { + name: value.names, + ip_address: None, + status: value.status.to_lowercase(), + image: value.image, + }) + } +} + +// note: this contains only a small subset of possible fields +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct ContainerInformation { + pub id: String, + pub state: State, + pub image: String, + pub name: String, + pub network_settings: NetworkSettings, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct State { + pub status: String, + pub running: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct NetworkSettings { + pub mac_address: String, + pub networks: HashMap, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct ContainerNetworkInformation { + #[serde(alias = "IPAddress")] + pub ip_address: String, + + #[serde(alias = "IPPrefixLen")] + pub ip_prefix_len: u8, + + #[serde(alias = "GlobalIPv6Address")] + pub global_ipv6_address: String, + + #[serde(alias = "GlobalIPv6PrefixLen")] + pub global_ipv6_prefix_len: u8, + + pub mac_address: String, +} + +impl TryFrom for super::CommonContainerInformation { + type Error = anyhow::Error; + + fn try_from(value: ContainerInformation) -> Result { + let status = value.state.status.to_lowercase(); + + let ip_address = if status == "running" || status == "up" { + if value.network_settings.networks.is_empty() { + bail!("no attached networks") + } + + // find first network with non-empty ip address + let Some(network) = value + .network_settings + .networks + .iter() + .find(|n| !n.1.ip_address.is_empty()) + .map(|n| n.1) + else { + bail!("no valid network") + }; + Some(network.ip_address.parse().context("invalid ip address")?) + } else { + None + }; + + Ok(super::CommonContainerInformation { + name: value.name, + image: value.image, + ip_address, + status, + }) + } +} + +#[cfg(test)] +mod tests { + use super::container_network_inspect::NetworkInspect; + use crate::serde_helpers::linux::ContainerInspect; + use std::net::{IpAddr, Ipv4Addr}; + + #[test] + fn sample_network_inspect_response_parsing() { + let raw = r#" +[ + { + "Name": "test", + "Id": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + "IPAM": { + "Config": [ + { + "Subnet": "10.4.2.0/24", + "Gateway": "10.4.2.1" + } + ] + }, + "Labels": {}, + "Containers": {} + } +] + "#; + + let parsed: NetworkInspect = serde_json::from_str(raw).unwrap(); + let inner = parsed.0.first().unwrap(); + assert_eq!(inner.name, "test"); + assert_eq!(inner.ipam.config.first().unwrap().subnet, "10.4.2.0/24") + } + + #[test] + fn sample_container_inspect_response_parsing() { + let raw = r#" + [ + { + "Id": "22a611624245e35fee8a15126f23f2b226d8ea3800a213823605303f4988183b", + "Created": "2025-12-04T17:18:57.561222924Z", + "Path": "sleep", + "Args": [ + "1000" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "Pid": 101647, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-12-04T17:18:57.806684779Z", + "FinishedAt": "" + }, + "Image": "docker.io/library/localnet-nyxd:v0.60.1", + "ResolvConfPath": "/var/lib/nerdctl/1935db59/containers/default/22a611624245e35fee8a15126f23f2b226d8ea3800a213823605303f4988183b/resolv.conf", + "HostnamePath": "/var/lib/nerdctl/1935db59/containers/default/22a611624245e35fee8a15126f23f2b226d8ea3800a213823605303f4988183b/hostname", + "HostsPath": "/var/lib/nerdctl/1935db59/etchosts/default/22a611624245e35fee8a15126f23f2b226d8ea3800a213823605303f4988183b/hosts", + "LogPath": "/var/lib/nerdctl/1935db59/containers/default/22a611624245e35fee8a15126f23f2b226d8ea3800a213823605303f4988183b/22a611624245e35fee8a15126f23f2b226d8ea3800a213823605303f4988183b-json.log", + "Name": "lab-nature-localnet-nyxdab", + "RestartCount": 0, + "Driver": "overlayfs", + "Platform": "linux", + "AppArmorProfile": "nerdctl-default", + "HostConfig": { + "ContainerIDFile": "", + "LogConfig": { + "driver": "json-file", + "address": "/run/containerd/containerd.sock" + }, + "PortBindings": {}, + "CgroupnsMode": "private", + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": [], + "GroupAdd": [ + "1", + "2", + "3", + "4", + "6", + "10", + "11", + "20", + "26", + "27" + ], + "IpcMode": "private", + "OomScoreAdj": 0, + "PidMode": "", + "ReadonlyRootfs": false, + "UTSMode": "", + "ShmSize": 0, + "Sysctls": null, + "Runtime": "io.containerd.runc.v2", + "CpusetMems": "", + "CpusetCpus": "", + "CpuQuota": 0, + "CpuShares": 0, + "CpuPeriod": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "Memory": 0, + "MemorySwap": 0, + "OomKillDisable": false, + "Devices": null, + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [] + }, + "Mounts": [ + { + "Type": "bind", + "Source": "/root/.nym/localnet-orchestrator/lab-nature/nyxd", + "Destination": "/root/.nyxd", + "Mode": "", + "RW": true, + "Propagation": "" + } + ], + "Config": { + "Hostname": "22a611624245", + "AttachStdin": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOSTNAME=22a611624245" + ], + "Image": "docker.io/library/localnet-nyxd:v0.60.1", + "Labels": { + "io.containerd.image.config.stop-signal": "SIGTERM", + "nerdctl/auto-remove": "false", + "nerdctl/dns": "{\"DNSServers\":null,\"DNSResolvConfOptions\":null,\"DNSSearchDomains\":null}", + "nerdctl/extraHosts": "[]", + "nerdctl/host-config": "{\"BlkioWeight\":0,\"CidFile\":\"\",\"Devices\":null}", + "nerdctl/hostname": "22a611624245", + "nerdctl/ipc": "{\"mode\":\"private\"}", + "nerdctl/log-config": "{\"driver\":\"json-file\",\"address\":\"/run/containerd/containerd.sock\"}", + "nerdctl/log-uri": "binary:///usr/local/bin/nerdctl?_NERDCTL_INTERNAL_LOGGING=%2Fvar%2Flib%2Fnerdctl%2F1935db59", + "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/root/.nym/localnet-orchestrator/lab-nature/nyxd\",\"Destination\":\"/root/.nyxd\",\"Mode\":\"\",\"RW\":true,\"Propagation\":\"\"}]", + "nerdctl/name": "lab-nature-localnet-nyxdab", + "nerdctl/namespace": "default", + "nerdctl/networks": "[\"nym-localnet\"]", + "nerdctl/platform": "linux/amd64", + "nerdctl/state-dir": "/var/lib/nerdctl/1935db59/containers/default/22a611624245e35fee8a15126f23f2b226d8ea3800a213823605303f4988183b" + } + }, + "NetworkSettings": { + "Ports": {}, + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "10.4.1.2", + "IPPrefixLen": 24, + "MacAddress": "3a:74:44:fc:cf:d2", + "Networks": { + "unknown-eth0": { + "IPAddress": "10.4.1.2", + "IPPrefixLen": 24, + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "3a:74:44:fc:cf:d2" + } + } + } + } +]"#; + + let parsed: ContainerInspect = serde_json::from_str(raw).unwrap(); + let inner = parsed.0.first().unwrap(); + assert_eq!(inner.name, "lab-nature-localnet-nyxdab"); + assert_eq!( + inner.network_settings.networks["unknown-eth0"].ip_address, + "10.4.1.2" + ); + + let another_raw = r#" +[ + { + "Id": "1de89e6c7815894e74155922cb4c4fd0524b0809000bb84e0ef5e0d98a8d7ed1", + "Created": "2025-12-05T21:53:08.721912948Z", + "Path": "nyxd", + "Args": [ + "start" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "Pid": 80138, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-12-05T21:53:09.212670473Z", + "FinishedAt": "" + }, + "Image": "docker.io/library/localnet-nyxd:v0.60.1", + "ResolvConfPath": "/var/lib/nerdctl/1935db59/containers/default/1de89e6c7815894e74155922cb4c4fd0524b0809000bb84e0ef5e0d98a8d7ed1/resolv.conf", + "HostnamePath": "/var/lib/nerdctl/1935db59/containers/default/1de89e6c7815894e74155922cb4c4fd0524b0809000bb84e0ef5e0d98a8d7ed1/hostname", + "HostsPath": "/var/lib/nerdctl/1935db59/etchosts/default/1de89e6c7815894e74155922cb4c4fd0524b0809000bb84e0ef5e0d98a8d7ed1/hosts", + "LogPath": "/var/lib/nerdctl/1935db59/containers/default/1de89e6c7815894e74155922cb4c4fd0524b0809000bb84e0ef5e0d98a8d7ed1/1de89e6c7815894e74155922cb4c4fd0524b0809000bb84e0ef5e0d98a8d7ed1-json.log", + "Name": "minimum-fatal-localnet-nyxd", + "RestartCount": 0, + "Driver": "overlayfs", + "Platform": "linux", + "AppArmorProfile": "nerdctl-default", + "HostConfig": { + "ContainerIDFile": "", + "LogConfig": { + "driver": "json-file", + "address": "/run/containerd/containerd.sock" + }, + "PortBindings": { + "26657/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "26657" + } + ] + }, + "CgroupnsMode": "private", + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": [], + "GroupAdd": [ + "1", + "2", + "3", + "4", + "6", + "10", + "11", + "20", + "26", + "27" + ], + "IpcMode": "private", + "OomScoreAdj": 0, + "PidMode": "", + "ReadonlyRootfs": false, + "UTSMode": "", + "ShmSize": 0, + "Sysctls": null, + "Runtime": "io.containerd.kata.v2", + "CpusetMems": "", + "CpusetCpus": "", + "CpuQuota": 0, + "CpuShares": 0, + "CpuPeriod": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "Memory": 0, + "MemorySwap": 0, + "OomKillDisable": false, + "Devices": null, + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [] + }, + "Mounts": [ + { + "Type": "bind", + "Source": "/root/.nym/localnet-orchestrator/minimum-fatal/nyxd", + "Destination": "/root/.nyxd", + "Mode": "", + "RW": true, + "Propagation": "" + } + ], + "Config": { + "Hostname": "1de89e6c7815", + "AttachStdin": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOSTNAME=1de89e6c7815" + ], + "Image": "docker.io/library/localnet-nyxd:v0.60.1", + "Labels": { + "io.containerd.image.config.stop-signal": "SIGTERM", + "nerdctl/auto-remove": "false", + "nerdctl/dns": "{\"DNSServers\":null,\"DNSResolvConfOptions\":null,\"DNSSearchDomains\":null}", + "nerdctl/extraHosts": "[]", + "nerdctl/host-config": "{\"BlkioWeight\":0,\"CidFile\":\"\",\"Devices\":null}", + "nerdctl/hostname": "1de89e6c7815", + "nerdctl/ipc": "{\"mode\":\"private\"}", + "nerdctl/log-config": "{\"driver\":\"json-file\",\"address\":\"/run/containerd/containerd.sock\"}", + "nerdctl/log-uri": "binary:///usr/local/bin/nerdctl?_NERDCTL_INTERNAL_LOGGING=%2Fvar%2Flib%2Fnerdctl%2F1935db59", + "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/root/.nym/localnet-orchestrator/minimum-fatal/nyxd\",\"Destination\":\"/root/.nyxd\",\"Mode\":\"\",\"RW\":true,\"Propagation\":\"\"}]", + "nerdctl/name": "minimum-fatal-localnet-nyxd", + "nerdctl/namespace": "default", + "nerdctl/networks": "[\"nym-localnet\"]", + "nerdctl/platform": "linux/amd64", + "nerdctl/state-dir": "/var/lib/nerdctl/1935db59/containers/default/1de89e6c7815894e74155922cb4c4fd0524b0809000bb84e0ef5e0d98a8d7ed1" + } + }, + "NetworkSettings": { + "Ports": { + "26657/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "26657" + } + ] + }, + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "10.4.1.19", + "IPPrefixLen": 24, + "MacAddress": "0a:5c:e1:01:0a:ee", + "Networks": { + "unknown-eth0": { + "IPAddress": "10.4.1.19", + "IPPrefixLen": 24, + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "0a:5c:e1:01:0a:ee" + }, + "unknown-tap0_kata": { + "IPAddress": "", + "IPPrefixLen": 0, + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "4a:84:9d:af:d0:a6" + } + } + } + } +] + +"#; + + let parsed: ContainerInspect = serde_json::from_str(another_raw).unwrap(); + let inner = parsed.0.first().unwrap(); + assert_eq!(inner.name, "minimum-fatal-localnet-nyxd"); + assert_eq!( + inner.network_settings.networks["unknown-eth0"].ip_address, + "10.4.1.19" + ); + } +} diff --git a/tools/internal/localnet-orchestrator/src/serde_helpers/macos.rs b/tools/internal/localnet-orchestrator/src/serde_helpers/macos.rs new file mode 100644 index 00000000000..5f6dd9e87e6 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/serde_helpers/macos.rs @@ -0,0 +1,254 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::CONTAINER_NETWORK_NAME; +use anyhow::{Context, bail}; +use serde::{Deserialize, Serialize}; +use std::net::IpAddr; + +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainersList(Vec); + +impl TryFrom for super::ContainersList { + type Error = anyhow::Error; + + fn try_from(value: ContainersList) -> Result { + Ok(super::ContainersList { + containers: value + .0 + .into_iter() + .filter(|c| c.configuration.id.contains("localnet")) + .map(TryInto::try_into) + .collect::>>()?, + }) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainerInspect(pub(crate) Vec); + +impl TryFrom for super::ContainerInspect { + type Error = anyhow::Error; + + fn try_from(mut value: ContainerInspect) -> Result { + if value.0.is_empty() { + return Ok(super::ContainerInspect::new_empty_container()); + } + + if value.0.len() != 1 { + bail!("more than a single container information") + } + + // SAFETY: we just checked we have exactly one element + #[allow(clippy::unwrap_used)] + let info = value.0.pop().unwrap(); + Ok(super::ContainerInspect { + info: Some(info.try_into()?), + }) + } +} + +// we only care about a tiny subset of fields +pub mod container_network_inspect { + use serde::{Deserialize, Serialize}; + use std::net::IpAddr; + + #[derive(Serialize, Deserialize, Debug)] + pub struct NetworkInspect(Vec); + + impl NetworkInspect { + pub fn is_running(&self) -> bool { + let Some(inner) = &self.0.first() else { + return false; + }; + inner.state == "running" + } + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct NetworkInspectInner { + pub config: Config, + pub status: Status, + pub state: String, + pub id: String, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct Config { + pub id: String, + pub mode: String, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct Status { + pub address: String, // represented in cidr location + pub gateway: IpAddr, + } +} + +// note: this contains only a small subset of possible fields +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainerInformation { + pub status: String, + pub configuration: ContainerConfiguration, + pub networks: Vec, +} + +impl ContainerInformation { + pub fn container_ip(&self) -> anyhow::Result { + for network in &self.networks { + if network.network == CONTAINER_NETWORK_NAME { + // perform the split in case the network is provided in cidr notation + let raw_address = network + .address + .split('/') + .next() + .unwrap_or(&network.address); + + return raw_address.parse().context("malformed network ip address"); + } + } + + bail!( + "no container ip address found. full network information: {:#?}", + self.networks + ) + } +} +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainerConfiguration { + pub id: String, + pub image: ContainerImage, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainerImage { + // e.g. "docker.io/library/localnet-nym-binaries:1.22.0" + pub reference: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ContainerNetwork { + pub hostname: String, + pub network: String, + pub gateway: IpAddr, + pub address: String, // represented in cidr location +} + +impl TryFrom for super::CommonContainerInformation { + type Error = anyhow::Error; + + #[track_caller] + fn try_from(value: ContainerInformation) -> Result { + let status = value.status.to_lowercase(); + let ip_address = if status == "running" || status == "up" { + Some(value.container_ip().context(format!( + "invalid container {} ({})", + value.configuration.id, value.configuration.image.reference + ))?) + } else { + None + }; + + Ok(super::CommonContainerInformation { + ip_address, + name: value.configuration.id, + image: value.configuration.image.reference, + status, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sample_container_inspect_response_parsing() { + let raw = r#" +[ + { + "networks": [ + { + "network": "nym-localnet", + "gateway": "192.168.64.1", + "hostname": "test2", + "address": "192.168.64.65/24" + } + ], + "configuration": { + "publishedPorts": [], + "publishedSockets": [], + "dns": { + "searchDomains": [], + "options": [], + "nameservers": [] + }, + "image": { + "descriptor": { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:448b70986d8b75d3d2d465c856e6cd861c6df92263cab8a8b8350d7eea717529", + "size": 856, + "annotations": { + "org.opencontainers.image.ref.name": "1.22.0", + "io.containerd.image.name": "docker.io/library/localnet-nym-binaries:1.22.0" + } + }, + "reference": "docker.io/library/localnet-nym-binaries:1.22.0" + }, + "virtualization": false, + "mounts": [], + "rosetta": true, + "labels": {}, + "initProcess": { + "user": { + "id": { + "uid": 0, + "gid": 0 + } + }, + "arguments": [], + "workingDirectory": "/nym", + "environment": [ + "PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "RUSTUP_HOME=/usr/local/rustup", + "CARGO_HOME=/usr/local/cargo", + "RUST_VERSION=1.91.1" + ], + "executable": "sh", + "supplementalGroups": [], + "rlimits": [], + "terminal": true + }, + "sysctls": {}, + "runtimeHandler": "container-runtime-linux", + "platform": { + "architecture": "amd64", + "os": "linux" + }, + "networks": [ + { + "network": "nym-localnet", + "options": { + "hostname": "test2" + } + } + ], + "ssh": false, + "id": "test2", + "resources": { + "cpus": 4, + "memoryInBytes": 1073741824 + } + }, + "status": "running" + } +] + "#; + + let parsed: ContainerInspect = serde_json::from_str(raw).unwrap(); + let inner = parsed.0.first().unwrap(); + assert_eq!(inner.configuration.id, "test2"); + assert_eq!(inner.networks[0].address, "192.168.64.65/24"); + } +} diff --git a/tools/internal/localnet-orchestrator/src/serde_helpers/mod.rs b/tools/internal/localnet-orchestrator/src/serde_helpers/mod.rs new file mode 100644 index 00000000000..80b4a5bf602 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/serde_helpers/mod.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, bail}; +use std::net::IpAddr; + +#[cfg(target_os = "macos")] +pub(crate) mod macos; + +#[cfg(target_os = "linux")] +pub(crate) mod linux; + +#[derive(Debug)] +pub struct ContainersList { + pub containers: Vec, +} + +impl ContainersList { + pub fn new_empty() -> Self { + ContainersList { + containers: Vec::new(), + } + } +} + +#[derive(Debug)] +pub struct ContainerInspect { + pub info: Option, +} + +impl ContainerInspect { + pub fn new_empty_container() -> ContainerInspect { + ContainerInspect { info: None } + } + + pub fn is_running(&self) -> bool { + let Some(info) = &self.info else { + return false; + }; + info.status == "running" || info.status == "up" + } + + pub fn container_ip(&self) -> anyhow::Result { + let Some(info) = &self.info else { + bail!("container is not running") + }; + + info.ip_address.context("ip address not available!") + } +} + +#[derive(Debug)] +pub struct CommonContainerInformation { + pub name: String, + pub ip_address: Option, + pub status: String, + pub image: String, +} diff --git a/tools/internal/testnet-manager/Makefile b/tools/internal/testnet-manager/Makefile deleted file mode 100644 index fb4f6f863d7..00000000000 --- a/tools/internal/testnet-manager/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -build-bypass-contract: - $(MAKE) -C dkg-bypass-contract build - - -COSMWASM_OPTIMIZER_IMAGE ?= cosmwasm/optimizer:0.17.0 -COSMWASM_OPTIMIZER_PLATFORM ?= linux/amd64 - -build-bypass-contract-docker: - docker volume rm nym_contracts_cache 2>/dev/null || true - docker volume rm registry_cache 2>/dev/null || true - docker run --rm --platform $(COSMWASM_OPTIMIZER_PLATFORM) \ - -v $(CURDIR)/../../..:/code \ - --mount type=volume,source=nym_contracts_cache,target=/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - -e CARGO_BUILD_INCREMENTAL=false \ - -e RUSTFLAGS="-C target-cpu=generic -C debuginfo=0" \ - -e SOURCE_DATE_EPOCH=1 \ - $(COSMWASM_OPTIMIZER_IMAGE) "tools/internal/testnet-manager/dkg-bypass-contract"; \ diff --git a/tools/internal/testnet-manager/README.md b/tools/internal/testnet-manager/README.md deleted file mode 100644 index 32a5b1dae13..00000000000 --- a/tools/internal/testnet-manager/README.md +++ /dev/null @@ -1,150 +0,0 @@ -# Testnet manager - -This is extremely experimental tool. Only to be used internally. Expect a lot of breaking changes. - -Currently (as of 11.07.24), it exposes the following commands: - -## `build-info` - -Show build information of this binary. Does it need any more than that? - -## `initialise-new-network` - -pre-requisites: - -1. you must have built all nym-contracts and put them in the same directory (just run `make contracts` from the root - directory) - -Initialises new testnet network: - -1. attempts to retrieve paths to all .wasm files of the nym-contracts based on provided arguments -2. uploads all the contracts to the specified nyxd -3. creates mnemonics for all contract admins -4. transfers some tokens to each created account -5. instantiates all the contracts -6. performs post-instantiation migration (like sets vesting contract address inside the mixnet contract) -7. queries each contract and retrieves its build information to display any warnings if they were built using some - ancient commits -8. persists all the network info (addresses, mnemonics, etc.) in the database for future use - -**note: if you intend to `bond-local-mixnet` afterward, you want to set `--custom-epoch-duration-secs` to a rather low -value (like 60s)** - -## `load-network-details` - -Attempt to load testnet network details using either the provided name, or if nothing was specified, the latest one -created. - -It outputs contents of an `.env` file you'd use with that network. - -## `bypass-dkg` - -pre-requisites: - -1. you must have built the `dkg-bypass contract` (just run `make build-bypass-contract` from **this** directory) - -Attempts to bypass the DKG by overwriting the contract state with pre-generated keys: - -1. generates data for each specified ecash signer: - - ecash keys via a ttp - - ed25519 identity keys - - cosmos mnemonic -2. validates the existing DKG contract to make sure the DKG hasn't actually already been run and checks the group - contract to make sure its empty -3. persists the signer data generated at the beginning -4. uploads the bypass contract -5. overwrites the contract state (endpoints, keys, etc.) using the uploaded contract -6. restores the original DKG contract code -7. adds the ecash signers to the CW4 group -8. transfers some tokens to each ecash signer so they could actually execute txs - -## `initialise-post-dkg-network` - -pre-requisites: - -1. you must have built all nym-contracts and put them in the same directory (just run `make contracts` from the root - directory) -2. you must have built the `dkg-bypass contract` (just run `make build-bypass-contract` from **this** directory) - -Initialises new network and bypasses the DKG. It's just the equivalent of running `initialise-new-network` -and `bypass-dkg` separately: - -1. runs equivalent of `initialise-new-network` -2. runs equivalent of `bypass-dkg` - -## `create-local-ecash-apis` - -pre-requisites: - -1. you must have built all nym-contracts and put them in the same directory (just run `make contracts` from the root - directory) -2. you must have built the `dkg-bypass contract` (just run `make build-bypass-contract` from **this** directory) -3. you must have built `nym-api` binary - -Attempt to create brand new network, in post DKG-state, using locally running nym-apis. - -1. runs equivalent of `initialise-post-dkg-network`, with one difference: rather than requiring you to provide api - endpoints to all signers, it defaults to `http:://127.0.0.1:X`, where `X = 10000 + i`, based on the number of apis - specified in the args -2. runs `nym-api init` for all required api -3. copies over keys generated during `bypass-dkg` into the correct path for each API, -4. generates an `.env` file to use in all subsequent `run` commands -5. generates and outputs (either as raw string or `json` if used with `--output=json`) run commands for each nym-api - using full canonical and absolute paths (so you could paste them regardless of local directory) - -## `bond-local-mixnet` - -pre-requisites: - -1. you must have a running network **including nym-api** (just run `create-local-ecash-apis` and start the binaries) -2. the mixnet epoch must be waiting for transition (thus `--custom-epoch-duration-secs` recommendation) -3. you must have built `nym-node` binary - -Attempt to bond minimal local mixnet (3 mixnodes + 1 gateways) and output the run commands. - -1. runs `nym-node init` 4 times, including once in `mode==entry` (with credentials) -2. generates mnemonics for each node -3. generates bonding signatures for each node -4. transfers some tokens to each bond owner -5. performs bonding of mixnode/gateway -6. assigns all nodes to the active set by: - - starting epoch transition - - reconciling epoch events - - advancing current epoch and assigning the nodes to the set -7. generates and outputs (either as raw string or `json` if used with `--output=json`) run commands for each nym-node - using full canonical and absolute paths (so you could paste them regardless of local directory) - -## `create-local-client` - -pre-requisites: - -1. you must have a running MIXNET **including nym-api AND nym-nodes** (just run `create-local-ecash-apis` followed - by `bond-local-mixnet` and start the binaries) -2. you must have built `nym-client` binary - -Initialise a locally run nym-client, adjust its config and output the run command: - -1. runs `nym-client init` in credentials mode -2. updates its config to add `minimum_mixnode_performance = 0` and `minimum_gateway_performance = 0` thus ignoring the - lack of a network monitor -3. generates and outputs run command for the client using full canonical and absolute paths (so you could paste it - regardless of local directory) - -### Extra - -For reference, my workflow was as follows: - -note: for the very first run you'll have to explicitly provide mnemonics and nyxd - -1. rebuild whichever binary/contract was needed -2. `cargo run -- create-local-ecash-apis --bypass-dkg-contract ../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm --number-of-apis=2 --nym-api-bin ../../../target/release/nym-api --built-contracts ../../../contracts/target/wasm32-unknown-unknown/release --custom-epoch-duration-secs=60` -3. run the apis in separate terminal window -4. `cargo run -- bond-local-mixnet --nym-node-bin ../../../target/release/nym-node` -5. start all the nym-nodes -6. `cargo run -- create-local-client --nym-client-bin ../../../target/debug/nym-client` -7. usually at this point I was using `nym-cli` to get some ticketbooks into my client before running it with the command - that was output in the previous step - - - - diff --git a/tools/internal/testnet-manager/migrations/01_initial_tables.sql b/tools/internal/testnet-manager/migrations/01_initial_tables.sql deleted file mode 100644 index 791a23e07a3..00000000000 --- a/tools/internal/testnet-manager/migrations/01_initial_tables.sql +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2024 - Nym Technologies SA - * SPDX-License-Identifier: GPL-3.0-only - */ - -CREATE TABLE metadata ( - id INTEGER PRIMARY KEY CHECK (id = 0), - latest_network_id INTEGER REFERENCES network(id), - - master_mnemonic TEXT NOT NULL, - rpc_endpoint TEXT NOT NULL -); - -CREATE TABLE network ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - - mixnet_contract_id INTEGER NOT NULL REFERENCES contract(id), - vesting_contract_id INTEGER NOT NULL REFERENCES contract(id), - ecash_contract_id INTEGER NOT NULL REFERENCES contract(id), - cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract(id), - cw4_group_contract_id INTEGER NOT NULL REFERENCES contract(id), - dkg_contract_id INTEGER NOT NULL REFERENCES contract(id), - - rewarder_address TEXT NOT NULL REFERENCES account(address), - ecash_holding_account_address TEXT NOT NULL REFERENCES account(address) -); - -CREATE TABLE contract ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - address TEXT NOT NULL, - admin_address TEXT NOT NULL REFERENCES account(address) -); - -CREATE TABLE account ( - address TEXT NOT NULL UNIQUE, - -- for the future 'import' feature this will have to be nullable - mnemonic TEXT NOT NULL -); - -CREATE TABLE node ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - identity_key TEXT NOT NULL, - network_id INTEGER NOT NULL REFERENCES network(id), - - -- i.e. mixnode or gateway - bonded_type TEXT NOT NULL, - owner_address TEXT NOT NULL REFERENCES account(address) -); \ No newline at end of file diff --git a/tools/internal/testnet-manager/migrations/02_performance_contract.sql b/tools/internal/testnet-manager/migrations/02_performance_contract.sql deleted file mode 100644 index 8ded7931302..00000000000 --- a/tools/internal/testnet-manager/migrations/02_performance_contract.sql +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2025 - Nym Technologies SA - * SPDX-License-Identifier: GPL-3.0-only - */ - --- 1. Rename old table to preserve data -ALTER TABLE network - RENAME TO network_old; - --- 2. Insert placeholder account (so that old networks would have _some_ value for performance contract) -INSERT INTO account (address, mnemonic) -VALUES ('n1tq2kggc6y44yqmnafh98vexxav8666cfkgvygf', - 'opinion scene salon slice noise easy security drift brown custom verb express old matrix mammal choose attract trash general staff manual elite destroy strategy'); - --- 3. Insert placeholder contract and record its id -INSERT INTO contract (name, address, admin_address) -VALUES ('placeholder', 'n14gl07zh58rydd4k9tyw320zvqd79vrwnjj4x9g', 'n1tq2kggc6y44yqmnafh98vexxav8666cfkgvygf'); - -CREATE TEMP TABLE tmp_placeholder -( - id INTEGER NOT NULL -); -INSERT INTO tmp_placeholder -VALUES (last_insert_rowid()); - - --- 4. Create the new network table with the new column -CREATE TABLE network -( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name TEXT NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - - mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), - vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), - ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), - cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), - cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), - dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), - performance_contract_id INTEGER NOT NULL REFERENCES contract (id), - - rewarder_address TEXT NOT NULL REFERENCES account (address), - ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) -); - --- 5. Copy existing data into the new table -INSERT INTO network(id, name, created_at, - mixnet_contract_id, vesting_contract_id, ecash_contract_id, - cw3_multisig_contract_id, cw4_group_contract_id, dkg_contract_id, - performance_contract_id, - rewarder_address, ecash_holding_account_address) -SELECT n.id, - n.name, - n.created_at, - n.mixnet_contract_id, - n.vesting_contract_id, - n.ecash_contract_id, - n.cw3_multisig_contract_id, - n.cw4_group_contract_id, - n.dkg_contract_id, - t.id, -- use the placeholder contract id - n.rewarder_address, - n.ecash_holding_account_address -FROM network_old AS n - CROSS JOIN tmp_placeholder AS t; - --- 6. recreate metadata table due to change in FK -ALTER TABLE metadata - RENAME TO metadata_old; - -CREATE TABLE metadata -( - id INTEGER PRIMARY KEY CHECK (id = 0), - latest_network_id INTEGER REFERENCES network (id), - - master_mnemonic TEXT NOT NULL, - rpc_endpoint TEXT NOT NULL -); - -INSERT INTO metadata -SELECT * -FROM metadata_old; - --- 7. recreate node table due to change in FK -ALTER Table node - RENAME TO node_old; - -CREATE TABLE node -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - identity_key TEXT NOT NULL, - network_id INTEGER NOT NULL REFERENCES network (id), - - -- i.e. mixnode or gateway - bonded_type TEXT NOT NULL, - owner_address TEXT NOT NULL REFERENCES account (address) -); - -INSERT INTO node -SELECT * -FROM node_old; - --- 8. Clean up -DROP TABLE tmp_placeholder; -DROP TABLE metadata_old; -DROP TABLE node_old; -DROP TABLE network_old; - - -CREATE TABLE authorised_network_monitor -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - network_id INTEGER NOT NULL REFERENCES network (id), - address TEXT NOT NULL REFERENCES account (address) -); diff --git a/tools/internal/testnet-manager/src/cli/bypass_dkg.rs b/tools/internal/testnet-manager/src/cli/bypass_dkg.rs deleted file mode 100644 index a938e73d474..00000000000 --- a/tools/internal/testnet-manager/src/cli/bypass_dkg.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::cli::CommonArgs; -use crate::error::NetworkManagerError; -use crate::helpers::default_storage_dir; -use std::path::PathBuf; -use url::Url; - -#[derive(clap::Args, Debug)] -pub(crate) struct Args { - #[clap(flatten)] - common: CommonArgs, - - #[clap(long)] - signer_data_output_directory: Option, - - #[clap(long)] - network_name: Option, - - /// The URLs of that the DKG parties would have put in the contract - #[clap(long, value_delimiter = ',')] - api_endpoints: Vec, - - /// Path to the contract built from the `dkg-bypass-contract` directory - #[clap(long)] - bypass_dkg_contract: PathBuf, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { - let manager = args.common.network_manager().await?; - let network = manager.load_existing_network(args.network_name).await?; - - let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { - explicit - } else { - default_storage_dir().join(&network.name) - }; - - manager - .attempt_bypass_dkg( - args.api_endpoints, - &network, - args.bypass_dkg_contract, - signer_data_output_directory, - ) - .await?; - - Ok(()) -} diff --git a/tools/internal/testnet-manager/src/cli/initialise_new_network.rs b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs deleted file mode 100644 index 99174e6d518..00000000000 --- a/tools/internal/testnet-manager/src/cli/initialise_new_network.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::cli::CommonArgs; -use crate::error::NetworkManagerError; -use crate::manager::env::Env; -use nym_bin_common::output_format::OutputFormat; -use std::path::PathBuf; -use std::time::Duration; - -#[derive(clap::Args, Debug)] -pub(crate) struct Args { - #[clap(flatten)] - common: CommonArgs, - - /// Path containing .wasm files of all contracts - #[clap(long)] - built_contracts: PathBuf, - - #[clap(long)] - network_name: Option, - - /// Specifies custom duration of mixnet epochs - /// It's recommended to set it to rather low value (like 60s) if you intend to bond the mixnet afterward. - #[clap(long)] - custom_epoch_duration_secs: Option, - - /// Specifies custom number of epochs sphinx keys are going to be valid for - #[clap(long)] - key_validity_in_epochs: Option, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { - let network = args - .common - .network_manager() - .await? - .initialise_new_network( - args.built_contracts, - args.network_name, - args.custom_epoch_duration_secs.map(Duration::from_secs), - args.key_validity_in_epochs, - ) - .await? - .into_loaded(); - - let env = Env::from(&network); - println!("add the following to your .env file: \n{env}",); - - Ok(()) -} diff --git a/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs deleted file mode 100644 index 1e5d24b1bff..00000000000 --- a/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::cli::CommonArgs; -use crate::error::NetworkManagerError; -use crate::helpers::default_storage_dir; -use crate::manager::env::Env; -use crate::manager::network::LoadedNetwork; -use nym_bin_common::output_format::OutputFormat; -use std::path::PathBuf; -use std::time::Duration; -use url::Url; - -#[derive(clap::Args, Debug)] -pub(crate) struct Args { - #[clap(flatten)] - common: CommonArgs, - - /// Path containing .wasm files of all contracts - #[clap(long)] - built_contracts: PathBuf, - - #[clap(long)] - network_name: Option, - - #[clap(long)] - signer_data_output_directory: Option, - - /// The URLs of that the DKG parties would have put in the contract - #[clap(long, value_delimiter = ',')] - api_endpoints: Vec, - - /// Path to the contract built from the `dkg-bypass-contract` directory - #[clap(long)] - bypass_dkg_contract: PathBuf, - - /// Specifies custom duration of mixnet epochs - /// It's recommended to set it to rather low value (like 60s) if you intend to bond the mixnet afterward. - #[clap(long)] - custom_epoch_duration_secs: Option, - - /// Specifies custom number of epochs sphinx keys are going to be valid for - #[clap(long)] - key_validity_in_epochs: Option, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { - let manager = args.common.network_manager().await?; - - let network: LoadedNetwork = manager - .initialise_new_network( - args.built_contracts, - args.network_name, - args.custom_epoch_duration_secs.map(Duration::from_secs), - args.key_validity_in_epochs, - ) - .await? - .into(); - - let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { - explicit - } else { - default_storage_dir().join(&network.name) - }; - - let env = Env::from(&network); - - manager - .attempt_bypass_dkg( - args.api_endpoints, - &network, - args.bypass_dkg_contract, - signer_data_output_directory, - ) - .await?; - - println!("add the following to your .env file: \n{env}",); - - Ok(()) -} diff --git a/tools/internal/testnet-manager/src/cli/load_network_details.rs b/tools/internal/testnet-manager/src/cli/load_network_details.rs deleted file mode 100644 index 414c7984e12..00000000000 --- a/tools/internal/testnet-manager/src/cli/load_network_details.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use crate::helpers::default_db_file; -use crate::manager::NetworkManager; -use crate::manager::env::Env; -use nym_bin_common::output_format::OutputFormat; -use std::path::PathBuf; - -#[derive(clap::Args, Debug)] -pub(crate) struct Args { - #[clap(long)] - network_name: Option, - - #[clap(long)] - storage_path: Option, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { - let storage = args.storage_path.unwrap_or_else(default_db_file); - - let network = NetworkManager::new(storage, None, None) - .await? - .load_existing_network(args.network_name) - .await?; - - let env = Env::from(&network); - println!("add the following to your .env file: \n{env}",); - - Ok(()) -} diff --git a/tools/internal/testnet-manager/src/cli/local_client.rs b/tools/internal/testnet-manager/src/cli/local_client.rs deleted file mode 100644 index 25a55f1253a..00000000000 --- a/tools/internal/testnet-manager/src/cli/local_client.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CommonArgs; -use crate::error::NetworkManagerError; -use nym_bin_common::output_format::OutputFormat; -use std::path::PathBuf; - -#[derive(clap::Args, Debug)] -pub(crate) struct Args { - #[clap(flatten)] - common: CommonArgs, - - /// Path to the `nym-client` binary - #[clap(long)] - nym_client_bin: PathBuf, - - #[clap(long)] - gateway: Option, - - #[clap(long)] - network_name: Option, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { - let manager = args.common.network_manager().await?; - let network = manager.load_existing_network(args.network_name).await?; - - let run_cmd = manager - .init_local_nym_client(args.nym_client_bin, &network, args.gateway) - .await?; - - if !args.output.is_text() { - args.output.to_stderr(&run_cmd) - } - - Ok(()) -} diff --git a/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs deleted file mode 100644 index 587e5c0375e..00000000000 --- a/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CommonArgs; -use crate::error::NetworkManagerError; -use crate::manager::network::LoadedNetwork; -use nym_bin_common::output_format::OutputFormat; -use std::path::PathBuf; -use std::time::Duration; -use tempfile::tempdir; -use url::Url; - -#[derive(clap::Args, Debug)] -pub(crate) struct Args { - #[clap(flatten)] - common: CommonArgs, - - /// Path to the `nym-api` binary - #[clap(long)] - nym_api_bin: PathBuf, - - /// Path containing .wasm files of all contracts - #[clap(long)] - built_contracts: PathBuf, - - #[clap(long)] - number_of_apis: usize, - - #[clap(long)] - network_name: Option, - - /// Path to the contract built from the `dkg-bypass-contract` directory - #[clap(long)] - bypass_dkg_contract: PathBuf, - - /// Specifies custom duration of mixnet epochs - #[clap(long)] - custom_epoch_duration_secs: Option, - - /// Specifies custom number of epochs sphinx keys are going to be valid for - #[clap(long)] - key_validity_in_epochs: Option, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { - let endpoints = (0..args.number_of_apis) - .map(|i| format!("http://127.0.0.1:{}", 10000 + i).parse().unwrap()) - .collect::>(); - - let manager = args.common.network_manager().await?; - - let network: LoadedNetwork = manager - .initialise_new_network( - args.built_contracts, - args.network_name, - args.custom_epoch_duration_secs.map(Duration::from_secs), - args.key_validity_in_epochs, - ) - .await? - .into(); - - let temp_output = tempdir()?; - - let signer_details = manager - .attempt_bypass_dkg( - endpoints, - &network, - args.bypass_dkg_contract, - temp_output.path(), - ) - .await?; - - let run_cmds = manager - .setup_local_apis(args.nym_api_bin, &network, signer_details) - .await?; - - if !args.output.is_text() { - args.output.to_stderr(&run_cmds) - } - - Ok(()) -} diff --git a/tools/internal/testnet-manager/src/cli/local_nodes.rs b/tools/internal/testnet-manager/src/cli/local_nodes.rs deleted file mode 100644 index d84b9a83e5c..00000000000 --- a/tools/internal/testnet-manager/src/cli/local_nodes.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CommonArgs; -use crate::error::NetworkManagerError; -use nym_bin_common::output_format::OutputFormat; -use std::path::PathBuf; - -#[derive(clap::Args, Debug)] -pub(crate) struct Args { - #[clap(flatten)] - common: CommonArgs, - - /// Path to the `nym-node` binary - #[clap(long)] - nym_node_bin: PathBuf, - - #[clap(long, default_value_t = 3)] - mixnodes: u16, - - #[clap(long, default_value_t = 1)] - gateways: u16, - - #[clap(long)] - network_name: Option, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { - let manager = args.common.network_manager().await?; - let network = manager.load_existing_network(args.network_name).await?; - - let run_cmds = manager - .init_local_nym_nodes(args.nym_node_bin, &network, args.mixnodes, args.gateways) - .await?; - - if !args.output.is_text() { - args.output.to_stderr(&run_cmds) - } - - Ok(()) -} diff --git a/tools/internal/testnet-manager/src/cli/migrate.rs b/tools/internal/testnet-manager/src/cli/migrate.rs deleted file mode 100644 index 13075aca1a2..00000000000 --- a/tools/internal/testnet-manager/src/cli/migrate.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use clap::Parser; -use nym_validator_client::nyxd::cosmwasm_client::types::{ContractCodeId, EmptyMsg}; - -// nyxd-style command so, for example `migrate ecash 123 '{}'` -#[derive(Debug, Parser)] -pub(crate) struct Args { - pub contract_name: String, - - pub code_id: ContractCodeId, - - pub message: serde_json::Value, -} - -pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { - todo!() -} diff --git a/tools/internal/testnet-manager/src/cli/mod.rs b/tools/internal/testnet-manager/src/cli/mod.rs deleted file mode 100644 index b8d143ae95e..00000000000 --- a/tools/internal/testnet-manager/src/cli/mod.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::path::PathBuf; -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only -use crate::error::NetworkManagerError; -use crate::helpers::default_db_file; -use crate::manager::NetworkManager; -use clap::{Parser, Subcommand}; -use nym_bin_common::bin_info; -use std::sync::OnceLock; -use url::Url; - -mod build_info; -mod bypass_dkg; -mod initialise_new_network; -mod initialise_post_dkg_network; -mod load_network_details; -mod local_client; -mod local_ecash_apis; -mod local_nodes; -// mod migrate; - -#[derive(clap::Args, Debug)] -pub(crate) struct CommonArgs { - #[clap(long)] - master_mnemonic: Option, - - #[clap(long)] - rpc_endpoint: Option, - - #[clap(long)] - storage_path: Option, -} - -impl CommonArgs { - pub(crate) async fn network_manager(self) -> Result { - let storage = self.storage_path.unwrap_or_else(default_db_file); - NetworkManager::new(storage, self.master_mnemonic, self.rpc_endpoint).await - } -} - -// Helper for passing LONG_VERSION to clap -fn pretty_build_info_static() -> &'static str { - static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); - PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) -} - -#[derive(Parser, Debug)] -#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] -pub(crate) struct Cli { - #[clap(subcommand)] - command: Commands, -} - -impl Cli { - pub(crate) async fn execute(self) -> Result<(), NetworkManagerError> { - match self.command { - Commands::BuildInfo(args) => build_info::execute(args), - Commands::InitialiseNewNetwork(args) => initialise_new_network::execute(args).await, - Commands::LoadNetworkDetails(args) => load_network_details::execute(args).await, - Commands::BypassDkg(args) => bypass_dkg::execute(args).await, - Commands::InitialisePostDkgNetwork(args) => { - initialise_post_dkg_network::execute(args).await - } - Commands::CreateLocalEcashApis(args) => local_ecash_apis::execute(args).await, - Commands::BondLocalMixnet(args) => local_nodes::execute(args).await, - Commands::CreateLocalClient(args) => local_client::execute(args).await, - } - } -} - -#[derive(Subcommand, Debug)] -pub(crate) enum Commands { - /// Show build information of this binary - BuildInfo(build_info::Args), - - /// Initialise new testnet network - InitialiseNewNetwork(initialise_new_network::Args), - - /// Attempt to load testnet network details - LoadNetworkDetails(load_network_details::Args), - - /// Attempt to bypass the DKG by overwriting the contract state with pre-generated keys - BypassDkg(bypass_dkg::Args), - - /// Initialise new network and bypass the DKG. - /// Equivalent of running `initialise-new-network` and `bypass-dkg` separately. - InitialisePostDkgNetwork(initialise_post_dkg_network::Args), - - /// Attempt to create brand new network, in post DKG-state, using locally running nym-apis - CreateLocalEcashApis(local_ecash_apis::Args), - - /// Attempt to bond minimal local mixnet (3 mixnodes + 1 gateways) and output the run commands - BondLocalMixnet(local_nodes::Args), - - /// Initialise a locally run nym-client, adjust its config and output the run command - CreateLocalClient(local_client::Args), -} - -#[cfg(test)] -mod tests { - use super::*; - use clap::CommandFactory; - - #[test] - fn verify_cli() { - Cli::command().debug_assert(); - } -} diff --git a/tools/internal/testnet-manager/src/error.rs b/tools/internal/testnet-manager/src/error.rs deleted file mode 100644 index 0a656c068b0..00000000000 --- a/tools/internal/testnet-manager/src/error.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only -use nym_compact_ecash::CompactEcashError; -use nym_validator_client::nyxd::error::NyxdError; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum NetworkManagerError { - #[error("io error: {0}")] - IoError(#[from] std::io::Error), - - #[error("failed to parse mnemonic: {0}")] - Bip39Error(#[from] bip39::Error), - - #[error("failed to parse the url: {0}")] - MalformedUrl(#[from] url::ParseError), - - #[error( - "one of the account addresses was malformed - the developer was too lazy to propagate the actual error message with the address" - )] - MalformedAccountAddress, - - #[error(transparent)] - Nyxd(#[from] NyxdError), - - #[error("you need to set the master mnemonic on initial run")] - MnemonicNotSet, - - #[error("you need to set the rpc endpoint on initial run")] - RpcEndpointNotSet, - - #[error("experienced internal database error: {0}")] - InternalDatabaseError(#[from] sqlx::Error), - - #[error("failed to perform startup SQL migration - {0}")] - StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), - - #[error("could not find .wasm file for {name} contract under the provided directory")] - ContractWasmNotFound { name: String }, - - #[error("could not find code_id for {name} contract")] - ContractNotUploaded { name: String }, - - #[error("could not find contract admin for {name} contract")] - ContractAdminNotSet { name: String }, - - #[error("could not find address for {name} contract")] - ContractNotInitialised { name: String }, - - #[error("could not find build information for {name} contract")] - ContractNotQueried { name: String }, - - #[error( - "contract {name} has been build before build information got standarised. this is not supported" - )] - MissingBuildInfo { name: String }, - - #[error("there aren't any initialised networks in the storage")] - NoNetworksInitialised, - - #[error("you must specify at least a single api endpoint for the DKG")] - NoApiEndpoints, - - #[error("the DKG process has already been started on the target network")] - DkgAlreadyStarted, - - #[error("the target network is already in non-zero DKG epoch")] - NonZeroEpoch, - - #[error("the target already has registered cw4 members")] - ExistingCW4Members, - - #[error("failed to compute ecash keys: {source}")] - EcashCryptoFailure { - #[from] - source: CompactEcashError, - }, - - #[error("the provided contract path does not point to a valid .wasm file")] - MalformedDkgBypassContractPath, - - #[error("nym api initialisation returned non-zero return code")] - NymApiExecutionFailure, - - #[error("nym node initialisation returned non-zero return code")] - NymNodeExecutionFailure, - - #[error("nym client initialisation returned non-zero return code")] - NymClientExecutionFailure, - - #[error("failed to deserialise nym-api config: {0}")] - TomlDeserialisationFailure(#[from] toml::de::Error), - - #[error("failed to deserialise nym-node output: {0}")] - JsonDeserialisationFailure(#[from] serde_json::Error), - - #[error( - "the corresponding env file hasn't been generated. you need to setup local apis first." - )] - EnvFileNotGenerated, - - #[error("the default, pre-generated, .env file does not have the nym-api endpoint set!")] - NymApiEndpointMissing, - - #[error( - "timed out while waiting for some gateway to appear in the directory (you don't need to run it)" - )] - ApiGatewayWaitTimeout, - - #[error( - "timed out while waiting for the gateway to start receiving traffic (you need to actually run it!)" - )] - GatewayWaitTimeout, - - #[error("attempted to bond nodes on a non-empty network")] - NetworkNotEmpty, -} diff --git a/tools/internal/testnet-manager/src/helpers.rs b/tools/internal/testnet-manager/src/helpers.rs deleted file mode 100644 index 8f8c6eacb04..00000000000 --- a/tools/internal/testnet-manager/src/helpers.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::NetworkManagerError; -use indicatif::{HumanDuration, ProgressBar}; -use nym_config::{NYM_DIR, must_get_home}; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::fmt::{Display, Formatter}; -use std::future::Future; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant}; -use tokio::pin; -use tokio::time::interval; - -// struct Ctx<'a, T> { -// progress: ProgressTracker, -// network: LoadedNetwork<'a>, -// inner: T, -// } - -pub(crate) trait ProgressCtx { - fn progress_tracker(&self) -> &ProgressTracker; - - fn println>(&self, msg: I) { - self.progress_tracker().println(msg) - } - - fn set_pb_prefix(&self, prefix: impl Into>) { - self.progress_tracker().set_pb_prefix(prefix) - } - - fn set_pb_message(&self, msg: impl Into>) { - self.progress_tracker().set_pb_message(msg) - } - - async fn async_with_progress(&self, fut: F) -> T - where - F: Future, - { - async_with_progress(fut, &self.progress_tracker().progress_bar).await - } -} - -// pub(crate) trait NetworkCtx { -// fn loaded_network(&self) -> &LoadedNetwork; -// } - -#[derive(Serialize, Deserialize)] -pub struct RunCommands(pub(crate) Vec); - -impl Display for RunCommands { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for cmd in &self.0 { - writeln!(f, "{cmd}")? - } - Ok(()) - } -} - -pub(crate) struct ProgressTracker { - start: Instant, - pub(crate) progress_bar: ProgressBar, -} - -impl ProgressTracker { - pub(crate) fn new>(msg: I) -> Self { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.println(msg); - - ProgressTracker { - start: Instant::now(), - progress_bar, - } - } - - pub(crate) fn println>(&self, msg: I) { - self.progress_bar.println(msg) - } - - pub(crate) fn set_pb_prefix(&self, prefix: impl Into>) { - self.progress_bar.set_prefix(prefix) - } - - pub(crate) fn set_pb_message(&self, msg: impl Into>) { - self.progress_bar.set_message(msg) - } - - pub(crate) fn output_run_commands(&self, cmds: &RunCommands) { - self.println("๐Ÿ‡ run the binaries with the following commands:"); - for cmd in &cmds.0 { - self.println(cmd) - } - } -} - -impl Default for ProgressTracker { - fn default() -> Self { - ProgressTracker { - start: Instant::now(), - progress_bar: ProgressBar::new_spinner(), - } - } -} - -impl Drop for ProgressTracker { - fn drop(&mut self) { - self.progress_bar.println(format!( - "โœจ Done in {}", - HumanDuration(self.start.elapsed()) - )); - self.progress_bar.finish_and_clear(); - } -} - -pub(crate) fn default_storage_dir() -> PathBuf { - must_get_home().join(NYM_DIR).join("testnet-manager") -} - -pub(crate) fn default_db_file() -> PathBuf { - default_storage_dir().join("network-data.sqlite") -} - -pub(crate) async fn async_with_progress(fut: F, pb: &ProgressBar) -> T -where - F: Future, -{ - pb.tick(); - pin!(fut); - let mut update_interval = interval(Duration::from_millis(50)); - - loop { - tokio::select! { - _ = update_interval.tick() => { - pb.tick() - } - res = &mut fut => { - return res - } - } - } -} - -pub(crate) fn wasm_code>(path: P) -> Result, NetworkManagerError> { - let path = path.as_ref(); - assert!(path.exists()); - let mut file = std::fs::File::open(path)?; - let mut data = Vec::new(); - - file.read_to_end(&mut data)?; - Ok(data) -} diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs deleted file mode 100644 index 4c04ade5d06..00000000000 --- a/tools/internal/testnet-manager/src/manager/contract.rs +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only -use crate::error::NetworkManagerError; -use nym_mixnet_contract_common::ContractBuildInformation; -use nym_validator_client::DirectSecp256k1HdWallet; -use nym_validator_client::nyxd::cosmwasm_client::types::{ - ContractCodeId, InstantiateResult, MigrateResult, UploadResult, -}; -use nym_validator_client::nyxd::{AccountId, Hash}; -use nym_validator_client::signing::signer::OfflineSigner; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct LoadedNymContracts { - pub(crate) mixnet: LoadedContract, - pub(crate) vesting: LoadedContract, - pub(crate) ecash: LoadedContract, - pub(crate) cw3_multisig: LoadedContract, - pub(crate) cw4_group: LoadedContract, - pub(crate) dkg: LoadedContract, - pub(crate) performance: LoadedContract, -} - -impl From for LoadedNymContracts { - fn from(value: NymContracts) -> Self { - LoadedNymContracts { - mixnet: value.mixnet.into(), - vesting: value.vesting.into(), - ecash: value.ecash.into(), - cw3_multisig: value.cw3_multisig.into(), - cw4_group: value.cw4_group.into(), - dkg: value.dkg.into(), - performance: value.performance.into(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct NymContracts { - pub(crate) mixnet: Contract, - pub(crate) vesting: Contract, - pub(crate) ecash: Contract, - pub(crate) cw3_multisig: Contract, - pub(crate) cw4_group: Contract, - pub(crate) dkg: Contract, - pub(crate) performance: Contract, -} - -impl NymContracts { - pub(crate) fn fake_iter(&self) -> Vec<&Contract> { - vec![ - &self.mixnet, - &self.vesting, - &self.ecash, - &self.cw3_multisig, - &self.cw4_group, - &self.dkg, - &self.performance, - ] - } - - pub(crate) fn fake_iter_mut(&mut self) -> Vec<&mut Contract> { - vec![ - &mut self.mixnet, - &mut self.vesting, - &mut self.ecash, - &mut self.cw3_multisig, - &mut self.cw4_group, - &mut self.dkg, - &mut self.performance, - ] - } - - pub(crate) fn count(&self) -> usize { - 7 - } - - pub(crate) fn discover_paths>( - &mut self, - base_path: P, - ) -> Result<(), NetworkManagerError> { - // just look in the base path, don't traverse - for entry_res in base_path.as_ref().read_dir()? { - let entry = entry_res?; - let Ok(name) = entry.file_name().into_string() else { - continue; - }; - - if name.ends_with(".wasm") { - if name.contains("mixnet") { - self.mixnet.wasm_path = Some(entry.path()) - } - if name.contains("vesting") { - self.vesting.wasm_path = Some(entry.path()) - } - if name.contains("ecash") { - self.ecash.wasm_path = Some(entry.path()) - } - if name.contains("cw4") { - self.cw4_group.wasm_path = Some(entry.path()) - } - if name.contains("cw3") { - self.cw3_multisig.wasm_path = Some(entry.path()) - } - if name.contains("dkg") { - self.dkg.wasm_path = Some(entry.path()) - } - if name.contains("performance") { - self.performance.wasm_path = Some(entry.path()) - } - } - } - - if let Some(no_path) = self.fake_iter().iter().find(|c| c.wasm_path.is_none()) { - return Err(NetworkManagerError::ContractWasmNotFound { - name: no_path.name.clone(), - }); - } - - Ok(()) - } -} - -impl Default for NymContracts { - fn default() -> Self { - NymContracts { - mixnet: Contract::new("mixnet"), - vesting: Contract::new("vesting"), - ecash: Contract::new("ecash"), - cw4_group: Contract::new("cw4_group"), - cw3_multisig: Contract::new("cw3_multisig"), - dkg: Contract::new("dkg"), - performance: Contract::new("performance"), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct Account { - pub(crate) address: AccountId, - pub(crate) mnemonic: bip39::Mnemonic, -} - -impl Account { - pub(crate) fn new() -> Account { - let mnemonic = bip39::Mnemonic::generate(24).unwrap(); - // sure, we're using hardcoded prefix, but realistically this will never change - let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic("n", mnemonic.clone()).unwrap(); - let address = wallet.signer_addresses().pop().unwrap(); - Account { address, mnemonic } - } - - pub(crate) fn address(&self) -> AccountId { - self.address.clone() - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct MinimalUploadInfo { - pub transaction_hash: Hash, - pub code_id: ContractCodeId, -} - -impl From for MinimalUploadInfo { - fn from(value: UploadResult) -> Self { - MinimalUploadInfo { - transaction_hash: value.transaction_hash, - code_id: value.code_id, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct MinimalInitInfo { - pub transaction_hash: Hash, - pub contract_address: AccountId, -} - -impl From for MinimalInitInfo { - fn from(value: InstantiateResult) -> Self { - MinimalInitInfo { - transaction_hash: value.transaction_hash, - contract_address: value.contract_address, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct MinimalMigrateInfo { - pub transaction_hash: Hash, -} - -impl From for MinimalMigrateInfo { - fn from(value: MigrateResult) -> Self { - MinimalMigrateInfo { - transaction_hash: value.transaction_hash, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct LoadedContract { - pub(crate) name: String, - pub(crate) address: AccountId, - pub(crate) admin_address: AccountId, - pub(crate) admin_mnemonic: bip39::Mnemonic, -} - -impl From for LoadedContract { - fn from(value: Contract) -> Self { - let admin = value.admin.expect("no admin set"); - LoadedContract { - name: value.name, - address: value.init_info.expect("uninitialised").contract_address, - admin_address: admin.address, - admin_mnemonic: admin.mnemonic, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct Contract { - pub(crate) name: String, - pub(crate) wasm_path: Option, - pub(crate) upload_info: Option, - pub(crate) admin: Option, - pub(crate) init_info: Option, - pub(crate) migrate_info: Option, - pub(crate) build_info: Option, -} - -impl Contract { - pub(crate) fn new>(name: S) -> Self { - Contract { - name: name.into(), - wasm_path: None, - upload_info: None, - admin: None, - init_info: None, - migrate_info: None, - build_info: None, - } - } - - pub(crate) fn wasm_path(&self) -> Result<&PathBuf, NetworkManagerError> { - self.wasm_path - .as_ref() - .ok_or_else(|| NetworkManagerError::ContractWasmNotFound { - name: self.name.clone(), - }) - } - - pub(crate) fn upload_info(&self) -> Result<&MinimalUploadInfo, NetworkManagerError> { - self.upload_info - .as_ref() - .ok_or_else(|| NetworkManagerError::ContractNotUploaded { - name: self.name.clone(), - }) - } - - pub(crate) fn admin(&self) -> Result<&Account, NetworkManagerError> { - self.admin - .as_ref() - .ok_or_else(|| NetworkManagerError::ContractAdminNotSet { - name: self.name.clone(), - }) - } - - pub(crate) fn init_info(&self) -> Result<&MinimalInitInfo, NetworkManagerError> { - self.init_info - .as_ref() - .ok_or_else(|| NetworkManagerError::ContractNotInitialised { - name: self.name.clone(), - }) - } - - #[allow(dead_code)] - pub(crate) fn build_info(&self) -> Result<&ContractBuildInformation, NetworkManagerError> { - self.build_info - .as_ref() - .ok_or_else(|| NetworkManagerError::ContractNotQueried { - name: self.name.clone(), - }) - } - - pub(crate) fn address(&self) -> Result<&AccountId, NetworkManagerError> { - self.init_info().map(|info| &info.contract_address) - } -} diff --git a/tools/internal/testnet-manager/src/manager/dkg_skip.rs b/tools/internal/testnet-manager/src/manager/dkg_skip.rs deleted file mode 100644 index 13126125b2a..00000000000 --- a/tools/internal/testnet-manager/src/manager/dkg_skip.rs +++ /dev/null @@ -1,483 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use crate::helpers::{ProgressCtx, ProgressTracker}; -use crate::manager::NetworkManager; -use crate::manager::contract::Account; -use crate::manager::network::LoadedNetwork; -use console::style; -use dkg_bypass_contract::msg::FakeDealerData; -use nym_compact_ecash::{Base58, KeyPairAuth, ttp_keygen}; -use nym_crypto::asymmetric::ed25519; -use nym_mixnet_contract_common::Addr; -use nym_pemstore::traits::PemStorableKey; -use nym_pemstore::{KeyPairPath, store_key, store_keypair}; -use nym_validator_client::DirectSigningHttpRpcNyxdClient; -use nym_validator_client::nyxd::contract_traits::{ - DkgQueryClient, GroupSigningClient, PagedGroupQueryClient, -}; -use nym_validator_client::nyxd::cosmwasm::ContractCodeId; -use nym_validator_client::nyxd::cw4::Member; -use nym_validator_client::nyxd::{AccountId, CosmWasmClient}; -use rand::rngs::OsRng; -use std::fs; -use std::ops::Deref; -use std::path::{Path, PathBuf}; -use url::Url; -use zeroize::Zeroizing; - -pub(crate) struct EcashSigner { - pub(crate) ed25519_keypair: ed25519::KeyPair, - pub(crate) ecash_keypair: nym_compact_ecash::KeyPairAuth, - pub(crate) cosmos_account: Account, - pub(crate) endpoint: Url, -} - -#[derive(Default)] -pub(crate) struct EcashSignerPaths { - pub(crate) ecash_key: PathBuf, - pub(crate) ed25519_keys: KeyPairPath, - pub(crate) mnemonic_path: PathBuf, - pub(crate) endpoint_path: PathBuf, -} - -pub(crate) struct EcashSignerWithPaths { - pub(crate) data: EcashSigner, - pub(crate) paths: EcashSignerPaths, -} - -// perform the same serialisation as the nym-api keys -struct FakeDkgKey<'a> { - inner: &'a KeyPairAuth, -} - -impl<'a> FakeDkgKey<'a> { - fn new(inner: &'a KeyPairAuth) -> Self { - FakeDkgKey { inner } - } -} - -impl PemStorableKey for FakeDkgKey<'_> { - type Error = NetworkManagerError; - - fn pem_type() -> &'static str { - "ECASH KEY WITH EPOCH" - } - - fn to_bytes(&self) -> Vec { - // our fake key is ALWAYS issued for epoch 0 - let mut bytes = vec![0u8; 8]; - bytes.append(&mut self.inner.secret_key().to_bytes()); - bytes - } - - fn from_bytes(_: &[u8]) -> Result { - unimplemented!("this is not meant to be ever called") - } -} - -struct DkgSkipCtx<'a> { - progress: ProgressTracker, - network: &'a LoadedNetwork, - dkg_admin: DirectSigningHttpRpcNyxdClient, - ecash_signers: Vec, -} - -impl ProgressCtx for DkgSkipCtx<'_> { - fn progress_tracker(&self) -> &ProgressTracker { - &self.progress - } -} - -impl<'a> DkgSkipCtx<'a> { - fn dkg_contract(&self) -> &AccountId { - &self.network.contracts.dkg.address - } - - fn new(network: &'a LoadedNetwork) -> Result { - let progress = ProgressTracker::new(format!( - "\n๐Ÿฅท attempting to skip DKG on network '{}'", - network.name - )); - - Ok(DkgSkipCtx { - progress, - dkg_admin: network.dkg_signing_client()?, - network, - ecash_signers: vec![], - }) - } - - fn group_signing_client(&self) -> Result { - self.network.cw4_group_signing_client() - } - - fn admin_signing_client( - &self, - mnemonic: bip39::Mnemonic, - ) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - self.network.client_config()?, - self.network.rpc_endpoint.as_str(), - mnemonic, - )?) - } -} - -impl NetworkManager { - fn generate_ecash_signer_data( - &self, - ctx: &mut DkgSkipCtx, - api_endpoints: Vec, - mut prime_api: Option, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ“ {}Generating ecash keys for all signers...", - style("[1/8]").bold().dim() - )); - - // generate required materials - let n = api_endpoints.len(); - let threshold = (2 * n).div_ceil(3); - - let ecash_keys = ttp_keygen(threshold as u64, n as u64)?; - - let mut ecash_signers = Vec::new(); - let mut rng = OsRng; - for (i, (endpoint, ecash_keypair)) in api_endpoints - .into_iter() - .zip(ecash_keys.into_iter()) - .enumerate() - { - // if available, use provided account for the first api (so that it would be permitted to do rewarding, etc.) - let cosmos_account = if i == 0 { - prime_api.take().unwrap_or(Account::new()) - } else { - Account::new() - }; - - let ed25519_keypair = ed25519::KeyPair::new(&mut rng); - let data = EcashSigner { - ed25519_keypair, - ecash_keypair, - cosmos_account, - endpoint, - }; - ctx.println(format!( - "\t{} will be managed by {}", - data.endpoint, data.cosmos_account.address - )); - let full = EcashSignerWithPaths { - data, - paths: EcashSignerPaths::default(), - }; - ecash_signers.push(full) - } - ctx.ecash_signers = ecash_signers; - - ctx.println("\tโœ… generated ecash keys for all signers"); - Ok(()) - } - - async fn validate_existing_contracts( - &self, - ctx: &DkgSkipCtx<'_>, - ) -> Result { - ctx.println(format!( - "๐Ÿ”ฌ {}Validating the current DKG and group contracts...", - style("[2/8]").bold().dim() - )); - - ctx.set_pb_prefix("[1/3]"); - ctx.set_pb_message("checking DKG epoch data..."); - let epoch_fut = ctx.dkg_admin.get_current_epoch(); - let dkg_epoch = ctx.async_with_progress(epoch_fut).await?; - if dkg_epoch.epoch_id != 0 { - return Err(NetworkManagerError::NonZeroEpoch); - } - - if !dkg_epoch.state.is_waiting_initialisation() { - return Err(NetworkManagerError::DkgAlreadyStarted); - } - - ctx.set_pb_prefix("[2/3]"); - ctx.set_pb_message("retrieving DKG contract code_id..."); - let code_fut = ctx - .dkg_admin - .get_contract_code_history(&ctx.network.contracts.dkg.address); - let code_history = ctx.async_with_progress(code_fut).await?; - - // SAFETY: - // if this is empty our abci query is invalid since we have just queried the contract so it must exist - let current_code = code_history.last().unwrap().code_id; - ctx.println("\tthe DKG contract is all good!"); - - ctx.set_pb_prefix("[3/3]"); - ctx.set_pb_message("checking cw4 group members data..."); - let members_fut = ctx.dkg_admin.get_all_members(); - let members = ctx.async_with_progress(members_fut).await?; - if !members.is_empty() { - return Err(NetworkManagerError::ExistingCW4Members); - } - - ctx.println("\tthe group contract is all good!"); - ctx.println("\tโœ… the existing contracts are all good!"); - - Ok(current_code) - } - - async fn persist_dkg_keys>( - &self, - ctx: &mut DkgSkipCtx<'_>, - output_dir: P, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ“ฆ {}Persisting the signer keys...", - style("[3/8]").bold().dim() - )); - - ctx.set_pb_message("storing the signer data on disk..."); - - let output_dir = output_dir.as_ref(); - let pb = &ctx.progress.progress_bar; - - for signer in &mut ctx.ecash_signers { - let address = &signer.data.cosmos_account.address; - let url = &signer.data.endpoint; - let signer_dir = output_dir.join(address.to_string()); - fs::create_dir_all(&signer_dir)?; - - let fake_ecash_key = FakeDkgKey::new(&signer.data.ecash_keypair); - - let ecash_path = signer_dir.join("ecash"); - - let ed25519_paths = KeyPairPath { - private_key_path: signer_dir.join("ed25519"), - public_key_path: signer_dir.join("ed25519.pub"), - }; - - let mnemonic_path = signer_dir.join("mnemonic"); - let endpoint_path = signer_dir.join("announce_address"); - - store_key(&fake_ecash_key, &ecash_path)?; - store_keypair(&signer.data.ed25519_keypair, &ed25519_paths)?; - - fs::write( - &mnemonic_path, - Zeroizing::new(signer.data.cosmos_account.mnemonic.to_string()), - )?; - fs::write(&endpoint_path, url.as_str())?; - - signer.paths.ecash_key = ecash_path; - signer.paths.ed25519_keys = ed25519_paths; - signer.paths.mnemonic_path = mnemonic_path; - signer.paths.endpoint_path = endpoint_path; - - pb.println(format!( - "\tpersisted {address} (endpoint: {url}) data under {}", - signer_dir.display() - )); - } - - ctx.println("\tโœ… persisted all the signer keys!"); - Ok(()) - } - - async fn upload_bypass_contract>( - &self, - ctx: &DkgSkipCtx<'_>, - dkg_bypass_contract: P, - ) -> Result { - ctx.println(format!( - "๐Ÿšš {}Uploading the bypass contract...", - style("[4/8]").bold().dim() - )); - - ctx.set_pb_message("uploading the bypass contract..."); - - let res = self - .upload_contract( - &ctx.dkg_admin, - &ctx.progress.progress_bar, - dkg_bypass_contract, - ) - .await?; - - ctx.println("\tโœ… uploaded the bypass contract!"); - - Ok(res.code_id) - } - - async fn migrate_to_bypass_contract( - &self, - ctx: &DkgSkipCtx<'_>, - code_id: ContractCodeId, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ”€ {}Attempting to migrate into the bypass contract...", - style("[5/8]").bold().dim() - )); - - ctx.set_pb_message("migrating the DKG contract..."); - - let migrate_msg = dkg_bypass_contract::MigrateMsg { - dealers: ctx - .ecash_signers - .iter() - .map(|signer| FakeDealerData { - vk: signer.data.ecash_keypair.verification_key().to_bs58(), - ed25519_identity: signer.data.ed25519_keypair.public_key().to_base58_string(), - announce: signer.data.endpoint.to_string(), - owner: Addr::unchecked(signer.data.cosmos_account.address.as_ref()), - }) - .collect(), - }; - - let migrate_fut = ctx.dkg_admin.migrate( - ctx.dkg_contract(), - code_id, - &migrate_msg, - "migrating bypass DKG contract from testnet-manager", - None, - ); - ctx.async_with_progress(migrate_fut).await?; - - ctx.println("\tโœ… migrated the DKG into the bypass contract!"); - - Ok(()) - } - - async fn restore_dkg_contract( - &self, - ctx: &DkgSkipCtx<'_>, - code_id: ContractCodeId, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "โ†ฉ๏ธ {}Attempting to migrate back into the original DKG contract...", - style("[6/8]").bold().dim() - )); - - ctx.set_pb_message("migrating the DKG contract..."); - - let migrate_msg = nym_coconut_dkg_common::msg::MigrateMsg {}; - let migrate_fut = ctx.dkg_admin.migrate( - ctx.dkg_contract(), - code_id, - &migrate_msg, - "migrating initial DKG contract from testnet-manager", - None, - ); - ctx.async_with_progress(migrate_fut).await?; - - ctx.println("\tโœ… restored the original DKG contract!"); - - Ok(()) - } - - async fn add_group_members(&self, ctx: &DkgSkipCtx<'_>) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ‘ช {}Adding all the cw4 group members...", - style("[7/8]").bold().dim() - )); - - ctx.set_pb_message("โ›ฝcreating a new big cw4 family..."); - let admin = ctx.group_signing_client()?; - let new_members = ctx - .ecash_signers - .iter() - .map(|s| Member { - addr: s.data.cosmos_account.address.to_string(), - weight: 1, - }) - .collect(); - - let update_fut = admin.update_members(new_members, Vec::new(), None); - - ctx.async_with_progress(update_fut).await?; - ctx.println("\tโœ… new cw4 group members got added"); - Ok(()) - } - - async fn transfer_signer_tokens( - &self, - ctx: &DkgSkipCtx<'_>, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ’ธ {}Transferring tokens to the new signers...", - style("[8/8]").bold().dim() - )); - - let admin = ctx.admin_signing_client(self.admin.deref().clone())?; - - let mut receivers = Vec::new(); - for signer in &ctx.ecash_signers { - // send 250nym to the admin - receivers.push(( - signer.data.cosmos_account.address.clone(), - admin.mix_coins(250_000000), - )) - } - - ctx.set_pb_message("attempting to send signer tokens..."); - - let send_future = admin.send_multiple( - receivers, - "signers token transfer from testnet-manager", - None, - ); - let res = ctx.async_with_progress(send_future).await?; - - ctx.println(format!( - "\tโœ… sent tokens in transaction: {} (height {})", - res.hash, res.height - )); - Ok(()) - } - - pub(crate) async fn attempt_bypass_dkg( - &self, - api_endpoints: Vec, - network: &LoadedNetwork, - dkg_bypass_contract: P1, - data_output_dir: P2, - ) -> Result, NetworkManagerError> - where - P1: AsRef, - P2: AsRef, - { - if api_endpoints.is_empty() { - return Err(NetworkManagerError::NoApiEndpoints); - } - - let dkg_bypass_contract = dkg_bypass_contract.as_ref(); - if !dkg_bypass_contract.is_file() { - return Err(NetworkManagerError::MalformedDkgBypassContractPath); - } - let Some(ext) = dkg_bypass_contract.extension() else { - return Err(NetworkManagerError::MalformedDkgBypassContractPath); - }; - if ext != "wasm" { - return Err(NetworkManagerError::MalformedDkgBypassContractPath); - } - - let mut ctx = DkgSkipCtx::new(network)?; - - self.generate_ecash_signer_data( - &mut ctx, - api_endpoints, - Some(network.auxiliary_addresses.mixnet_rewarder.clone()), - )?; - let current_code_id = self.validate_existing_contracts(&ctx).await?; - self.persist_dkg_keys(&mut ctx, data_output_dir).await?; - let new_code_id = self - .upload_bypass_contract(&ctx, dkg_bypass_contract) - .await?; - self.migrate_to_bypass_contract(&ctx, new_code_id).await?; - self.restore_dkg_contract(&ctx, current_code_id).await?; - self.add_group_members(&ctx).await?; - self.transfer_signer_tokens(&ctx).await?; - - Ok(ctx.ecash_signers) - } -} diff --git a/tools/internal/testnet-manager/src/manager/env.rs b/tools/internal/testnet-manager/src/manager/env.rs deleted file mode 100644 index 005e6b48f42..00000000000 --- a/tools/internal/testnet-manager/src/manager/env.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use crate::manager::network::LoadedNetwork; -use nym_config::defaults::var_names; -use std::fmt::{Display, Formatter}; -use std::fs; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use tracing::{trace, warn}; - -#[derive(Default)] -pub struct Env { - pub(crate) mixnet_contract_address: Option, - pub(crate) vesting_contract_address: Option, - pub(crate) ecash_contract_address: Option, - pub(crate) cw4_group_contract_address: Option, - pub(crate) cw3_multisig_contract_address: Option, - pub(crate) dkg_contract_address: Option, - pub(crate) nyxd_endpoint: Option, - pub(crate) nym_api_endpoint: Option, -} - -impl Env { - pub fn with_nym_api>(mut self, nym_api: S) -> Self { - self.nym_api_endpoint = Some(nym_api.into()); - self - } - - // this will be used eventually - #[allow(dead_code)] - pub fn try_load>(path: P) -> Result { - let mut env = Env::default(); - let content = fs::read_to_string(path)?; - - for entry in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) { - let Some((k, v)) = entry.split_once('=') else { - warn!("malformed .env entry: '{entry}'"); - continue; - }; - - match k { - var_names::CONFIGURED - | var_names::BECH32_PREFIX - | var_names::MIX_DENOM - | var_names::MIX_DENOM_DISPLAY - | var_names::STAKE_DENOM - | var_names::STAKE_DENOM_DISPLAY - | var_names::DENOMS_EXPONENT => { - trace!("ignoring values for {k} and using default instead") - } - var_names::MIXNET_CONTRACT_ADDRESS => { - env.mixnet_contract_address = Some(v.to_string()) - } - var_names::VESTING_CONTRACT_ADDRESS => { - env.vesting_contract_address = Some(v.to_string()) - } - var_names::ECASH_CONTRACT_ADDRESS => { - env.ecash_contract_address = Some(v.to_string()) - } - var_names::GROUP_CONTRACT_ADDRESS => { - env.cw4_group_contract_address = Some(v.to_string()) - } - var_names::MULTISIG_CONTRACT_ADDRESS => { - env.cw3_multisig_contract_address = Some(v.to_string()) - } - var_names::COCONUT_DKG_CONTRACT_ADDRESS => { - env.dkg_contract_address = Some(v.to_string()) - } - var_names::NYXD => env.nyxd_endpoint = Some(v.to_string()), - var_names::NYM_API => env.nym_api_endpoint = Some(v.to_string()), - other => warn!("unsupported .env entry: '{other}'"), - } - } - - Ok(env) - } - - pub fn save>(&self, path: P) -> Result<(), NetworkManagerError> { - let path = path.as_ref(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - let mut env_file = File::create(path)?; - let content = self.to_string(); - env_file.write_all(content.as_bytes())?; - Ok(()) - } -} - -impl<'a> From<&'a LoadedNetwork> for Env { - fn from(network: &'a LoadedNetwork) -> Self { - Env { - mixnet_contract_address: Some(network.contracts.mixnet.address.to_string()), - vesting_contract_address: Some(network.contracts.vesting.address.to_string()), - ecash_contract_address: Some(network.contracts.ecash.address.to_string()), - cw4_group_contract_address: Some(network.contracts.cw4_group.address.to_string()), - cw3_multisig_contract_address: Some(network.contracts.cw3_multisig.address.to_string()), - dkg_contract_address: Some(network.contracts.dkg.address.to_string()), - nyxd_endpoint: Some(network.rpc_endpoint.to_string()), - nym_api_endpoint: None, - } - } -} - -impl Display for Env { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "CONFIGURED=true\n\ -\n\ -BECH32_PREFIX=n\n\ -MIX_DENOM=unym\n\ -MIX_DENOM_DISPLAY=nym\n\ -STAKE_DENOM=unyx\n\ -STAKE_DENOM_DISPLAY=nyx\n\ -DENOMS_EXPONENT=6\n\ -\n\ -" - )?; - if let Some(mixnet_contract_address) = &self.mixnet_contract_address { - writeln!( - f, - "{}={mixnet_contract_address}", - var_names::MIXNET_CONTRACT_ADDRESS - )?; - } - if let Some(vesting_contract_address) = &self.vesting_contract_address { - writeln!( - f, - "{}={vesting_contract_address}", - var_names::VESTING_CONTRACT_ADDRESS - )?; - } - if let Some(ecash_contract_address) = &self.ecash_contract_address { - writeln!( - f, - "{}={ecash_contract_address}", - var_names::ECASH_CONTRACT_ADDRESS - )?; - } - if let Some(cw4_group_contract_address) = &self.cw4_group_contract_address { - writeln!( - f, - "{}={cw4_group_contract_address}", - var_names::GROUP_CONTRACT_ADDRESS - )?; - } - if let Some(cw3_multisig_contract_address) = &self.cw3_multisig_contract_address { - writeln!( - f, - "{}={cw3_multisig_contract_address}", - var_names::MULTISIG_CONTRACT_ADDRESS - )?; - } - if let Some(dkg_contract_address) = &self.dkg_contract_address { - writeln!( - f, - "{}={dkg_contract_address}", - var_names::COCONUT_DKG_CONTRACT_ADDRESS - )?; - } - if let Some(nyxd_endpoint) = &self.nyxd_endpoint { - writeln!(f, "{}={nyxd_endpoint}", var_names::NYXD)?; - } - if let Some(nym_api_endpoint) = &self.nym_api_endpoint { - writeln!(f, "{}={nym_api_endpoint}", var_names::NYM_API)?; - } - Ok(()) - } -} diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs deleted file mode 100644 index e54e4589270..00000000000 --- a/tools/internal/testnet-manager/src/manager/local_apis.rs +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; -use crate::manager::NetworkManager; -use crate::manager::dkg_skip::EcashSignerWithPaths; -use crate::manager::env::Env; -use crate::manager::network::LoadedNetwork; -use console::style; -use nym_config::{ - DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_NYM_APIS_DIR, NYM_DIR, must_get_home, -}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use tokio::process::Command; -use zeroize::Zeroizing; - -struct LocalApisCtx<'a> { - nym_api_binary: PathBuf, - progress: ProgressTracker, - network: &'a LoadedNetwork, - signers: Vec, -} - -impl ProgressCtx for LocalApisCtx<'_> { - fn progress_tracker(&self) -> &ProgressTracker { - &self.progress - } -} - -impl<'a> LocalApisCtx<'a> { - fn signer_id(&self, signer: &EcashSignerWithPaths) -> String { - format!( - "{}-{}", - signer.data.cosmos_account.address, self.network.name - ) - } - - fn new( - nym_api_binary: PathBuf, - network: &'a LoadedNetwork, - signers: Vec, - ) -> Result { - let progress = ProgressTracker::new(format!( - "\n๐Ÿš€ setting up new local signing nym-APIs for network '{}' over {}", - network.name, network.rpc_endpoint - )); - - Ok(LocalApisCtx { - nym_api_binary, - network, - progress, - signers, - }) - } -} - -impl NetworkManager { - fn nym_api_config(&self, api_id: &str) -> PathBuf { - must_get_home() - .join(NYM_DIR) - .join(DEFAULT_NYM_APIS_DIR) - .join(api_id) - .join(DEFAULT_CONFIG_DIR) - .join(DEFAULT_CONFIG_FILENAME) - } - - async fn initialise_api( - &self, - ctx: &LocalApisCtx<'_>, - info: &EcashSignerWithPaths, - ) -> Result<(), NetworkManagerError> { - let address = &info.data.cosmos_account.address; - - ctx.set_pb_message(format!("initialising api {address}...")); - - let id = ctx.signer_id(info); - - // setup the binary itself - let mut child = Command::new(&ctx.nym_api_binary) - .args([ - "init", - "--id", - &id, - "--nyxd-validator", - ctx.network.rpc_endpoint.as_ref(), - "--mnemonic", - &Zeroizing::new(info.data.cosmos_account.mnemonic.to_string()), - "--enable-zk-nym", - "--announce-address", - info.data.endpoint.as_ref(), - "--bind-address", - &format!("0.0.0.0:{}", info.data.endpoint.port().unwrap()), - ]) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .kill_on_drop(true) - .spawn()?; - let child_fut = child.wait(); - let out = ctx.async_with_progress(child_fut).await?; - if !out.success() { - return Err(NetworkManagerError::NymApiExecutionFailure); - } - - // load the config (and do very nasty things to it) - let config_path = self.nym_api_config(&id); - let config_content = fs::read_to_string(config_path)?; - let parsed_config: toml::Table = toml::from_str(&config_content)?; - let storage_paths = &parsed_config["base"] - .as_table() - .expect("nym-api config serialisation has changed")["storage_paths"] - .as_table() - .expect("nym-api config serialisation has changed"); - - let priv_id = &storage_paths["private_identity_key_file"] - .as_str() - .expect("nym-api config serialisation has changed"); - let pub_id = &storage_paths["public_identity_key_file"] - .as_str() - .expect("nym-api config serialisation has changed"); - let ecash = &parsed_config["ecash_signer"] - .as_table() - .expect("nym-api config serialisation has changed")["storage_paths"] - .as_table() - .expect("nym-api config serialisation has changed")["ecash_key_path"] - .as_str() - .expect("nym-api config serialisation has changed"); - - // overwrite pre-generated files - fs::copy(&info.paths.ecash_key, ecash)?; - fs::copy(&info.paths.ed25519_keys.private_key_path, priv_id)?; - fs::copy(&info.paths.ed25519_keys.public_key_path, pub_id)?; - - ctx.println(format!("\t nym-API {address} got initialised")); - - Ok(()) - } - - async fn initialise_apis(&self, ctx: &LocalApisCtx<'_>) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ” {}Initialising local nym-apis...", - style("[1/1]").bold().dim() - )); - - for signer in &ctx.signers { - self.initialise_api(ctx, signer).await? - } - - ctx.println("\tโœ… all APIs got initialised!"); - Ok(()) - } - - fn prepare_api_run_commands>( - &self, - ctx: &LocalApisCtx, - env_file: P, - ) -> Result { - let bin_canon = fs::canonicalize(&ctx.nym_api_binary)?; - let env_canon = fs::canonicalize(env_file)?; - let bin_canon_display = bin_canon.display(); - let env_canon_display = env_canon.display(); - - let mut cmds = Vec::new(); - for signer in &ctx.signers { - let id = ctx.signer_id(signer); - - cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --allow-illegal-ips" - )); - } - Ok(RunCommands(cmds)) - } - - fn output_api_run_commands(&self, ctx: &LocalApisCtx, cmds: &RunCommands) { - ctx.progress.output_run_commands(cmds) - } - - fn prepare_env_file>( - &self, - ctx: &LocalApisCtx, - env_file: P, - ) -> Result<(), NetworkManagerError> { - let env = Env::from(ctx.network).with_nym_api(ctx.signers[0].data.endpoint.as_ref()); - - let latest = self.default_latest_env_file_path(); - if fs::read_link(&latest).is_ok() { - fs::remove_file(&latest)?; - } - - let env_file_path = env_file.as_ref(); - env.save(env_file_path)?; - - // make symlink for usability purposes - std::os::unix::fs::symlink(env_file_path, &latest)?; - - Ok(()) - } - - pub(crate) async fn setup_local_apis>( - &self, - nym_api_binary: P, - network: &LoadedNetwork, - signer_data: Vec, - ) -> Result { - let ctx = LocalApisCtx::new(nym_api_binary.as_ref().to_path_buf(), network, signer_data)?; - let env_file = ctx.network.default_env_file_path(); - - self.initialise_apis(&ctx).await?; - self.prepare_env_file(&ctx, &env_file)?; - let cmds = self.prepare_api_run_commands(&ctx, env_file)?; - self.output_api_run_commands(&ctx, &cmds); - - Ok(cmds) - } -} diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs deleted file mode 100644 index 3969bfea33f..00000000000 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use crate::helpers::{ProgressCtx, ProgressTracker}; -use crate::manager::NetworkManager; -use crate::manager::network::LoadedNetwork; -use console::style; -use nym_config::{DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR, must_get_home}; -use nym_validator_client::nym_api::NymApiClientExt; -use rand::{RngCore, thread_rng}; -use std::fs; -use std::fs::OpenOptions; -use std::io::prelude::*; -use std::net::SocketAddr; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::time::Duration; -use tokio::net::TcpStream; -use tokio::process::Command; -use tokio::time::sleep; -use url::Url; - -struct LocalClientCtx<'a> { - nym_client_binary: PathBuf, - client_id: String, - gateway: Option, - - progress: ProgressTracker, - network: &'a LoadedNetwork, -} - -impl ProgressCtx for LocalClientCtx<'_> { - fn progress_tracker(&self) -> &ProgressTracker { - &self.progress - } -} - -impl<'a> LocalClientCtx<'a> { - fn new( - nym_client_binary: PathBuf, - gateway: Option, - network: &'a LoadedNetwork, - ) -> Result { - let progress = ProgressTracker::new(format!( - "\n๐Ÿš€ setting up new local nym-client for network '{}' over {}", - network.name, network.rpc_endpoint - )); - let mut rng = thread_rng(); - let client_id = format!("{}-client-{}", network.name, rng.next_u32()); - - Ok(LocalClientCtx { - nym_client_binary, - network, - progress, - client_id, - gateway, - }) - } - - // hehe, that's disgusting, but it's not meant to be used by users - fn nym_api_url(&self) -> Result { - let env_file = fs::read_to_string(self.network.default_env_file_path())?; - for entry in env_file.lines() { - if let Some(raw_url) = entry.strip_prefix("NYM_API=") { - return Ok(raw_url.parse()?); - } - } - Err(NetworkManagerError::NymApiEndpointMissing) - } -} - -impl NetworkManager { - fn nym_client_config(&self, client_id: &str) -> PathBuf { - must_get_home() - .join(NYM_DIR) - .join("clients") - .join(client_id) - .join(DEFAULT_CONFIG_DIR) - .join(DEFAULT_CONFIG_FILENAME) - } - - async fn wait_for_api_gateway( - &self, - ctx: &LocalClientCtx<'_>, - ) -> Result { - // create api client - // hehe, that's disgusting, but it's not meant to be used by users - let api_url = ctx.nym_api_url()?; - ctx.set_pb_message(format!( - "โŒ›waiting for any gateway to appear in the directory ({api_url})..." - )); - - let api_client = nym_http_api_client::Client::builder(api_url.clone()) - .expect("Failed to create API client builder") - .build() - .expect("Failed to build API client"); - - let wait_fut = async { - let inner_fut = async { - loop { - let nodes = match api_client.get_all_basic_nodes_with_metadata().await { - Ok(nodes) => nodes.nodes, - Err(err) => { - ctx.println(format!( - "โŒ {} {err}", - style("[API QUERY FAILURE]: ").bold().dim() - )); - continue; - } - }; - - // if we explicitly specified some identity, find THIS node - if let Some(identity) = ctx.gateway.as_ref() { - if let Some(node) = nodes - .iter() - .find(|gw| &gw.ed25519_identity_pubkey.to_base58_string() == identity) - { - return SocketAddr::new( - node.ip_addresses[0], - node.entry.clone().unwrap().ws_port, - ); - } - } - - // otherwise look for ANY node - if let Some(node) = nodes.iter().find(|n| n.supported_roles.entry) { - return SocketAddr::new( - node.ip_addresses[0], - node.entry.as_ref().unwrap().ws_port, - ); - } - - sleep(Duration::from_secs(10)).await; - } - }; - tokio::time::timeout(Duration::from_secs(240), inner_fut).await - }; - - match ctx.async_with_progress(wait_fut).await { - Ok(endpoint) => { - ctx.println(format!( - "\twe finally got a gateway in the directory! it's at: {endpoint}" - )); - Ok(endpoint) - } - Err(_) => Err(NetworkManagerError::ApiGatewayWaitTimeout), - } - } - - async fn wait_for_gateway_endpoint( - &self, - ctx: &LocalClientCtx<'_>, - gateway: SocketAddr, - ) -> Result<(), NetworkManagerError> { - ctx.set_pb_message(format!( - "โŒ›waiting for gateway at {gateway} to start receiving traffic..." - )); - - let wait_fut = async { - let inner_fut = async { - loop { - if TcpStream::connect(gateway).await.is_ok() { - break; - } - sleep(Duration::from_secs(10)).await; - } - }; - tokio::time::timeout(Duration::from_secs(240), inner_fut).await - }; - - if ctx.async_with_progress(wait_fut).await.is_err() { - return Err(NetworkManagerError::GatewayWaitTimeout); - } - - ctx.println(format!( - "\tthe gateway at {gateway} has finally come online" - )); - - Ok(()) - } - - async fn wait_for_gateway(&self, ctx: &LocalClientCtx<'_>) -> Result<(), NetworkManagerError> { - let endpoint = self.wait_for_api_gateway(ctx).await?; - self.wait_for_gateway_endpoint(ctx, endpoint).await - } - - async fn prepare_nym_client( - &self, - ctx: &LocalClientCtx<'_>, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ” {}Initialising local nym-client...", - style("[1/1]").bold().dim() - )); - - let env = ctx.network.default_env_file_path(); - let id = &ctx.client_id; - - self.wait_for_gateway(ctx).await?; - let mut rng = thread_rng(); - let mut port = rng.next_u32(); - port = (port + 1000) % (u16::MAX as u32); - - ctx.set_pb_message(format!("initialising client {id}...")); - ctx.println(format!("\tinitialising client {id}...")); - let mut cmd = Command::new(&ctx.nym_client_binary); - cmd.args([ - "-c", - &env.display().to_string(), - "init", - "--id", - id, - "--enabled-credentials-mode", - "true", - "--minimum-gateway-performance", - "0", - "--port", - &port.to_string(), - ]) - // .stdout(Stdio::null()) - .stdin(Stdio::null()) - // .stderr(Stdio::null()) - .kill_on_drop(true); - - if let Some(gateway) = &ctx.gateway { - cmd.args(["--gateway", gateway]); - } - - let mut child = cmd.spawn()?; - - let child_fut = child.wait(); - let out = ctx.async_with_progress(child_fut).await?; - if !out.success() { - return Err(NetworkManagerError::NymClientExecutionFailure); - } - - ctx.println(format!("\tupdating client {id} config...")); - - let config_path = self.nym_client_config(id); - let mut config_file = OpenOptions::new().append(true).open(config_path)?; - - // make the client ignore the performance of the nodes since we're not running network monitor - writeln!( - config_file, - r#" - - [debug.topology] - minimum_mixnode_performance = 0 - minimum_gateway_performance = 0 - "# - )?; - - ctx.println(format!("\tโœ…client {id} is ready to use!")); - - Ok(()) - } - - fn prepare_client_run_command( - &self, - ctx: &LocalClientCtx, - ) -> Result { - let env_file = ctx.network.default_env_file_path(); - - let bin_canon = fs::canonicalize(&ctx.nym_client_binary)?; - let env_canon = fs::canonicalize(env_file)?; - let bin_canon_display = bin_canon.display(); - let env_canon_display = env_canon.display(); - - let id = &ctx.client_id; - - Ok(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id}" - )) - } - - pub(crate) async fn init_local_nym_client>( - &self, - nym_client_binary: P, - network: &LoadedNetwork, - gateway: Option, - ) -> Result { - let ctx = LocalClientCtx::new(nym_client_binary.as_ref().to_path_buf(), gateway, network)?; - - let env_file = ctx.network.default_env_file_path(); - if !env_file.exists() { - return Err(NetworkManagerError::EnvFileNotGenerated); - } - - self.prepare_nym_client(&ctx).await?; - let cmd = self.prepare_client_run_command(&ctx)?; - - ctx.println("๐Ÿ‡ run the binary with the following commands:"); - ctx.println(&cmd); - - Ok(cmd) - } -} diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs deleted file mode 100644 index 3ed0af70b36..00000000000 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ /dev/null @@ -1,656 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; -use crate::manager::NetworkManager; -use crate::manager::network::LoadedNetwork; -use crate::manager::node::NymNode; -use console::style; -use nym_crypto::asymmetric::ed25519; -use nym_mixnet_contract_common::RoleAssignment; -use nym_mixnet_contract_common::nym_node::Role; -use nym_validator_client::DirectSigningHttpRpcNyxdClient; -use nym_validator_client::nyxd::contract_traits::{ - MixnetQueryClient, MixnetSigningClient, PagedMixnetQueryClient, -}; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::ops::Deref; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use time::OffsetDateTime; -use tokio::process::Command; -use tokio::time::sleep; -use tracing::error; -use zeroize::Zeroizing; - -struct LocalNodesCtx<'a> { - nym_node_binary: PathBuf, - - progress: ProgressTracker, - network: &'a LoadedNetwork, - admin: DirectSigningHttpRpcNyxdClient, - - mix_nodes: Vec, - gateways: Vec, -} - -impl ProgressCtx for LocalNodesCtx<'_> { - fn progress_tracker(&self) -> &ProgressTracker { - &self.progress - } -} - -impl<'a> LocalNodesCtx<'a> { - fn nym_node_id(&self, node: &NymNode) -> String { - format!("{}-{}", self.network.name, node.owner.address) - } - - fn new( - nym_node_binary: PathBuf, - network: &'a LoadedNetwork, - admin_mnemonic: bip39::Mnemonic, - ) -> Result { - let progress = ProgressTracker::new(format!( - "\n๐Ÿš€ setting up new local nym-nodes for network '{}' over {}", - network.name, network.rpc_endpoint - )); - - Ok(LocalNodesCtx { - nym_node_binary, - network, - admin: DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - network.client_config()?, - network.rpc_endpoint.as_str(), - admin_mnemonic, - )?, - mix_nodes: Vec::new(), - progress, - gateways: Vec::new(), - }) - } - - fn signing_node_owner( - &self, - node: &NymNode, - ) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - self.network.client_config()?, - self.network.rpc_endpoint.as_str(), - node.owner.mnemonic.clone(), - )?) - } - - fn signing_rewarder(&self) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - self.network.client_config()?, - self.network.rpc_endpoint.as_str(), - self.network - .auxiliary_addresses - .mixnet_rewarder - .mnemonic - .clone(), - )?) - } - - fn signing_mixnet_contract_admin( - &self, - ) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - self.network.client_config()?, - self.network.rpc_endpoint.as_str(), - self.network.contracts.mixnet.admin_mnemonic.clone(), - )?) - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct BondingInformation { - host: String, - identity_key: ed25519::PublicKey, -} - -#[derive(Deserialize)] -struct ReducedSignatureOut { - encoded_signature: String, -} - -impl NetworkManager { - async fn initialise_nym_node( - &self, - ctx: &mut LocalNodesCtx<'_>, - offset: u16, - is_gateway: bool, - ) -> Result<(), NetworkManagerError> { - let mut node = NymNode::new_empty(); - let env = ctx.network.default_env_file_path(); - let id = ctx.nym_node_id(&node); - - let output_dir = tempfile::tempdir()?; - let output_file_path = output_dir.path().join("bonding_info.json"); - - ctx.set_pb_message(format!("initialising node {id}...")); - let mix_port = 5000 + offset; - let verloc_port = 6000 + offset; - let clients_port = 7000 + offset; - let http_port = 8000 + offset; - - node.mix_port = mix_port; - node.verloc_port = verloc_port; - node.clients_port = clients_port; - node.http_port = http_port; - - let mut cmd = Command::new(&ctx.nym_node_binary); - cmd.args([ - "-c", - &env.display().to_string(), - "run", - "--id", - &id, - "--init-only", - "--public-ips", - "127.0.0.1", - "--http-bind-address", - &format!("127.0.0.1:{http_port}"), - "--mixnet-bind-address", - &format!("127.0.0.1:{mix_port}"), - "--verloc-bind-address", - &format!("127.0.0.1:{verloc_port}"), - "--entry-bind-address", - &format!("127.0.0.1:{clients_port}"), - "--mixnet-announce-port", - &mix_port.to_string(), - "--verloc-announce-port", - &verloc_port.to_string(), - "--mnemonic", - &Zeroizing::new(node.owner.mnemonic.to_string()), - "--local", - "--accept-operator-terms-and-conditions", - "--output", - "json", - "--bonding-information-output", - &output_file_path.display().to_string(), - ]) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .kill_on_drop(true); - - if is_gateway { - cmd.args(["--mode", "entry"]); - } else { - // be explicit about it, even though we don't have to be - cmd.args(["--mode", "mixnode"]); - } - - let child = cmd.spawn()?; - let child_fut = child.wait_with_output(); - let out = ctx.async_with_progress(child_fut).await?; - if !out.status.success() { - error!("nym node failure"); - println!("{}", String::from_utf8_lossy(&out.stderr)); - return Err(NetworkManagerError::NymNodeExecutionFailure); - } - - let output_file = fs::File::open(&output_file_path)?; - let bonding_info: BondingInformation = serde_json::from_reader(&output_file)?; - - node.identity_key = bonding_info.identity_key.to_string(); - - ctx.set_pb_message(format!("generating bonding signature for node {id}...")); - - let msg = node.bonding_payload(); - - let child = Command::new(&ctx.nym_node_binary) - .args([ - "--no-banner", - "sign", - "--id", - &id, - "--contract-msg", - &msg, - "--output", - "json", - ]) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .kill_on_drop(true) - .output(); - - let out = ctx.async_with_progress(child).await?; - if !out.status.success() { - error!("nym node failure"); - println!("{}", String::from_utf8_lossy(&out.stderr)); - return Err(NetworkManagerError::NymNodeExecutionFailure); - } - let signature: ReducedSignatureOut = serde_json::from_slice(&out.stdout)?; - node.bonding_signature = signature.encoded_signature; - - ctx.println(format!( - "\tinitialised node {} (gateway: {})", - node.identity_key, is_gateway - )); - - if is_gateway { - ctx.gateways.push(node) - } else { - ctx.mix_nodes.push(node) - } - Ok(()) - } - - async fn check_if_network_is_empty( - &self, - ctx: &LocalNodesCtx<'_>, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿฝ {}Making sure the network is fresh...", - style("[0/5]").bold().dim() - )); - - ctx.set_pb_message("checking network state..."); - - let client = ctx.signing_mixnet_contract_admin()?; - let fut = client.get_all_nymnode_bonds(); - let nym_nodes = ctx.async_with_progress(fut).await?; - - if !nym_nodes.is_empty() { - return Err(NetworkManagerError::NetworkNotEmpty); - } - - let fut = client.get_all_mixnode_bonds(); - let mixnodes = ctx.async_with_progress(fut).await?; - if !mixnodes.is_empty() { - return Err(NetworkManagerError::NetworkNotEmpty); - } - - let fut = client.get_all_gateways(); - let gateways = ctx.async_with_progress(fut).await?; - if !gateways.is_empty() { - return Err(NetworkManagerError::NetworkNotEmpty); - } - - Ok(()) - } - - async fn initialise_nym_nodes( - &self, - ctx: &mut LocalNodesCtx<'_>, - mixnodes: u16, - gateways: u16, - ) -> Result<(), NetworkManagerError> { - const OFFSET: u16 = 100; - if mixnodes > OFFSET { - panic!("seriously? over 100 mixnodes?") - } - - ctx.println(format!( - "๐Ÿ” {}Initialising local nym-nodes...", - style("[1/5]").bold().dim() - )); - - for i in 0..mixnodes { - self.initialise_nym_node(ctx, i, false).await?; - } - for i in 0..gateways { - self.initialise_nym_node(ctx, i + OFFSET, true).await?; - } - - ctx.println("\tโœ… all nym nodes got initialised!"); - - Ok(()) - } - - async fn transfer_bonding_tokens( - &self, - ctx: &LocalNodesCtx<'_>, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ’ธ {}Transferring tokens to the bond owners...", - style("[2/5]").bold().dim() - )); - - let mut receivers = Vec::new(); - for node in ctx.mix_nodes.iter().chain(ctx.gateways.iter()) { - // send 101nym to the owner - receivers.push((node.owner.address.clone(), ctx.admin.mix_coins(101_000000))) - } - - ctx.set_pb_message("attempting to send signer tokens..."); - - let send_future = ctx.admin.send_multiple( - receivers, - "bond owners token transfer from testnet-manager", - None, - ); - let res = ctx.async_with_progress(send_future).await?; - - ctx.println(format!( - "\tโœ… sent tokens in transaction: {} (height {})", - res.hash, res.height - )); - Ok(()) - } - - async fn bond_node( - &self, - ctx: &LocalNodesCtx<'_>, - node: &NymNode, - is_gateway: bool, - ) -> Result<(), NetworkManagerError> { - let prefix = if is_gateway { "[gateway]" } else { "[mixnode]" }; - ctx.set_pb_prefix(prefix); - - let id = ctx.nym_node_id(node); - ctx.set_pb_message(format!("attempting to bond node {id}...")); - - let owner = ctx.signing_node_owner(node)?; - - let typ = if is_gateway { - "gateway [as nym-node]" - } else { - "mixnode [as nym-node]" - }; - - let bonding_fut = owner.bond_nymnode( - node.bonding_nym_node(), - node.cost_params(), - node.bonding_signature(), - node.pledge().into(), - None, - ); - - let res = ctx.async_with_progress(bonding_fut).await?; - ctx.println(format!( - "\t{id} ({typ}) bonded in transaction: {}", - res.transaction_hash - )); - - Ok(()) - } - - async fn bond_nym_nodes(&self, ctx: &LocalNodesCtx<'_>) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "โ›“๏ธ {}Bonding the local nym-nodes...", - style("[3/5]").bold().dim() - )); - - for mix_node in &ctx.mix_nodes { - self.bond_node(ctx, mix_node, false).await?; - } - for gateway in &ctx.gateways { - self.bond_node(ctx, gateway, true).await?; - } - - ctx.println("\tโœ… all nym nodes got bonded!"); - - Ok(()) - } - - async fn assign_to_active_set( - &self, - ctx: &LocalNodesCtx<'_>, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ”Œ {}Assigning nodes to the active set...", - style("[4/5]").bold().dim() - )); - - // this could be batched in a single tx, but that's too much effort for now - let rewarder = ctx.signing_rewarder()?; - - ctx.set_pb_message("checking and temporarily adjusting epoch lengths..."); - let fut = rewarder.get_current_interval_details(); - let original_epoch = ctx.async_with_progress(fut).await?; - - let expected_end = original_epoch.interval.current_epoch_end(); - let now = OffsetDateTime::now_utc(); - if expected_end > now { - loop { - let now = OffsetDateTime::now_utc(); - let diff = expected_end - now; - if diff.is_negative() { - break; - } - - let std_diff = diff.unsigned_abs(); - let fut = sleep(std::time::Duration::from_millis(500)); - ctx.set_pb_message(format!( - "waiting for {} for the epoch end...", - humantime::format_duration(std_diff) - )); - ctx.async_with_progress(fut).await; - } - // wait extra 10s due to possible block time desync - ctx.set_pb_message("waiting extra 10s to make sure blocks have advanced".to_string()); - let fut = sleep(std::time::Duration::from_secs(10)); - ctx.async_with_progress(fut).await; - } - - // TODO: for some reason contract rejects correct admin. won't be debugging it now. - // let changed_length = if expected_end > now { - // - // // if it's < 10s, just wait - // let diff = expected_end - now; - // - // if diff < Duration::seconds(10) { - // let std_diff = diff.unsigned_abs(); - // let fut = sleep(std_diff); - // ctx.set_pb_message(format!( - // "waiting for {} for the epoch end...", - // humantime::format_duration(std_diff) - // )); - // ctx.async_with_progress(fut).await; - // false - // } else { - // ctx.println(format!( - // "๐Ÿ™ˆ {}Reducing epoch length...", - // style("[4.pre/5]").bold().dim() - // )); - // - // // just lower the epoch length and later restore it - // let admin = ctx.signing_mixnet_contract_admin()?; - // let fut = admin.update_interval_config( - // original_epoch.interval.epochs_in_interval(), - // 10, - // true, - // None, - // ); - // ctx.async_with_progress(fut).await?; - // let fut = sleep(std::time::Duration::from_secs(10)); - // ctx.set_pb_message("waiting for 10s for the epoch end..."); - // ctx.async_with_progress(fut).await; - // true - // } - // } else { - // false - // }; - - // reduce epoch length if it would prevent us from the advancing the state - - ctx.set_pb_message("starting epoch transition..."); - let fut = rewarder.begin_epoch_transition(None); - ctx.async_with_progress(fut).await?; - - ctx.set_pb_message("reconciling (no) epoch events..."); - let fut = rewarder.reconcile_epoch_events(None, None); - ctx.async_with_progress(fut).await?; - - ctx.set_pb_message("finally assigning the active set... exit..."); - let fut = rewarder.assign_roles( - RoleAssignment { - role: Role::ExitGateway, - nodes: vec![], - }, - None, - ); - ctx.async_with_progress(fut).await?; - - ctx.set_pb_message("finally assigning the active set... entry..."); - let fut = rewarder.assign_roles( - RoleAssignment { - role: Role::EntryGateway, - nodes: vec![4], - }, - None, - ); - ctx.async_with_progress(fut).await?; - - ctx.set_pb_message("finally assigning the active set... layer1..."); - let fut = rewarder.assign_roles( - RoleAssignment { - role: Role::Layer1, - nodes: vec![1], - }, - None, - ); - ctx.async_with_progress(fut).await?; - - ctx.set_pb_message("finally assigning the active set... layer2..."); - let fut = rewarder.assign_roles( - RoleAssignment { - role: Role::Layer2, - nodes: vec![2], - }, - None, - ); - ctx.async_with_progress(fut).await?; - - ctx.set_pb_message("finally assigning the active set... layer3..."); - let fut = rewarder.assign_roles( - RoleAssignment { - role: Role::Layer3, - nodes: vec![3], - }, - None, - ); - ctx.async_with_progress(fut).await?; - - ctx.set_pb_message("finally assigning the active set... [empty] standby..."); - let fut = rewarder.assign_roles( - RoleAssignment { - role: Role::Standby, - nodes: vec![], - }, - None, - ); - ctx.async_with_progress(fut).await?; - - // TODO: for some reason contract rejects correct admin. won't be debugging it now. - // if changed_length { - // ctx.println(format!( - // "๐Ÿ™ˆ {}Restoring epoch length...", - // style("[4.post/5]").bold().dim() - // )); - // ctx.set_pb_message("restoring original epoch length..."); - // let admin = ctx.signing_mixnet_contract_admin()?; - // let fut = admin.update_interval_config( - // original_epoch.interval.epochs_in_interval(), - // original_epoch.interval.epoch_length_secs(), - // true, - // None, - // ); - // ctx.async_with_progress(fut).await?; - // } - - Ok(()) - } - - fn prepare_nym_nodes_run_commands( - &self, - ctx: &LocalNodesCtx, - ) -> Result { - let env_file = ctx.network.default_env_file_path(); - - let bin_canon = fs::canonicalize(&ctx.nym_node_binary)?; - let env_canon = fs::canonicalize(env_file)?; - let bin_canon_display = bin_canon.display(); - let env_canon_display = env_canon.display(); - - let mut cmds = Vec::new(); - for mixnode in ctx.mix_nodes.iter() { - ctx.println(format!( - "\tpreparing node {} (mixnode)", - mixnode.identity_key - )); - let id = ctx.nym_node_id(mixnode); - cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" - )); - } - - for gateway in ctx.gateways.iter() { - ctx.println(format!( - "\tpreparing node {} (gateway)", - gateway.identity_key - )); - let id = ctx.nym_node_id(gateway); - cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" - )); - } - - Ok(RunCommands(cmds)) - } - - fn output_nym_nodes_run_commands(&self, ctx: &LocalNodesCtx, cmds: &RunCommands) { - ctx.progress.output_run_commands(cmds) - } - - async fn persist_nodes_in_database( - &self, - ctx: &LocalNodesCtx<'_>, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ“ฆ {}Storing the node information in the database", - style("[5/5]").bold().dim() - )); - - ctx.set_pb_message("attempting to persist node information..."); - let mix_save_future = self - .storage - .persist_mixnodes(&ctx.mix_nodes, ctx.network.id); - let gw_save_future = self.storage.persist_gateways(&ctx.gateways, ctx.network.id); - ctx.async_with_progress(mix_save_future).await?; - ctx.async_with_progress(gw_save_future).await?; - - ctx.println( - "\tโœ… the bonded node information got persisted in the database for future use", - ); - - Ok(()) - } - - pub(crate) async fn init_local_nym_nodes>( - &self, - nym_node_binary: P, - network: &LoadedNetwork, - mixnodes: u16, - gateways: u16, - ) -> Result { - let mut ctx = LocalNodesCtx::new( - nym_node_binary.as_ref().to_path_buf(), - network, - self.admin.deref().clone(), - )?; - - let env_file = ctx.network.default_env_file_path(); - if !env_file.exists() { - return Err(NetworkManagerError::EnvFileNotGenerated); - } - - self.check_if_network_is_empty(&ctx).await?; - self.initialise_nym_nodes(&mut ctx, mixnodes, gateways) - .await?; - self.transfer_bonding_tokens(&ctx).await?; - self.bond_nym_nodes(&ctx).await?; - self.assign_to_active_set(&ctx).await?; - self.persist_nodes_in_database(&ctx).await?; - let cmds = self.prepare_nym_nodes_run_commands(&ctx)?; - self.output_nym_nodes_run_commands(&ctx, &cmds); - - Ok(cmds) - } -} diff --git a/tools/internal/testnet-manager/src/manager/mod.rs b/tools/internal/testnet-manager/src/manager/mod.rs deleted file mode 100644 index 12b657bf246..00000000000 --- a/tools/internal/testnet-manager/src/manager/mod.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::NetworkManagerError; -use crate::helpers::{async_with_progress, default_storage_dir, wasm_code}; -use crate::manager::network::LoadedNetwork; -use crate::manager::storage::NetworkManagerStorage; -use bip39::rand::prelude::SliceRandom; -use bip39::rand::thread_rng; -use indicatif::ProgressBar; -use nym_config::defaults::NymNetworkDetails; -use nym_validator_client::nyxd::Config; -use nym_validator_client::nyxd::cosmwasm_client::types::UploadResult; -use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; -use std::path::{Path, PathBuf}; -use url::Url; -use zeroize::Zeroizing; - -mod contract; -mod dkg_skip; -pub(crate) mod env; -mod local_apis; -mod local_client; -mod local_nodes; -pub(crate) mod network; -mod network_init; -mod node; -pub(crate) mod storage; - -pub(crate) struct NetworkManager { - admin: Zeroizing, - storage: NetworkManagerStorage, - rpc_endpoint: Url, -} - -impl NetworkManager { - pub(crate) async fn new>( - database_path: P, - mnemonic: Option, - rpc_endpoint: Option, - ) -> Result { - let storage = NetworkManagerStorage::init(database_path).await?; - - let (mnemonic, rpc_endpoint) = if !storage.metadata_set().await? { - let mnemonic = mnemonic.ok_or(NetworkManagerError::MnemonicNotSet)?; - let rpc_endpoint = rpc_endpoint.ok_or(NetworkManagerError::RpcEndpointNotSet)?; - - storage - .set_initial_metadata(&mnemonic, &rpc_endpoint) - .await?; - (mnemonic, rpc_endpoint) - } else { - let mnemonic = storage - .get_master_mnemonic() - .await? - .ok_or(NetworkManagerError::MnemonicNotSet)?; - - let rpc_endpoint = storage - .get_rpc_endpoint() - .await? - .ok_or(NetworkManagerError::RpcEndpointNotSet)?; - - (mnemonic, rpc_endpoint) - }; - - Ok(NetworkManager { - admin: Zeroizing::new(mnemonic), - storage, - rpc_endpoint, - }) - } - - pub fn default_latest_env_file_path(&self) -> PathBuf { - default_storage_dir().join("latest.env") - } - - #[allow(unused)] - pub(crate) fn query_client( - &self, - network: &LoadedNetwork, - ) -> Result { - let network_details = NymNetworkDetails::from(network); - let config = Config::try_from_nym_network_details(&network_details)?; - - Ok(QueryHttpRpcNyxdClient::connect( - config, - self.rpc_endpoint.as_str(), - )?) - } - - fn get_network_name(&self, user_provided: Option) -> String { - user_provided.unwrap_or_else(|| { - // a hack to get human-readable words without extra deps : ) - let mut rng = thread_rng(); - - let words = bip39::Language::English.word_list(); - let first = words.choose(&mut rng).unwrap(); - let second = words.choose(&mut rng).unwrap(); - format!("{first}-{second}") - }) - } - - async fn upload_contract>( - &self, - admin: &DirectSigningHttpRpcNyxdClient, - pb: &ProgressBar, - path: P, - ) -> Result { - let wasm = wasm_code(path)?; - let upload_future = admin.upload(wasm, "contract upload from testnet-manager", None); - - async_with_progress(upload_future, pb) - .await - .map_err(Into::into) - } - - pub(crate) async fn load_existing_network( - &self, - network_name: Option, - ) -> Result { - let network_name = if let Some(explicit) = network_name { - explicit - } else { - self.storage.get_latest_network_name().await? - }; - - self.storage.try_load_network(&network_name).await - } -} diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs deleted file mode 100644 index 39e04f10cf6..00000000000 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::NetworkManagerError; -use crate::helpers::default_storage_dir; -use crate::manager::contract::{Account, LoadedNymContracts, NymContracts}; -use nym_config::defaults::{NymNetworkDetails, ValidatorDetails}; -use nym_validator_client::nyxd::Config; -use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use time::OffsetDateTime; -use url::Url; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Network { - pub name: String, - - pub rpc_endpoint: Url, - - #[serde(with = "time::serde::rfc3339")] - pub created_at: OffsetDateTime, - - pub contracts: NymContracts, - - pub auxiliary_addresses: SpecialAddresses, -} - -impl Network { - pub fn into_loaded(self) -> LoadedNetwork { - self.into() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) struct LoadedNetwork { - pub(crate) id: i64, - pub(crate) name: String, - - pub(crate) rpc_endpoint: Url, - - #[serde(with = "time::serde::rfc3339")] - pub(crate) created_at: OffsetDateTime, - - pub(crate) contracts: LoadedNymContracts, - - pub(crate) auxiliary_addresses: SpecialAddresses, -} - -impl From for LoadedNetwork { - fn from(value: Network) -> Self { - LoadedNetwork { - id: i64::MAX, - name: value.name, - rpc_endpoint: value.rpc_endpoint, - created_at: value.created_at, - contracts: value.contracts.into(), - auxiliary_addresses: value.auxiliary_addresses, - } - } -} - -impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { - fn from(value: &'a LoadedNetwork) -> Self { - let contracts = nym_config::defaults::NymContracts { - mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()), - vesting_contract_address: Some(value.contracts.vesting.address.to_string()), - performance_contract_address: Some(value.contracts.performance.address.to_string()), - ecash_contract_address: Some(value.contracts.ecash.address.to_string()), - group_contract_address: Some(value.contracts.cw4_group.address.to_string()), - multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()), - coconut_dkg_contract_address: Some(value.contracts.dkg.address.to_string()), - }; - // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet - let mainnet = NymNetworkDetails::new_mainnet(); - NymNetworkDetails { - chain_details: mainnet.chain_details, - network_name: "foomp".to_string(), - endpoints: vec![ValidatorDetails { - nyxd_url: value.rpc_endpoint.to_string(), - websocket_url: None, - api_url: None, - }], - contracts, - nym_vpn_api_url: None, - nym_vpn_api_urls: None, - nym_api_urls: None, - } - } -} - -impl LoadedNetwork { - pub fn default_env_file_path(&self) -> PathBuf { - default_storage_dir() - .join(&self.name) - .join(format!("{}.env", &self.name)) - } - - #[allow(dead_code)] - pub fn query_client(&self) -> Result { - Ok(QueryHttpRpcNyxdClient::connect( - self.client_config()?, - self.rpc_endpoint.as_str(), - )?) - } - - pub fn dkg_signing_client( - &self, - ) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - self.client_config()?, - self.rpc_endpoint.as_str(), - self.contracts.dkg.admin_mnemonic.clone(), - )?) - } - - pub fn client_config(&self) -> Result { - let network_details = NymNetworkDetails::from(self); - let config = Config::try_from_nym_network_details(&network_details)?; - Ok(config) - } - - pub fn cw4_group_signing_client( - &self, - ) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - self.client_config()?, - self.rpc_endpoint.as_str(), - self.contracts.cw4_group.admin_mnemonic.clone(), - )?) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SpecialAddresses { - pub ecash_holding_account: Account, - pub mixnet_rewarder: Account, - pub network_monitors: Vec, -} - -impl Default for SpecialAddresses { - fn default() -> Self { - SpecialAddresses { - ecash_holding_account: Account::new(), - mixnet_rewarder: Account::new(), - // by default use one address; to be adjusted in the future - network_monitors: vec![Account::new()], - } - } -} diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs deleted file mode 100644 index 701e57406e5..00000000000 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ /dev/null @@ -1,772 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::NetworkManagerError; -use crate::helpers::{ProgressCtx, ProgressTracker, async_with_progress}; -use crate::manager::NetworkManager; -use crate::manager::contract::Account; -use crate::manager::network::Network; -use console::style; -use cw_utils::Threshold; -use indicatif::HumanDuration; -use nym_coconut_dkg_common::types::TimeConfiguration; -use nym_config::defaults::NymNetworkDetails; -use nym_mixnet_contract_common::reward_params::RewardedSetParams; -use nym_mixnet_contract_common::{Decimal, InitialRewardingParams, Percent}; -use nym_validator_client::DirectSigningHttpRpcNyxdClient; -use nym_validator_client::nyxd::Config; -use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; -use std::ops::Deref; -use std::path::Path; -use std::time::Duration; -use time::OffsetDateTime; -use time::format_description::well_known::Rfc3339; -use tracing::error; -use url::Url; - -struct InitCtx { - progress: ProgressTracker, - network: Network, - admin: DirectSigningHttpRpcNyxdClient, -} - -impl InitCtx { - fn dummy_client_config() -> Result { - // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet - let mainnet = NymNetworkDetails::new_mainnet(); - let network_details = NymNetworkDetails { - chain_details: mainnet.chain_details, - network_name: "foomp".to_string(), // does this matter? - endpoints: vec![], - contracts: Default::default(), - nym_vpn_api_url: None, - nym_vpn_api_urls: None, - nym_api_urls: None, - }; - Ok(Config::try_from_nym_network_details(&network_details)?) - } - - fn new( - network_name: String, - admin_mnemonic: bip39::Mnemonic, - rpc_endpoint: &Url, - ) -> Result { - let admin = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - Self::dummy_client_config()?, - rpc_endpoint.as_str(), - admin_mnemonic, - )?; - - let progress = ProgressTracker::new(format!( - "\n๐Ÿš€ setting up new testnet '{network_name}' over {rpc_endpoint}", - )); - - Ok(InitCtx { - progress, - network: Network { - name: network_name, - rpc_endpoint: rpc_endpoint.clone(), - created_at: OffsetDateTime::now_utc(), - contracts: Default::default(), - auxiliary_addresses: Default::default(), - }, - admin, - }) - } - - fn mixnet_signing_client(&self) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - Self::dummy_client_config()?, - self.network.rpc_endpoint.as_str(), - self.network.contracts.mixnet.admin()?.mnemonic.clone(), - )?) - } - - fn multisig_signing_client( - &self, - ) -> Result { - Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - Self::dummy_client_config()?, - self.network.rpc_endpoint.as_str(), - self.network - .contracts - .cw3_multisig - .admin()? - .mnemonic - .clone(), - )?) - } -} - -impl ProgressCtx for InitCtx { - fn progress_tracker(&self) -> &ProgressTracker { - &self.progress - } -} - -impl NetworkManager { - fn mixnet_migrate_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_mixnet_contract_common::MigrateMsg { - vesting_contract_address: Some(ctx.network.contracts.vesting.address()?.to_string()), - unsafe_skip_state_updates: Some(true), - }) - } - - fn multisig_migrate_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_multisig_contract_common::msg::MigrateMsg { - coconut_bandwidth_address: ctx.network.contracts.ecash.address()?.to_string(), - coconut_dkg_address: ctx.network.contracts.dkg.address()?.to_string(), - }) - } - - fn mixnet_init_message( - &self, - ctx: &InitCtx, - custom_epoch_duration: Option, - key_validity_in_epochs: Option, - ) -> Result { - Ok(nym_mixnet_contract_common::InstantiateMsg { - rewarding_validator_address: ctx - .network - .auxiliary_addresses - .mixnet_rewarder - .address - .to_string(), - // PLACEHOLDER \/ - vesting_contract_address: ctx - .network - .auxiliary_addresses - .mixnet_rewarder - .address - .to_string(), - // PLACEHOLDER /\ - rewarding_denom: ctx.admin.mix_coin(0).denom, - epochs_in_interval: 720, - epoch_duration: custom_epoch_duration.unwrap_or(Duration::from_secs(60 * 60)), - initial_rewarding_params: InitialRewardingParams { - initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0).unwrap(), - initial_staking_supply: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), - staking_supply_scale_factor: Percent::from_percentage_value(50).unwrap(), - sybil_resistance: Percent::from_percentage_value(30).unwrap(), - active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), - interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_params: RewardedSetParams { - entry_gateways: 70, - exit_gateways: 50, - mixnodes: 120, - standby: 0, - }, - }, - current_nym_node_version: "1.1.10".to_string(), - version_score_weights: Default::default(), - version_score_params: Default::default(), - profit_margin: Default::default(), - interval_operating_cost: Default::default(), - key_validity_in_epochs, - }) - } - - fn vesting_init_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_vesting_contract_common::InitMsg { - mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), - mix_denom: ctx.admin.mix_coin(0).denom, - }) - } - - fn dkg_init_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_coconut_dkg_common::msg::InstantiateMsg { - group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), - multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), - time_configuration: Some(TimeConfiguration { - public_key_submission_time_secs: 3600, - dealing_exchange_time_secs: 3600, - verification_key_submission_time_secs: 3600, - verification_key_validation_time_secs: 3600, - verification_key_finalization_time_secs: 3600, - in_progress_time_secs: 10000000000, - }), - mix_denom: ctx.admin.mix_coin(0).denom, - key_size: 5, - }) - } - - fn ecash_init_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_ecash_contract_common::msg::InstantiateMsg { - holding_account: ctx - .network - .auxiliary_addresses - .ecash_holding_account - .address - .to_string(), - multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), - group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), - deposit_amount: ctx.admin.mix_coin(75_000_000).into(), - }) - } - - fn cw3_multisig_init_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_multisig_contract_common::msg::InstantiateMsg { - group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), - - // PLACEHOLDER \/ - coconut_bandwidth_contract_address: ctx - .network - .contracts - .cw4_group - .address()? - .to_string(), - coconut_dkg_contract_address: ctx.network.contracts.cw4_group.address()?.to_string(), - // PLACEHOLDER /\ - threshold: Threshold::AbsolutePercentage { - percentage: "0.67".parse().unwrap(), - }, - max_voting_period: cw_utils::Duration::Time(3600), - executor: None, - proposal_deposit: None, - }) - } - - fn cw4_group_init_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_group_contract_common::msg::InstantiateMsg { - admin: Some( - ctx.network - .contracts - .cw4_group - .admin()? - .address() - .to_string(), - ), - // TODO: prepopulate - members: vec![], - }) - } - - fn performance_init_message( - &self, - ctx: &InitCtx, - ) -> Result { - Ok(nym_performance_contract_common::msg::InstantiateMsg { - mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), - authorised_network_monitors: ctx - .network - .auxiliary_addresses - .network_monitors - .iter() - .map(|acc| acc.address.to_string()) - .collect(), - }) - } - - fn find_contracts>( - &self, - ctx: &mut InitCtx, - base_dir: P, - ) -> Result<(), NetworkManagerError> { - ctx.network.contracts.discover_paths(base_dir)?; - - ctx.println(format!( - "๐Ÿ” {}Locating .wasm files...", - style("[1/8]").bold().dim() - )); - ctx.println(format!( - "\tdiscovered mixnet contract at '{}'", - ctx.network.contracts.mixnet.wasm_path()?.display() - )); - ctx.println(format!( - "\tdiscovered vesting contract at '{}'", - ctx.network.contracts.vesting.wasm_path()?.display() - )); - ctx.println(format!( - "\tdiscovered ecash contract at '{}'", - ctx.network.contracts.ecash.wasm_path()?.display() - )); - ctx.println(format!( - "\tdiscovered cw4_group contract at '{}'", - ctx.network.contracts.cw4_group.wasm_path()?.display() - )); - ctx.println(format!( - "\tdiscovered cw3_multisig contract at '{}'", - ctx.network.contracts.cw3_multisig.wasm_path()?.display() - )); - ctx.println(format!( - "\tdiscovered dkg contract at '{}'", - ctx.network.contracts.dkg.wasm_path()?.display() - )); - ctx.println(format!( - "\tdiscovered performance contract at '{}'", - ctx.network.contracts.performance.wasm_path()?.display() - )); - - ctx.println("\tโœ… found all the contracts!"); - - Ok(()) - } - - async fn upload_contracts(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿšš {}Uploading contracts...", - style("[2/8]").bold().dim() - )); - - let total = ctx.network.contracts.count() as u64; - let pb = &ctx.progress.progress_bar; - - for (progress, contract) in ctx - .network - .contracts - .fake_iter_mut() - .into_iter() - .enumerate() - { - pb.set_prefix(format!("[{}/{total}]", progress + 1)); - let name = &contract.name; - pb.set_message(format!("uploading {name} contract...")); - let upload_res = self - .upload_contract( - &ctx.admin, - &ctx.progress.progress_bar, - &contract.wasm_path()?, - ) - .await?; - pb.println(format!( - "\t{name} contract uploaded with code: {}", - upload_res.code_id - )); - contract.upload_info = Some(upload_res.into()); - } - - ctx.println("\tโœ… uploaded all the contracts!"); - - Ok(()) - } - - fn create_contract_admins_mnemonics( - &self, - ctx: &mut InitCtx, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ“ {}Generating admin mnemonics...", - style("[3/8]").bold().dim() - )); - - let total = ctx.network.contracts.count() as u64; - let pb = &ctx.progress.progress_bar; - for (progress, contract) in ctx - .network - .contracts - .fake_iter_mut() - .into_iter() - .enumerate() - { - pb.set_prefix(format!("[{}/{total}]", progress + 1)); - let name = &contract.name; - pb.set_message(format!("generating admin mnemonic for {name} contract...")); - let admin = Account::new(); - pb.println(format!( - "\t{} is going to be admin for the {name} contract", - admin.address - )); - contract.admin = Some(admin) - } - - ctx.println("\tโœ… generated all admin mnemonics!"); - - Ok(()) - } - - async fn transfer_admin_tokens(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ’ธ {}Transferring tokens to the admin accounts...", - style("[4/8]").bold().dim() - )); - - let mut receivers = Vec::new(); - for contract in ctx.network.contracts.fake_iter() { - // send 10nym to the admin - receivers.push((contract.admin()?.address(), ctx.admin.mix_coins(10_000000))) - } - - // also send them to the rewarder - receivers.push(( - ctx.network.auxiliary_addresses.mixnet_rewarder.address(), - ctx.admin.mix_coins(10_000000), - )); - - // and to any network monitors - for network_monitor in &ctx.network.auxiliary_addresses.network_monitors { - receivers.push((network_monitor.address(), ctx.admin.mix_coins(10_000000))) - } - - ctx.set_pb_message("attempting to send admin tokens..."); - - let send_future = - ctx.admin - .send_multiple(receivers, "admin token transfer from testnet-manager", None); - let res = ctx.async_with_progress(send_future).await?; - - ctx.println(format!( - "\tโœ… sent tokens in transaction: {} (height {})", - res.hash, res.height - )); - - Ok(()) - } - - async fn instantiate_contracts( - &self, - ctx: &mut InitCtx, - custom_epoch_duration: Option, - key_validity_in_epochs: Option, - ) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ’ฝ {}Instantiating all the contracts...", - style("[5/8]").bold().dim() - )); - - let total = ctx.network.contracts.count() as u64; - - // mixnet - ctx.set_pb_prefix(format!("[1/{total}]")); - let name = &ctx.network.contracts.mixnet.name; - let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; - let admin = ctx.network.contracts.mixnet.admin()?.address.clone(); - ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = - self.mixnet_init_message(ctx, custom_epoch_duration, key_validity_in_epochs)?; - let init_fut = ctx.admin.instantiate( - code_id, - &init_msg, - format!("{name} contract"), - "contract instantiation from testnet-manager", - Some(InstantiateOptions::default().with_admin(admin)), - None, - ); - let res = ctx.async_with_progress(init_fut).await?; - let address = &res.contract_address; - ctx.println(format!( - "\t{name} contract instantiated with address: {address}", - )); - ctx.network.contracts.mixnet.init_info = Some(res.into()); - - // vesting - ctx.set_pb_prefix(format!("[2/{total}]")); - let name = &ctx.network.contracts.vesting.name; - let code_id = ctx.network.contracts.vesting.upload_info()?.code_id; - let admin = ctx.network.contracts.vesting.admin()?.address.clone(); - ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = self.vesting_init_message(ctx)?; - let init_fut = ctx.admin.instantiate( - code_id, - &init_msg, - format!("{name} contract"), - "contract instantiation from testnet-manager", - Some(InstantiateOptions::default().with_admin(admin)), - None, - ); - let res = ctx.async_with_progress(init_fut).await?; - let address = &res.contract_address; - ctx.println(format!( - "\t{name} contract instantiated with address: {address}", - )); - ctx.network.contracts.vesting.init_info = Some(res.into()); - - // group - ctx.set_pb_prefix(format!("[3/{total}]")); - let name = &ctx.network.contracts.cw4_group.name; - let code_id = ctx.network.contracts.cw4_group.upload_info()?.code_id; - let admin = ctx.network.contracts.cw4_group.admin()?.address.clone(); - ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = self.cw4_group_init_message(ctx)?; - let init_fut = ctx.admin.instantiate( - code_id, - &init_msg, - format!("{name} contract"), - "contract instantiation from testnet-manager", - Some(InstantiateOptions::default().with_admin(admin)), - None, - ); - let res = ctx.async_with_progress(init_fut).await?; - let address = &res.contract_address; - ctx.println(format!( - "\t{name} contract instantiated with address: {address}", - )); - ctx.network.contracts.cw4_group.init_info = Some(res.into()); - - // multisig - ctx.set_pb_prefix(format!("[4/{total}]")); - let name = &ctx.network.contracts.cw3_multisig.name; - let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; - let admin = ctx.network.contracts.cw3_multisig.admin()?.address.clone(); - ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = self.cw3_multisig_init_message(ctx)?; - let init_fut = ctx.admin.instantiate( - code_id, - &init_msg, - format!("{name} contract"), - "contract instantiation from testnet-manager", - Some(InstantiateOptions::default().with_admin(admin)), - None, - ); - let res = ctx.async_with_progress(init_fut).await?; - let address = &res.contract_address; - ctx.println(format!( - "\t{name} contract instantiated with address: {address}", - )); - ctx.network.contracts.cw3_multisig.init_info = Some(res.into()); - - // dkg - ctx.set_pb_prefix(format!("[5/{total}]")); - let name = &ctx.network.contracts.dkg.name; - let code_id = ctx.network.contracts.dkg.upload_info()?.code_id; - let admin = ctx.network.contracts.dkg.admin()?.address.clone(); - ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = self.dkg_init_message(ctx)?; - let init_fut = ctx.admin.instantiate( - code_id, - &init_msg, - format!("{name} contract"), - "contract instantiation from testnet-manager", - Some(InstantiateOptions::default().with_admin(admin)), - None, - ); - let res = ctx.async_with_progress(init_fut).await?; - let address = &res.contract_address; - ctx.println(format!( - "\t{name} contract instantiated with address: {address}", - )); - ctx.network.contracts.dkg.init_info = Some(res.into()); - - // ecash - ctx.set_pb_prefix(format!("[6/{total}]")); - let name = &ctx.network.contracts.ecash.name; - let code_id = ctx.network.contracts.ecash.upload_info()?.code_id; - let admin = ctx.network.contracts.ecash.admin()?.address.clone(); - ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = self.ecash_init_message(ctx)?; - let init_fut = ctx.admin.instantiate( - code_id, - &init_msg, - format!("{name} contract"), - "contract instantiation from testnet-manager", - Some(InstantiateOptions::default().with_admin(admin)), - None, - ); - let res = ctx.async_with_progress(init_fut).await?; - let address = &res.contract_address; - ctx.println(format!( - "\t{name} contract instantiated with address: {address}", - )); - ctx.network.contracts.ecash.init_info = Some(res.into()); - - // performance (semi-temp) - ctx.set_pb_prefix(format!("[7/{total}]")); - let name = &ctx.network.contracts.performance.name; - let code_id = ctx.network.contracts.performance.upload_info()?.code_id; - let admin = ctx.network.contracts.performance.admin()?.address.clone(); - ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = self.performance_init_message(ctx)?; - let init_fut = ctx.admin.instantiate( - code_id, - &init_msg, - format!("{name} contract"), - "contract instantiation from testnet-manager", - Some(InstantiateOptions::default().with_admin(admin)), - None, - ); - let res = ctx.async_with_progress(init_fut).await?; - let address = &res.contract_address; - ctx.println(format!( - "\t{name} contract instantiated with address: {address}", - )); - ctx.network.contracts.performance.init_info = Some(res.into()); - - ctx.println("\tโœ… instantiated all the contracts!"); - - Ok(()) - } - - async fn perform_final_migrations(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿงน {}Performing final migrations and contract cleanup...", - style("[6/8]").bold().dim() - )); - - // migrate mixnet - ctx.set_pb_prefix("[1/2]"); - let name = &ctx.network.contracts.mixnet.name; - let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; - let address = ctx.network.contracts.mixnet.address()?; - ctx.set_pb_message(format!("attempting to migrate {name} contract...")); - let migrate_msg = self.mixnet_migrate_message(ctx)?; - let client = ctx.mixnet_signing_client()?; - let migrate_fut = client.migrate( - address, - code_id, - &migrate_msg, - "contract migration from testnet-manager", - None, - ); - let migrate_res = ctx.async_with_progress(migrate_fut).await?; - ctx.network.contracts.mixnet.migrate_info = Some(migrate_res.into()); - ctx.println(format!("\t{name} contract has been migrated")); - - // migrate multisig - ctx.set_pb_prefix("[2/2]"); - let name = &ctx.network.contracts.cw3_multisig.name; - let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; - let address = ctx.network.contracts.cw3_multisig.address()?; - ctx.set_pb_message(format!("attempting to migrate {name} contract...")); - let migrate_msg = self.multisig_migrate_message(ctx)?; - let client = ctx.multisig_signing_client()?; - let migrate_fut = client.migrate( - address, - code_id, - &migrate_msg, - "contract migration from testnet-manager", - None, - ); - let migrate_res = ctx.async_with_progress(migrate_fut).await?; - ctx.network.contracts.cw3_multisig.migrate_info = Some(migrate_res.into()); - ctx.println(format!("\t{name} contract has been migrated")); - - ctx.println("\tโœ… performed all the needed migrations!"); - - Ok(()) - } - - async fn get_build_info(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ—๏ธ {}Obtaining contracts build information", - style("[7/8]").bold().dim() - )); - - let total = ctx.network.contracts.count() as u64; - - let pb = &ctx.progress.progress_bar; - for (progress, contract) in ctx - .network - .contracts - .fake_iter_mut() - .into_iter() - .enumerate() - { - pb.set_prefix(format!("[{}/{total}]", progress + 1)); - let name = &contract.name; - let address = contract.address()?; - pb.set_message(format!("querying {name} contract...")); - let build_info_fut = ctx.admin.try_get_contract_build_information(address); - let build_info = async_with_progress(build_info_fut, &ctx.progress.progress_bar) - .await - .ok_or_else(|| NetworkManagerError::MissingBuildInfo { - name: name.to_string(), - })?; - - let now = OffsetDateTime::now_utc(); - let commit_timestamp = OffsetDateTime::parse(&build_info.commit_timestamp, &Rfc3339) - .inspect_err(|err| { - error!( - "failed to parse contract build information: {err}. set timestamp was: {}", - build_info.commit_timestamp - ) - }) - .unwrap_or(OffsetDateTime::UNIX_EPOCH); - - let age = now - commit_timestamp; - - pb.println(format!( - "\t{name} contract was built from branch: {} (sha: {}); age: {}", - build_info.commit_branch, - build_info.commit_sha, - HumanDuration(age.unsigned_abs()) - )); - - if age > time::Duration::days(30) { - pb.println(format!( - "\t\t๏ธโ˜ ๏ธ๏ธ {}", - style("this commit is ANCIENT - please double check if this is intended") - .bold() - .red() - )) - } else if age > time::Duration::days(7) { - pb.println(format!( - "\t\t๏ธโ—๏ธ {}", - style("this commit is rather old - please double check if this is intended") - .bold() - .red() - )) - } else if age > time::Duration::days(1) { - pb.println(format!( - "\t\t๏ธ๏ธโš ๏ธ {}", - style("this commit seems outdated - please double check if this is intended") - .bold() - .yellow() - )) - } - - contract.build_info = Some(build_info); - } - - ctx.println("\tโœ… updated all contract metadata!"); - - Ok(()) - } - - async fn persist_network_in_database(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { - ctx.println(format!( - "๐Ÿ“ฆ {}Storing all the results in the database", - style("[8/8]").bold().dim() - )); - - ctx.set_pb_message("attempting to persist network data..."); - let save_future = self.storage.persist_network(&ctx.network); - ctx.async_with_progress(save_future).await?; - - ctx.println("\tโœ… the network information got persisted in the database for future use"); - - Ok(()) - } - - pub(crate) async fn initialise_new_network>( - &self, - contracts: P, - network_name: Option, - custom_epoch_duration: Option, - key_validity_in_epochs: Option, - ) -> Result { - let network_name = self.get_network_name(network_name); - let mut ctx = InitCtx::new(network_name, self.admin.deref().clone(), &self.rpc_endpoint)?; - - self.find_contracts(&mut ctx, contracts)?; - self.upload_contracts(&mut ctx).await?; - self.create_contract_admins_mnemonics(&mut ctx)?; - self.transfer_admin_tokens(&ctx).await?; - self.instantiate_contracts(&mut ctx, custom_epoch_duration, key_validity_in_epochs) - .await?; - self.perform_final_migrations(&mut ctx).await?; - self.get_build_info(&mut ctx).await?; - self.persist_network_in_database(&ctx).await?; - - Ok(ctx.network.clone()) - } -} diff --git a/tools/internal/testnet-manager/src/manager/node.rs b/tools/internal/testnet-manager/src/manager/node.rs deleted file mode 100644 index 8eab0c6499e..00000000000 --- a/tools/internal/testnet-manager/src/manager/node.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::manager::contract::Account; -use nym_coconut_dkg_common::types::Addr; -use nym_contracts_common::Percent; -use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{NodeCostParams, construct_nym_node_bonding_sign_payload}; -use nym_validator_client::nyxd::CosmWasmCoin; - -pub(crate) struct NymNode { - // host is always 127.0.0.1 - pub(crate) mix_port: u16, - pub(crate) verloc_port: u16, - pub(crate) http_port: u16, - pub(crate) clients_port: u16, - pub(crate) identity_key: String, - - pub(crate) owner: Account, - pub(crate) bonding_signature: String, -} - -impl NymNode { - pub(crate) fn new_empty() -> NymNode { - NymNode { - mix_port: 0, - verloc_port: 0, - http_port: 0, - clients_port: 0, - identity_key: "".to_string(), - owner: Account::new(), - bonding_signature: "".to_string(), - } - } - - pub(crate) fn pledge(&self) -> CosmWasmCoin { - CosmWasmCoin::new(100_000000u32, "unym") - } - - pub(crate) fn bonding_nym_node(&self) -> nym_mixnet_contract_common::NymNode { - nym_mixnet_contract_common::NymNode { - host: "127.0.0.1".to_string(), - custom_http_port: Some(self.http_port), - identity_key: self.identity_key.clone(), - } - } - - pub(crate) fn cost_params(&self) -> NodeCostParams { - NodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: CosmWasmCoin::new(40_000000u32, "unym"), - } - } - - pub(crate) fn bonding_signature(&self) -> MessageSignature { - // this is a valid bs58 string - self.bonding_signature.parse().unwrap() - } - - pub(crate) fn bonding_payload(&self) -> String { - let payload = construct_nym_node_bonding_sign_payload( - 0, - Addr::unchecked(self.owner.address.to_string()), - self.pledge(), - self.bonding_nym_node(), - self.cost_params(), - ); - payload.to_base58_string().unwrap() - } -} diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs deleted file mode 100644 index 1bd3713d3bd..00000000000 --- a/tools/internal/testnet-manager/src/manager/storage/manager.rs +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only -use crate::manager::storage::models::{ - RawAccount, RawAuthorisedNetworkMonitor, RawContract, RawNetwork, -}; -use time::OffsetDateTime; - -#[derive(Clone)] -pub(crate) struct StorageManager { - pub(crate) connection_pool: sqlx::SqlitePool, -} - -// all SQL goes here -impl StorageManager { - pub(crate) async fn metadata_set(&self) -> Result { - Ok(sqlx::query("SELECT id FROM metadata") - .fetch_optional(&self.connection_pool) - .await? - .is_some()) - } - - pub(crate) async fn get_master_mnemonic(&self) -> Result, sqlx::Error> { - sqlx::query!("SELECT master_mnemonic FROM metadata") - .fetch_optional(&self.connection_pool) - .await - .map(|maybe_record| maybe_record.map(|r| r.master_mnemonic)) - } - - pub(crate) async fn get_rpc_endpoint(&self) -> Result, sqlx::Error> { - sqlx::query!("SELECT rpc_endpoint FROM metadata") - .fetch_optional(&self.connection_pool) - .await - .map(|maybe_record| maybe_record.map(|r| r.rpc_endpoint)) - } - - pub(crate) async fn get_latest_network_id(&self) -> Result, sqlx::Error> { - let maybe_record = sqlx::query!("SELECT latest_network_id FROM metadata") - .fetch_optional(&self.connection_pool) - .await?; - Ok(maybe_record.and_then(|r| r.latest_network_id)) - } - - pub(crate) async fn get_network_name(&self, network_id: i64) -> Result { - sqlx::query!("SELECT name FROM network WHERE id = ?", network_id) - .fetch_one(&self.connection_pool) - .await - .map(|record| record.name) - } - - pub(crate) async fn set_initial_metadata( - &self, - master_mnemonic: &str, - rpc_endpoint: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - "INSERT INTO metadata (id, master_mnemonic, rpc_endpoint) VALUES (0, ?, ?)", - master_mnemonic, - rpc_endpoint - ) - .execute(&self.connection_pool) - .await?; - - Ok(()) - } - - pub(crate) async fn save_latest_network_id( - &self, - latest_network_id: i64, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - "UPDATE metadata SET latest_network_id = ?", - latest_network_id - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - pub(crate) async fn save_network( - &self, - name: &str, - created_at: OffsetDateTime, - mixnet_id: i64, - vesting_id: i64, - ecash_id: i64, - cw3_id: i64, - cw4_id: i64, - dkg_id: i64, - performance_id: i64, - rewarder_address: &str, - ecash_holding_address: &str, - ) -> Result { - let network_id = sqlx::query!( - r#" - INSERT INTO network ( - name, - created_at, - mixnet_contract_id, - vesting_contract_id, - ecash_contract_id, - cw3_multisig_contract_id, - cw4_group_contract_id, - dkg_contract_id, - performance_contract_id, - rewarder_address, - ecash_holding_account_address - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - "#, - name, - created_at, - mixnet_id, - vesting_id, - ecash_id, - cw3_id, - cw4_id, - dkg_id, - performance_id, - rewarder_address, - ecash_holding_address, - ) - .execute(&self.connection_pool) - .await? - .last_insert_rowid(); - Ok(network_id) - } - - pub(crate) async fn load_network(&self, name: &str) -> Result { - sqlx::query_as("SELECT * FROM network WHERE name = ?") - .bind(name) - .fetch_one(&self.connection_pool) - .await - } - - pub(crate) async fn save_contract( - &self, - name: &str, - address: &str, - admin_address: &str, - ) -> Result { - let id = sqlx::query!( - "INSERT INTO contract (name, address, admin_address) VALUES (?, ?, ?)", - name, - address, - admin_address - ) - .execute(&self.connection_pool) - .await? - .last_insert_rowid(); - Ok(id) - } - - pub(crate) async fn load_contract(&self, id: i64) -> Result { - sqlx::query_as( - r#" - SELECT t1.id, t1.name, t1.address, t1.admin_address, t2.mnemonic - FROM contract t1 - JOIN account t2 ON t1.admin_address = t2.address - WHERE t1.id = ?"#, - ) - .bind(id) - .fetch_one(&self.connection_pool) - .await - } - - pub(crate) async fn save_authorised_network_monitor( - &self, - network_id: i64, - address: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - "INSERT INTO authorised_network_monitor (network_id, address) VALUES (?, ?)", - network_id, - address, - ) - .execute(&self.connection_pool) - .await?; - - Ok(()) - } - - pub(crate) async fn load_authorised_network_monitors( - &self, - network_id: i64, - ) -> Result, sqlx::Error> { - sqlx::query_as("SELECT * FROM authorised_network_monitor WHERE network_id = ?") - .bind(network_id) - .fetch_all(&self.connection_pool) - .await - } - - pub(crate) async fn save_account( - &self, - address: &str, - mnemonic: &str, - ) -> Result { - let account_id = sqlx::query!( - "INSERT INTO account (address, mnemonic) VALUES (?, ?)", - address, - mnemonic - ) - .execute(&self.connection_pool) - .await? - .last_insert_rowid(); - Ok(account_id) - } - - pub(crate) async fn load_account(&self, address: &str) -> Result { - sqlx::query_as("SELECT * FROM account WHERE address = ?") - .bind(address) - .fetch_one(&self.connection_pool) - .await - } - - pub(crate) async fn save_node( - &self, - identity_key: &str, - network_id: i64, - bonded_type: &str, - owner_address: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - INSERT INTO node(identity_key, network_id, bonded_type, owner_address) - VALUES (?, ?, ?, ?) - "#, - identity_key, - network_id, - bonded_type, - owner_address - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } -} diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs deleted file mode 100644 index 8067a9ea3c4..00000000000 --- a/tools/internal/testnet-manager/src/manager/storage/mod.rs +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::{ - error::NetworkManagerError, - manager::{ - contract::{Account, Contract, LoadedNymContracts}, - network::{LoadedNetwork, Network, SpecialAddresses}, - node::NymNode, - storage::manager::StorageManager, - }, -}; -use sqlx::{ - ConnectOptions, - sqlite::{SqliteAutoVacuum, SqliteSynchronous}, -}; -use std::fs; -use std::path::Path; -use tracing::{error, info}; -use url::Url; -use zeroize::Zeroizing; - -mod manager; -mod models; - -#[derive(Clone)] -pub(crate) struct NetworkManagerStorage { - manager: StorageManager, -} - -impl NetworkManagerStorage { - pub async fn init>(database_path: P) -> Result { - let database_path = database_path.as_ref(); - info!( - "attempting to initialise storage at {}", - database_path.display() - ); - - if let Some(parent) = database_path.parent() { - fs::create_dir_all(parent)?; - } - - // TODO: we can inject here more stuff based on our nym-api global config - // struct. Maybe different pool size or timeout intervals? - let opts = sqlx::sqlite::SqliteConnectOptions::new() - .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) - .synchronous(SqliteSynchronous::Normal) - .auto_vacuum(SqliteAutoVacuum::Incremental) - .filename(database_path) - .create_if_missing(true) - .disable_statement_logging(); - - // TODO: do we want auto_vacuum ? - - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { - Ok(db) => db, - Err(err) => { - error!("Failed to connect to SQLx database: {err}"); - return Err(err.into()); - } - }; - - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { - error!("Failed to initialize SQLx database: {err}"); - return Err(err.into()); - } - - info!("Database migration finished!"); - - let storage = NetworkManagerStorage { - manager: StorageManager { connection_pool }, - }; - - Ok(storage) - } - - pub(crate) async fn metadata_set(&self) -> Result { - Ok(self.manager.metadata_set().await?) - } - - pub(crate) async fn get_master_mnemonic( - &self, - ) -> Result, NetworkManagerError> { - Ok(self - .manager - .get_master_mnemonic() - .await? - .map(|m| m.parse()) - .transpose()?) - } - - pub(crate) async fn get_rpc_endpoint(&self) -> Result, NetworkManagerError> { - Ok(self - .manager - .get_rpc_endpoint() - .await? - .map(|m| m.parse()) - .transpose()?) - } - - pub(crate) async fn get_latest_network_name(&self) -> Result { - let Some(id) = self.manager.get_latest_network_id().await? else { - return Err(NetworkManagerError::NoNetworksInitialised); - }; - Ok(self.manager.get_network_name(id).await?) - } - - pub(crate) async fn set_initial_metadata( - &self, - master_mnemonic: &bip39::Mnemonic, - rpc_endpoint: &Url, - ) -> Result<(), NetworkManagerError> { - let master = Zeroizing::new(master_mnemonic.to_string()); - Ok(self - .manager - .set_initial_metadata(master.as_str(), rpc_endpoint.as_str()) - .await?) - } - - async fn persist_contract(&self, contract: &Contract) -> Result { - Ok(self - .manager - .save_contract( - &contract.name, - contract.init_info()?.contract_address.as_ref(), - contract.admin()?.address.as_ref(), - ) - .await?) - } - - async fn persist_mixnode( - &self, - node: &NymNode, - network_id: i64, - ) -> Result<(), NetworkManagerError> { - Ok(self - .manager - .save_node( - &node.identity_key, - network_id, - "mixnode", - node.owner.address.as_ref(), - ) - .await?) - } - - async fn persist_gateway( - &self, - node: &NymNode, - network_id: i64, - ) -> Result<(), NetworkManagerError> { - Ok(self - .manager - .save_node( - &node.identity_key, - network_id, - "gateway", - node.owner.address.as_ref(), - ) - .await?) - } - - async fn persist_account(&self, account: &Account) -> Result { - let as_str = Zeroizing::new(account.mnemonic.to_string()); - Ok(self - .manager - .save_account(account.address.as_ref(), as_str.as_str()) - .await?) - } - - pub(crate) async fn persist_mixnodes( - &self, - nodes: &[NymNode], - network_id: i64, - ) -> Result<(), NetworkManagerError> { - for node in nodes { - self.persist_account(&node.owner).await?; - self.persist_mixnode(node, network_id).await?; - } - Ok(()) - } - - pub(crate) async fn persist_gateways( - &self, - nodes: &[NymNode], - network_id: i64, - ) -> Result<(), NetworkManagerError> { - for node in nodes { - self.persist_account(&node.owner).await?; - self.persist_gateway(node, network_id).await?; - } - Ok(()) - } - - async fn persist_authorised_network_monitor( - &self, - network_id: i64, - account: &Account, - ) -> Result<(), NetworkManagerError> { - self.persist_account(account).await?; - self.manager - .save_authorised_network_monitor(network_id, account.address.as_ref()) - .await?; - Ok(()) - } - - pub(crate) async fn persist_network( - &self, - network: &Network, - ) -> Result<(), NetworkManagerError> { - self.persist_account(network.contracts.mixnet.admin()?) - .await?; - self.persist_account(network.contracts.vesting.admin()?) - .await?; - self.persist_account(network.contracts.ecash.admin()?) - .await?; - self.persist_account(network.contracts.cw3_multisig.admin()?) - .await?; - self.persist_account(network.contracts.cw4_group.admin()?) - .await?; - self.persist_account(network.contracts.dkg.admin()?).await?; - self.persist_account(network.contracts.performance.admin()?) - .await?; - - self.persist_account(&network.auxiliary_addresses.mixnet_rewarder) - .await?; - self.persist_account(&network.auxiliary_addresses.ecash_holding_account) - .await?; - - let mixnet_id = self.persist_contract(&network.contracts.mixnet).await?; - let vesting_id = self.persist_contract(&network.contracts.vesting).await?; - let ecash_id = self.persist_contract(&network.contracts.ecash).await?; - let cw3_multisig_id = self - .persist_contract(&network.contracts.cw3_multisig) - .await?; - let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?; - let dkg_id = self.persist_contract(&network.contracts.dkg).await?; - let performance_id = self - .persist_contract(&network.contracts.performance) - .await?; - - let network_id = self - .manager - .save_network( - &network.name, - network.created_at, - mixnet_id, - vesting_id, - ecash_id, - cw3_multisig_id, - cw4_group_id, - dkg_id, - performance_id, - network.auxiliary_addresses.mixnet_rewarder.address.as_ref(), - network - .auxiliary_addresses - .ecash_holding_account - .address - .as_ref(), - ) - .await?; - - self.manager.save_latest_network_id(network_id).await?; - for nm in &network.auxiliary_addresses.network_monitors { - self.persist_authorised_network_monitor(network_id, nm) - .await? - } - - Ok(()) - } - - pub(crate) async fn try_load_network( - &self, - name: &str, - ) -> Result { - let base_network = self.manager.load_network(name).await?; - let rpc_endpoint = self - .get_rpc_endpoint() - .await? - .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; - - let authorised = self - .manager - .load_authorised_network_monitors(base_network.id) - .await?; - let mut network_monitors = Vec::with_capacity(authorised.len()); - for authorised in authorised { - network_monitors.push( - self.manager - .load_account(&authorised.address) - .await? - .try_into()?, - ) - } - - Ok(LoadedNetwork { - id: base_network.id, - name: base_network.name, - rpc_endpoint, - created_at: base_network.created_at, - contracts: LoadedNymContracts { - mixnet: self - .manager - .load_contract(base_network.mixnet_contract_id) - .await? - .try_into()?, - vesting: self - .manager - .load_contract(base_network.vesting_contract_id) - .await? - .try_into()?, - ecash: self - .manager - .load_contract(base_network.ecash_contract_id) - .await? - .try_into()?, - cw3_multisig: self - .manager - .load_contract(base_network.cw3_multisig_contract_id) - .await? - .try_into()?, - cw4_group: self - .manager - .load_contract(base_network.cw4_group_contract_id) - .await? - .try_into()?, - dkg: self - .manager - .load_contract(base_network.dkg_contract_id) - .await? - .try_into()?, - performance: self - .manager - .load_contract(base_network.performance_contract_id) - .await? - .try_into()?, - }, - auxiliary_addresses: SpecialAddresses { - ecash_holding_account: self - .manager - .load_account(&base_network.ecash_holding_account_address) - .await? - .try_into()?, - mixnet_rewarder: self - .manager - .load_account(&base_network.rewarder_address) - .await? - .try_into()?, - network_monitors, - }, - }) - } -} diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs deleted file mode 100644 index 9df7ebb481f..00000000000 --- a/tools/internal/testnet-manager/src/manager/storage/models.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::NetworkManagerError; -use crate::manager::contract::{Account, LoadedContract}; -use sqlx::FromRow; -use time::OffsetDateTime; - -#[allow(dead_code)] -#[derive(FromRow)] -pub(crate) struct RawAuthorisedNetworkMonitor { - pub(crate) id: i64, - pub(crate) network_id: i64, - pub(crate) address: String, -} - -#[derive(FromRow)] -pub(crate) struct RawAccount { - pub(crate) address: String, - pub(crate) mnemonic: String, -} - -impl TryFrom for Account { - type Error = NetworkManagerError; - - fn try_from(value: RawAccount) -> Result { - Ok(Account { - address: value - .address - .parse() - .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, - mnemonic: value.mnemonic.parse()?, - }) - } -} - -#[derive(FromRow)] -pub(crate) struct RawContract { - #[allow(unused)] - pub(crate) id: i64, - pub(crate) name: String, - pub(crate) address: String, - pub(crate) admin_address: String, - pub(crate) mnemonic: String, -} - -impl TryFrom for LoadedContract { - type Error = NetworkManagerError; - - fn try_from(value: RawContract) -> Result { - Ok(LoadedContract { - name: value.name, - address: value - .address - .parse() - .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, - admin_address: value - .admin_address - .parse() - .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, - admin_mnemonic: value - .mnemonic - .parse() - .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, - }) - } -} - -#[derive(FromRow)] -pub(crate) struct RawNetwork { - pub(crate) id: i64, - pub(crate) name: String, - pub(crate) created_at: OffsetDateTime, - - pub(crate) mixnet_contract_id: i64, - pub(crate) vesting_contract_id: i64, - pub(crate) ecash_contract_id: i64, - pub(crate) cw3_multisig_contract_id: i64, - pub(crate) cw4_group_contract_id: i64, - pub(crate) dkg_contract_id: i64, - pub(crate) performance_contract_id: i64, - - pub(crate) rewarder_address: String, - pub(crate) ecash_holding_account_address: String, -}