From a07620e0b673c8f07f0b6e5af392966220e20103 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Sat, 20 Dec 2025 21:54:55 -0500 Subject: [PATCH 1/4] Add WASM build script and fix paths for standalone repo - Port build_wasm_beekeeper.sh from hive repository - Fix wasm_project.cmake paths for standalone structure - Update package.json repository URL --- .../beekeeper_wasm/cmake/wasm_project.cmake | 5 +- .../beekeeper/beekeeper_wasm/package.json | 2 +- scripts/build_wasm_beekeeper.sh | 65 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100755 scripts/build_wasm_beekeeper.sh diff --git a/programs/beekeeper/beekeeper_wasm/cmake/wasm_project.cmake b/programs/beekeeper/beekeeper_wasm/cmake/wasm_project.cmake index 26ed459..f0e415e 100644 --- a/programs/beekeeper/beekeeper_wasm/cmake/wasm_project.cmake +++ b/programs/beekeeper/beekeeper_wasm/cmake/wasm_project.cmake @@ -6,8 +6,9 @@ LIST(APPEND BOOST_COMPONENTS filesystem program_options system) -include( ${CMAKE_CURRENT_LIST_DIR}/../../../../cmake/hive_targets.cmake ) -add_subdirectory( ${CMAKE_CURRENT_LIST_DIR}/../../../../libraries/fc build_fc_minimal ) +# Paths adjusted for beekeeper standalone repo structure +include( ${CMAKE_CURRENT_LIST_DIR}/../../../../libraries/plugins/cmake/hive_targets.cmake ) +add_subdirectory( ${CMAKE_CURRENT_LIST_DIR}/../../../../libraries/plugins/libraries/fc build_fc_minimal ) set(WASM_RUNTIME_COMPONENT_NAME "wasm_runtime_components") diff --git a/programs/beekeeper/beekeeper_wasm/package.json b/programs/beekeeper/beekeeper_wasm/package.json index af8b2f1..a293127 100644 --- a/programs/beekeeper/beekeeper_wasm/package.json +++ b/programs/beekeeper/beekeeper_wasm/package.json @@ -75,7 +75,7 @@ }, "repository": { "type": "git", - "url": "https://gitlab.syncad.com/hive/hive.git" + "url": "https://gitlab.syncad.com/hive/beekeeper.git" }, "engines": { "node": "^20.11 || >= 21.2" diff --git a/scripts/build_wasm_beekeeper.sh b/scripts/build_wasm_beekeeper.sh new file mode 100755 index 0000000..e576b7e --- /dev/null +++ b/scripts/build_wasm_beekeeper.sh @@ -0,0 +1,65 @@ +#! /bin/bash + +set -xeuo pipefail + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +PROJECT_DIR="${SCRIPTPATH}/.." + +DIRECT_EXECUTION_DEFAULT=0 +EXECUTION_PATH_DEFAULT="/src/" + +# Check for usage inside dev container providing all tools (emscripten image) +if [ $# -eq 0 ]; then + EXECUTOR=$(whoami) + if [ "${EXECUTOR}" = "emscripten" ]; then + DIRECT_EXECUTION_DEFAULT=1 + EXECUTION_PATH_DEFAULT="${PROJECT_DIR}" + fi +fi + +DIRECT_EXECUTION=${1:-${DIRECT_EXECUTION_DEFAULT}} +EXECUTION_PATH=${2:-"${EXECUTION_PATH_DEFAULT}"} + +build() { + BUILD_DIR="${EXECUTION_PATH}/programs/beekeeper/beekeeper_wasm/src/build" + mkdir -vp "${BUILD_DIR}" + cd "${BUILD_DIR}" + + #-DBoost_DEBUG=TRUE -DBoost_VERBOSE=TRUE -DCMAKE_STATIC_LIBRARY_SUFFIX=".a;.bc" + cmake \ + -DBoost_NO_WARN_NEW_VERSIONS=1 \ + -DBoost_USE_STATIC_RUNTIME=ON \ + -DCMAKE_TOOLCHAIN_FILE=/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -G "Ninja" \ + -S "${EXECUTION_PATH}/programs/beekeeper/beekeeper_wasm/" \ + -B "${BUILD_DIR}" + ninja -v -j8 2>&1 | tee -i "${BUILD_DIR}/build.log" + + cmake --install "${BUILD_DIR}" --component wasm_runtime_components --prefix "${BUILD_DIR}/" + + # Emscripten still uses redundant createRequire for legacy CJS support - remove it so we have proper bundlers support + sed -i "s#var require = createRequire(import.meta.url);##g" "${BUILD_DIR}/beekeeper_wasm.node.js" + sed -i "s#const {createRequire} = await import(\"module\");##g" "${BUILD_DIR}/beekeeper_wasm.node.js" + + # Replace requires with our await import-s + sed -i "s#require(\"fs\");#(await import(\"fs\"))#g" "${BUILD_DIR}/beekeeper_wasm.node.js" + sed -i "s#require(\"path\")#(await import(\"path\"))#g" "${BUILD_DIR}/beekeeper_wasm.node.js" + sed -i "s#require(\"url\")#(await import(\"url\"))#g" "${BUILD_DIR}/beekeeper_wasm.node.js" + + # Remove Node.js "crypto" module import, as we already have crypto API support in Node.js 19+ + sed -i "s#var nodeCrypto = require(\"crypto\");##g" "${BUILD_DIR}/beekeeper_wasm.node.js" + sed -i "s#return view => nodeCrypto.randomFillSync(view);##g" "${BUILD_DIR}/beekeeper_wasm.node.js" +} + +if [ ${DIRECT_EXECUTION} -eq 0 ]; then + echo "Performing a docker run" + docker run \ + -it --rm \ + -v "${PROJECT_DIR}/":"${EXECUTION_PATH}" \ + -u $(id -u):$(id -g) \ + registry.gitlab.syncad.com/hive/common-ci-configuration/emsdk:4.0.18-1@sha256:79edc8ecfe7b13848466d33791daa955eb1762edc329a48c07aa700bc6cfb712 \ + /bin/bash /src/scripts/build_wasm_beekeeper.sh 1 "${EXECUTION_PATH}" +else + echo "Performing a build..." + cd "${EXECUTION_PATH}" + build +fi -- GitLab From 454bc15fa80b8e759562a145bf628d2c442ba59a Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Sat, 20 Dec 2025 21:55:01 -0500 Subject: [PATCH 2/4] Add npm-common-config submodule for TypeScript build Required for pnpm catalog dependencies and shared TypeScript configuration. --- .gitmodules | 3 +++ programs/beekeeper/beekeeper_wasm/npm-common-config | 1 + 2 files changed, 4 insertions(+) create mode 160000 programs/beekeeper/beekeeper_wasm/npm-common-config diff --git a/.gitmodules b/.gitmodules index 8403e75..f006e73 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "libraries/plugins"] path = libraries/plugins url = ../plugins.git +[submodule "programs/beekeeper/beekeeper_wasm/npm-common-config"] + path = programs/beekeeper/beekeeper_wasm/npm-common-config + url = ../common-ci-configuration.git diff --git a/programs/beekeeper/beekeeper_wasm/npm-common-config b/programs/beekeeper/beekeeper_wasm/npm-common-config new file mode 160000 index 0000000..9925b2a --- /dev/null +++ b/programs/beekeeper/beekeeper_wasm/npm-common-config @@ -0,0 +1 @@ +Subproject commit 9925b2ae030bd88e7eb532933189c013211a1d91 -- GitLab From 056014ff4f3c993b38bed5caf2411a61106298c8 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Sat, 20 Dec 2025 21:55:10 -0500 Subject: [PATCH 3/4] Add CI configuration with native and WASM builds and tests CI Pipeline: - beekeeper_native_build: Build native executable and test binary - beekeeper_native_test: Run C++ unit tests - beekeeper_wasm_build: Build WASM module and TypeScript bindings - beekeeper_wasm_test: Run Playwright browser tests Build changes: - Disable websocketpp tests/examples to fix Boost CMake compatibility - Create beekeeper_lib static library for test linking - Fix fc tests linking to Boost.Unit_Test_Framework Test infrastructure: - Port beekeeper unit tests from hive repository - Use Boost.Test included header with BOOST_TEST_MODULE --- .gitlab-ci.yml | 90 + CMakeLists.txt | 42 + programs/beekeeper/beekeeper/CMakeLists.txt | 13 +- tests/CMakeLists.txt | 7 + tests/unit/CMakeLists.txt | 48 + tests/unit/beekeeper_api_tests.cpp | 947 +++++++ tests/unit/beekeeper_mgr.hpp | 83 + tests/unit/beekeeper_tests.cpp | 2437 +++++++++++++++++++ 8 files changed, 3664 insertions(+), 3 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 tests/CMakeLists.txt create mode 100644 tests/unit/CMakeLists.txt create mode 100644 tests/unit/beekeeper_api_tests.cpp create mode 100644 tests/unit/beekeeper_mgr.hpp create mode 100644 tests/unit/beekeeper_tests.cpp diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..9d2efda --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,90 @@ +stages: + - build + - test + +variables: + GIT_SUBMODULE_STRATEGY: recursive + +beekeeper_native_build: + stage: build + image: registry.gitlab.syncad.com/hive/hive/ci-base-image@sha256:50bcbfe37fc031a902071ceaf586aff4554b15aa84e33b8bd84c31ad2386a26f + script: + - mkdir -p build && cd build + - cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON .. + - make -j$(nproc) + artifacts: + paths: + - build/programs/beekeeper/beekeeper/beekeeper + - build/tests/unit/beekeeper_test + expire_in: 1 week + tags: + - public-runner-docker + +beekeeper_native_test: + stage: test + image: registry.gitlab.syncad.com/hive/hive/ci-base-image@sha256:50bcbfe37fc031a902071ceaf586aff4554b15aa84e33b8bd84c31ad2386a26f + needs: + - job: beekeeper_native_build + artifacts: true + script: + - ./build/tests/unit/beekeeper_test --log_level=test_suite + tags: + - public-runner-docker + +beekeeper_wasm_build: + stage: build + image: registry.gitlab.syncad.com/hive/common-ci-configuration/emsdk:4.0.18-1@sha256:79edc8ecfe7b13848466d33791daa955eb1762edc329a48c07aa700bc6cfb712 + variables: + SOURCE_DIR: "${CI_PROJECT_DIR}/programs/beekeeper/beekeeper_wasm" + before_script: + - git config --global --add safe.directory '*' + - . "${NVM_DIR}/nvm.sh" + - nvm use "${NODEJS_VERSION}" + script: + # Build WASM + - ./scripts/build_wasm_beekeeper.sh 1 "${CI_PROJECT_DIR}" + # Install npm dependencies and build TypeScript + - cd "${SOURCE_DIR}" + - pnpm install --frozen-lockfile + - pnpm build + artifacts: + paths: + - "${SOURCE_DIR}/dist" + - "${SOURCE_DIR}/src/build/*.wasm" + - "${SOURCE_DIR}/src/build/*.js" + - "${SOURCE_DIR}/src/build/*.d.ts" + - "${SOURCE_DIR}/src/build/node" + - "${SOURCE_DIR}/src/build/web" + expire_in: 1 week + tags: + - public-runner-docker + +beekeeper_wasm_test: + stage: test + image: registry.gitlab.syncad.com/hive/common-ci-configuration/emsdk:4.0.18-1@sha256:79edc8ecfe7b13848466d33791daa955eb1762edc329a48c07aa700bc6cfb712 + variables: + SOURCE_DIR: "${CI_PROJECT_DIR}/programs/beekeeper/beekeeper_wasm" + needs: + - job: beekeeper_wasm_build + artifacts: true + before_script: + - git config --global --add safe.directory '*' + - . "${NVM_DIR}/nvm.sh" + - nvm use "${NODEJS_VERSION}" + script: + - cd "${SOURCE_DIR}" + - pnpm install --frozen-lockfile + # Install playwright browser + - pnpm exec playwright install chromium + # Run tests + - pnpm test + artifacts: + paths: + - "${SOURCE_DIR}/results.xml" + - "${SOURCE_DIR}/results.json" + reports: + junit: "${SOURCE_DIR}/results.xml" + when: always + expire_in: 1 week + tags: + - public-runner-docker diff --git a/CMakeLists.txt b/CMakeLists.txt index a2f6b41..f059647 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,12 +14,54 @@ message(STATUS "=== Beekeeper Standalone Build ===") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "") +# Find Boost with unit_test_framework for fc tests +find_package(Boost REQUIRED COMPONENTS unit_test_framework) + +# Disable websocketpp tests/examples to avoid Boost CMake compatibility issues +# (websocketpp's Boost detection is incompatible with newer Boost/CMake versions) +set(BUILD_TESTS OFF CACHE BOOL "Disable websocketpp tests" FORCE) +set(BUILD_EXAMPLES OFF CACHE BOOL "Disable websocketpp examples" FORCE) + # Add plugins submodule (includes fc and appbase) add_subdirectory(libraries/plugins) +# Re-enable tests for beekeeper itself +set(BUILD_TESTS ON CACHE BOOL "Enable beekeeper tests" FORCE) + +# Fix fc tests linking - they need Boost.Unit_Test_Framework +# (fc tests link only to fc, but don't get Boost.Test symbols) +foreach(fc_test_target all_tests bloom_test ntp_test task_cancel_test thread_test + real128_test blind hmac_test ecc_test log_test + saturation_test ecdsa_canon_test sha_test) + if(TARGET ${fc_test_target}) + target_link_libraries(${fc_test_target} Boost::unit_test_framework) + endif() +endforeach() + # Create target for minimal protocol config (standalone beekeeper builds only) add_library(beekeeper_protocol_config INTERFACE) target_include_directories(beekeeper_protocol_config INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") # Add beekeeper programs add_subdirectory(programs/beekeeper) + +# Optional: Build tests +option(BUILD_TESTS "Build unit tests" OFF) +if(BUILD_TESTS) + message(STATUS "Building tests enabled") + + # Build native beekeeper_wasm library for testing + # (The actual beekeeper_wasm is WASM-only, but tests need a native version) + add_library(beekeeper_wasm_native STATIC + programs/beekeeper/beekeeper_wasm/beekeeper_wasm_api.cpp + programs/beekeeper/beekeeper_wasm/beekeeper_wasm_app.cpp + ) + target_include_directories(beekeeper_wasm_native PUBLIC + programs/beekeeper/beekeeper_wasm/include + ) + target_link_libraries(beekeeper_wasm_native PUBLIC + beekeeper_core + ) + + add_subdirectory(tests) +endif() diff --git a/programs/beekeeper/beekeeper/CMakeLists.txt b/programs/beekeeper/beekeeper/CMakeLists.txt index bd07ae1..d3d4991 100644 --- a/programs/beekeeper/beekeeper/CMakeLists.txt +++ b/programs/beekeeper/beekeeper/CMakeLists.txt @@ -1,7 +1,7 @@ file(GLOB HEADERS "include/beekeeper/*.hpp") -add_executable(beekeeper - main.cpp +# Static library with beekeeper implementation (used by both executable and tests) +add_library(beekeeper_lib STATIC beekeeper_app.cpp beekeeper_instance.cpp beekeeper_wallet_api.cpp @@ -11,7 +11,7 @@ add_executable(beekeeper ${HEADERS} ) -target_link_libraries(beekeeper +target_link_libraries(beekeeper_lib PUBLIC appbase webserver_plugin app_status_api_plugin @@ -20,6 +20,13 @@ target_link_libraries(beekeeper beekeeper_protocol_config ) +target_include_directories(beekeeper_lib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") + +# Executable +add_executable(beekeeper main.cpp) + +target_link_libraries(beekeeper beekeeper_lib) + target_include_directories(beekeeper PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") # Enable static linking for portability diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..a7a4481 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,7 @@ +# Beekeeper Tests +# +# Enable tests with: cmake -DBUILD_TESTS=ON .. + +enable_testing() + +add_subdirectory(unit) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 0000000..40fbc26 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,48 @@ +# Beekeeper Unit Tests +# +# These tests are ported from the hive repository and test the beekeeper +# wallet manager functionality. +# +# Some tests require HIVE_PROTOCOL_AVAILABLE to be defined (for transaction signing tests) +# Some tests require HIVE_CHAIN_AVAILABLE to be defined (for database fixture tests) +# +# When building standalone beekeeper without hive protocol, these protocol-dependent +# tests are automatically skipped via preprocessor guards. + +cmake_minimum_required(VERSION 3.16) + +find_package(Boost REQUIRED COMPONENTS unit_test_framework) + +# Main test executable +add_executable(beekeeper_test + beekeeper_tests.cpp + beekeeper_api_tests.cpp +) + +target_include_directories(beekeeper_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/programs/beekeeper/core/include + ${CMAKE_SOURCE_DIR}/programs/beekeeper/beekeeper/include + ${CMAKE_SOURCE_DIR}/programs/beekeeper/beekeeper_wasm/include +) + +target_link_libraries(beekeeper_test PRIVATE + beekeeper_core + beekeeper_lib + beekeeper_wasm_native + fc + appbase + Boost::unit_test_framework +) + +# Define IS_TEST_NET to enable test compilation +target_compile_definitions(beekeeper_test PRIVATE IS_TEST_NET) + +# Optional: Define HIVE_PROTOCOL_AVAILABLE if hive protocol is linked +# target_compile_definitions(beekeeper_test PRIVATE HIVE_PROTOCOL_AVAILABLE) + +# Optional: Define HIVE_CHAIN_AVAILABLE if full hive chain is available +# target_compile_definitions(beekeeper_test PRIVATE HIVE_CHAIN_AVAILABLE) + +# Register with CTest +add_test(NAME beekeeper_unit_tests COMMAND beekeeper_test --log_level=test_suite) diff --git a/tests/unit/beekeeper_api_tests.cpp b/tests/unit/beekeeper_api_tests.cpp new file mode 100644 index 0000000..6c52c82 --- /dev/null +++ b/tests/unit/beekeeper_api_tests.cpp @@ -0,0 +1,947 @@ +// NOTE: This test file requires the full hive chain database fixture +// and can only be compiled when building with the hive repository. +// When building standalone beekeeper, these tests are skipped. +#if defined(IS_TEST_NET) && defined(HIVE_CHAIN_AVAILABLE) +#include + +#include "../db_fixture/hived_fixture.hpp" + +#include "beekeeper_mgr.hpp" + +#include + +#include +#include +#include + +#include + +#include +#include + +using namespace hive::chain; + +using beekeeper_wallet_manager = beekeeper::beekeeper_wallet_manager; +using beekeeper_instance = beekeeper::beekeeper_instance; +using beekeeper_wallet_api = beekeeper::beekeeper_wallet_api; + +BOOST_FIXTURE_TEST_SUITE( beekeeper_api_tests, json_rpc_database_fixture ) + +BOOST_AUTO_TEST_CASE(beekeeper_api_unlock_blocking) +{ + try + { + auto _sleep = []( uint32_t delay ) + { + std::this_thread::sleep_for( std::chrono::milliseconds( delay ) ); + }; + + struct wallet_data + { + std::string name; + std::string password; + }; + + struct wallet_status + { + std::string name; + bool unlocked = false; + }; + + struct cmp + { + bool operator()( const wallet_status& a, const wallet_status& b ) const + { + return a.name < b.name; + } + }; + using set_type = std::set; + + std::vector _wallets{ {"w0"}, {"w1"}, {"w2"} }; + + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + uint64_t _interval = 500; + + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 3, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + auto _list_created_wallets_checker = [&_api]( const std::string& token, const set_type& unlock_statuses ) + { + flat_set _wallets = _api.list_created_wallets( beekeeper::list_wallets_args{ token } ).wallets; + BOOST_REQUIRE( _wallets.size() == unlock_statuses.size() ); + + for( auto& item : _wallets ) + { + auto _found = unlock_statuses.find( { item.name } ); + BOOST_REQUIRE( _found != unlock_statuses.end() ); + BOOST_REQUIRE_EQUAL( item.unlocked, _found->unlocked ); + } + }; + + //************preparation************ + std::string _token = _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token; + for( size_t i = 0; i < _wallets.size(); ++i ) + { + auto _password = _api.create( beekeeper::create_args{ _token, _wallets[i].name } ).password; + BOOST_REQUIRE( !_password.empty() ); + _wallets[i].password = _password; + } + _list_created_wallets_checker( _token, { {"w0", true}, {"w1", true}, {"w2", true} } ); + //************end of preparation************ + + { + BOOST_TEST_MESSAGE( "lock_all" ); + _api.lock_all( beekeeper::lock_all_args{ _token } ); + _list_created_wallets_checker( _token, { {"w0", false}, {"w1", false}, {"w2", false} } ); + } + { + { + BOOST_TEST_MESSAGE( "sleep: _interval + 5" ); + _sleep(_interval + 5 ); + BOOST_TEST_MESSAGE( "unlock: _wallets[0]" ); + _api.unlock( beekeeper::unlock_args{ _token, _wallets[0].name, _wallets[0].password } ); + _list_created_wallets_checker( _token, { {"w0", true}, {"w1", false}, {"w2", false} } ); + } + { + BOOST_TEST_MESSAGE( "sleep: " + std::to_string( _interval / 2 ) ); + _sleep( _interval / 2 ); + BOOST_TEST_MESSAGE( "unlock: _wallets[1]" ); + _api.unlock( beekeeper::unlock_args{ _token, _wallets[1].name, _wallets[1].password } ); + _list_created_wallets_checker( _token, { {"w0", true}, {"w1", true}, {"w2", false} } ); + } + { + BOOST_TEST_MESSAGE( "sleep: 5" ); + _sleep( 5 ); + BOOST_TEST_MESSAGE( "unlock: _wallets[2]" ); + _api.unlock( beekeeper::unlock_args{ _token, _wallets[2].name, _wallets[2].password } ); + _list_created_wallets_checker( _token, { {"w0", true}, {"w1", true}, {"w2", true} } ); + } + } + { + BOOST_TEST_MESSAGE( "lock_all" ); + _api.lock_all( beekeeper::lock_all_args{ _token } ); + _list_created_wallets_checker( _token, { {"w0", false}, {"w1", false}, {"w2", false} } ); + } + { + { + BOOST_TEST_MESSAGE( "sleep: " + std::to_string( _interval + 5 ) ); + _sleep(_interval + 5 ); + BOOST_TEST_MESSAGE( "unlock: _wallets[0]" ); + bool _unlock_passed = true; + try + { + _api.unlock( beekeeper::unlock_args{ _token, _wallets[0].name, _wallets[1].password } ); + } + catch( const fc::exception& e ) + { + _unlock_passed = false; + BOOST_TEST_MESSAGE( e.to_string() ); + BOOST_REQUIRE( e.to_string().find( "Invalid password for wallet:" ) != std::string::npos ); + } + BOOST_REQUIRE( _unlock_passed == false ); + _list_created_wallets_checker( _token, { {"w0", false}, {"w1", false}, {"w2", false} } ); + } + { + BOOST_TEST_MESSAGE( "sleep: " + std::to_string( _interval / 2 ) ); + _sleep( _interval / 2 ); + BOOST_TEST_MESSAGE( "unlock: _wallets[1]" ); + try + { + _api.unlock( beekeeper::unlock_args{ _token, _wallets[1].name, _wallets[1].password } ); + } + catch( const fc::exception& e ) + { + BOOST_TEST_MESSAGE( e.to_string() ); + BOOST_REQUIRE( e.to_string().find( "unlock is not accessible" ) != std::string::npos ); + } + _list_created_wallets_checker( _token, { {"w0", false}, {"w1", false}, {"w2", false} } ); + } + { + BOOST_TEST_MESSAGE( "sleep: 5" ); + _sleep( 5 ); + BOOST_TEST_MESSAGE( "unlock: _wallets[2]" ); + try + { + _api.unlock( beekeeper::unlock_args{ _token, _wallets[2].name, _wallets[2].password } ); + } + catch( const fc::exception& e ) + { + BOOST_TEST_MESSAGE( e.to_string() ); + BOOST_REQUIRE( e.to_string().find( "unlock is not accessible" ) != std::string::npos ); + } + _list_created_wallets_checker( _token, { {"w0", false}, {"w1", false}, {"w2", false} } ); + } + { + BOOST_TEST_MESSAGE( "sleep: " + std::to_string( _interval / 2 ) ); + _sleep( _interval / 2 ); + BOOST_TEST_MESSAGE( "unlock: _wallets[1]" ); + _api.unlock( beekeeper::unlock_args{ _token, _wallets[1].name, _wallets[1].password } ); + _list_created_wallets_checker( _token, { {"w0", false}, {"w1", true}, {"w2", false} } ); + } + } + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(beekeeper_api_endpoints) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + uint64_t _interval = 500; + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 3, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + std::string _wallet_name = "w0"; + std::string _private_key = "5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n"; + std::string _public_key = "STM6LLegbAgLAy28EHrffBVuANFWcFgmqRMW13wBmTExqFE9SCkg4"; + hive::protocol::digest_type _sig_digest = hive::protocol::digest_type( "9b29ba0710af3918e81d7b935556d7ab205d8a8f5ca2e2427535980c2e8bdaff" ); + + std::string _token = _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token; + auto _password = _api.create( beekeeper::create_args{ _token, _wallet_name } ).password; + BOOST_REQUIRE( !_password.empty() ); + + _api.open( beekeeper::wallet_args{ _token, _wallet_name } ); + + flat_set _wallets = _api.list_wallets( beekeeper::list_wallets_args{ _token } ).wallets; + BOOST_REQUIRE( _wallets.size() == 1 ); + BOOST_REQUIRE( _wallets.begin()->unlocked == true ); + + const uint32_t _nr_threads = 10; + + std::vector> threads; + + BOOST_SCOPE_EXIT(&threads) + { + for( auto& thread : threads ) + if( thread ) + thread->join(); + } BOOST_SCOPE_EXIT_END + + auto _call = [&]( int nr_thread ) + { + uint32_t _max = 10000; + for( uint32_t _cnt = 0; _cnt < _max; ++_cnt ) + { + switch( nr_thread ) + { + case 0: + { + try + { + _api.lock( beekeeper::lock_args{ _token, _wallet_name } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "Unable to lock a locked wallet" ) != std::string::npos || + e.to_string().find( "Wallet not found" ) != std::string::npos + ); + } + }break; + case 1: + { + try + { + _api.unlock( beekeeper::unlock_args{ _token, _wallet_name, _password } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "Wallet is already unlocked" ) != std::string::npos || + e.to_string().find( "unlock is not accessible" ) != std::string::npos + ); + } + }break; + case 2: + { + _api.lock_all( beekeeper::lock_all_args{ _token } ); + }break; + case 3: + { + _api.list_wallets( beekeeper::list_wallets_args{ _token } ); + }break; + case 4: + { + try + { + _api.get_public_keys( beekeeper::get_public_keys_args{ _token } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "You don't have any wallet" ) != std::string::npos || + e.to_string().find( "You don't have any unlocked wallet" ) != std::string::npos + ); + } + }break; + case 5: + { + try + { + _api.import_key( beekeeper::import_key_args{ _token, _wallet_name, _private_key } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "Wallet not found" ) != std::string::npos || + e.to_string().find( "Wallet is locked" ) != std::string::npos + ); + } + }break; + case 6: + { + try + { + _api.remove_key( beekeeper::remove_key_args{ _token, _wallet_name, _public_key } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "Wallet not found" ) != std::string::npos || + e.to_string().find( "Wallet is locked" ) != std::string::npos || + e.to_string().find( "Key not in wallet" ) != std::string::npos + ); + } + }break; + case 7: + { + try + { + _api.sign_digest( beekeeper::sign_digest_args{ _token, _sig_digest, _public_key } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "not found in unlocked wallets" ) != std::string::npos ); + } + }break; + case 8: + { + _api.open( beekeeper::open_args{ _token, _wallet_name } ); + }break; + case 9: + { + _api.close( beekeeper::close_args{ _token, _wallet_name } ); + }break; + case 10: + { + _api.is_wallet_unlocked( beekeeper::is_wallet_unlocked_args{ _token, _wallet_name } ); + }break; + } + } + }; + + for( size_t i = 0; i < _nr_threads; ++i ) + threads.emplace_back( std::make_shared( _call, i ) ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(beekeeper_timeout_list_wallets_stability) +{ + try + { + auto _run = [this]( bool set_timeout_enabled ) + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + uint64_t _interval = 500; + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 3, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + std::string _token = _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token; + + struct wallet + { + std::string name; + std::string password; + }; + std::vector _wallets{ + { "0" }, { "1" }, { "2" }, { "3" }, { "4" }, + { "5" }, { "6" }, { "7" }, { "8" }, { "9" } + }; + + for( auto& wallet : _wallets ) + { + wallet.password = _api.create( beekeeper::create_args{ _token, wallet.name } ).password; + _api.close( beekeeper::close_args{ _token, wallet.name } ); + } + + { + for( auto& wallet : _wallets ) + { + _api.unlock( beekeeper::unlock_args{ _token, wallet.name, wallet.password } ); + } + } + + const uint32_t _nr_threads = 2; + + std::vector> threads; + + std::mutex _mtx; + + BOOST_SCOPE_EXIT(&threads) + { + for( auto& thread : threads ) + if( thread ) + thread->join(); + } BOOST_SCOPE_EXIT_END + + auto _call = [&]( int nr_thread ) + { + uint32_t _max = 10; + for( uint32_t _cnt = 0; _cnt < _max; ++_cnt ) + { + switch( nr_thread ) + { + case 0: + { + if( set_timeout_enabled ) + { + if( _cnt ) + std::this_thread::sleep_for( std::chrono::milliseconds( 1300 ) ); + { + std::lock_guard _guard( _mtx ); + _api.set_timeout( beekeeper::set_timeout_args{ _token, 1 } ); + } + } + else + { + if( _cnt ) + std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) ); + { + std::lock_guard _guard( _mtx ); + _api.lock_all( beekeeper::lock_all_args{ _token } ); + } + } + }break; + case 1: + { + size_t _cnt_locked; + size_t _cnt_unlocked; + + do + { + _cnt_locked = 0; + _cnt_unlocked = 0; + + flat_set _w = _api.list_wallets( beekeeper::list_wallets_args{ _token } ).wallets; + for( auto& wallet : _w ) + { + if( wallet.unlocked ) + ++_cnt_unlocked; + else + ++_cnt_locked; + } + + // BOOST_TEST_MESSAGE("unlocked: " + std::to_string( _cnt_unlocked ) + " locked: " + std::to_string( _cnt_locked ) + (set_timeout_enabled ? " | set_timeout_enabled" : "") ); + + BOOST_REQUIRE( _cnt_unlocked + _cnt_locked == _wallets.size() ); + BOOST_REQUIRE( _cnt_unlocked == 0 || _cnt_unlocked == _wallets.size() ); + BOOST_REQUIRE( _cnt_locked == 0 || _cnt_locked == _wallets.size() ); + + } while( _cnt_locked < _wallets.size() ); + + { + std::lock_guard _guard( _mtx ); + for( auto& wallet : _wallets ) + { + _api.unlock( beekeeper::unlock_args{ _token, wallet.name, wallet.password } ); + } + } + BOOST_TEST_MESSAGE("====================iteration " + std::to_string( _cnt ) + " finished====================" ); + + }break; + } + } + }; + + for( size_t i = 0; i < _nr_threads; ++i ) + threads.emplace_back( std::make_shared( _call, i ) ); + }; + + BOOST_TEST_MESSAGE("====================set_timeout====================" ); + _run( true/*set_timeout_enabled*/ ); + + BOOST_TEST_MESSAGE("====================lock_all====================" ); + _run( false/*set_timeout_enabled*/ ); + + } FC_LOG_AND_RETHROW() +} + +struct password +{ + std::mutex mtx; + std::vector tokens; + + auto add( const std::string& token ) + { + std::lock_guard _guard( mtx ); + tokens.emplace_back( token ); + }; + + auto get( bool remove = true ) + { + std::string _token; + std::lock_guard _guard( mtx ); + if( !tokens.empty() ) + { + auto _idx = rand() % tokens.size(); + _token = tokens[ _idx ]; + + if( remove ) + tokens.erase( tokens.begin() + _idx ); + } + return _token; + }; +}; + +BOOST_AUTO_TEST_CASE(beekeeper_api_sessions_create_close) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + uint64_t _interval = 500; + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 64, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + std::srand( time(0) ); + + struct counters + { + size_t limit = 1000; + + std::atomic_size_t create_session_cnt = { 0 }; + std::atomic_size_t create_session_error_cnt = { 0 }; + std::atomic_size_t close_session_cnt = { 0 }; + std::atomic_size_t close_session_error_cnt = { 0 }; + + void inc( std::atomic_size_t& val ) + { + val.store( val.load() + 1 ); + } + }; + counters _cnts; + password _password; + + const uint32_t _nr_threads = 10; + + std::vector> threads; + + auto _create_session = [&]() + { + while( _cnts.create_session_cnt + _cnts.close_session_cnt < _cnts.limit ) + { + _cnts.inc( _cnts.create_session_cnt ); + + try + { + _password.add( _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token ); + } + catch( const fc::exception& e ) + { + BOOST_TEST_MESSAGE( e.to_detail_string() ); + BOOST_REQUIRE( e.to_string().find( "Number of concurrent sessions reached a limit" ) != std::string::npos ); + _cnts.inc( _cnts.create_session_error_cnt ); + } + } + }; + + auto _close_session = [&]() + { + while( _cnts.create_session_cnt + _cnts.close_session_cnt < _cnts.limit ) + { + _cnts.inc( _cnts.close_session_cnt ); + + try + { + _api.close_session( beekeeper::close_session_args{ _password.get() } ); + } + catch( const fc::exception& e ) + { + BOOST_TEST_MESSAGE( e.to_detail_string() ); + BOOST_REQUIRE( e.to_string().find( "A session attached to" ) != std::string::npos ); + _cnts.inc( _cnts.close_session_error_cnt ); + } + } + }; + + for( size_t i = 0; i < _nr_threads; ++i ) + threads.emplace_back( std::make_shared( [&]() + { + if( i % 2 == 0 ) + _create_session(); + else + _close_session(); + } ) ); + + BOOST_SCOPE_EXIT( &threads, &_cnts ) + { + for( auto& thread : threads ) + thread->join(); + + BOOST_TEST_MESSAGE("_create_session_cnt: " + std::to_string( _cnts.create_session_cnt.load() ) ); + BOOST_TEST_MESSAGE("_close_session_cnt: " + std::to_string( _cnts.close_session_cnt.load() ) ); + + BOOST_TEST_MESSAGE("_create_session_error_cnt: " + std::to_string( _cnts.create_session_error_cnt.load() ) ); + BOOST_TEST_MESSAGE("_close_session_error_cnt: " + std::to_string( _cnts.close_session_error_cnt.load() ) ); + + BOOST_REQUIRE_EQUAL( _cnts.create_session_cnt.load() + _cnts.close_session_cnt.load(), _cnts.limit ); + } BOOST_SCOPE_EXIT_END + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(beekeeper_api_sessions) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + uint64_t _interval = 500; + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 3, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + password _password; + + const uint32_t _nr_threads = 10; + + std::vector> threads; + + std::string _public_key = "STM6LLegbAgLAy28EHrffBVuANFWcFgmqRMW13wBmTExqFE9SCkg4"; + hive::protocol::digest_type _sig_digest = hive::protocol::digest_type( "9b29ba0710af3918e81d7b935556d7ab205d8a8f5ca2e2427535980c2e8bdaff" ); + + BOOST_SCOPE_EXIT(&threads) + { + for( auto& thread : threads ) + if( thread ) + thread->join(); + } BOOST_SCOPE_EXIT_END + + auto _call = [&]( int nr_thread ) + { + uint32_t _max = 10000; + for( uint32_t _cnt = 0; _cnt < _max; ++_cnt ) + { + switch( nr_thread ) + { + case 0: case 1: case 2: case 3: + { + try + { + _password.add( _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "Number of concurrent sessions reached a limit" ) != std::string::npos ); + } + }break; + + case 4: + { + try + { + _api.set_timeout( beekeeper::set_timeout_args{ _password.get( false/*remove*/ ), 100 } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "A session attached to" ) != std::string::npos ); + } + }break; + + case 5: + { + try + { + _api.get_info( beekeeper::get_info_args{ _password.get( false/*remove*/ ) } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "A session attached to" ) != std::string::npos ); + } + }break; + + case 6: case 7: case 8: + { + try + { + _api.close_session( beekeeper::close_session_args{ _password.get() } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "A session attached to" ) != std::string::npos ); + } + }break; + + case 9: + { + try + { + _api.sign_digest( beekeeper::sign_digest_args{ _password.get( false/*remove*/ ), _sig_digest, _public_key } ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "not found in unlocked wallets" ) != std::string::npos || + e.to_string().find( "A session attached to " ) != std::string::npos + ); + } + }break; + } + } + }; + + for( size_t i = 0; i < _nr_threads; ++i ) + threads.emplace_back( std::make_shared( _call, i ) ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallet_manager_threads_wallets) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint32_t _nr_threads = 10; + + std::vector _wallet_names; + for( uint32_t i = 0; i < _nr_threads; ++i ) + _wallet_names.emplace_back( "w" + std::to_string(i) ); + + auto _delete_wallet_file = []( const std::string& wallet_name ) + { + try + { + fc::remove( wallet_name + ".wallet" ); + } + catch(...) + { + } + }; + + for( auto& wallet_name : _wallet_names ) + _delete_wallet_file( wallet_name ); + + uint64_t _interval = 500; + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 3, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + std::string _token = _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token; + + std::vector> threads; + + BOOST_SCOPE_EXIT(&threads) + { + for( auto& thread : threads ) + if( thread ) + thread->join(); + } BOOST_SCOPE_EXIT_END + + auto _call = [&]( int nr_thread ) + { + uint32_t _max = 1000; + for( uint32_t _cnt = 0; _cnt < _max; ++_cnt ) + { + switch( nr_thread ) + { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: + { + auto _idx = std::rand() % _wallet_names.size(); + try + { + auto _password = _api.create( beekeeper::create_args{ _token, _wallet_names[_idx] } ).password; + BOOST_REQUIRE( !_password.empty() ); + } + catch( const fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "already exists at" ) != std::string::npos ); + _delete_wallet_file( _wallet_names[_idx] ); + } + }break; + case 7: case 8: case 9: + { + auto _idx = std::rand() % _wallet_names.size(); + _delete_wallet_file( _wallet_names[_idx] ); + }break; + } + } + }; + + for( size_t i = 0; i < _nr_threads; ++i ) + threads.emplace_back( std::make_shared( _call, i ) ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(beekeeper_api_performance_sign_transaction) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + uint64_t _interval = 500; + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 3, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + std::string _wallet_name = "w0"; + std::string _private_key = "5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n"; + std::string _public_key = "STM6LLegbAgLAy28EHrffBVuANFWcFgmqRMW13wBmTExqFE9SCkg4"; + hive::protocol::digest_type _sig_digest = hive::protocol::digest_type( "9b29ba0710af3918e81d7b935556d7ab205d8a8f5ca2e2427535980c2e8bdaff" ); + + std::string _token = _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token; + auto _password = _api.create( beekeeper::create_args{ _token, _wallet_name } ).password; + BOOST_REQUIRE( !_password.empty() ); + + _api.import_key( beekeeper::import_key_args{ _token, _wallet_name, _private_key } ); + + auto _call = [&]( uint32_t nr_threads ) + { + uint32_t _max = 50000 / nr_threads; + for( uint32_t _cnt = 0; _cnt < _max; ++_cnt ) + { + _api.sign_digest( beekeeper::sign_digest_args{ _token, _sig_digest, _public_key } ); + } + }; + + auto _execute_calls = [&_call]( uint32_t nr_threads ) + { + std::vector> threads; + + for( size_t i = 0; i < nr_threads; ++i ) + threads.emplace_back( std::make_shared( _call, nr_threads ) ); + + for( auto& thread : threads ) + if( thread ) + thread->join(); + }; + + int64_t _duration_1 = 0; + int64_t _duration_10 = 0; + { + auto _start = std::chrono::high_resolution_clock::now(); + + _execute_calls( 1 ); + + _duration_1 = std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - _start ).count(); + BOOST_TEST_MESSAGE( std::to_string( _duration_1 ) + " [ms]" ); + } + + { + auto _start = std::chrono::high_resolution_clock::now(); + + _execute_calls( 10 ); + + _duration_10 = std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - _start ).count(); + BOOST_TEST_MESSAGE( std::to_string( _duration_10 ) + " [ms]" ); + } + + /* + AMD Ryzen 7 5800X 8-Core Processor + _duration_1 = 1344 [ms] + _duration_10 = 227 [ms] + */ + BOOST_REQUIRE( _duration_10 * 3 < _duration_1 ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallets_synchronization_threads) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const size_t _nr_threads = 10; + + std::string _wallet_name = "www"; + + uint64_t _interval = 500; + auto _mtx_handler = std::make_shared(); + beekeeper::beekeeper_wallet_api _api( b_mgr.create_wallet_ptr( theApp, 900, 64, [](){}, _mtx_handler ), _mtx_handler, theApp, _interval ); + + std::vector _tokens; + + for( size_t i = 0; i < _nr_threads; ++i ) + _tokens.emplace_back( _api.create_session( beekeeper::create_session_args{ "this is salt" } ).token ); + + auto _password = _api.create( beekeeper::create_args{ _tokens[0], _wallet_name } ).password; + + for( size_t i = 1; i < _nr_threads; ++i ) + _api.unlock( beekeeper::unlock_args{ _tokens[i], _wallet_name, _password } ); + + std::vector> threads; + + auto _call = [&]( int nr_thread ) + { + size_t _max = 300; + for( size_t _cnt = 0; _cnt < _max; ++_cnt ) + { + if( nr_thread % 2 ) + { + auto _priv = fc::ecc::private_key::generate(); + try + { + _api.import_key( beekeeper::import_key_args{ _tokens[nr_thread], _wallet_name, _priv.key_to_wif() } ); + } + catch( fc::exception& e ) + { + elog( "${e}", (e) ); + BOOST_REQUIRE( false ); + } + } + else + { + auto _keys = _api.get_public_keys( beekeeper::get_public_keys_args{ _tokens[nr_thread] } ).keys; + if( !_keys.empty() ) + { + beekeeper::public_key_details _key; + + if( _keys.size() == 1 ) + _key = *_keys.begin(); + else + _key = *_keys.rbegin(); + + try + { + _api.remove_key( beekeeper::remove_key_args{ _tokens[nr_thread], _wallet_name, _key.public_key } ); + } + catch( fc::exception& e ) + { + BOOST_REQUIRE( e.to_string().find( "Key not in wallet" ) != std::string::npos ); + } + } + } + } + }; + + for( size_t i = 0; i < _nr_threads; ++i ) + threads.emplace_back( std::make_shared( _call, i ) ); + + for( auto& thread : threads ) + if( thread ) + thread->join(); + + auto _pattern_keys = _api.get_public_keys( beekeeper::get_public_keys_args{ _tokens[0] } ).keys; + for( size_t i = 1; i < _nr_threads; ++i ) + { + auto _keys = _api.get_public_keys( beekeeper::get_public_keys_args{ _tokens[i] } ).keys; + + BOOST_REQUIRE( _pattern_keys.size() == _keys.size() ); + + auto _itr = _keys.begin(); + for( auto& item : _pattern_keys ) + { + BOOST_REQUIRE( item.public_key == _itr->public_key ); + ++_itr; + } + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() +#endif diff --git a/tests/unit/beekeeper_mgr.hpp b/tests/unit/beekeeper_mgr.hpp new file mode 100644 index 0000000..3bcdce5 --- /dev/null +++ b/tests/unit/beekeeper_mgr.hpp @@ -0,0 +1,83 @@ +#ifdef IS_TEST_NET + +#include + +#include +#include + +#include +#include +#include +#include + +using beekeeper_wallet_manager = beekeeper::beekeeper_wallet_manager; +using wallet_content_handler = beekeeper::wallet_content_handler; +using session_manager_base = beekeeper::session_manager_base; +using time_manager = beekeeper::time_manager; +using session_manager = beekeeper::session_manager; +using beekeeper_instance = beekeeper::beekeeper_instance; + +namespace test_utils +{ + struct beekeeper_mgr + { + fc::path dir; + + beekeeper_mgr() + : dir( fc::current_path() / "beekeeper-storage" ) + { + fc::create_directories( dir ); + } + + void remove_wallets() + { + boost::filesystem::directory_iterator _end_itr; + + for( boost::filesystem::directory_iterator itr( dir ); itr != _end_itr; ++itr ) + boost::filesystem::remove_all( itr->path() ); + } + + void remove_wallet( const std::string& wallet_name ) + { + try + { + auto _wallet_name = wallet_name + ".wallet"; + fc::remove( dir / _wallet_name ); + } + catch(...) + { + } + } + + bool exists_wallet( const std::string& wallet_name ) + { + auto _wallet_name = wallet_name + ".wallet"; + return fc::exists( dir / _wallet_name ); + } + + beekeeper_wallet_manager create_wallet( appbase::application& app, uint64_t cmd_unlock_timeout, uint32_t cmd_session_limit, std::function&& method = [](){}, std::shared_ptr mtx_handler = std::make_shared() ) + { + return beekeeper_wallet_manager( std::make_shared( mtx_handler ), + std::make_shared( app, dir ), + dir, + cmd_unlock_timeout, + cmd_session_limit, + std::move( method ) + ); + } + + std::shared_ptr create_wallet_ptr( appbase::application& app, uint64_t cmd_unlock_timeout, uint32_t cmd_session_limit, std::function&& method = [](){}, std::shared_ptr mtx_handler = std::make_shared() ) + { + return std::shared_ptr( new beekeeper_wallet_manager( std::make_shared( mtx_handler ), + std::make_shared( app, dir ), + dir, + cmd_unlock_timeout, + cmd_session_limit, + std::move( method ) + ) ); + } + + }; +} + +#endif diff --git a/tests/unit/beekeeper_tests.cpp b/tests/unit/beekeeper_tests.cpp new file mode 100644 index 0000000..aa51ce3 --- /dev/null +++ b/tests/unit/beekeeper_tests.cpp @@ -0,0 +1,2437 @@ +#ifdef IS_TEST_NET + +#define BOOST_TEST_MODULE beekeeper_tests + +#include "beekeeper_mgr.hpp" + +#ifdef HIVE_PROTOCOL_AVAILABLE +#include +#include +#endif + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +using public_key_type = beekeeper::public_key_type; +using private_key_type = beekeeper::private_key_type; +using beekeeper_api = beekeeper::beekeeper_api; +using wallet_data = beekeeper::wallet_data; +using wallet_details = beekeeper::wallet_details; +using keys_details = beekeeper::keys_details; +using fc::flat_set; + +BOOST_AUTO_TEST_SUITE(beekeeper_tests) + +/// Test creating the wallet +BOOST_AUTO_TEST_CASE(wallet_test) +{ try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + wallet_content_handler wallet; + BOOST_REQUIRE(wallet.is_locked()); + + wallet.set_password("pass"); + BOOST_REQUIRE(wallet.is_locked()); + + wallet.unlock("pass"); + BOOST_REQUIRE(!wallet.is_locked()); + + auto _wallet_file_name = ( b_mgr.dir / "test" ).string(); + + wallet.set_wallet_name( _wallet_file_name ); + BOOST_REQUIRE_EQUAL( _wallet_file_name , wallet.get_wallet_name() ); + + BOOST_REQUIRE_EQUAL(0u, wallet.get_keys_details().size()); + + auto _key_generation = []( size_t nr_keys ) + { + using keys_pair = std::pair; + std::vector _result; + + for( size_t i = 0 ; i < nr_keys; ++i ) + { + auto priv = fc::ecc::private_key::generate(); + _result.emplace_back( std::make_pair( priv.key_to_wif(), priv.get_public_key()) ); + } + return _result; + }; + + auto _keys_a = _key_generation( 1 ); + + auto _prefix = "STM"; + wallet.import_key( _keys_a[0].first, _prefix); + BOOST_REQUIRE_EQUAL(1u, wallet.get_keys_details().size()); + + auto privCopy = wallet.get_private_key( _keys_a[0].second ); + BOOST_REQUIRE_EQUAL(_keys_a[0].first, privCopy.key_to_wif()); + + wallet.lock(); + BOOST_REQUIRE(wallet.is_locked()); + wallet.unlock("pass"); + BOOST_REQUIRE_EQUAL(1u, wallet.get_keys_details().size()); + wallet.save_wallet_file( _wallet_file_name ); + BOOST_REQUIRE( fc::exists( _wallet_file_name ) ); + + wallet_content_handler wallet2; + + BOOST_REQUIRE(wallet2.is_locked()); + wallet2.load_wallet_file( _wallet_file_name ); + BOOST_REQUIRE(wallet2.is_locked()); + + wallet2.unlock("pass"); + BOOST_REQUIRE_EQUAL(1u, wallet2.get_keys_details().size()); + + auto privCopy2 = wallet2.get_private_key( _keys_a[0].second ); + BOOST_REQUIRE_EQUAL(_keys_a[0].first, privCopy2.key_to_wif()); + + auto _keys_b = _key_generation( 4 ); + std::vector _keys; + for( auto& keys : _keys_b ) + _keys.emplace_back( keys.first ); + + wallet.import_keys( _keys, _prefix ); + + BOOST_REQUIRE_EQUAL(5u, wallet.get_keys_details().size()); + for( auto& keys : _keys_b ) + { + auto _private_key = wallet.get_private_key( keys.second ); + BOOST_REQUIRE_EQUAL( keys.first, _private_key.key_to_wif() ); + } + +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE(wallet_name_test) +{ try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + appbase::application app; + + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, 900, 3 ); + + BOOST_REQUIRE( wm.start() ); + std::string _token = wm.create_session( "this is salt" ); + + wm.create(_token, "wallet.wallet", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "wallet_wallet", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "wallet-wallet", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "wallet@wallet", std::optional(), false/*is_temporary*/ ); + + wm.create(_token, ".wallet", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "_wallet", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "-wallet", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "@wallet", std::optional(), false/*is_temporary*/ ); + + wm.create(_token, "wallet.", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "wallet_", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "wallet-", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "wallet@", std::optional(), false/*is_temporary*/ ); + + wm.create(_token, ".wallet.", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "_wallet_", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "-wallet-", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "@wallet@", std::optional(), false/*is_temporary*/ ); + +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE(wallet_complex_name_test) +{ try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + appbase::application app; + + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, 900, 3 ); + + BOOST_REQUIRE( wm.start() ); + std::string _token = wm.create_session( "this is salt" ); + + std::string _wallet_name = "small.minion.wallet"; + + wm.create( _token, _wallet_name, std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE_EQUAL( wm.list_created_wallets( _token ).size(), 1 ); + BOOST_REQUIRE_EQUAL( wm.list_wallets( _token ).size(), 1 ); + + wm.lock( _token, _wallet_name ); + BOOST_REQUIRE_EQUAL( wm.list_created_wallets( _token ).size(), 1 ); + BOOST_REQUIRE_EQUAL( wm.list_wallets( _token ).size(), 1 ); + + wm.open( _token, _wallet_name ); + BOOST_REQUIRE_EQUAL( wm.list_created_wallets( _token ).size(), 1 ); + BOOST_REQUIRE_EQUAL( wm.list_wallets( _token ).size(), 1 ); + +} FC_LOG_AND_RETHROW() } + +/// Test wallet manager +BOOST_AUTO_TEST_CASE(wallet_manager_test) +{ try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + appbase::application app; + + const auto key1_str = "5JktVNHnRX48BUdtewU7N1CyL4Z886c42x7wYW7XhNWkDQRhdcS"; + const auto key2_str = "5Ju5RTcVDo35ndtzHioPMgebvBM6LkJ6tvuU6LTNQv8yaz3ggZr"; + const auto key3_str = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"; + + const auto key1 = private_key_type::wif_to_key( key1_str ).value(); + const auto key2 = private_key_type::wif_to_key( key2_str ).value(); + const auto key3 = private_key_type::wif_to_key( key3_str ).value(); + + auto _prefix = "STM"; + + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, 900, 3 ); + + BOOST_REQUIRE( wm.start() ); + std::string _token = wm.create_session( "this is salt" ); + + BOOST_REQUIRE_EQUAL(0u, wm.list_wallets(_token).size()); + BOOST_REQUIRE_THROW(wm.get_public_keys(_token, std::optional()), fc::exception); + BOOST_REQUIRE_THROW(wm.get_public_keys(_token, std::optional("avocado")), fc::exception); + BOOST_REQUIRE_NO_THROW(wm.lock_all(_token)); + + BOOST_REQUIRE_THROW(wm.lock(_token, "test"), fc::exception); + BOOST_REQUIRE_THROW(wm.unlock(_token, "test", "pw"), fc::exception); + BOOST_REQUIRE_THROW(wm.import_key(_token, "test", "pw", _prefix), fc::exception); + + auto pw = wm.create(_token, "test", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE(!pw.empty()); + BOOST_REQUIRE_EQUAL(0u, pw.find("PW")); // starts with PW + BOOST_REQUIRE_EQUAL(1u, wm.list_wallets(_token).size()); + // wallet has no keys when it is created + BOOST_REQUIRE_EQUAL(0u, wm.get_public_keys(_token, std::optional()).size()); + BOOST_REQUIRE_EQUAL(0u, wm.get_public_keys(_token, std::optional("test")).size()); + BOOST_REQUIRE_EQUAL(0u, wm.list_keys(_token, "test", pw).size()); + BOOST_REQUIRE(wm.list_wallets(_token).begin()->unlocked); + wm.lock(_token, "test"); + BOOST_REQUIRE(!wm.list_wallets(_token).begin()->unlocked); + wm.unlock(_token, "test", pw); + BOOST_REQUIRE_THROW(wm.unlock(_token, "test", pw), fc::exception); + BOOST_REQUIRE(wm.list_wallets(_token).begin()->unlocked); + wm.import_key(_token, "test", key1_str, _prefix); + BOOST_REQUIRE_EQUAL(1u, wm.get_public_keys(_token, std::optional()).size()); + BOOST_REQUIRE_EQUAL(1u, wm.get_public_keys(_token, std::optional( "test" )).size()); + BOOST_REQUIRE_THROW(wm.get_public_keys(_token, std::optional("avocado")), fc::exception); + auto keys = wm.list_keys(_token, "test", pw); + + auto pub_pri_pair = [ &_prefix ]( const private_key_type& private_key ) -> auto + { + return beekeeper::key_detail_pair( private_key.get_public_key(), std::make_pair( private_key, _prefix ) ); + }; + + auto cmp_keys = [&]( const private_key_type& private_key, const keys_details& keys ) + { + return std::find_if( keys.begin(), keys.end(), [&]( const beekeeper::key_detail_pair& item ) + { + return pub_pri_pair( private_key ) == item; + }); + }; + + BOOST_REQUIRE( cmp_keys( key1, keys ) != keys.end() ); + + wm.import_key(_token, "test", key2_str, _prefix); + keys = wm.list_keys(_token, "test", pw); + BOOST_REQUIRE( cmp_keys( key1, keys ) != keys.end() ); + BOOST_REQUIRE( cmp_keys( key2, keys ) != keys.end() ); + // key3 was not automatically imported + BOOST_REQUIRE( cmp_keys( key3, keys ) == keys.end() ); + + wm.remove_key(_token, "test", key2.get_public_key() ); + BOOST_REQUIRE_EQUAL(1u, wm.get_public_keys(_token, std::optional()).size()); + keys = wm.list_keys(_token, "test", pw); + BOOST_REQUIRE( cmp_keys( key2, keys ) == keys.end() ); + wm.import_key(_token, "test", key2_str, _prefix); + BOOST_REQUIRE_EQUAL(2u, wm.get_public_keys(_token, std::optional()).size()); + keys = wm.list_keys(_token, "test", pw); + BOOST_REQUIRE( cmp_keys( key2, keys ) != keys.end() ); + BOOST_REQUIRE_THROW(wm.remove_key(_token, "test", key3.get_public_key() ), fc::exception); + BOOST_REQUIRE_EQUAL(2u, wm.get_public_keys(_token, std::optional()).size()); + BOOST_REQUIRE_THROW(wm.remove_key(_token, "test_xyz", key2.get_public_key() ), fc::exception); + BOOST_REQUIRE_THROW(wm.remove_key(_token, "test", public_key_type::from_base58( "this-is-not-key" ) ), fc::exception); + BOOST_REQUIRE_EQUAL(2u, wm.get_public_keys(_token, std::optional()).size()); + + wm.lock(_token, "test"); + BOOST_REQUIRE_THROW(wm.list_keys(_token, "test", pw), fc::exception); + BOOST_REQUIRE_THROW(wm.get_public_keys(_token, std::optional()), fc::exception); + wm.unlock(_token, "test", pw); + BOOST_REQUIRE_EQUAL(2u, wm.get_public_keys(_token, std::optional()).size()); + BOOST_REQUIRE_EQUAL(2u, wm.list_keys(_token, "test", pw).size()); + wm.lock_all(_token); + BOOST_REQUIRE_THROW(wm.get_public_keys(_token, std::optional()), fc::exception); + BOOST_REQUIRE(!wm.list_wallets(_token).begin()->unlocked); + + auto pw2 = wm.create(_token, "test2", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE_EQUAL(2u, wm.list_wallets(_token).size()); + // wallet has no keys when it is created + BOOST_REQUIRE_EQUAL(0u, wm.get_public_keys(_token, std::optional()).size()); + wm.import_key(_token, "test2", key3_str, _prefix); + BOOST_REQUIRE_EQUAL(1u, wm.get_public_keys(_token, std::optional()).size()); + wm.import_key(_token, "test2", key3_str, _prefix); + keys = wm.list_keys(_token, "test2", pw2); + BOOST_REQUIRE( cmp_keys( key1, keys ) == keys.end() ); + BOOST_REQUIRE( cmp_keys( key2, keys ) == keys.end() ); + BOOST_REQUIRE( cmp_keys( key3, keys ) != keys.end() ); + wm.unlock(_token, "test", pw); + keys = wm.list_keys(_token, "test", pw); + auto keys2 = wm.list_keys(_token, "test2", pw2); + keys.insert(keys2.begin(), keys2.end()); + BOOST_REQUIRE( cmp_keys( key1, keys ) != keys.end() ); + BOOST_REQUIRE( cmp_keys( key2, keys ) != keys.end() ); + BOOST_REQUIRE( cmp_keys( key3, keys ) != keys.end() ); + BOOST_REQUIRE_EQUAL(3u, keys.size()); + + BOOST_REQUIRE_THROW(wm.list_keys(_token, "test2", "PWnogood"), fc::exception); + + BOOST_REQUIRE_EQUAL(3u, wm.get_public_keys(_token, std::optional()).size()); + wm.set_timeout(_token, 0); + BOOST_REQUIRE_THROW(wm.get_public_keys(_token, std::optional()), fc::exception); + BOOST_REQUIRE_THROW(wm.list_keys(_token, "test", pw), fc::exception); + + wm.set_timeout(_token, 15); + + wm.create(_token, "testgen", std::optional(), false/*is_temporary*/ ); + wm.lock(_token, "testgen"); + fc::remove( b_mgr.dir / "testgen.wallet" ); + + pw = wm.create(_token, "testgen", std::optional(), false/*is_temporary*/ ); + + wm.lock(_token, "testgen"); + BOOST_REQUIRE(fc::exists( b_mgr.dir / "testgen.wallet" )); + +} FC_LOG_AND_RETHROW() } + +/// Test wallet manager +BOOST_AUTO_TEST_CASE(wallet_manager_create_test) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + appbase::application app; + + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, 900, 3 ); + + BOOST_REQUIRE( wm.start() ); + std::string _token = wm.create_session( "this is salt" ); + auto _prefix = "STM"; + + wm.create(_token, "test", std::optional(), false/*is_temporary*/ ); + constexpr auto key1 = "5JktVNHnRX48BUdtewU7N1CyL4Z886c42x7wYW7XhNWkDQRhdcS"; + wm.import_key(_token, "test", key1, _prefix); + BOOST_REQUIRE_THROW(wm.create(_token, "test", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "./test", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "../../test", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "/tmp/test", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "/tmp/", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "/", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, ",/", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, ",", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "<<", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "<", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, ",<", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, ",<<", std::optional(), false/*is_temporary*/ ), fc::exception); + BOOST_REQUIRE_THROW(wm.create(_token, "", std::optional(), false/*is_temporary*/ ), fc::exception); + + wm.create(_token, ".test", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE(fc::exists( b_mgr.dir / ".test.wallet" )); + + wm.create(_token, "..test", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE(fc::exists( b_mgr.dir / "..test.wallet" )); + + wm.create(_token, "...test", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE(fc::exists( b_mgr.dir / "...test.wallet" )); + + wm.create(_token, ".", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE(fc::exists( b_mgr.dir / "..wallet" )); + + wm.create(_token, "__test_test", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE(fc::exists( b_mgr.dir / "__test_test.wallet" )); + + wm.create(_token, "t-t", std::optional(), false/*is_temporary*/ ); + BOOST_REQUIRE(fc::exists( b_mgr.dir / "t-t.wallet" )); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallet_manager_sessions) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _limit = 3; + + appbase::application app; + + { + bool _checker = false; + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _limit, [&_checker](){ _checker = true; } ); + + auto _token = wm.create_session( "this is salt" ); + wm.close_session( _token ); + BOOST_REQUIRE( _checker ); + } + { + bool _checker = false; + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _limit, [&_checker](){ _checker = true; } ); + + auto _token_00 = wm.create_session( "this is salt" ); + auto _token_01 = wm.create_session( "this is salt" ); + wm.close_session( _token_00 ); + BOOST_REQUIRE( !_checker ); + wm.close_session( _token_01 ); + BOOST_REQUIRE( _checker ); + } + { + bool _checker = false; + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _limit, [&_checker](){ _checker = true; } ); + BOOST_REQUIRE( wm.start() ); + + auto _token_00 = wm.create_session( "aaaa" ); + auto _token_01 = wm.create_session( "bbbb" ); + + std::string _pass_00 = wm.create(_token_00, "avocado", std::optional(), false/*is_temporary*/ ); + std::string _pass_01 = wm.create(_token_01, "banana", std::optional(), false/*is_temporary*/ ); + std::string _pass_02 = wm.create(_token_01, "cherry", std::optional(), false/*is_temporary*/ ); + + BOOST_REQUIRE_THROW( wm.list_wallets( "not existed token" ), fc::exception ); + BOOST_REQUIRE_THROW( wm.list_created_wallets( "not existed token" ), fc::exception ); + BOOST_REQUIRE_EQUAL( wm.list_wallets( _token_00 ).size(), 1 ); + BOOST_REQUIRE_EQUAL( wm.list_wallets( _token_01 ).size(), 2 ); + BOOST_REQUIRE_EQUAL( wm.list_created_wallets( _token_00 ).size(), 3 ); + BOOST_REQUIRE_EQUAL( wm.list_created_wallets( _token_01 ).size(), 3 ); + + wm.close_session( _token_00 ); + BOOST_REQUIRE( !_checker ); + + BOOST_REQUIRE_THROW( wm.list_wallets( "not existed token" ), fc::exception ); + BOOST_REQUIRE_THROW( wm.list_wallets( _token_00 ), fc::exception ); + BOOST_REQUIRE_EQUAL( wm.list_wallets( _token_01 ).size(), 2 ); + BOOST_REQUIRE_THROW( wm.list_created_wallets( _token_00 ), fc::exception ); + BOOST_REQUIRE_EQUAL( wm.list_created_wallets( _token_01 ).size(), 3 ); + + wm.close_session( _token_01 ); + BOOST_REQUIRE( _checker ); + + BOOST_REQUIRE_THROW( wm.list_wallets( "not existed token" ), fc::exception ); + BOOST_REQUIRE_THROW( wm.list_wallets( _token_00 ), fc::exception ); + BOOST_REQUIRE_THROW( wm.list_wallets( _token_01 ), fc::exception ); + BOOST_REQUIRE_THROW( wm.list_created_wallets( _token_00 ), fc::exception ); + BOOST_REQUIRE_THROW( wm.list_created_wallets( _token_01 ), fc::exception ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallet_manager_wallets_with_dots) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _limit = 3; + + appbase::application app; + { + bool _checker = false; + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _limit, [&_checker](){ _checker = true; } ); + BOOST_REQUIRE( wm.start() ); + + auto _token = wm.create_session( "aaaa" ); + + wm.create(_token, "...watermelon", std::optional(), false/*is_temporary*/ ); + wm.create(_token, ".lemon", std::optional(), false/*is_temporary*/ ); + wm.create(_token, ".peach.pear", std::optional(), false/*is_temporary*/ ); + wm.create(_token, ".plum.", std::optional(), false/*is_temporary*/ ); + wm.create(_token, "avocado.banana", std::optional(), false/*is_temporary*/ ); + + BOOST_REQUIRE_EQUAL( wm.list_created_wallets( _token ).size(), 5 ); + BOOST_REQUIRE_EQUAL( wm.list_wallets( _token ).size(), 5 ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallet_manager_info) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _limit = 3; + + appbase::application app; + + { + bool _checker = false; + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _limit, [&_checker](){ _checker = true; } ); + BOOST_REQUIRE( wm.start() ); + + auto _token_00 = wm.create_session( "aaaa" ); + + std::this_thread::sleep_for( std::chrono::seconds(3) ); + + auto _token_01 = wm.create_session( "bbbb" ); + + auto _info_00 = wm.get_info( _token_00 ); + auto _info_01 = wm.get_info( _token_01 ); + + BOOST_TEST_MESSAGE( _info_00.timeout_time ); + BOOST_TEST_MESSAGE( _info_01.timeout_time ); + + auto _time_00 = fc::time_point::from_iso_string( _info_00.timeout_time ); + auto _time_01 = fc::time_point::from_iso_string( _info_01.timeout_time ); + + BOOST_REQUIRE( _time_01 >_time_00 ); + + wm.close_session( _token_01 ); + BOOST_REQUIRE( !_checker ); + + auto _token_02 = wm.create_session( "cccc" ); + + auto _info_02 = wm.get_info( _token_02 ); + + BOOST_TEST_MESSAGE( _info_02.timeout_time ); + + auto _time_02 = fc::time_point::from_iso_string( _info_02.timeout_time ); + + BOOST_REQUIRE( _time_02 >_time_00 ); + + wm.close_session( _token_02 ); + BOOST_REQUIRE( !_checker ); + + wm.close_session( _token_00 ); + BOOST_REQUIRE( _checker ); + + BOOST_REQUIRE_THROW( wm.close_session( _token_00 ), fc::exception ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallet_manager_session_limit) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + bool _checker = false; + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _session_limit, [&_checker](){ _checker = true; } ); + BOOST_REQUIRE( wm.start() ); + + std::vector _tokens; + for( uint32_t i = 0; i < _session_limit; ++i ) + { + _tokens.emplace_back( wm.create_session( "salt" ) ); + } + BOOST_REQUIRE_THROW( wm.create_session( "salt" ), fc::exception ); + + BOOST_REQUIRE( _tokens.size() == _session_limit ); + + wm.close_session( _tokens[0] ); + wm.close_session( _tokens[1] ); + _tokens.erase( _tokens.begin() ); + _tokens.erase( _tokens.begin() ); + + _tokens.emplace_back( wm.create_session( std::optional() ) ); + _tokens.emplace_back( wm.create_session( std::optional() ) ); + + BOOST_REQUIRE_THROW( wm.create_session( std::optional() ), fc::exception ); + + BOOST_REQUIRE( _tokens.size() == _session_limit ); + + BOOST_REQUIRE( _checker == false ); + + for( auto& token : _tokens ) + { + wm.close_session( token ); + } + + BOOST_REQUIRE( _checker == true ); + + _tokens.emplace_back( wm.create_session( "salt" ) ); + _tokens.emplace_back( wm.create_session( std::optional() ) ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallet_manager_close) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + bool _checker = false; + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _session_limit, [&_checker](){ _checker = true; } ); + BOOST_REQUIRE( wm.start() ); + + auto wallet_name_0 = "0"; + auto wallet_name_1 = "1"; + + { + auto _token = wm.create_session( "salt" ); + wm.create(_token, wallet_name_0, std::optional(), false/*is_temporary*/ ); + + auto _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE( _wallets.size() == 1 ); + BOOST_REQUIRE( _wallets.begin()->name == wallet_name_0 ); + + wm.close( _token, wallet_name_1 );//Here is lack of an exception. Only a warning is generated. + wm.close( _token, wallet_name_0 ); + + _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 0 ); + + wm.create(_token, wallet_name_1, std::optional(), false/*is_temporary*/ ); + + _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->name, wallet_name_1 ); + + wm.close( _token, wallet_name_1 ); + + _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 0 ); + } + + { + fc::remove( b_mgr.dir / "0.wallet" ); + fc::remove( b_mgr.dir / "1.wallet" ); + + auto _token = wm.create_session( "salt" ); + wm.create(_token, wallet_name_0, std::optional(), false/*is_temporary*/ ); + + wm.lock_all( _token ); + + auto _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->name, wallet_name_0 ); + + wm.close( _token, wallet_name_0 ); + + _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 0 ); + } + + { + fc::remove( b_mgr.dir / "0.wallet" ); + fc::remove( b_mgr.dir / "1.wallet" ); + + auto _token = wm.create_session( "salt" ); + wm.create(_token, wallet_name_0, std::optional(), false/*is_temporary*/ ); + wm.create(_token, wallet_name_1, std::optional(), false/*is_temporary*/ ); + + wm.lock( _token, wallet_name_1 ); + + auto _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 2 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->name, wallet_name_0 ); + BOOST_REQUIRE_EQUAL( _wallets.rbegin()->name, wallet_name_1 ); + + wm.close( _token, wallet_name_1 ); + + _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->name, wallet_name_0 ); + + wm.lock( _token, wallet_name_0 ); + + _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->name, wallet_name_0 ); + + wm.close( _token, wallet_name_0 ); + + _wallets = wm.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 0 ); + } + + } FC_LOG_AND_RETHROW() +} + +#ifdef HIVE_PROTOCOL_AVAILABLE +BOOST_AUTO_TEST_CASE(wallet_manager_sign_transaction) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + { + hive::protocol::serialization_mode_controller::pack_guard guard( hive::protocol::pack_type::hf26 ); + + auto _private_key_str = "5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n"; + auto _public_key_str = "6LLegbAgLAy28EHrffBVuANFWcFgmqRMW13wBmTExqFE9SCkg4"; + + const auto _private_key = private_key_type::wif_to_key( _private_key_str ).value(); + const auto _public_key = public_key_type::from_base58( _public_key_str, false/*is_sha256*/ ); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + const std::string _wallet_name = "0"; + auto _prefix = "STM"; + + appbase::application app; + + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( wm.start() ); + + auto _token = wm.create_session( "salt" ); + auto _password = wm.create(_token, _wallet_name, std::optional(), false/*is_temporary*/ ); + auto _imported_public_key = wm.import_key( _token, _wallet_name, _private_key_str, _prefix ); + BOOST_REQUIRE( _imported_public_key != _public_key_str ); + BOOST_REQUIRE( _imported_public_key == std::string( HIVE_ADDRESS_PREFIX ) + _public_key_str ); + + auto _calculate_signature = [&]( const std::string& json_trx, const std::string& signature_pattern ) + { + hive::protocol::transaction _trx = fc::json::from_string( json_trx, fc::json::format_validation_mode::full ).as(); + hive::protocol::digest_type _sig_digest = _trx.sig_digest( HIVE_CHAIN_ID, hive::protocol::pack_type::hf26 ); + + auto _signature_local = _private_key.sign_compact( _sig_digest ); + auto __signature_local = fc::json::to_string( _signature_local ); + + auto _signature_wallet = wm.sign_digest( _token, _wallet_name, _sig_digest.str(), beekeeper::utility::public_key::create( _imported_public_key, _prefix ), _prefix ); + auto __signature_wallet = fc::json::to_string( _signature_wallet ); + + BOOST_TEST_MESSAGE( __signature_local ); + BOOST_REQUIRE( __signature_local.substr( 1, __signature_local.size() - 2 ) == signature_pattern ); + + BOOST_TEST_MESSAGE( __signature_wallet ); + BOOST_REQUIRE( __signature_wallet.substr( 1, __signature_wallet.size() - 2 ) == signature_pattern ); + }; + + std::string _signature_00_result = "1f17cc07f7c769073d39fac3385220b549e261fb33c5f619c5dced7f5b0fe9c0954f2684e703710840b7ea01ad7238b8db1d8a9309d03e93de212f86de38d66f21"; + _calculate_signature( "{}", _signature_00_result ); + + std::string _signature_01_result = "1f69e091fc79b0e8d1812fc662f12076561f9e38ffc212b901ae90fe559f863ad266fe459a8e946cff9bbe7e56ce253bbfab0cccdde944edc1d05161c61ae86340"; + _calculate_signature( "{\"ref_block_num\":95,\"ref_block_prefix\":4189425605,\"expiration\":\"2023-07-18T08:38:29\",\"operations\":[{\"type\":\"transfer_operation\",\"value\":{\"from\":\"initminer\",\"to\":\"alice\",\"amount\":{\"amount\":\"666\",\"precision\":3,\"nai\":\"@@000000021\"},\"memo\":\"memmm\"}}],\"extensions\":[],\"signatures\":[],\"transaction_id\":\"cc9630cdbc39da1c9b6264df3588c7bedb5762fa\",\"block_num\":0,\"transaction_num\":0}", + _signature_01_result ); + + BOOST_REQUIRE_THROW( wm.sign_digest( _token, _wallet_name, "", beekeeper::utility::public_key::create( _imported_public_key, _prefix ), _prefix ), fc::exception ); + } + + } FC_LOG_AND_RETHROW() +} +#endif // HIVE_PROTOCOL_AVAILABLE + +std::string extract_json( const std::string& str ) +{ + BOOST_TEST_MESSAGE( "JSON: " + str ); + if( str.empty() ) + return str; + auto _v_init = fc::json::from_string( str, fc::json::format_validation_mode::full ); + BOOST_REQUIRE( _v_init.is_object() && ( _v_init.get_object().contains("result") || _v_init.get_object().contains("error") ) ); + + std::string _result; + if( _v_init.get_object().contains("result") ) + fc::from_variant( _v_init.get_object()["result"], _result ); + else + fc::from_variant( _v_init.get_object()["error"], _result ); + + return _result; +}; + +std::string get_wasm_data( const std::string& json ) +{ + std::vector< std::string > _elements; + boost::split( _elements, json, boost::is_any_of( "\"" ) ); + BOOST_REQUIRE( _elements.size() >= 2 ); + return _elements[ _elements.size() - 2 ]; +}; + +BOOST_AUTO_TEST_CASE(wasm_beekeeper) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + beekeeper_api _obj( { "--wallet-dir", b_mgr.dir.string() } ); + + BOOST_REQUIRE( fc::json::from_string( extract_json( _obj.init() ), fc::json::format_validation_mode::full ).as().status == 0 ); + + auto _token = extract_json( _obj.create_session( "banana" ) ); + BOOST_TEST_MESSAGE( _token ); + _token = get_wasm_data( _token ); + + auto _password_0 = extract_json( _obj.create( _token, "wallet_0", "pear" ) ); + BOOST_TEST_MESSAGE( _password_0 ); + _password_0 = get_wasm_data( _password_0 ); + + auto _password_1 = extract_json( _obj.create( _token, "wallet_1", "cherry" ) ); + BOOST_TEST_MESSAGE( _password_1 ); + BOOST_REQUIRE( _password_1.find( "cherry" ) != std::string::npos ); + _password_1 = get_wasm_data( _password_1 ); + + auto _public_key_0 = extract_json( _obj.import_key( _token, "wallet_0", "5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n" ) ); + BOOST_TEST_MESSAGE( _public_key_0 ); + BOOST_REQUIRE( _public_key_0.find( "6LLegbAgLAy28EHrffBVuANFWcFgmqRMW13wBmTExqFE9SCkg4" ) != std::string::npos ); + + auto _public_key_1a = extract_json( _obj.import_key( _token, "wallet_1", "5KGKYWMXReJewfj5M29APNMqGEu173DzvHv5TeJAg9SkjUeQV78" ) ); + BOOST_TEST_MESSAGE( _public_key_1a ); + BOOST_REQUIRE( _public_key_1a.find( "6oR6ckA4TejTWTjatUdbcS98AKETc3rcnQ9dWxmeNiKDzfhBZa" ) != std::string::npos ); + + auto _public_key_1b = extract_json( _obj.import_key( _token, "wallet_1", "5KNbAE7pLwsLbPUkz6kboVpTR24CycqSNHDG95Y8nbQqSqd6tgS" ) ); + BOOST_TEST_MESSAGE( _public_key_1b ); + BOOST_REQUIRE( _public_key_1b.find( "7j1orEPpWp4bU2SuH46eYXuXkFKEMeJkuXkZVJSaru2zFDGaEH" ) != std::string::npos ); + + auto _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); + BOOST_REQUIRE( _result.wallets.begin()->unlocked ); + BOOST_REQUIRE( _result.wallets.rbegin()->unlocked ); + } + + { + auto _keys_all = fc::json::from_string( extract_json( _obj.get_public_keys( _token ) ), fc::json::format_validation_mode::full ).as().keys; + auto _keys_wallet_0 = fc::json::from_string( extract_json( _obj.get_public_keys( _token, "wallet_0" ) ), fc::json::format_validation_mode::full ).as().keys; + auto _keys_wallet_1 = fc::json::from_string( extract_json( _obj.get_public_keys( _token, "wallet_1" ) ), fc::json::format_validation_mode::full ).as().keys; + + BOOST_REQUIRE_EQUAL( _keys_all.size(), 3 ); + BOOST_REQUIRE_EQUAL( _keys_wallet_0.size(), 1 ); + BOOST_REQUIRE_EQUAL( _keys_wallet_1.size(), 2 ); + } + + _obj.remove_key( _token, "wallet_1", "6oR6ckA4TejTWTjatUdbcS98AKETc3rcnQ9dWxmeNiKDzfhBZa" ); + + auto _public_keys = extract_json( _obj.get_public_keys( _token ) ); + BOOST_TEST_MESSAGE( _public_keys ); + + _obj.close_session( _token ); + + _token = extract_json( _obj.create_session( "banana" ) ); + BOOST_TEST_MESSAGE( _token ); + _token = get_wasm_data( _token ); + + _obj.open( _token, "wallet_1" ); + + BOOST_REQUIRE( extract_json( _obj.get_public_keys( _token ) ).find( "You don't have any unlocked wallet" ) != std::string::npos ); + + _obj.close( _token, "wallet_1" ); + + _obj.unlock( _token, "wallet_0", _password_0 ); + + _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); /// Both wallets should be returned + BOOST_REQUIRE( _result.wallets.begin()->name == "wallet_0" ); + BOOST_REQUIRE( _result.wallets.begin()->unlocked ); + BOOST_REQUIRE( _result.wallets.rbegin()->name == "wallet_1" ); + BOOST_REQUIRE( _result.wallets.rbegin()->unlocked == false ); + + } + + _obj.unlock( _token, "wallet_1", _password_1 ); + + _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); + BOOST_REQUIRE( _result.wallets.begin()->unlocked ); + BOOST_REQUIRE( _result.wallets.rbegin()->unlocked ); + } + + auto _info = _obj.get_info( _token ); + BOOST_TEST_MESSAGE( _info ); + + _obj.set_timeout( _token, 1 ); + + std::this_thread::sleep_for( std::chrono::seconds(2) ); + + _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); + //although WASM beekeeper automatic locking is disabled, calling API endpoint triggers locks for every wallet + BOOST_REQUIRE( !_result.wallets.begin()->unlocked ); + BOOST_REQUIRE( !_result.wallets.rbegin()->unlocked ); + } + + _info = _obj.get_info( _token ); + BOOST_TEST_MESSAGE( _info ); + + _obj.unlock( _token, "wallet_0", _password_0 ); + _obj.unlock( _token, "wallet_1", _password_1 ); + + _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); + BOOST_REQUIRE( _result.wallets.begin()->unlocked ); + BOOST_REQUIRE( _result.wallets.rbegin()->unlocked ); + } + + _obj.lock( _token, "wallet_0" ); + + _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); + BOOST_REQUIRE( !_result.wallets.begin()->unlocked ); + BOOST_REQUIRE( _result.wallets.begin()->name == "wallet_0" ); + BOOST_REQUIRE( _result.wallets.rbegin()->unlocked ); + } + + _obj.unlock( _token, "wallet_0", _password_0 ); + + _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); + BOOST_REQUIRE( _result.wallets.begin()->unlocked ); + BOOST_REQUIRE( _result.wallets.rbegin()->unlocked ); + } + + _obj.lock_all( _token ); + + _wallets = extract_json( _obj.list_wallets( _token ) ); + BOOST_TEST_MESSAGE( _wallets ); + { + beekeeper::list_wallets_return _result = fc::json::from_string( _wallets, fc::json::format_validation_mode::full ).as(); + BOOST_REQUIRE( _result.wallets.size() == 2 ); + BOOST_REQUIRE( !_result.wallets.begin()->unlocked ); + BOOST_REQUIRE( !_result.wallets.rbegin()->unlocked ); + } + +#ifdef HIVE_PROTOCOL_AVAILABLE + { + _obj.unlock( _token, "wallet_0", _password_0 ); + + auto _private_key_str = "5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n"; + auto _public_key_str = "6LLegbAgLAy28EHrffBVuANFWcFgmqRMW13wBmTExqFE9SCkg4"; + + const auto _private_key = private_key_type::wif_to_key( _private_key_str ).value(); + + auto _calculate_signature = [&]( const std::string& json_trx, const std::string& signature_pattern ) + { + hive::protocol::transaction _trx = fc::json::from_string( json_trx, fc::json::format_validation_mode::full ).as(); + hive::protocol::digest_type _sig_digest = _trx.sig_digest( HIVE_CHAIN_ID, hive::protocol::pack_type::hf26 ); + + auto _signature_local = _private_key.sign_compact( _sig_digest ); + + auto _signature_beekeeper = fc::json::from_string( extract_json( _obj.sign_digest( _token, _sig_digest, std::string( HIVE_ADDRESS_PREFIX ) + _public_key_str ) ), fc::json::format_validation_mode::full ).as(); + + auto _error_message = _obj.sign_digest( _token, _sig_digest, _public_key_str, "avocado" ); + BOOST_REQUIRE( _error_message.find( "public key requires STM prefix" ) != std::string::npos ); + + _error_message = _obj.sign_digest( _token, _sig_digest, std::string( HIVE_ADDRESS_PREFIX ) + _public_key_str, "avocado" ); + BOOST_REQUIRE( _error_message.find( "not found in avocado wallet" ) != std::string::npos ); + + auto _signature_beekeeper_2 = fc::json::from_string( extract_json( _obj.sign_digest( _token, _sig_digest, std::string( HIVE_ADDRESS_PREFIX ) + _public_key_str, "wallet_0" ) ), fc::json::format_validation_mode::full ).as(); + + auto _local = fc::json::to_string( _signature_local ); + auto _beekeeper = fc::json::to_string( _signature_beekeeper.signature ); + auto _beekeeper_2 = fc::json::to_string( _signature_beekeeper_2.signature ); + BOOST_TEST_MESSAGE( _local ); + BOOST_TEST_MESSAGE( _beekeeper ); + BOOST_REQUIRE( _beekeeper == _beekeeper_2 ); + BOOST_REQUIRE( _local.substr( 1, _local.size() - 2 ) == signature_pattern ); + BOOST_REQUIRE( _beekeeper.substr( 1, _beekeeper.size() - 2 ) == signature_pattern ); + }; + + std::string _signature_00_result = "1f17cc07f7c769073d39fac3385220b549e261fb33c5f619c5dced7f5b0fe9c0954f2684e703710840b7ea01ad7238b8db1d8a9309d03e93de212f86de38d66f21"; + _calculate_signature( "{}", _signature_00_result ); + + //trx: "{\"ref_block_num\":95,\"ref_block_prefix\":4189425605,\"expiration\":\"2023-07-18T08:38:29\",\"operations\":[{\"type\":\"transfer_operation\",\"value\":{\"from\":\"initminer\",\"to\":\"alice\",\"amount\":{\"amount\":\"666\",\"precision\":3,\"nai\":\"@@000000021\"},\"memo\":\"memmm\"}}],\"extensions\":[],\"signatures\":[],\"transaction_id\":\"cc9630cdbc39da1c9b6264df3588c7bedb5762fa\",\"block_num\":0,\"transaction_num\":0}" + std::string _signature_01_result = "1f69e091fc79b0e8d1812fc662f12076561f9e38ffc212b901ae90fe559f863ad266fe459a8e946cff9bbe7e56ce253bbfab0cccdde944edc1d05161c61ae86340"; + _calculate_signature( "{\"ref_block_num\":95,\"ref_block_prefix\":4189425605,\"expiration\":\"2023-07-18T08:38:29\",\"operations\":[{\"type\":\"transfer_operation\",\"value\":{\"from\":\"initminer\",\"to\":\"alice\",\"amount\":{\"amount\":\"666\",\"precision\":3,\"nai\":\"@@000000021\"},\"memo\":\"memmm\"}}],\"extensions\":[],\"signatures\":[],\"transaction_id\":\"cc9630cdbc39da1c9b6264df3588c7bedb5762fa\",\"block_num\":0,\"transaction_num\":0}", + _signature_01_result ); + + auto _error_message = _obj.sign_digest( _token, "", std::string( HIVE_ADDRESS_PREFIX ) + _public_key_str, "avocado" ); + BOOST_REQUIRE( _error_message.find( "`sig_digest` can't be empty" ) != std::string::npos ); + } +#endif // HIVE_PROTOCOL_AVAILABLE + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wasm_beekeeper_false) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + { + beekeeper_api _obj( { "--unknown-parameter", "value_without_sense" } ); + + auto _init_error_msg = extract_json( _obj.init() ); + BOOST_REQUIRE( _init_error_msg.find( "unrecognised option" ) != std::string::npos ); + BOOST_REQUIRE( _init_error_msg.find( "--unknown-parameter" ) != std::string::npos ); + BOOST_REQUIRE( _init_error_msg.find( "value_without_sense" ) != std::string::npos ); + + auto _create_session_error_msg = extract_json( _obj.create_session( "banana" ) ); + BOOST_REQUIRE( _create_session_error_msg.find( "Initialization failed. API call aborted." ) != std::string::npos ); + } + + { + beekeeper_api _obj( { "--export-keys-wallet", "[\"2\", \"PW5JViFn5gd4rt6ohk7DQMgHzQN6Z9FuMRfKoE5Ysk25mkjy5AY1b\"]" } ); + + auto _init_error_msg = extract_json( _obj.init() ); + BOOST_REQUIRE( _init_error_msg.find( "unrecognised option" ) != std::string::npos ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallet_manager_brute_force_protection_test) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + uint64_t _interval = 10; + beekeeper::extended_api _api( _interval ); + + const uint32_t _nr_threads = 10; + + auto _unlock_in_threads = [&]() + { + std::atomic _ready{false}; + std::mutex _mtx_message; + std::mutex _mtx; + std::condition_variable _cv; + + std::vector> threads; + + auto _start = std::chrono::high_resolution_clock::now(); + + auto _calculate_interval = [&_start]( size_t number_thread ) + { + std::string _message = "*****thread: " + std::to_string( number_thread ) + " *****"; + auto _duration = std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - _start ); + BOOST_TEST_MESSAGE( _message + std::to_string( _duration.count() ) + " [ms]" ); + + return _duration; + }; + + auto _calculate_summary = [&_start]() + { + auto _duration = std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - _start ); + BOOST_TEST_MESSAGE( std::to_string( _duration.count() ) + " [ms]" ); + + return _duration; + }; + + std::atomic _cnt{0}; + + for( size_t i = 0; i < _nr_threads; ++i ) + threads.emplace_back( std::make_shared( [&]( size_t number_thread ) + { + { + std::unique_lock _lock( _mtx ); + _cv.wait( _lock, [&_ready](){ return true; } ); + } + + if( number_thread % 2 == 0 ) + { + while( _cnt.load() < _nr_threads / 2 ) + { + _api.was_error(); + std::this_thread::sleep_for( std::chrono::milliseconds(1) ); + } + } + else + { + while( _api.unlock_allowed() != beekeeper::extended_api::enabled_after_interval ) + { + std::this_thread::sleep_for( std::chrono::milliseconds(1) ); + } + { + std::lock_guard _guard( _mtx_message ); + _calculate_interval( number_thread / 2 ); + _cnt.store( _cnt.load() + 1 ); + } + } + }, i ) ); + + std::this_thread::sleep_for( std::chrono::milliseconds(10) ); + _ready.store( true ); + _cv.notify_all(); + + for( auto& thread : threads ) + thread->join(); + + return _calculate_summary(); + }; + + const uint32_t _nr_attempts = 20; + bool _work_in_threads_was_correct = false; + for( uint32_t i = 0; i < _nr_attempts; ++i ) + { + auto _duration = _unlock_in_threads(); + if( _duration.count() >= (int64_t)( _interval * ( _nr_threads / 2 ) ) ) + { + BOOST_TEST_MESSAGE("********unlocks work correctly in many threads. Finished: (" + std::to_string(i) + ")"); + _work_in_threads_was_correct = true; + break; + } + else + { + BOOST_TEST_MESSAGE("********unlocks didn't work correctly in many threads. Repeating: (" + std::to_string(i) + ")"); + } + } + BOOST_REQUIRE( _work_in_threads_was_correct ); + + } FC_LOG_AND_RETHROW() +} + +template +class timeout_simulation +{ + struct wallet + { + std::string name; + std::string password; + + bool operator<( const wallet& obj ) const + { + return name < obj.name; + } + }; + + struct session + { + std::string name; + size_t timeout = 0; + std::string token; + + std::set wallets; + }; + + struct simulation + { + std::vector sessions; + }; + + public: + + std::string create_session( bekeeper_type& beekeeper_obj ); + std::string create( bekeeper_type& beekeeper_obj, const std::string& token, const std::string& name ); + flat_set list_wallets( bekeeper_type& beekeeper_obj, const std::string& token ); + keys_details get_public_keys( bekeeper_type& beekeeper_obj, const std::string& token, const std::optional& wallet_name ); + + simulation create( bekeeper_type& beekeeper_obj, const std::string& name, size_t nr_sessions, size_t nr_wallets, const std::vector& timeouts ) + { + BOOST_TEST_MESSAGE("*********************" + name + "*********************"); + + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + simulation _sim; + BOOST_REQUIRE( nr_sessions == timeouts.size() ); + for( size_t session_cnt = 0; session_cnt < nr_sessions; ++session_cnt ) + { + session _s{ name + "-" + std::to_string( session_cnt ), timeouts[ session_cnt ] }; + + _s.token = create_session( beekeeper_obj ); + + for( uint32_t wallet_cnt = 0; wallet_cnt < nr_wallets; ++wallet_cnt ) + { + wallet _w{ _s.name + "-w-" + std::to_string( wallet_cnt ) }; + + _w.password = create( beekeeper_obj, _s.token, _w.name ); + + _s.wallets.emplace( _w ); + } + + beekeeper_obj.set_timeout( _s.token, _s.timeout ); + _sim.sessions.emplace_back( _s ); + } + + return _sim; + }; + + void test( bekeeper_type& beekeeper_obj, const std::string& name, const simulation& sim, size_t timeout ) + { + BOOST_TEST_MESSAGE("=============" + name + "============="); + + std::this_thread::sleep_for( std::chrono::seconds( timeout ) ); + + for( size_t session_cnt = 0; session_cnt < sim.sessions.size(); ++session_cnt ) + { + auto& _s = sim.sessions[ session_cnt ]; + BOOST_TEST_MESSAGE( "+++++ session: name: " + _s.name + " token: " + _s.token + " timeout: " + std::to_string( _s.timeout ) + " +++++" ); + + /* + Call this method only in order to refresh timeout, because a method `list_wallets` doesn't refresh timeout anymore. + */ + for( auto& wallet_item : _s.wallets ) + { + try + { + get_public_keys( beekeeper_obj, _s.token, wallet_item.name ); + } + catch(...) + { + + } + } + + auto _wallets = list_wallets( beekeeper_obj, _s.token ); + + for( auto& wallet_item : _wallets ) + { + if( _s.wallets.find( wallet{ wallet_item.name } ) != _s.wallets.end() ) + { + BOOST_TEST_MESSAGE( "+++++ " + wallet_item.name + " +++++" ); + BOOST_REQUIRE( timeout >= _s.timeout ? !wallet_item.unlocked : wallet_item.unlocked ); + } + } + BOOST_TEST_MESSAGE( "" ); + + } + }; + +}; + +template<> +std::string timeout_simulation::create_session( beekeeper_api& beekeeper_obj ) +{ + return get_wasm_data( extract_json( beekeeper_obj.create_session( "salt" ) ) ); +} + +template<> +std::string timeout_simulation::create_session( beekeeper_wallet_manager& beekeeper_obj ) +{ + return beekeeper_obj.create_session( "this is salt" ); +} + +template<> +std::string timeout_simulation::create( beekeeper_api& beekeeper_obj, const std::string& token, const std::string& name ) +{ + return get_wasm_data( extract_json( beekeeper_obj.create( token, name ) ) ); +} + +template<> +std::string timeout_simulation::create( beekeeper_wallet_manager& beekeeper_obj, const std::string& token, const std::string& name ) +{ + return beekeeper_obj.create( token, name, std::optional(), false ); +} + +template<> +flat_set timeout_simulation::list_wallets( beekeeper_api& beekeeper_obj, const std::string& token ) +{ + auto _result = extract_json( beekeeper_obj.list_wallets( token ) ); + return fc::json::from_string( _result, fc::json::format_validation_mode::full ). template as().wallets; +} + +template<> +flat_set timeout_simulation::list_wallets( beekeeper_wallet_manager& beekeeper_obj, const std::string& token ) +{ + return beekeeper_obj.list_wallets( token ); +} + +template<> +keys_details timeout_simulation::get_public_keys( beekeeper_api& beekeeper_obj, const std::string& token, const std::optional& wallet_name ) +{ + auto _result = extract_json( wallet_name ? beekeeper_obj.get_public_keys( token, *wallet_name ) : beekeeper_obj.get_public_keys( token ) ); + return fc::json::from_string( _result, fc::json::format_validation_mode::full ). template as(); +} + +template<> +keys_details timeout_simulation::get_public_keys( beekeeper_wallet_manager& beekeeper_obj, const std::string& token, const std::optional& wallet_name ) +{ + return beekeeper_obj.get_public_keys( token, wallet_name ); +} + +class wasm_simulation_executor +{ + test_utils::beekeeper_mgr b_mgr; + timeout_simulation sim; + + public: + + void run( const std::string& simulation_name, const uint32_t nr_sessions, const uint32_t nr_wallets, const std::vector& timeouts, const std::vector& stage_timeouts ) + { + for( auto& stage_timeout : stage_timeouts ) + { + beekeeper_api _beekeeper( { "--wallet-dir", b_mgr.dir.string() } ); + BOOST_REQUIRE( fc::json::from_string( extract_json( _beekeeper.init() ), fc::json::format_validation_mode::full ).as().status == 0 ); + + auto _details = sim.create( _beekeeper, simulation_name, nr_sessions, nr_wallets, timeouts ); + sim.test( _beekeeper, "Wait for: " + std::to_string( stage_timeout ) + "[s]", _details, stage_timeout ); + } + } +}; + +class simulation_executor +{ + test_utils::beekeeper_mgr b_mgr; + timeout_simulation sim; + + appbase::application app; + + uint64_t _unlock_timeout = 900; + int32_t _session_limit = 64; + + public: + + void run( const std::string& simulation_name, const uint32_t nr_sessions, const uint32_t nr_wallets, const std::vector& timeouts, const std::vector& stage_timeouts ) + { + for( auto& stage_timeout : stage_timeouts ) + { + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _unlock_timeout, _session_limit ); + + auto _details = sim.create( _beekeeper, simulation_name, nr_sessions, nr_wallets, timeouts ); + sim.test( _beekeeper, "Wait for: " + std::to_string( stage_timeout ) + "[s]", _details, stage_timeout ); + } + } +}; + +BOOST_AUTO_TEST_CASE(wasm_beekeeper_timeout) +{ + try { + { + wasm_simulation_executor _executer; + _executer.run( "a-sim", 2/*nr_sessions*/, 3/*nr_wallets*/, {1, 1}/*timeouts*/, {0}/*stage_timeouts*/ ); + } + + { + wasm_simulation_executor _executer; + _executer.run( "b-sim", 2/*nr_sessions*/, 2/*nr_wallets*/, {1, 3}/*timeouts*/, {1}/*stage_timeouts*/ ); + } + + { + wasm_simulation_executor _executer; + _executer.run( "c-sim", 3/*nr_sessions*/, 1/*nr_wallets*/, {3, 1, 2}/*timeouts*/, {0, 1, 3}/*stage_timeouts*/ ); + } + + { + wasm_simulation_executor _executer; + _executer.run( "d-sim", 5/*nr_sessions*/, 1/*nr_wallets*/, {4, 3, 2, 1, 0}/*timeouts*/, {1, 2, 1, 1, 1}/*stage_timeouts*/ ); + } + + { + wasm_simulation_executor _executer; + _executer.run( "e-sim", 4/*nr_sessions*/, 3/*nr_wallets*/, {3, 3, 3, 1}/*timeouts*/, {1, 1, 1}/*stage_timeouts*/ ); + } + + { + wasm_simulation_executor _executer; + _executer.run( "f-sim", 4/*nr_sessions*/, 2/*nr_wallets*/, {1, 1, 1, 1}/*timeouts*/, {2, 1}/*stage_timeouts*/ ); + } + + { + wasm_simulation_executor _executer; + _executer.run( "g-sim", 4/*nr_sessions*/, 1/*nr_wallets*/, {3, 1, 3, 1}/*timeouts*/, {2}/*stage_timeouts*/ ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(beekeeper_timeout) +{ + try { + { + simulation_executor _executer; + _executer.run( "a-sim", 2/*nr_sessions*/, 3/*nr_wallets*/, {1, 1}/*timeouts*/, {0}/*stage_timeouts*/ ); + } + + { + simulation_executor _executer; + _executer.run( "b-sim", 2/*nr_sessions*/, 2/*nr_wallets*/, {1, 3}/*timeouts*/, {1}/*stage_timeouts*/ ); + } + + { + simulation_executor _executer; + _executer.run( "c-sim", 3/*nr_sessions*/, 1/*nr_wallets*/, {3, 1, 2}/*timeouts*/, {0, 1, 3}/*stage_timeouts*/ ); + } + + { + simulation_executor _executer; + _executer.run( "d-sim", 5/*nr_sessions*/, 1/*nr_wallets*/, {4, 3, 2, 1, 0}/*timeouts*/, {1, 2, 1, 1, 1}/*stage_timeouts*/ ); + } + + { + simulation_executor _executer; + _executer.run( "e-sim", 4/*nr_sessions*/, 3/*nr_wallets*/, {3, 3, 3, 1}/*timeouts*/, {1, 1, 1}/*stage_timeouts*/ ); + } + + { + simulation_executor _executer; + _executer.run( "f-sim", 4/*nr_sessions*/, 2/*nr_wallets*/, {1, 1, 1, 1}/*timeouts*/, {2, 1}/*stage_timeouts*/ ); + } + + { + simulation_executor _executer; + _executer.run( "g-sim", 4/*nr_sessions*/, 1/*nr_wallets*/, {3, 1, 3, 1}/*timeouts*/, {2}/*stage_timeouts*/ ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wasm_beekeeper_refresh_timeout) +{ + try { + auto _list_wallets_action = []( beekeeper_api& beekeeper, const std::string& token ) + { + auto _result = extract_json( beekeeper.list_wallets( token ) ); + auto _wallets = fc::json::from_string( _result, fc::json::format_validation_mode::full ).as().wallets; + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->unlocked, true ); + }; + + auto _set_timeout_action = []( beekeeper_api& beekeeper, const std::string& token ) + { + beekeeper.set_timeout( token, 1 ); + }; + + using action_type = std::function; + + auto _refresh_timeout_simulation = []( action_type&& action, action_type&& aux_action = action_type() ) + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + beekeeper_api _beekeeper( { "--wallet-dir", b_mgr.dir.string() } ); + BOOST_REQUIRE( fc::json::from_string( extract_json( _beekeeper.init() ), fc::json::format_validation_mode::full ).as().status == 0 ); + + auto _token = get_wasm_data( extract_json( _beekeeper.create_session( "salt" ) ) ); + _beekeeper.create( _token, "w0" ); + + _beekeeper.set_timeout( _token, 1 ); + + for( uint32_t i = 0; i < 4; ++i ) + { + std::this_thread::sleep_for( std::chrono::milliseconds(500) ); + action( _beekeeper, _token ); + } + + if( aux_action ) + aux_action( _beekeeper, _token ); + }; + + _refresh_timeout_simulation( _list_wallets_action ); + _refresh_timeout_simulation( _set_timeout_action ,_list_wallets_action ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(beekeeper_refresh_timeout) +{ + try { + const std::string _wallet_name = "0"; + + auto _list_wallets_action = [&_wallet_name]( beekeeper_wallet_manager& beekeeper, const std::string& token ) + { + /* + Call this method only in order to refresh timeout, because a method `list_wallets` doesn't refresh timeout anymore. + */ + try + { + beekeeper.get_public_keys( token, _wallet_name ); + } + catch(...) + { + + } + + auto _wallets = beekeeper.list_wallets( token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->unlocked, true ); + }; + + auto _set_timeout_action = []( beekeeper_wallet_manager& beekeeper, const std::string& token ) + { + beekeeper.set_timeout( token, 1 ); + }; + + using action_type = std::function; + + auto _refresh_timeout_simulation = [&_wallet_name]( action_type&& action, action_type&& aux_action = action_type() ) + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + auto _password = _beekeeper.create( _token, "0", std::optional(), false/*is_temporary*/ ); + _beekeeper.set_timeout( _token, 1 ); + + for( uint32_t i = 0; i < 12; ++i ) + { + std::this_thread::sleep_for( std::chrono::milliseconds(250) ); + action( _beekeeper, _token ); + } + + if( aux_action ) + aux_action( _beekeeper, _token ); + }; + + _refresh_timeout_simulation( _list_wallets_action ); + _refresh_timeout_simulation( _set_timeout_action ,_list_wallets_action ); + + } FC_LOG_AND_RETHROW() +} + +#ifdef HIVE_PROTOCOL_AVAILABLE +BOOST_AUTO_TEST_CASE(has_matching_private_key_endpoint_test) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + hive::protocol::serialization_mode_controller::pack_guard guard( hive::protocol::pack_type::hf26 ); + + auto _private_key_str = "5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n"; + auto _public_key = public_key_type::from_base58( "6LLegbAgLAy28EHrffBVuANFWcFgmqRMW13wBmTExqFE9SCkg4", false/*is_sha256*/ ); + + auto _private_key_str_2 = "5J8C7BMfvMFXFkvPhHNk2NHGk4zy3jF4Mrpf5k5EzAecuuzqDnn"; + auto _public_key_2 = public_key_type::from_base58( "6Pg5jd1w8rXgGoqvpZXy1tHPdz43itPW6L2AGJuw8kgSAbtsxm", false/*is_sha256*/ ); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + auto _prefix = "ABC"; + + appbase::application app; + + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _session_limit, [](){} ); + BOOST_REQUIRE( wm.start() ); + + auto _token = wm.create_session( "salt" ); + auto _password = wm.create( _token, "0", std::optional(), false/*is_temporary*/ ); + + wm.import_key( _token, "0", _private_key_str, _prefix ); + + BOOST_REQUIRE_THROW( wm.has_matching_private_key( _token, "pear", _public_key ), fc::exception ); + BOOST_REQUIRE_THROW( wm.has_matching_private_key( "_token", "0", _public_key ), fc::exception ); + + BOOST_REQUIRE_EQUAL( wm.has_matching_private_key( _token, "0", _public_key ), true ); + BOOST_REQUIRE_EQUAL( wm.has_matching_private_key( _token, "0", _public_key_2 ), false ); + + wm.import_key( _token, "0", _private_key_str_2, _prefix ); + + BOOST_REQUIRE_EQUAL( wm.has_matching_private_key( _token, "0", _public_key ), true ); + BOOST_REQUIRE_EQUAL( wm.has_matching_private_key( _token, "0", _public_key_2 ), true ); + + wm.close( _token, "0" ); + + BOOST_REQUIRE_THROW( wm.has_matching_private_key( _token, "0", _public_key ), fc::exception ); + BOOST_REQUIRE_THROW( wm.has_matching_private_key( _token, "0", _public_key_2 ), fc::exception ); + + } FC_LOG_AND_RETHROW() +} +#endif // HIVE_PROTOCOL_AVAILABLE + +BOOST_AUTO_TEST_CASE(beekeeper_timeout_unlock) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + + struct wallet + { + std::string name; + std::string password; + }; + std::vector _wallets{ { "0" }, { "1" } }; + + for( auto& wallet : _wallets ) + { + wallet.password = _beekeeper.create( _token, wallet.name, std::optional(), false/*is_temporary*/ ); + } + { + _beekeeper.set_timeout( _token, 1 ); + std::this_thread::sleep_for( std::chrono::milliseconds(1200) ); + } + { + for( auto& wallet : _wallets ) + { + _beekeeper.unlock( _token, wallet.name, wallet.password ); + } + } + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(beekeeper_timeout_list_wallets) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + + struct wallet + { + std::string name; + std::string password; + }; + std::vector _wallets{ { "0" }, { "1" }, { "2" } }; + + for( auto& wallet : _wallets ) + { + wallet.password = _beekeeper.create( _token, wallet.name, std::optional(), false/*is_temporary*/ ); + _beekeeper.close( _token, wallet.name ); + } + { + auto _wallets = _beekeeper.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 0 ); + } + { + const size_t _wallet_cnt = 1; + _beekeeper.unlock( _token, _wallets[_wallet_cnt].name, _wallets[_wallet_cnt].password ); + } + { + _beekeeper.set_timeout( _token, 1 ); + std::this_thread::sleep_for( std::chrono::milliseconds(1200) ); + } + { + auto _wallets = _beekeeper.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->unlocked, false ); + } + { + auto _iter = _wallets.begin(); + ++_iter; + _beekeeper.unlock( _token, _iter->name, _iter->password ); + } + { + auto _wallets = _beekeeper.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->unlocked, true ); + } + { + std::this_thread::sleep_for( std::chrono::milliseconds(1200) ); + } + { + auto _wallets = _beekeeper.list_wallets( _token ); + BOOST_REQUIRE_EQUAL( _wallets.size(), 1 ); + BOOST_REQUIRE_EQUAL( _wallets.begin()->unlocked, false ); + } + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(data_reliability_when_file_with_wallet_is_removed) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + auto _prefix = "STM"; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + + struct keys + { + std::string private_key; + }; + std::vector _keys_a = + { + {"5J15npVK6qABGsbdsLnJdaF5esrEWxeejeE3KUx6r534ug4tyze"}, + {"5K1gv5rEtHiACVTFq9ikhEijezMh4rkbbTPqu4CAGMnXcTLC1su"}, + {"5KLytoW1AiGSoHHBA73x1AmgZnN16QDgU1SPpG9Vd2dpdiBgSYw"}, + {"5KXNQP5feaaXpp28yRrGaFeNYZT7Vrb1PqLEyo7E3pJiG1veLKG"}, + {"5KKvoNaCPtN9vUEU1Zq9epSAVsEPEtocbJsp7pjZndt9Rn4dNRg"} + }; + std::vector _keys_b = + { + {"5JkFnXrLM2ap9t3AmAxBJvQHF7xSKtnTrCTginQCkhzU5S7ecPT"}, + {"5KGKYWMXReJewfj5M29APNMqGEu173DzvHv5TeJAg9SkjUeQV78"}, + {"5KNbAE7pLwsLbPUkz6kboVpTR24CycqSNHDG95Y8nbQqSqd6tgS"}, + {"5JNHfZYKGaomSFvd4NUdQ9qMcEAC43kujbfjueTHpVapX1Kzq2n"}, + {"5J8C7BMfvMFXFkvPhHNk2NHGk4zy3jF4Mrpf5k5EzAecuuzqDnn"} + }; + + struct wallet + { + std::string name; + std::string password; + }; + std::vector _wallets{ { "0" }, { "1" }, { "2" } }; + + for( auto& wallet : _wallets ) + { + wallet.password = _beekeeper.create( _token, wallet.name, std::optional(), false/*is_temporary*/ ); + for( auto& item : _keys_a ) + { + _beekeeper.import_key( _token, wallet.name, item.private_key, _prefix ); + } + } + + b_mgr.remove_wallet( _wallets[0].name ); + b_mgr.remove_wallet( _wallets[1].name ); + + auto _cmp = []( const keys_details& a, const keys_details& b ) + { + flat_set _a; + boost::copy( a | boost::adaptors::map_keys, std::inserter( _a, _a.end() ) ); + + flat_set _b; + boost::copy( b | boost::adaptors::map_keys, std::inserter( _b, _b.end() ) ); + + if( _a.size() != _b.size() ) + return false; + + for( auto& item : _a ) + { + if( _b.find( item ) == _b.end() ) + { + return false; + } + } + + return true; + }; + + { + for( auto& item : _keys_a ) + _beekeeper.import_key( _token, _wallets[0].name, item.private_key, _prefix ); + + auto _public_keys_0 = _beekeeper.get_public_keys( _token, _wallets[0].name ); + auto _public_keys_2 = _beekeeper.get_public_keys( _token, _wallets[2].name ); + BOOST_REQUIRE_EQUAL( _public_keys_0.size(), 5 ); + BOOST_REQUIRE( _cmp( _public_keys_0, _public_keys_2 ) ); + } + { + for( auto& item : _keys_b ) + { + _beekeeper.import_key( _token, _wallets[1].name, item.private_key, _prefix ); + _beekeeper.import_key( _token, _wallets[2].name, item.private_key, _prefix ); + } + + auto _public_keys_1 = _beekeeper.get_public_keys( _token, _wallets[1].name ); + auto _public_keys_2 = _beekeeper.get_public_keys( _token, _wallets[2].name ); + BOOST_REQUIRE_EQUAL( _public_keys_1.size(), 10 ); + BOOST_REQUIRE( _cmp( _public_keys_1, _public_keys_2 ) ); + } + + BOOST_REQUIRE( !b_mgr.exists_wallet( _wallets[0].name ) ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallets[1].name ) ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallets[2].name ) ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(encrypt_decrypt_data) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + auto _prefix = "STM"; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + + struct keys + { + std::string private_key; + std::string public_key; + }; + std::vector _keys = + { + {"5J15npVK6qABGsbdsLnJdaF5esrEWxeejeE3KUx6r534ug4tyze", "6TqSJaS1aRj6p6yZEo5xicX7bvLhrfdVqi5ToNrKxHU3FRBEdW"}, + {"5K1gv5rEtHiACVTFq9ikhEijezMh4rkbbTPqu4CAGMnXcTLC1su", "8LbCRyqtXk5VKbdFwK1YBgiafqprAd7yysN49PnDwAsyoMqQME"}, + {"5KLytoW1AiGSoHHBA73x1AmgZnN16QDgU1SPpG9Vd2dpdiBgSYw", "8FDsHdPkHbY8fuUkVLyAmrnKMvj6DddLopi3YJ51dVqsG9vZa4"}, + {"5KXNQP5feaaXpp28yRrGaFeNYZT7Vrb1PqLEyo7E3pJiG1veLKG", "6a34GANY5LD8deYvvfySSWGd7sPahgVNYoFPapngMUD27pWb45"} + }; + + const std::string _fruits_content = "avocado-banana-cherry-durian"; + const std::string _empty_content = ""; + const std::string _dummy_content = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"; + const std::optional _nonce; + + auto _encrypt_with_nonce = [&_token, &_beekeeper, &_keys, &_prefix ]( uint32_t nr_from_public_key, uint32_t nr_to_public_key, const std::string& wallet_name, const std::string& content, const std::optional& nonce ) + { + return _beekeeper.encrypt_data( _token, public_key_type::from_base58( _keys[nr_from_public_key].public_key, false/*is_sha256*/ ), public_key_type::from_base58( _keys[nr_to_public_key].public_key, false/*is_sha256*/ ), wallet_name, content, nonce, _prefix ); + }; + + auto _encrypt = [&_token, &_beekeeper, &_keys, &_prefix]( uint32_t nr_from_public_key, uint32_t nr_to_public_key, const std::string& wallet_name, const std::string& content ) + { + auto __encrypt = [&]() + { + return _beekeeper.encrypt_data(_token, public_key_type::from_base58( _keys[nr_from_public_key].public_key, false/*is_sha256*/ ), public_key_type::from_base58( _keys[nr_to_public_key].public_key, false/*is_sha256*/ ), wallet_name, content, std::optional(), _prefix ); + }; + + std::string _encrypted_content = __encrypt(); + std::string _encrypted_content_2 = __encrypt(); + + BOOST_REQUIRE( _encrypted_content != _encrypted_content_2 ); + + return std::make_pair( _encrypted_content, _encrypted_content_2 ); + }; + + auto _decrypt = [&_token, &_beekeeper, &_keys]( const std::string& pattern, uint32_t nr_from_public_key, uint32_t nr_to_public_key, const std::string& wallet_name, const std::string& content ) + { + std::string _encrypted_content = _beekeeper.decrypt_data( _token, public_key_type::from_base58( _keys[nr_from_public_key].public_key, false/*is_sha256*/ ), public_key_type::from_base58( _keys[nr_to_public_key].public_key, false/*is_sha256*/ ), wallet_name, content ); + BOOST_REQUIRE_EQUAL( _encrypted_content, pattern ); + }; + + struct wallet + { + std::string name; + std::string password; + }; + std::vector _wallets{ { "0" }, { "1" }, { "2" }, { "3" } }; + + //========================Preparation======================== + /* + wallet "0" has _keys[0] + wallet "1" has _keys[1] + wallet "2" has _keys[0] and _keys[1] + wallet "3" has _keys[2] and _keys[3] + */ + auto _cnt = 0; + for( auto& wallet : _wallets ) + { + wallet.password = _beekeeper.create( _token, wallet.name, std::optional(), false/*is_temporary*/ ); + switch( _cnt ) + { + case 0: + _beekeeper.import_key( _token, wallet.name, _keys[0].private_key, _prefix ); + break; + case 1: + _beekeeper.import_key( _token, wallet.name, _keys[1].private_key, _prefix ); + break; + case 2: + _beekeeper.import_key( _token, wallet.name, _keys[0].private_key, _prefix ); + _beekeeper.import_key( _token, wallet.name, _keys[1].private_key, _prefix ); + break; + case 3: + _beekeeper.import_key( _token, wallet.name, _keys[2].private_key, _prefix ); + _beekeeper.import_key( _token, wallet.name, _keys[3].private_key, _prefix ); + break; + } + ++_cnt; + } + _beekeeper.lock_all( _token ); + //========================End of preparation======================== + + { + //lack of unlocked wallets + BOOST_REQUIRE_THROW( _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content ), fc::exception ); + BOOST_REQUIRE_THROW( _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _empty_content ), fc::exception ); + } + { + //unlock wallet "0" + _beekeeper.unlock( _token, _wallets[0].name, _wallets[0].password ); + + auto _encrypted_content = _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content ); + _decrypt( _fruits_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content.first ); + _decrypt( _fruits_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content.second ); + + auto _encrypted_content_2 = _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _empty_content ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content_2.first ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content_2.second ); + + _beekeeper.lock_all( _token ); + + BOOST_REQUIRE_THROW( _decrypt( _dummy_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content.first ), fc::exception ); + BOOST_REQUIRE_THROW( _decrypt( _dummy_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content.second ), fc::exception ); + + BOOST_REQUIRE_THROW( _decrypt( _dummy_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content_2.first ), fc::exception ); + BOOST_REQUIRE_THROW( _decrypt( _dummy_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content_2.second ), fc::exception ); + } + { + //unlock wallet "1" + _beekeeper.unlock( _token, _wallets[1].name, _wallets[1].password ); + + BOOST_REQUIRE_THROW( _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[1].name, _fruits_content ), fc::exception ); + BOOST_REQUIRE_THROW( _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[1].name, _empty_content ), fc::exception ); + + _beekeeper.lock_all( _token ); + } + { + //unlock wallet "2" + _beekeeper.unlock( _token, _wallets[2].name, _wallets[2].password ); + + auto _encrypted_content = _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _fruits_content ); + _decrypt( _fruits_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _encrypted_content.first ); + _decrypt( _fruits_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _encrypted_content.second ); + + _encrypted_content = _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _empty_content ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _encrypted_content.first ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _encrypted_content.second ); + + _beekeeper.lock_all( _token ); + } + { + //unlock wallet "3" + _beekeeper.unlock( _token, _wallets[3].name, _wallets[3].password ); + + BOOST_REQUIRE_THROW( _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[3].name, _fruits_content ), fc::exception ); + BOOST_REQUIRE_THROW( _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[3].name, _empty_content ), fc::exception ); + + _beekeeper.lock_all( _token ); + } + { + //unlock all wallets + for( auto& wallet : _wallets ) + _beekeeper.unlock( _token, wallet.name, wallet.password ); + + auto _encrypted_content = _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content ); + _decrypt( _fruits_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _encrypted_content.first ); + _decrypt( _fruits_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content.second ); + + _encrypted_content = _encrypt( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _empty_content ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[2].name, _encrypted_content.first ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content.second ); + + //`from` key == `to` key + _encrypted_content = _encrypt( 0 /*nr_from_public_key*/, 0 /*nr_to_public_key*/, _wallets[2].name, _empty_content ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 0 /*nr_to_public_key*/, _wallets[0].name, _encrypted_content.first ); + _decrypt( _empty_content, 0 /*nr_from_public_key*/, 0 /*nr_to_public_key*/, _wallets[2].name, _encrypted_content.second ); + } + { + //test with different nonce + auto _777 = _encrypt_with_nonce( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content, 777 ); + auto _777_2 = _encrypt_with_nonce( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content, 777 ); + auto _888 = _encrypt_with_nonce( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content, 888 ); + auto _999 = _encrypt_with_nonce( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content, 999 ); + auto _empty_0 = _encrypt_with_nonce( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content, std::optional() ); + auto _empty_1 = _encrypt_with_nonce( 0 /*nr_from_public_key*/, 1 /*nr_to_public_key*/, _wallets[0].name, _fruits_content, std::optional() ); + + BOOST_REQUIRE( _777 == _777_2 ); + BOOST_REQUIRE( _777 != _888 ); + BOOST_REQUIRE( _888 != _999 ); + BOOST_REQUIRE( _empty_0 != _empty_1 ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(encrypt_decrypt_data_with_many_keys) +{ + try + { + const std::string _fruits_content = "avocado-banana-cherry-durian"; + const std::string _hex_content = "0123456789ABCDEFabcdef"; + const std::string _empty_content = ""; + + auto _different_keys = []( const std::string& content ) + { + //Using the `crypto_data` class do encryption/decryption for many `from`, `to` keys. + fc::crypto_data _cd; + + for( auto i = 0; i < 1000; ++i ) + { + auto _private_from = fc::ecc::private_key::generate(); + auto _private_to = fc::ecc::private_key::generate(); + + auto _public_from = _private_from.get_public_key(); + auto _public_to = _private_to.get_public_key(); + + std::string _encrypted_content = _cd.encrypt( _private_from, _public_to, content ); + + auto _private_key_finder_from = [&_public_from, &_private_from]( const fc::crypto_data::public_key_type& public_key ) + { + if( public_key == _public_from ) + return fc::optional( _private_from ); + return fc::optional(); + }; + + auto _private_key_finder_to = [&_public_to, &_private_to]( const fc::crypto_data::public_key_type& public_key ) + { + if( public_key == _public_to ) + return fc::optional( _private_to ); + return fc::optional(); + }; + + auto _result_00 = _cd.decrypt( _private_key_finder_from, _public_from, _public_to, _encrypted_content ); + BOOST_REQUIRE_EQUAL( _result_00, content ); + + auto _result_01 = _cd.decrypt( _private_key_finder_to, _public_from, _public_to, _encrypted_content ); + BOOST_REQUIRE_EQUAL( _result_01, content ); + } + }; + + auto _the_same_keys = []( const std::string& content ) + { + //Using the `crypto_data` class do encryption/decryption for many `from`, `to` keys. + //`from` == `to` + fc::crypto_data _cd; + + for( auto i = 0; i < 1000; ++i ) + { + auto _private = fc::ecc::private_key::generate(); + auto _public = _private.get_public_key(); + + std::string _encrypted_content = _cd.encrypt( _private, _public, content ); + + auto _private_key_finder = [&_private]( const fc::crypto_data::public_key_type& public_key ) + { + return fc::optional( _private ); + }; + + auto _result = _cd.decrypt( _private_key_finder, _public, _public, _encrypted_content ); + BOOST_REQUIRE_EQUAL( _result, content ); + } + }; + + _different_keys( _fruits_content ); + _different_keys( _hex_content ); + _different_keys( _empty_content ); + + _the_same_keys( _fruits_content ); + _the_same_keys( _hex_content ); + _the_same_keys( _empty_content ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(get_version) +{ + try { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + std::string _version_a; + std::string _version_b; + { + BOOST_TEST_MESSAGE( "Network beekeeper" ); + const uint64_t _timeout = 90; + const uint32_t _limit = 3; + + appbase::application app; + bool _checker = false; + + beekeeper_wallet_manager wm = b_mgr.create_wallet( app, _timeout, _limit, [&_checker](){ _checker = true; } ); + BOOST_REQUIRE( wm.start() ); + + auto _version = wm.get_version(); + _version_a = _version.version; + BOOST_REQUIRE( !_version_a.empty() ); + } + { + BOOST_TEST_MESSAGE( "WASM beekeeper" ); + beekeeper_api _obj( { "--wallet-dir", b_mgr.dir.string() } ); + + BOOST_REQUIRE( fc::json::from_string( extract_json( _obj.init() ), fc::json::format_validation_mode::full ).as().status == 0 ); + + auto _version_str = extract_json( _obj.get_version() ); + beekeeper::get_version_return _version = fc::json::from_string( _version_str, fc::json::format_validation_mode::full ).as(); + _version_b = _version.version; + BOOST_REQUIRE( !_version_b.empty() ); + } + { + BOOST_REQUIRE_EQUAL( _version_a, _version_b ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(import_keys) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + auto _prefix = "STM"; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + + const std::string _wallet_name = "wallet-0"; + + _beekeeper.create( _token, _wallet_name, std::optional(), false/*is_temporary*/ ); + + const size_t _nr_keys = 20'000; + std::vector _keys( _nr_keys ); + + for( size_t i = 0; i < _nr_keys; ++i ) + { + auto _priv = fc::ecc::private_key::generate(); + _keys[i] = _priv.key_to_wif(); + } + + auto _start = std::chrono::high_resolution_clock::now(); + + _beekeeper.import_keys( _token, _wallet_name, _keys, _prefix ); + + auto _duration = std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - _start ); + auto _time = _duration.count(); + + //AMD Ryzen 7 5800X 8-Core Processor ~800ms + BOOST_REQUIRE_LT( _time, 1500 ); + + BOOST_TEST_MESSAGE( std::to_string( _time ) + " [ms]" ); + + auto _public_keys = _beekeeper.get_public_keys( _token, _wallet_name ); + BOOST_REQUIRE_EQUAL( _public_keys.size(), _nr_keys ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(has_wallet) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + + const std::string _wallet_name = "wallet"; + const std::string _wallet_name_2 = "wallet-2"; + const std::string _wallet_name_3 = "wallet-3"; + const std::string _password = "avocado"; + + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name_3 ), false ); + _beekeeper.create( _token, _wallet_name_3, _password, true ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name_3 ), true ); + + _beekeeper.close( _token, _wallet_name_3 ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name_3 ), false ); + + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name ), false ); + _beekeeper.create( _token, _wallet_name, _password, false/*is_temporary*/ ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name ), true ); + + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name_2 ), false ); + _beekeeper.create( _token, _wallet_name_2, _password, false/*is_temporary*/ ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name ), true ); + + _beekeeper.close( _token, _wallet_name ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name ), true ); + + _beekeeper.close( _token, _wallet_name_2 ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name_2 ), true ); + + _beekeeper.unlock( _token, _wallet_name, _password ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name ), true ); + + _beekeeper.lock( _token, _wallet_name ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name ), true ); + BOOST_REQUIRE_EQUAL( _beekeeper.has_wallet( _token, _wallet_name_2 ), true ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(temporary_wallets) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _prefix = "STM"; + + struct keys + { + std::string private_key; + std::string public_key; + }; + std::vector _keys = + { + {"5J15npVK6qABGsbdsLnJdaF5esrEWxeejeE3KUx6r534ug4tyze", "6TqSJaS1aRj6p6yZEo5xicX7bvLhrfdVqi5ToNrKxHU3FRBEdW"}, + {"5K1gv5rEtHiACVTFq9ikhEijezMh4rkbbTPqu4CAGMnXcTLC1su", "8LbCRyqtXk5VKbdFwK1YBgiafqprAd7yysN49PnDwAsyoMqQME"}, + {"5KLytoW1AiGSoHHBA73x1AmgZnN16QDgU1SPpG9Vd2dpdiBgSYw", "8FDsHdPkHbY8fuUkVLyAmrnKMvj6DddLopi3YJ51dVqsG9vZa4"}, + {"5KXNQP5feaaXpp28yRrGaFeNYZT7Vrb1PqLEyo7E3pJiG1veLKG", "6a34GANY5LD8deYvvfySSWGd7sPahgVNYoFPapngMUD27pWb45"} + }; + + auto _token = _beekeeper.create_session( "salt" ); + + const std::string _wallet_name_0 = "wallet-0"; + const std::string _wallet_name_1 = "wallet-1"; + const std::string _wallet_name_2 = "wallet-2"; + const std::string _password = "avocado"; + + _beekeeper.create( _token, _wallet_name_0, _password, false/*is_temporary*/ ); + { + flat_set _wallets = _beekeeper.list_created_wallets( _token ); + BOOST_REQUIRE( _wallets.size() == 1 ); + BOOST_REQUIRE( _wallets.begin()->name == _wallet_name_0 ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_0 ) == true ); + } + + _beekeeper.create( _token, _wallet_name_1, _password, true/*is_temporary*/ ); + { + flat_set _wallets = _beekeeper.list_created_wallets( _token ); + BOOST_REQUIRE( _wallets.size() == 2 ); + BOOST_REQUIRE( _wallets.begin()->name == _wallet_name_0 ); + BOOST_REQUIRE( _wallets.rbegin()->name == _wallet_name_1 ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_0 ) == true ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_1 ) == false ); + } + + _beekeeper.create( _token, _wallet_name_2, _password, true/*is_temporary*/ ); + { + flat_set _wallets = _beekeeper.list_created_wallets( _token ); + auto _iter = _wallets.begin(); + BOOST_REQUIRE( _wallets.size() == 3 ); + BOOST_REQUIRE( _iter->name == _wallet_name_0 ); + ++_iter; + BOOST_REQUIRE( _iter->name == _wallet_name_1 ); + ++_iter; + BOOST_REQUIRE( _iter->name == _wallet_name_2 ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_0 ) == true ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_1 ) == false ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_2 ) == false ); + } + _beekeeper.import_key(_token, _wallet_name_1, _keys[0].private_key, _prefix ); + { + keys_details _public_keys = _beekeeper.get_public_keys( _token, _wallet_name_1 ); + BOOST_REQUIRE( _public_keys.size() == 1 ); + BOOST_REQUIRE( _public_keys.find( public_key_type::from_base58( _keys[0].public_key, false/*is_sha256*/ ) ) != _public_keys.end() ); + } + _beekeeper.import_key(_token, _wallet_name_2, _keys[1].private_key, _prefix ); + _beekeeper.import_key(_token, _wallet_name_2, _keys[2].private_key, _prefix ); + { + keys_details _public_keys = _beekeeper.get_public_keys( _token, _wallet_name_2 ); + BOOST_REQUIRE( _public_keys.size() == 2 ); + BOOST_REQUIRE( _public_keys.find( public_key_type::from_base58( _keys[1].public_key, false/*is_sha256*/ ) ) != _public_keys.end() ); + BOOST_REQUIRE( _public_keys.find( public_key_type::from_base58( _keys[2].public_key, false/*is_sha256*/ ) ) != _public_keys.end() ); + } + _beekeeper.remove_key(_token, _wallet_name_2, public_key_type::from_base58( _keys[1].public_key, false/*is_sha256*/ ) ); + { + keys_details _public_keys = _beekeeper.get_public_keys( _token, _wallet_name_2 ); + BOOST_REQUIRE( _public_keys.size() == 1 ); + BOOST_REQUIRE( _public_keys.find( public_key_type::from_base58( _keys[2].public_key, false/*is_sha256*/ ) ) != _public_keys.end() ); + } + _beekeeper.close( _token, _wallet_name_2 ); + { + flat_set _wallets = _beekeeper.list_created_wallets( _token ); + BOOST_REQUIRE( _wallets.size() == 2 ); + BOOST_REQUIRE( _wallets.begin()->name == _wallet_name_0 ); + BOOST_REQUIRE( _wallets.rbegin()->name == _wallet_name_1 ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_0 ) == true ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_1 ) == false ); + } + _beekeeper.close( _token, _wallet_name_0 ); + { + flat_set _wallets = _beekeeper.list_created_wallets( _token ); + BOOST_REQUIRE( _wallets.size() == 2 ); + BOOST_REQUIRE( _wallets.begin()->name == _wallet_name_0 ); + BOOST_REQUIRE( _wallets.rbegin()->name == _wallet_name_1 ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_0 ) == true ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_1 ) == false ); + } + _beekeeper.close( _token, _wallet_name_1 ); + { + flat_set _wallets = _beekeeper.list_created_wallets( _token ); + BOOST_REQUIRE( _wallets.size() == 1 ); + BOOST_REQUIRE( _wallets.begin()->name == _wallet_name_0 ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_0 ) == true ); + } + + BOOST_REQUIRE_THROW( _beekeeper.open( _token, _wallet_name_1 ), fc::exception ); + + _beekeeper.open( _token, _wallet_name_0 ); + { + flat_set _wallets = _beekeeper.list_created_wallets( _token ); + BOOST_REQUIRE( _wallets.size() == 1 ); + BOOST_REQUIRE( _wallets.begin()->name == _wallet_name_0 ); + BOOST_REQUIRE( b_mgr.exists_wallet( _wallet_name_0 ) == true ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(wallets_synchronization) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + auto _prefix = "STM"; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token_00 = _beekeeper.create_session( "salt" ); + auto _token_01 = _beekeeper.create_session( "salt" ); + + const std::string _wallet_name = "wallet-0"; + + _beekeeper.create( _token_00, _wallet_name, "avocado", false/*is_temporary*/ ); + _beekeeper.unlock( _token_01, _wallet_name, "avocado" ); + + std::vector> _keys = + { + { "5KGKYWMXReJewfj5M29APNMqGEu173DzvHv5TeJAg9SkjUeQV78", "6oR6ckA4TejTWTjatUdbcS98AKETc3rcnQ9dWxmeNiKDzfhBZa" }, + { "5KLytoW1AiGSoHHBA73x1AmgZnN16QDgU1SPpG9Vd2dpdiBgSYw", "8FDsHdPkHbY8fuUkVLyAmrnKMvj6DddLopi3YJ51dVqsG9vZa4" }, + { "5KKvoNaCPtN9vUEU1Zq9epSAVsEPEtocbJsp7pjZndt9Rn4dNRg", "8mmxXz5BfQc2NJfqhiPkbgcyJm4EvWEr2UAUdr56gEWSN9ZnA5" } + }; + + _beekeeper.import_keys( _token_00, _wallet_name, { _keys[0].first }, _prefix ); + { + auto _public_keys = _beekeeper.get_public_keys( _token_00, _wallet_name ); + BOOST_REQUIRE_EQUAL( _public_keys.size(), 1 ); + } + { + auto _public_keys = _beekeeper.get_public_keys( _token_01, _wallet_name ); + BOOST_REQUIRE_EQUAL( _public_keys.size(), 1 ); + } + _beekeeper.import_keys( _token_01, _wallet_name, { _keys[1].first, _keys[2].first }, _prefix ); + { + auto _public_keys = _beekeeper.get_public_keys( _token_00, _wallet_name ); + BOOST_REQUIRE_EQUAL( _public_keys.size(), 3 ); + } + { + auto _public_keys = _beekeeper.get_public_keys( _token_01, _wallet_name ); + BOOST_REQUIRE_EQUAL( _public_keys.size(), 3 ); + } + + _beekeeper.close( _token_00, _wallet_name ); + _beekeeper.unlock( _token_00, _wallet_name, "avocado" ); + + auto _checker = [&]( const std::string& token ) + { + auto _public_keys = _beekeeper.get_public_keys( token, _wallet_name ); + BOOST_REQUIRE_EQUAL( _public_keys.size(), _keys.size() ); + + for( auto& key : _keys ) + BOOST_REQUIRE( _public_keys.find( public_key_type::from_base58( key.second, false/*is_sha256*/ ) ) != _public_keys.end() ); + }; + + _checker( _token_00 ); + _checker( _token_01 ); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(is_wallet_unlocked) +{ + try + { + test_utils::beekeeper_mgr b_mgr; + b_mgr.remove_wallets(); + + const uint64_t _timeout = 90; + const uint32_t _session_limit = 64; + + appbase::application app; + + beekeeper_wallet_manager _beekeeper = b_mgr.create_wallet( app, _timeout, _session_limit ); + BOOST_REQUIRE( _beekeeper.start() ); + + auto _token = _beekeeper.create_session( "salt" ); + + const std::string _wallet_name = "wallet_name"; + const std::string _password = "avocado"; + + { + auto _wallet_info = _beekeeper.is_wallet_unlocked( _token, _wallet_name ); + BOOST_REQUIRE( _wallet_info.unlocked == false ); + } + + _beekeeper.create( _token, _wallet_name, _password, false/*is_temporary*/ ); + + { + auto _wallet_info = _beekeeper.is_wallet_unlocked( _token, _wallet_name ); + BOOST_REQUIRE( _wallet_info.unlocked == true ); + } + + _beekeeper.lock( _token, _wallet_name ); + + { + auto _wallet_info = _beekeeper.is_wallet_unlocked( _token, _wallet_name ); + BOOST_REQUIRE( _wallet_info.unlocked == false ); + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + +#endif -- GitLab From 4ddcfdd94b39b654091bd745504e56ce6d5cdce1 Mon Sep 17 00:00:00 2001 From: Dan Notestein Date: Sat, 20 Dec 2025 22:22:30 -0500 Subject: [PATCH 4/4] Fix thread safety issues in beekeeper time_manager - Change stop_requested from bool to std::atomic to fix data race - Use condition variable instead of sleep_for to enable immediate thread wakeup during destruction - Properly synchronize thread shutdown to prevent race conditions during cleanup This fixes flaky beekeeper_timeout_unlock test failures caused by the thread potentially accessing destroyed objects during shutdown. Cherry-picked from hive commit 51d26ea24 --- .../beekeeper/include/beekeeper/time_manager.hpp | 5 ++++- programs/beekeeper/beekeeper/time_manager.cpp | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/programs/beekeeper/beekeeper/include/beekeeper/time_manager.hpp b/programs/beekeeper/beekeeper/include/beekeeper/time_manager.hpp index 7132d35..ee7bc38 100644 --- a/programs/beekeeper/beekeeper/include/beekeeper/time_manager.hpp +++ b/programs/beekeeper/beekeeper/include/beekeeper/time_manager.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include @@ -10,10 +12,11 @@ class time_manager: public time_manager_base { private: - bool stop_requested = false; + std::atomic stop_requested{false}; std::unique_ptr lock_thread; std::mutex mtx; + std::condition_variable cv; public: diff --git a/programs/beekeeper/beekeeper/time_manager.cpp b/programs/beekeeper/beekeeper/time_manager.cpp index e7a4f56..cac938c 100644 --- a/programs/beekeeper/beekeeper/time_manager.cpp +++ b/programs/beekeeper/beekeeper/time_manager.cpp @@ -8,17 +8,22 @@ time_manager::time_manager() { lock_thread = std::make_unique( [this]() { - while( !stop_requested ) + while( !stop_requested.load() ) { run(); - std::this_thread::sleep_for( std::chrono::milliseconds(200) ); + std::unique_lock lock( mtx ); + cv.wait_for( lock, std::chrono::milliseconds(200), [this]() { return stop_requested.load(); } ); } } ); } time_manager::~time_manager() { - stop_requested = true; + { + std::lock_guard lock( mtx ); + stop_requested.store( true ); + } + cv.notify_one(); lock_thread->join(); } -- GitLab