From e2dd8ac7435865965b888c742a75b3c7448bb2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 12 Mar 2026 14:46:00 +0000 Subject: [PATCH] feat: localnet v2 (#6277) * squashing localnet-v2 commits (again) cargo fmt fixes to localnet purge provide path in the error message output args log failed exec print based on tty check-prerequisites cmd checked iptables update basic kernel features check enable ipv6 rules add forwarding rules squashing localnet-v2 commits additional changes propagate custom-dns flag to all run containers remove is_mock from EcashManager another localnet squash unused import chore: remove redundant testnet manager missing impl additional linux fixes command to rebuild container image wait for at least 2 blocks additional node startup fixes added --custom-dns flag to nym node setup add gateway probe + wait for DKG magic file fixed localnet down on linux container ls re-enable state resync additional feature locking macos adjustments working nyxd startup on linux wip linux box wip separating network inspect betweewn macos and linux initial linux feature locking moved all container commands into a single location finally working initial node performance squashing orchestrator commits cleanup fixed condition for naive rearrangement added cache of cosmwasm contracts for speed up on subsequent runs 'down' command refreshing described cache after nodes are bonded nym nodes setup + wip on nym api refresh nodes setup WIP first pass cleanup placeholder for nym-node setup bypassing the dkg further progress on nym-api setup wip: api setup up/down/purge placeholders persisting contract setup data fix contract upload by forcing amd64 container platform wip: contracts setup4 wip: contracts setup3 wip: contracts setup2 wip: contracts setup include network setup init and spawn nyxd build nyxd image in dedicated orchestrator build nyxd image squashed cherry-picked lp changes Bits and bobs to make everything work Title MacOS setup instructions Docker/Container localnet * clippy * fixes on non-unix targets --------- Co-authored-by: durch --- Cargo.lock | 586 +++++++++--- Cargo.toml | 6 +- common/client-core/Cargo.toml | 2 +- common/client-core/config-types/src/lib.rs | 7 + common/client-core/config-types/src/old/v6.rs | 1 + .../src/cli_helpers/client_add_gateway.rs | 5 +- .../client-core/src/client/base_client/mod.rs | 105 ++- .../client/base_client/non_wasm_helpers.rs | 12 +- .../src/client/base_client/storage/helpers.rs | 2 +- .../base_client/storage/migration_helpers.rs | 2 +- .../src/client/cover_traffic_stream.rs | 12 +- .../src/client/key_manager/persistence.rs | 4 +- .../src/client/mix_traffic/transceiver.rs | 2 +- .../acknowledgement_listener.rs | 6 +- .../action_controller.rs | 4 +- .../input_message_listener.rs | 2 +- .../acknowledgement_control/mod.rs | 2 +- .../retransmission_request_listener.rs | 4 +- .../sent_notification_listener.rs | 4 +- .../real_messages_control/message_handler.rs | 10 +- .../src/client/real_messages_control/mod.rs | 4 +- .../real_traffic_stream.rs | 18 +- .../sending_delay_controller.rs | 2 +- .../client-core/src/client/received_buffer.rs | 37 +- .../reply_controller/receiver_controller.rs | 54 +- .../replies/reply_controller/requests.rs | 2 +- .../reply_controller/sender_controller.rs | 11 +- .../src/client/statistics_control.rs | 2 +- .../src/client/topology_control/accessor.rs | 2 +- .../src/client/topology_control/mod.rs | 33 +- .../topology_control/nym_api_provider.rs | 11 +- .../src/client/transmission_buffer.rs | 4 +- common/client-core/src/error.rs | 18 +- common/client-core/src/init/helpers.rs | 10 +- common/client-core/src/init/mod.rs | 2 +- common/client-core/src/init/types.rs | 4 +- .../client_traits/query_client.rs | 2 +- .../validator-client/src/nyxd/mod.rs | 12 + common/network-defaults/src/network.rs | 16 +- common/topology/src/error.rs | 3 + common/wasm/client-core/src/config/mod.rs | 10 + .../wasm/client-core/src/config/override.rs | 8 + docker/localnet/localnet.sh | 5 + .../localnet/nym-binaries-localnet.Dockerfile | 107 +++ .../authenticator/mixnet_client.rs | 4 +- .../authenticator/mod.rs | 10 + gateway/src/node/mod.rs | 3 + nym-api/nym-api-requests/src/models/mod.rs | 1 + .../nym-api-requests/src/models/utility.rs | 40 + nym-api/src/ecash/tests/mod.rs | 30 +- nym-api/src/main.rs | 1 + .../src/network_monitor/monitor/preparer.rs | 42 +- .../src/node_status_api/cache/refresher.rs | 4 + nym-api/src/node_status_api/mod.rs | 5 +- nym-api/src/support/caching/refresher.rs | 2 +- nym-api/src/support/cli/init.rs | 18 +- nym-api/src/support/cli/run.rs | 92 +- nym-api/src/support/config/mod.rs | 9 + nym-api/src/support/config/override.rs | 6 + nym-api/src/support/config/template.rs | 3 + nym-api/src/support/http/router.rs | 61 +- nym-api/src/support/http/state/helpers.rs | 38 + .../http/state/mixnet_contract_cache.rs | 21 + nym-api/src/support/http/state/mod.rs | 33 +- .../http/state/node_annotations_cache.rs | 21 + nym-api/src/utility_routes.rs | 170 ++++ nym-node/src/config/gateway_tasks.rs | 9 + nym-node/src/config/helpers.rs | 1 + .../src/config/old_configs/old_config_v12.rs | 4 + nym-node/src/error.rs | 3 + nym-node/src/node/mod.rs | 11 +- nym-node/src/node/shared_network.rs | 86 +- sdk/rust/nym-sdk/src/mixnet/client.rs | 23 +- .../ip-packet-router/src/config/mod.rs | 5 + .../src/config/old_config_v1.rs | 1 + .../ip-packet-router/src/ip_packet_router.rs | 10 + .../ip-packet-router/src/messages/response.rs | 1 + .../ip-packet-router/src/mixnet_client.rs | 4 +- .../src/request_filter/exit_policy/mod.rs | 7 + .../src/request_filter/mod.rs | 18 +- .../network-requester/src/core.rs | 16 +- .../Cargo.toml | 37 +- .../internal/localnet-orchestrator/README.md | 3 + .../build.rs | 8 +- .../dkg-bypass-contract/Cargo.toml | 0 .../dkg-bypass-contract/Makefile | 0 .../dkg-bypass-contract/src/contract.rs | 19 +- .../dkg-bypass-contract/src/lib.rs | 0 .../dkg-bypass-contract/src/msg.rs | 0 .../migrations/01_initial_tables.sql | 98 ++ .../localnet-orchestrator/src/README.md | 221 +++++ .../src/cli/build_info.rs | 9 +- .../src/cli/check_prerequisites.rs | 20 + .../localnet-orchestrator/src/cli/down.rs | 21 + .../src/cli/initialise_contracts.rs | 79 ++ .../src/cli/initialise_nym_api.rs | 57 ++ .../src/cli/initialise_nym_nodes.rs | 52 ++ .../src/cli/initialise_nyxd.rs | 53 ++ .../localnet-orchestrator/src/cli/mod.rs | 122 +++ .../localnet-orchestrator/src/cli/purge.rs | 41 + .../src/cli/rebuild_binaries_image.rs | 38 + .../src/cli/run_gateway_probe_test.rs | 43 + .../localnet-orchestrator/src/cli/up.rs | 104 +++ .../localnet-orchestrator/src/constants.rs | 36 + .../localnet-orchestrator/src/helpers.rs | 305 ++++++ .../src/main.rs | 19 +- .../src/orchestrator/account.rs | 34 + .../orchestrator/container_helpers/linux.rs | 75 ++ .../orchestrator/container_helpers/macos.rs | 53 ++ .../src/orchestrator/container_helpers/mod.rs | 360 +++++++ .../src/orchestrator/context.rs | 218 +++++ .../src/orchestrator/cosmwasm_contract.rs | 138 +++ .../src/orchestrator/helpers.rs | 176 ++++ .../src/orchestrator/mod.rs | 299 ++++++ .../src/orchestrator/network.rs | 413 ++++++++ .../src/orchestrator/nym_node.rs | 54 ++ .../orchestrator/setup/cosmwasm_contracts.rs | 869 +++++++++++++++++ .../src/orchestrator/setup/down.rs | 70 ++ .../src/orchestrator/setup/mod.rs | 11 + .../src/orchestrator/setup/nym_api.rs | 687 ++++++++++++++ .../src/orchestrator/setup/nym_nodes.rs | 880 ++++++++++++++++++ .../src/orchestrator/setup/nyxd.rs | 341 +++++++ .../src/orchestrator/setup/purge.rs | 103 ++ .../setup/rebuild_binaries_image.rs | 95 ++ .../src/orchestrator/setup/up.rs | 33 + .../src/orchestrator/state.rs | 23 + .../src/orchestrator/storage/cache.rs | 47 + .../src/orchestrator/storage/mod.rs | 121 +++ .../storage/orchestrator/manager.rs | 325 +++++++ .../orchestrator/storage/orchestrator/mod.rs | 370 ++++++++ .../storage/orchestrator/models.rs | 104 +++ .../orchestrator/test_cmds/gateway_probe.rs | 98 ++ .../src/orchestrator/test_cmds/mod.rs | 4 + .../src/serde_helpers/linux.rs | 542 +++++++++++ .../src/serde_helpers/macos.rs | 254 +++++ .../src/serde_helpers/mod.rs | 58 ++ tools/internal/testnet-manager/Makefile | 18 - tools/internal/testnet-manager/README.md | 150 --- .../migrations/01_initial_tables.sql | 51 - .../migrations/02_performance_contract.sql | 115 --- .../testnet-manager/src/cli/bypass_dkg.rs | 50 - .../src/cli/initialise_new_network.rs | 54 -- .../src/cli/initialise_post_dkg_network.rs | 83 -- .../src/cli/load_network_details.rs | 35 - .../testnet-manager/src/cli/local_client.rs | 41 - .../src/cli/local_ecash_apis.rs | 85 -- .../testnet-manager/src/cli/local_nodes.rs | 44 - .../testnet-manager/src/cli/migrate.rs | 20 - tools/internal/testnet-manager/src/cli/mod.rs | 108 --- tools/internal/testnet-manager/src/error.rs | 117 --- tools/internal/testnet-manager/src/helpers.rs | 153 --- .../testnet-manager/src/manager/contract.rs | 290 ------ .../testnet-manager/src/manager/dkg_skip.rs | 483 ---------- .../testnet-manager/src/manager/env.rs | 173 ---- .../testnet-manager/src/manager/local_apis.rs | 218 ----- .../src/manager/local_client.rs | 298 ------ .../src/manager/local_nodes.rs | 656 ------------- .../testnet-manager/src/manager/mod.rs | 129 --- .../testnet-manager/src/manager/network.rs | 150 --- .../src/manager/network_init.rs | 772 --------------- .../testnet-manager/src/manager/node.rs | 70 -- .../src/manager/storage/manager.rs | 237 ----- .../src/manager/storage/mod.rs | 353 ------- .../src/manager/storage/models.rs | 85 -- 164 files changed, 9812 insertions(+), 5434 deletions(-) create mode 100644 docker/localnet/nym-binaries-localnet.Dockerfile create mode 100644 nym-api/nym-api-requests/src/models/utility.rs create mode 100644 nym-api/src/support/http/state/helpers.rs create mode 100644 nym-api/src/support/http/state/mixnet_contract_cache.rs create mode 100644 nym-api/src/support/http/state/node_annotations_cache.rs create mode 100644 nym-api/src/utility_routes.rs rename tools/internal/{testnet-manager => localnet-orchestrator}/Cargo.toml (74%) create mode 100644 tools/internal/localnet-orchestrator/README.md rename tools/internal/{testnet-manager => localnet-orchestrator}/build.rs (67%) rename tools/internal/{testnet-manager => localnet-orchestrator}/dkg-bypass-contract/Cargo.toml (100%) rename tools/internal/{testnet-manager => localnet-orchestrator}/dkg-bypass-contract/Makefile (100%) rename tools/internal/{testnet-manager => localnet-orchestrator}/dkg-bypass-contract/src/contract.rs (89%) rename tools/internal/{testnet-manager => localnet-orchestrator}/dkg-bypass-contract/src/lib.rs (100%) rename tools/internal/{testnet-manager => localnet-orchestrator}/dkg-bypass-contract/src/msg.rs (100%) create mode 100644 tools/internal/localnet-orchestrator/migrations/01_initial_tables.sql create mode 100644 tools/internal/localnet-orchestrator/src/README.md rename tools/internal/{testnet-manager => localnet-orchestrator}/src/cli/build_info.rs (59%) create mode 100644 tools/internal/localnet-orchestrator/src/cli/check_prerequisites.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/down.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/initialise_contracts.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/initialise_nym_api.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/initialise_nym_nodes.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/initialise_nyxd.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/mod.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/purge.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/rebuild_binaries_image.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/run_gateway_probe_test.rs create mode 100644 tools/internal/localnet-orchestrator/src/cli/up.rs create mode 100644 tools/internal/localnet-orchestrator/src/constants.rs create mode 100644 tools/internal/localnet-orchestrator/src/helpers.rs rename tools/internal/{testnet-manager => localnet-orchestrator}/src/main.rs (70%) create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/account.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/linux.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/macos.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/mod.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/context.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/cosmwasm_contract.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/helpers.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/mod.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/network.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/nym_node.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/cosmwasm_contracts.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/down.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/mod.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_api.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/nym_nodes.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/nyxd.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/purge.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/rebuild_binaries_image.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/setup/up.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/state.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/storage/cache.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/storage/mod.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/manager.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/mod.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/storage/orchestrator/models.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/gateway_probe.rs create mode 100644 tools/internal/localnet-orchestrator/src/orchestrator/test_cmds/mod.rs create mode 100644 tools/internal/localnet-orchestrator/src/serde_helpers/linux.rs create mode 100644 tools/internal/localnet-orchestrator/src/serde_helpers/macos.rs create mode 100644 tools/internal/localnet-orchestrator/src/serde_helpers/mod.rs delete mode 100644 tools/internal/testnet-manager/Makefile delete mode 100644 tools/internal/testnet-manager/README.md delete mode 100644 tools/internal/testnet-manager/migrations/01_initial_tables.sql delete mode 100644 tools/internal/testnet-manager/migrations/02_performance_contract.sql delete mode 100644 tools/internal/testnet-manager/src/cli/bypass_dkg.rs delete mode 100644 tools/internal/testnet-manager/src/cli/initialise_new_network.rs delete mode 100644 tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs delete mode 100644 tools/internal/testnet-manager/src/cli/load_network_details.rs delete mode 100644 tools/internal/testnet-manager/src/cli/local_client.rs delete mode 100644 tools/internal/testnet-manager/src/cli/local_ecash_apis.rs delete mode 100644 tools/internal/testnet-manager/src/cli/local_nodes.rs delete mode 100644 tools/internal/testnet-manager/src/cli/migrate.rs delete mode 100644 tools/internal/testnet-manager/src/cli/mod.rs delete mode 100644 tools/internal/testnet-manager/src/error.rs delete mode 100644 tools/internal/testnet-manager/src/helpers.rs delete mode 100644 tools/internal/testnet-manager/src/manager/contract.rs delete mode 100644 tools/internal/testnet-manager/src/manager/dkg_skip.rs delete mode 100644 tools/internal/testnet-manager/src/manager/env.rs delete mode 100644 tools/internal/testnet-manager/src/manager/local_apis.rs delete mode 100644 tools/internal/testnet-manager/src/manager/local_client.rs delete mode 100644 tools/internal/testnet-manager/src/manager/local_nodes.rs delete mode 100644 tools/internal/testnet-manager/src/manager/mod.rs delete mode 100644 tools/internal/testnet-manager/src/manager/network.rs delete mode 100644 tools/internal/testnet-manager/src/manager/network_init.rs delete mode 100644 tools/internal/testnet-manager/src/manager/node.rs delete mode 100644 tools/internal/testnet-manager/src/manager/storage/manager.rs delete mode 100644 tools/internal/testnet-manager/src/manager/storage/mod.rs delete mode 100644 tools/internal/testnet-manager/src/manager/storage/models.rs diff --git a/Cargo.lock b/Cargo.lock index d9546295c2..c070c058fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,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" @@ -1037,6 +1048,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" @@ -1120,6 +1140,34 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo-edit" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece153e2ba81bf4550b18830097a8f9bdf14f17e90135be93dd113a5d19647ff" +dependencies = [ + "anyhow", + "cargo_metadata 0.21.0", + "clap", + "clap-cargo", + "clap-verbosity-flag", + "concolor-control", + "dunce", + "env_logger", + "home", + "indexmap 2.13.0", + "log", + "pathdiff", + "semver 1.0.27", + "serde", + "serde_derive", + "tame-index", + "termcolor", + "toml 0.9.12+spec-1.1.0", + "toml_edit 0.23.10+spec-1.0.0", + "url", +] + [[package]] name = "cargo-platform" version = "0.1.9" @@ -1129,6 +1177,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.12", + "toml 0.8.23", + "unicode-xid", + "url", +] + [[package]] name = "cargo_metadata" version = "0.18.1" @@ -1136,8 +1209,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", @@ -1150,8 +1223,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.12", +] + +[[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.12", @@ -1292,24 +1380,45 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", ] [[package]] -name = "clap_builder" -version = "4.5.41" +name = "clap-cargo" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +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.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -1333,9 +1442,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1345,9 +1454,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "classic-mceliece-rust" @@ -1417,6 +1526,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" @@ -2071,7 +2197,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "schemars 0.8.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "thiserror 1.0.69", ] @@ -2672,6 +2798,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.13" @@ -2858,9 +2995,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", ] @@ -3211,7 +3348,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.10.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -3230,7 +3367,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.10.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -3297,6 +3434,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" @@ -3392,6 +3535,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" @@ -3527,11 +3679,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]] @@ -3720,10 +3872,12 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls 0.23.29", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", "tower-service", + "webpki-roots 1.0.2", ] [[package]] @@ -3920,9 +4074,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", @@ -4032,13 +4186,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -4219,7 +4374,7 @@ 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.59.0", ] @@ -4757,6 +4912,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.13.1", + "serde", + "serde_json", + "sqlx", + "strum_macros 0.28.0", + "tempfile", + "time", + "tokio", + "toml 0.8.23", + "tracing", + "url", + "zeroize", +] + [[package]] name = "lock_api" version = "0.4.13" @@ -4769,9 +4970,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" @@ -5404,7 +5605,7 @@ dependencies = [ "rand_chacha 0.3.1", "reqwest 0.13.1", "schemars 0.8.22", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "sha2 0.10.9", @@ -5494,7 +5695,7 @@ dependencies = [ "nym-service-provider-requests-common", "nym-validator-client", "nym-wireguard-types", - "semver 1.0.26", + "semver 1.0.27", "thiserror 2.0.12", "tokio", "tokio-util", @@ -5516,7 +5717,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 0.28.0", @@ -6324,7 +6525,7 @@ dependencies = [ "nym-http-api-client", "nym-network-defaults", "nym-validator-client", - "semver 1.0.26", + "semver 1.0.27", "serde", "thiserror 2.0.12", "tokio", @@ -6338,7 +6539,7 @@ version = "1.20.4" dependencies = [ "nym-coconut-dkg-common", "nym-crypto", - "semver 1.0.26", + "semver 1.0.27", "serde", "thiserror 2.0.12", "time", @@ -6520,7 +6721,7 @@ dependencies = [ "rand 0.8.5", "rand 0.9.2", "reqwest 0.13.1", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "time", @@ -6849,7 +7050,7 @@ dependencies = [ "blake3", "libcrux-sha3", "num_enum", - "semver 1.0.26", + "semver 1.0.27", "strum 0.28.0", "strum_macros 0.28.0", "thiserror 2.0.12", @@ -6974,7 +7175,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.12", @@ -7321,7 +7522,7 @@ dependencies = [ "rand_chacha 0.3.1", "regex", "reqwest 0.13.1", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "serde_json_path", @@ -8742,6 +8943,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "p256" version = "0.13.2" @@ -8833,6 +9043,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +[[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" @@ -8882,9 +9098,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" @@ -8937,7 +9153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.13.0", ] [[package]] @@ -9259,7 +9475,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -9705,25 +9921,34 @@ version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" 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", "hyper 1.6.0", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls 0.23.29", + "rustls-native-certs 0.8.3", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", + "tokio-rustls 0.26.2", + "tokio-util", "tower 0.5.2", "tower-http", "tower-service", @@ -9731,6 +9956,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 1.0.2", ] [[package]] @@ -9936,6 +10162,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" @@ -9951,7 +10183,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]] @@ -10360,11 +10592,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]] @@ -10392,6 +10625,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" @@ -10566,6 +10821,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -10588,7 +10852,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.13.0", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -10616,7 +10880,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.13.0", "itoa", "ryu", "serde", @@ -10788,6 +11052,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "snafu" version = "0.7.5" @@ -10923,7 +11197,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.4", "hashlink", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -11324,6 +11598,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.12", + "tokio", + "toml-span", + "twox-hash", +] + [[package]] name = "tap" version = "1.0.1" @@ -11429,7 +11730,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", @@ -11467,6 +11768,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" @@ -11480,48 +11791,6 @@ dependencies = [ "syn 2.0.106", ] -[[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.12", - "time", - "tokio", - "toml 0.8.23", - "tracing", - "url", - "zeroize", -] - [[package]] name = "textwrap" version = "0.16.2" @@ -11867,9 +12136,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "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]] @@ -11881,26 +12174,63 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.13.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tonic" version = "0.12.3" @@ -12029,7 +12359,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.10.0", + "indexmap 2.13.0", "pin-project-lite", "slab", "sync_wrapper 1.0.2", @@ -12332,6 +12662,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" @@ -12352,6 +12688,12 @@ dependencies = [ "syn 2.0.106", ] +[[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" @@ -12469,7 +12811,7 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.13.0", "once_cell", "serde", "tempfile", @@ -12495,7 +12837,7 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.13.0", "once_cell", "serde", "tempfile", @@ -12560,7 +12902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f64bec2f3a33f2f08df8150e67fa45ba59a2ca740bf20c1beb010d4d791f9a1b" dependencies = [ "anyhow", - "indexmap 2.10.0", + "indexmap 2.13.0", "proc-macro2", "quote", "syn 2.0.106", @@ -12573,7 +12915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f51ebca0d9a4b2aa6c644d5ede45c56f73906b96403c08a1985e75ccb64a01" dependencies = [ "anyhow", - "indexmap 2.10.0", + "indexmap 2.13.0", "proc-macro2", "quote", "syn 2.0.106", @@ -12645,7 +12987,7 @@ checksum = "b925b6421df15cf4bedee27714022cd9626fb4d7eee0923522a608b274ba4371" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.13.0", "tempfile", "uniffi_internal_macros 0.29.3", ] @@ -12658,7 +13000,7 @@ checksum = "a806dddc8208f22efd7e95a5cdf88ed43d0f3271e8f63b47e757a8bbdb43b63a" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.13.0", "tempfile", "uniffi_internal_macros 0.31.0", ] @@ -12717,14 +13059,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -12751,7 +13094,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.13.0", "serde", "serde_json", "utoipa-gen", @@ -13137,7 +13480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.10.0", + "indexmap 2.13.0", "wasm-encoder", "wasmparser", ] @@ -13163,8 +13506,8 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.9.1", "hashbrown 0.15.4", - "indexmap 2.10.0", - "semver 1.0.26", + "indexmap 2.13.0", + "semver 1.0.27", ] [[package]] @@ -13544,6 +13887,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -13806,9 +14158,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -13876,7 +14228,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.13.0", "prettyplease", "syn 2.0.106", "wasm-metadata", @@ -13907,7 +14259,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.9.1", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -13926,9 +14278,9 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_derive", "serde_json", @@ -14108,7 +14460,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.10.0", + "indexmap 2.13.0", "memchr", "thiserror 2.0.12", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index ff8cb1aa72..61411826b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,8 +157,8 @@ members = [ "tools/internal/mixnet-connectivity-check", # "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", @@ -192,6 +192,7 @@ default-members = [ "service-providers/network-requester", "tools/nymvisor", "nym-registration-client", + "tools/internal/localnet-orchestrator" ] exclude = ["contracts", "nym-wallet", "cpu-cycles"] @@ -233,6 +234,7 @@ bloomfilter = "3.0.1" bs58 = "0.5.1" bytecodec = "0.4.15" bytes = "1.11.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 c6275532e6..ce2e932b17 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -2,7 +2,7 @@ name = "nym-client-core" version.workspace = true authors = ["Dave Hrycyszyn "] -edition = "2021" +edition = "2024" rust-version = "1.85" license.workspace = true description = "Crate containing core client functionality and configs, used by all other Nym client implentations" diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 8f0f100206..5af519cabf 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 704de10e52..14c9bab61b 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 5a1950a35a..ae2a611a0f 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 92565c144b..7b27d7b1b1 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 365aabd0e8..335ca8c522 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 922402e9e2..02c54d9214 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 b115b23f97..788e25f795 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 9d5d46f1d5..7beaeeaa63 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 6395e07da6..7a0f495728 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 4895588697..8edc17dbfb 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 2167718e57..fe25bf24fa 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 6262a37e23..4e1d41bf87 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 69ca92709f..db4a48baeb 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 fd17348ee9..ecbd01109c 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 597bfecf56..2172923f7e 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 02560805a8..80d4986c3e 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 366b266090..77677b749e 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 9b852535fa..809088f434 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 12fa62e3ad..423951c91d 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 nym_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 fa9898e42b..4b36ad7c31 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 b31cccb419..51e6b537b9 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 11905c3a85..0398787681 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 4d2782ceba..d941b7181a 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; 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 4d5aac999b..0dd71f4623 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 dcfbd2e19c..30e2737a77 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 9841127b52..5d66f60c56 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 c083aa7378..9eb2079f82 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 2d131cb22b..62c8dbc915 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 ee15210d14..12bb7ec59e 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 44eca8ac0b..59c4b2b540 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 3400981309..2a801395de 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}; @@ -28,10 +28,10 @@ use nym_wasm_utils::websocket::JSWebsocket; #[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 wasmtimer::std::Instant; diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 9a6910e327..8d0d1eb8ad 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 75ee6a6a31..deafd830c5 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 80af295644..d9916328ea 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 d7768b4bfd..0c931c2bd5 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 c85101b690..ccba4af768 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -22,6 +22,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 { @@ -181,11 +191,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 8350baaa94..91d0145660 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/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index 35ca0a51e9..37d30ae134 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -403,6 +403,10 @@ pub struct TopologyWasm { /// before abandoning the procedure. pub max_startup_gateway_waiting_period_ms: u32, + /// Defines how long the client is going to wait on startup for minimal topology to become online, + /// before abandoning the procedure. + pub max_startup_network_waiting_period_ms: u32, + /// Specifies whether the client should not refresh the network topology after obtaining /// the first valid instance. /// Supersedes `topology_refresh_rate_ms`. @@ -446,6 +450,9 @@ impl From for ConfigTopology { max_startup_gateway_waiting_period: Duration::from_millis( topology.max_startup_gateway_waiting_period_ms as u64, ), + max_startup_network_waiting_period: Duration::from_millis( + topology.max_startup_network_waiting_period_ms as u64, + ), minimum_mixnode_performance: topology.minimum_mixnode_performance, minimum_gateway_performance: topology.minimum_gateway_performance, use_extended_topology: topology.use_extended_topology, @@ -463,6 +470,9 @@ impl From for TopologyWasm { max_startup_gateway_waiting_period_ms: topology .max_startup_gateway_waiting_period .as_millis() as u32, + max_startup_network_waiting_period_ms: topology + .max_startup_network_waiting_period + .as_millis() as u32, disable_refreshing: topology.disable_refreshing, minimum_mixnode_performance: topology.minimum_mixnode_performance, minimum_gateway_performance: topology.minimum_gateway_performance, diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index e7dd9110e9..54e04df47f 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -276,6 +276,11 @@ pub struct TopologyWasmOverride { #[tsify(optional)] pub max_startup_gateway_waiting_period_ms: Option, + /// Defines how long the client is going to wait on startup for minimal topology to become online, + /// before abandoning the procedure. + #[tsify(optional)] + pub max_startup_network_waiting_period_ms: Option, + /// Specifies whether the client should not refresh the network topology after obtaining /// the first valid instance. /// Supersedes `topology_refresh_rate_ms`. @@ -322,6 +327,9 @@ impl From for TopologyWasm { max_startup_gateway_waiting_period_ms: value .max_startup_gateway_waiting_period_ms .unwrap_or(def.max_startup_gateway_waiting_period_ms), + max_startup_network_waiting_period_ms: value + .max_startup_network_waiting_period_ms + .unwrap_or(def.max_startup_network_waiting_period_ms), disable_refreshing: value.disable_refreshing.unwrap_or(def.disable_refreshing), minimum_mixnode_performance: value .minimum_mixnode_performance diff --git a/docker/localnet/localnet.sh b/docker/localnet/localnet.sh index e021310bc2..1342d35cd9 100755 --- a/docker/localnet/localnet.sh +++ b/docker/localnet/localnet.sh @@ -185,6 +185,7 @@ start_mixnode() { container run \ --name "$container_name" \ + --dns 1.1.1.1 \ -m 2G \ --network "$NETWORK_NAME" \ -p "${mixnet_port}:${mixnet_port}" \ @@ -226,6 +227,7 @@ start_gateway() { container run \ --name "$GATEWAY_CONTAINER" \ + --dns 1.1.1.1 \ -m 2G \ --network "$NETWORK_NAME" \ -p 9000:9000 \ @@ -365,6 +367,7 @@ start_network_requester() { container run \ --name "$REQUESTER_CONTAINER" \ + --dns 1.1.1.1 \ --network "$NETWORK_NAME" \ -v "$VOLUME_PATH:/localnet" \ -v "$NYM_VOLUME_PATH:/root/.nym" \ @@ -400,6 +403,7 @@ start_socks5_client() { container run \ --name "$SOCKS5_CONTAINER" \ + --dns 1.1.1.1 \ --network "$NETWORK_NAME" \ -p 1080:1080 \ -v "$VOLUME_PATH:/localnet:ro" \ @@ -570,6 +574,7 @@ build_topology() { # 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" \ diff --git a/docker/localnet/nym-binaries-localnet.Dockerfile b/docker/localnet/nym-binaries-localnet.Dockerfile new file mode 100644 index 0000000000..df4cc29b5a --- /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 136e1d2708..2591ab4f51 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 fb50f6ddb0..9c97194923 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -36,6 +36,7 @@ pub struct Authenticator { peer_registrator: PeerRegistrator, upgrade_mode_state: UpgradeModeDetails, wait_for_gateway: bool, + wait_for_topology: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, wireguard_gateway_data: WireguardGatewayData, @@ -56,6 +57,7 @@ impl Authenticator { peer_registrator, upgrade_mode_state, wait_for_gateway: false, + wait_for_topology: false, custom_topology_provider: None, custom_gateway_transceiver: None, wireguard_gateway_data, @@ -71,6 +73,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; @@ -123,6 +132,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 cd9e3dfe4b..89722e9f72 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -354,6 +354,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); @@ -389,6 +390,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); @@ -488,6 +490,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); diff --git a/nym-api/nym-api-requests/src/models/mod.rs b/nym-api/nym-api-requests/src/models/mod.rs index c5f0026d44..6a0c523a52 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 0000000000..4ea1e5bc85 --- /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 4df9966f5d..2640a25009 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -5,15 +5,20 @@ use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::error::{EcashError, Result}; use crate::ecash::keys::KeyPairWithEpoch; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; +use crate::node_status_api::NodeStatusCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::RefreshRequester; use crate::support::config; 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::nyxd::Client; use crate::support::storage::NymApiStorage; @@ -1275,15 +1280,29 @@ impl TestFixture { storage: NymApiStorage, ecash_state: EcashState, nyxd_client: Client, + tmp_dir: &TempDir, ) -> AppState { + let mixnet_contract_paths = tmp_dir.path().join("mixnet_contract"); + let node_annotations_paths = tmp_dir.path().join("node_annotations"); + + let mixnet_contract_cache_state = + MixnetContractCache::new(&mixnet_contract_paths, Duration::from_secs(42)); + let mixnet_contract_cache = + MixnetContractCacheState::new(mixnet_contract_cache_state, RefreshRequester::default()); + + let node_status_cache_state = + NodeStatusCache::new(&node_annotations_paths, Duration::from_secs(42)); + let node_annotations_cache = + NodeAnnotationsCache::new(node_status_cache_state, RefreshRequester::default()); + AppState { nyxd_client, chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)), ecash_signers_cache: Default::default(), address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000), forced_refresh: ForcedRefresh::new(true), - mixnet_contract_cache: SharedCache::new().into(), - node_status_cache: SharedCache::new().into(), + mixnet_contract_cache, + node_annotations_cache, storage, described_nodes_cache: SharedCache::::new(), network_details: NetworkDetails::new( @@ -1366,7 +1385,12 @@ impl TestFixture { TestFixture { axum: TestServer::new(Router::new().nest("/v1/ecash", ecash_routes()).with_state( - Self::build_app_state(storage.clone(), ecash_state, another_fake_nyxd_client), + Self::build_app_state( + storage.clone(), + ecash_state, + another_fake_nyxd_client, + &tmp_dir, + ), )) .unwrap(), storage, diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 96c7cb22d7..b2f52320e0 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 11cb040073..10e8f6a48d 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.contains_key(&l)) { + 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>( diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index e79e458b42..bf5d0033c5 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -78,6 +78,10 @@ impl NodeStatusCacheRefresher { } } + 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(); diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index c296ae402a..afd8265e35 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, @@ -42,7 +43,7 @@ pub(crate) fn start_cache_refresh( described_cache_cache_listener: watch::Receiver, on_disk_file: PathBuf, shutdown_manager: &ShutdownManager, -) { +) -> RefreshRequester { let mut nym_api_cache_refresher = NodeStatusCacheRefresher::new( node_status_cache_state.to_owned(), config.debug.caching_interval, @@ -53,6 +54,8 @@ pub(crate) fn start_cache_refresh( performance_provider, on_disk_file, ); + 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/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 627a9ee73c..f80b57d535 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -283,7 +283,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 d93e90545c..01be826603 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 625e711935..2a120cc6fd 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, } -async fn start_nym_api_tasks(config: &Config) -> anyhow::Result { +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(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 mix_denom = network_details.network.chain_details.mix_denom.base.clone(); let storage_cfg = &config.base.storage_paths; @@ -216,7 +226,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, @@ -255,25 +265,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), - }); let describe_cache_refresh_requester = describe_cache_refresher.refresh_requester(); @@ -313,7 +304,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, @@ -347,7 +338,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(), @@ -364,9 +355,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, @@ -379,10 +370,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?; @@ -407,7 +429,7 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate_and_fixup()?; - 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 9e3b9f8e11..4824158f28 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -200,6 +200,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) } @@ -307,6 +310,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")] @@ -332,6 +340,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 4f8b96db31..d755082f6f 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 3a4132a947..7b19567caa 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 59890b249f..00ab46c8af 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -11,18 +11,22 @@ use crate::support::http::state::AppState; use crate::unstable_routes::v1::unstable_routes_v1; use crate::unstable_routes::v2::unstable_routes_v2; use crate::unstable_routes::v3::unstable_routes_v3; +use crate::utility_routes::utility_routes; use crate::{nym_nodes, status}; 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,40 @@ 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_nodes::handlers::v1::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()) + .nest("/nym-nodes", nym_nodes::handlers::v2::routes()) + } + + fn v3_routes() -> Router { + Router::new().nest("/unstable", unstable_routes_v3()) + } + /// 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,27 +87,9 @@ 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_nodes::handlers::v1::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("/nym-nodes", nym_nodes::handlers::v2::routes()), - ) - .nest("/v3", Router::new().nest("/unstable", unstable_routes_v3())); + .nest("/v1", Self::v1_routes(network_monitor, bearer_token)) + .nest("/v2", Self::v2_routes()) + .nest("/v3", Self::v3_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 0000000000..07581ae53d --- /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 0000000000..658abb5f79 --- /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 1ae60a7fd7..35c2416f85 100644 --- a/nym-api/src/support/http/state/mod.rs +++ b/nym-api/src/support/http/state/mod.rs @@ -15,6 +15,8 @@ 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; @@ -28,6 +30,9 @@ 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 { @@ -52,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, @@ -98,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() @@ -117,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 { @@ -173,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 0000000000..ff731821b1 --- /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/utility_routes.rs b/nym-api/src/utility_routes.rs new file mode 100644 index 0000000000..71f5e28d70 --- /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/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index ed6335fed8..306678ebf6 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 7c2e96215b..88f225fc93 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 diff --git a/nym-node/src/config/old_configs/old_config_v12.rs b/nym-node/src/config/old_configs/old_config_v12.rs index 3b43e62b0a..9e71ab6be4 100644 --- a/nym-node/src/config/old_configs/old_config_v12.rs +++ b/nym-node/src/config/old_configs/old_config_v12.rs @@ -765,6 +765,10 @@ pub async fn try_upgrade_config_v12>( message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + // \/ ADDED + maximum_initial_topology_waiting_time: gateway_tasks::Debug::default() + .maximum_initial_topology_waiting_time, + // /\ ADDED max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, stale_messages: StaleMessageDebug { cleaner_run_interval: old_cfg diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index f1328c61c9..582cf321a4 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -206,6 +206,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 d755e7b12b..7f857f3a5b 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -645,6 +645,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 @@ -1384,8 +1389,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, lp_nodes, metrics_sender, active_clients_store, @@ -1396,7 +1404,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 c32fae2acc..d89ad44a1b 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -29,7 +29,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; @@ -149,27 +149,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.",); @@ -196,6 +181,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 { @@ -217,12 +225,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())? @@ -245,7 +256,8 @@ impl NetworkRefresher { lp_nodes: Default::default(), }; - this.obtain_initial_network().await?; + this.obtain_initial_network(max_startup_waiting_period, min_mix_performance) + .await?; Ok(this) } @@ -322,7 +334,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()) @@ -359,10 +371,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 20e562e662..26f5358568 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, @@ -765,6 +783,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 30e53c6e6f..3b39fa9706 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 8a5ffd8635..1352812a24 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 1fa4148e1b..084adb4009 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 5f3c655205..56b6680500 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 f3cd81a7c5..0fe7c51773 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 7baca7b5c5..133df560c7 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 5e2f359570..2c724af9eb 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 6ca9b3b588..ad9623daba 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 74% rename from tools/internal/testnet-manager/Cargo.toml rename to tools/internal/localnet-orchestrator/Cargo.toml index 2ca0b01ece..7aef726f07 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,39 +8,44 @@ documentation.workspace = true edition.workspace = true license.workspace = true rust-version.workspace = true +readme.workspace = true publish = false [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"] } 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 = { workspace = true, features = ["output_format", "basic_tracing"] } nym-crypto = { workspace = true, features = ["asymmetric", "rand", "serde"] } nym-config = { workspace = true } -nym-validator-client = { workspace = true } +nym-validator-client = { workspace = true, features = ["http-client"] } nym-compact-ecash = { workspace = true } -nym-http-api-client = { workspace = true } -dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false } +nym-pemstore = { workspace = true } # contracts: nym-mixnet-contract-common = { workspace = true } @@ -51,10 +56,14 @@ nym-ecash-contract-common = { workspace = true } nym-coconut-dkg-common = { workspace = true } nym-multisig-contract-common = { workspace = true } nym-performance-contract-common = { workspace = true } -nym-pemstore = { workspace = true } +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 0000000000..064b55fec1 --- /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 be394b3c41..72a8e21b80 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 3c25a60f63..574c329f66 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 0000000000..ea0c8a09f2 --- /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 ed3d7ecadb..2abae16aee 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 0000000000..e8b6c008c7 --- /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 0000000000..0976b50b35 --- /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 0000000000..967e7431e3 --- /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 0000000000..3178f437b9 --- /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 0000000000..4014fcfc9b --- /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 0000000000..58c177271a --- /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 0000000000..9b9e959c45 --- /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 0000000000..c0dc2f19dc --- /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 0000000000..0dc8b0a41a --- /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 0000000000..cb824fbf42 --- /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 0000000000..4738f85ebd --- /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 0000000000..c3e70de661 --- /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 0000000000..6f6fa21d1a --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/helpers.rs @@ -0,0 +1,305 @@ +// 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::network::NymContractsBeingInitialised; + + #[test] + fn all_contracts_are_included() { + 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 70% rename from tools/internal/testnet-manager/src/main.rs rename to tools/internal/localnet-orchestrator/src/main.rs index 49d93a46dc..3c30fa5a55 100644 --- a/tools/internal/testnet-manager/src/main.rs +++ b/tools/internal/localnet-orchestrator/src/main.rs @@ -1,17 +1,20 @@ -// 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))] +#[cfg(unix)] +pub mod cli; #[cfg(unix)] -pub(crate) mod cli; +pub mod constants; + #[cfg(unix)] -pub(crate) mod error; +pub mod helpers; + #[cfg(unix)] -mod helpers; +pub mod orchestrator; + #[cfg(unix)] -mod manager; +pub mod serde_helpers; #[cfg(unix)] #[tokio::main] @@ -35,5 +38,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 0000000000..c1a715c7e3 --- /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 0000000000..e58d576c7b --- /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 0000000000..1365d2a0ab --- /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 0000000000..bff396824d --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/orchestrator/container_helpers/mod.rs @@ -0,0 +1,360 @@ +// 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 +} + +// makes code more readable due to the target specific code +#[allow(clippy::vec_init_then_push)] +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 0000000000..25743f1d86 --- /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 0000000000..154a7c0664 --- /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 0000000000..1b5f85c474 --- /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 0000000000..80f115e9b5 --- /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 0000000000..825b97d9b0 --- /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 0000000000..b96eb33239 --- /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 0000000000..5962a36b7c --- /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 0000000000..0cf461c661 --- /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 0000000000..2d5cf4d15f --- /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 0000000000..260235fa42 --- /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 0000000000..58ebf3002d --- /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 0000000000..795b35b270 --- /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 0000000000..8ee4ee5f46 --- /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 0000000000..307b84d51f --- /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 0000000000..1092432fe7 --- /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 0000000000..4de7072884 --- /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 0000000000..c7f2066168 --- /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 0000000000..b45758a707 --- /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 0000000000..1cef695927 --- /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 0000000000..0a7b4d66c6 --- /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 0000000000..f40ae002af --- /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 0000000000..da4786075c --- /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 0000000000..29ff688f7d --- /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 0000000000..9364fd8842 --- /dev/null +++ b/tools/internal/localnet-orchestrator/src/serde_helpers/linux.rs @@ -0,0 +1,542 @@ +// 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; + + #[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 0000000000..5f6dd9e87e --- /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 0000000000..80b4a5bf60 --- /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 fb4f6f863d..0000000000 --- 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 32a5b1dae1..0000000000 --- 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 791a23e07a..0000000000 --- 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 8ded793130..0000000000 --- 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 a938e73d47..0000000000 --- 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 99174e6d51..0000000000 --- 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 1e5d24b1bf..0000000000 --- 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 414c7984e1..0000000000 --- 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 25a55f1253..0000000000 --- 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 587e5c0375..0000000000 --- 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 d84b9a83e5..0000000000 --- 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 13075aca1a..0000000000 --- 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 b8d143ae95..0000000000 --- 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 0a656c068b..0000000000 --- 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 8f8c6eacb0..0000000000 --- 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 4c04ade5d0..0000000000 --- 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 13126125b2..0000000000 --- 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 005e6b48f4..0000000000 --- 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 e54e458927..0000000000 --- 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 3969bfea33..0000000000 --- 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 3ed0af70b3..0000000000 --- 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 12b657bf24..0000000000 --- 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 39e04f10cf..0000000000 --- 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 701e57406e..0000000000 --- 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 8eab0c6499..0000000000 --- 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 1bd3713d3b..0000000000 --- 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 8067a9ea3c..0000000000 --- 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 9df7ebb481..0000000000 --- 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, -}