diff --git a/CMakeLists.txt b/CMakeLists.txt index b83fc9e41..cb55b0602 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,5 +76,6 @@ endif() add_subdirectory(src) add_subdirectory(tests) add_subdirectory(benchmarks) +add_subdirectory(tools/config_generator) add_subdirectory(tools/bench) add_subdirectory(tools/shard-seeder) diff --git a/Dockerfile b/Dockerfile index a9dc9f58a..4a1f6110e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ COPY --from=builder /opt/tx-processor/scripts/test-transaction.sh ./scripts/test COPY --from=builder /opt/tx-processor/build/src/uhs/client/client-cli ./build/src/uhs/client/client-cli # Copy 2PC config -COPY --from=builder /opt/tx-processor/2pc-compose.cfg ./2pc-compose.cfg +COPY --from=builder /opt/tx-processor/config/general/2pc-compose.cfg ./config/general/2pc-compose.cfg # Create Atomizer Deployment Image FROM $IMAGE_VERSION AS atomizer @@ -74,4 +74,4 @@ COPY --from=builder /opt/tx-processor/scripts/test-transaction.sh ./scripts/test COPY --from=builder /opt/tx-processor/build/src/uhs/client/client-cli ./build/src/uhs/client/client-cli # Copy atomizer config -COPY --from=builder /opt/tx-processor/atomizer-compose.cfg ./atomizer-compose.cfg +COPY --from=builder /opt/tx-processor/config/general/atomizer-compose.cfg ./config/general/atomizer-compose.cfg diff --git a/README.md b/README.md index 4cf9c4f48..77263adf8 100644 --- a/README.md +++ b/README.md @@ -160,11 +160,11 @@ You can find the images [in the Github Container Registry](https://github.com/mi ## Setup test wallets and test them The following commands are all performed from within the second container we started in the previous step. -In each of the below commands, you should pass `atomizer-compose.cfg` instead of `2pc-compose.cfg` if you started the atomizer architecture. +In each of the below commands, you should pass `config/general/atomizer-compose.cfg` instead of `config/general/2pc-compose.cfg` if you started the atomizer architecture. * Mint new coins (e.g., 10 new UTXOs each with a value of 5 atomic units of currency) ```terminal - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool0.dat wallet0.dat mint 10 5 + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool0.dat wallet0.dat mint 10 5 [2021-08-17 15:11:57.686] [WARN ] Existing wallet file not found [2021-08-17 15:11:57.686] [WARN ] Existing mempool not found 4bc23da407c3a8110145c5b6c38199c8ec3b0e35ea66bbfd78f0ed65304ce6fa @@ -172,18 +172,18 @@ In each of the below commands, you should pass `atomizer-compose.cfg` instead of If using the atomizer architecture, you'll need to sync the wallet after: ```terminal - # ./build/src/uhs/client/client-cli atomizer-compose.cfg mempool0.dat wallet0.dat sync + # ./build/src/uhs/client/client-cli config/general/atomizer-compose.cfg mempool0.dat wallet0.dat sync ``` * Inspect the balance of a wallet ```terminal - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool0.dat wallet0.dat info + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool0.dat wallet0.dat info Balance: $0.50, UTXOs: 10, pending TXs: 0 ``` * Make a new wallet ```terminal - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool1.dat wallet1.dat newaddress + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool1.dat wallet1.dat newaddress [2021-08-17 15:13:16.148] [WARN ] Existing wallet file not found [2021-08-17 15:13:16.148] [WARN ] Existing mempool not found usd1qrw038lx5n4wxx3yvuwdndpr7gnm347d6pn37uywgudzq90w7fsuk52kd5u @@ -191,7 +191,7 @@ In each of the below commands, you should pass `atomizer-compose.cfg` instead of * Send currency from the first wallet to the second wallet created in the previous step (e.g., 30 atomic units of currency) ```terminal - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool0.dat wallet0.dat send 30 usd1qrw038lx5n4wxx3yvuwdndpr7gnm347d6pn37uywgudzq90w7fsuk52kd5u + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool0.dat wallet0.dat send 30 usd1qrw038lx5n4wxx3yvuwdndpr7gnm347d6pn37uywgudzq90w7fsuk52kd5u tx_id: cc1f7dc708be5b07e23e125cf0674002ff8546a9342928114bc97031d8b96e75 Data for recipient importinput: @@ -201,20 +201,20 @@ In each of the below commands, you should pass `atomizer-compose.cfg` instead of If using the atomizer architecture, you'll need to sync the sending wallet after: ```terminal - # ./build/src/uhs/client/client-cli atomizer-compose.cfg mempool0.dat wallet0.dat sync + # ./build/src/uhs/client/client-cli config/general/atomizer-compose.cfg mempool0.dat wallet0.dat sync ``` * Check that the currency is no longer available in the sending wallet ```terminal - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool0.dat wallet0.dat info + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool0.dat wallet0.dat info Balance: $0.20, UTXOs: 4, pending TXs: 0 ``` * Import coins to the receiving wallet using the string after `importinput` from the currency transfer step above ```terminal - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool1.dat wallet1.dat importinput cc1f7dc708be5b07e23e125cf0674002ff8546a9342928114bc97031d8b96e750000000000000000d0e4f689b550f623e9370edae235de50417860be0f2f8e924eca9f402fcefeaa1e00000000000000 - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool1.dat wallet1.dat sync - # ./build/src/uhs/client/client-cli 2pc-compose.cfg mempool1.dat wallet1.dat info + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool1.dat wallet1.dat importinput cc1f7dc708be5b07e23e125cf0674002ff8546a9342928114bc97031d8b96e750000000000000000d0e4f689b550f623e9370edae235de50417860be0f2f8e924eca9f402fcefeaa1e00000000000000 + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool1.dat wallet1.dat sync + # ./build/src/uhs/client/client-cli config/general/2pc-compose.cfg mempool1.dat wallet1.dat info Balance: $0.30, UTXOs: 1, pending TXs: 0 ``` diff --git a/2pc-compose.cfg b/config/general/2pc-compose.cfg similarity index 100% rename from 2pc-compose.cfg rename to config/general/2pc-compose.cfg diff --git a/2pc.cfg.sample b/config/general/2pc.cfg.sample similarity index 100% rename from 2pc.cfg.sample rename to config/general/2pc.cfg.sample diff --git a/atomizer-compose.cfg b/config/general/atomizer-compose.cfg similarity index 100% rename from atomizer-compose.cfg rename to config/general/atomizer-compose.cfg diff --git a/config.cfg.sample b/config/general/config.cfg.sample similarity index 100% rename from config.cfg.sample rename to config/general/config.cfg.sample diff --git a/multimachine.cfg.sample b/config/general/multimachine.cfg.sample similarity index 100% rename from multimachine.cfg.sample rename to config/general/multimachine.cfg.sample diff --git a/tests/integration/integration_tests.cfg b/config/integration/integration_tests.cfg similarity index 100% rename from tests/integration/integration_tests.cfg rename to config/integration/integration_tests.cfg diff --git a/tests/integration/integration_tests_2pc.cfg b/config/integration/integration_tests_2pc.cfg similarity index 100% rename from tests/integration/integration_tests_2pc.cfg rename to config/integration/integration_tests_2pc.cfg diff --git a/tests/integration/replicated_atomizer.cfg b/config/integration/replicated_atomizer.cfg similarity index 100% rename from tests/integration/replicated_atomizer.cfg rename to config/integration/replicated_atomizer.cfg diff --git a/tests/integration/replicated_shard.cfg b/config/integration/replicated_shard.cfg similarity index 100% rename from tests/integration/replicated_shard.cfg rename to config/integration/replicated_shard.cfg diff --git a/config/unit/2pc_template_unit_test.tmpl b/config/unit/2pc_template_unit_test.tmpl new file mode 100644 index 000000000..4a2a2b1bd --- /dev/null +++ b/config/unit/2pc_template_unit_test.tmpl @@ -0,0 +1,44 @@ +2pc=1 +sentinel_count=1 +shard_count=1 +coordinator_count=1 +coordinator_max_threads=1 +election_timeout_upper=4000 +election_timeout_lower=3000 +heartbeat=1000 +raft_max_batch=100000 +snapshot_distance=1000000000 +batch_size=1 +wait_for_followers=0 +loadgen_invalid_tx_rate=0.00 +loadgen_fixed_tx_rate=0.01 +loadgen_sendtx_output_count=1 +loadgen_sendtx_input_count=1 +initial_mint_count=20000 +initial_mint_value=100 +tmpl_randomize_values=1 +tmpl_shard_start=0 +tmpl_shard_size=255 +tmpl_max_shard_raft_replication_count=1 +tmpl_avg_shard_start_end_overlap_percent=0.15 +tmpl_max_coordinator_raft_replication_count=1 +tmpl_default_log_level="INFO" +tmpl_universal_override_log_level="WARN" +tmpl_sentinel_log_level="WARN" +tmpl_coordinator_log_level="DEBUG" +tmpl_shard_log_level="DEBUG" +num_wallets=10 +num_minters=1 +num_redeemers=1 +total_number_of_transactions=100 +avg_mint_value=10 +avg_mint_count=10 +avg_redemption_value=5 +avg_redemption_count=5 +transaction_frequency=5 +mint_frequency=0.1 +redemption_frequency=0.05 +sentinel_offline_probability=0.01 +shard_offline_probability=0.01 +coordinator_offline_probability=0.01 +randomize_execution=0 diff --git a/config/unit/atomizer_template_unit_test.tmpl b/config/unit/atomizer_template_unit_test.tmpl new file mode 100644 index 000000000..a293505e7 --- /dev/null +++ b/config/unit/atomizer_template_unit_test.tmpl @@ -0,0 +1,32 @@ +2pc=0 +archiver_count=1 +atomizer_count=1 +shard_count=1 +sentinel_count=1 +watchtower_count=1 +target_block_interval=3000 +stxo_cache_depth=2 +target_block_interval=250 +election_timeout_upper=4000 +election_timeout_lower=3000 +heartbeat=1000 +raft_max_batch=100000 +snapshot_distance=1000000000 +batch_size=1 +wait_for_followers=0 +loadgen_invalid_tx_rate=0.00 +loadgen_fixed_tx_rate=0.01 +loadgen_sendtx_output_count=1 +loadgen_sendtx_input_count=1 +initial_mint_count=20000 +initial_mint_value=100 +tmpl_randomize_values=1 +tmpl_shard_start=0 +tmpl_shard_size=255 +tmpl_avg_shard_start_end_overlap_percent=0.15 +tmpl_default_log_level="INFO" +tmpl_sentinel_log_level="DEBUG" +tmpl_shard_log_level="INFO" +tmpl_watchtower_log_level="DEBUG" +tmpl_archiver_log_level="DEBUG" +tmpl_atomizer_log_level="DEBUG" diff --git a/tests/unit/coordinator/coordinator.cfg b/config/unit/coordinator/coordinator.cfg similarity index 100% rename from tests/unit/coordinator/coordinator.cfg rename to config/unit/coordinator/coordinator.cfg diff --git a/tests/unit/locking_shard/locking_shard.cfg b/config/unit/locking_shard/locking_shard.cfg similarity index 100% rename from tests/unit/locking_shard/locking_shard.cfg rename to config/unit/locking_shard/locking_shard.cfg diff --git a/docker-compose-2pc.yml b/docker-compose-2pc.yml index 93c81f0ce..5411fc69f 100644 --- a/docker-compose-2pc.yml +++ b/docker-compose-2pc.yml @@ -9,7 +9,7 @@ services: image: opencbdc-tx-twophase tty: true restart: always - command: ./build/src/uhs/twophase/sentinel_2pc/sentineld-2pc 2pc-compose.cfg 0 + command: ./build/src/uhs/twophase/sentinel_2pc/sentineld-2pc ./config/general/2pc-compose.cfg 0 ports: - 5555:5555 depends_on: @@ -29,7 +29,7 @@ services: target: twophase image: opencbdc-tx-twophase tty: true - command: ./build/src/uhs/twophase/coordinator/coordinatord 2pc-compose.cfg 0 0 + command: ./build/src/uhs/twophase/coordinator/coordinatord ./config/general/2pc-compose.cfg 0 0 expose: - "7777" depends_on: @@ -49,7 +49,7 @@ services: target: twophase image: opencbdc-tx-twophase tty: true - command: ./build/src/uhs/twophase/locking_shard/locking-shardd 2pc-compose.cfg 0 0 + command: ./build/src/uhs/twophase/locking_shard/locking-shardd ./config/general/2pc-compose.cfg 0 0 expose: - "6666" ports: diff --git a/docker-compose-atomizer.yml b/docker-compose-atomizer.yml index 1bba2e512..f1b7a7580 100644 --- a/docker-compose-atomizer.yml +++ b/docker-compose-atomizer.yml @@ -8,7 +8,7 @@ services: target: atomizer image: opencbdc-tx-atomizer tty: true - command: ./build/src/uhs/atomizer/watchtower/watchtowerd atomizer-compose.cfg 0 + command: ./build/src/uhs/atomizer/watchtower/watchtowerd ./config/general/atomizer-compose.cfg 0 ports: - 8555:8555 expose: @@ -28,7 +28,7 @@ services: target: atomizer image: opencbdc-tx-atomizer tty: true - command: ./build/src/uhs/atomizer/atomizer/atomizer-raftd atomizer-compose.cfg 0 + command: ./build/src/uhs/atomizer/atomizer/atomizer-raftd ./config/general/atomizer-compose.cfg 0 expose: - "5555" depends_on: @@ -48,7 +48,7 @@ services: target: atomizer image: opencbdc-tx-atomizer tty: true - command: ./build/src/uhs/atomizer/archiver/archiverd atomizer-compose.cfg 0 + command: ./build/src/uhs/atomizer/archiver/archiverd ./config/general/atomizer-compose.cfg 0 expose: - "4555" depends_on: @@ -69,7 +69,7 @@ services: target: atomizer image: opencbdc-tx-atomizer tty: true - command: ./build/src/uhs/atomizer/shard/shardd atomizer-compose.cfg 0 + command: ./build/src/uhs/atomizer/shard/shardd ./config/general/atomizer-compose.cfg 0 expose: - "6555" depends_on: @@ -91,7 +91,7 @@ services: target: atomizer image: opencbdc-tx-atomizer tty: true - command: ./build/src/uhs/atomizer/sentinel/sentineld atomizer-compose.cfg 0 + command: ./build/src/uhs/atomizer/sentinel/sentineld ./config/general/atomizer-compose.cfg 0 ports: - 7555:7555 depends_on: diff --git a/scripts/test.sh b/scripts/test.sh index 615a8acb5..fd2cfba03 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -164,7 +164,7 @@ run_test_suite () { if [[ "$RUN_UNIT_TESTS" == "true" ]] then echo "Running unit tests..." - find "${REPO_TOP_DIR}"/tests/unit/ -name '*.cfg' \ + find "${REPO_TOP_DIR}"/config/unit/ -name '*.cfg' \ -exec rsync \{\} "$BUILD_DIR" \; run_test_suite "tests/unit/run_unit_tests" "unit_tests_coverage" else @@ -175,7 +175,8 @@ echo if [[ "$RUN_INTEGRATION_TESTS" == "true" ]] then echo "Running integration tests..." - cp "${REPO_TOP_DIR}"/tests/integration/*.cfg "$BUILD_DIR" + cp "${REPO_TOP_DIR}"/config/integration/*.cfg "${BUILD_DIR}" + cp "${REPO_TOP_DIR}"/tools/config_generator/*.tmpl "${BUILD_DIR}"/tools/config_generator run_test_suite "tests/integration/run_integration_tests" \ "integration_tests_coverage" else diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82d4a03de..7488c6792 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,6 @@ project(tests) -include_directories(. ../src ../tools/watchtower ../3rdparty ../3rdparty/secp256k1/include) +include_directories(. ../src ../ ../tools/config_generator ../tools/watchtower ../3rdparty ../3rdparty/secp256k1/include) set(SECP256K1_LIBRARY $) add_library(util util.cpp) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 7f9df8f6b..81e774e3c 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(run_unit_tests archiver_test.cpp atomizer_test.cpp buffer_test.cpp common/hash_test.cpp + config_gen_test.cpp config_test.cpp coordinator/messages_test.cpp locking_shard/format_test.cpp @@ -50,6 +51,7 @@ target_link_libraries(run_unit_tests ${GTEST_LIBRARY} transaction network common + config_generator serialization crypto secp256k1 diff --git a/tests/unit/config_gen_test.cpp b/tests/unit/config_gen_test.cpp new file mode 100644 index 000000000..e4192cefa --- /dev/null +++ b/tests/unit/config_gen_test.cpp @@ -0,0 +1,59 @@ +// Copyright (c) 2022 MIT Digital Currency Initiative, +// Federal Reserve Bank of Boston +// MITRE Corporation +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "tools/config_generator/config_generator.hpp" +#include "util/common/config.hpp" + +#include +#include +#include + +// TODO: Add file parsing tests. + +class config_generation_validation_test : public ::testing::Test { + protected: + void SetUp() override { + template_file_atomizer + = "../config/unit/atomizer_template_unit_test.tmpl"; + template_file_2pc = "../config/unit/2pc_template_unit_test.tmpl"; + port_num = 5555; + } + + std::string template_file_atomizer; + std::string template_file_2pc; + size_t port_num; +}; + +TEST_F(config_generation_validation_test, + generate_configuration_file_atomizer_test) { + // Assumes build dir is "build". Cannot find a way around this since unit + // tests can be run from either root dir or build dir and we don't + // necessarily know which one + cbdc::generate_config::config_generator new_config_gen( + template_file_atomizer, + port_num); + testing::internal::CaptureStderr(); + auto cfg_or_err = new_config_gen.generate_configuration_file(); + ASSERT_NE(testing::internal::GetCapturedStderr().find("SUCCESS"), -1); + // TODO + // Reload generate file and check values + // Delete generated file +} + +TEST_F(config_generation_validation_test, + generate_configuration_file_two_phase_test) { + // Assumes build dir is "build". Cannot find a way around this since unit + // tests can be run from either root dir or build dir and we don't + // necessarily know which one + cbdc::generate_config::config_generator new_config_gen(template_file_2pc, + port_num); + testing::internal::CaptureStderr(); + auto cfg_or_err = new_config_gen.generate_configuration_file(); + ASSERT_NE(testing::internal::GetCapturedStderr().find("SUCCESS"), -1); + // TODO + // Reload generate file and check values + // Delete generated file +} diff --git a/tools/config_generator/2pc_config_template.tmpl b/tools/config_generator/2pc_config_template.tmpl new file mode 100644 index 000000000..4a2a2b1bd --- /dev/null +++ b/tools/config_generator/2pc_config_template.tmpl @@ -0,0 +1,44 @@ +2pc=1 +sentinel_count=1 +shard_count=1 +coordinator_count=1 +coordinator_max_threads=1 +election_timeout_upper=4000 +election_timeout_lower=3000 +heartbeat=1000 +raft_max_batch=100000 +snapshot_distance=1000000000 +batch_size=1 +wait_for_followers=0 +loadgen_invalid_tx_rate=0.00 +loadgen_fixed_tx_rate=0.01 +loadgen_sendtx_output_count=1 +loadgen_sendtx_input_count=1 +initial_mint_count=20000 +initial_mint_value=100 +tmpl_randomize_values=1 +tmpl_shard_start=0 +tmpl_shard_size=255 +tmpl_max_shard_raft_replication_count=1 +tmpl_avg_shard_start_end_overlap_percent=0.15 +tmpl_max_coordinator_raft_replication_count=1 +tmpl_default_log_level="INFO" +tmpl_universal_override_log_level="WARN" +tmpl_sentinel_log_level="WARN" +tmpl_coordinator_log_level="DEBUG" +tmpl_shard_log_level="DEBUG" +num_wallets=10 +num_minters=1 +num_redeemers=1 +total_number_of_transactions=100 +avg_mint_value=10 +avg_mint_count=10 +avg_redemption_value=5 +avg_redemption_count=5 +transaction_frequency=5 +mint_frequency=0.1 +redemption_frequency=0.05 +sentinel_offline_probability=0.01 +shard_offline_probability=0.01 +coordinator_offline_probability=0.01 +randomize_execution=0 diff --git a/tools/config_generator/2pc_loadgen_config_template.tmpl b/tools/config_generator/2pc_loadgen_config_template.tmpl new file mode 100644 index 000000000..bde3aa040 --- /dev/null +++ b/tools/config_generator/2pc_loadgen_config_template.tmpl @@ -0,0 +1,50 @@ +2pc=1 +sentinel_count=1 +shard_count=1 +coordinator_count=1 +coordinator_max_threads=1 +election_timeout_upper=4000 +election_timeout_lower=3000 +heartbeat=1000 +raft_max_batch=100000 +snapshot_distance=1000000000 +batch_size=100000 +batch_delay=1 +wait_for_followers=0 +seed_value=1000000 +seed_from=0 +seed_to=1000000 +loadgen_count=1 +loadgen_invalid_tx_rate=0.00 +loadgen_fixed_tx_rate=0.0001 +loadgen_sendtx_output_count=2 +loadgen_sendtx_input_count=2 +initial_mint_count=20000 +initial_mint_value=100 +tmpl_randomize_values=1 +tmpl_shard_start=0 +tmpl_shard_size=255 +tmpl_max_shard_raft_replication_count=1 +tmpl_avg_shard_start_end_overlap_percent=0.15 +tmpl_max_coordinator_raft_replication_count=1 +tmpl_default_log_level="INFO" +tmpl_universal_override_log_level="WARN" +tmpl_sentinel_log_level="WARN" +tmpl_coordinator_log_level="DEBUG" +tmpl_shard_log_level="DEBUG" +num_wallets=10 +num_minters=1 +num_redeemers=1 +total_number_of_transactions=100 +avg_mint_value=10 +avg_mint_count=10 +avg_redemption_value=5 +avg_redemption_count=5 +transaction_frequency=5 +mint_frequency=0.1 +redemption_frequency=0.05 +sentinel_offline_probability=0.01 +shard_offline_probability=0.01 +coordinator_offline_probability=0.01 +randomize_execution=0 + diff --git a/tools/config_generator/CMakeLists.txt b/tools/config_generator/CMakeLists.txt new file mode 100644 index 000000000..f111d13d9 --- /dev/null +++ b/tools/config_generator/CMakeLists.txt @@ -0,0 +1,14 @@ +project(config_gen) + +include_directories(../../src ../../3rdparty/secp256k1/include) + +add_library(config_generator config_generator.cpp) + +add_executable(generate_config generate_config.cpp) + +target_link_libraries(generate_config config_generator + util + network + common + crypto + secp256k1) diff --git a/tools/config_generator/atomizer_config_template.tmpl b/tools/config_generator/atomizer_config_template.tmpl new file mode 100644 index 000000000..a293505e7 --- /dev/null +++ b/tools/config_generator/atomizer_config_template.tmpl @@ -0,0 +1,32 @@ +2pc=0 +archiver_count=1 +atomizer_count=1 +shard_count=1 +sentinel_count=1 +watchtower_count=1 +target_block_interval=3000 +stxo_cache_depth=2 +target_block_interval=250 +election_timeout_upper=4000 +election_timeout_lower=3000 +heartbeat=1000 +raft_max_batch=100000 +snapshot_distance=1000000000 +batch_size=1 +wait_for_followers=0 +loadgen_invalid_tx_rate=0.00 +loadgen_fixed_tx_rate=0.01 +loadgen_sendtx_output_count=1 +loadgen_sendtx_input_count=1 +initial_mint_count=20000 +initial_mint_value=100 +tmpl_randomize_values=1 +tmpl_shard_start=0 +tmpl_shard_size=255 +tmpl_avg_shard_start_end_overlap_percent=0.15 +tmpl_default_log_level="INFO" +tmpl_sentinel_log_level="DEBUG" +tmpl_shard_log_level="INFO" +tmpl_watchtower_log_level="DEBUG" +tmpl_archiver_log_level="DEBUG" +tmpl_atomizer_log_level="DEBUG" diff --git a/tools/config_generator/config_generator.cpp b/tools/config_generator/config_generator.cpp new file mode 100644 index 000000000..60149f9b2 --- /dev/null +++ b/tools/config_generator/config_generator.cpp @@ -0,0 +1,806 @@ +// Copyright (c) 2022 MIT Digital Currency Initiative, +// Federal Reserve Bank of Boston +// MITRE Corporation +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "config_generator.hpp" + +#include "util/common/config.hpp" +#include "util/common/keys.hpp" +#include "util/common/variant_overloaded.hpp" +#include "util/network/tcp_listener.hpp" + +#include +#include +#include +#include +#include + +// NORMAL CONFIGS + +using namespace cbdc::config; + +static constexpr auto MAX_SHARD_NUM = 256; +// Using the following values from src/common/config.hpp: + +// "cbdc::config::two_phase_mode" : Name of value in config template file that +// is an integer which determines if this is 2PC or Atomizer. 1 means 2PC +// "cbdc::config::shard_count_key" : Number of shards to be created +// "cbdc::config::sentinel_count_key" : Number of sentinels to be created +// "cbdc::config::coordinator_count_key" : Number of coordinators to be created +// "cbdc::config::archiver_count_key" : Number of archivers to be created +// "cbdc::config::atomizer_count_key" : Number of atomizers to be created +// "cbdc::config::watchtower_count_key" : Number of watchtowers to be created + +// Prefix of interest that denotes parameters in the file that are used +// here to help generate the config file but will not be present in the +// final product +static constexpr auto template_prefix = "tmpl_"; + +// TEMPLATE CONFIGS + +// Parameter to tell us whether or not to randomize private/public key +// pairs, shard start - end and others +static constexpr auto tmpl_randomize_values = "tmpl_randomize_values"; +// Average difference between shard_start and shard_end +static constexpr auto tmpl_shard_size = "tmpl_shard_size"; +// Override for all log levels if they are not specified +static constexpr auto tmpl_universal_override_log_level + = "tmpl_universal_override_log_level"; +// Max number of raft replicated shards to make. +static constexpr auto tmpl_avg_shard_start_end_overlap_percent + = "tmpl_avg_shard_start_end_overlap_percent"; +// How march each shards coverage zones, roughly, are allowed to overlap. +static constexpr auto tmpl_max_shard_raft_replication_count + = "tmpl_max_shard_raft_replication_count"; +// Max number of raft replicated coordinators to make. +static constexpr auto tmpl_max_coordinator_raft_replication_count + = "tmpl_max_coordinator_raft_replication_count"; +// Default for all log levels if they are not specified +static constexpr auto tmpl_default_log_level = "tmpl_default_log_level"; +static constexpr auto tmpl_sentinel_log_level = "tmpl_sentinel_log_level"; +static constexpr auto tmpl_coordinator_log_level + = "tmpl_coordinator_log_level"; +static constexpr auto tmpl_shard_log_level = "tmpl_shard_log_level"; +static constexpr auto tmpl_archiver_log_level = "tmpl_archiver_log_level"; +static constexpr auto tmpl_atomizer_log_level = "tmpl_atomizer_log_level"; +static constexpr auto tmpl_watchtower_log_level = "tmpl_watchtower_log_level"; + +// All acceptable log levels +static const std::set log_levels + = {"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"}; + +namespace cbdc::generate_config { + + config_generator::config_generator(std::string& _template_config_file, + const size_t _start_port) + : m_template_config_file(_template_config_file), + m_current_port(_start_port) { + // Get Project root dir and build dir + std::filesystem::path build_dir = std::filesystem::current_path(); + // This config generator class assumes project root is "opencbdc-tx" + if(!std::filesystem::exists(m_template_config_file)) { + m_template_file_is_valid = false; + std::cerr << "template file cannot be found at " + << _template_config_file << std::endl; + } + } + + void config_generator::calculate_shard_coverage(const size_t num_shards, + const size_t shard_size) { + std::vector shard_index_sum_total(shard_size, 0); + shard_index_sum_total.reserve(shard_size); + // Setup shard metadata + for(size_t i = 0; i < num_shards; i++) { + size_t randNum = rand() % (shard_size - 0 + 1) + 0; + shard_info.emplace_back(ShardInfo()); + shard_info.at(i).coverage = std::vector(MAX_SHARD_NUM, 0); + shard_info.at(i).coverage.at(randNum) = 1; + shard_info.at(i).still_expanding = true; + shard_info.at(i).allow_overlap = false; + shard_info.at(i).current_coverage_expansion_limits + = std::make_pair(randNum, randNum); + shard_info.at(i).numbers_covered = 1; + shard_info.at(i).shard_id = i; + auto overlap_percentage + = find_value(tmpl_avg_shard_start_end_overlap_percent); + shard_info.at(i).overlap_percentage_allowed + = std::abs(calculate_normal_distribution_point( + 0, + overlap_percentage.value())); + shard_index_sum_total.at(randNum)++; + } + auto still_expanding = true; + while(still_expanding) { + still_expanding = false; + // Loop over all shards + auto shard_it = shard_info.begin(); + while(shard_it != shard_info.end()) { + // If all ORd still_expanding equal false, then they are all + // false + still_expanding |= shard_it->still_expanding; + if(shard_it->still_expanding) { + // Next index to the upside in shard ids + size_t next_index_upside = std::min( + (shard_it->current_coverage_expansion_limits.second + + 1), + shard_size - 1); + // Next index to the downside in shard ids + size_t next_index_downside = std::min( + (shard_it->current_coverage_expansion_limits.first + - 1), + static_cast(0)); + // If shard can expand in coverage upwards and upwards + // coverage is more lightly covered than downwards + if(shard_it->current_coverage_expansion_limits.second + < (shard_size - 1) + && shard_index_sum_total.at(next_index_upside) + <= shard_index_sum_total.at( + next_index_downside)) { + // If all values have been visited (overlap allowed) or + // has not yet been visited + if(shard_index_sum_total.at( + shard_it->current_coverage_expansion_limits + .second + + 1) + == 0 + || shard_it->allow_overlap) { + // Update expansion limits to upside + shard_it->current_coverage_expansion_limits + .second++; + // Update sum array of all shards + shard_index_sum_total.at( + shard_it->current_coverage_expansion_limits + .second)++; + shard_it->numbers_covered += 1; + // Update coverage array + shard_it->coverage.at( + shard_it->current_coverage_expansion_limits + .second) + = 1; + } + // Else if shard can expand in coverage downwards and + } else if(shard_it->current_coverage_expansion_limits.first + > 0) { + // If all values have been visited (overlap allowed) or + // has not yet been visited + if(shard_index_sum_total.at( + shard_it->current_coverage_expansion_limits + .first + - 1) + == 0 + || shard_it->allow_overlap) { + { + // Update expansion limits to downside + shard_it->current_coverage_expansion_limits + .first--; + // Update sum array of all shards + shard_index_sum_total.at( + shard_it->current_coverage_expansion_limits + .first)++; + shard_it->numbers_covered += 1; + // Update coverage array + shard_it->coverage.at( + shard_it->current_coverage_expansion_limits + .first) + = 1; + } + } + } + } + shard_bookkeeping(shard_index_sum_total, shard_it->shard_id); + shard_it++; + } + } + } + + void + config_generator::shard_bookkeeping(const std::vector& array_total, + const size_t shard_id) { + // Get sum of all coverage vectors for this shard over its expansion so + // far + double total_sum = 0; + for(size_t i + = shard_info.at(shard_id).current_coverage_expansion_limits.first; + i <= shard_info.at(shard_id) + .current_coverage_expansion_limits.second; + i++) { + total_sum += static_cast(array_total.at(i)); + } + auto percentage_overlapped_so_far + = (total_sum + / static_cast(shard_info.at(shard_id).numbers_covered)) + - 1.0; + if(shard_info.at(shard_id).overlap_percentage_allowed + <= percentage_overlapped_so_far + || ((shard_info.at(shard_id) + .current_coverage_expansion_limits.second + - shard_info.at(shard_id) + .current_coverage_expansion_limits.first) + == (array_total.size() - 1))) { + // This shard has reached its allowed overlap limit + shard_info.at(shard_id).still_expanding = false; + } + // Check if all indices have been visited at least once from 0 to + // shard_size + auto all_visited = true; + for(auto b : array_total) { + if(b == 0) { + all_visited = false; + break; + } + } + // If all indices have been visited at least once from 0 to + // shard_size, allow shards to start overlapping in coverage + if(all_visited) { + auto shard_it = shard_info.begin(); + while(shard_it != shard_info.end()) { + shard_it->allow_overlap = true; + shard_it++; + } + } + } + + // Get random value based on mean and standard deviation + [[nodiscard]] auto + config_generator::calculate_normal_distribution_point(const size_t mean, + const double std_dev) + -> double { + std::normal_distribution distribution( + static_cast(mean), + std_dev); + return distribution(generator); + } + + // This is not a failsafe because we are simply generating a + // configuration file here, however, it is still good to check that the + // port is available + [[nodiscard]] auto config_generator::get_open_port() -> unsigned short { + unsigned short port = m_current_port % MAX_PORT_NUM; + m_current_port++; + auto ep = cbdc::network::endpoint_t{cbdc::network::localhost, port}; + auto listener = cbdc::network::tcp_listener(); + while(!listener.listen(ep.first, ep.second)) { + port = m_current_port % MAX_PORT_NUM; + ep = cbdc::network::endpoint_t{cbdc::network::localhost, port}; + m_current_port++; + } + return port; + } + + [[nodiscard]] auto config_generator::create_repeatable_key_pair() + -> std::pair { + cbdc::privkey_t seckey; + for(auto&& b : seckey) { + b = rand(); + } + cbdc::pubkey_t ret = cbdc::pubkey_from_privkey(seckey, m_secp.get()); + auto seckey_str = cbdc::to_string(seckey); + auto pubkey_str = cbdc::to_string(ret); + return std::make_pair(seckey_str, pubkey_str); + } + + [[nodiscard]] auto config_generator::create_random_key_pair() + -> std::pair { + std::uniform_int_distribution keygen; + + cbdc::privkey_t seckey; + for(auto&& b : seckey) { + b = keygen(*m_random_source); + } + cbdc::pubkey_t ret = cbdc::pubkey_from_privkey(seckey, m_secp.get()); + auto seckey_str = cbdc::to_string(seckey); + auto pubkey_str = cbdc::to_string(ret); + return std::make_pair(seckey_str, pubkey_str); + } + + [[nodiscard]] auto config_generator::create_key_pair() const + -> std::pair { + if(m_randomize) { + return create_random_key_pair(); + } + return create_repeatable_key_pair(); + } + + [[nodiscard]] auto config_generator::parse_value(const std::string& value, + const bool keep_quotes) + -> value_t { + if(value[0] != '\"' && value[value.size() - 1] != '\"') { + if(value.find('.') == std::string::npos) { + const auto as_int = static_cast(std::stoull(value)); + return as_int; + } + const auto as_dbl = std::stod(value); + return as_dbl; + } + + if(!keep_quotes) { + const auto unquoted = value.substr(1, value.size() - 2); + return unquoted; + } + return value; + } + + [[nodiscard]] auto config_generator::get_param_from_template_file( + const std::string& option, + std::map& config_map) + -> std::variant { + auto it = config_map.find(option); + if(it != config_map.end()) { + value_t parsed_val = parse_value(it->second, false); + if(std::holds_alternative(parsed_val)) { + return std::get(parsed_val); + } + if(std::holds_alternative(parsed_val)) { + return std::get(parsed_val); + } + if(std::holds_alternative(parsed_val)) { + return std::get(parsed_val); + } + __builtin_unreachable(); + } + auto error_msg = "Warning: Could not find param, " + option + "."; + std::cerr << error_msg << std::endl; + return error_msg; + } + + void config_generator::set_param_to_config_file(const std::string& key, + const std::string& value) { + m_new_config << key << "=" << '"' << value << '"' << '\n'; + } + + void config_generator::set_param_to_config_file(const std::string& key, + const double value) { + m_new_config << key << "=" << value << '\n'; + } + + void config_generator::set_param_to_config_file(const std::string& key, + const size_t value) { + m_new_config << key << "=" << value << '\n'; + } + + void config_generator::set_log_level(const std::string& key, + std::string& log_level) { + if(log_levels.find(log_level) == log_levels.end()) { + log_level = "DEBUG"; + std::cerr << "Warning: Log level not recognized. Setting to DEBUG" + << std::endl; + } else if(!find_value(key).has_value()) { + log_level + = find_value(tmpl_universal_override_log_level) + .value(); + } + } + + [[maybe_unused]] auto + config_generator::create_component(const char* type, + const size_t component_count, + bool create_2pc) -> std::string { + std::string return_msg; + auto _default_log_level + = find_value(tmpl_default_log_level); + if(component_count == 0) { + return_msg += "Warning: 0 count for at least one component. " + "Fix configuration template and rerun.\n"; + } else if(create_2pc) { + for(size_t i = 0; i < component_count; i++) { + if(type == shard_count_key) { + create_2pc_shard(_default_log_level.value(), i); + } else if(type == sentinel_count_key) { + create_2pc_sentinel(_default_log_level.value(), i); + } else if(type == coordinator_count_key) { + create_2pc_coordinator(_default_log_level.value(), i); + } else { + std::cerr << "Warning: Unrecognized component type, " + << type + << ", in Two-Phase Commit configuration " + "generation." + << std::endl; + } + } + } else { + for(size_t i = 0; i < component_count; i++) { + if(type == shard_count_key) { + create_atomizer_shard(_default_log_level.value(), i); + } else if(type == sentinel_count_key) { + create_atomizer_sentinel(_default_log_level.value(), i); + } else if(type == archiver_count_key) { + create_atomizer_archiver(_default_log_level.value(), i); + } else if(type == atomizer_count_key) { + create_atomizer_atomizer(_default_log_level.value(), i); + } else if(type == watchtower_count_key) { + create_atomizer_watchtower(_default_log_level.value(), i); + } else { + std::cerr << "Warning: Unrecognized component type, " + << type + << ", in Atomizer configuration generation." + << std::endl; + } + } + } + return return_msg; + } + + void + config_generator::create_2pc_shard(const std::string& default_log_level, + size_t current_component_num) { + auto shard_name = "shard" + std::to_string(current_component_num); + + auto db_key = shard_name + "_db"; + set_param_to_config_file(db_key, db_key); + auto log_level_key = shard_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_shard_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + auto count_key = shard_name + "_count"; + auto max_shard_raft_count + = find_value(tmpl_max_shard_raft_replication_count); + size_t shard_raft_rep_count + = (rand() % (max_shard_raft_count.value()) + 1); + set_param_to_config_file(count_key, shard_raft_rep_count); + auto start_key = shard_name + "_start"; + auto end_key = shard_name + "_end"; + set_param_to_config_file(start_key, + shard_info.at(current_component_num) + .current_coverage_expansion_limits.first); + set_param_to_config_file( + end_key, + shard_info.at(current_component_num) + .current_coverage_expansion_limits.second); + + shard_name += "_"; + shard_name += std::to_string(current_component_num); + auto endpoint_key = shard_name + "_endpoint"; + auto endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(endpoint_key, endpoint_val); + auto raft_endpoint_key = shard_name + "_raft_endpoint"; + auto raft_endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(raft_endpoint_key, raft_endpoint_val); + auto read_only_endpoint_key = shard_name + "_readonly_endpoint"; + auto read_only_endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(read_only_endpoint_key, + read_only_endpoint_val); + } + + void + config_generator::create_2pc_sentinel(const std::string& default_log_level, + size_t current_component_num) { + auto sentinel_name + = "sentinel" + std::to_string(current_component_num); + auto endpoint_key = sentinel_name + "_endpoint"; + auto endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(endpoint_key, endpoint_val); + auto log_level_key = sentinel_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_sentinel_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + std::pair key_pair = create_key_pair(); + auto private_key_key = sentinel_name + "_private_key"; + auto private_key_val = key_pair.first; + set_param_to_config_file(private_key_key, private_key_val); + auto public_key_key = sentinel_name + "_public_key"; + auto public_key_val = key_pair.second; + set_param_to_config_file(public_key_key, public_key_val); + } + + void config_generator::create_2pc_coordinator( + const std::string& default_log_level, + size_t current_component_num) { + auto coordinator_name + = "coordinator" + std::to_string(current_component_num); + + auto log_level_key = coordinator_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_coordinator_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + auto count_key = coordinator_name + "_count"; + auto max_coordinator_raft_count + = find_value(tmpl_max_coordinator_raft_replication_count); + size_t coordinator_raft_rep_count + = (rand() % (max_coordinator_raft_count.value()) + 1); + set_param_to_config_file(count_key, coordinator_raft_rep_count); + auto threads_key = coordinator_name + "_max_threads"; + size_t threads_val = 1; + set_param_to_config_file(threads_key, threads_val); + + coordinator_name += "_"; + coordinator_name += std::to_string(current_component_num); + auto endpoint_key = coordinator_name + "_endpoint"; + auto endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(endpoint_key, endpoint_val); + auto raft_endpoint_key = coordinator_name + "_raft_endpoint"; + auto raft_endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(raft_endpoint_key, raft_endpoint_val); + } + + void config_generator::create_atomizer_shard( + const std::string& default_log_level, + size_t current_component_num) { + auto shard_name = "shard" + std::to_string(current_component_num); + auto endpoint_key = shard_name + "_endpoint"; + auto endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(endpoint_key, endpoint_val); + auto db_key = shard_name + "_db"; + set_param_to_config_file(db_key, db_key); + auto log_level_key = shard_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_shard_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + auto start_key = shard_name + "_start"; + auto end_key = shard_name + "_end"; + set_param_to_config_file(start_key, + shard_info.at(current_component_num) + .current_coverage_expansion_limits.first); + set_param_to_config_file( + end_key, + shard_info.at(current_component_num) + .current_coverage_expansion_limits.second); + } + + void config_generator::create_atomizer_sentinel( + const std::string& default_log_level, + size_t current_component_num) { + auto sentinel_name + = "sentinel" + std::to_string(current_component_num); + auto endpoint_key = sentinel_name + "_endpoint"; + auto endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(endpoint_key, endpoint_val); + auto log_level_key = sentinel_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_sentinel_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + std::pair key_pair = create_key_pair(); + auto private_key_key = sentinel_name + "_private_key"; + auto private_key_val = key_pair.first; + set_param_to_config_file(private_key_key, private_key_val); + auto public_key_key = sentinel_name + "_endpoint"; + auto public_key_val = key_pair.second; + set_param_to_config_file(public_key_key, public_key_val); + } + + void config_generator::create_atomizer_archiver( + const std::string& default_log_level, + size_t current_component_num) { + auto archive_name = "archiver" + std::to_string(current_component_num); + auto endpoint_key = archive_name + "_endpoint"; + auto endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(endpoint_key, endpoint_val); + auto db_key = archive_name + "_db"; + set_param_to_config_file(db_key, db_key); + auto log_level_key = archive_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_archiver_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + } + + void config_generator::create_atomizer_atomizer( + const std::string& default_log_level, + size_t current_component_num) { + auto atomizer_name + = "atomizer" + std::to_string(current_component_num); + auto endpoint_key = atomizer_name + "_endpoint"; + auto endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(endpoint_key, endpoint_val); + auto raft_endpoint_key = atomizer_name + "_raft_endpoint"; + auto raft_endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(raft_endpoint_key, raft_endpoint_val); + auto log_level_key = atomizer_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_atomizer_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + } + + void config_generator::create_atomizer_watchtower( + const std::string& default_log_level, + size_t current_component_num) { + auto watchtower_name + = "watchtower" + std::to_string(current_component_num); + auto client_endpoint_key = watchtower_name + "_client_endpoint"; + auto client_endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(client_endpoint_key, client_endpoint_val); + auto internal_endpoint_key = watchtower_name + "_internal_endpoint"; + auto internal_endpoint_val + = cbdc::network::localhost + ":" + std::to_string(get_open_port()); + set_param_to_config_file(internal_endpoint_key, internal_endpoint_val); + auto log_level_key = watchtower_name + "_loglevel"; + auto log_level_val = default_log_level; + set_log_level(tmpl_watchtower_log_level, log_level_val); + set_param_to_config_file(log_level_key, log_level_val); + } + + void config_generator::load_template( + const std::string& filename, + std::map& config_map) { + std::ifstream file(filename); + assert(file.good()); + std::string line; + while(std::getline(file, line)) { + std::istringstream line_stream(line); + std::string key; + if(std::getline(line_stream, key, '=')) { + std::string value; + if(std::getline(line_stream, value)) { + config_map.emplace(key, value); + } + } + } + } + + void config_generator::copy_templates_to_build_dir() { + std::filesystem::path config_dir = m_project_root_dir; + config_dir.append("config").append("tools"); + std::filesystem::path build_config_dir = m_build_dir; + build_config_dir.append("config").append("tools"); + for(auto const& dir_entry : + std::filesystem::directory_iterator{config_dir}) { + std::string filename = dir_entry.path().filename(); + const auto* match_str = ".tmpl"; + std::string tmp_str + = filename.substr(filename.size() - strlen(match_str), + strlen(match_str)); + if(tmp_str == match_str) { + const auto copyOptions + = std::filesystem::copy_options::overwrite_existing; + std::filesystem::copy(dir_entry, + build_config_dir, + copyOptions); + std::cerr << "Copying " << dir_entry.path().string() << " to " + << build_config_dir.string() << std::endl; + } + } + } + + template + [[nodiscard]] auto config_generator::find_value(const std::string& key) + -> std::optional { + if(template_options.find(key) != template_options.end()) { + if(std::holds_alternative(template_options.at(key))) { + return std::get(template_options.at(key)); + } + std::cerr << "Warning: Unknown type for " << key + << " template parameter." << std::endl; + return std::nullopt; + } + std::cerr << "Warning: Missing " << key << " template parameter." + << std::endl; + return std::nullopt; + } + + [[maybe_unused]] auto config_generator::generate_configuration_file() + -> std::string { + if(!m_template_file_is_valid) { + std::filesystem::path temp_build_dir = m_build_dir; + temp_build_dir.append("config").append("tools"); + m_new_config + << "File provided, " + m_template_config_file + + ", did not exist and could not be copied to " + + temp_build_dir.string() + + ". Aborting operation. Please rerun with proper " + "template location \n"; + return m_new_config.str(); + } + + std::map config_map; + load_template(m_template_config_file, config_map); + // Deal with all config values in file with "tmpl_" + auto config_map_it = config_map.begin(); + while(config_map_it != config_map.end()) { + if(config_map_it->first.find(template_prefix) + != std::string::npos) { + template_options.emplace( + config_map_it->first, + parse_value(config_map_it->second, false)); + + } else { + value_t parsed_val = parse_value(config_map_it->second, true); + if(std::holds_alternative(parsed_val)) { + set_param_to_config_file(config_map_it->first, + std::get(parsed_val)); + } else if(std::holds_alternative(parsed_val)) { + set_param_to_config_file(config_map_it->first, + std::get(parsed_val)); + } else if(std::holds_alternative(parsed_val)) { + set_param_to_config_file( + config_map_it->first, + std::get(parsed_val)); + } else { + __builtin_unreachable(); + } + } + config_map_it++; + } + + auto randomize_int = find_value(tmpl_randomize_values); + m_randomize = randomize_int == 1; + if(m_randomize) { + srand(std::chrono::system_clock::now().time_since_epoch().count()); + generator.seed( + std::chrono::system_clock::now().time_since_epoch().count()); + } else { + generator.seed(1); + srand(1); + } + + // Create Components + const auto is_two_phase_mode + = get_param_from_template_file(two_phase_mode, config_map); + const auto shard_count + = get_param_from_template_file(shard_count_key, config_map); + const auto sentinel_count + = get_param_from_template_file(sentinel_count_key, config_map); + + auto shard_size = find_value(tmpl_shard_size); + // Add one since this is 255 and we are 0 indexing but want + // 0-255 inclusive + calculate_shard_coverage(std::get(shard_count), + shard_size.value() + 1); + + if(std::holds_alternative(is_two_phase_mode) + && std::get(is_two_phase_mode) == 1) { + const auto coordinator_count + = get_param_from_template_file(coordinator_count_key, + config_map); + m_new_config << create_component(sentinel_count_key, + std::get(sentinel_count), + true); + m_new_config << create_component(shard_count_key, + std::get(shard_count), + true); + m_new_config << create_component( + coordinator_count_key, + std::get(coordinator_count), + true); + } else { + const auto atomizer_count + = get_param_from_template_file(atomizer_count_key, config_map); + const auto archiver_count + = get_param_from_template_file(archiver_count_key, config_map); + const auto watchtower_count + = get_param_from_template_file(watchtower_count_key, + config_map); + m_new_config << create_component(shard_count_key, + std::get(shard_count), + false); + m_new_config << create_component(sentinel_count_key, + std::get(sentinel_count), + false); + m_new_config << create_component(archiver_count_key, + std::get(archiver_count), + false); + m_new_config << create_component(atomizer_count_key, + std::get(atomizer_count), + false); + m_new_config << create_component( + watchtower_count_key, + std::get(watchtower_count), + false); + } + // add support for pre-seeding + // only has effect if pre-seeding is active + const auto seed_begin + = get_param_from_template_file(seed_from, config_map); + const auto seed_end + = get_param_from_template_file(seed_to, config_map); + if(!std::holds_alternative(seed_begin) + && !std::holds_alternative(seed_end) + && std::get(seed_begin) != std::get(seed_end)) { + std::pair key_pair = create_key_pair(); + set_param_to_config_file(seed_privkey, key_pair.first); + } + std::cerr << "SUCCESS\n"; + return m_new_config.str(); + } +} diff --git a/tools/config_generator/config_generator.hpp b/tools/config_generator/config_generator.hpp new file mode 100644 index 000000000..dc17e19ec --- /dev/null +++ b/tools/config_generator/config_generator.hpp @@ -0,0 +1,161 @@ +// Copyright (c) 2022 MIT Digital Currency Initiative, +// Federal Reserve Bank of Boston +// MITRE Corporation +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef OPENCBDC_TX_CONFIG_TOOLS_CONFIG_GENERATOR_H_ +#define OPENCBDC_TX_CONFIG_TOOLS_CONFIG_GENERATOR_H_ + +#include "util/common/config.hpp" +#include "util/common/random_source.hpp" + +#include +#include +#include + +static constexpr auto MAX_PORT_NUM = 65535; + +// Structure to assist in creating shard id coverage +using ShardInfo = struct shard_info_struct { + std::vector coverage; + size_t shard_id{0}; + size_t numbers_covered{0}; + double overlap_percentage_allowed{0}; + bool still_expanding{true}; + bool allow_overlap{true}; + std::pair current_coverage_expansion_limits; +}; + +using value_t = std::variant; + +namespace cbdc::generate_config { + /// \brief Config_generator implementation. + /// + /// This class takes in a template configuration file, denoted by the + /// ending "*.tmpl" and produces are usable (*.cfg) configuration file. The + /// purpose of this class is to allow the user to create more complicated + /// testing scenarios by removing some amount of manual effort when + /// creating configurations. + class config_generator { + public: + /// Constructor. + /// + /// \param _template_config_file The template configuration file from which + /// the larger more intricate configuration file will be generated. + /// \param _start_port Port to begin using and incrementing from for generated + /// configuration file's endpoints + config_generator(std::string& _template_config_file, + size_t _start_port); + + /// \brief generate_configuration_file + /// Main workhorse method of this class. This method will generate a + /// usable configuration file + /// + /// \return a string with all the error/warning/success messages, if any, + /// that were produced while executing + [[maybe_unused]] auto generate_configuration_file() -> std::string; + + private: + // Boolean that tells us if file is valid or not + bool m_template_file_is_valid{true}; + // Whether to randomize things that can be randomized + bool m_randomize{false}; + // Template file loaded to create configuration file from + std::string& m_template_config_file; + // Incrementing port to use in config file for all ports + unsigned short m_current_port; + // Build directory + std::filesystem::path m_build_dir; + // Project root directory + std::filesystem::path m_project_root_dir; + // Map with shard ranges (shard_id, (start range, end_range) + std::vector shard_info; + std::default_random_engine generator; + static const inline auto m_random_source + = std::make_unique( + cbdc::config::random_source); + static const inline auto m_secp + = std::unique_ptr( + secp256k1_context_create(SECP256K1_CONTEXT_SIGN), + &secp256k1_context_destroy); + + // Where the newly created configuration parameters will be stored as + // we go along generating them + std::stringstream m_new_config; + + std::map template_options; + + // Calculate shard coverage + void calculate_shard_coverage(size_t num_shards, size_t shard_size); + // Helper for calculating shard id coverage + void shard_bookkeeping(const std::vector& array_total, + size_t shard_id); + // Get random value based on mean and standard deviation + [[nodiscard]] auto calculate_normal_distribution_point(size_t mean, + double std_dev) + -> double; + // This is not a failsafe because we are simply generating a + // configuration file here, however, it is still good to check that the + // port is available + [[nodiscard]] auto get_open_port() -> unsigned short; + // Create Private/Public key pair repeatably (repeatable from run to + // run) + [[nodiscard]] static auto create_repeatable_key_pair() + -> std::pair; + // Create Private/Public key pair randomly (NOT repeatable from run to + // run) + [[nodiscard]] static auto create_random_key_pair() + -> std::pair; + // Create Private/Public key pair + [[nodiscard]] auto create_key_pair() const + -> std::pair; + // Parse value as int, double or string from config file + [[nodiscard]] static auto parse_value(const std::string& value, + bool keep_quotes) -> value_t; + [[nodiscard]] static auto get_param_from_template_file( + const std::string& option, + std::map& config_map) + -> std::variant; + void set_param_to_config_file(const std::string& key, + const std::string& value); + void set_param_to_config_file(const std::string& key, double value); + void set_param_to_config_file(const std::string& key, size_t value); + // Helper to set proper log level for either Two-Phase Commit or + // Atomizer components + void set_log_level(const std::string& key, std::string& log_level); + // Method to create all the related components for generated config + // file + [[maybe_unused]] auto create_component(const char* type, + size_t component_count, + bool create_2pc) -> std::string; + void create_2pc_shard(const std::string& default_log_level, + size_t current_component_num); + void create_2pc_sentinel(const std::string& default_log_level, + size_t current_component_num); + void create_2pc_coordinator(const std::string& default_log_level, + size_t current_component_num); + void create_atomizer_shard(const std::string& default_log_level, + size_t current_component_num); + void create_atomizer_sentinel(const std::string& default_log_level, + size_t current_component_num); + void create_atomizer_archiver(const std::string& default_log_level, + size_t current_component_num); + void create_atomizer_atomizer(const std::string& default_log_level, + size_t current_component_num); + void create_atomizer_watchtower(const std::string& default_log_level, + size_t current_component_num); + // Load template file from which we will generate the configuration + // file. + static void + load_template(const std::string& filename, + std::map& config_map); + void copy_templates_to_build_dir(); + template + [[nodiscard]] auto find_value(const std::string& key) + -> std::optional; + }; +} + +#endif // OPENCBDC_TX_CONFIG_TOOLS_CONFIG_GENERATOR_H_ diff --git a/tools/config_generator/generate_config.cpp b/tools/config_generator/generate_config.cpp new file mode 100644 index 000000000..c902b7ff8 --- /dev/null +++ b/tools/config_generator/generate_config.cpp @@ -0,0 +1,69 @@ +// Copyright (c) 2022 MIT Digital Currency Initiative, +// Federal Reserve Bank of Boston +// MITRE Corporation +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "config_generator.hpp" + +#include + +auto main(int argc, char** argv) -> int { + auto args = cbdc::config::get_args(argc, argv); + // Min and max number of params + size_t max_param_num = 4; + size_t min_param_num = 3; + // Default build dir + std::string build_dir = "build"; + // Help string + std::string help_string + = "Usage: " + args[0] + + " > \n\nPARAM 1, : The relative " + "path from current working directory to the template configuration " + "file including the filename itself.\nPARAM 2, : The first port number to use and increment from. Must be " + "less than 65535.\nPARAM 3, : The path relative " + "to project root directory, but not including project root " + "directory itself, of the build directory. Use '/' as separators if " + "build dir has depth is greater than 1. E.g. if build directory is " + "located at '/tmp/build' input should be 'tmp/build'. " + "(defaults to 'build' if left empty)."; + + bool valid_config = true; + if(args.size() == 2 && (args[1] == "-h" || args[1] == "--help")) { + // Catch help in case user intuitively types this + std::cerr << help_string << std::endl; + valid_config = false; + } else if(args.size() < min_param_num || args.size() > max_param_num) { + // Catch error case where there are no params + std::cerr << help_string << std::endl + << std::endl + << "Rerun with proper parameters." << std::endl; + valid_config = false; + } + if(!valid_config) { + return -1; + } + + auto port_is_valid = std::isdigit(*args[2].c_str()); + + // Catch error case where port numbers are invalid (either not a number or + // too large) + if(port_is_valid == 0) { + std::cerr << "Port number provided, " << args[2] + << ", is not a valid number. Exiting..." << std::endl; + return -1; + } + auto port_num = static_cast(std::stoull(args[2])); + if(port_num > MAX_PORT_NUM) { + std::cerr << "Port number provided, " << args[2] + << ", is too large. Exiting..." << std::endl; + return -1; + } + cbdc::generate_config::config_generator new_config_gen(args[1], port_num); + auto cfg_or_err = new_config_gen.generate_configuration_file(); + std::cout << cfg_or_err << std::endl; + std::cout << std::endl; + return 0; +}