diff --git a/.vscode/launch.json b/.vscode/launch.json index c4af1ce02..45b3d0075 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -74,6 +74,25 @@ } ] }, + { + "type": "cppdbg", + "request": "launch", + "name": "Migration Sync Test (macOS)", + "program": "${workspaceFolder}/build/OSX/Debug/test_bin/migration_sync_test", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "environment": [], + "externalConsole": false, + "MIMode": "lldb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, { "type": "cppdbg", "request": "launch", diff --git a/cmake/version.cmake b/cmake/version.cmake index d5500fc93..a38a6dcc5 100644 --- a/cmake/version.cmake +++ b/cmake/version.cmake @@ -1,4 +1,4 @@ -set(PROJECT_VERSION 3.6.0) +set(PROJECT_VERSION 3.7.0) if(NOT SGNS_NETWORK STREQUAL "release") add_compile_definitions(DEV_NET) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index c6d154e13..ba9fc59d6 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -4,7 +4,6 @@ add_subdirectory(echo_client) add_subdirectory(crdt_globaldb) add_subdirectory(ipfs_client) add_subdirectory(ipfs_pubsub) -add_subdirectory(account_handling) add_subdirectory(node_test) add_subdirectory(mnn_chunkprocess) add_subdirectory(processing_json) diff --git a/example/account_handling/AccountHandling.cpp b/example/account_handling/AccountHandling.cpp deleted file mode 100644 index 5925ab419..000000000 --- a/example/account_handling/AccountHandling.cpp +++ /dev/null @@ -1,159 +0,0 @@ -/** - * @file AccountHandling.cpp - * @brief - * @date 2024-03-12 - * @author Henrique A. Klein (hklein@gnus.ai) - */ -#include -#include -#include -#include - -#include -#include -#include -#include "account/TransactionManager.hpp" -#include "AccountHelper.hpp" - -using namespace boost::multiprecision; - -std::vector wallet_addr{ "0x4E8794BE4831C45D0699865028C8BE23D608C19C1E24371E3089614A50514262", - "0x06DDC80283462181C02917CC3E99C7BC4BDB2856E19A392300A62DBA6262212C" }; - -using GossipPubSub = sgns::ipfs_pubsub::GossipPubSub; - -std::mutex keyboard_mutex; -std::condition_variable cv; -std::queue events; - -void keyboard_input_thread() -{ - std::string line; - while ( std::getline( std::cin, line ) ) - { - { - std::lock_guard lock( keyboard_mutex ); - events.push( line ); - } - cv.notify_one(); - } -} - -void CreateTransferTransaction( const std::vector &args, sgns::TransactionManager &transaction_manager ) -{ - if ( args.size() != 3 ) - { - std::cerr << "Invalid transfer command format.\n"; - return; - } - uint64_t amount = std::stoull( args[1] ); - if ( !transaction_manager.TransferFunds( amount, { args[2] }, sgns::TokenID::FromBytes( { 0x00 } ) ) ) - { - std::cout << "Insufficient funds.\n"; - } -} - -void CreateProcessingTransaction( const std::vector &args, sgns::TransactionManager &transaction_manager ) -{ - if ( args.size() != 2 ) - { - std::cerr << "Invalid process command format.\n"; - return; - } - - //TODO - Create processing transaction -} - -void MintTokens( const std::vector &args, sgns::TransactionManager &transaction_manager ) -{ - if ( args.size() != 2 ) - { - std::cerr << "Invalid process command format.\n"; - return; - } - transaction_manager.MintFunds( std::stoull( args[1] ), "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); -} - -void PrintAccountInfo( const std::vector &args, sgns::TransactionManager &transaction_manager ) -{ - if ( args.size() != 1 ) - { - std::cerr << "Invalid info command format.\n"; - return; - } - transaction_manager.PrintAccountInfo(); - - //TODO - Create processing transaction -} - -std::vector split_string( const std::string &str ) -{ - std::istringstream iss( str ); - std::vector results( ( std::istream_iterator( iss ) ), - std::istream_iterator() ); - return results; -} - -void process_events( sgns::TransactionManager &transaction_manager ) -{ - std::unique_lock lock( keyboard_mutex ); - cv.wait( lock, [] { return !events.empty(); } ); - - while ( !events.empty() ) - { - std::string event = events.front(); - events.pop(); - - auto arguments = split_string( event ); - if ( arguments.size() == 0 ) - { - return; - } - if ( arguments[0] == "transfer" ) - { - CreateTransferTransaction( arguments, transaction_manager ); - } - else if ( arguments[0] == "process" ) - { - CreateProcessingTransaction( arguments, transaction_manager ); - } - else if ( arguments[0] == "info" ) - { - PrintAccountInfo( arguments, transaction_manager ); - } - else if ( arguments[0] == "mint" ) - { - MintTokens( arguments, transaction_manager ); - } - else - { - std::cerr << "Unknown command: " << arguments[0] << "\n"; - } - } -} - -int main( int argc, char *argv[] ) -{ - std::thread input_thread( keyboard_input_thread ); - - size_t serviceindex = std::strtoul( argv[1], nullptr, 10 ); - std::string own_wallet_address( argv[2] ); - - AccountKey2 key; - DevConfig_st2 local_config{ "0xbeefbeef", "0.65" }; - - strncpy( key, argv[2], sizeof( key ) ); - - sgns::AccountHelper helper( key, local_config, "deadbeef" ); - - while ( true ) - { - process_events( *( helper.GetManager() ) ); - } - - if ( input_thread.joinable() ) - { - input_thread.join(); - } - return 0; -} diff --git a/example/account_handling/AccountHelper.cpp b/example/account_handling/AccountHelper.cpp deleted file mode 100644 index 9f13202ed..000000000 --- a/example/account_handling/AccountHelper.cpp +++ /dev/null @@ -1,180 +0,0 @@ -/** - * @file AccountHelper.cpp - * @brief - * @date 2024-05-15 - * @author Henrique A. Klein (hklein@gnus.ai) - */ -#include "AccountHelper.hpp" - -#include - -#include -#include -#include -#include -#include "account/TokenID.hpp" -#include "local_secure_storage/impl/json/JSONSecureStorage.hpp" - -extern AccountKey2 ACCOUNT_KEY; -extern DevConfig_st2 DEV_CONFIG; - -static const std::string logger_config( R"( - # ---------------- - sinks: - - name: console - type: console - color: true - groups: - - name: SuperGeniusDemo - sink: console - level: error - children: - - name: libp2p - - name: Gossip - # ---------------- - )" ); - -namespace sgns -{ - AccountHelper::AccountHelper( const AccountKey2 &priv_key_data, - const DevConfig_st2 &dev_config, - const char *eth_private_key ) : - account_( GeniusAccount::New( sgns::TokenID::FromBytes( { 0x00 } ), - eth_private_key, - boost::filesystem::path( "." ) ) ), - utxo_manager_( - true, - account_->GetAddress(), - [this]( const std::vector &data ) { return this->account_->Sign( data ); }, - []( const std::string &address, const std::vector &signature, const std::vector &data ) - { - return GeniusAccount::VerifySignature( address, - std::string( signature.begin(), signature.end() ), - data ); - } ), - io_( std::make_shared() ), - dev_config_( dev_config ) - { - logging_system = std::make_shared( std::make_shared( - // Original LibP2P logging config - std::make_shared(), - // Additional logging config for application - logger_config ) ); - logging_system->configure(); - libp2p::log::setLoggingSystem( logging_system ); - libp2p::log::setLevelOfGroup( "SuperGNUSNode", soralog::Level::ERROR_ ); - - auto loggerGlobalDB = base::createLogger( "GlobalDB" ); - loggerGlobalDB->set_level( spdlog::level::debug ); - - auto loggerDAGSyncer = base::createLogger( "GraphsyncDAGSyncer" ); - loggerDAGSyncer->set_level( spdlog::level::debug ); - - auto logkad = sgns::base::createLogger( "Kademlia" ); - logkad->set_level( spdlog::level::trace ); - - auto logNoise = sgns::base::createLogger( "Noise" ); - logNoise->set_level( spdlog::level::trace ); - - auto pubsubKeyPath = ( boost::format( "SuperGNUSNode.TestNet.%s/pubs_processor" ) % account_->GetAddress() ) - .str(); - - pubsub_ = std::make_shared( - crdt::KeyPairFileStorage( pubsubKeyPath ).GetKeyPair().value() ); - pubsub_->Start( 40001, {} ); - - auto scheduler = std::make_shared( std::make_shared( io_ ), libp2p::basic::Scheduler::Config{ std::chrono::milliseconds( 100 ) } ); - auto graphsyncnetwork = std::make_shared( pubsub_->GetHost(), - scheduler ); - auto generator = std::make_shared(); - - auto globaldc_ret = crdt::GlobalDB::New( - io_, - ( boost::format( "SuperGNUSNode.TestNet.%s" ) % account_->GetAddress() ).str(), - pubsub_, - crdt::CrdtOptions::DefaultOptions(), - graphsyncnetwork, - scheduler, - generator ); - - if ( globaldc_ret.has_error() ) - { - throw std::runtime_error( globaldc_ret.error().message() ); - } - - globaldb_ = std::move( globaldc_ret.value() ); - - account_->InitMessenger( pubsub_ ); - - globaldb_->AddListenTopic( std::string( PROCESSING_CHANNEL ) ); - globaldb_->AddBroadcastTopic( std::string( PROCESSING_CHANNEL ) ); - globaldb_->Start(); - - base::Buffer root_hash; - root_hash.put( std::vector( 32ul, 1 ) ); - hasher_ = std::make_shared(); - - header_repo_ = std::make_shared( - globaldb_, - hasher_, - ( boost::format( std::string( db_path_ ) ) % TEST_NET ).str() ); - auto maybe_block_storage = blockchain::KeyValueBlockStorage::create( root_hash, - globaldb_, - hasher_, - header_repo_, - []( auto & ) {} ); - - if ( !maybe_block_storage ) - { - std::cout << "Error initializing blockchain" << std::endl; - throw std::runtime_error( "Error initializing blockchain" ); - } - block_storage_ = std::move( maybe_block_storage.value() ); - transaction_manager_ = TransactionManager::New( globaldb_, io_, account_, hasher_ ); - transaction_manager_->Start(); - - // Encode the string to UTF-8 bytes - std::string temp = std::string( PROCESSING_CHANNEL ); - std::vector inputBytes( temp.begin(), temp.end() ); - - // Compute the SHA-256 hash of the input bytes - std::vector hash( SHA256_DIGEST_LENGTH ); - SHA256( inputBytes.data(), inputBytes.size(), hash.data() ); - //Provide CID - auto key = libp2p::multi::ContentIdentifierCodec::encodeCIDV0( hash.data(), hash.size() ); - pubsub_->GetDHT()->Start(); - pubsub_->GetDHT()->ProvideCID( key, true ); - - auto cidtest = libp2p::multi::ContentIdentifierCodec::decode( key ); - - auto cidstring = libp2p::multi::ContentIdentifierCodec::toString( cidtest.value() ); - std::cout << "CID Test::" << cidstring.value() << '\n'; - - //Also Find providers - pubsub_->StartFindingPeers( key ); - - io_thread = std::thread( [this]() { io_->run(); } ); - } - - AccountHelper::~AccountHelper() - { - if ( io_ ) - { - io_->stop(); - } - if ( pubsub_ ) - { - pubsub_->Stop(); - } - if ( io_thread.joinable() ) - { - io_thread.join(); - } - } - - std::shared_ptr AccountHelper::GetManager() - { - return transaction_manager_; - } - -} diff --git a/example/account_handling/AccountHelper.hpp b/example/account_handling/AccountHelper.hpp deleted file mode 100644 index e0a743513..000000000 --- a/example/account_handling/AccountHelper.hpp +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @file AccountHelper.hpp - * @brief - * @date 2024-05-15 - * @author Henrique A. Klein (hklein@gnus.ai) - */ - -#ifndef _ACCOUNT_HELPER_HPP_ -#define _ACCOUNT_HELPER_HPP_ - -#include -#include -#include -#include -#include "account/GeniusAccount.hpp" -#include "ipfs_pubsub/gossip_pubsub.hpp" -#include "crdt/globaldb/globaldb.hpp" -#include "crdt/globaldb/keypair_file_storage.hpp" -#include "crdt/globaldb/proto/broadcast.pb.h" -#include "account/TransactionManager.hpp" -#include -#include -#include -#include -#include -#include "crypto/hasher/hasher_impl.hpp" -#include "blockchain/impl/key_value_block_header_repository.hpp" -#include "blockchain/impl/key_value_block_storage.hpp" -#include "singleton/IComponent.hpp" -#include "processing/impl/processing_task_queue_impl.hpp" -#include "processing/impl/processing_core_impl.hpp" -#include "processing/processing_service.hpp" -#include - -typedef struct DevConfig -{ - char Addr[255]; - std::string Cut; -} DevConfig_st2; - -typedef char AccountKey2[255]; - -namespace sgns -{ - - class AccountHelper : public IComponent - { - public: - AccountHelper( const AccountKey2 &priv_key_data, const DevConfig_st2 &dev_config, const char *eth_private_key ); - - ~AccountHelper() override; - - std::string GetName() override - { - return "AccountHelper"; - } - - std::shared_ptr GetManager(); - - private: - std::shared_ptr account_; - UTXOManager utxo_manager_; - std::shared_ptr pubsub_; - std::shared_ptr io_; - std::shared_ptr globaldb_; - std::shared_ptr hasher_; - std::shared_ptr header_repo_; - std::shared_ptr block_storage_; - std::shared_ptr transaction_manager_; - std::shared_ptr task_queue_; - std::shared_ptr processing_core_; - std::shared_ptr processing_service_; - std::shared_ptr logging_system; - - std::thread io_thread; - - DevConfig_st2 dev_config_; - - static constexpr std::string_view db_path_ = "bc-%d/"; - static constexpr std::uint16_t MAIN_NET = 369; - static constexpr std::uint16_t TEST_NET = 963; - static constexpr std::size_t MAX_NODES_COUNT = 1; - static constexpr std::string_view PROCESSING_GRID_CHANNEL = "GRID_CHANNEL_ID"; - static constexpr std::string_view PROCESSING_CHANNEL = "SGNUS.TestNet.Channel"; - }; -} -#endif diff --git a/example/account_handling/CMakeLists.txt b/example/account_handling/CMakeLists.txt deleted file mode 100644 index 7c402d5f4..000000000 --- a/example/account_handling/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -add_executable(account_handling - AccountHandling.cpp - AccountHelper.cpp -) -set_target_properties(account_handling PROPERTIES UNITY_BUILD ON) - -include_directories( - ${PROJECT_SOURCE_DIR}/src -) - -target_include_directories(account_handling PRIVATE ${GSL_INCLUDE_DIR} ${TrustWalletCore_INCLUDE_DIR}) -target_link_libraries(account_handling PRIVATE - genius_node - blockchain_common - block_header_repository - block_storage - logger - crdt_globaldb - p2p::p2p_basic_host - p2p::p2p_default_network - p2p::p2p_peer_repository - p2p::p2p_inmem_address_repository - p2p::p2p_inmem_key_repository - p2p::p2p_inmem_protocol_repository - p2p::p2p_literals - p2p::p2p_kademlia - p2p::p2p_identify - p2p::p2p_ping - Boost::Boost.DI - Boost::program_options - ipfs-bitswap-cpp - rapidjson - ${WIN_CRYPT_LIBRARY} -) diff --git a/src/account/CMakeLists.txt b/src/account/CMakeLists.txt index 406b17d54..ae8c8187b 100644 --- a/src/account/CMakeLists.txt +++ b/src/account/CMakeLists.txt @@ -8,6 +8,7 @@ endif() add_library(sgns_genius_account UTXOStructs.cpp UTXOManager.cpp + MigrationAllowList.cpp GeniusAccount.cpp AccountMessenger.cpp ) @@ -57,9 +58,10 @@ add_library(genius_node TransferTransaction.cpp MintTransaction.cpp MintTransactionV2.cpp + MigrationTransaction.cpp ProcessingTransaction.cpp EscrowTransaction.cpp - EscrowReleaseTransaction.cpp + InputValidators.cpp TransactionManager.cpp TokenAmount.cpp MigrationManager.cpp @@ -67,6 +69,7 @@ add_library(genius_node Migration1_0_0To3_4_0.cpp Migration3_4_0To3_5_0.cpp Migration3_5_0To3_6_0.cpp + Migration3_6_0To3_7_0.cpp GeniusNode.cpp ) diff --git a/src/account/EscrowReleaseTransaction.cpp b/src/account/EscrowReleaseTransaction.cpp deleted file mode 100644 index 28884feba..000000000 --- a/src/account/EscrowReleaseTransaction.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @file EscrowReleaseTransaction.cpp - * @brief Implementation of the EscrowReleaseTransaction class. - */ - -#include "account/EscrowReleaseTransaction.hpp" -#include "account/proto/SGTransaction.pb.h" -#include "crypto/hasher/hasher_impl.hpp" -#include "base/blob.hpp" -#include - -namespace sgns -{ - - EscrowReleaseTransaction::EscrowReleaseTransaction( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ) : - IGeniusTransactions( "escrow-release", SetDAGWithType( std::move( dag ), "escrow-release" ) ), - utxo_params_( std::move( params ) ), - release_amount_( release_amount ), - release_address_( std::move( release_address ) ), - escrow_source_( std::move( escrow_source ) ), - original_escrow_hash_( std::move( original_escrow_hash ) ) - { - } - - EscrowReleaseTransaction EscrowReleaseTransaction::New( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ) - { - EscrowReleaseTransaction instance( std::move( params ), - release_amount, - std::move( release_address ), - std::move( escrow_source ), - std::move( original_escrow_hash ), - std::move( dag ) ); - instance.FillHash(); - return instance; - } - - std::vector EscrowReleaseTransaction::SerializeByteVector() - { - SGTransaction::EscrowReleaseTx tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( this->dag_st ); - auto *utxo_proto_params = tx_struct.mutable_utxo_params(); - for ( const auto &[txid_hash_, output_idx_, signature_] : utxo_params_.first ) - { - auto *input_proto = utxo_proto_params->add_inputs(); - input_proto->set_tx_id_hash( txid_hash_.toReadableString() ); - input_proto->set_output_index( output_idx_ ); - input_proto->set_signature( signature_.data(), signature_.size() ); - } - for ( const auto &[encrypted_amount, dest_address, token_id] : utxo_params_.second ) - { - auto *out_proto = utxo_proto_params->add_outputs(); - out_proto->set_encrypted_amount( encrypted_amount ); - out_proto->set_dest_addr( dest_address ); - out_proto->set_token_id( token_id.bytes().data(), token_id.size() ); - } - tx_struct.set_release_amount( release_amount_ ); - tx_struct.set_release_address( release_address_ ); - tx_struct.set_escrow_source( escrow_source_ ); - tx_struct.set_original_escrow_hash( original_escrow_hash_ ); - size_t size = tx_struct.ByteSizeLong(); - std::vector serialized_proto( size ); - if ( !tx_struct.SerializeToArray( serialized_proto.data(), static_cast( size ) ) ) - { - std::cerr << "Failed to serialize transaction\n"; - } - return serialized_proto; - } - - std::shared_ptr EscrowReleaseTransaction::DeSerializeByteVector( - const std::vector &data ) - { - SGTransaction::EscrowReleaseTx tx_struct; - if ( !tx_struct.ParseFromArray( data.data(), static_cast( data.size() ) ) ) - { - std::cerr << "Failed to parse EscrowReleaseTx from array.\n"; - return nullptr; - } - std::vector inputs; - auto *utxo_proto_params = tx_struct.mutable_utxo_params(); - for ( int i = 0; i < utxo_proto_params->inputs_size(); ++i ) - { - const auto &input_proto = utxo_proto_params->inputs( i ); - InputUTXOInfo curr; - auto maybe_hash = base::Hash256::fromReadableString( input_proto.tx_id_hash() ); - if ( !maybe_hash ) - { - std::cerr << "Invalid hash in input\n"; - return nullptr; - } - curr.txid_hash_ = maybe_hash.value(); - curr.output_idx_ = input_proto.output_index(); - curr.signature_ = std::vector( input_proto.signature().cbegin(), input_proto.signature().cend() ); - inputs.push_back( curr ); - } - std::vector outputs; - for ( int i = 0; i < utxo_proto_params->outputs_size(); ++i ) - { - const auto &out_proto = utxo_proto_params->outputs( i ); - outputs.push_back( { out_proto.encrypted_amount(), - out_proto.dest_addr(), - TokenID::FromBytes( out_proto.token_id().data(), out_proto.token_id().size() ) } ); - } - UTXOTxParameters utxo_params( inputs, outputs ); - auto releaseTx = std::make_shared( - EscrowReleaseTransaction( std::move( utxo_params ), - tx_struct.release_amount(), - tx_struct.release_address(), - tx_struct.escrow_source(), - tx_struct.original_escrow_hash(), - tx_struct.dag_struct() ) ); - return releaseTx; - } - - UTXOTxParameters EscrowReleaseTransaction::GetUTXOParameters() const - { - return utxo_params_; - } - - bool EscrowReleaseTransaction::HasUTXOParameters() const - { - return true; - } - - std::optional EscrowReleaseTransaction::GetUTXOParametersOpt() const - { - return utxo_params_; - } - - uint64_t EscrowReleaseTransaction::GetReleaseAmount() const - { - return release_amount_; - } - - std::string EscrowReleaseTransaction::GetReleaseAddress() const - { - return release_address_; - } - - std::string EscrowReleaseTransaction::GetEscrowSource() const - { - return escrow_source_; - } - - std::string EscrowReleaseTransaction::GetOriginalEscrowHash() const - { - return original_escrow_hash_; - } - - std::string EscrowReleaseTransaction::GetTransactionSpecificPath() const - { - return GetType(); - } - - std::unordered_set EscrowReleaseTransaction::GetTopics() const - { - auto topics = IGeniusTransactions::GetTopics(); - topics.emplace( GetEscrowSource() ); - return topics; - } -} diff --git a/src/account/EscrowReleaseTransaction.hpp b/src/account/EscrowReleaseTransaction.hpp deleted file mode 100644 index a8b9fe28a..000000000 --- a/src/account/EscrowReleaseTransaction.hpp +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @file EscrowReleaseTransaction.hpp - * @brief Declaration of the EscrowReleaseTransaction class. - */ - -#ifndef _ESCROW_RELEASE_TRANSACTION_HPP_ -#define _ESCROW_RELEASE_TRANSACTION_HPP_ - -#include "account/IGeniusTransactions.hpp" -#include "account/UTXOStructs.hpp" - -namespace sgns -{ - /** - * @brief Represents a transaction used to release funds from an escrow. - * - * This transaction holds the UTXO parameters, the amount to be released, the release address, - * the escrow source, and the original escrow hash. - */ - class EscrowReleaseTransaction final : public IGeniusTransactions - { - public: - /** - * @brief Creates a new EscrowReleaseTransaction. - * - * @param params UTXO transaction parameters. - * @param release_amount Amount to be released. - * @param release_address Address where funds will be sent. - * @param escrow_source Source of the escrow. - * @param original_escrow_hash Original hash of the escrow transaction. - * @param dag DAG structure containing transaction metadata. - * @return An instance of EscrowReleaseTransaction. - */ - static EscrowReleaseTransaction New( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ); - - /** - * @brief Deserializes a byte vector into an EscrowReleaseTransaction. - * - * @param data Serialized transaction data. - * @return A shared pointer to an EscrowReleaseTransaction instance. - */ - static std::shared_ptr DeSerializeByteVector( const std::vector &data ); - - /** - * @brief Destructor. - */ - ~EscrowReleaseTransaction() override = default; - - /** - * @brief Serializes the transaction to a byte vector. - * - * @return A vector of bytes representing the serialized transaction. - */ - std::vector SerializeByteVector() override; - - /** - * @brief Gets the UTXO parameters. - * - * @return The UTXO parameters of the transaction. - */ - UTXOTxParameters GetUTXOParameters() const; - - /** - * @brief Returns if transaction supports UTXOs - * @return True if supported, false otherwise - */ - bool HasUTXOParameters() const override; - - /** - * @brief Returns the UTXOs - * @return If exists, returns the UTXOs of the transaction - */ - std::optional GetUTXOParametersOpt() const override; - - /** - * @brief Gets the release amount. - * - * @return The amount to be released. - */ - uint64_t GetReleaseAmount() const; - - /** - * @brief Gets the release address. - * - * @return The address where funds will be released. - */ - std::string GetReleaseAddress() const; - - /** - * @brief Gets the escrow source. - * - * @return The source address of the escrow. - */ - std::string GetEscrowSource() const; - - /** - * @brief Gets the original escrow hash. - * - * @return The original hash of the escrow transaction. - */ - std::string GetOriginalEscrowHash() const; - - /** - * @brief Gets the transaction-specific path. - * - * @return A string representing the transaction path. - */ - std::string GetTransactionSpecificPath() const override; - - std::unordered_set GetTopics() const override; - - private: - /** - * @brief Private constructor. - * - * @param params UTXO transaction parameters. - * @param release_amount Amount to be released. - * @param release_address Address where funds will be sent. - * @param original_escrow_hash Original hash of the escrow transaction. - * @param escrow_source Source address of the escrow. - * @param dag DAG structure containing transaction metadata. - */ - EscrowReleaseTransaction( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ); - - UTXOTxParameters utxo_params_; ///< UTXO parameters for the transaction. - uint64_t release_amount_; ///< Amount to be released. - std::string release_address_; ///< Address to which funds will be released. - std::string escrow_source_; ///< Source address of the escrow. - std::string original_escrow_hash_; ///< Original hash of the escrow transaction. - - /** - * @brief Registers the deserializer for the escrow release transaction. - * - * @return True if registration is successful. - */ - static bool Register() - { - RegisterDeserializer( "escrow-release", &EscrowReleaseTransaction::DeSerializeByteVector ); - return true; - } - - static inline bool registered_ = Register(); ///< Ensures deserializer registration. - }; -} - -#endif // _ESCROW_RELEASE_TRANSACTION_HPP_ diff --git a/src/account/EscrowTransaction.cpp b/src/account/EscrowTransaction.cpp index d51e0366f..027b7f463 100644 --- a/src/account/EscrowTransaction.cpp +++ b/src/account/EscrowTransaction.cpp @@ -37,10 +37,10 @@ namespace sgns return instance; } - std::vector EscrowTransaction::SerializeByteVector() + std::vector EscrowTransaction::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const { SGTransaction::EscrowTx tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( this->dag_st ); + tx_struct.mutable_dag_struct()->CopyFrom( dag ); SGTransaction::UTXOTxParams *utxo_proto_params = tx_struct.mutable_utxo_params(); for ( const auto &[txid_hash_, output_idx_, signature_] : utxo_params_.first ) diff --git a/src/account/EscrowTransaction.hpp b/src/account/EscrowTransaction.hpp index 38652f79e..ba7c8e4c8 100644 --- a/src/account/EscrowTransaction.hpp +++ b/src/account/EscrowTransaction.hpp @@ -1,6 +1,6 @@ /** * @file EscrowTransaction.hpp - * @brief + * @brief Transaction type used to lock UTXO funds into an escrow address. * @date 2024-04-24 * @author Henrique A. Klein (hklein@gnus.ai) */ @@ -13,35 +13,74 @@ namespace sgns { + /** + * @brief Transaction that reserves funds for a job escrow while tracking peer payout metadata. + */ class EscrowTransaction : public IGeniusTransactions { public: + /** + * @brief Creates a new escrow-hold transaction from signed UTXO parameters. + * @param[in] params Signed UTXO inputs and escrow/change outputs for the hold. + * @param[in] amount Total amount locked in escrow. + * @param[in] dev_addr Developer payout address that receives the post-peer remainder. + * @param[in] peers_cut Per-peer payout multiplier used when releasing escrow. + * @param[in] dag DAG metadata shared by all transaction types. + * @return Escrow transaction with transaction type set and hash populated. + */ static EscrowTransaction New( UTXOTxParameters params, uint64_t amount, std::string dev_addr, uint64_t peers_cut, SGTransaction::DAGStruct dag ); + /** + * @brief Deserializes a serialized escrow transaction. + * @param[in] data Serialized @c SGTransaction::EscrowTx bytes. + * @return Shared pointer to the parsed escrow transaction, or nullptr if parsing fails. + */ static std::shared_ptr DeSerializeByteVector( const std::vector &data ); + /** + * @brief Destroys the escrow transaction. + */ ~EscrowTransaction() override = default; - std::vector SerializeByteVector() override; - uint64_t GetNumChunks() const; + using IGeniusTransactions::SerializeByteVector; + /** + * @brief Serializes the escrow transaction payload and DAG metadata. + * @param[in] dag DAG metadata to serialize into the transaction payload. + * @return Serialized @c SGTransaction::EscrowTx bytes. + */ + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; + + /** + * @brief Returns the number of chunks reserved for peer payouts. + * @return Number of chunks associated with this escrow transaction. + */ + uint64_t GetNumChunks() const; + /** + * @brief Returns the transaction-specific storage path component. + * @return Transaction type string used as the path component. + */ std::string GetTransactionSpecificPath() const override { return GetType(); } + /** + * @brief Returns the escrow transaction UTXO inputs and outputs. + * @return UTXO parameters carried by this escrow hold. + */ UTXOTxParameters GetUTXOParameters() const { return utxo_params_; } /** - * @brief Returns if transaction supports UTXOs - * @return True if supported, false otherwise + * @brief Returns whether the transaction supports UTXO parameters. + * @return Always true for escrow-hold transactions. */ bool HasUTXOParameters() const override { @@ -49,44 +88,64 @@ namespace sgns } /** - * @brief Returns the UTXOs - * @return If exists, returns the UTXOs of the transaction + * @brief Returns the escrow transaction UTXO parameters. + * @return UTXO parameters carried by this escrow hold. */ std::optional GetUTXOParametersOpt() const override { return utxo_params_; } + /** + * @brief Returns the developer payout address. + * @return Address that receives escrow remainder after peer payouts. + */ std::string GetDevAddress() const { return dev_addr_; } + /** + * @brief Returns the total amount locked in escrow. + * @return Total amount locked by this escrow hold. + */ uint64_t GetAmount() const { return amount_; } + /** + * @brief Returns the configured peer-share multiplier. + * @return Peer payout multiplier applied during escrow release. + */ uint64_t GetPeersCut() const { return peers_cut_; } private: + /** + * @brief Constructs an escrow-hold transaction from its payload and DAG metadata. + * @param[in] params Signed UTXO inputs and escrow/change outputs for the hold. + * @param[in] amount Total amount locked in escrow. + * @param[in] dev_addr Developer payout address that receives the post-peer remainder. + * @param[in] peers_cut Per-peer payout multiplier used when releasing escrow. + * @param[in] dag DAG metadata shared by all transaction types. + */ EscrowTransaction( UTXOTxParameters params, uint64_t amount, std::string dev_addr, uint64_t peers_cut, SGTransaction::DAGStruct dag ); - UTXOTxParameters utxo_params_; - uint64_t amount_; - std::string dev_addr_; - uint64_t peers_cut_; + UTXOTxParameters utxo_params_; ///< Signed inputs and outputs for the escrow hold. + uint64_t amount_; ///< Total amount locked in escrow. + std::string dev_addr_; ///< Developer payout address for escrow remainder. + uint64_t peers_cut_; ///< Peer payout multiplier used during escrow release. /** - * @brief Registers the deserializer for the transfer transaction type. - * @return A boolean indicating successful registration. + * @brief Registers the deserializer for the escrow-hold transaction type. + * @return True when registration completes. */ static bool Register() { @@ -94,8 +153,8 @@ namespace sgns return true; } - /** - * @brief Static variable to ensure registration happens on inclusion of header file. + /** + * @brief Forces static initialization of the escrow-hold transaction deserializer. */ static inline bool registered = Register(); }; diff --git a/src/account/GeniusAccount.cpp b/src/account/GeniusAccount.cpp index 201566619..f49eeacdb 100644 --- a/src/account/GeniusAccount.cpp +++ b/src/account/GeniusAccount.cpp @@ -11,9 +11,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -157,6 +159,24 @@ namespace return WritePublicKeysToFile( file_path, existing_keys ); } + bool TryParseUint64( std::string_view input, uint64_t &value, bool allow_suffix = false ) + { + if ( input.empty() ) + { + return false; + } + + const auto *begin = input.data(); + const auto *end = input.data() + input.size(); + auto [ptr, ec] = std::from_chars( begin, end, value ); + if ( ec != std::errc() || ptr == begin ) + { + return false; + } + + return allow_suffix || ptr == end; + } + outcome::result> MigrateSecureStorage( const boost::filesystem::path &base_path ) { JSONSecureStorage json_storage( base_path.generic_string() ); @@ -785,14 +805,24 @@ namespace sgns return signed_vector; } - void GeniusAccount::SetLocalConfirmedNonce( uint64_t nonce ) + std::vector GeniusAccount::CreateInputsFromUTXOs( const std::vector &utxos ) const { - genius_account_logger()->debug( "Setting local confirmed nonce to {}", nonce ); - SetPeerConfirmedNonce( nonce, eth_keypair_->GetEntirePubValue() ); - std::lock_guard lock( nonce_mutex_ ); + std::vector inputs; + inputs.reserve( utxos.size() ); + + for ( const auto &utxo : utxos ) + { + InputUTXOInfo input; + input.txid_hash_ = utxo.GetTxID(); + input.output_idx_ = utxo.GetOutputIdx(); + input.signature_ = Sign( input.SerializeForSigning() ); + inputs.emplace_back( std::move( input ) ); + } + + return inputs; } - void GeniusAccount::SetPeerConfirmedNonce( uint64_t nonce, const std::string &address ) + void GeniusAccount::SetPeerConfirmedNonce( uint64_t nonce, const std::string &address, const std::string &tx_hash ) { std::unique_lock lock( nonce_mutex_ ); auto current_confirmed_nonce = confirmed_nonces_[address]; @@ -810,6 +840,10 @@ namespace sgns { local_confirmed_nonce_ = updated_nonce; } + if ( !tx_hash.empty() ) + { + UpdateLocalConfirmedTxHistoryLocked( updated_nonce, tx_hash ); + } auto it = pending_nonces_.begin(); while ( it != pending_nonces_.end() && ( !local_confirmed_nonce_ || *it <= local_confirmed_nonce_.value() ) ) @@ -824,9 +858,10 @@ namespace sgns void GeniusAccount::RollBackPeerConfirmedNonce( uint64_t nonce, const std::string &address ) { - std::lock_guard lock( nonce_mutex_ ); - auto it = confirmed_nonces_.find( address ); - uint64_t current_confirmed_nonce = 0; + std::unique_lock lock( nonce_mutex_ ); + auto it = confirmed_nonces_.find( address ); + uint64_t current_confirmed_nonce = 0; + bool should_persist = false; if ( it != confirmed_nonces_.end() ) { current_confirmed_nonce = it->second; @@ -845,6 +880,7 @@ namespace sgns { confirmed_nonces_.erase( it ); } + should_persist = true; } if ( address == eth_keypair_->GetEntirePubValue() ) @@ -860,7 +896,20 @@ namespace sgns local_confirmed_nonce_.reset(); } } + RollbackLocalConfirmedTxHistoryLocked( nonce ); pending_nonces_.erase( nonce ); + should_persist = true; + } + + const auto final_it = confirmed_nonces_.find( address ); + const uint64_t persisted_nonce = address == eth_keypair_->GetEntirePubValue() + ? local_confirmed_nonce_.value_or( 0 ) + : ( final_it != confirmed_nonces_.end() ? final_it->second : 0 ); + + lock.unlock(); + if ( should_persist ) + { + PersistConfirmedNonce( address, persisted_nonce ); } } @@ -914,6 +963,20 @@ namespace sgns return GetPeerNonce( eth_keypair_->GetEntirePubValue() ); } + outcome::result GeniusAccount::GetLocalConfirmedTxHash( uint64_t nonce ) const + { + std::shared_lock lock( nonce_mutex_ ); + for ( auto it = local_confirmed_transactions_.rbegin(); it != local_confirmed_transactions_.rend(); ++it ) + { + if ( it->nonce == nonce ) + { + return it->hash; + } + } + + return outcome::failure( std::errc::no_message_available ); + } + outcome::result> GeniusAccount::FetchNetworkNonce( uint64_t timeout_ms ) const { if ( !messenger_ ) @@ -1186,6 +1249,8 @@ namespace sgns return; } + const auto self_address = eth_keypair_->GetEntirePubValue(); + base::Buffer prefix; prefix.put( std::string( NONCE_KEY_PREFIX ) ); auto query_res = nonce_db_->query( prefix ); @@ -1194,41 +1259,58 @@ namespace sgns return; } - std::unordered_map loaded; - uint64_t max_local = 0; - bool has_local = false; - + std::unordered_map loaded_nonces; for ( const auto &[key_buf, val_buf] : query_res.value() ) { const auto key = std::string( key_buf.toString() ); - if ( key.rfind( std::string( NONCE_KEY_PREFIX ) ) != 0 ) { continue; } - auto address = key.substr( std::string( NONCE_KEY_PREFIX ).size() ); - try + const auto address = key.substr( std::string( NONCE_KEY_PREFIX ).size() ); + uint64_t parsed_nonce = 0; + if ( TryParseUint64( val_buf.toString(), parsed_nonce, true ) ) { - uint64_t nonce = std::stoull( std::string( val_buf.toString() ) ); - loaded[address] = nonce; + loaded_nonces[address] = parsed_nonce; } - catch ( const std::exception & ) + else { genius_account_logger()->error( "Failed to parse nonce for {:.8}: ", address, val_buf.toString() ); } } - std::lock_guard lock( nonce_mutex_ ); - for ( const auto &[address, nonce] : loaded ) + std::optional local_nonce; + if ( auto it = loaded_nonces.find( self_address ); it != loaded_nonces.end() ) { - confirmed_nonces_[address] = nonce; + local_nonce = it->second; } - if ( has_local ) + std::deque local_history; + base::Buffer local_history_key; + local_history_key.put( std::string( LOCAL_CONFIRMED_TX_HISTORY_KEY_PREFIX ) + self_address ); + if ( auto local_history_res = nonce_db_->get( local_history_key ); local_history_res.has_value() ) { - local_confirmed_nonce_ = max_local; + local_history = DeserializeConfirmedTxHistory( std::string( local_history_res.value().toString() ) ); } + + std::lock_guard lock( nonce_mutex_ ); + confirmed_nonces_ = std::move( loaded_nonces ); + if ( local_nonce.has_value() ) + { + confirmed_nonces_[self_address] = local_nonce.value(); + local_confirmed_nonce_ = local_nonce.value(); + } + else if ( !local_history.empty() ) + { + confirmed_nonces_[self_address] = local_history.back().nonce; + local_confirmed_nonce_ = local_history.back().nonce; + } + else + { + local_confirmed_nonce_.reset(); + } + local_confirmed_transactions_ = std::move( local_history ); } void GeniusAccount::PersistConfirmedNonce( const std::string &address, uint64_t nonce ) @@ -1238,16 +1320,118 @@ namespace sgns return; } - base::Buffer key; - - key.put( std::string( NONCE_KEY_PREFIX ) + address ); + base::Buffer nonce_key; + nonce_key.put( std::string( NONCE_KEY_PREFIX ) + address ); - base::Buffer value; - value.put( std::to_string( nonce ) ); - auto put_res = nonce_db_->put( key, value ); - if ( put_res.has_error() ) + base::Buffer nonce_value; + nonce_value.put( std::to_string( nonce ) ); + auto nonce_put_res = nonce_db_->put( nonce_key, nonce_value ); + if ( nonce_put_res.has_error() ) { genius_account_logger()->error( "Failed to persist nonce for {:.8}", address ); } + + if ( address != eth_keypair_->GetEntirePubValue() ) + { + return; + } + + std::deque history_copy; + { + std::shared_lock lock( nonce_mutex_ ); + history_copy = local_confirmed_transactions_; + } + + base::Buffer history_key; + history_key.put( std::string( LOCAL_CONFIRMED_TX_HISTORY_KEY_PREFIX ) + address ); + + base::Buffer history_value; + history_value.put( SerializeConfirmedTxHistory( history_copy ) ); + auto history_put_res = nonce_db_->put( history_key, history_value ); + if ( history_put_res.has_error() ) + { + genius_account_logger()->error( "Failed to persist confirmed tx history for {}", address.substr( 0, 8 ) ); + } + } + + std::string GeniusAccount::SerializeConfirmedTxHistory( const std::deque &history ) + { + std::ostringstream out; + for ( const auto &record : history ) + { + out << record.nonce << '|' << record.hash << '\n'; + } + + return out.str(); + } + + std::deque GeniusAccount::DeserializeConfirmedTxHistory( + const std::string &serialized ) + { + std::deque history; + std::istringstream input( serialized ); + std::string line; + + while ( std::getline( input, line ) ) + { + if ( line.empty() ) + { + continue; + } + + const auto separator = line.find( '|' ); + if ( separator == std::string::npos ) + { + continue; + } + + uint64_t parsed_nonce = 0; + if ( TryParseUint64( line.substr( 0, separator ), parsed_nonce ) ) + { + history.push_back( { parsed_nonce, line.substr( separator + 1 ) } ); + } + else + { + continue; + } + } + + while ( history.size() > LOCAL_CONFIRMED_TX_HISTORY_LIMIT ) + { + history.pop_front(); + } + + return history; + } + + void GeniusAccount::UpdateLocalConfirmedTxHistoryLocked( uint64_t nonce, const std::string &tx_hash ) + { + while ( !local_confirmed_transactions_.empty() && local_confirmed_transactions_.back().nonce > nonce ) + { + local_confirmed_transactions_.pop_back(); + } + + for ( auto &record : local_confirmed_transactions_ ) + { + if ( record.nonce == nonce ) + { + record.hash = tx_hash; + return; + } + } + + local_confirmed_transactions_.push_back( { nonce, tx_hash } ); + while ( local_confirmed_transactions_.size() > LOCAL_CONFIRMED_TX_HISTORY_LIMIT ) + { + local_confirmed_transactions_.pop_front(); + } + } + + void GeniusAccount::RollbackLocalConfirmedTxHistoryLocked( uint64_t nonce ) + { + while ( !local_confirmed_transactions_.empty() && local_confirmed_transactions_.back().nonce >= nonce ) + { + local_confirmed_transactions_.pop_back(); + } } } diff --git a/src/account/GeniusAccount.hpp b/src/account/GeniusAccount.hpp index df1856104..c153eaaa6 100644 --- a/src/account/GeniusAccount.hpp +++ b/src/account/GeniusAccount.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -149,17 +150,19 @@ namespace sgns std::vector Sign( const std::vector &data ) const; /** - * @brief Set the local confirmed nonce - * @param[in] nonce The nonce value to be set + * @brief Build signed transaction inputs from UTXOs + * @param[in] utxos UTXOs to turn into transaction inputs + * @return Signed input descriptors */ - void SetLocalConfirmedNonce( uint64_t nonce ); + std::vector CreateInputsFromUTXOs( const std::vector &utxos ) const; /** - * @brief Set the local confirmed nonce for a peer + * @brief Set the confirmed nonce for an address * @param[in] nonce The nonce value to be set - * @param[in] address The address of the peer + * @param[in] address The address whose nonce is being updated + * @param[in] tx_hash The confirmed transaction hash. Persisted only for the local address */ - void SetPeerConfirmedNonce( uint64_t nonce, const std::string &address ); + void SetPeerConfirmedNonce( uint64_t nonce, const std::string &address, const std::string &tx_hash = "" ); /** * @brief Rollback the local confirmed nonce for a peer @@ -181,6 +184,13 @@ namespace sgns */ outcome::result GetLocalConfirmedNonce() const; + /** + * @brief Get a locally persisted confirmed transaction hash by nonce + * @param[in] nonce The confirmed nonce to search for + * @return The confirmed transaction hash if it exists, error otherwise + */ + outcome::result GetLocalConfirmedTxHash( uint64_t nonce ) const; + /** * @brief Get confirmed nonce from the network * @param[in] timeout_ms Timeout in miliseconds to get the confirmed nonce @@ -290,7 +300,14 @@ namespace sgns void SetNonceStore( std::shared_ptr db ); private: + struct ConfirmedTxRecord + { + uint64_t nonce; + std::string hash; + }; + static constexpr size_t SIGNATURE_EXP_SIZE = 64; ///< Expected size of the signature in bytes + static constexpr size_t LOCAL_CONFIRMED_TX_HISTORY_LIMIT = 5; static outcome::result LoadGeniusAccount( const boost::filesystem::path &base_path ); @@ -310,6 +327,7 @@ namespace sgns mutable std::shared_mutex nonce_mutex_; ///< Mutex for the nonce map std::set pending_nonces_; ///< Reserved but not confirmed nonces std::optional local_confirmed_nonce_; ///< Highest locally confirmed nonce + std::deque local_confirmed_transactions_; ///< Recent local confirmed txs std::shared_ptr messenger_; ///< Messenger instance UTXOManager utxo_manager_; @@ -334,9 +352,15 @@ namespace sgns std::shared_ptr nonce_db_; ///< RocksDB for nonce persistence static constexpr std::string_view NONCE_KEY_PREFIX = "gnus-confirmed-nonce-"; + static constexpr std::string_view LOCAL_CONFIRMED_TX_HISTORY_KEY_PREFIX = + "gnus-local-confirmed-tx-history-"; void LoadConfirmedNonces(); void PersistConfirmedNonce( const std::string &address, uint64_t nonce ); + static std::string SerializeConfirmedTxHistory( const std::deque &history ); + static std::deque DeserializeConfirmedTxHistory( const std::string &serialized ); + void UpdateLocalConfirmedTxHistoryLocked( uint64_t nonce, const std::string &tx_hash ); + void RollbackLocalConfirmedTxHistoryLocked( uint64_t nonce ); uint64_t GetNextNonceLocked() const; diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index 8ef4d71a8..8ae17af6f 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -329,6 +329,7 @@ namespace sgns blockchain_ = Blockchain::New( tx_globaldb_, account_, + pubsub_, [weak_self]( outcome::result result ) { if ( auto strong = weak_self.lock() ) @@ -390,6 +391,7 @@ namespace sgns io_, account_, std::make_shared(), + blockchain_, is_full_node_ ); transaction_manager_->RegisterStateChangeCallback( @@ -500,11 +502,11 @@ namespace sgns auto loggerDAGSyncer = ConfigureLogger( "GraphsyncDAGSyncer", logdir, spdlog::level::err ); auto loggerGraphsync = ConfigureLogger( "graphsync", logdir, spdlog::level::err ); auto loggerBroadcaster = ConfigureLogger( "PubSubBroadcasterExt", logdir, spdlog::level::err ); - auto loggerDataStore = ConfigureLogger( "CrdtDatastore", logdir, spdlog::level::debug ); + auto loggerDataStore = ConfigureLogger( "CrdtDatastore", logdir, spdlog::level::err ); auto loggerCRDTHeads = ConfigureLogger( "CrdtHeads", logdir, spdlog::level::err ); auto loggerTransactions = ConfigureLogger( "TransactionManager", logdir, spdlog::level::debug ); - auto loggerMigration = ConfigureLogger( "MigrationManager", logdir, spdlog::level::trace ); - auto loggerMigrationStep = ConfigureLogger( "MigrationStep", logdir, spdlog::level::trace ); + auto loggerMigration = ConfigureLogger( "MigrationManager", logdir, spdlog::level::debug ); + auto loggerMigrationStep = ConfigureLogger( "MigrationStep", logdir, spdlog::level::debug ); auto loggerQueue = ConfigureLogger( "ProcessingTaskQueueImpl", logdir, spdlog::level::err ); auto loggerRocksDB = ConfigureLogger( "rocksdb", logdir, spdlog::level::err ); auto logkad = ConfigureLogger( "Kademlia", logdir, spdlog::level::err ); @@ -516,16 +518,17 @@ namespace sgns auto loggerUPNP = ConfigureLogger( "UPNP", logdir, spdlog::level::err ); auto loggerProcessingNode = ConfigureLogger( "ProcessingNode", logdir, spdlog::level::err ); auto loggerGossipPubsub = ConfigureLogger( "GossipPubSub", logdir, spdlog::level::err ); - auto loggerAccountMessenger = ConfigureLogger( "AccountMessenger", logdir, spdlog::level::debug ); - auto loggerGeniusAccount = ConfigureLogger( "GeniusAccount", logdir, spdlog::level::debug ); - auto loggerKeyPair = ConfigureLogger( "KeyPairFileStorage", logdir, spdlog::level::err ); - auto loggerBlockchain = ConfigureLogger( "Blockchain", logdir, spdlog::level::trace ); - auto loggerValidator = ConfigureLogger( "ValidatorRegistry", logdir, spdlog::level::debug ); - auto loggerProcMgr = ConfigureLogger( "SGProcessingManager", logdir, spdlog::level::err ); - auto loggerProcessor = ConfigureLogger( "SGProcessor", logdir, spdlog::level::err ); - auto loggerCrdtCallback = ConfigureLogger( "CRDTCallbackManager", logdir, spdlog::level::err ); - auto loggerCoinPrices = ConfigureLogger( "CoinPrices", logdir, spdlog::level::err ); - auto loggerUTXOManager = ConfigureLogger( "UTXOManager", logdir, spdlog::level::err ); + auto loggerAccountMessenger = ConfigureLogger( "AccountMessenger", logdir, spdlog::level::err ); + auto loggerGeniusAccount = ConfigureLogger( "GeniusAccount", logdir, spdlog::level::err ); + auto loggerKeyPair = ConfigureLogger( "KeyPairFileStorage", logdir, spdlog::level::err ); + auto loggerBlockchain = ConfigureLogger( "Blockchain", logdir, spdlog::level::err ); + auto loggerValidator = ConfigureLogger( "ValidatorRegistry", logdir, spdlog::level::trace ); + auto loggerProcMgr = ConfigureLogger( "SGProcessingManager", logdir, spdlog::level::err ); + auto loggerProcessor = ConfigureLogger( "SGProcessor", logdir, spdlog::level::err ); + auto loggerCrdtCallback = ConfigureLogger( "CRDTCallbackManager", logdir, spdlog::level::err ); + auto loggerCoinPrices = ConfigureLogger( "CoinPrices", logdir, spdlog::level::err ); + auto loggerUTXOManager = ConfigureLogger( "UTXOManager", logdir, spdlog::level::err ); + auto loggerConsensusManager = ConfigureLogger( "ConsensusManager", logdir, spdlog::level::trace ); // AsyncIOManager loggers auto asioFileCommon = ConfigureLogger( "FILECommon", logdir, spdlog::level::err ); auto asioFileManager = ConfigureLogger( "FileManager", logdir, spdlog::level::err ); @@ -550,7 +553,7 @@ namespace sgns libp2p::log::setLevelOfGroup( "yamux", soralog::Level::DEBUG ); #else // Release mode - node_logger_ = ConfigureLogger( "SuperGeniusNode", logdir, spdlog::level::trace ); + node_logger_ = ConfigureLogger( "SuperGeniusNode", logdir, spdlog::level::err ); auto loggerGeniusNode = ConfigureLogger( "GeniusNode", logdir, spdlog::level::err ); auto loggerGlobalDB = ConfigureLogger( "GlobalDB", logdir, spdlog::level::err ); auto loggerDAGSyncer = ConfigureLogger( "GraphsyncDAGSyncer", logdir, spdlog::level::err ); @@ -572,16 +575,18 @@ namespace sgns auto loggerUPNP = ConfigureLogger( "UPNP", logdir, spdlog::level::err ); auto loggerProcessingNode = ConfigureLogger( "ProcessingNode", logdir, spdlog::level::err ); auto loggerGossipPubsub = ConfigureLogger( "GossipPubSub", logdir, spdlog::level::err ); - auto loggerAccountMessenger = ConfigureLogger( "AccountMessenger", logdir, spdlog::level::err ); - auto loggerGeniusAccount = ConfigureLogger( "GeniusAccount", logdir, spdlog::level::err ); - auto loggerKeyPair = ConfigureLogger( "KeyPairFileStorage", logdir, spdlog::level::err ); - auto loggerBlockchain = ConfigureLogger( "Blockchain", logdir, spdlog::level::err ); - auto loggerValidator = ConfigureLogger( "ValidatorRegistry", logdir, spdlog::level::err ); - auto loggerProcMgr = ConfigureLogger( "SGProcessingManager", logdir, spdlog::level::err ); - auto loggerProcessor = ConfigureLogger( "SGProcessor", logdir, spdlog::level::err ); - auto loggerCrdtCallback = ConfigureLogger( "CRDTCallbackManager", logdir, spdlog::level::err ); - auto loggerCoinPrices = ConfigureLogger( "CoinPrices", logdir, spdlog::level::err ); - auto loggerUTXOManager = ConfigureLogger( "UTXOManager", logdir, spdlog::level::err ); + auto loggerAccountMessenger = ConfigureLogger( "AccountMessenger", logdir, spdlog::level::err ); + auto loggerGeniusAccount = ConfigureLogger( "GeniusAccount", logdir, spdlog::level::err ); + auto loggerKeyPair = ConfigureLogger( "KeyPairFileStorage", logdir, spdlog::level::err ); + auto loggerBlockchain = ConfigureLogger( "Blockchain", logdir, spdlog::level::err ); + auto loggerValidator = ConfigureLogger( "ValidatorRegistry", logdir, spdlog::level::err ); + auto loggerProcMgr = ConfigureLogger( "SGProcessingManager", logdir, spdlog::level::err ); + auto loggerProcessor = ConfigureLogger( "SGProcessor", logdir, spdlog::level::err ); + auto loggerCrdtCallback = ConfigureLogger( "CRDTCallbackManager", logdir, spdlog::level::err ); + auto loggerCoinPrices = ConfigureLogger( "CoinPrices", logdir, spdlog::level::err ); + auto loggerUTXOManager = ConfigureLogger( "UTXOManager", logdir, spdlog::level::err ); + auto loggerConsensusManager = ConfigureLogger( "ConsensusManager", logdir, spdlog::level::err ); + //AsyncIOManager Loggers auto asioFileCommon = ConfigureLogger( "FILECommon", logdir, spdlog::level::err ); auto asioFileManager = ConfigureLogger( "FileManager", logdir, spdlog::level::err ); @@ -780,7 +785,8 @@ namespace sgns generator_, // generator write_base_path_, // writeBasePath base58key_, // base58key - account_ ); + account_, + is_full_node_ ); std::thread migration_thread( [manager = std::move( migrationManager ), cb = std::move( callback )] diff --git a/src/account/GeniusNode.hpp b/src/account/GeniusNode.hpp index da4cb6317..93d222ca9 100644 --- a/src/account/GeniusNode.hpp +++ b/src/account/GeniusNode.hpp @@ -1,3 +1,9 @@ +/** + * @file GeniusNode.hpp + * @brief Top-level node orchestration API for account, transaction, blockchain, and processing services. + * @date 2024-03-11 + * @author Henrique A. Klein (hklein@gnus.ai) + */ #ifndef _GENIUS_NODE_HPP_ #define _GENIUS_NODE_HPP_ @@ -31,13 +37,16 @@ #include #include +/** + * @brief Runtime configuration values used to bootstrap a Genius node instance. + */ typedef struct DevConfig { - std::string Addr; - std::string Cut; - std::string TokenValueInGNUS; - TokenID TokenID; - std::string BaseWritePath; + std::string Addr; ///< Developer payout address. + std::string Cut; ///< Developer or peer cut encoded as a string. + std::string TokenValueInGNUS; ///< Conversion rate used for child-token. + TokenID TokenID; ///< Child token identifier configured for this node. + std::string BaseWritePath; ///< Base directory for node databases, logs, and account storage. } DevConfig_st; extern DevConfig_st DEV_CONFIG; @@ -47,9 +56,23 @@ extern DevConfig_st DEV_CONFIG; namespace sgns { + /** + * @brief High-level facade that initializes and coordinates account, networking, + * transaction, blockchain, and processing subsystems. + */ class GeniusNode : public IComponent, public std::enable_shared_from_this { public: + /** + * @brief Creates a node using a generated or persisted account identity. + * @param[in] dev_config Runtime configuration for paths, token settings, and payout data. + * @param[in] autodht Whether to start DHT discovery. + * @param[in] isprocessor Whether this node should run processing services. + * @param[in] base_port Base pubsub port used to derive the node listening port. + * @param[in] is_full_node Whether the node should run in full-node mode. + * @param[in] use_upnp Whether to attempt UPnP port mapping. + * @return Shared node instance after asynchronous database initialization is scheduled. + */ static std::shared_ptr New( const DevConfig_st &dev_config, bool autodht = true, bool isprocessor = true, @@ -57,6 +80,17 @@ namespace sgns bool is_full_node = false, bool use_upnp = true ); + /** + * @brief Creates a node bound to the provided Ethereum private key. + * @param[in] dev_config Runtime configuration for paths, token settings, and payout data. + * @param[in] eth_private_key Ethereum private key used to derive the account identity. + * @param[in] autodht Whether to start DHT discovery. + * @param[in] isprocessor Whether this node should run processing services. + * @param[in] base_port Base pubsub port used to derive the node listening port. + * @param[in] is_full_node Whether the node should run in full-node mode. + * @param[in] use_upnp Whether to attempt UPnP port mapping. + * @return Shared node instance after asynchronous database initialization is scheduled. + */ static std::shared_ptr New( const DevConfig_st &dev_config, const char *eth_private_key, bool autodht = true, @@ -65,6 +99,17 @@ namespace sgns bool is_full_node = false, bool use_upnp = true ); + /** + * @brief Creates a node from an existing mnemonic phrase. + * @param[in] dev_config Runtime configuration for paths, token settings, and payout data. + * @param[in] mnemonic Mnemonic phrase used to restore the account identity. + * @param[in] autodht Whether to start DHT discovery. + * @param[in] isprocessor Whether this node should run processing services. + * @param[in] base_port Base pubsub port used to derive the node listening port. + * @param[in] is_full_node Whether the node should run in full-node mode. + * @param[in] use_upnp Whether to attempt UPnP port mapping. + * @return Shared node instance after asynchronous database initialization is scheduled, or nullptr on restore failure. + */ static std::shared_ptr NewFromMnemonic( const DevConfig_st &dev_config, const std::string &mnemonic, bool autodht = true, @@ -73,82 +118,140 @@ namespace sgns bool is_full_node = false, bool use_upnp = true ); + /** + * @brief Stops node services, joins background threads, and releases processing callbacks. + */ ~GeniusNode() override; + /** + * @brief Lifecycle states reported while the node is bootstrapping. + */ enum class NodeState : uint8_t { - CREATING = 0, - MIGRATING_DATABASE, - INITIALIZING_DATABASE, - INITIALIZING_PROCESSING, - INITIALIZING_BLOCKCHAIN, - INITIALIZING_TRANSACTIONS, - READY, + CREATING = 0, ///< Object construction is in progress. + MIGRATING_DATABASE, ///< Versioned database migrations are running. + INITIALIZING_DATABASE, ///< Primary CRDT database is being initialized. + INITIALIZING_PROCESSING, ///< Processing modules are being initialized. + INITIALIZING_BLOCKCHAIN, ///< Blockchain service is being initialized. + INITIALIZING_TRANSACTIONS, ///< Transaction manager is being initialized. + READY, ///< Node is ready for external operations. }; /** - * @brief GeniusNode Error class + * @brief Error codes returned by GeniusNode operations. */ enum class Error : uint8_t { - INSUFFICIENT_FUNDS = 1, ///< Insufficient funds for a transaction - DATABASE_WRITE_ERROR = 2, ///< Error writing data into the database - INVALID_TRANSACTION_HASH = 3, ///< Input transaction hash is invalid - INVALID_CHAIN_ID = 4, ///< Chain ID is invalid - INVALID_TOKEN_ID = 5, ///< Token ID is invalid - TOKEN_ID_MISMATCH = 6, ///< Informed Token ID doesn't match initialized ID - PROCESS_COST_ERROR = 7, ///< The calculated Processing cost was negative - PROCESS_INFO_MISSING = 8, ///< Processing information missing on JSON file - INVALID_JSON = 9, ///< JSON cannot be parsed> - INVALID_BLOCK_PARAMETERS = 10, ///< JSON params for blocks incorrect or missing> - NO_PROCESSOR = 11, ///< No processor for this type - NO_PRICE = 12, ///< Couldn't get price of gnus - TRANSACTIONS_NOT_READY = 13, ///< Transactions aren't ready - TRANSACTION_NOT_FINALIZED = 14, ///< Requested transaction not finalized within timeout - TRANSACTION_FAILED = 15, ///< Requested transaction failed + INSUFFICIENT_FUNDS = 1, ///< Insufficient funds for a transaction. + DATABASE_WRITE_ERROR = 2, ///< Error writing data into the database. + INVALID_TRANSACTION_HASH = 3, ///< Input transaction hash is invalid. + INVALID_CHAIN_ID = 4, ///< Chain ID is invalid. + INVALID_TOKEN_ID = 5, ///< Token ID is invalid. + TOKEN_ID_MISMATCH = 6, ///< Provided token ID does not match the configured token. + PROCESS_COST_ERROR = 7, ///< Processing cost could not be calculated. + PROCESS_INFO_MISSING = 8, ///< Processing information is missing from the JSON request. + INVALID_JSON = 9, ///< JSON cannot be parsed. + INVALID_BLOCK_PARAMETERS = 10, ///< JSON block parameters are incorrect or missing. + NO_PROCESSOR = 11, ///< No processor is available for this request type. + NO_PRICE = 12, ///< GNUS price could not be retrieved. + TRANSACTIONS_NOT_READY = 13, ///< Transaction manager is not ready. + TRANSACTION_NOT_FINALIZED = 14, ///< Requested transaction did not finalize within the timeout. + TRANSACTION_FAILED = 15, ///< Requested transaction failed. }; #ifdef SGNS_DEBUG - static constexpr std::chrono::milliseconds TIMEOUT_ESCROW_PAY{ 50000 }; - static constexpr std::chrono::milliseconds TIMEOUT_TRANSFER{ 50000 }; - static constexpr std::chrono::milliseconds TIMEOUT_MINT{ 50000 }; + static constexpr std::chrono::milliseconds TIMEOUT_ESCROW_PAY{ 50000 }; ///< Debug escrow payout timeout. + static constexpr std::chrono::milliseconds TIMEOUT_TRANSFER{ 50000 }; ///< Debug transfer timeout. + static constexpr std::chrono::milliseconds TIMEOUT_MINT{ 50000 }; ///< Debug mint timeout. #else - static constexpr std::chrono::milliseconds TIMEOUT_ESCROW_PAY{ 30000 }; - static constexpr std::chrono::milliseconds TIMEOUT_TRANSFER{ 30000 }; - static constexpr std::chrono::milliseconds TIMEOUT_MINT{ 30000 }; + static constexpr std::chrono::milliseconds TIMEOUT_ESCROW_PAY{ 30000 }; ///< Escrow payout timeout. + static constexpr std::chrono::milliseconds TIMEOUT_TRANSFER{ 30000 }; ///< Transfer timeout. + static constexpr std::chrono::milliseconds TIMEOUT_MINT{ 30000 }; ///< Mint timeout. #endif + /** + * @brief Lists the account addresses currently available in local storage. + * @return Public addresses stored under the configured base write path. + */ std::vector GetAvailableAccounts(); + /** + * @brief Selects the active account for subsequent node operations. + * @param[in] public_address Stored account address to activate. + * @return Success after services are reset and database initialization is restarted, or an address error. + */ outcome::result SelectAccount( std::string_view public_address ); + /** + * @brief Transfers node ownership to another stored account address. + * @param[in] public_address Stored account address that should receive the current balance and become active. + * @return Success after funds are transferred and the target account is selected, or an address/transaction error. + */ outcome::result TransferAccount( std::string_view public_address ); + /** + * @brief Deletes a locally stored account. + * @param[in] public_address Stored account address to delete. + * @return Success when the account is deleted; failure when the address is active or unavailable. + */ outcome::result DeleteAccount( std::string_view public_address ); + /** + * @brief Merges data from another account into the currently selected one. + * @param[in] public_address Stored account address to transfer into and then delete. + * @return Success when transfer and delete both complete. + */ outcome::result MergeAccount( std::string_view public_address ); + /** + * @brief Updates the payout address used by processing rewards. + * @param[in] payout_address Address to save as the processing payout destination. + * @return Success when the address is persisted and processing reinitialization is scheduled. + */ outcome::result SetPayoutAddress( std::string_view payout_address ); + /** + * @brief Submits an image-processing request described by JSON input. + * @param[in] jsondata Processing request JSON. + * @return Escrow transaction hash on success, or a validation, balance, or database error. + */ outcome::result ProcessImage( const std::string &jsondata ); + /** + * @brief Estimates the GNUS cost of a processing request manager. + * @param[in] procmgr Processing manager containing parsed request data. + * @return Estimated cost in minions, or 0 when the request size, price, or cost calculation fails. + */ uint64_t GetProcessCost( std::shared_ptr &procmgr ); + /** + * @brief Retrieves the current GNUS market price from the configured pricing service. + * @return Current GNUS price in USD, or Error::NO_PRICE when unavailable. + */ outcome::result GetGNUSPrice(); + /** + * @brief Returns the component name used by the component framework. + * @return Static component name "GeniusNode". + */ std::string GetName() override { return "GeniusNode"; } + /** + * @brief Returns the full SuperGenius version string. + * @return Version string built from the compiled version metadata. + */ std::string GetVersion(); /** - * @brief Fires of a Mint transaction and returns the transaction hash - * @param[in] amount Amount to be minted - * @param[in] transaction_hash Hash of the transaction that burned the tokens elsewhere - * @param[in] chainid The chain ID where the burn transaction took place - * @param[in] tokenid The token ID of the tokens to mint - * @return The transaction hash of the mint transaction if successful, or an error otherwise + * @brief Creates and submits a mint transaction. + * @param[in] amount Amount to mint in token base units. + * @param[in] transaction_hash Source-chain transaction hash that justifies the mint. + * @param[in] chainid Source chain identifier where the burn or lock event occurred. + * @param[in] tokenid Token identifier to mint. + * @param[in] destination Recipient address; defaults to the active account address when empty. + * @return Mint transaction hash on success, or a transaction readiness/submission error. */ outcome::result MintTokens( uint64_t amount, const std::string &transaction_hash, @@ -157,13 +260,14 @@ namespace sgns std::string destination = "" ); /** - * @brief Mints tokens by converting a string amount to fixed-point representation - * @param[in] amount: Numeric value with amount in Minion Tokens (1e-6 GNUS Token) - * @param[in] transaction_hash Transaction hash on the source chain. - * @param[in] chainid Source chain identifier. - * @param[in] tokenid Token identifier to mint. - * @param[in] timeout Timeout for the mint operation. - * @return Outcome of mint token operation + * @brief Creates a mint transaction and waits for it to finalize. + * @param[in] amount Amount to mint in token base units. + * @param[in] transaction_hash Source-chain transaction hash that justifies the mint. + * @param[in] chainid Source chain identifier where the burn or lock event occurred. + * @param[in] tokenid Token identifier to mint. + * @param[in] destination Recipient address for the minted tokens. + * @param[in] timeout Maximum time to wait for finalization. + * @return Pair of transaction hash and elapsed milliseconds on success, or a transaction/finalization error. */ outcome::result> MintTokens( uint64_t amount, const std::string &transaction_hash, @@ -172,14 +276,50 @@ namespace sgns std::string destination, std::chrono::milliseconds timeout ); + /** + * @brief Adds a peer address to the underlying PubSub service. + * @param[in] peer Peer multiaddress to add. + */ void AddPeer( const std::string &peer ); + + /** + * @brief Starts or restarts the background UPnP port refresh thread. + * @param[in] pubsubport TCP port to keep mapped through UPnP. + */ void RefreshUPNP( uint16_t pubsubport ); + /** + * @brief Returns the active account balance across all tokens. + * @return Total local UTXO balance for the active account. + */ uint64_t GetBalance(); + + /** + * @brief Returns the active account balance for a token. + * @param[in] token_id Token identifier to filter by. + * @return Local UTXO balance for @p token_id. + */ uint64_t GetBalance( TokenID token_id ); + + /** + * @brief Returns an address balance across all tokens. + * @param[in] address Address whose UTXO balance should be queried. + * @return Total local UTXO balance for @p address. + */ uint64_t GetBalance( const std::string &address ); + + /** + * @brief Returns an address balance for a token. + * @param[in] token_id Token identifier to filter by. + * @param[in] address Address whose UTXO balance should be queried. + * @return Local UTXO balance for @p address and @p token_id. + */ uint64_t GetBalance( TokenID token_id, const std::string &address ); + /** + * @brief Returns serialized incoming transactions known to the transaction manager. + * @return Incoming transaction byte vectors, or an empty vector when transactions are not ready. + */ [[nodiscard]] std::vector> GetInTransactions() const { auto manager_result = GetTransactionManager(); @@ -190,6 +330,10 @@ namespace sgns return manager_result.value()->GetInTransactions(); } + /** + * @brief Returns serialized outgoing transactions known to the transaction manager. + * @return Outgoing transaction byte vectors, or an empty vector when transactions are not ready. + */ [[nodiscard]] std::vector> GetOutTransactions() const { auto manager_result = GetTransactionManager(); @@ -200,16 +344,44 @@ namespace sgns return manager_result.value()->GetOutTransactions(); } + /** + * @brief Returns serialized transactions filtered by optional status. + * @param[in] tx_status Optional transaction status filter. + * @return Transaction byte vectors, or an empty vector when transactions are not ready. + */ + [[nodiscard]] const std::vector> GetTransactions( + std::optional tx_status = std::nullopt ) const + { + auto manager_result = GetTransactionManager(); + if ( !manager_result.has_value() ) + { + return {}; + } + return manager_result.value()->GetTransactions( tx_status ); + } + + /** + * @brief Returns the active account public address. + * @return Public address of the active account. + */ std::string GetAddress() const { return account_->GetAddress(); } + /** + * @brief Returns the configured child token identifier. + * @return Token identifier from the node runtime configuration. + */ TokenID GetTokenID() const { return dev_config_.TokenID; } + /** + * @brief Returns the current processing service status. + * @return Processing status, or DISABLED when the service is not initialized. + */ [[nodiscard]] processing::ProcessingServiceImpl::ProcessingStatus GetProcessingStatus() const { return processing_service_ == nullptr ? processing::ProcessingServiceImpl::ProcessingStatus( @@ -218,30 +390,76 @@ namespace sgns : processing_service_->GetProcessingStatus(); } + /** + * @brief Transfers funds and waits for the transaction to finalize. + * @param[in] amount Amount to transfer in token base units. + * @param[in] destination Recipient address. + * @param[in] token_id Token identifier to transfer. + * @param[in] timeout Maximum time to wait for finalization. + * @return Pair of transaction hash and elapsed milliseconds on success, or a transfer/finalization error. + */ outcome::result> TransferFunds( uint64_t amount, const std::string &destination, TokenID token_id, std::chrono::milliseconds timeout ); + /** + * @brief Transfers funds without waiting for finalization. + * @param[in] amount Amount to transfer in token base units. + * @param[in] destination Recipient address. + * @param[in] token_id Token identifier to transfer. + * @return Transfer transaction hash on success, or a readiness, balance, or submission error. + */ outcome::result TransferFunds( uint64_t amount, const std::string &destination, TokenID token_id ); + /** + * @brief Transfers funds to the configured developer address. + * @param[in] amount Amount to transfer in token base units. + * @param[in] token_id Token identifier to transfer. + * @return Transfer transaction hash on success, or a readiness, balance, or submission error. + */ outcome::result PayDev( uint64_t amount, TokenID token_id ); + /** + * @brief Transfers funds to the configured developer address and waits for finalization. + * @param[in] amount Amount to transfer in token base units. + * @param[in] token_id Token identifier to transfer. + * @param[in] timeout Maximum time to wait for finalization. + * @return Pair of transaction hash and elapsed milliseconds on success, or a transfer/finalization error. + */ outcome::result> PayDev( uint64_t amount, TokenID token_id, std::chrono::milliseconds timeout ); + /** + * @brief Waits until an outgoing transaction reaches a terminal state. + * @param[in] tx_id Transaction hash to poll. + * @param[in] timeout Maximum time to wait. + * @return Pair of terminal status and elapsed milliseconds, or Error::TRANSACTION_NOT_FINALIZED on timeout. + */ outcome::result> WaitForFinalized( const std::string &tx_id, std::chrono::milliseconds timeout ); + /** + * @brief Checks whether an outgoing transaction has reached a terminal state. + * @param[in] tx_id Transaction hash to check. + * @return Terminal transaction status when available; otherwise std::nullopt. + */ std::optional IsFinalized( const std::string &tx_id ); + /** + * @brief Returns the underlying PubSub service. + * @return Shared PubSub instance used by the node. + */ std::shared_ptr GetPubSub() { return pubsub_; } + /** + * @brief Releases processing service, core, queue, and result-storage references. + */ void ResetProcessingMembers(); /** @@ -266,44 +484,106 @@ namespace sgns */ outcome::result ParseTokens( const std::string &str, TokenID tokenId ); + /** + * @brief Prints the transaction GlobalDB datastore for debugging. + */ void PrintDataStore() const; + + /** + * @brief Stops the processing service if it is initialized. + */ void StopProcessing(); + + /** + * @brief Starts the processing service on the configured processing grid channel. + */ void StartProcessing(); + /** + * @brief Retrieves current USD prices for token identifiers, using a short local cache. + * @param[in] tokenIds CoinGecko token identifiers to price. + * @return Map from token identifier to current USD price, or a price-retrieval error. + */ outcome::result> GetCoinprice( const std::vector &tokenIds ); + + /** + * @brief Retrieves historical USD prices for token identifiers at exact timestamps. + * @param[in] tokenIds CoinGecko token identifiers to price. + * @param[in] timestamps Unix timestamps to query. + * @return Nested map from token identifier to timestamp to USD price. + */ outcome::result>> GetCoinPriceByDate( const std::vector &tokenIds, const std::vector ×tamps ); + + /** + * @brief Retrieves historical USD prices for token identifiers over a date range. + * @param[in] tokenIds CoinGecko token identifiers to price. + * @param[in] from Start Unix timestamp for the range. + * @param[in] to End Unix timestamp for the range. + * @return Nested map from token identifier to timestamp to USD price. + */ outcome::result>> GetCoinPricesByDateRange( const std::vector &tokenIds, int64_t from, int64_t to ); - // Wait for an incoming transaction to be processed with a timeout + + /** + * @brief Waits for an incoming transaction to be processed. + * @param[in] txId Transaction hash to wait for. + * @param[in] timeout Maximum time to wait. + * @return Incoming transaction status, or INVALID when transactions are not ready. + */ TransactionManager::TransactionStatus WaitForTransactionIncoming( const std::string &txId, std::chrono::milliseconds timeout ); - // Wait for a outgoing transaction to be processed with a timeout + + /** + * @brief Waits for an outgoing transaction to be processed. + * @param[in] txId Transaction hash to wait for. + * @param[in] timeout Maximum time to wait. + * @return Outgoing transaction status, or INVALID when transactions are not ready. + */ TransactionManager::TransactionStatus WaitForTransactionOutgoing( const std::string &txId, std::chrono::milliseconds timeout ); + /** + * @brief Waits for an escrow release transaction tied to an escrow hold. + * @param[in] originalEscrowId Hash of the original escrow hold transaction. + * @param[in] timeout Maximum time to wait. + * @return Escrow release transaction status, or INVALID when transactions are not ready. + */ TransactionManager::TransactionStatus WaitForEscrowRelease( const std::string &originalEscrowId, std::chrono::milliseconds timeout ); + /** + * @brief Returns the current transaction manager lifecycle state. + * @return Transaction manager state, or CREATING when the manager is not available. + */ TransactionManager::State GetTransactionManagerState() const; + /** + * @brief Returns a tracked transaction status by transaction hash. + * @param[in] txId Transaction hash to look up. + * @return Outgoing status when present, then incoming status, or INVALID when unknown/not ready. + */ TransactionManager::TransactionStatus GetTransactionStatus( const std::string &txId ) const; /** - * @brief Set the authorized full node address for blockchain genesis verification - * @param pub_address The public address that is authorized to create genesis blocks + * @brief Sets the authorized full-node address for blockchain genesis verification. + * @param[in] pub_address Public address authorized to create genesis blocks. */ void SetAuthorizedFullNodeAddress( const std::string &pub_address ); /** - * @brief Get the current authorized full node public address - * @return The authorized full node public address + * @brief Gets the current authorized full-node public address. + * @return Public address authorized to create genesis blocks. */ const std::string &GetAuthorizedFullNodeAddress() const; + /** + * @brief Returns the current GeniusNode lifecycle state. + * @return Current node state. + */ NodeState GetState() const { return state_.load(); @@ -312,35 +592,57 @@ namespace sgns protected: friend class TransactionSyncTest; + /** + * @brief Enqueues a transaction and its proof directly through the transaction manager. + * @param[in] tx Transaction to enqueue. + * @param[in] proof Serialized proof bytes associated with @p tx. + */ void SendTransactionAndProof( std::shared_ptr tx, std::vector proof ); + + /** + * @brief Configures transaction filtering time windows for tests. + * @param[in] timeframe_limit_ms Timestamp tolerance in milliseconds. + * @param[in] mutability_window_ms Mutability window in milliseconds. + */ void ConfigureTransactionFilterTimeoutsMs( uint64_t timeframe_limit_ms, uint64_t mutability_window_ms ); - std::string write_base_path_; - std::shared_ptr account_; + std::string write_base_path_; ///< Base path for node databases, logs, and account storage. + std::shared_ptr account_; ///< Active account used by node services. private: - std::shared_ptr io_; - boost::asio::executor_work_guard io_work_guard_; - std::shared_ptr tx_globaldb_; - std::shared_ptr job_globaldb_; - std::shared_ptr pubsub_; - std::shared_ptr transaction_manager_; - std::shared_ptr task_queue_; - std::shared_ptr processing_core_; - std::shared_ptr processing_service_; - std::shared_ptr task_result_storage_; - std::shared_ptr logging_system_; - bool autodht_; - bool isprocessor_; - bool is_full_node_; - base::Logger node_logger_; - DevConfig_st dev_config_; - std::string gnus_network_full_path_; - std::string processing_channel_topic_; - std::string processing_grid_chanel_topic_; - uint16_t pubsubport_; - std::shared_ptr blockchain_; + std::shared_ptr io_; ///< Shared IO context for async services. + boost::asio::executor_work_guard + io_work_guard_; ///< Keeps @ref io_ alive. + std::shared_ptr tx_globaldb_; ///< Transaction/global state CRDT DB. + std::shared_ptr job_globaldb_; ///< Reserved job CRDT DB handle. + std::shared_ptr pubsub_; ///< PubSub networking service. + std::shared_ptr transaction_manager_; ///< Transaction service. + std::shared_ptr task_queue_; ///< Processing task queue. + std::shared_ptr processing_core_; ///< Processing engine core. + std::shared_ptr processing_service_; ///< Processing network service. + std::shared_ptr task_result_storage_; ///< Subtask result store. + std::shared_ptr logging_system_; ///< libp2p logging system. + bool autodht_; ///< Whether DHT discovery is enabled. + bool isprocessor_; ///< Whether processing service should run. + bool is_full_node_; ///< Whether this node runs in full-node mode. + base::Logger node_logger_; ///< Main node logger. + DevConfig_st dev_config_; ///< Runtime node configuration. + std::string gnus_network_full_path_; ///< Versioned network DB path. + std::string processing_channel_topic_; ///< Processing task channel topic. + std::string processing_grid_chanel_topic_; ///< Processing grid topic. + uint16_t pubsubport_; ///< Active PubSub TCP port. + std::shared_ptr blockchain_; ///< Blockchain service. + /** + * @brief Constructs a node around an already-created account. + * @param[in] dev_config Runtime configuration for paths, token settings, and payout data. + * @param[in] account Account instance to bind to this node. + * @param[in] autodht Whether to start DHT discovery. + * @param[in] isprocessor Whether this node should run processing services. + * @param[in] base_port Base pubsub port used to derive the node listening port. + * @param[in] is_full_node Whether the node should run in full-node mode. + * @param[in] use_upnp Whether to attempt UPnP port mapping. + */ GeniusNode( const DevConfig_st &dev_config, std::shared_ptr account, bool autodht, @@ -349,79 +651,187 @@ namespace sgns bool is_full_node, bool use_upnp ); - void InitOpenSSL(); - bool InitLoggers( const std::string &base_path ); + /** + * @brief Initializes OpenSSL library state used by networking dependencies. + */ + void InitOpenSSL(); + + /** + * @brief Initializes application and dependency loggers. + * @param[in] base_path Base directory used for log files. + * @return True when logging configuration succeeds. + */ + bool InitLoggers( const std::string &base_path ); + + /** + * @brief Creates a tagged logger with the requested sink and level. + * @param[in] tag Logger tag. + * @param[in] logdir Optional log file path. + * @param[in] level Logger severity threshold. + * @return Configured logger instance. + */ base::Logger ConfigureLogger( const std::string &tag, const std::string &logdir, spdlog::level::level_enum level ); - bool InitNetwork( uint16_t base_port, bool is_full_node ); - bool InitUPNP(); - bool InitDatabase(); - bool InitProcessingModules(); - void BeginDBInitialization(); - void StateTransition( NodeState next_state ); - void MigrateDatabase( std::function )> callback ); - void ScheduleMigrationRetry(); - void ScheduleBlockchainRetry(); + + /** + * @brief Initializes PubSub, GraphSync networking, and optional DHT discovery. + * @param[in] base_port Base pubsub port used to derive the node listening port. + * @param[in] is_full_node Whether to use full-node connection limits. + * @return True when network initialization succeeds. + */ + bool InitNetwork( uint16_t base_port, bool is_full_node ); + + /** + * @brief Attempts initial UPnP port mapping for the PubSub port. + * @return True when no gateway exists or a usable port is mapped. + */ + bool InitUPNP(); + + /** + * @brief Initializes and starts the transaction GlobalDB. + * @return True when the database is opened and started. + */ + bool InitDatabase(); + + /** + * @brief Initializes processing queue, core, and result storage components. + * @return True when processing modules are constructed. + */ + bool InitProcessingModules(); + + /** + * @brief Begins the asynchronous database migration and initialization state flow. + */ + void BeginDBInitialization(); + + /** + * @brief Moves the node to the next lifecycle state and runs state-specific work. + * @param[in] next_state State to enter. + */ + void StateTransition( NodeState next_state ); + + /** + * @brief Runs versioned database migrations on a detached thread. + * @param[in] callback Callback invoked with the migration result. + */ + void MigrateDatabase( std::function )> callback ); + + /** + * @brief Schedules a delayed migration retry after migration bootstrap failure. + */ + void ScheduleMigrationRetry(); + + /** + * @brief Schedules a delayed blockchain initialization retry. + */ + void ScheduleBlockchainRetry(); + + /** + * @brief Returns the transaction manager when initialized. + * @return Shared transaction manager, or Error::TRANSACTIONS_NOT_READY. + */ outcome::result> GetTransactionManager() const; - outcome::result CheckProcessValidity( const std::string &jsondata ); + /** + * @brief Validates a processing request JSON before submission. + * @param[in] jsondata Processing request JSON. + * @return Success when the request is valid, or a GeniusNode error. + */ + outcome::result CheckProcessValidity( const std::string &jsondata ); + + /** + * @brief Starts DHT provider discovery for the processing grid topic. + */ void DHTInit(); struct PriceInfo { - double price; - std::chrono::time_point lastUpdate; + double price; ///< Cached USD token price. + std::chrono::time_point lastUpdate; ///< Time when @ref price was fetched. }; - std::map m_tokenPriceCache; - const std::chrono::minutes m_cacheValidityDuration{ 1 }; - std::chrono::time_point m_lastApiCall{}; - static constexpr std::chrono::seconds MIN_API_CALL_INTERVAL{ 5 }; + std::map m_tokenPriceCache; ///< Cached token price data by token id. + const std::chrono::minutes m_cacheValidityDuration{ 1 }; ///< Price cache TTL. + std::chrono::time_point m_lastApiCall{}; ///< Last external price API call time. + static constexpr std::chrono::seconds MIN_API_CALL_INTERVAL{ 5 }; ///< Minimum price API interval. - static constexpr size_t DEFAULT_IO_THREADS = 4; - size_t io_thread_count_{ DEFAULT_IO_THREADS }; - std::vector io_threads_; - std::thread upnp_thread; - std::atomic stop_upnp{ false }; - std::string base58key_; - std::shared_ptr scheduler_; - std::shared_ptr generator_; - std::shared_ptr graphsyncnetwork_; + static constexpr size_t DEFAULT_IO_THREADS = 4; ///< Default IO thread count. + size_t io_thread_count_{ DEFAULT_IO_THREADS }; ///< IO thread count. + std::vector io_threads_; ///< Threads running @ref io_. + std::thread upnp_thread; ///< Background UPnP refresh thread. + std::atomic stop_upnp{ false }; ///< UPnP thread stop flag. + std::string base58key_; ///< Base58 key suffix for DB paths. + std::shared_ptr scheduler_; ///< libp2p scheduler. + std::shared_ptr generator_; ///< GraphSync request ID generator. + std::shared_ptr graphsyncnetwork_; ///< GraphSync network. - std::unique_ptr processing_callback_pool_; + std::unique_ptr processing_callback_pool_; ///< Processing callback execution pool. - std::atomic state_{ NodeState::CREATING }; - bool use_upnp_; + std::atomic state_{ NodeState::CREATING }; ///< Current node lifecycle state. + bool use_upnp_; ///< Whether UPnP mapping is enabled. + /** + * @brief Submits an escrow payout transaction and waits for confirmation. + * @param[in] escrow_path Escrow address/path associated with the completed task. + * @param[in] taskresult Processing task result that defines payout recipients. + * @param[in] crdt_transaction Atomic CRDT transaction that marks task completion. + * @param[in] timeout Maximum time to wait for escrow payout confirmation. + * @return Pair of payout transaction hash and elapsed milliseconds, or a payout/timeout error. + */ outcome::result> PayEscrow( const std::string &escrow_path, const SGProcessing::TaskResult &taskresult, std::shared_ptr crdt_transaction, std::chrono::milliseconds timeout = std::chrono::milliseconds( TIMEOUT_ESCROW_PAY ) ); + /** + * @brief Handles successful processing completion and triggers escrow payout. + * @param[in] task_id Completed task identifier. + * @param[in] taskresult Processing task result to persist and pay out. + */ void ProcessingDone( const std::string &task_id, const SGProcessing::TaskResult &taskresult ); + + /** + * @brief Handles processing failure notifications. + * @param[in] task_id Failed task identifier. + */ void ProcessingError( const std::string &task_id ); + /** + * @brief Rotates existing node log files before logger initialization. + * @param[in] base_path Directory containing node log files. + */ void RotateLogFiles( const std::string &base_path ); + /** * @brief Parse and sum all "block_len" values from the JSON. - * @param json_data JSON string containing an "input" array. + * @param[in] json_data JSON string containing an "input" array. * @return outcome::result with total bytes, or an error code. */ outcome::result ParseBlockSize( const std::string &json_data ); + /** + * @brief Reacts to transaction manager state changes by starting or stopping processing. + * @param[in] old_state Previous transaction manager state. + * @param[in] new_state Current transaction manager state. + */ void TransactionStateChanged( TransactionManager::State old_state, TransactionManager::State new_state ); - static constexpr std::string_view DB_PATH = "bc-%d/"; - static constexpr std::uint16_t MAIN_NET = 369; - static constexpr std::uint16_t TEST_NET = 963; - static constexpr std::size_t MAX_NODES_COUNT = 1; + static constexpr std::string_view DB_PATH = "bc-%d/"; ///< Blockchain DB path format. + static constexpr std::uint16_t MAIN_NET = 369; ///< Main network identifier. + static constexpr std::uint16_t TEST_NET = 963; ///< Test network identifier. + static constexpr std::size_t MAX_NODES_COUNT = 1; ///< Processing service node count limit. - static constexpr std::string_view PROCESSING_GRID_CHANNEL = "SGNUS.Jobs.Channel"; - static constexpr std::string_view PROCESSING_CHANNEL = "SGNUS.Processing.Channel"; - static constexpr std::string_view GNUS_NETWORK_PATH = "SuperGNUSNode.Node"; + static constexpr std::string_view PROCESSING_GRID_CHANNEL = "SGNUS.Jobs.Channel"; ///< Processing job topic. + static constexpr std::string_view PROCESSING_CHANNEL = "SGNUS.Processing.Channel"; ///< Processing result topic. + static constexpr std::string_view GNUS_NETWORK_PATH = "SuperGNUSNode.Node"; ///< Base network DB path. + /** + * @brief Builds the YAML logging configuration used by the node. + * @param[in] base_path Directory where the main log file should be written. + * @return YAML logging configuration with @p base_path substituted. + */ static std::string GetLoggingSystem( const std::string &base_path ) { std::string config( R"( diff --git a/src/account/GeniusUTXO.hpp b/src/account/GeniusUTXO.hpp index acb8c9794..dcf9cf187 100644 --- a/src/account/GeniusUTXO.hpp +++ b/src/account/GeniusUTXO.hpp @@ -1,6 +1,6 @@ /** * @file GeniusUTXO.hpp - * @brief + * @brief Lightweight value type representing a spendable UTXO entry and its outpoint. * @date 2024-04-25 * @author Henrique A. Klein (hklein@gnus.ai) */ @@ -10,56 +10,148 @@ #include "base/blob.hpp" #include "account/TokenID.hpp" +#include +#include + namespace sgns { + /** + * @brief Unique identifier for a transaction output. + */ + struct OutPoint + { + base::Hash256 txid_hash_; ///< Hash of the transaction that produced the output. + uint32_t output_idx_{ 0 }; ///< Output index within the producing transaction. + + /** + * @brief Compares two outpoints for exact transaction-and-index equality. + * @param[in] other Outpoint to compare against. + * @return True when both the transaction hash and output index match. + */ + bool operator==( const OutPoint &other ) const + { + return txid_hash_ == other.txid_hash_ && output_idx_ == other.output_idx_; + } + }; + + /** + * @brief Immutable-style UTXO value object containing ownership, token, amount, and outpoint metadata. + */ class GeniusUTXO { public: + /** + * @brief Constructs an empty UTXO placeholder. + * + * The placeholder has a default outpoint, zero amount, default token identifier, + * and no owner address. + */ + GeniusUTXO() : outpoint_{}, amount_( 0 ), token_id_(), owner_address_() + { + } + + /** + * @brief Constructs a UTXO without an owner address. + * @param[in] hash Hash of the transaction that produced this output. + * @param[in] previous_index Output index within the producing transaction. + * @param[in] amount Amount carried by the output. + * @param[in] token_id Token identifier carried by the output. + */ GeniusUTXO( const base::Hash256 &hash, uint32_t previous_index, uint64_t amount, TokenID token_id ) : - txid_hash_( hash ), // - output_idx_( previous_index ), // - amount_( amount ), // - locked_( false ), // - token_id_( token_id ) // + outpoint_{ hash, previous_index }, // + amount_( amount ), // + token_id_( token_id ) // + { + } + + /** + * @brief Constructs a fully specified UTXO. + * @param[in] hash Hash of the transaction that produced this output. + * @param[in] previous_index Output index within the producing transaction. + * @param[in] amount Amount carried by the output. + * @param[in] token_id Token identifier carried by the output. + * @param[in] owner_address Address that owns or can spend the output. + */ + GeniusUTXO( const base::Hash256 &hash, + uint32_t previous_index, + uint64_t amount, + TokenID token_id, + std::string owner_address ) : + outpoint_{ hash, previous_index }, // + amount_( amount ), // + token_id_( token_id ), // + owner_address_( std::move( owner_address ) ) + { + } + + /** + * @brief Sets the owner address associated with this UTXO. + * @param[in] owner_address Address that owns or can spend the output. + */ + void SetOwnerAddress( std::string owner_address ) { + owner_address_ = std::move( owner_address ); } - void SetLocked( const bool locked ) + /** + * @brief Returns the owner address associated with this UTXO. + * @return Address that owns or can spend the output; empty when not set. + */ + const std::string &GetOwnerAddress() const { - locked_ = locked; + return owner_address_; } + /** + * @brief Returns the full outpoint descriptor. + * @return Outpoint containing the producing transaction hash and output index. + */ + OutPoint GetOutPoint() const + { + return outpoint_; + } + + /** + * @brief Returns the originating transaction id. + * @return Hash of the transaction that produced this output. + */ base::Hash256 GetTxID() const { - return txid_hash_; + return outpoint_.txid_hash_; } + /** + * @brief Returns the output index within the originating transaction. + * @return Output index within the producing transaction. + */ uint32_t GetOutputIdx() const { - return output_idx_; + return outpoint_.output_idx_; } + /** + * @brief Returns the unencrypted amount represented by the UTXO. + * @return Amount carried by the output. + */ uint64_t GetAmount() const { return amount_; } - bool GetLock() const - { - return locked_; - } - + /** + * @brief Returns the token identifier associated with the UTXO. + * @return Token identifier carried by the output. + */ TokenID GetTokenID() const { return token_id_; } private: - base::Hash256 txid_hash_; - uint32_t output_idx_; - uint64_t amount_; - bool locked_; - TokenID token_id_; + OutPoint outpoint_; ///< Producing transaction hash and output index. + uint64_t amount_; ///< Amount carried by the output. + TokenID token_id_; ///< Token identifier carried by the output. + std::string owner_address_; ///< Address that owns or can spend the output. }; } diff --git a/src/account/IGeniusTransactions.cpp b/src/account/IGeniusTransactions.cpp index e27050f27..74ffafb05 100644 --- a/src/account/IGeniusTransactions.cpp +++ b/src/account/IGeniusTransactions.cpp @@ -44,17 +44,16 @@ namespace sgns dag_st.set_signature( std::move( signature ) ); } - bool IGeniusTransactions::CheckHash() + bool IGeniusTransactions::CheckHash() const { - auto signature = dag_st.signature(); - auto hash = dag_st.data_hash(); - dag_st.clear_signature(); - dag_st.clear_data_hash(); + const auto hash = dag_st.data_hash(); + + SGTransaction::DAGStruct dag_copy = dag_st; + dag_copy.clear_signature(); + dag_copy.clear_data_hash(); auto hasher_ = std::make_shared(); - auto calculated_hash = hasher_->blake2b_256( SerializeByteVector() ); - dag_st.set_data_hash( hash ); - dag_st.set_signature( std::move( signature ) ); + auto calculated_hash = hasher_->blake2b_256( SerializeByteVector( dag_copy ) ); return hash == calculated_hash.toReadableString(); } @@ -72,27 +71,28 @@ namespace sgns return signed_vector; } - bool IGeniusTransactions::CheckSignature() + bool IGeniusTransactions::CheckSignature() const { - auto str_signature = dag_st.signature(); - dag_st.clear_signature(); - auto serialized = SerializeByteVector(); - dag_st.set_signature( str_signature ); + auto str_signature = dag_st.signature(); + + SGTransaction::DAGStruct dag_copy = dag_st; + dag_copy.clear_signature(); + auto serialized = SerializeByteVector(dag_copy); return GeniusAccount::VerifySignature( dag_st.source_addr(), str_signature, serialized ); } - bool IGeniusTransactions::CheckDAGSignatureLegacy() + bool IGeniusTransactions::CheckDAGSignatureLegacy() const { auto str_signature = dag_st.signature(); - dag_st.clear_signature(); - auto size = dag_st.ByteSizeLong(); + SGTransaction::DAGStruct dag_copy = dag_st; + dag_copy.clear_signature(); + auto size = dag_copy.ByteSizeLong(); std::vector serialized( size ); - if ( !dag_st.SerializeToArray( serialized.data(), size ) ) + if ( !dag_copy.SerializeToArray( serialized.data(), size ) ) { std::cerr << "Failed to serialize DAG struct\n"; } - dag_st.set_signature( str_signature ); return GeniusAccount::VerifySignature( dag_st.source_addr(), str_signature, serialized ) && CheckHash(); } @@ -102,6 +102,16 @@ namespace sgns return dag_st.data_hash(); } + std::string IGeniusTransactions::GetPreviousHash() const + { + return dag_st.previous_hash(); + } + + std::string IGeniusTransactions::GetUncleHash() const + { + return dag_st.uncle_hash(); + } + std::unordered_set IGeniusTransactions::GetTopics() const { return { GetSrcAddress() }; diff --git a/src/account/IGeniusTransactions.hpp b/src/account/IGeniusTransactions.hpp index d97f70cd8..e1e6365fd 100644 --- a/src/account/IGeniusTransactions.hpp +++ b/src/account/IGeniusTransactions.hpp @@ -30,6 +30,8 @@ namespace sgns class IGeniusTransactions { public: + static constexpr std::string_view GENIUS_CHAIN_ID = "supergenius_chain"; + /** * @brief Alias for the de-serializer method type to be implemented in derived classes */ @@ -57,7 +59,12 @@ namespace sgns return dag; } - virtual std::vector SerializeByteVector() = 0; + virtual std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const = 0; + + std::vector SerializeByteVector() const + { + return SerializeByteVector( dag_st ); + } /** * @brief Returns if transaction supports UTXOs @@ -77,6 +84,15 @@ namespace sgns return std::nullopt; } + /** + * @brief Returns the source chain id for input validation routing + * @return The source chain id + */ + virtual std::string GetChainId() const + { + return std::string( GENIUS_CHAIN_ID ); + } + virtual std::string GetTransactionSpecificPath() const = 0; static std::string GetTransactionFullPath( const std::string &tx_hash ) @@ -99,21 +115,28 @@ namespace sgns return dag_st.source_addr(); } - std::string GetHash() const; + [[nodiscard]] std::string GetHash() const; + [[nodiscard]] std::string GetPreviousHash() const; + [[nodiscard]] std::string GetUncleHash() const; uint64_t GetTimestamp() const { return dag_st.timestamp(); } + uint64_t GetNonce() const + { + return dag_st.nonce(); + } + virtual std::unordered_set GetTopics() const; void FillHash(); - bool CheckHash(); + bool CheckHash() const; std::vector MakeSignature( GeniusAccount &account ); - bool CheckSignature(); - bool CheckDAGSignatureLegacy(); + bool CheckSignature() const; + bool CheckDAGSignatureLegacy() const; SGTransaction::DAGStruct dag_st; diff --git a/src/account/InputValidators.cpp b/src/account/InputValidators.cpp new file mode 100644 index 000000000..812ba0fc3 --- /dev/null +++ b/src/account/InputValidators.cpp @@ -0,0 +1,441 @@ +/** + * @file InputValidators.cpp + * @brief Input validation strategies for source chains + * @date 2026-03-23 + */ +#include "account/InputValidators.hpp" + +#include +#include +#include + +#include "account/GeniusAccount.hpp" +#include "account/UTXOMerkle.hpp" + +namespace sgns +{ + namespace + { + using utxo_merkle::HashLeaf; + using utxo_merkle::HashNode; + using utxo_merkle::OutPointKey; + using utxo_merkle::AppendUInt32BE; + using utxo_merkle::AppendUInt64BE; + using utxo_merkle::ReadUInt32BE; + using utxo_merkle::ReadUInt64BE; + + std::vector SerializeOutpointLeafPayload( const base::Hash256 &txid_hash, uint32_t output_index ) + { + std::vector payload; + payload.reserve( 32 + 4 ); + payload.insert( payload.end(), txid_hash.begin(), txid_hash.end() ); + AppendUInt32BE( payload, output_index ); + return payload; + } + + std::vector SerializeOutputLeafPayload( const base::Hash256 &txid_hash, + uint32_t output_index, + const std::string &owner_address, + gsl::span token_bytes, + uint64_t amount ) + { + std::vector payload; + payload.reserve( 32 + 4 + 4 + owner_address.size() + token_bytes.size() + 8 ); + payload.insert( payload.end(), txid_hash.begin(), txid_hash.end() ); + AppendUInt32BE( payload, output_index ); + AppendUInt32BE( payload, static_cast( owner_address.size() ) ); + payload.insert( payload.end(), owner_address.begin(), owner_address.end() ); + payload.insert( payload.end(), token_bytes.begin(), token_bytes.end() ); + AppendUInt64BE( payload, amount ); + return payload; + } + + base::Hash256 ComputeMerkleRootFromPayloads( std::vector> payloads ) + { + if ( payloads.empty() ) + { + return utxo_merkle::EmptyUTXOMerkleRoot(); + } + + std::sort( payloads.begin(), payloads.end() ); + std::vector leaf_hashes; + leaf_hashes.reserve( payloads.size() ); + for ( const auto &payload : payloads ) + { + leaf_hashes.push_back( HashLeaf( payload ) ); + } + return utxo_merkle::ComputeMerkleRootFromLeafHashes( std::move( leaf_hashes ) ); + } + + } // namespace + + bool GeniusInputValidator::ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const + { + if ( params.first.empty() || params.second.empty() ) + { + return false; + } + + return utxo_manager.VerifyParameters( params, address ); + } + + bool GeniusInputValidator::ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const + { + if ( !tx || !blockchain ) + { + return false; + } + if ( !subject.has_nonce() || !subject.nonce().has_utxo_witness() || !subject.nonce().has_utxo_commitment() ) + { + return false; + } + + const auto &inputs = params.first; + const auto &outputs = params.second; + if ( inputs.empty() || outputs.empty() ) + { + return false; + } + const auto tx_hash_result = base::Hash256::fromReadableString( tx->GetHash() ); + if ( tx_hash_result.has_error() ) + { + return false; + } + const auto &commitment = subject.nonce().utxo_commitment(); + if ( commitment.consumed_outpoints_root().size() != base::Hash256::size() || + commitment.produced_outputs_root().size() != base::Hash256::size() ) + { + return false; + } + auto consumed_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( commitment.consumed_outpoints_root().data() ) ), + commitment.consumed_outpoints_root().size() ) ); + if ( consumed_root_result.has_error() ) + { + return false; + } + auto produced_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( commitment.produced_outputs_root().data() ) ), + commitment.produced_outputs_root().size() ) ); + if ( produced_root_result.has_error() ) + { + return false; + } + + if ( commitment.consumed_outpoints_size() != static_cast( inputs.size() ) || + commitment.produced_outputs_size() != static_cast( outputs.size() ) ) + { + return false; + } + + std::unordered_set commitment_outpoints; + commitment_outpoints.reserve( commitment.consumed_outpoints_size() ); + std::vector> committed_consumed_payloads; + committed_consumed_payloads.reserve( commitment.consumed_outpoints_size() ); + for ( const auto &committed_outpoint : commitment.consumed_outpoints() ) + { + auto out_hash_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( committed_outpoint.tx_id_hash().data() ) ), + committed_outpoint.tx_id_hash().size() ) ); + if ( out_hash_result.has_error() ) + { + return false; + } + if ( !commitment_outpoints.emplace( OutPointKey( out_hash_result.value(), committed_outpoint.output_index() ) ).second ) + { + return false; + } + committed_consumed_payloads.push_back( + SerializeOutpointLeafPayload( out_hash_result.value(), committed_outpoint.output_index() ) ); + } + + std::vector> tx_consumed_payloads; + tx_consumed_payloads.reserve( inputs.size() ); + for ( const auto &input : inputs ) + { + tx_consumed_payloads.push_back( SerializeOutpointLeafPayload( input.txid_hash_, input.output_idx_ ) ); + } + + if ( ComputeMerkleRootFromPayloads( committed_consumed_payloads ) != consumed_root_result.value() || + ComputeMerkleRootFromPayloads( tx_consumed_payloads ) != consumed_root_result.value() ) + { + return false; + } + + std::unordered_set commitment_outputs; + commitment_outputs.reserve( commitment.produced_outputs_size() ); + std::vector> committed_produced_payloads; + committed_produced_payloads.reserve( commitment.produced_outputs_size() ); + for ( const auto &committed_output : commitment.produced_outputs() ) + { + auto out_hash_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( committed_output.tx_id_hash().data() ) ), + committed_output.tx_id_hash().size() ) ); + if ( out_hash_result.has_error() ) + { + return false; + } + auto payload = SerializeOutputLeafPayload( + out_hash_result.value(), + committed_output.output_index(), + committed_output.owner_address(), + gsl::span( reinterpret_cast( committed_output.token_id().data() ), + committed_output.token_id().size() ), + committed_output.amount() ); + const std::string payload_key( reinterpret_cast( payload.data() ), payload.size() ); + if ( !commitment_outputs.emplace( payload_key ).second ) + { + return false; + } + committed_produced_payloads.push_back( std::move( payload ) ); + } + + std::unordered_set tx_outputs; + tx_outputs.reserve( outputs.size() ); + std::vector> tx_produced_payloads; + tx_produced_payloads.reserve( outputs.size() ); + for ( size_t i = 0; i < outputs.size(); ++i ) + { + const auto &output = outputs[i]; + const auto &token_bytes = output.token_id.bytes(); + auto payload = SerializeOutputLeafPayload( tx_hash_result.value(), + static_cast( i ), + output.dest_address, + gsl::span( token_bytes.data(), token_bytes.size() ), + output.encrypted_amount ); + tx_outputs.emplace( reinterpret_cast( payload.data() ), payload.size() ); + tx_produced_payloads.push_back( std::move( payload ) ); + } + + if ( tx_outputs != commitment_outputs || + ComputeMerkleRootFromPayloads( committed_produced_payloads ) != produced_root_result.value() || + ComputeMerkleRootFromPayloads( tx_produced_payloads ) != produced_root_result.value() ) + { + return false; + } + + std::unordered_map proofs; + proofs.reserve( subject.nonce().utxo_witness().consumed_inputs_size() ); + for ( const auto &proof : subject.nonce().utxo_witness().consumed_inputs() ) + { + auto hash_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( proof.tx_id_hash().data() ) ), + proof.tx_id_hash().size() ) ); + if ( hash_result.has_error() ) + { + return false; + } + if ( !proofs.emplace( OutPointKey( hash_result.value(), proof.output_index() ), &proof ).second ) + { + return false; + } + } + + const auto add_amount = []( std::unordered_map &bucket, + const std::string &token_key, + uint64_t amount ) -> bool + { + auto &total = bucket[token_key]; + if ( amount > std::numeric_limits::max() - total ) + { + return false; + } + total += amount; + return true; + }; + + std::unordered_set seen_inputs; + std::unordered_map input_amounts_by_token; + std::unordered_map output_amounts_by_token; + seen_inputs.reserve( inputs.size() ); + input_amounts_by_token.reserve( inputs.size() ); + output_amounts_by_token.reserve( outputs.size() ); + + for ( const auto &input : inputs ) + { + if ( !GeniusAccount::VerifySignature( + tx->GetSrcAddress(), + std::string_view( reinterpret_cast( input.signature_.data() ), + input.signature_.size() ), + input.SerializeForSigning() ) ) + { + return false; + } + + auto proof_it = proofs.find( OutPointKey( input.txid_hash_, input.output_idx_ ) ); + if ( proof_it == proofs.end() ) + { + return false; + } + + const auto outpoint_key = OutPointKey( input.txid_hash_, input.output_idx_ ); + if ( !seen_inputs.insert( outpoint_key ).second ) + { + return false; + } + const auto &proof = *proof_it->second; + + const auto &payload = proof.leaf_payload(); + if ( payload.size() < 32 + 4 + 4 + 32 + 8 ) + { + return false; + } + + auto payload_hash_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( payload.data() ) ), 32 ) ); + if ( payload_hash_result.has_error() || payload_hash_result.value() != input.txid_hash_ ) + { + return false; + } + const auto payload_output_idx = ReadUInt32BE( reinterpret_cast( payload.data() ) + 32 ); + if ( payload_output_idx != input.output_idx_ ) + { + return false; + } + const auto owner_len = ReadUInt32BE( reinterpret_cast( payload.data() ) + 36 ); + if ( payload.size() < 40 + owner_len + 32 + 8 ) + { + return false; + } + const std::string payload_owner( payload.data() + 40, payload.data() + 40 + owner_len ); + const bool delegated_escrow_spend = + payload_owner != tx->GetSrcAddress() && tx->GetType() == "transfer" && input.output_idx_ == 0 && + utxo_address::IsEscrowLockAddress( payload_owner ) && tx->GetUncleHash() == payload_owner; + if ( payload_owner != tx->GetSrcAddress() && !delegated_escrow_spend ) + { + return false; + } + const size_t token_offset = 40 + owner_len; + const size_t amount_offset = token_offset + 32; + const std::string token_key( payload.data() + token_offset, payload.data() + amount_offset ); + const uint64_t input_amount = ReadUInt64BE( reinterpret_cast( payload.data() ) + + amount_offset ); + if ( !add_amount( input_amounts_by_token, token_key, input_amount ) ) + { + return false; + } + + std::vector payload_vec( payload.begin(), payload.end() ); + + auto producer_cert_result = blockchain->GetCertificateBySubjectHash( input.txid_hash_.toReadableString() ); + if ( producer_cert_result.has_error() ) + { + return false; + } + const auto &producer_subject = producer_cert_result.value().proposal().subject(); + if ( !producer_subject.has_nonce() || !producer_subject.nonce().has_utxo_commitment() ) + { + return false; + } + const auto &producer_commitment = producer_subject.nonce().utxo_commitment(); + if ( producer_commitment.produced_outputs_root().size() != base::Hash256::size() ) + { + return false; + } + auto produced_root_result = base::Hash256::fromSpan( gsl::span( + reinterpret_cast( const_cast( producer_commitment.produced_outputs_root().data() ) ), + producer_commitment.produced_outputs_root().size() ) ); + if ( produced_root_result.has_error() ) + { + return false; + } + + auto produced_hash = HashLeaf( payload_vec ); + for ( const auto &step : proof.produced_branch() ) + { + auto sibling_hash_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( step.sibling_hash().data() ) ), + step.sibling_hash().size() ) ); + if ( sibling_hash_result.has_error() ) + { + return false; + } + + if ( step.is_left_sibling() ) + { + produced_hash = HashNode( sibling_hash_result.value(), produced_hash ); + } + else + { + produced_hash = HashNode( produced_hash, sibling_hash_result.value() ); + } + } + + if ( produced_hash != produced_root_result.value() ) + { + return false; + } + } + + for ( const auto &output : outputs ) + { + const auto &token_bytes = output.token_id.bytes(); + const std::string token_key( reinterpret_cast( token_bytes.data() ), token_bytes.size() ); + if ( !add_amount( output_amounts_by_token, token_key, output.encrypted_amount ) ) + { + return false; + } + } + + if ( input_amounts_by_token != output_amounts_by_token ) + { + return false; + } + + return true; + } + + bool PublicChainInputValidator::ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const + { + (void)address; + (void)utxo_manager; + // Public-chain claims are not validated against local UTXO ownership. + // We still require input references and minted outputs to be explicit. + return !params.first.empty() && !params.second.empty(); + } + + bool PublicChainInputValidator::ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const + { + (void)subject; + (void)blockchain; + if ( !tx || params.first.empty() || params.second.empty() ) + { + return false; + } + + // Feed the public-chain verification with the explicit input hash. + // If we had to fallback to an empty Hash256 input, use uncle_hash as external source reference. + std::string source_reference; + const auto &input_tx_hash = params.first.front().txid_hash_; + if ( input_tx_hash != base::Hash256{} ) + { + source_reference = input_tx_hash.toReadableString(); + } + else + { + source_reference = tx->GetUncleHash(); + } + + return VerifyPublicChainSmartContract( tx, source_reference ); + } + + bool PublicChainInputValidator::VerifyPublicChainSmartContract( const std::shared_ptr &tx, + const std::string &source_reference ) const + { + (void)tx; + (void)source_reference; + // Placeholder for real burn/finality/contract validation. + // Empty source_reference is accepted for bootstrap/test mints where no external source hash is provided yet. + return true; + } +} // namespace sgns diff --git a/src/account/InputValidators.hpp b/src/account/InputValidators.hpp new file mode 100644 index 000000000..71ce85192 --- /dev/null +++ b/src/account/InputValidators.hpp @@ -0,0 +1,156 @@ +/** + * @file InputValidators.hpp + * @brief Input validation strategies for different source chains + * @date 2026-03-23 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#pragma once + +#include +#include + +#include "account/IGeniusTransactions.hpp" +#include "account/UTXOManager.hpp" +#include "blockchain/Blockchain.hpp" +#include "blockchain/impl/proto/Consensus.pb.h" +#include "base/blob.hpp" + +namespace sgns +{ + /** + * @brief Strategy interface for validating transaction inputs and their witness data. + */ + class IInputValidator + { + public: + /** + * @brief Destroys the input validator. + */ + virtual ~IInputValidator() = default; + + /** + * @brief Validates ownership and structure of the supplied UTXO parameters. + * @param[in] params UTXO inputs and outputs carried by the transaction. + * @param[in] address Source address expected to own or authorize the inputs. + * @param[in] utxo_manager Local UTXO manager used for ownership and signature checks when required. + * @return True when the parameters are structurally valid for this source chain. + */ + virtual bool ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const = 0; + + /** + * @brief Validates the chain-specific witness data associated with a transaction input set. + * @param[in] subject Consensus subject that carries nonce, witness, and UTXO commitment data. + * @param[in] tx Transaction whose inputs and outputs are being validated. + * @param[in] params UTXO inputs and outputs carried by @p tx. + * @param[in] blockchain Blockchain service used to resolve producer certificates when required. + * @return True when the witness proves that @p params are valid for @p tx. + */ + virtual bool ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const = 0; + + /** + * @brief Indicates whether this validator requires consensus-provided UTXO data. + * @return True when the validator needs UTXO witness and commitment data from consensus. + */ + virtual bool RequiresConsensusUTXOData() const = 0; + }; + + /** + * @brief Validator for native Genius-chain transactions. + */ + class GeniusInputValidator final : public IInputValidator + { + public: + /** + * @brief Validates UTXO ownership and signatures for Genius-native inputs. + * @param[in] params UTXO inputs and outputs carried by the transaction. + * @param[in] address Source address expected to own or authorize the inputs. + * @param[in] utxo_manager Local UTXO manager used to verify the inputs. + * @return True when both input and output lists are non-empty and @p utxo_manager accepts the parameters. + */ + bool ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const override; + + /** + * @brief Validates witness data against Genius-chain consensus state. + * + * Checks the transaction hash, UTXO commitment roots, consumed input proofs, + * input signatures, ownership, duplicate inputs, and per-token input/output balance. + * + * @param[in] subject Consensus subject containing the UTXO witness and commitment. + * @param[in] tx Genius-chain transaction being validated. + * @param[in] params UTXO inputs and outputs carried by @p tx. + * @param[in] blockchain Blockchain service used to resolve producer certificates. + * @return True when the witness and transaction UTXO parameters are consistent. + */ + bool ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const override; + + /** + * @brief Genius-native validation requires consensus UTXO context. + * @return Always true. + */ + bool RequiresConsensusUTXOData() const override + { + return true; + } + }; + + /** + * @brief Validator for transactions that reference external public-chain proofs. + */ + class PublicChainInputValidator final : public IInputValidator + { + public: + /** + * @brief Validates local UTXO structure for externally sourced claims. + * @param[in] params UTXO inputs and outputs carried by the transaction. + * @param[in] address Source address; ignored for public-chain validation. + * @param[in] utxo_manager Local UTXO manager; ignored for public-chain validation. + * @return True when both input and output lists are non-empty. + */ + bool ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const override; + + /** + * @brief Validates the external witness data supplied by consensus. + * @param[in] subject Consensus subject; currently unused by public-chain validation. + * @param[in] tx Transaction that references the public-chain source event. + * @param[in] params UTXO inputs and outputs carrying the source reference and minted outputs. + * @param[in] blockchain Blockchain service; currently unused by public-chain validation. + * @return True when @p tx is present, @p params are non-empty, and the source reference verification succeeds. + */ + bool ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const override; + + /** + * @brief Public-chain validation does not require consensus UTXO payloads. + * @return Always false. + */ + bool RequiresConsensusUTXOData() const override + { + return false; + } + + private: + /** + * @brief Verifies that the referenced public-chain smart-contract event matches the transaction. + * @param[in] tx Transaction claiming the public-chain event. + * @param[in] source_reference Public-chain transaction hash or external source reference. + * @return True when the external source reference is accepted for @p tx. + * @note This is currently a placeholder that accepts all references, including empty bootstrap/test references. + */ + bool VerifyPublicChainSmartContract( const std::shared_ptr &tx, + const std::string &source_reference ) const; + }; +} // namespace sgns diff --git a/src/account/Migration0_2_0To1_0_0.cpp b/src/account/Migration0_2_0To1_0_0.cpp index f68d331ca..df017a9d5 100644 --- a/src/account/Migration0_2_0To1_0_0.cpp +++ b/src/account/Migration0_2_0To1_0_0.cpp @@ -13,7 +13,6 @@ #include #include "account/TransactionManager.hpp" #include "account/TransferTransaction.hpp" -#include "account/EscrowReleaseTransaction.hpp" #include "proof/IBasicProof.hpp" #include "MigrationManager.hpp" #include "base/sgns_version.hpp" @@ -321,11 +320,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } sgns::crdt::GlobalDB::Buffer data_transaction; data_transaction.put( tx->SerializeByteVector() ); diff --git a/src/account/Migration1_0_0To3_4_0.cpp b/src/account/Migration1_0_0To3_4_0.cpp index 9892d6b7d..dd61e2127 100644 --- a/src/account/Migration1_0_0To3_4_0.cpp +++ b/src/account/Migration1_0_0To3_4_0.cpp @@ -9,7 +9,6 @@ #include #include "MigrationManager.hpp" -#include "EscrowReleaseTransaction.hpp" #include "TransactionManager.hpp" #include "TransferTransaction.hpp" #include "base/sgns_version.hpp" @@ -161,11 +160,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } sgns::crdt::GlobalDB::Buffer data_transaction; data_transaction.put( tx->SerializeByteVector() ); diff --git a/src/account/Migration3_4_0To3_5_0.cpp b/src/account/Migration3_4_0To3_5_0.cpp index f6989c4a0..cf8a7b6e5 100644 --- a/src/account/Migration3_4_0To3_5_0.cpp +++ b/src/account/Migration3_4_0To3_5_0.cpp @@ -7,7 +7,6 @@ #include "Migration3_4_0To3_5_0.hpp" -#include "account/EscrowReleaseTransaction.hpp" #include "account/GeniusAccount.hpp" #include "account/MintTransaction.hpp" #include "account/TransactionManager.hpp" @@ -132,6 +131,7 @@ namespace sgns blockchain_ = Blockchain::New( db_3_5_0_, account_, + pubSub_, [wptr( weak_from_this() )]( outcome::result result ) { if ( auto strong = wptr.lock() ) @@ -274,11 +274,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( record.tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } ++migrated_count; if ( migrated_count >= BATCH_SIZE ) diff --git a/src/account/Migration3_4_0To3_5_0.hpp b/src/account/Migration3_4_0To3_5_0.hpp index f5b00caa0..589850d6f 100644 --- a/src/account/Migration3_4_0To3_5_0.hpp +++ b/src/account/Migration3_4_0To3_5_0.hpp @@ -1,6 +1,6 @@ /** * @file Migration3_4_0To3_5_0.hpp - * @brief + * @brief Migration step that upgrades account data from schema version 3.4.0 to 3.5.0. * @date 2025-11-11 * @author Henrique A. Klein (hklein@gnus.ai) */ @@ -21,76 +21,116 @@ namespace sgns class GeniusAccount; /** - * @brief Migration step for version 1.0.0 to 3.4.0. - * Changes the full node topic from CRDT heads + * @brief Migration step for version 3.4.0 to 3.5.0. + * Updates persisted data required by the 3.5.0 node layout. */ class Migration3_4_0To3_5_0 : public IMigrationStep, public std::enable_shared_from_this { public: + /** + * @brief Constructs the migration step with the services required to read and write both database versions. + * @param[in] ioContext Shared IO context used by GlobalDB and migration services. + * @param[in] pubSub PubSub service used by the legacy and target GlobalDB instances. + * @param[in] graphsync GraphSync network used for CRDT data exchange. + * @param[in] scheduler libp2p scheduler used by GraphSync. + * @param[in] generator Request ID generator used by GraphSync. + * @param[in] writeBasePath Base path containing versioned node database directories. + * @param[in] base58key Base58 node key suffix used to locate the legacy and target databases. + * @param[in] account Local account used to configure target storage and sign filler transactions. + */ Migration3_4_0To3_5_0( std::shared_ptr ioContext, std::shared_ptr pubSub, std::shared_ptr graphsync, - std::shared_ptr scheduler, + std::shared_ptr scheduler, std::shared_ptr generator, std::string writeBasePath, std::string base58key, std::shared_ptr account ); + + /** + * @brief Destroys the migration step. + */ ~Migration3_4_0To3_5_0() override; /** - * @brief Get the source version for this step. - * @return std::string "1.0.0" + * @brief Returns the source schema version handled by this step. + * @return Source version string, "3.4.0". */ std::string FromVersion() const override; /** - * @brief Get the target version for this step. - * @return std::string "3.4.0" + * @brief Returns the target schema version produced by this step. + * @return Target version string, "3.5.0". */ std::string ToVersion() const override; + /** + * @brief Initializes migration resources before the step is applied. + * @return Success after opening the legacy database and, when present, the target database. + */ outcome::result Init() override; /** - * @brief Check if this migration should run. - * @return outcome::result true if migration should run; false to skip. On error, returns failure. + * @brief Determines whether the 3.4.0 to 3.5.0 migration must run. + * @return True when the target database is older than 3.5.0 or has no version marker; false otherwise. */ outcome::result IsRequired() const override; /** - * @brief Apply the migration: initialize legacy DBs and migrate data. - * @return outcome::result success on completion; failure on error. + * @brief Applies the migration and writes the upgraded transaction state. + * + * Configures account storage against the target database, starts the target blockchain, + * migrates transactions for all monitored networks, synthesizes zero-value mint + * transactions for missing local nonces, commits sync topics in batches, and records + * the target version on success. Transactions that cannot be fetched or validated are skipped. + * + * @return Success when the migration is complete or no legacy database exists; failure on database, + * blockchain initialization, serialization, or commit errors. */ outcome::result Apply() override; + /** + * @brief Releases resources allocated during migration. + * @return Success after deconfiguring account storage and releasing database and blockchain references. + */ outcome::result ShutDown() override; private: + /** + * @brief Internal status used while waiting for blockchain-backed migration tasks. + */ enum class Status { - ST_INIT = 0, - ST_ERROR, - ST_SUCCESS, + ST_INIT = 0, ///< Blockchain initialization is pending. + ST_ERROR, ///< Blockchain initialization failed. + ST_SUCCESS, ///< Blockchain initialization completed successfully. }; + /** - * @brief Open a legacy GlobalDB from 1.0.0 - * @return Opened DB, nullptr if absent, or error. + * @brief Opens the legacy 3.4.0 database view. + * @return Legacy GlobalDB, nullptr when the legacy database path does not exist, or an initialization error. */ - outcome::result> InitLegacyDb(); - outcome::result> InitTargetDb(); - std::shared_ptr ioContext_; ///< IO context for DB I/O. - std::shared_ptr pubSub_; ///< PubSub instance for legacy DB. + outcome::result> InitLegacyDb(); + + /** + * @brief Opens the target 3.5.0 database view. + * @return Target GlobalDB, or an initialization error. + */ + outcome::result> InitTargetDb(); + + std::shared_ptr ioContext_; ///< IO context for GlobalDB services. + std::shared_ptr pubSub_; ///< PubSub service for CRDT sync. std::shared_ptr graphsync_; ///< GraphSync network. - std::shared_ptr scheduler_; ///< libp2p scheduler. - std::shared_ptr generator_; ///< Request ID generator. - std::string writeBasePath_; ///< Base path for writing DB files. - std::string base58key_; ///< Key to build legacy paths. + std::shared_ptr scheduler_; ///< libp2p scheduler. + std::shared_ptr generator_; ///< GraphSync request ID generator. + std::string writeBasePath_; ///< Base path for versioned DBs. + std::string base58key_; ///< Node key suffix for DB paths. base::Logger logger_ = base::createLogger( "MigrationStep" ); ///< Logger for this step. - std::shared_ptr db_3_5_0_; ///< Target GlobalDB. - std::shared_ptr db_3_4_0_; ///< Legacy DB - std::shared_ptr blockchain_; - std::shared_ptr account_; - std::atomic blockchain_status_{ Status::ST_INIT }; + std::shared_ptr db_3_5_0_; ///< Target 3.5.0 database. + std::shared_ptr db_3_4_0_; ///< Legacy 3.4.0 database. + std::shared_ptr blockchain_; ///< Blockchain instance used during migration. + std::shared_ptr account_; ///< Local account being migrated. + std::atomic blockchain_status_{ Status::ST_INIT }; ///< Async blockchain startup status. }; } diff --git a/src/account/Migration3_5_0To3_6_0.cpp b/src/account/Migration3_5_0To3_6_0.cpp index 174c414c9..badc68a6b 100644 --- a/src/account/Migration3_5_0To3_6_0.cpp +++ b/src/account/Migration3_5_0To3_6_0.cpp @@ -3,7 +3,6 @@ #include "account/MigrationManager.hpp" #include "account/TransactionManager.hpp" #include "account/TransferTransaction.hpp" -#include "account/EscrowReleaseTransaction.hpp" #include "blockchain/Blockchain.hpp" #include "blockchain/ValidatorRegistry.hpp" #include "base/sgns_version.hpp" @@ -95,7 +94,7 @@ namespace sgns logger_->info( "Starting migration from {} to {}", FromVersion(), ToVersion() ); - BOOST_OUTCOME_TRY( blockchain::ValidatorRegistry::MigrateCids( db_3_5_1_, db_3_6_0_ ) ); + BOOST_OUTCOME_TRY( ValidatorRegistry::MigrateCids( db_3_5_1_, db_3_6_0_ ) ); BOOST_OUTCOME_TRY( Blockchain::MigrateCids( db_3_5_1_, db_3_6_0_ ) ); auto crdt_transaction_ = db_3_6_0_->BeginTransaction(); @@ -163,11 +162,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } ++migrated_count; if ( migrated_count >= BATCH_SIZE ) diff --git a/src/account/Migration3_5_0To3_6_0.hpp b/src/account/Migration3_5_0To3_6_0.hpp index 63ce9c715..b94747298 100644 --- a/src/account/Migration3_5_0To3_6_0.hpp +++ b/src/account/Migration3_5_0To3_6_0.hpp @@ -1,3 +1,9 @@ +/** + * @file Migration3_5_0To3_6_0.hpp + * @brief Migration step that upgrades account data from schema version 3.5.1 to 3.6.0. + * @date 2026-01-22 + * @author Henrique A. Klein (hklein@gnus.ai) + */ #pragma once #include "IMigrationStep.hpp" @@ -7,39 +13,89 @@ namespace sgns { + /** + * @brief Executes the storage migration from database version 3.5.1 to 3.6.0. + */ class Migration3_5_0To3_6_0 : public IMigrationStep, public std::enable_shared_from_this { public: + /** + * @brief Constructs the migration step with the services required to read and write both database versions. + * @param[in] ioContext Shared IO context used by GlobalDB services. + * @param[in] pubSub PubSub service used by the legacy and target GlobalDB instances. + * @param[in] graphsync GraphSync network used for CRDT data exchange. + * @param[in] scheduler libp2p scheduler used by GraphSync. + * @param[in] generator Request ID generator used by GraphSync. + * @param[in] writeBasePath Base path containing versioned node database directories. + * @param[in] base58key Base58 node key suffix used to locate the legacy and target databases. + */ Migration3_5_0To3_6_0( std::shared_ptr ioContext, std::shared_ptr pubSub, std::shared_ptr graphsync, - std::shared_ptr scheduler, + std::shared_ptr scheduler, std::shared_ptr generator, std::string writeBasePath, std::string base58key ); + /** + * @brief Returns the source schema version handled by this step. + * @return Source version string, "3.5.1". + */ std::string FromVersion() const override; + /** + * @brief Returns the target schema version produced by this step. + * @return Target version string, "3.6.0". + */ std::string ToVersion() const override; + /** + * @brief Initializes migration resources before the step is applied. + * @return Success after opening the legacy database and, when present, the target database. + */ outcome::result Init() override; + /** + * @brief Applies the migration logic and persists the upgraded data. + * + * Migrates validator registry CIDs, blockchain CIDs, transaction records for all monitored + * networks, transaction sync topics, and the target database version marker. + * + * @return Success when the migration is complete or no legacy database exists; failure on database, + * transaction fetch, serialization, or commit errors. + */ outcome::result Apply() override; + /** + * @brief Releases any temporary migration resources. + * @return Success after releasing legacy and target database references. + */ outcome::result ShutDown() override; + /** + * @brief Determines whether the migration needs to run for the current node state. + * @return True when the target database is older than 3.6.0 or has no version marker; false otherwise. + */ outcome::result IsRequired() const override; private: + /** + * @brief Opens the legacy 3.5.1 database view. + * @return Legacy GlobalDB, nullptr when the legacy database path does not exist, or an initialization error. + */ outcome::result> InitLegacyDb() const; + /** + * @brief Opens the target 3.6.0 database view. + * @return Target GlobalDB, or an initialization error. + */ outcome::result> InitTargetDb() const; - base::Logger logger_ = base::createLogger( "MigrationStep" ); + base::Logger logger_ = base::createLogger( "MigrationStep" ); ///< Logger for migration progress and errors. - std::shared_ptr ioContext_; - std::shared_ptr pubSub_; - std::shared_ptr graphsync_; - std::shared_ptr scheduler_; - std::shared_ptr generator_; - std::string writeBasePath_; - std::string base58key_; + std::shared_ptr ioContext_; ///< IO context for GlobalDB services. + std::shared_ptr pubSub_; ///< PubSub service for CRDT sync. + std::shared_ptr graphsync_; ///< GraphSync network. + std::shared_ptr scheduler_; ///< libp2p scheduler. + std::shared_ptr generator_; ///< GraphSync request ID generator. + std::string writeBasePath_; ///< Base path for versioned DBs. + std::string base58key_; ///< Node key suffix for DB paths. - std::shared_ptr db_3_5_1_; - std::shared_ptr db_3_6_0_; + std::shared_ptr db_3_5_1_; ///< Legacy 3.5.1 database. + std::shared_ptr db_3_6_0_; ///< Target 3.6.0 database. }; } diff --git a/src/account/Migration3_6_0To3_7_0.cpp b/src/account/Migration3_6_0To3_7_0.cpp new file mode 100644 index 000000000..91ba652d5 --- /dev/null +++ b/src/account/Migration3_6_0To3_7_0.cpp @@ -0,0 +1,587 @@ +#include "Migration3_6_0To3_7_0.hpp" + +#include "account/MigrationAllowList.hpp" +#include "account/MigrationManager.hpp" +#include "account/MintTransaction.hpp" +#include "account/TransactionManager.hpp" +#include "account/proto/SGTransaction.pb.h" +#include "base/sgns_version.hpp" +#include "blockchain/Blockchain.hpp" +#include "crypto/hasher/hasher_impl.hpp" +#include "storage/database_error.hpp" + +#include +#include +#include +#include + +namespace sgns +{ + namespace + { + constexpr std::string_view kLegacyUTXOPrefix = "/utxo/"; + + struct LegacyProducedOutput + { + std::string owner_address; + uint64_t amount; + }; + + std::optional ParseLegacyUTXOOwnerAddress( std::string_view key ) + { + if ( key.substr( 0, kLegacyUTXOPrefix.size() ) != kLegacyUTXOPrefix ) + { + return std::nullopt; + } + + const auto address = key.substr( kLegacyUTXOPrefix.size() ); + if ( address.empty() || address.find( '/' ) != std::string_view::npos ) + { + return std::nullopt; + } + + return std::string( address ); + } + + std::string MakeLegacyOutPointKey( std::string_view tx_hash, uint32_t output_idx ) + { + return std::string( tx_hash ) + ":" + std::to_string( output_idx ); + } + + bool IsMigratableBalanceAddress( std::string_view address ) + { + return utxo_address::IsAccountPublicKeyAddress( address ); + } + } + + Migration3_6_0To3_7_0::Migration3_6_0To3_7_0( + std::shared_ptr ioContext, + std::shared_ptr pubSub, + std::shared_ptr graphsync, + std::shared_ptr scheduler, + std::shared_ptr generator, + std::string writeBasePath, + std::string base58key, + std::shared_ptr account, + bool is_full_node ) : + ioContext_( std::move( ioContext ) ), + pubSub_( std::move( pubSub ) ), + graphsync_( std::move( graphsync ) ), + scheduler_( std::move( scheduler ) ), + generator_( std::move( generator ) ), + writeBasePath_( std::move( writeBasePath ) ), + base58key_( std::move( base58key ) ), + account_( std::move( account ) ), + is_full_node_( is_full_node ) + { + } + + std::string Migration3_6_0To3_7_0::FromVersion() const + { + return "3.6.0"; + } + + std::string Migration3_6_0To3_7_0::ToVersion() const + { + return "3.7.0"; + } + + outcome::result Migration3_6_0To3_7_0::IsRequired() const + { + if ( !db_3_6_0_ ) + { + logger_->info( "Legacy {} DB not found; skipping migration to {}", FromVersion(), ToVersion() ); + return false; + } + + if ( !db_3_7_0_ ) + { + logger_->warn( "Target {} DB not initialized yet", ToVersion() ); + return false; + } + + crdt::GlobalDB::Buffer version_key; + version_key.put( std::string( MigrationManager::VERSION_INFO_KEY ) ); + auto version_ret = db_3_7_0_->GetDataStore()->get( version_key ); + + if ( version_ret.has_error() ) + { + logger_->info( "No version info found in GlobalDB, migration from {} to {} is required", + FromVersion(), + ToVersion() ); + return true; + } + + const auto version_buffer = version_ret.value(); + if ( !IsVersionLessThan( std::string( version_buffer.toString() ), ToVersion() ) ) + { + logger_->info( "GlobalDB already at target version {}, skipping migration", ToVersion() ); + return false; + } + + logger_->info( "GlobalDB at version {}, need to migrate to {}", version_buffer.toString(), ToVersion() ); + return true; + } + + outcome::result Migration3_6_0To3_7_0::Init() + { + BOOST_OUTCOME_TRY( auto legacy_db, InitLegacyDb() ); + db_3_6_0_ = std::move( legacy_db ); + if ( db_3_6_0_ ) + { + BOOST_OUTCOME_TRY( auto new_db, InitTargetDb() ); + db_3_7_0_ = std::move( new_db ); + } + return outcome::success(); + } + + outcome::result Migration3_6_0To3_7_0::Apply() + { + if ( !db_3_6_0_ ) + { + logger_->error( "Legacy {} DB not initialized; nothing to migrate to {}", FromVersion(), ToVersion() ); + return outcome::success(); + } + if ( !db_3_7_0_ ) + { + logger_->error( "Target {} DB not initialized", ToVersion() ); + return outcome::failure( std::errc::no_such_device ); + } + + logger_->info( "Starting migration from {} to {}", FromVersion(), ToVersion() ); + + account_->ConfigureDatabaseDependencies( db_3_7_0_ ); + logger_->debug( "{}: Configured account database dependencies for {}", __func__, ToVersion() ); + + BOOST_OUTCOME_TRY( Blockchain::MigrateCids( db_3_6_0_, db_3_7_0_ ) ); + logger_->debug( "{}: Migrated blockchain CIDs from {} to {}", __func__, FromVersion(), ToVersion() ); + db_3_7_0_->StartCICSync(); + logger_->debug( "{}: Started CID processing for target {}", __func__, ToVersion() ); + + if ( !blockchain_ ) + { + logger_->debug( "{}: Creating blockchain for target {}", __func__, ToVersion() ); + blockchain_ = Blockchain::New( + db_3_7_0_, + account_, + pubSub_, + [wptr( weak_from_this() )]( outcome::result result ) + { + if ( auto strong = wptr.lock() ) + { + if ( result.has_error() ) + { + strong->logger_->error( "Error starting blockchain: {}", result.error().message() ); + strong->blockchain_status_.store( Status::ST_ERROR ); + return; + } + strong->blockchain_status_.store( Status::ST_SUCCESS ); + } + } ); + } + blockchain_status_.store( Status::ST_INIT, std::memory_order_release ); + logger_->debug( "{}: Starting blockchain bootstrap for {}", __func__, ToVersion() ); + + auto retry_duration = std::chrono::minutes( 2 ); + auto retry_interval = std::chrono::seconds( 5 ); + auto retry_start_time = std::chrono::steady_clock::now(); + auto last_log_time = retry_start_time; + outcome::result start_result = outcome::failure( Blockchain::Error::BLOCKCHAIN_NOT_INITIALIZED ); + do + { + start_result = blockchain_->Start(); + if ( start_result.has_error() ) + { + logger_->error( "Error starting blockchain: {}", start_result.error().message() ); + } + + const auto current_time = std::chrono::steady_clock::now(); + if ( current_time - last_log_time >= std::chrono::seconds( 30 ) ) + { + const auto elapsed_seconds = + std::chrono::duration_cast( current_time - retry_start_time ).count(); + logger_->info( "{}: Retrying blockchain start (elapsed: {}s)", __func__, elapsed_seconds ); + last_log_time = current_time; + } + std::this_thread::sleep_for( retry_interval ); + } while ( std::chrono::steady_clock::now() - retry_start_time < retry_duration && start_result.has_error() ); + + const auto timeout_duration = std::chrono::minutes( 4 ); + auto start_time = std::chrono::steady_clock::now(); + last_log_time = start_time; + bool blockchain_succeeded = false; + + while ( std::chrono::steady_clock::now() - start_time < timeout_duration ) + { + const auto current_time = std::chrono::steady_clock::now(); + if ( blockchain_status_.load( std::memory_order_acquire ) != Status::ST_INIT ) + { + if ( blockchain_status_.load( std::memory_order_acquire ) == Status::ST_SUCCESS ) + { + blockchain_succeeded = true; + } + break; + } + if ( current_time - last_log_time >= std::chrono::seconds( 30 ) ) + { + const auto elapsed_seconds = + std::chrono::duration_cast( current_time - start_time ).count(); + logger_->info( "{}: Still waiting for the blockchain to initialize (elapsed: {}s)", + __func__, + elapsed_seconds ); + last_log_time = current_time; + } + std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); + } + if ( !blockchain_succeeded ) + { + return outcome::failure( MigrationManager::Error::BLOCKCHAIN_INIT_FAILED ); + } + + start_time = std::chrono::steady_clock::now(); + blockchain_succeeded = false; + while ( std::chrono::steady_clock::now() - start_time < timeout_duration ) + { + auto genesis_cid_result = blockchain_->GetGenesisCID(); + auto account_creation_cid_result = blockchain_->GetAccountCreationCID(); + if ( genesis_cid_result.has_value() && account_creation_cid_result.has_value() && + blockchain_->validator_registry_initialized_.load( std::memory_order_acquire ) ) + { + blockchain_succeeded = true; + break; + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); + } + if ( !blockchain_succeeded ) + { + logger_->error( "{}: Genesis, Account Creation and/or Validator Registry not initialized", __func__ ); + return outcome::failure( MigrationManager::Error::BLOCKCHAIN_INIT_FAILED ); + } + logger_->debug( "{}: Blockchain bootstrap complete for {}", __func__, ToVersion() ); + + BOOST_OUTCOME_TRY( auto balances, ComputeLegacyBalances() ); + MigrationAllowList allow_list( db_3_7_0_->GetDataStore(), FromVersion() ); + BOOST_OUTCOME_TRY( allow_list.StoreObservedBalances( balances ) ); + logger_->info( "Computed {} legacy address balances for {} -> {} migration", + balances.size(), + FromVersion(), + ToVersion() ); + + BOOST_OUTCOME_TRY( auto observed_balance, allow_list.LoadObservedBalance( account_->GetAddress() ) ); + logger_->debug( "{}: Observed balance lookup for {} returned has_value={} balance={}", + __func__, + account_->GetAddress(), + observed_balance.has_value(), + observed_balance.has_value() ? observed_balance.value() : 0 ); + + if ( observed_balance.has_value() && observed_balance.value() > 0 ) + { + logger_->debug( "{}: Starting CID receiving for target {}", __func__, ToVersion() ); + db_3_7_0_->StartCIDReceiving(); + logger_->debug( "{}: Starting head rebroadcast for target {}", __func__, ToVersion() ); + db_3_7_0_->StartRebroadcastHeads(); + + if ( !transaction_manager_ ) + { + logger_->debug( "{}: Creating transaction manager for migration flow", __func__ ); + transaction_manager_ = TransactionManager::New( db_3_7_0_, + ioContext_, + account_, + std::make_shared(), + blockchain_, + is_full_node_ ); + } + + logger_->debug( "{}: Registering transaction topic names for migration flow", __func__ ); + transaction_manager_->RegisterTopicNames(); + logger_->debug( "{}: Starting transaction manager core for migration flow", __func__ ); + transaction_manager_->StartCore(); + + start_time = std::chrono::steady_clock::now(); + while ( std::chrono::steady_clock::now() - start_time < timeout_duration ) + { + if ( transaction_manager_->GetState() == TransactionManager::State::READY ) + { + break; + } + std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) ); + } + if ( transaction_manager_->GetState() != TransactionManager::State::READY ) + { + logger_->error( "{}: Transaction Manager did not reach READY", __func__ ); + return outcome::failure( MigrationManager::Error::BLOCKCHAIN_INIT_FAILED ); + } + logger_->debug( "{}: Transaction Manager is READY for migration flow", __func__ ); + + const auto token_id = TokenID::FromBytes( { 0x00 } ); + logger_->info( "{}: Submitting migration transaction for {:.8} amount={}", + __func__, + account_->GetAddress(), + observed_balance.value() ); + BOOST_OUTCOME_TRY( auto tx_hash, + transaction_manager_->MigrationFunds( observed_balance.value(), + FromVersion(), + token_id, + account_->GetAddress() ) ); + logger_->debug( "{}: Waiting for migration transaction confirmation tx={}", __func__, tx_hash ); + + auto tx_status = transaction_manager_->WaitForTransactionOutgoing( tx_hash, std::chrono::minutes( 4 ) ); + if ( tx_status != TransactionManager::TransactionStatus::CONFIRMED ) + { + logger_->error( "{}: Migration transaction {} did not confirm. status={}", + __func__, + tx_hash, + static_cast( tx_status ) ); + return outcome::failure( std::errc::timed_out ); + } + } + else + { + logger_->info( "{}: Local account has no observed legacy balance; skipping migration claim", __func__ ); + } + + crdt::GlobalDB::Buffer version_key; + version_key.put( std::string( MigrationManager::VERSION_INFO_KEY ) ); + crdt::GlobalDB::Buffer version_value; + version_value.put( ToVersion() ); + BOOST_OUTCOME_TRY( db_3_7_0_->GetDataStore()->put( version_key, version_value ) ); + logger_->info( "Migration from {} to {} completed successfully", FromVersion(), ToVersion() ); + + return outcome::success(); + } + + outcome::result Migration3_6_0To3_7_0::ShutDown() + { + if ( transaction_manager_ ) + { + transaction_manager_->Stop(); + transaction_manager_.reset(); + } + if ( blockchain_ ) + { + (void)blockchain_->Stop(); + blockchain_.reset(); + } + if ( account_ ) + { + account_->DeconfigureDatabaseDependencies(); + account_->GetUTXOManager().ReleaseStorage(); + } + blockchain_status_.store( Status::ST_INIT, std::memory_order_release ); + db_3_6_0_.reset(); + db_3_7_0_.reset(); + + return outcome::success(); + } + + outcome::result> Migration3_6_0To3_7_0::ComputeLegacyBalances() + const + { + if ( !db_3_6_0_ ) + { + logger_->error( "Legacy {} DB not initialized", FromVersion() ); + return std::errc::state_not_recoverable; + } + + crdt::GlobalDB::Buffer key_buf; + key_buf.put( std::string( kLegacyUTXOPrefix.substr( 0, kLegacyUTXOPrefix.size() - 1 ) ) ); + auto utxo_list = db_3_6_0_->GetDataStore()->query( key_buf ); + if ( utxo_list.has_error() ) + { + if ( utxo_list.error() == storage::DatabaseError::NOT_FOUND ) + { + return std::vector{}; + } + logger_->error( "Failed to query legacy UTXOs: {}", utxo_list.error().message() ); + return utxo_list.error(); + } + + std::vector balances; + balances.reserve( utxo_list.value().size() ); + + for ( const auto &[key, value] : utxo_list.value() ) + { + auto address_opt = ParseLegacyUTXOOwnerAddress( key.toString() ); + if ( !address_opt.has_value() ) + { + logger_->debug( "Skipping non-legacy UTXO key {}", key.toString() ); + continue; + } + + SGTransaction::UTXOList utxos; + if ( !utxos.ParseFromArray( value.data(), value.size() ) ) + { + logger_->error( "Failed to deserialize legacy UTXOs for address {}", address_opt.value() ); + return std::errc::bad_message; + } + + uint64_t balance = 0; + for ( int i = 0; i < utxos.utxos_size(); ++i ) + { + balance += utxos.utxos( i ).amount(); + } + + if ( IsMigratableBalanceAddress( address_opt.value() ) ) + { + balances.emplace_back( std::move( address_opt.value() ), balance ); + } + } + + if ( !balances.empty() ) + { + std::sort( + balances.begin(), + balances.end(), + []( const AddressBalance &lhs, const AddressBalance &rhs ) { return lhs.first < rhs.first; } ); + return balances; + } + + logger_->info( "No legacy UTXO snapshots found in {}; reconstructing balances from migrated transactions", + FromVersion() ); + + std::unordered_map produced_outputs; + std::unordered_set consumed_outpoints; + size_t scanned_transactions = 0; + + for ( auto network_id : TransactionManager::GetMonitoredNetworkIDs() ) + { + const std::string query_path = TransactionManager::GetBlockChainBase( network_id ) + "tx"; + BOOST_OUTCOME_TRY( auto transaction_list, db_3_6_0_->QueryKeyValues( query_path ) ); + + for ( const auto &[key, value] : transaction_list ) + { + ++scanned_transactions; + + BOOST_OUTCOME_TRY( auto tx, TransactionManager::DeSerializeTransaction( value ) ); + if ( tx->GetHash().empty() ) + { + tx->FillHash(); + } + + const auto tx_hash = tx->GetHash(); + if ( tx_hash.empty() ) + { + logger_->error( "Failed to determine hash while reconstructing legacy balance from {}", + key.toString() ); + return std::errc::bad_message; + } + + if ( auto params_opt = tx->GetUTXOParametersOpt() ) + { + const auto &[inputs, outputs] = params_opt.value(); + for ( const auto &input : inputs ) + { + consumed_outpoints.emplace( + MakeLegacyOutPointKey( input.txid_hash_.toReadableString(), input.output_idx_ ) ); + } + + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) + { + produced_outputs[MakeLegacyOutPointKey( tx_hash, i )] = + LegacyProducedOutput{ outputs[i].dest_address, outputs[i].encrypted_amount }; + } + + continue; + } + + if ( auto mint_tx = std::dynamic_pointer_cast( tx ) ) + { + produced_outputs[MakeLegacyOutPointKey( tx_hash, 0 )] = + LegacyProducedOutput{ mint_tx->GetSrcAddress(), mint_tx->GetAmount() }; + } + } + } + + std::unordered_map balance_by_address; + for ( const auto &[outpoint_key, output] : produced_outputs ) + { + if ( consumed_outpoints.find( outpoint_key ) != consumed_outpoints.end() || + !IsMigratableBalanceAddress( output.owner_address ) ) + { + continue; + } + + balance_by_address[output.owner_address] += output.amount; + } + + balances.clear(); + balances.reserve( balance_by_address.size() ); + for ( auto &[address, balance] : balance_by_address ) + { + balances.emplace_back( std::move( address ), balance ); + } + + std::sort( + balances.begin(), + balances.end(), + []( const AddressBalance &lhs, const AddressBalance &rhs ) { return lhs.first < rhs.first; } ); + + logger_->info( "Reconstructed {} legacy balances from {} transactions and {} produced outputs", + balances.size(), + scanned_transactions, + produced_outputs.size() ); + + return balances; + } + + outcome::result> Migration3_6_0To3_7_0::InitLegacyDb() const + { + static constexpr std::string_view GNUS_NETWORK_PATH_3_6_0 = "SuperGNUSNode.Node"; + + auto full_path = writeBasePath_ + std::string( GNUS_NETWORK_PATH_3_6_0 ) + + version::GetNetAndVersionAppendix( 3, 6, version::GetNetworkID() ) + base58key_; + + if ( !std::filesystem::exists( full_path ) ) + { + logger_->info( "Legacy {} DB not found at {}; skipping initialization", FromVersion(), full_path ); + return std::shared_ptr{}; + } + + logger_->debug( "Initializing legacy {} DB at path {}", FromVersion(), full_path ); + + auto maybe_db_3_6_0 = crdt::GlobalDB::New( ioContext_, + full_path, + pubSub_, + crdt::CrdtOptions::DefaultOptions(), + graphsync_, + scheduler_, + generator_ ); + + if ( !maybe_db_3_6_0.has_value() ) + { + logger_->error( "Legacy {} DB error at path {}", FromVersion(), full_path ); + return outcome::failure( boost::system::error_code{} ); + } + + logger_->debug( "Started legacy {} DB at path {}", FromVersion(), full_path ); + return std::move( maybe_db_3_6_0.value() ); + } + + outcome::result> Migration3_6_0To3_7_0::InitTargetDb() const + { + static constexpr std::string_view GNUS_NETWORK_PATH_3_7_0 = "SuperGNUSNode.Node"; + + auto full_path = writeBasePath_ + std::string( GNUS_NETWORK_PATH_3_7_0 ) + + version::GetNetAndVersionAppendix( 3, 7, version::GetNetworkID() ) + base58key_; + + logger_->debug( "Initializing target {} DB at path {}", ToVersion(), full_path ); + + auto maybe_db_3_7_0 = crdt::GlobalDB::New( ioContext_, + full_path, + pubSub_, + crdt::CrdtOptions::DefaultOptions(), + graphsync_, + scheduler_, + generator_ ); + + if ( !maybe_db_3_7_0.has_value() ) + { + logger_->error( "Target {} DB error at path {}", ToVersion(), full_path ); + return outcome::failure( boost::system::error_code{} ); + } + + logger_->debug( "Started target {} DB at path {}", ToVersion(), full_path ); + return std::move( maybe_db_3_7_0.value() ); + } +} diff --git a/src/account/Migration3_6_0To3_7_0.hpp b/src/account/Migration3_6_0To3_7_0.hpp new file mode 100644 index 000000000..ec009c58f --- /dev/null +++ b/src/account/Migration3_6_0To3_7_0.hpp @@ -0,0 +1,143 @@ +/** + * @file Migration3_6_0To3_7_0.hpp + * @brief Migration step that upgrades account and balance data from schema version 3.6.0 to 3.7.0. + * @date 2026-05-06 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#pragma once + +#include "IMigrationStep.hpp" +#include "base/logger.hpp" +#include "blockchain/Blockchain.hpp" +#include "crdt/globaldb/globaldb.hpp" + +#include +#include +#include +#include + +namespace sgns +{ + class GeniusAccount; + class TransactionManager; + + /** + * @brief Executes the 3.6.0 to 3.7.0 migration, including legacy balance recovery. + */ + class Migration3_6_0To3_7_0 : public IMigrationStep, public std::enable_shared_from_this + { + public: + /** + * @brief Address and balance pair recovered from the legacy database. + */ + using AddressBalance = std::pair; + + /** + * @brief Constructs the migration step with access to legacy and target storage plus account services. + * @param[in] ioContext Shared IO context used by GlobalDB and migration services. + * @param[in] pubSub PubSub service used by the migrated GlobalDB instances. + * @param[in] graphsync GraphSync network used for CRDT data exchange. + * @param[in] scheduler libp2p scheduler used by GraphSync. + * @param[in] generator Request ID generator used by GraphSync. + * @param[in] writeBasePath Base path containing versioned node database directories. + * @param[in] base58key Base58 node key suffix used to locate the legacy and target databases. + * @param[in] account Local account used to configure storage and submit the migration claim. + * @param[in] is_full_node Whether the node is running as a full node during migration. + */ + Migration3_6_0To3_7_0( std::shared_ptr ioContext, + std::shared_ptr pubSub, + std::shared_ptr graphsync, + std::shared_ptr scheduler, + std::shared_ptr generator, + std::string writeBasePath, + std::string base58key, + std::shared_ptr account, + bool is_full_node ); + + /** + * @brief Returns the source schema version handled by this step. + * @return Source version string, "3.6.0". + */ + std::string FromVersion() const override; + /** + * @brief Returns the target schema version produced by this step. + * @return Target version string, "3.7.0". + */ + std::string ToVersion() const override; + /** + * @brief Initializes migration resources before applying the step. + * @return Success after opening the legacy database and, when present, the target database. + */ + outcome::result Init() override; + /** + * @brief Applies the migration and writes the upgraded balance state. + * + * Migrates blockchain CIDs, computes legacy balances, persists the migration allow-list, + * submits a local migration claim when this account has a legacy balance, and records + * the target version on success. + * + * @return Success when the migration is complete or no legacy database exists; failure on database, + * blockchain initialization, transaction confirmation, or serialization errors. + */ + outcome::result Apply() override; + /** + * @brief Releases resources allocated during migration. + * @return Success after stopping migration services and releasing database references. + */ + outcome::result ShutDown() override; + /** + * @brief Determines whether the 3.6.0 to 3.7.0 migration must run. + * @return True when the target database is older than 3.7.0 or has no version marker; false otherwise. + */ + outcome::result IsRequired() const override; + + private: + /** + * @brief Internal status used while waiting for blockchain-backed migration tasks. + */ + enum class Status + { + ST_INIT = 0, ///< Blockchain initialization is pending. + ST_ERROR, ///< Blockchain initialization failed. + ST_SUCCESS, ///< Blockchain initialization completed successfully. + }; + + /** + * @brief Opens the legacy 3.6.0 database view. + * @return Legacy GlobalDB, nullptr when the legacy database path does not exist, or an initialization error. + */ + outcome::result> InitLegacyDb() const; + /** + * @brief Opens the target 3.7.0 database view. + * @return Target GlobalDB, or an initialization error. + */ + outcome::result> InitTargetDb() const; + /** + * @brief Scans the legacy dataset and computes the balances to migrate. + * + * Reads legacy UTXO snapshots when present. If no snapshots are found, reconstructs + * balances from migrated transactions by collecting produced and consumed outpoints. + * + * @return Migratable address balances sorted by address, or an error when legacy data cannot be decoded. + */ + outcome::result> ComputeLegacyBalances() const; + + base::Logger logger_ = base::createLogger( "MigrationStep" ); ///< Logger for migration progress and errors. + + std::shared_ptr ioContext_; ///< IO context for GlobalDB services. + std::shared_ptr pubSub_; ///< PubSub service for CRDT sync. + std::shared_ptr graphsync_; ///< GraphSync network. + std::shared_ptr scheduler_; ///< libp2p scheduler. + std::shared_ptr generator_; ///< GraphSync request ID generator. + std::string writeBasePath_; ///< Base path for versioned DBs. + std::string base58key_; ///< Node key suffix for DB paths. + + std::shared_ptr db_3_6_0_; ///< Legacy 3.6.0 database. + std::shared_ptr db_3_7_0_; ///< Target 3.7.0 database. + std::shared_ptr blockchain_; ///< Blockchain instance used during migration. + std::shared_ptr transaction_manager_; ///< Transaction manager used for the migration claim. + std::shared_ptr account_; ///< Local account being migrated. + bool is_full_node_ = false; ///< Whether this node is a full node. + std::atomic blockchain_status_{ Status::ST_INIT }; ///< Async blockchain startup status. + }; +} diff --git a/src/account/MigrationAllowList.cpp b/src/account/MigrationAllowList.cpp new file mode 100644 index 000000000..0032a7a27 --- /dev/null +++ b/src/account/MigrationAllowList.cpp @@ -0,0 +1,171 @@ +#include "account/MigrationAllowList.hpp" + +#include "storage/database_error.hpp" + +#include +#include +#include +#include + +namespace sgns +{ + namespace + { + constexpr std::string_view kPrefixBase = "/migration-allowlist"; + std::atomic_bool g_migration_allowlist_enabled_for_tests{ true }; + } + + MigrationAllowList::MigrationAllowList( std::shared_ptr db, std::string migration_version ) : + db_( std::move( db ) ), + migration_version_( std::move( migration_version ) ), + prefix_( BuildPrefix( migration_version_ ) ) + { + } + + void MigrationAllowList::SetEligibilityCheckEnabledForTests( bool enabled ) + { + g_migration_allowlist_enabled_for_tests.store( enabled, std::memory_order_release ); + } + + bool MigrationAllowList::IsEligibilityCheckEnabledForTests() + { + return g_migration_allowlist_enabled_for_tests.load( std::memory_order_acquire ); + } + + outcome::result MigrationAllowList::StoreObservedBalance( const std::string &address, uint64_t balance ) + { + if ( !db_ ) + { + return outcome::failure( storage::DatabaseError::UNITIALIZED ); + } + + base::Buffer key_buf; + key_buf.put( BuildKey( migration_version_, address ) ); + + base::Buffer value_buf; + value_buf.putUint64( balance ); + + BOOST_OUTCOME_TRY( db_->put( key_buf, value_buf ) ); + return outcome::success(); + } + + outcome::result MigrationAllowList::StoreObservedBalances( const std::vector &balances ) + { + for ( const auto &[address, balance] : balances ) + { + BOOST_OUTCOME_TRY( StoreObservedBalance( address, balance ) ); + } + + return outcome::success(); + } + + outcome::result> MigrationAllowList::LoadObservedBalance( const std::string &address ) const + { + if ( !db_ ) + { + return outcome::failure( storage::DatabaseError::UNITIALIZED ); + } + + base::Buffer key_buf; + key_buf.put( BuildKey( migration_version_, address ) ); + auto value = db_->get( key_buf ); + if ( value.has_error() ) + { + if ( value.error() == storage::DatabaseError::NOT_FOUND ) + { + return std::optional{}; + } + return value.error(); + } + + BOOST_OUTCOME_TRY( auto balance, DecodeBalance( value.value() ) ); + return std::optional{ balance }; + } + + outcome::result MigrationAllowList::IsEligible( const std::string &address, uint64_t claimed_balance ) const + { + if ( !IsEligibilityCheckEnabledForTests() ) + { + return true; + } + + BOOST_OUTCOME_TRY( auto maybe_balance, LoadObservedBalance( address ) ); + if ( !maybe_balance.has_value() ) + { + return false; + } + + const auto observed_balance = maybe_balance.value(); + const auto max_claim = observed_balance > ( std::numeric_limits::max() / 2 ) + ? std::numeric_limits::max() + : observed_balance * 2; + return claimed_balance <= max_claim; + } + + outcome::result> MigrationAllowList::ListObservedBalances() const + { + if ( !db_ ) + { + return outcome::failure( storage::DatabaseError::UNITIALIZED ); + } + + base::Buffer prefix_buf; + prefix_buf.put( prefix_ ); + auto entries = db_->query( prefix_buf ); + if ( entries.has_error() ) + { + if ( entries.error() == storage::DatabaseError::NOT_FOUND ) + { + return std::vector{}; + } + return entries.error(); + } + + std::vector balances; + balances.reserve( entries.value().size() ); + for ( const auto &[key, value] : entries.value() ) + { + const auto key_str = std::string( key.toString() ); + if ( key_str.size() <= prefix_.size() + 1 || key_str.compare( 0, prefix_.size(), prefix_ ) != 0 || + key_str[prefix_.size()] != '/' ) + { + logger_->warn( "Skipping malformed migration allowlist key {}", key_str ); + continue; + } + + BOOST_OUTCOME_TRY( auto balance, DecodeBalance( value ) ); + balances.emplace_back( key_str.substr( prefix_.size() + 1 ), balance ); + } + + std::sort( balances.begin(), + balances.end(), + []( const AddressBalance &lhs, const AddressBalance &rhs ) { return lhs.first < rhs.first; } ); + + return balances; + } + + std::string MigrationAllowList::BuildPrefix( std::string_view migration_version ) + { + return std::string( kPrefixBase ) + "/" + std::string( migration_version ); + } + + std::string MigrationAllowList::BuildKey( std::string_view migration_version, std::string_view address ) + { + return BuildPrefix( migration_version ) + "/" + std::string( address ); + } + + outcome::result MigrationAllowList::DecodeBalance( const base::Buffer &buffer ) + { + if ( buffer.size() != sizeof( uint64_t ) ) + { + return outcome::failure( std::errc::bad_message ); + } + + uint64_t value = 0; + for ( size_t i = 0; i < sizeof( uint64_t ); ++i ) + { + value = ( value << 8u ) | buffer[i]; + } + return value; + } +} diff --git a/src/account/MigrationAllowList.hpp b/src/account/MigrationAllowList.hpp new file mode 100644 index 000000000..cead1ccee --- /dev/null +++ b/src/account/MigrationAllowList.hpp @@ -0,0 +1,122 @@ +/** + * @file MigrationAllowList.hpp + * @brief Persistent allow-list used to track balances eligible for migration claims. + * @date 2026-05-01 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#pragma once + +#include "base/buffer.hpp" +#include "base/logger.hpp" +#include "outcome/outcome.hpp" +#include "storage/rocksdb/rocksdb.hpp" + +#include +#include +#include +#include +#include +#include + +class MigrationParamTest; + +namespace sgns +{ + /** + * @brief Stores observed legacy balances and validates migration claim eligibility. + */ + class MigrationAllowList + { + public: + /** + * @brief Address and observed balance pair stored in the migration allow-list. + */ + using AddressBalance = std::pair; + + /** + * @brief Creates an allow-list view scoped to a specific migration version. + * @param[in] db RocksDB instance used to persist and query observed balances. + * @param[in] migration_version Source migration version namespace, for example "3.6.0". + */ + MigrationAllowList( std::shared_ptr db, std::string migration_version ); + + /** + * @brief Persists the observed legacy balance for a single address. + * @param[in] address Legacy source address whose balance was observed. + * @param[in] balance Observed balance for @p address. + * @return Success when the balance is written, or a database error on failure. + */ + outcome::result StoreObservedBalance( const std::string &address, uint64_t balance ); + + /** + * @brief Persists observed legacy balances for multiple addresses. + * @param[in] balances Address and balance pairs to persist. + * @return Success when every balance is written, or the first write error encountered. + */ + outcome::result StoreObservedBalances( const std::vector &balances ); + + /** + * @brief Loads the observed legacy balance for a single address when present. + * @param[in] address Legacy source address to look up. + * @return Optional observed balance; empty when no allow-list entry exists for @p address. + */ + outcome::result> LoadObservedBalance( const std::string &address ) const; + + /** + * @brief Checks whether an address may claim the provided migrated balance. + * @param[in] address Legacy source address making the migration claim. + * @param[in] claimed_balance Balance claimed by the migration transaction. + * @return True when @p address exists in the allow-list and @p claimed_balance is no more than twice the observed balance. + * @note The maximum allowed claim saturates at @c std::numeric_limits::max() to avoid overflow. + */ + outcome::result IsEligible( const std::string &address, uint64_t claimed_balance ) const; + + /** + * @brief Lists every stored observed balance in the current migration namespace. + * @return Address and balance pairs sorted by address. + */ + outcome::result> ListObservedBalances() const; + + /** + * @brief Builds the RocksDB key prefix used for a migration version namespace. + * @param[in] migration_version Source migration version namespace. + * @return Key prefix for all allow-list entries under @p migration_version. + */ + static std::string BuildPrefix( std::string_view migration_version ); + + /** + * @brief Builds the RocksDB key used for a specific address inside a migration namespace. + * @param[in] migration_version Source migration version namespace. + * @param[in] address Legacy source address. + * @return Full allow-list key for @p address under @p migration_version. + */ + static std::string BuildKey( std::string_view migration_version, std::string_view address ); + + private: + friend class ::MigrationParamTest; + + /** + * @brief Enables or disables eligibility checks for migration tests. + * @param[in] enabled True to enforce allow-list checks, false to allow every claim. + */ + static void SetEligibilityCheckEnabledForTests( bool enabled ); + + /** + * @brief Returns whether eligibility checks are enabled for migration tests. + * @return True when allow-list eligibility is enforced. + */ + static bool IsEligibilityCheckEnabledForTests(); + + /** + * @brief Decodes a stored observed balance from its serialized database value. + * @param[in] buffer Buffer containing an encoded 64-bit balance. + * @return Decoded balance, or @c std::errc::bad_message when the buffer size is invalid. + */ + static outcome::result DecodeBalance( const base::Buffer &buffer ); + + std::shared_ptr db_; ///< Database storing allow-list entries. + std::string migration_version_; ///< Source migration version namespace. + std::string prefix_; ///< Cached key prefix for @ref migration_version_. + base::Logger logger_ = base::createLogger( "MigrationAllowList" ); ///< Component logger. + }; +} diff --git a/src/account/MigrationManager.cpp b/src/account/MigrationManager.cpp index a13234edf..723129d6f 100644 --- a/src/account/MigrationManager.cpp +++ b/src/account/MigrationManager.cpp @@ -11,6 +11,7 @@ #include "account/Migration1_0_0To3_4_0.hpp" #include "account/Migration3_4_0To3_5_0.hpp" #include "account/Migration3_5_0To3_6_0.hpp" +#include "account/Migration3_6_0To3_7_0.hpp" #include #include @@ -38,7 +39,8 @@ namespace sgns std::shared_ptr generator, std::string writeBasePath, std::string base58key, - std::shared_ptr account ) + std::shared_ptr account, + bool is_full_node ) { auto instance = std::shared_ptr( new MigrationManager() ); instance->RegisterStep( std::make_shared( ioContext, @@ -70,6 +72,15 @@ namespace sgns generator, writeBasePath, base58key ) ); + instance->RegisterStep( std::make_shared( ioContext, + pubSub, + graphsync, + scheduler, + generator, + writeBasePath, + base58key, + account, + is_full_node ) ); return instance; } diff --git a/src/account/MigrationManager.hpp b/src/account/MigrationManager.hpp index 8c7f0d4a6..5ff7b3b6b 100644 --- a/src/account/MigrationManager.hpp +++ b/src/account/MigrationManager.hpp @@ -61,7 +61,8 @@ namespace sgns std::shared_ptr generator, std::string writeBasePath, std::string base58key, - std::shared_ptr account ); + std::shared_ptr account, + bool is_full_node ); /** * @brief Register a migration step. diff --git a/src/account/MigrationTransaction.cpp b/src/account/MigrationTransaction.cpp new file mode 100644 index 000000000..e97165a0f --- /dev/null +++ b/src/account/MigrationTransaction.cpp @@ -0,0 +1,203 @@ +/** + * @file MigrationTransaction.cpp + * @brief One-time migration mint transaction implementation. + * @date 2026-04-29 + */ +#include "account/MigrationTransaction.hpp" + +#include "base/blob.hpp" +#include "crypto/hasher/hasher_impl.hpp" + +namespace sgns +{ + MigrationTransaction::MigrationTransaction( UTXOTxParameters utxo_params, + std::string from_version, + TokenID token_id, + SGTransaction::DAGStruct dag ) : + IGeniusTransactions( "migration", SetDAGWithType( std::move( dag ), "migration" ) ), + utxo_params_( std::move( utxo_params ) ), + from_version_( std::move( from_version ) ), + token_id_( std::move( token_id ) ) + { + } + + std::vector MigrationTransaction::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const + { + SGTransaction::MigrationTx tx_struct; + tx_struct.mutable_dag_struct()->CopyFrom( dag ); + + auto *utxo_proto_params = tx_struct.mutable_utxo_params(); + for ( const auto &[txid_hash_, output_idx_, signature_] : utxo_params_.first ) + { + auto *input_proto = utxo_proto_params->add_inputs(); + input_proto->set_tx_id_hash( txid_hash_.toReadableString() ); + input_proto->set_output_index( output_idx_ ); + input_proto->set_signature( signature_.data(), signature_.size() ); + } + + for ( const auto &[encrypted_amount, dest_address, token_id] : utxo_params_.second ) + { + auto *output_proto = utxo_proto_params->add_outputs(); + output_proto->set_encrypted_amount( encrypted_amount ); + output_proto->set_dest_addr( dest_address ); + output_proto->set_token_id( token_id.bytes().data(), token_id.size() ); + } + + const auto amount = GetAmount(); + const auto token = GetTokenID(); + tx_struct.set_amount( amount ); + tx_struct.set_token_id( token.bytes().data(), token.size() ); + tx_struct.set_from_version( from_version_ ); + + std::vector serialized_proto( tx_struct.ByteSizeLong() ); + if ( !tx_struct.SerializeToArray( serialized_proto.data(), serialized_proto.size() ) ) + { + std::cerr << "Failed to Serialize MigrationTx to array" << std::endl; + } + return serialized_proto; + } + + std::shared_ptr MigrationTransaction::DeSerializeByteVector( const std::vector &data ) + { + SGTransaction::MigrationTx tx_struct; + if ( !tx_struct.ParseFromArray( data.data(), data.size() ) ) + { + std::cerr << "Failed to parse MigrationTx from array\n"; + return nullptr; + } + + const uint64_t amount = tx_struct.amount(); + const std::string from_version = tx_struct.from_version(); + const TokenID token_id = TokenID::FromBytes( tx_struct.token_id().data(), tx_struct.token_id().size() ); + + std::vector inputs; + auto *utxo_proto_params = tx_struct.mutable_utxo_params(); + for ( int i = 0; i < utxo_proto_params->inputs_size(); ++i ) + { + const auto &input_proto = utxo_proto_params->inputs( i ); + auto maybe_hash = base::Hash256::fromReadableString( input_proto.tx_id_hash() ); + if ( !maybe_hash ) + { + std::cerr << "Invalid hash in migration input." << std::endl; + return nullptr; + } + + inputs.push_back( { maybe_hash.value(), + input_proto.output_index(), + std::vector( input_proto.signature().cbegin(), input_proto.signature().cend() ) } ); + } + + std::vector outputs; + for ( int i = 0; i < utxo_proto_params->outputs_size(); ++i ) + { + const auto &output_proto = utxo_proto_params->outputs( i ); + outputs.push_back( { output_proto.encrypted_amount(), + output_proto.dest_addr(), + TokenID::FromBytes( output_proto.token_id().data(), output_proto.token_id().size() ) } ); + } + + if ( outputs.empty() ) + { + outputs.push_back( { amount, tx_struct.dag_struct().source_addr(), token_id } ); + } + + return std::make_shared( + MigrationTransaction( { std::move( inputs ), std::move( outputs ) }, + from_version, + token_id, + tx_struct.dag_struct() ) ); + } + + uint64_t MigrationTransaction::GetAmount() const + { + if ( utxo_params_.second.empty() ) + { + return 0; + } + return utxo_params_.second.front().encrypted_amount; + } + + TokenID MigrationTransaction::GetTokenID() const + { + if ( utxo_params_.second.empty() ) + { + return token_id_; + } + return utxo_params_.second.front().token_id; + } + + std::string MigrationTransaction::GetChainId() const + { + return "migration"; + } + + UTXOTxParameters MigrationTransaction::GetUTXOParameters() const + { + return utxo_params_; + } + + bool MigrationTransaction::HasUTXOParameters() const + { + return true; + } + + std::optional MigrationTransaction::GetUTXOParametersOpt() const + { + return utxo_params_; + } + + std::string MigrationTransaction::GetFromVersion() const + { + return from_version_; + } + + std::unordered_set MigrationTransaction::GetTopics() const + { + auto topics = IGeniusTransactions::GetTopics(); + for ( const auto &output : utxo_params_.second ) + { + topics.emplace( output.dest_address ); + } + return topics; + } + + std::string MigrationTransaction::DeriveUniqueSourceKey( std::string_view from_version, + std::string_view source_address, + const TokenID &token_id ) + { + const auto payload = + "migration:" + std::string( from_version ) + ":" + std::string( source_address ) + ":" + token_id.ToHex(); + auto hasher = std::make_shared(); + return hasher->blake2b_256( std::vector( payload.begin(), payload.end() ) ).toReadableString(); + } + + MigrationTransaction MigrationTransaction::New( uint64_t amount, + std::string from_version, + TokenID token_id, + SGTransaction::DAGStruct dag, + std::string destination ) + { + if ( destination.empty() ) + { + destination = dag.source_addr(); + } + + const auto source_key = DeriveUniqueSourceKey( from_version, dag.source_addr(), token_id ); + dag.set_uncle_hash( source_key ); + + auto source_hash = base::Hash256::fromReadableString( source_key ); + std::vector migration_inputs; + if ( source_hash.has_value() ) + { + migration_inputs.push_back( { source_hash.value(), 0, {} } ); + } + + std::vector migration_outputs{ { amount, destination, token_id } }; + MigrationTransaction instance( { std::move( migration_inputs ), std::move( migration_outputs ) }, + std::move( from_version ), + std::move( token_id ), + std::move( dag ) ); + instance.FillHash(); + return instance; + } +} diff --git a/src/account/MigrationTransaction.hpp b/src/account/MigrationTransaction.hpp new file mode 100644 index 000000000..777f07210 --- /dev/null +++ b/src/account/MigrationTransaction.hpp @@ -0,0 +1,77 @@ +/** + * @file MigrationTransaction.hpp + * @brief Header file for one-time migration mint transactions. + * @date 2026-04-29 + */ +#pragma once + +#include +#include +#include +#include +#include + +#include "account/IGeniusTransactions.hpp" +#include "account/TokenID.hpp" +#include "account/UTXOStructs.hpp" + +namespace sgns +{ + class MigrationTransaction final : public IGeniusTransactions + { + public: + using IGeniusTransactions::SerializeByteVector; + + ~MigrationTransaction() override = default; + + static std::shared_ptr DeSerializeByteVector( const std::vector &data ); + + static MigrationTransaction New( uint64_t amount, + std::string from_version, + TokenID token_id, + SGTransaction::DAGStruct dag, + std::string destination = "" ); + + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; + + uint64_t GetAmount() const; + TokenID GetTokenID() const; + + std::string GetChainId() const override; + + UTXOTxParameters GetUTXOParameters() const; + bool HasUTXOParameters() const override; + std::optional GetUTXOParametersOpt() const override; + + std::string GetFromVersion() const; + + std::string GetTransactionSpecificPath() const override + { + return GetType(); + } + + std::unordered_set GetTopics() const override; + + static std::string DeriveUniqueSourceKey( std::string_view from_version, + std::string_view source_address, + const TokenID &token_id ); + + private: + MigrationTransaction( UTXOTxParameters utxo_params, + std::string from_version, + TokenID token_id, + SGTransaction::DAGStruct dag ); + + UTXOTxParameters utxo_params_; + std::string from_version_; + TokenID token_id_; + + static bool Register() + { + RegisterDeserializer( "migration", &MigrationTransaction::DeSerializeByteVector ); + return true; + } + + static inline bool registered = Register(); + }; +} diff --git a/src/account/MintTransaction.cpp b/src/account/MintTransaction.cpp index bbca4034c..5d53f6a1b 100644 --- a/src/account/MintTransaction.cpp +++ b/src/account/MintTransaction.cpp @@ -19,10 +19,10 @@ namespace sgns { } - std::vector MintTransaction::SerializeByteVector() + std::vector MintTransaction::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const { SGTransaction::MintTx tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( this->dag_st ); + tx_struct.mutable_dag_struct()->CopyFrom( dag ); tx_struct.set_amount( amount ); tx_struct.set_chain_id( chain_id ); tx_struct.set_token_id( token_id.bytes().data(), token_id.size() ); @@ -64,6 +64,11 @@ namespace sgns return token_id; } + std::string MintTransaction::GetChainId() const + { + return chain_id; + } + MintTransaction MintTransaction::New( uint64_t new_amount, std::string chain_id, TokenID token_id, diff --git a/src/account/MintTransaction.hpp b/src/account/MintTransaction.hpp index e5c7ab614..cdd6097ac 100644 --- a/src/account/MintTransaction.hpp +++ b/src/account/MintTransaction.hpp @@ -1,6 +1,6 @@ /** * @file MintTransaction.hpp - * @brief + * @brief Transaction type used to mint tokens from an external chain reference. * @date 2024-03-15 * @author Henrique A. Klein (hklein@gnus.ai) */ @@ -15,39 +15,89 @@ namespace sgns { + /** + * @brief Transaction that mints tokens after proving a corresponding source-chain event. + */ class MintTransaction final : public IGeniusTransactions { public: + /** + * @brief Destroys the mint transaction. + */ ~MintTransaction() override = default; + /** + * @brief Deserializes a serialized mint transaction. + * @param[in] data Serialized @c SGTransaction::MintTx bytes. + * @return Shared pointer to the parsed mint transaction, or nullptr if parsing fails. + */ static std::shared_ptr DeSerializeByteVector( const std::vector &data ); + /** + * @brief Creates a new mint transaction instance. + * @param[in] new_amount Amount of token units to mint. + * @param[in] chain_id Source chain identifier associated with the mint event. + * @param[in] token_id Token identifier for the asset being minted. + * @param[in] dag DAG metadata shared by all transaction types. + * @return Mint transaction with the transaction type set and hash populated. + */ static MintTransaction New( uint64_t new_amount, std::string chain_id, TokenID token_id, SGTransaction::DAGStruct dag ); - std::vector SerializeByteVector() override; + using IGeniusTransactions::SerializeByteVector; + /** + * @brief Serializes the mint transaction payload and DAG metadata. + * @param[in] dag DAG metadata to serialize into the transaction payload. + * @return Serialized @c SGTransaction::MintTx bytes. + */ + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; + /** + * @brief Returns the minted amount. + * @return Amount of token units minted by this transaction. + */ uint64_t GetAmount() const; + /** + * @brief Returns the minted token identifier. + * @return Token identifier for the asset minted by this transaction. + */ TokenID GetTokenID() const; + /** + * @brief Returns the source chain identifier associated with the mint. + * @return Source chain identifier used for input validation routing. + */ + std::string GetChainId() const override; + + /** + * @brief Returns the transaction-specific storage path component. + * @return Transaction type string used as the path component. + */ std::string GetTransactionSpecificPath() const override { return GetType(); } private: + /** + * @brief Constructs a mint transaction from its payload and DAG metadata. + * @param[in] new_amount Amount of token units to mint. + * @param[in] chain_id Source chain identifier associated with the mint event. + * @param[in] token_id Token identifier for the asset being minted. + * @param[in] dag DAG metadata shared by all transaction types. + */ MintTransaction( uint64_t new_amount, std::string chain_id, TokenID token_id, SGTransaction::DAGStruct dag ); - uint64_t amount; - std::string chain_id; - TokenID token_id; + uint64_t amount; ///< Amount of token units minted by this transaction. + std::string chain_id; ///< Source chain identifier associated with the mint event. + TokenID token_id; ///< Token identifier for the asset being minted. /** - * @brief Registers the deserializer for the transfer transaction type. - * @return A boolean indicating successful registration. + * @brief Registers the deserializer for the mint transaction type. + * @return True when registration completes. */ static bool Register() { @@ -55,8 +105,8 @@ namespace sgns return true; } - /** - * @brief Static variable to ensure registration happens on inclusion of header file. + /** + * @brief Forces static initialization of the mint transaction deserializer. */ static inline bool registered = Register(); }; diff --git a/src/account/MintTransactionV2.cpp b/src/account/MintTransactionV2.cpp index 9acdd55d4..34056f0e9 100644 --- a/src/account/MintTransactionV2.cpp +++ b/src/account/MintTransactionV2.cpp @@ -21,10 +21,10 @@ namespace sgns { } - std::vector MintTransactionV2::SerializeByteVector() + std::vector MintTransactionV2::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const { SGTransaction::MintTxV2 tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( this->dag_st ); + tx_struct.mutable_dag_struct()->CopyFrom( dag ); tx_struct.set_chain_id( chain_id_ ); auto *utxo_proto_params = tx_struct.mutable_utxo_params(); @@ -102,15 +102,6 @@ namespace sgns outputs.push_back( { amount, tx_struct.dag_struct().source_addr(), tokenid } ); } - if ( inputs.empty() && !tx_struct.dag_struct().previous_hash().empty() ) - { - auto maybe_prev_hash = base::Hash256::fromReadableString( tx_struct.dag_struct().previous_hash() ); - if ( maybe_prev_hash ) - { - inputs.push_back( { maybe_prev_hash.value(), 0, {} } ); - } - } - return std::make_shared( MintTransactionV2( { std::move( inputs ), std::move( outputs ) }, chainid, tokenid, @@ -135,6 +126,11 @@ namespace sgns return utxo_params_.second.front().token_id; } + std::string MintTransactionV2::GetChainId() const + { + return chain_id_; + } + UTXOTxParameters MintTransactionV2::GetUTXOParameters() const { return utxo_params_; @@ -164,18 +160,9 @@ namespace sgns std::string chain_id, TokenID token_id, SGTransaction::DAGStruct dag, + std::vector mint_inputs, std::string mint_destination ) { - std::vector mint_inputs; - if ( !dag.previous_hash().empty() ) - { - auto maybe_hash = base::Hash256::fromReadableString( dag.previous_hash() ); - if ( maybe_hash ) - { - mint_inputs.push_back( { maybe_hash.value(), 0, {} } ); - } - } - if ( mint_destination.empty() ) { mint_destination = dag.source_addr(); diff --git a/src/account/MintTransactionV2.hpp b/src/account/MintTransactionV2.hpp index 6aa9de0e9..ec53f126c 100644 --- a/src/account/MintTransactionV2.hpp +++ b/src/account/MintTransactionV2.hpp @@ -21,6 +21,7 @@ namespace sgns class MintTransactionV2 final : public IGeniusTransactions { public: + using IGeniusTransactions::SerializeByteVector; /** * @brief Destroy the Mint Transaction V 2 object */ @@ -39,6 +40,7 @@ namespace sgns * @param[in] chain_id The chain ID from where the mint came from * @param[in] token_id The token ID * @param[in] dag The DAG structure with the common transaction data + * @param[in] mint_inputs Explicit input references for the source-chain burn(s) * @param[in] mint_destination The destination of the Mint * @return A @ref MintTransactionV2 */ @@ -46,13 +48,14 @@ namespace sgns std::string chain_id, TokenID token_id, SGTransaction::DAGStruct dag, + std::vector mint_inputs, std::string mint_destination ); /** * @brief Serializes the transaction * @return The serialized byte vector */ - std::vector SerializeByteVector() override; + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; /** * @brief Get the amount of the mint @@ -66,6 +69,12 @@ namespace sgns */ TokenID GetTokenID() const; + /** + * @brief Get source chain identifier for bridge mint validation routing + * @return Source chain id + */ + std::string GetChainId() const override; + /** * @brief Returns the UTXOs * @return The UTXOs of the MintV2 transaction diff --git a/src/account/ProcessingTransaction.cpp b/src/account/ProcessingTransaction.cpp index 47f623078..836160021 100644 --- a/src/account/ProcessingTransaction.cpp +++ b/src/account/ProcessingTransaction.cpp @@ -42,10 +42,10 @@ namespace sgns return instance; } - std::vector ProcessingTransaction::SerializeByteVector() + std::vector ProcessingTransaction::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const { SGTransaction::ProcessingTx tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( this->dag_st ); + tx_struct.mutable_dag_struct()->CopyFrom( dag ); tx_struct.set_mpc_magic_key( 0 ); tx_struct.set_offset( 0 ); tx_struct.set_job_cid( job_id_ ); diff --git a/src/account/ProcessingTransaction.hpp b/src/account/ProcessingTransaction.hpp index 3f7d0723d..7e7c33891 100644 --- a/src/account/ProcessingTransaction.hpp +++ b/src/account/ProcessingTransaction.hpp @@ -28,7 +28,8 @@ namespace sgns ~ProcessingTransaction() override = default; - std::vector SerializeByteVector() override; + using IGeniusTransactions::SerializeByteVector; + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; uint256_t GetJobHash() const { diff --git a/src/account/TokenID.hpp b/src/account/TokenID.hpp index e39f2045f..e5eedc545 100644 --- a/src/account/TokenID.hpp +++ b/src/account/TokenID.hpp @@ -1,6 +1,6 @@ /** * @file TokenID.hpp - * @brief + * @brief Fixed-size token identifier wrapper with GNUS compatibility helpers. * @date 2025-06-19 * @author Henrique A. Klein (hklein@gnus.ai) */ @@ -16,24 +16,64 @@ namespace sgns { - + /** + * @brief Represents a 32-byte token identifier while preserving legacy GNUS semantics. + */ class TokenID { public: + /** + * @brief Fixed-size byte storage used for token identifiers. + */ using ByteArray = std::array; + /** + * @brief Constructs an invalid or legacy-default token identifier. + */ TokenID() : data_{}, valid_( false ) {} + /** + * @brief Copy-constructs a token identifier. + * @param[in] other Token identifier to copy. + */ TokenID( const TokenID &other ) = default; + + /** + * @brief Move-constructs a token identifier. + * @param[in] other Token identifier to move from. + */ TokenID( TokenID &&other ) = default; + + /** + * @brief Copy-assigns a token identifier. + * @param[in] other Token identifier to copy. + * @return Reference to this token identifier. + */ TokenID &operator=( const TokenID &other ) = default; + + /** + * @brief Move-assigns a token identifier. + * @param[in] other Token identifier to move from. + * @return Reference to this token identifier. + */ TokenID &operator=( TokenID &&other ) = default; + /** + * @brief Builds a token identifier from a byte initializer list. + * @param[in] list Bytes used to build the identifier. + * @return Token identifier containing @p list, left-padded to 32 bytes when shorter. + */ static TokenID FromBytes( std::initializer_list list ) { return FromBytes( list.begin(), list.size() ); } + /** + * @brief Builds a token identifier from up to 32 bytes, left-padding shorter inputs. + * @param[in] data Pointer to the source bytes. + * @param[in] size Number of bytes available at @p data. + * @return Valid token identifier when @p data is non-null and @p size is 1 to 32; otherwise an invalid identifier. + */ static TokenID FromBytes( const void *data, size_t size ) { TokenID id; @@ -54,31 +94,58 @@ namespace sgns return id; } + /** + * @brief Returns the raw 32-byte storage buffer. + * @return Const reference to the internal 32-byte array. + */ const ByteArray &bytes() const { return data_; } + /** + * @brief Returns 32 for valid token identifiers or 0 for legacy-invalid ones. + * @return Serialized byte size of this identifier. + */ size_t size() const { return valid_ ? 32 : 0; } + /** + * @brief Tests exact equality including validity state. + * @param[in] other Token identifier to compare against. + * @return True when both identifiers have the same validity state and bytes. + */ bool operator==( const TokenID &other ) const { return valid_ == other.valid_ && data_ == other.data_; } + /** + * @brief Tests exact inequality including validity state. + * @param[in] other Token identifier to compare against. + * @return True when @p other is not exactly equal to this identifier. + */ bool operator!=( const TokenID &other ) const { return !( *this == other ); } + /** + * @brief Orders token identifiers by raw byte value. + * @param[in] other Token identifier to compare against. + * @return True when this identifier's bytes compare lexicographically before @p other. + */ bool operator<( const TokenID &other ) const { return data_ < other.data_; // lexicographic comparison } + /** + * @brief Converts the token identifier to a lowercase hexadecimal string. + * @return Lowercase hexadecimal representation of the 32-byte storage buffer. + */ std::string ToHex() const { std::ostringstream oss; @@ -89,11 +156,20 @@ namespace sgns return oss.str(); } + /** + * @brief Returns true when this identifier refers to the default GNUS token. + * @return True when the identifier is invalid or all bytes are zero. + */ bool IsGNUS() const { return !valid_ || std::all_of( data_.begin(), data_.end(), []( uint8_t b ) { return b == 0; } ); } + /** + * @brief Compares token identifiers while treating all GNUS representations as equivalent. + * @param[in] other Token identifier to compare against. + * @return True when the identifiers are exactly equal or both represent the GNUS token. + */ bool Equals( const TokenID &other ) const { if ( *this == other ) @@ -104,12 +180,17 @@ namespace sgns } private: - ByteArray data_; - bool valid_; + ByteArray data_; ///< Raw 32-byte token identifier storage. + bool valid_; ///< Whether the identifier was constructed from non-empty byte input. }; } -// Overload for printing to streams +/** + * @brief Streams a token identifier as hexadecimal text. + * @param[in,out] os Output stream to write to. + * @param[in] id Token identifier to stream. + * @return Reference to @p os after writing the token identifier. + */ inline std::ostream &operator<<( std::ostream &os, const sgns::TokenID &id ) { return os << id.ToHex(); diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index eb845bb53..06028f414 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -9,8 +9,7 @@ #include #include #include -#include -#include +#include #include #include @@ -19,8 +18,10 @@ #include "TransferTransaction.hpp" #include "MintTransaction.hpp" #include "MintTransactionV2.hpp" +#include "MigrationTransaction.hpp" +#include "MigrationAllowList.hpp" #include "EscrowTransaction.hpp" -#include "EscrowReleaseTransaction.hpp" +#include "UTXOMerkle.hpp" #include "account/TokenAmount.hpp" #include "account/AccountMessenger.hpp" #include "account/proto/SGTransaction.pb.h" @@ -32,10 +33,65 @@ namespace sgns { + namespace + { + using utxo_merkle::HashLeaf; + using utxo_merkle::HashNode; + using utxo_merkle::OutPointKey; + using utxo_merkle::ReadUInt32BE; + using utxo_merkle::ReadUInt64BE; + using utxo_merkle::SerializeUTXOLeafPayload; + + bool ExtractProducedUTXOs( const std::shared_ptr &tx, std::vector &outputs ) + { + if ( !tx ) + { + return false; + } + auto tx_hash = base::Hash256::fromReadableString( tx->GetHash() ); + if ( tx_hash.has_error() ) + { + return false; + } + + outputs.clear(); + if ( !tx->HasUTXOParameters() ) + { + return false; + } + + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + return false; + } + + const auto &dst_infos = params_opt->second; + outputs.reserve( dst_infos.size() ); + for ( std::uint32_t i = 0; i < dst_infos.size(); ++i ) + { + outputs.emplace_back( tx_hash.value(), + i, + dst_infos[i].encrypted_amount, + dst_infos[i].token_id, + dst_infos[i].dest_address ); + } + return true; + } + } // namespace + + base::Logger TransactionManagerLogger() + { + // Always call base::createLogger to get the current logger + // This will return existing logger or create new one as needed + return base::createLogger( "TransactionManager" ); + } + std::shared_ptr TransactionManager::New( std::shared_ptr processing_db, std::shared_ptr ctx, std::shared_ptr account, std::shared_ptr hasher, + std::shared_ptr blockchain, bool full_node, std::chrono::milliseconds timestamp_tolerance, std::chrono::milliseconds mutability_window ) @@ -44,10 +100,45 @@ namespace sgns std::move( ctx ), std::move( account ), std::move( hasher ), + std::move( blockchain ), full_node, timestamp_tolerance, mutability_window ) ); + instance->blockchain_->RegisterCertificateHandler( + SubjectType::SUBJECT_NONCE, + [weak_ptr( std::weak_ptr( instance ) )]( + const std::string &subject_hash, + const ConsensusCertificate &certificate ) -> outcome::result + { + if ( auto strong = weak_ptr.lock() ) + { + auto process_result = strong->OnConsensusCertificate( subject_hash ); + if ( process_result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] Failed to process certificate proposal_id={} error={}", + strong->account_m->GetAddress().substr( 0, 8 ), + strong->full_node_m, + certificate.proposal_id(), + process_result.error().message() ); + } + return process_result; + } + return outcome::failure( std::errc::owner_dead ); + } ); + instance->blockchain_->RegisterSubjectHandler( + SubjectType::SUBJECT_NONCE, + [weak_ptr( std::weak_ptr( instance ) )]( + const ConsensusManager::Subject &subject ) -> outcome::result + { + if ( auto strong = weak_ptr.lock() ) + { + return strong->HandleNonceConsensusSubject( subject ); + } + return outcome::failure( std::errc::owner_dead ); + } ); + auto monitored_networks = GetMonitoredNetworkIDs(); for ( auto network_id : monitored_networks ) { @@ -116,6 +207,7 @@ namespace sgns std::shared_ptr ctx, std::shared_ptr account, std::shared_ptr hasher, + std::shared_ptr blockchain, bool full_node, std::chrono::milliseconds timestamp_tolerance, std::chrono::milliseconds mutability_window ) : @@ -123,6 +215,7 @@ namespace sgns ctx_m( std::move( ctx ) ), account_m( std::move( account ) ), hasher_m( std::move( hasher ) ), + blockchain_( std::move( blockchain ) ), full_node_m( full_node ), state_m( State::CREATING ), last_periodic_sync_time_( std::chrono::steady_clock::now() ), @@ -135,9 +228,25 @@ namespace sgns TransactionManager::~TransactionManager() { - m_logger->debug( "[{} - full: {}] ~TransactionManager CALLED", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] ~TransactionManager CALLED", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); + if ( globaldb_m ) + { + auto monitored_networks = GetMonitoredNetworkIDs(); + for ( auto network_id : monitored_networks ) + { + std::string blockchain_base = GetBlockChainBase( network_id ); + const std::string tx_pattern = "^/?" + blockchain_base + "tx/[^/]+"; + const std::string proof_pattern = "^/?" + blockchain_base + "proof/[^/]+"; + + globaldb_m->UnregisterNewElementCallback( tx_pattern ); + globaldb_m->UnregisterDeletedElementCallback( tx_pattern ); + globaldb_m->UnregisterElementFilter( tx_pattern ); + globaldb_m->UnregisterElementFilter( proof_pattern ); + } + } + account_m->ClearGetTransactionCIDMethod(); Stop(); } @@ -153,30 +262,61 @@ namespace sgns void TransactionManager::Start() { - if ( GetState() != State::CREATING || stopped_.load() ) + RegisterTopicNames(); + StartListeningTopics(); + StartCore(); + } + + void TransactionManager::RegisterTopicNames() + { + if ( stopped_.load() || topic_names_registered_.exchange( true ) ) { return; } - m_logger->info( "[{} - full: {}] Starting Transaction Manager", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - full_node_topic_m = std::string( GNUS_FULL_NODES_TOPIC ); + globaldb_m->AddTopicName( account_m->GetAddress() ); + if ( full_node_m ) + { + globaldb_m->AddTopicName( full_node_topic_m ); + } + } + + void TransactionManager::StartListeningTopics() + { + if ( stopped_.load() || listening_topics_started_.exchange( true ) ) + { + return; + } + + RegisterTopicNames(); + globaldb_m->AddListenTopic( account_m->GetAddress() ); - m_logger->info( "[{} - full: {}] Adding broadcast to full node on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - full_node_topic_m ); + TransactionManagerLogger()->info( "[{} - full: {}] Adding broadcast to full node on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + full_node_topic_m ); if ( full_node_m ) { - m_logger->debug( "[{} - full: {}] Listening full node on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - full_node_topic_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Listening full node on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + full_node_topic_m ); globaldb_m->AddListenTopic( full_node_topic_m ); } + } + + void TransactionManager::StartCore() + { + if ( GetState() != State::CREATING || stopped_.load() || core_started_.exchange( true ) ) + { + return; + } + + TransactionManagerLogger()->info( "[{} - full: {}] Starting Transaction Manager", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); ChangeState( State::INITIALIZING ); @@ -201,7 +341,7 @@ namespace sgns auto now = std::chrono::steady_clock::now(); auto time_since_last_loop = std::chrono::duration_cast( now - last_loop_time_ ) .count(); - last_loop_time_ = now; + last_loop_time_ = now; std::vector elements_to_delete; std::vector elements_to_process; @@ -221,25 +361,25 @@ namespace sgns for ( auto &deletion_key : elements_to_delete ) { - m_logger->debug( "[{} - full: {}] Deleting key: {} ", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - deletion_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Deleting key: {} ", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + deletion_key ); ProcessDeletion( deletion_key ); } for ( auto &new_data : elements_to_process ) { - m_logger->debug( "[{} - full: {}] Adding key: {} ", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_data.first ); + TransactionManagerLogger()->debug( "[{} - full: {}] Adding key: {} ", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_data.first ); ProcessNewData( new_data ); } - m_logger->trace( "[{} - full: {}] Loop iteration - time since last: {}ms", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - time_since_last_loop ); + TransactionManagerLogger()->trace( "[{} - full: {}] Loop iteration - time since last: {}ms", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + time_since_last_loop ); switch ( GetState() ) { @@ -247,9 +387,10 @@ namespace sgns InitTransactions(); if ( GetState() == State::READY ) { - m_logger->debug( "[{} - full: {}] Transaction Manager is now READY - starting regular updates", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Transaction Manager is now READY - starting regular updates", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); } break; @@ -257,7 +398,7 @@ namespace sgns break; case State::SYNCING: - this->SyncNonce(); + SyncNonce(); break; case State::READY: @@ -271,57 +412,46 @@ namespace sgns auto send_result = SendTransactionItem( tx_queue_m.front() ); if ( send_result.has_error() ) { + const auto err = send_result.error(); + const bool retryable_error = ( err == boost::system::errc::make_error_code( + boost::system::errc::timed_out ) ) || + ( err == boost::system::errc::make_error_code( + boost::system::errc::resource_unavailable_try_again ) ); + + if ( retryable_error ) + { + TransactionManagerLogger()->info( + "[{} - full: {}] Send deferred/retryable ({}). Keeping transaction in queue", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + err.message() ); + break; + } + ChangeState( State::SYNCING ); - m_logger->error( "[{} - full: {}] Error in SendTransactionItem: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - send_result.error().message() ); + TransactionManagerLogger()->error( "[{} - full: {}] Error in SendTransactionItem: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + err.message() ); auto rollback_result = RollbackTransactions( tx_queue_m.front() ); if ( rollback_result.has_error() ) { - m_logger->error( "[{} - full: {}] RollbackTransactions error, couldn't fetch nonce", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->error( "[{} - full: {}] {} error, couldn't fetch nonce", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); break; } - - if ( send_result.error() == boost::system::errc::make_error_code( boost::system::errc::timed_out ) ) - { - m_logger->info( "[{} - full: {}] Network timeout - keeping transaction in queue for retry", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - } - else - { - tx_queue_m.pop_front(); - } + tx_queue_m.pop_front(); break; } - auto nonces_sent = send_result.value(); - for ( auto nonce : nonces_sent ) - { - m_logger->debug( "[{} - full: {}] Confirming local nonce to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); - account_m->SetLocalConfirmedNonce( nonce ); - } tx_queue_m.pop_front(); - lock.unlock(); } break; } - auto confirm_result = ConfirmTransactions(); - if ( confirm_result.has_error() ) - { - m_logger->trace( "[{} - full: {}] Unknown ConfirmTransactions error", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - } - bool should_sync = false; if ( !received_first_periodic_sync_response_.load() ) { @@ -339,33 +469,33 @@ namespace sgns if ( should_sync ) { auto interval_desc = received_first_periodic_sync_response_.load() ? "10 minutes" : "30 seconds"; - m_logger->debug( "[{} - full: {}] Periodic sync - requesting heads (interval: {})", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - interval_desc ); + TransactionManagerLogger()->debug( "[{} - full: {}] Periodic sync - requesting heads (interval: {})", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + interval_desc ); auto topics_result = globaldb_m->GetMonitoredTopics(); if ( topics_result.has_value() ) { if ( account_m->RequestHeads( topics_result.value() ) ) { last_periodic_sync_time_ = now; - m_logger->debug( "[{} - full: {}] Periodic sync head request sent for {} topics", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - topics_result.value().size() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Periodic sync head request sent for {} topics", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + topics_result.value().size() ); } else { - m_logger->warn( "[{} - full: {}] Periodic sync head request failed", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->warn( "[{} - full: {}] Periodic sync head request failed", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); } } else { - m_logger->warn( "[{} - full: {}] Could not get monitored topics for head request", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->warn( "[{} - full: {}] Could not get monitored topics for head request", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); } } @@ -410,6 +540,7 @@ namespace sgns } BOOST_OUTCOME_TRY( auto params, + account_m->GetUTXOManager().CreateTxParameter( amount, std::move( destination ), token_id ) ); auto [inputs, outputs] = params; @@ -418,7 +549,7 @@ namespace sgns transfer_transaction->MakeSignature( *account_m ); - account_m->GetUTXOManager().ReserveUTXOs( inputs ); + account_m->GetUTXOManager().ReserveUTXOs( inputs, transfer_transaction->GetHash() ); EnqueueTransaction( std::make_pair( transfer_transaction, std::nullopt ) ); @@ -439,11 +570,38 @@ namespace sgns { destination = account_m->GetAddress(); } + if ( chainid.empty() ) + { + // MintV2 represents bridge/public-chain input. Empty chain id must not fall back to Genius validation. + chainid = "public"; + } + + auto source_hash = base::Hash256::fromReadableString( transaction_hash ); + base::Hash256 source_input_hash; + if ( source_hash.has_error() ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Source hash parse inconsistency for mint tx_ref={}, using empty input hash and uncle_hash fallback", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + transaction_hash ); + } + else + { + source_input_hash = source_hash.value(); + } + + std::vector source_utxos; + source_utxos.emplace_back( source_input_hash, 0, amount, tokenid, account_m->GetAddress() ); + auto mint_inputs = account_m->CreateInputsFromUTXOs( source_utxos ); + auto mint_transaction = std::make_shared( MintTransactionV2::New( amount, std::move( chainid ), tokenid, FillDAGStruct( std::move( transaction_hash ) ), + std::move( mint_inputs ), destination ) ); mint_transaction->MakeSignature( *account_m ); @@ -456,6 +614,36 @@ namespace sgns return txId; } + outcome::result TransactionManager::MigrationFunds( uint64_t amount, + std::string from_version, + TokenID tokenid, + std::string destination ) + { + if ( GetState() != State::READY ) + { + return outcome::failure( boost::system::error_code{} ); + } + if ( destination.empty() ) + { + destination = account_m->GetAddress(); + } + + auto migration_transaction = std::make_shared( + MigrationTransaction::New( amount, + std::move( from_version ), + tokenid, + FillDAGStruct(), + destination ) ); + + migration_transaction->MakeSignature( *account_m ); + + auto txId = migration_transaction->GetHash(); + + EnqueueTransaction( std::make_pair( std::move( migration_transaction ), std::nullopt ) ); + + return txId; + } + outcome::result> TransactionManager::HoldEscrow( uint64_t amount, const std::string &dev_addr, uint64_t peers_cut, @@ -465,19 +653,18 @@ namespace sgns { return outcome::failure( boost::system::error_code{} ); } - auto hash_data = hasher_m->blake2b_256( std::vector{ job_id.begin(), job_id.end() } ); - - BOOST_OUTCOME_TRY( auto params, - account_m->GetUTXOManager().CreateTxParameter( amount, - "0x" + hash_data.toReadableString(), - TokenID::FromBytes( { 0x00 } ) ) ); - auto [inputs, outputs] = params; - account_m->GetUTXOManager().ReserveUTXOs( inputs ); + auto hash_data = hasher_m->blake2b_256( std::vector{ job_id.begin(), job_id.end() } ); + const std::string lock_id = "0x" + hash_data.toReadableString(); + BOOST_OUTCOME_TRY( + auto params, + account_m->GetUTXOManager().CreateTxParameter( amount, lock_id, TokenID::FromBytes( { 0x00 } ) ) ); + auto [inputs, outputs] = params; auto escrow_transaction = std::make_shared( - EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct() ) ); + EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct( lock_id ) ) ); escrow_transaction->MakeSignature( *account_m ); + account_m->GetUTXOManager().ReserveUTXOs( inputs, escrow_transaction->GetHash() ); // Get the transaction ID for tracking auto txId = escrow_transaction->GetHash(); @@ -488,8 +675,7 @@ namespace sgns data_transaction.put( escrow_transaction->SerializeByteVector() ); // Return both the transaction ID and the original EscrowDataPair - return std::make_pair( txId, - std::make_pair( "0x" + hash_data.toReadableString(), std::move( data_transaction ) ) ); + return std::make_pair( txId, std::make_pair( lock_id, std::move( data_transaction ) ) ); } outcome::result TransactionManager::PayEscrow( @@ -499,26 +685,32 @@ namespace sgns { if ( task_result.subtask_results().size() == 0 ) { - m_logger->error( "[{} - full: {}] No result found on escrow {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - escrow_path ); + TransactionManagerLogger()->error( "[{} - full: {}] No result found on escrow {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + escrow_path ); return std::errc::invalid_argument; } if ( escrow_path.empty() ) { - m_logger->error( "[{} - full: {}] Escrow path empty", account_m->GetAddress().substr( 0, 8 ), full_node_m ); + TransactionManagerLogger()->error( "[{} - full: {}] Escrow path empty", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return std::errc::invalid_argument; } - m_logger->debug( "[{} - full: {}] Fetching escrow from processing DB at {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - escrow_path ); + TransactionManagerLogger()->debug( "[{} - full: {}] Fetching escrow from processing DB at {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + escrow_path ); BOOST_OUTCOME_TRY( auto transaction, FetchTransaction( globaldb_m, escrow_path ) ); std::shared_ptr escrow_tx = std::dynamic_pointer_cast( transaction ); - std::vector subtask_ids; - std::vector payout_peers; + if ( crdt_transaction && escrow_tx && !escrow_tx->GetSrcAddress().empty() ) + { + BOOST_OUTCOME_TRY( crdt_transaction->AddTopic( escrow_tx->GetSrcAddress() ) ); + } + std::vector subtask_ids; + std::vector payout_peers; BOOST_OUTCOME_TRY( auto escrow_amount_ptr, TokenAmount::New( escrow_tx->GetAmount() ) ); @@ -533,11 +725,11 @@ namespace sgns for ( auto &subtask : task_result.subtask_results() ) { - m_logger->debug( "[{} - full: {}] Paying out {} in {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - peers_amount, - subtask.token_id() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Paying out {} in {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + peers_amount, + subtask.token_id() ); subtask_ids.push_back( subtask.subtaskid() ); payout_peers.push_back( { peers_amount, subtask.node_address(), @@ -545,10 +737,10 @@ namespace sgns remainder -= peers_amount; } //TODO: see what do with token_id here - m_logger->debug( "[{} - full: {}] Sending to dev {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - remainder ); + TransactionManagerLogger()->debug( "[{} - full: {}] Sending to dev {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + remainder ); payout_peers.push_back( { remainder, escrow_tx->GetDevAddress(), escrowTokenId } ); InputUTXOInfo escrow_utxo_input; @@ -556,44 +748,45 @@ namespace sgns escrow_utxo_input.output_idx_ = 0; escrow_utxo_input.signature_ = account_m->Sign( escrow_utxo_input.SerializeForSigning() ); - auto transfer_transaction = std::make_shared( - TransferTransaction::New( std::vector{ escrow_utxo_input }, payout_peers, FillDAGStruct() ) ); - - auto escrow_release_tx = std::make_shared( - EscrowReleaseTransaction::New( escrow_tx->GetUTXOParameters(), - escrow_tx->GetAmount(), - escrow_tx->GetDevAddress(), - escrow_tx->dag_st.source_addr(), - escrow_tx->GetHash(), - FillDAGStruct() ) ); + std::string lock_id = escrow_tx->GetUncleHash(); + if ( lock_id.empty() && !escrow_tx->GetUTXOParameters().second.empty() ) + { + lock_id = escrow_tx->GetUTXOParameters().second[0].dest_address; + TransactionManagerLogger()->warn( + "[{} - full: {}] Escrow transaction {} has empty lock_id but has UTXO parameters - using dest_address as fallback lock_id: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + escrow_tx->GetHash(), + lock_id ); + } - TransactionBatch tx_batch; + auto transfer_transaction = std::make_shared( + TransferTransaction::New( std::vector{ escrow_utxo_input }, payout_peers, FillDAGStruct( lock_id ) ) ); transfer_transaction->MakeSignature( *account_m ); - escrow_release_tx->MakeSignature( *account_m ); - - tx_batch.emplace_back( transfer_transaction, std::nullopt ); - tx_batch.emplace_back( escrow_release_tx, std::nullopt ); + TransactionBatch tx_batch; + tx_batch.push_back( std::make_pair( transfer_transaction, std::nullopt ) ); EnqueueTransaction( std::make_pair( tx_batch, std::move( crdt_transaction ) ) ); return transfer_transaction->GetHash(); } void TransactionManager::EnqueueTransaction( TransactionItem element ) { - m_logger->debug( "[{} - full: {}] Transaction enqueuing", account_m->GetAddress().substr( 0, 8 ), full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Transaction enqueuing", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); { - std::unique_lock tx_lock( tx_mutex_m ); for ( auto &&[tx, _] : element.first ) { - const auto key = GetTransactionPath( *tx ); - const auto nonce = tx->dag_st.nonce(); - // tx visible to status queries immediately - tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::CREATED, nonce }; - m_logger->debug( "[{} - full: {}] Setting {} to CREATED", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx->GetHash() ); + auto result = ChangeTransactionState( tx, TransactionStatus::CREATED ); + if ( !result ) + { + TransactionManagerLogger()->error( "[{} - full: {}] Failed to change transaction state for {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx->GetHash() ); + } } } std::lock_guard lock( mutex_m ); @@ -606,87 +799,264 @@ namespace sgns } //TODO - Fill hash stuff on DAGStruct - SGTransaction::DAGStruct TransactionManager::FillDAGStruct( std::string transaction_hash ) const + SGTransaction::DAGStruct TransactionManager::FillDAGStruct( std::optional other_chain_hash ) { SGTransaction::DAGStruct dag; - auto timestamp = std::chrono::system_clock::now(); + std::string chain_hash; + const auto nonce = account_m->ReserveNextNonce(); + auto previous_hash = GetOutgoingPreviousHash( nonce ); + auto timestamp = std::chrono::system_clock::now(); + + if ( other_chain_hash.has_value() ) + { + chain_hash = std::move( other_chain_hash.value() ); + } - dag.set_previous_hash( transaction_hash ); - dag.set_nonce( account_m->ReserveNextNonce() ); + dag.set_previous_hash( previous_hash ); + dag.set_nonce( nonce ); dag.set_source_addr( account_m->GetAddress() ); dag.set_timestamp( std::chrono::duration_cast( timestamp.time_since_epoch() ).count() ); - dag.set_uncle_hash( "" ); - dag.set_data_hash( "" ); //filled by transaction class + dag.set_uncle_hash( chain_hash ); return dag; } - outcome::result> TransactionManager::SendTransactionItem( TransactionItem &item ) + std::string TransactionManager::GetOutgoingPreviousHash( uint64_t nonce ) const { - std::unordered_set nonces_set; - auto [transaction_batch, maybe_crdt_transaction] = item; - std::shared_ptr crdt_transaction = nullptr; + if ( nonce == 0 ) + { + return ""; + } - m_logger->trace( "{} called", __func__ ); + auto tracked_hash = GetTrackedOutgoingPreviousHash( nonce ); + if ( !tracked_hash.empty() ) + { + return tracked_hash; + } - if ( maybe_crdt_transaction.has_value() && maybe_crdt_transaction.value() ) + auto persisted_hash = GetPersistedOutgoingPreviousHash( nonce ); + if ( !persisted_hash.empty() ) { - crdt_transaction = std::move( maybe_crdt_transaction.value() ); + return persisted_hash; } - else + + return QueryOutgoingPreviousHashFromCRDT( nonce ); + } + + std::string TransactionManager::GetTrackedOutgoingPreviousHash( uint64_t nonce ) const + { { - crdt_transaction = globaldb_m->BeginTransaction(); + std::shared_lock tx_lock( tx_mutex_m ); + for ( const auto &[_, tracked] : tx_processed_m ) + { + if ( !tracked.tx ) + { + continue; + } + if ( tracked.tx->GetSrcAddress() != account_m->GetAddress() ) + { + continue; + } + if ( tracked.cached_nonce != ( nonce - 1 ) ) + { + continue; + } + if ( tracked.status == TransactionStatus::FAILED || tracked.status == TransactionStatus::INVALID ) + { + continue; + } + return tracked.tx->GetHash(); + } } - auto nonce_result = account_m->GetConfirmedNonce( NONCE_REQUEST_TIMEOUT_MS ); - uint64_t expected_next_nonce = 0; - int64_t confirmed_nonce = -1; + return ""; + } - if ( nonce_result.has_value() ) + std::string TransactionManager::GetPersistedOutgoingPreviousHash( uint64_t nonce ) const + { + if ( nonce == 0 ) + { + return ""; + } + + auto persisted_hash_result = account_m->GetLocalConfirmedTxHash( nonce - 1 ); + if ( persisted_hash_result.has_error() ) + { + return ""; + } + + const auto &persisted_hash = persisted_hash_result.value(); + if ( persisted_hash.empty() || !blockchain_->CheckCertificate( persisted_hash ) ) + { + return ""; + } + + TransactionManagerLogger()->debug( + "[{} - full: {}] Recovered previous hash {} for nonce {} from persisted head", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + persisted_hash, + nonce ); + return persisted_hash; + } + + std::string TransactionManager::QueryOutgoingPreviousHashFromCRDT( uint64_t nonce ) const + { + if ( nonce == 0 ) { - confirmed_nonce = static_cast( nonce_result.value() ); - m_logger->debug( "[{} - full: {}] Set nonce to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - confirmed_nonce ); - expected_next_nonce = static_cast( confirmed_nonce ) + 1; + return ""; } - else if ( nonce_result.has_error() && nonce_result.error() == AccountMessenger::Error::NO_RESPONSE_RECEIVED ) + + const uint64_t expected_previous_nonce = nonce - 1; + std::string selected_hash; + auto monitored_networks = GetMonitoredNetworkIDs(); + for ( auto network_id : monitored_networks ) { - if ( !full_node_m ) + const std::string query_path = GetBlockChainBase( network_id ) + "tx"; + auto tx_list = globaldb_m->QueryKeyValues( query_path ); + if ( !tx_list.has_value() ) { - m_logger->error( "[{} - full: {}] {}: Network unreachable when fetching nonce", - __func__, - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return outcome::failure( boost::system::errc::make_error_code( boost::system::errc::timed_out ) ); + continue; } - m_logger->warn( "[{} - full: {}] Could not fetch nonce, but proceeding since full node", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - if ( auto local_confirmed = account_m->GetLocalConfirmedNonce(); local_confirmed.has_value() ) + for ( const auto &[_, value] : tx_list.value() ) { - confirmed_nonce = static_cast( local_confirmed.value() ); + auto tx_result = DeSerializeTransaction( value ); + if ( !tx_result.has_value() || !tx_result.value() ) + { + continue; + } + + const auto &candidate = tx_result.value(); + if ( candidate->GetSrcAddress() != account_m->GetAddress() || + candidate->GetNonce() != expected_previous_nonce ) + { + continue; + } + + if ( !blockchain_->CheckCertificate( candidate->GetHash() ) ) + { + continue; + } + + if ( selected_hash.empty() || + blockchain_->BestHash( selected_hash, candidate->GetHash() ) == candidate->GetHash() ) + { + selected_hash = candidate->GetHash(); + } + } + } + + if ( !selected_hash.empty() ) + { + TransactionManagerLogger()->debug( + "[{} - full: {}] Recovered previous hash {} for nonce {} from persisted transactions", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + selected_hash, + nonce ); + return selected_hash; + } + return ""; + } - m_logger->debug( "[{} - full: {}] Using local confirmed nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - local_confirmed.value() ); - expected_next_nonce = static_cast( confirmed_nonce ) + 1; + std::string TransactionManager::GetValidationChainId( const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return std::string( GENIUS_CHAIN_ID ); + } + const auto chain_id = tx->GetChainId(); + if ( chain_id.empty() ) + { + if ( tx->GetType() == "mint-v2" ) + { + return "public"; } + return std::string( GENIUS_CHAIN_ID ); + } + return chain_id; + } + + const IInputValidator &TransactionManager::GetInputValidator( const std::string &chain_id ) const + { + if ( chain_id.empty() || chain_id == GENIUS_CHAIN_ID ) + { + return genius_input_validator_; + } + + return public_chain_input_validator_; + } + + outcome::result TransactionManager::SendTransactionItem( TransactionItem &item ) + { + auto [transaction_batch, maybe_crdt_transaction] = item; + std::shared_ptr crdt_transaction = nullptr; + + TransactionManagerLogger()->trace( "{} called", __func__ ); + + if ( maybe_crdt_transaction.has_value() && maybe_crdt_transaction.value() ) + { + crdt_transaction = std::move( maybe_crdt_transaction.value() ); + } + else + { + crdt_transaction = globaldb_m->BeginTransaction(); + } + std::optional expected_next_nonce; + if ( auto local_confirmed = account_m->GetLocalConfirmedNonce(); local_confirmed.has_value() ) + { + expected_next_nonce = local_confirmed.value() + 1; + TransactionManagerLogger()->debug( "[{} - full: {}] Using local confirmed nonce {} as send baseline", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + local_confirmed.value() ); + } + else if ( !transaction_batch.empty() ) + { + // If confirmed nonce is not available yet, preserve local enqueue order. + expected_next_nonce = transaction_batch.front().first->GetNonce(); + TransactionManagerLogger()->debug( "[{} - full: {}] Local confirmed nonce unavailable, using first " + "queued nonce {} as send baseline", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + expected_next_nonce.value() ); + } + std::unordered_set topicSet; + std::set> transactions_sent; + if ( !transaction_batch.empty() ) + { + topicSet.emplace( full_node_topic_m ); + topicSet.emplace( account_m->GetAddress() ); } for ( auto &[transaction, maybe_proof] : transaction_batch ) { - if ( transaction->dag_st.nonce() != expected_next_nonce ) + if ( !expected_next_nonce.has_value() ) + { + expected_next_nonce = transaction->GetNonce(); + } + + if ( transaction->GetNonce() != expected_next_nonce.value() ) { - m_logger->error( "[{} - full: {}] Transaction with unexpected nonce - Expected: {}, Tried to send: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - expected_next_nonce, - transaction->dag_st.nonce() ); + if ( transaction->GetNonce() > expected_next_nonce.value() ) + { + TransactionManagerLogger()->debug( + "[{} - full: {}] Deferring transaction send due to nonce gap - Expected: {}, Tried to send: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + expected_next_nonce.value(), + transaction->GetNonce() ); + return outcome::failure( + boost::system::errc::make_error_code( boost::system::errc::resource_unavailable_try_again ) ); + } + TransactionManagerLogger()->error( + "[{} - full: {}] Transaction with unexpected nonce - Expected: {}, Tried to send: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + expected_next_nonce.value(), + transaction->GetNonce() ); return outcome::failure( boost::system::errc::make_error_code( boost::system::errc::invalid_argument ) ); } @@ -695,10 +1065,10 @@ namespace sgns crdt::HierarchicalKey tx_key( transaction_path ); crdt::GlobalDB::Buffer data_transaction; - m_logger->debug( "[{} - full: {}] Recording the transaction on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key.GetKey() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Recording the transaction on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key.GetKey() ); data_transaction.put( transaction->SerializeByteVector() ); BOOST_OUTCOME_TRY( crdt_transaction->Put( std::move( tx_key ), std::move( data_transaction ) ) ); @@ -709,137 +1079,87 @@ namespace sgns crdt::GlobalDB::Buffer proof_transaction; auto &proof = maybe_proof.value(); - m_logger->debug( "[{} - full: {}] Recording the proof on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - proof_key.GetKey() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Recording the proof on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + proof_key.GetKey() ); proof_transaction.put( proof ); BOOST_OUTCOME_TRY( crdt_transaction->Put( std::move( proof_key ), std::move( proof_transaction ) ) ); } - nonces_set.insert( transaction->dag_st.nonce() ); - expected_next_nonce++; + TransactionManagerLogger()->debug( "[{} - full: {}] Creating Consensus Proposal for tx {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_path ); + + topicSet.merge( transaction->GetTopics() ); + transactions_sent.insert( transaction ); + + expected_next_nonce = expected_next_nonce.value() + 1; } - std::unordered_set topicSet; - if ( !transaction_batch.empty() ) - { - topicSet.emplace( full_node_topic_m ); - topicSet.emplace( account_m->GetAddress() ); - } - for ( auto &[tx, _] : transaction_batch ) + BOOST_OUTCOME_TRY( crdt_transaction->Commit( topicSet ) ); + + for ( auto &transaction : transactions_sent ) { - BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); - topicSet.merge( tx->GetTopics() ); - std::unique_lock tx_lock( tx_mutex_m ); - const auto key = GetTransactionPath( *tx ); - const auto nonce = tx->dag_st.nonce(); - auto it = tx_processed_m.find( key ); - auto tx_state = TransactionStatus::VERIFYING; - if ( full_node_m ) - { - tx_state = TransactionStatus::CONFIRMED; - } - if ( it != tx_processed_m.end() ) + const auto chain_id = GetValidationChainId( transaction ); + const auto &validator = GetInputValidator( chain_id ); + const bool utxo_data_required = validator.RequiresConsensusUTXOData(); + + std::optional utxo_commitment; + std::optional utxo_witness; + + if ( transaction->HasUTXOParameters() ) { - if ( it->second.status != tx_state && tx_state == TransactionStatus::VERIFYING ) + utxo_commitment = BuildUTXOTransitionCommitment( transaction ); + if ( !utxo_commitment.has_value() ) { - verifying_count_.fetch_add( 1, std::memory_order_relaxed ); + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Missing required UTXO commitment for tx={} type={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + transaction->GetHash(), + transaction->GetType() ); + return outcome::failure( std::errc::invalid_argument ); } - it->second.status = tx_state; - } - else - { - tx_processed_m[key] = TrackedTx{ tx, tx_state, nonce }; - if ( tx_state == TransactionStatus::VERIFYING ) + + if ( utxo_data_required ) { - verifying_count_.fetch_add( 1, std::memory_order_relaxed ); + utxo_witness = BuildUTXOWitness( transaction ); + if ( !utxo_witness.has_value() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Missing required UTXO witness for tx={} type={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + transaction->GetHash(), + transaction->GetType() ); + return outcome::failure( std::errc::invalid_argument ); + } } } - } - BOOST_OUTCOME_TRY( crdt_transaction->Commit( topicSet ) ); + BOOST_OUTCOME_TRY( auto &&proposal, + blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), + transaction->GetNonce(), + transaction->GetHash(), + utxo_commitment, + utxo_witness ) ); + BOOST_OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); + BOOST_OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); + } - return nonces_set; + return outcome::success(); } outcome::result TransactionManager::RollbackTransactions( TransactionItem &item_to_rollback ) { - int64_t confirmed_nonce = -1; - - if ( auto nonce_result = account_m->GetConfirmedNonce( NONCE_REQUEST_TIMEOUT_MS ); nonce_result.has_value() ) - { - confirmed_nonce = static_cast( nonce_result.value() ); - m_logger->debug( "[{} - full: {}] Set nonce to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - confirmed_nonce ); - } - else - { - m_logger->error( "[{} - full: {}] {}: Could not fetch confirmed nonce ({}). Attempting rollback with " - "local state", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - nonce_result.error().message() ); - auto local_nonce_result = account_m->GetLocalConfirmedNonce(); - if ( local_nonce_result.has_value() ) - { - confirmed_nonce = static_cast( local_nonce_result.value() ); - m_logger->debug( "[{} - full: {}] Falling back to local confirmed nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - confirmed_nonce ); - } - else - { - m_logger->error( "[{} - full: {}] No local confirmed nonce available, rolling back assuming none", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - confirmed_nonce = -1; - } - } - - auto [transaction_batch, _dontcare] = item_to_rollback; - - for ( auto &[transaction, __dontcare] : transaction_batch ) + auto [transaction_batch, _] = item_to_rollback; + for ( auto &[transaction, maybe_proof] : transaction_batch ) { - auto signed_previous_nonce = static_cast( transaction->dag_st.nonce() ) - 1; - - for ( auto tx_nonce = signed_previous_nonce; tx_nonce > confirmed_nonce; --tx_nonce ) - { - //let's verify if we didn't mistakenly confirm any bad transactions. - m_logger->debug( "[{} - full: {}] Setting \"VERIFYING\" status to transaction with nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_nonce ); - (void)SetOutgoingStatusByNonce( static_cast( tx_nonce ), TransactionStatus::VERIFYING ); - } - { - std::unique_lock tx_lock( tx_mutex_m ); - const auto key = GetTransactionPath( *transaction ); - const auto nonce = transaction->dag_st.nonce(); - - if ( auto it = tx_processed_m.find( key ); it != tx_processed_m.end() ) - { - // Update verifying_count if status is changing from VERIFYING - if ( it->second.status == TransactionStatus::VERIFYING ) - { - verifying_count_.fetch_sub( 1, std::memory_order_relaxed ); - } - it->second.tx = transaction; - it->second.status = TransactionStatus::FAILED; - it->second.cached_nonce = nonce; - } - else - { - // New entry rolled back: start directly as FAILED - tx_processed_m.emplace( key, TrackedTx{ transaction, TransactionStatus::FAILED, nonce } ); - } - } - RemoveTransactionFromProcessedMaps( GetTransactionPath( *transaction ) ); - account_m->ReleaseNonce( transaction->dag_st.nonce() ); + BOOST_OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::FAILED ) ); } return outcome::success(); } @@ -954,13 +1274,18 @@ namespace sgns auto it = transaction_parsers.find( tx->GetType() ); if ( it == transaction_parsers.end() ) { - m_logger->info( "[{} - full: {}] No Parser Available", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->info( "[{} - full: {}] No Parser Available", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return std::errc::invalid_argument; } - return ( this->*it->second.first )( tx ); + BOOST_OUTCOME_TRY( ( this->*it->second.first )( tx ) ); + if ( DoesTransactionMutateUTXOState( tx ) && utxo_state_tracking_suppression_.load() == 0 ) + { + UpdateAccountUTXOState( CollectTouchedAccounts( tx ), true ); + } + return outcome::success(); } outcome::result TransactionManager::RevertTransaction( const std::shared_ptr &tx ) @@ -968,13 +1293,134 @@ namespace sgns auto it = transaction_parsers.find( tx->GetType() ); if ( it == transaction_parsers.end() ) { - m_logger->info( "[{} - full: {}] No Reverter Available", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->info( "[{} - full: {}] No Reverter Available", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return std::errc::invalid_argument; } - return ( this->*( it->second.second ) )( tx ); + utxo_state_tracking_suppression_.fetch_add( 1 ); + auto revert_result = ( this->*( it->second.second ) )( tx ); + utxo_state_tracking_suppression_.fetch_sub( 1 ); + BOOST_OUTCOME_TRY( revert_result ); + if ( DoesTransactionMutateUTXOState( tx ) && utxo_state_tracking_suppression_.load() == 0 ) + { + UpdateAccountUTXOState( CollectTouchedAccounts( tx ), false ); + } + return outcome::success(); + } + + bool TransactionManager::DoesTransactionMutateUTXOState( const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return false; + } + + if ( tx->HasUTXOParameters() ) + { + return true; + } + + // Legacy mint transactions still create UTXOs for the source account. + return tx->GetType() == "mint"; + } + + std::unordered_set TransactionManager::CollectTouchedAccounts( + const std::shared_ptr &tx ) const + { + std::unordered_set addresses; + if ( !tx ) + { + return addresses; + } + + if ( tx->HasUTXOParameters() ) + { + auto params_opt = tx->GetUTXOParametersOpt(); + if ( params_opt.has_value() ) + { + const auto &[inputs, outputs] = params_opt.value(); + if ( !inputs.empty() ) + { + if ( full_node_m || tx->GetSrcAddress() == account_m->GetAddress() ) + { + addresses.insert( tx->GetSrcAddress() ); + } + } + for ( const auto &output : outputs ) + { + if ( !output.dest_address.empty() && + ( full_node_m || output.dest_address == account_m->GetAddress() ) ) + { + addresses.insert( output.dest_address ); + } + } + } + } + else if ( tx->GetType() == "mint" && !tx->GetSrcAddress().empty() && + ( full_node_m || tx->GetSrcAddress() == account_m->GetAddress() ) ) + { + addresses.insert( tx->GetSrcAddress() ); + } + + return addresses; + } + + TransactionManager::AccountUTXOState TransactionManager::GetOrInitAccountUTXOState( + const std::string &address ) const + { + const auto current_root = account_m->GetUTXOManager().ComputeUTXOMerkleRoot( address ); + + std::unique_lock state_lock( account_utxo_state_mutex_ ); + auto &state = account_utxo_state_[address]; + if ( !state.initialized ) + { + state.version = 0; + state.initialized = true; + } + state.root = current_root; + return state; + } + + void TransactionManager::UpdateAccountUTXOState( const std::unordered_set &addresses, + bool increment_version ) + { + if ( addresses.empty() ) + { + return; + } + + std::unordered_map roots; + roots.reserve( addresses.size() ); + for ( const auto &address : addresses ) + { + if ( !full_node_m && address != account_m->GetAddress() ) + { + continue; + } + roots.emplace( address, account_m->GetUTXOManager().ComputeUTXOMerkleRoot( address ) ); + } + + std::unique_lock state_lock( account_utxo_state_mutex_ ); + for ( const auto &[address, root] : roots ) + { + auto &state = account_utxo_state_[address]; + if ( !state.initialized ) + { + state.version = 0; + state.initialized = true; + } + if ( increment_version ) + { + state.version++; + } + else if ( state.version > 0 ) + { + state.version--; + } + state.root = root; + } } outcome::result> TransactionManager::FetchTransaction( @@ -1004,17 +1450,17 @@ namespace sgns outcome::result TransactionManager::CheckProof( const std::shared_ptr &tx ) { auto proof_path = GetTransactionProofPath( *tx ); - m_logger->debug( "[{} - full: {}] Checking the proof in {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - proof_path ); + TransactionManagerLogger()->debug( "[{} - full: {}] Checking the proof in {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + proof_path ); BOOST_OUTCOME_TRY( auto proof_data, globaldb_m->Get( { proof_path } ) ); auto proof_data_vector = proof_data.toVector(); - m_logger->debug( "[{} - full: {}] Proof data acquired. Verifying...", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Proof data acquired. Verifying...", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return IBasicProof::VerifyFullProof( proof_data_vector ); } @@ -1026,34 +1472,34 @@ namespace sgns { std::string blockchain_base = GetBlockChainBase( network_id ); std::string query_path = blockchain_base + "tx"; - m_logger->trace( "[{} - full: {}] Probing transactions on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - query_path ); + TransactionManagerLogger()->trace( "[{} - full: {}] Probing transactions on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + query_path ); BOOST_OUTCOME_TRY( auto transaction_list, globaldb_m->QueryKeyValues( query_path ) ); - m_logger->trace( "[{} - full: {}] Transaction list grabbed from CRDT with Size {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_list.size() ); + TransactionManagerLogger()->trace( "[{} - full: {}] Transaction list grabbed from CRDT with Size {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_list.size() ); for ( const auto &[key, value] : transaction_list ) { auto transaction_key = globaldb_m->KeyToString( key ); if ( !transaction_key.has_value() ) { - m_logger->error( "[{} - full: {}] Unable to convert a key to string", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->error( "[{} - full: {}] Unable to convert a key to string", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); continue; } auto process_result = FetchAndProcessTransaction( transaction_key.value(), value ); if ( !transaction_key.has_value() ) { - m_logger->error( "[{} - full: {}] Unable to fetch and process transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_key.value() ); + TransactionManagerLogger()->error( "[{} - full: {}] Unable to fetch and process transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_key.value() ); } } } @@ -1069,15 +1515,10 @@ namespace sgns auto tracked = tx_processed_m.find( tx_key ); if ( tracked != tx_processed_m.end() ) { - if ( tracked->second.tx ) - { - std::lock_guard missing_lock( missing_tx_mutex_ ); - missing_tx_hashes_.erase( tracked->second.tx->GetHash() ); - } - m_logger->trace( "[{} - full: {}] Transaction already processed: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->trace( "[{} - full: {}] Transaction already processed: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); return outcome::success(); } } @@ -1086,61 +1527,57 @@ namespace sgns { if ( tx_data.has_value() ) { - m_logger->debug( "[{} - full: {}] Deserializing transaction: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Deserializing transaction: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); return DeSerializeTransaction( tx_data.value() ); } - m_logger->debug( "[{} - full: {}] Finding transaction: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Finding transaction: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); return FetchTransaction( globaldb_m, tx_key ); }(); if ( transaction_result.has_error() ) { - m_logger->debug( "[{} - full: {}] Can't fetch transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Can't fetch transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); return outcome::failure( transaction_result.error() ); } auto &transaction = transaction_result.value(); if ( transaction->GetHash().empty() ) { - m_logger->error( "[{} - full: {}] Error, received transaction without hash: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->error( "[{} - full: {}] Error, received transaction without hash: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); return outcome::failure( std::errc::invalid_argument ); } - auto maybe_parsed = ParseTransaction( transaction ); - if ( maybe_parsed.has_error() ) - { - m_logger->debug( "[{} - full: {}] Can't parse the transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); - return outcome::failure( maybe_parsed.error() ); - } - - const auto nonce = transaction->dag_st.nonce(); + TransactionManagerLogger()->debug( + "[{} - full: {}] Checking if the transaction has a valid certificate to be confirmed {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); - account_m->SetPeerConfirmedNonce( nonce, transaction->dag_st.source_addr() ); + auto next_tx_state = TransactionStatus::VERIFYING; + if ( blockchain_->CheckCertificate( transaction->GetHash() ) ) { - std::unique_lock tx_lock( tx_mutex_m ); - tx_processed_m[tx_key] = TrackedTx{ transaction, TransactionStatus::CONFIRMED, nonce }; - } - { - std::lock_guard missing_lock( missing_tx_mutex_ ); - missing_tx_hashes_.erase( transaction->GetHash() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Transaction has a valid certificate, marking as CONFIRMED {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); + next_tx_state = TransactionStatus::CONFIRMED; } + BOOST_OUTCOME_TRY( ChangeTransactionState( transaction, next_tx_state ) ); return outcome::success(); } @@ -1156,23 +1593,23 @@ namespace sgns GeniusUTXO new_utxo( hash, i, dest_infos[i].encrypted_amount, dest_infos[i].token_id ); BOOST_OUTCOME_TRY( account_m->GetUTXOManager().PutUTXO( new_utxo, dest_infos[i].dest_address ) ); - m_logger->debug( "[{} - full: {}] Notify {} of transfer of {} to it", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - dest_infos[i].dest_address, - dest_infos[i].encrypted_amount ); + TransactionManagerLogger()->debug( "[{} - full: {}] Notify {} of transfer of {} to it", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + dest_infos[i].dest_address, + dest_infos[i].encrypted_amount ); } for ( auto &input : transfer_tx->GetInputInfos() ) { - m_logger->trace( "[{} - full: {}] UTXO to be updated {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - input.txid_hash_.toReadableString() ); - m_logger->trace( "[{} - full: {}] UTXO output {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - input.output_idx_ ); + TransactionManagerLogger()->trace( "[{} - full: {}] UTXO to be updated {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + input.txid_hash_.toReadableString() ); + TransactionManagerLogger()->trace( "[{} - full: {}] UTXO output {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + input.output_idx_ ); } BOOST_OUTCOME_TRY( account_m->GetUTXOManager().ConsumeUTXOs( transfer_tx->GetInputInfos(), transfer_tx->GetSrcAddress() ) ); @@ -1181,6 +1618,29 @@ namespace sgns outcome::result TransactionManager::ParseMintTransaction( const std::shared_ptr &tx ) { + if ( auto migration_tx = std::dynamic_pointer_cast( tx ) ) + { + auto [inputs, outputs] = migration_tx->GetUTXOParameters(); + auto hash = ( base::Hash256::fromReadableString( migration_tx->GetHash() ) ).value(); + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) + { + GeniusUTXO new_utxo( hash, i, outputs[i].encrypted_amount, outputs[i].token_id ); + account_m->GetUTXOManager().PutUTXO( new_utxo, outputs[i].dest_address ); + } + + if ( !inputs.empty() ) + { + account_m->GetUTXOManager().ConsumeUTXOs( inputs, migration_tx->GetSrcAddress() ); + } + + TransactionManagerLogger()->info( "[{} - full: {}] Created tokens (migration), amount {} balance {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + std::to_string( migration_tx->GetAmount() ), + std::to_string( account_m->GetUTXOManager().GetBalance() ) ); + return outcome::success(); + } + if ( auto mint_tx_v2 = std::dynamic_pointer_cast( tx ) ) { auto [inputs, outputs] = mint_tx_v2->GetUTXOParameters(); @@ -1196,11 +1656,11 @@ namespace sgns account_m->GetUTXOManager().ConsumeUTXOs( inputs, mint_tx_v2->GetSrcAddress() ); } - m_logger->info( "[{} - full: {}] Created tokens (mint-v2), amount {} balance {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - std::to_string( mint_tx_v2->GetAmount() ), - std::to_string( account_m->GetUTXOManager().GetBalance() ) ); + TransactionManagerLogger()->info( "[{} - full: {}] Created tokens (mint-v2), amount {} balance {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + std::to_string( mint_tx_v2->GetAmount() ), + std::to_string( account_m->GetUTXOManager().GetBalance() ) ); return outcome::success(); } @@ -1214,11 +1674,11 @@ namespace sgns BOOST_OUTCOME_TRY( account_m->GetUTXOManager().PutUTXO( GeniusUTXO( hash, 0, mint_tx->GetAmount(), mint_tx->GetTokenID() ), mint_tx->GetSrcAddress() ) ); - m_logger->info( "[{} - full: {}] Created tokens, amount {} balance {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - std::to_string( mint_tx->GetAmount() ), - std::to_string( account_m->GetUTXOManager().GetBalance() ) ); + TransactionManagerLogger()->info( "[{} - full: {}] Created tokens, amount {} balance {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + std::to_string( mint_tx->GetAmount() ), + std::to_string( account_m->GetUTXOManager().GetBalance() ) ); return outcome::success(); } @@ -1226,46 +1686,25 @@ namespace sgns outcome::result TransactionManager::ParseEscrowTransaction( const std::shared_ptr &tx ) { auto escrow_tx = std::dynamic_pointer_cast( tx ); - - if ( escrow_tx->GetSrcAddress() == account_m->GetAddress() ) + if ( !escrow_tx ) { - auto [_, outputs] = escrow_tx->GetUTXOParameters(); - - if ( !outputs.empty() ) - { - //The first is the escrow, second is the change (might not happen) - auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); - if ( outputs.size() > 1 ) - { - GeniusUTXO new_utxo( hash, 1, outputs[1].encrypted_amount, outputs[1].token_id ); - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().PutUTXO( new_utxo, outputs[1].dest_address ) ); - } - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().ConsumeUTXOs( escrow_tx->GetUTXOParameters().first, - escrow_tx->GetSrcAddress() ) ); - } + return std::errc::invalid_argument; } - return outcome::success(); - } - - outcome::result TransactionManager::ParseEscrowReleaseTransaction( - const std::shared_ptr &tx ) - { - auto escrowReleaseTx = std::dynamic_pointer_cast( tx ); + auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); + auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); - if ( !escrowReleaseTx ) + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) { - m_logger->error( "[{} - full: {}] Failed to cast transaction to EscrowReleaseTransaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return std::errc::invalid_argument; + // output[0] is escrow hold, optional output[1] is change. + GeniusUTXO new_utxo( hash, i, outputs[i].encrypted_amount, outputs[i].token_id ); + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().PutUTXO( new_utxo, outputs[i].dest_address ) ); } - std::string originalEscrowHash = escrowReleaseTx->GetOriginalEscrowHash(); - m_logger->debug( "[{} - full: {}] Successfully fetched release for escrow: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - originalEscrowHash ); + if ( !inputs.empty() ) + { + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().ConsumeUTXOs( inputs, escrow_tx->GetSrcAddress() ) ); + } return outcome::success(); } @@ -1276,69 +1715,97 @@ namespace sgns auto transfer_tx = std::dynamic_pointer_cast( tx ); auto dest_infos = transfer_tx->GetDstInfos(); - for ( const auto &dest_info : dest_infos ) + for ( std::uint32_t i = 0; i < dest_infos.size(); ++i ) { - auto hash = ( base::Hash256::fromReadableString( transfer_tx->GetHash() ) ).value(); - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, dest_info.dest_address ) ); + const auto &dest_info = dest_infos[i]; + auto hash = ( base::Hash256::fromReadableString( transfer_tx->GetHash() ) ).value(); + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, i, dest_info.dest_address ) ); - m_logger->debug( "[{} - full: {}] Notify {} of deletion of {} to it", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - dest_info.dest_address, - dest_info.encrypted_amount ); + TransactionManagerLogger()->debug( "[{} - full: {}] Notify {} of deletion of {} to it", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + dest_info.dest_address, + dest_info.encrypted_amount ); } - m_logger->debug( "[{} - full: {}] Adding origin address to Broadcast: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transfer_tx->GetSrcAddress() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Adding origin address to Broadcast: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transfer_tx->GetSrcAddress() ); - m_logger->debug( "[{} - full: {}] Re-parsing inputs to be added as UTXOs", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Re-parsing inputs to be added as UTXOs", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); for ( const auto &input : transfer_tx->GetInputInfos() ) { - m_logger->debug( "[{} - full: {}] Fetching transaction {} ", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - input.txid_hash_.toReadableString() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Fetching transaction {} ", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + input.txid_hash_.toReadableString() ); auto tx = GetTransactionByHashNoLock( input.txid_hash_.toReadableString() ); if ( tx ) { - m_logger->debug( "[{} - full: {}] Re-parsing {} transaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx->GetType() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Re-parsing {} transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx->GetType() ); BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); } } - account_m->GetUTXOManager().RollbackUTXOs( transfer_tx->GetInputInfos() ); + account_m->GetUTXOManager().RollbackUTXOs( transfer_tx->GetInputInfos(), transfer_tx->GetHash() ); return outcome::success(); } outcome::result TransactionManager::RevertMintTransaction( const std::shared_ptr &tx ) { + if ( auto migration_tx = std::dynamic_pointer_cast( tx ) ) + { + auto [inputs, outputs] = migration_tx->GetUTXOParameters(); + auto hash = ( base::Hash256::fromReadableString( migration_tx->GetHash() ) ).value(); + + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) + { + const auto &dest_info = outputs[i]; + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, i, dest_info.dest_address ) ) + } + if ( !inputs.empty() ) + { + account_m->GetUTXOManager().RollbackUTXOs( inputs, tx->GetHash() ); + } + + TransactionManagerLogger()->info( + "[{} - full: {}] Deleted {} tokens (migration), from tx {}, final balance {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + migration_tx->GetAmount(), + migration_tx->GetHash(), + std::to_string( account_m->GetUTXOManager().GetBalance() ) ); + return outcome::success(); + } + if ( auto mint_tx_v2 = std::dynamic_pointer_cast( tx ) ) { auto [inputs, outputs] = mint_tx_v2->GetUTXOParameters(); auto hash = ( base::Hash256::fromReadableString( mint_tx_v2->GetHash() ) ).value(); - for ( const auto &dest_info : outputs ) + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) { - account_m->GetUTXOManager().DeleteUTXO( hash, dest_info.dest_address ); + const auto &dest_info = outputs[i]; + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, i, dest_info.dest_address ) ) } if ( !inputs.empty() ) { - account_m->GetUTXOManager().RollbackUTXOs( inputs ); + account_m->GetUTXOManager().RollbackUTXOs( inputs, tx->GetHash() ); } - m_logger->info( "[{} - full: {}] Deleted {} tokens (mint-v2), from tx {}, final balance {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - mint_tx_v2->GetAmount(), - mint_tx_v2->GetHash(), - std::to_string( account_m->GetUTXOManager().GetBalance() ) ); + TransactionManagerLogger()->info( + "[{} - full: {}] Deleted {} tokens (mint-v2), from tx {}, final balance {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + mint_tx_v2->GetAmount(), + mint_tx_v2->GetHash(), + std::to_string( account_m->GetUTXOManager().GetBalance() ) ); return outcome::success(); } @@ -1349,13 +1816,13 @@ namespace sgns } auto hash = ( base::Hash256::fromReadableString( mint_tx->GetHash() ) ).value(); - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, mint_tx->GetSrcAddress() ) ); - m_logger->info( "[{} - full: {}] Deleted {} tokens, from tx {}, final balance {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - mint_tx->GetAmount(), - mint_tx->GetHash(), - std::to_string( account_m->GetUTXOManager().GetBalance() ) ); + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, 0, mint_tx->GetSrcAddress() ) ); + TransactionManagerLogger()->info( "[{} - full: {}] Deleted {} tokens, from tx {}, final balance {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + mint_tx->GetAmount(), + mint_tx->GetHash(), + std::to_string( account_m->GetUTXOManager().GetBalance() ) ); return outcome::success(); } @@ -1363,58 +1830,36 @@ namespace sgns outcome::result TransactionManager::RevertEscrowTransaction( const std::shared_ptr &tx ) { auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return std::errc::invalid_argument; + } - if ( escrow_tx->GetSrcAddress() == account_m->GetAddress() ) + if ( auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); !outputs.empty() ) { - if ( auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); !outputs.empty() ) + auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) { - //The first is the escrow, second is the change (might not happen) - auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); - if ( outputs.size() > 1 ) - { - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, outputs[1].dest_address ) ); - } - for ( auto &input : inputs ) + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, i, outputs[i].dest_address ) ); + } + for ( auto &input : inputs ) + { + auto tx = GetTransactionByHashNoLock( input.txid_hash_.toReadableString() ); + if ( tx ) { - auto tx = GetTransactionByHashNoLock( input.txid_hash_.toReadableString() ); - if ( tx ) - { - m_logger->debug( "[{} - full: {}] Re-parsing {} transaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx->GetType() ); - BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); - } + TransactionManagerLogger()->debug( "[{} - full: {}] Re-parsing {} transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx->GetType() ); + BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); } - account_m->GetUTXOManager().RollbackUTXOs( inputs ); } + account_m->GetUTXOManager().RollbackUTXOs( inputs, escrow_tx->GetHash() ); } return outcome::success(); } - outcome::result TransactionManager::RevertEscrowReleaseTransaction( - const std::shared_ptr &tx ) - { - auto escrowReleaseTx = std::dynamic_pointer_cast( tx ); - - if ( !escrowReleaseTx ) - { - m_logger->error( "[{} - full: {}] Failed to cast transaction to EscrowReleaseTransaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return std::errc::invalid_argument; - } - - std::string originalEscrowHash = escrowReleaseTx->GetOriginalEscrowHash(); - m_logger->debug( "[{} - full: {}] Successfully fetched release for escrow: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - originalEscrowHash ); - - return outcome::success(); - } - std::vector> TransactionManager::GetOutTransactions() const { std::vector> result; @@ -1449,6 +1894,24 @@ namespace sgns return result; } + std::vector> TransactionManager::GetTransactions( + std::optional tx_status ) const + { + std::vector> result; + { + std::shared_lock tx_lock( tx_mutex_m ); + result.reserve( tx_processed_m.size() ); + for ( const auto &[_, value] : tx_processed_m ) + { + if ( !tx_status || value.status == tx_status.value() ) + { + result.push_back( value.tx->SerializeByteVector() ); + } + } + } + return result; + } + TransactionManager::TransactionStatus TransactionManager::WaitForTransactionIncoming( const std::string &txId, std::chrono::milliseconds timeout ) const @@ -1473,9 +1936,9 @@ namespace sgns if ( retval == TransactionStatus::CONFIRMED ) { - m_logger->debug( "[{} - full: {}] Transaction is FINALIZED", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Transaction is FINALIZED", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); break; } std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); @@ -1495,10 +1958,10 @@ namespace sgns { { std::shared_lock tx_lock( tx_mutex_m ); - m_logger->trace( "[{} - full: {}] Searching for transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - txId ); + TransactionManagerLogger()->trace( "[{} - full: {}] Searching for transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + txId ); bool found = false; for ( const auto &[_, tracked] : tx_processed_m ) { @@ -1506,19 +1969,19 @@ namespace sgns tracked.tx->GetSrcAddress() == account_m->GetAddress() ) { retval = tracked.status; - m_logger->trace( "[{} - full: {}] Transaction status is {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - static_cast( retval ) ); + TransactionManagerLogger()->trace( "[{} - full: {}] Transaction status is {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + static_cast( retval ) ); found = true; break; } } if ( !found ) { - m_logger->trace( "[{} - full: {}] Transaction untracked", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->trace( "[{} - full: {}] Transaction untracked", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); retval = TransactionStatus::FAILED; } } @@ -1526,10 +1989,10 @@ namespace sgns if ( retval == TransactionStatus::INVALID || retval == TransactionStatus::CONFIRMED || retval == TransactionStatus::FAILED ) { - m_logger->trace( "[{} - full: {}] Transaction has finalized state {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - static_cast( retval ) ); + TransactionManagerLogger()->trace( "[{} - full: {}] Transaction has finalized state {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + static_cast( retval ) ); break; } std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); @@ -1542,96 +2005,165 @@ namespace sgns const std::string &originalEscrowId, std::chrono::milliseconds timeout ) const { - auto start = std::chrono::steady_clock::now(); - auto retval = TransactionStatus::INVALID; + auto start = std::chrono::steady_clock::now(); + auto escrow_hash_result = base::Hash256::fromReadableString( originalEscrowId ); + if ( escrow_hash_result.has_error() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] Invalid original escrow tx id while waiting release: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowId ); + return TransactionStatus::INVALID; + } + const auto escrow_hash = escrow_hash_result.value(); - while ( std::chrono::steady_clock::now() - start < timeout ) + auto is_escrow_spent_by_confirmed_transfer = [this, &escrow_hash]() -> bool { + std::shared_lock tx_lock( tx_mutex_m ); + for ( const auto &[_, tracked] : tx_processed_m ) { - std::shared_lock tx_lock( tx_mutex_m ); - for ( const auto &[_, tracked] : tx_processed_m ) + if ( tracked.status != TransactionStatus::CONFIRMED || !tracked.tx || !tracked.tx->HasUTXOParameters() ) { - if ( !tracked.tx ) - { - continue; - } + continue; + } - if ( tracked.tx->GetType() == "escrow-release" ) - { - auto escrowReleaseTx = std::dynamic_pointer_cast( tracked.tx ); - if ( escrowReleaseTx && escrowReleaseTx->GetOriginalEscrowHash() == originalEscrowId ) - { - m_logger->debug( "[{} - full: {}] Found matching escrow release transaction with tx id: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tracked.tx->GetHash() ); - - retval = tracked.status; - - // If finalized, return immediately; otherwise keep waiting. - if ( retval == TransactionStatus::CONFIRMED || retval == TransactionStatus::FAILED || - retval == TransactionStatus::INVALID ) - { - return retval; - } - } - } + const auto params_opt = tracked.tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + continue; } - } - std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); - } + const auto &inputs = params_opt->first; + const bool spends_original_escrow = std::any_of( + inputs.begin(), + inputs.end(), + [&escrow_hash]( const InputUTXOInfo &input ) + { return input.txid_hash_ == escrow_hash && input.output_idx_ == 0; } ); - return retval; // Will be INVALID if not seen within timeout - } + if ( spends_original_escrow ) + { + return true; + } + } + return false; + }; - void TransactionManager::InitializeUTXOs() - { + while ( std::chrono::steady_clock::now() - start < timeout ) { - std::lock_guard missing_lock( missing_tx_mutex_ ); - missing_tx_hashes_.clear(); + if ( account_m->GetUTXOManager().IsOutPointConsumed( escrow_hash, 0 ) ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] Escrow hold ({},0) is consumed", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowId ); + return TransactionStatus::CONFIRMED; + } + + if ( is_escrow_spent_by_confirmed_transfer() ) + { + TransactionManagerLogger()->debug( + "[{} - full: {}] Escrow release confirmed via tracked transfer spend for {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowId ); + return TransactionStatus::CONFIRMED; + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); + } + + return TransactionStatus::INVALID; + } + + void TransactionManager::InitializeUTXOs() + { + { + std::lock_guard missing_lock( missing_tx_mutex_ ); + missing_tx_hashes_.clear(); } - m_logger->debug( "[{} - full: {}] Initializing UTXOs", account_m->GetAddress().substr( 0, 8 ), full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Initializing UTXOs", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); auto utxo_result = account_m->GetUTXOManager().LoadUTXOs( globaldb_m->GetDataStore() ); if ( utxo_result.has_error() ) { - m_logger->error( "[{} - full: {}] Failed to load UTXOs from storage", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->error( "[{} - full: {}] Failed to load UTXOs from storage", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); } - const bool has_local_utxos = utxo_result.has_value() && utxo_result.value(); - auto monitored_networks = GetMonitoredNetworkIDs(); + bool has_local_utxos = utxo_result.has_value() && utxo_result.value(); + auto monitored_networks = GetMonitoredNetworkIDs(); + + if ( has_local_utxos ) + { + auto checkpoint_result = account_m->GetUTXOManager().LoadLatestCheckpoint( account_m->GetAddress() ); + if ( checkpoint_result.has_error() ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] Failed to load local UTXO checkpoint during init: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + checkpoint_result.error().message() ); + } + else if ( checkpoint_result.value().has_value() ) + { + const auto local_root = account_m->GetUTXOManager().ComputeUTXOMerkleRoot( account_m->GetAddress() ); + if ( local_root != checkpoint_result.value()->utxo_merkle_root ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] Local UTXO root mismatch with checkpoint during init. Clearing local UTXOs and rebuilding", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); + + auto clear_result = account_m->GetUTXOManager().SetUTXOs( std::vector{}, + account_m->GetAddress() ); + if ( clear_result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] Failed to clear local UTXOs after checkpoint mismatch: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + clear_result.error().message() ); + } + else + { + has_local_utxos = false; + } + } + } + } std::unordered_set network_hashes; bool has_network_utxos = false; - m_logger->debug( "[{} - full: {}] Requesting UTXOs from network during init", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Requesting UTXOs from network during init", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); auto network_utxos = account_m->RequestUTXOs( 8000, account_m->GetAddress() ); if ( network_utxos.has_value() && !network_utxos.value().empty() ) { network_hashes = network_utxos.value(); has_network_utxos = true; - m_logger->debug( "[{} - full: {}] Received {} UTXOs from network", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - network_hashes.size() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Received {} UTXOs from network", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + network_hashes.size() ); } else { - m_logger->debug( "[{} - full: {}] No UTXO response received from network during init", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] No UTXO response received from network during init", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); } if ( !has_local_utxos && !has_network_utxos ) { - m_logger->info( "[{} - full: {}] No local or network UTXOs found, querying transactions to mount UTXOs", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->info( + "[{} - full: {}] No local or network UTXOs found, querying transactions to mount UTXOs", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); QueryTransactions(); return; } @@ -1642,30 +2174,31 @@ namespace sgns { for ( const auto &[address, utxo_data_vector] : utxo_map ) { - m_logger->debug( "[{} - full: {}] Loaded {} UTXOs for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - utxo_data_vector.size(), - address.substr( 0, 8 ) ); + TransactionManagerLogger()->debug( "[{} - full: {}] Loaded {} UTXOs for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + utxo_data_vector.size(), + address.substr( 0, 8 ) ); for ( auto &utxo_data : utxo_data_vector ) { auto &[utxo_state, utxo] = utxo_data; const auto tx_hash = utxo.GetTxID().toReadableString(); - m_logger->debug( "[{} - full: {}] UTXO - state: {}, tx_hash: {}, index: {}, amount: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - static_cast( utxo_state ), - tx_hash, - utxo.GetOutputIdx(), - utxo.GetAmount() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] UTXO - state: {}, tx_hash: {}, index: {}, amount: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + static_cast( utxo_state ), + tx_hash, + utxo.GetOutputIdx(), + utxo.GetAmount() ); if ( utxo_state != UTXOManager::UTXOState::UTXO_READY ) { - m_logger->debug( "[{} - full: {}] Skipping UTXO in state {} for tx {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - static_cast( utxo_state ), - tx_hash ); + TransactionManagerLogger()->debug( "[{} - full: {}] Skipping UTXO in state {} for tx {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + static_cast( utxo_state ), + tx_hash ); continue; } @@ -1676,10 +2209,10 @@ namespace sgns auto process_result = FetchAndProcessTransaction( tx_path ); if ( !process_result.has_error() ) { - m_logger->debug( "[{} - full: {}] Processed transaction in {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_path ); + TransactionManagerLogger()->debug( "[{} - full: {}] Processed transaction in {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_path ); processed = true; break; } @@ -1705,10 +2238,10 @@ namespace sgns auto process_result = FetchAndProcessTransaction( tx_path ); if ( !process_result.has_error() ) { - m_logger->debug( "[{} - full: {}] Processed transaction in {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_path ); + TransactionManagerLogger()->debug( "[{} - full: {}] Processed transaction in {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_path ); processed = true; break; } @@ -1744,18 +2277,18 @@ namespace sgns // TODO - Remove this once we remove the passive heads processing or we want transactions we are not subscribed here return; - m_logger->info( "[{} - full: {}] Missing {} transactions during init", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - missing_count ); + TransactionManagerLogger()->info( "[{} - full: {}] Missing {} transactions during init", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + missing_count ); auto now = std::chrono::steady_clock::now(); if ( last_init_tx_request_time_ != std::chrono::steady_clock::time_point{} && now - last_init_tx_request_time_ < std::chrono::milliseconds( k_init_tx_request_cooldown_ms ) ) { - m_logger->debug( "[{} - full: {}] Skipping tx requests (init cooldown)", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Skipping tx requests (init cooldown)", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return; } last_init_tx_request_time_ = now; @@ -1763,45 +2296,46 @@ namespace sgns const auto request_timeout = std::chrono::milliseconds( k_init_tx_request_cooldown_ms ); for ( const auto &tx_hash : missing_tx_hashes_copy ) { - m_logger->debug( "[{} - full: {}] Requesting transaction with hash {} (this: {})", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_hash, - reinterpret_cast( this ) ); + TransactionManagerLogger()->debug( "[{} - full: {}] Requesting transaction with hash {} (this: {})", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_hash, + reinterpret_cast( this ) ); auto request_result = account_m->RequestTransaction( request_timeout.count(), tx_hash ); if ( request_result.has_error() ) { - m_logger->error( "[{} - full: {}] Failed to request transaction with hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_hash ); + TransactionManagerLogger()->error( "[{} - full: {}] Failed to request transaction with hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_hash ); } else { - m_logger->debug( "[{} - full: {}] Successfully requested transaction with hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_hash ); + TransactionManagerLogger()->debug( "[{} - full: {}] Successfully requested transaction with hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_hash ); } } } bool TransactionManager::CheckNonce() const { - m_logger->debug( "[{} - full: {}] Checking if my local confirmed nonce is in sync with the network", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Checking if my local confirmed nonce is in sync with the network", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); auto nonce_from_network_result = account_m->FetchNetworkNonce( NONCE_REQUEST_TIMEOUT_MS ); if ( nonce_from_network_result.has_error() ) { - m_logger->error( "[{} - full: {}] Failed to fetch network nonce: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce_from_network_result.error().message() ); + TransactionManagerLogger()->error( "[{} - full: {}] Failed to fetch network nonce: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + nonce_from_network_result.error().message() ); if ( full_node_m ) { - m_logger->debug( + TransactionManagerLogger()->debug( "[{} - full: {}] Network nonce fetch failed, but we have a full node configured. Allowing for it to boot", account_m->GetAddress().substr( 0, 8 ), full_node_m ); @@ -1812,47 +2346,47 @@ namespace sgns auto maybe_nonce = nonce_from_network_result.value(); if ( !maybe_nonce.has_value() ) { - m_logger->error( "[{} - full: {}] Network doesn't have nonce info, trusting local nonce", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->error( "[{} - full: {}] Network doesn't have nonce info, trusting local nonce", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return true; } auto network_nonce = maybe_nonce.value(); - auto local_nonce_result = account_m->GetPeerNonce( account_m->GetAddress() ); + auto local_nonce_result = account_m->GetLocalConfirmedNonce(); if ( local_nonce_result.has_error() ) { - m_logger->debug( "[{} - full: {}] No local nonce found. Network nonce exists: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - network_nonce ); + TransactionManagerLogger()->debug( "[{} - full: {}] No local nonce found. Network nonce exists: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + network_nonce ); return false; } auto local_nonce = local_nonce_result.value(); if ( network_nonce > local_nonce ) { - m_logger->error( "[{} - full: {}] Nonce mismatch - Network: {}, Local: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - network_nonce, - local_nonce ); + TransactionManagerLogger()->error( "[{} - full: {}] Nonce mismatch - Network: {}, Local: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + network_nonce, + local_nonce ); return false; } - m_logger->debug( "[{} - full: {}] Nonce is in sync with the network - Network: {}, Local: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - network_nonce, - local_nonce ); + TransactionManagerLogger()->debug( "[{} - full: {}] Nonce is in sync with the network - Network: {}, Local: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + network_nonce, + local_nonce ); return true; } void TransactionManager::SyncNonce() { - m_logger->debug( "[{} - full: {}] Checking if my nonce is updated", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Checking if my nonce is updated", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); auto nonce_result = account_m->GetConfirmedNonce( NONCE_REQUEST_TIMEOUT_MS ); uint64_t confirmed_nonce = 0; @@ -1879,27 +2413,28 @@ namespace sgns { //Either my old txs are outdated or //The responder has not updated yet - m_logger->debug( "[{} - full: {}] Network nonce updated: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - expected_next_nonce ); + TransactionManagerLogger()->debug( "[{} - full: {}] Network nonce updated: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + expected_next_nonce ); ChangeState( State::READY ); } else if ( proposed_nonce > expected_next_nonce ) { - m_logger->error( "[{} - full: {}] Local nonce ahead - Local: {}, Expected: {}. Checking for invalid tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - proposed_nonce, - expected_next_nonce ); + TransactionManagerLogger()->error( + "[{} - full: {}] Local nonce ahead - Local: {}, Expected: {}. Checking for invalid tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + proposed_nonce, + expected_next_nonce ); std::set nonces_to_check; for ( auto i = expected_next_nonce; i < proposed_nonce; ++i ) { nonces_to_check.insert( i ); - m_logger->debug( "[{} - full: {}] Inserting nonce to check: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - i ); + TransactionManagerLogger()->debug( "[{} - full: {}] Inserting nonce to check: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + i ); } (void)CheckTransactionValidity( nonces_to_check ); @@ -1907,12 +2442,13 @@ namespace sgns else if ( proposed_nonce < expected_next_nonce ) { uint64_t nonce_gap = expected_next_nonce - proposed_nonce; - m_logger->error( "[{} - full: {}] Local nonce behind - Local: {}, Expected: {}. Gap: {}. Waiting to sync", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - proposed_nonce, - expected_next_nonce, - nonce_gap ); + TransactionManagerLogger()->error( + "[{} - full: {}] Local nonce behind - Local: {}, Expected: {}. Gap: {}. Waiting to sync", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + proposed_nonce, + expected_next_nonce, + nonce_gap ); // If we're behind at all, we need to catch up - even a gap of 1 means // there's transaction data in CRDT that we don't have, and we cannot @@ -1934,10 +2470,11 @@ namespace sgns auto elapsed = std::chrono::duration_cast( now - last_head_request_time_.value() ); if ( elapsed.count() < 30 ) { - m_logger->trace( "[{} - full: {}] Skipping head request - too soon since last request ({}s ago)", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - elapsed.count() ); + TransactionManagerLogger()->trace( + "[{} - full: {}] Skipping head request - too soon since last request ({}s ago)", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + elapsed.count() ); return; } } @@ -1945,29 +2482,29 @@ namespace sgns auto topics_result = globaldb_m->GetMonitoredTopics(); if ( !topics_result.has_value() ) { - m_logger->warn( "[{} - full: {}] Could not get monitored topics for head request", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->warn( "[{} - full: {}] Could not get monitored topics for head request", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return; } - m_logger->info( "[{} - full: {}] Requesting heads for {} topics", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - topics_result.value().size() ); + TransactionManagerLogger()->info( "[{} - full: {}] Requesting heads for {} topics", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + topics_result.value().size() ); if ( account_m->RequestHeads( topics_result.value() ) ) { last_head_request_time_ = now; - m_logger->debug( "[{} - full: {}] Periodic sync head request sent for {} topics", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - topics_result.value().size() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Periodic sync head request sent for {} topics", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + topics_result.value().size() ); } else { - m_logger->warn( "[{} - full: {}] Failed to request heads", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->warn( "[{} - full: {}] Failed to request heads", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); } } @@ -1977,10 +2514,10 @@ namespace sgns std::vector invalid_transaction_keys; { std::unique_lock tx_lock( tx_mutex_m ); - m_logger->debug( "[{} - full: {}] {}: Checking transactions", - __func__, - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking transactions", + __func__, + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); for ( auto &nonce : nonces_to_check ) { @@ -1991,53 +2528,43 @@ namespace sgns continue; } - m_logger->debug( "[{} - full: {}] {}: Seeing if transaction {} is valid {}", - __func__, - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tracked.tx->dag_st.nonce(), - nonce ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Seeing if transaction {} is valid {}", + __func__, + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tracked.cached_nonce, + nonce ); - if ( tracked.tx->dag_st.nonce() == nonce ) + if ( tracked.cached_nonce == nonce ) { bool valid_tx = true; - if ( !tracked.tx->CheckSignature() ) + if ( !CheckTransactionAuthorization( *tracked.tx ) ) { - if ( !tracked.tx->CheckDAGSignatureLegacy() ) - { - m_logger->error( - "[{} - full: {}] Could not validate signature of transaction with nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); - valid_tx = false; - } - else - { - m_logger->debug( "[{} - full: {}] Legacy transaction validated with nonce: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); - } + TransactionManagerLogger()->error( + "[{} - full: {}] Could not validate signature of transaction with nonce {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + nonce ); + valid_tx = false; } else { - m_logger->debug( "[{} - full: {}] {}: Transaction is valid with {}", - __func__, - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Transaction is valid with {}", + __func__, + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + nonce ); } if ( !valid_tx ) { // Collect the key for later removal invalid_transaction_keys.push_back( key ); changed = true; - m_logger->debug( "[{} - full: {}] {}: INVALID TX {}", - __func__, - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: INVALID TX {}", + __func__, + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + nonce ); } else { @@ -2060,24 +2587,24 @@ namespace sgns { std::shared_ptr crdt_transaction = globaldb_m->BeginTransaction(); - m_logger->debug( "[{} - full: {}] Deleting transaction on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Deleting transaction on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); BOOST_OUTCOME_TRY( crdt_transaction->Remove( { std::move( tx_key ) } ) ); - m_logger->debug( "[{} - full: {}] Removed key transaction on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Removed key transaction on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); BOOST_OUTCOME_TRY( crdt_transaction->Commit( topics ) ); - m_logger->debug( "[{} - full: {}] Commited tx on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Commited tx on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); return outcome::success(); } @@ -2093,12 +2620,11 @@ namespace sgns { for ( const auto &[_, tracked] : tx_processed_m ) { - m_logger->debug( "[{} - full: {}] Searching for hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_hash ); - if ( tracked.tx && tracked.tx->GetHash() == tx_hash && - tracked.tx->GetSrcAddress() == account_m->GetAddress() ) + TransactionManagerLogger()->debug( "[{} - full: {}] Searching for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_hash ); + if ( tracked.tx && tracked.tx->GetHash() == tx_hash ) { return tracked.tx; } @@ -2113,7 +2639,7 @@ namespace sgns std::shared_lock tx_lock( tx_mutex_m ); for ( const auto &[_, tracked] : tx_processed_m ) { - if ( tracked.tx && ( tracked.tx->dag_st.nonce() == nonce ) && ( tracked.tx->GetSrcAddress() == address ) ) + if ( tracked.tx && ( tracked.cached_nonce == nonce ) && ( tracked.tx->GetSrcAddress() == address ) ) { return tracked.tx; } @@ -2121,6 +2647,36 @@ namespace sgns return nullptr; } + std::optional TransactionManager::GetTrackedTxByNonceAndAddress( + uint64_t nonce, + const std::string &address ) const + { + std::shared_lock tx_lock( tx_mutex_m ); + for ( const auto &[_, tracked] : tx_processed_m ) + { + if ( tracked.tx && ( tracked.cached_nonce == nonce ) && ( tracked.tx->GetSrcAddress() == address ) ) + { + return tracked; + } + } + return std::nullopt; + } + + std::optional TransactionManager::GetTrackedTxByHash( + const std::string &tx_hash ) const + { + //TODO - Check for all monitored networks + auto tx_path = GetTransactionPath( tx_hash ); + + std::shared_lock tx_lock( tx_mutex_m ); + auto maybe_tracked = tx_processed_m.find( tx_path ); + if ( maybe_tracked != tx_processed_m.end() ) + { + return maybe_tracked->second; + } + return std::nullopt; + } + TransactionManager::TransactionStatus TransactionManager::GetOutgoingStatusByTxId( const std::string &txId ) const { std::shared_lock tx_lock( tx_mutex_m ); @@ -2149,7 +2705,9 @@ namespace sgns bool TransactionManager::SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ) { - std::unique_lock tx_lock( tx_mutex_m ); + bool ret = false; + std::shared_ptr tx; + std::unique_lock tx_lock( tx_mutex_m ); for ( auto &[_, tracked] : tx_processed_m ) { if ( !tracked.tx ) @@ -2160,211 +2718,69 @@ namespace sgns { continue; } - if ( tracked.tx->dag_st.nonce() != nonce ) + if ( tracked.cached_nonce != nonce ) { continue; } - - auto old_status = tracked.status; - tracked.status = s; - - // Update verifying_count - if ( old_status == TransactionStatus::VERIFYING && s != TransactionStatus::VERIFYING ) - { - verifying_count_.fetch_sub( 1, std::memory_order_relaxed ); - } - else if ( old_status != TransactionStatus::VERIFYING && s == TransactionStatus::VERIFYING ) - { - verifying_count_.fetch_add( 1, std::memory_order_relaxed ); - } - - m_logger->debug( "[{} - full: {}] Set tx {} (nonce {}) to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tracked.tx->GetHash(), - nonce, - static_cast( s ) ); - return true; - } - m_logger->debug( "[{} - full: {}] No outgoing tx found with nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); - return false; - } - - outcome::result TransactionManager::ConfirmTransactions() - { - // Fast path: check if there are any VERIFYING transactions - if ( verifying_count_.load( std::memory_order_relaxed ) == 0 ) - { - m_logger->trace( "[{} - full: {}] No VERIFYING transactions, skipping nonce check in ConfirmTransactions", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return outcome::success(); + tx = tracked.tx; + break; } - - // Collect nonces of VERIFYING transactions using index - std::vector verifying_nonces; + tx_lock.unlock(); + if ( tx ) { - std::shared_lock tx_lock( tx_mutex_m ); - for ( const auto &[_, tracked] : tx_processed_m ) + auto result = ChangeTransactionState( std::move( tx ), s ); + if ( !result.has_error() ) { - if ( !tracked.tx ) - { - continue; - } - if ( tracked.tx->GetSrcAddress() != account_m->GetAddress() ) - { - continue; - } - if ( tracked.status == TransactionStatus::VERIFYING ) - { - verifying_nonces.push_back( tracked.tx->dag_st.nonce() ); - } + ret = true; } } - - // If nothing to confirm after lock, skip - if ( verifying_nonces.empty() ) - { - m_logger->trace( "[{} - full: {}] No VERIFYING transactions after lock check", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return outcome::success(); - } - - // Fetch confirmed nonce only if we have VERIFYING transactions - auto nonce_result = account_m->GetConfirmedNonce( NONCE_REQUEST_TIMEOUT_MS ); - if ( !nonce_result.has_value() ) - { - m_logger->debug( "[{} - full: {}] Can't fetch nonce from the network in ConfirmTransactions", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return outcome::failure( boost::system::error_code{} ); - } - - uint64_t confirmed_nonce = nonce_result.value(); - m_logger->debug( "[{} - full: {}] Confirmed nonce from network: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - confirmed_nonce ); - - // Use nonce index for O(1) lookup and update + else { - std::unique_lock tx_lock( tx_mutex_m ); - for ( uint64_t nonce : verifying_nonces ) - { - if ( nonce <= confirmed_nonce ) - { - for ( auto &[key, tracked] : tx_processed_m ) - { - if ( !tracked.tx ) - { - continue; - } - if ( tracked.tx->GetSrcAddress() != account_m->GetAddress() ) - { - continue; - } - if ( tracked.tx->dag_st.nonce() != nonce ) - { - continue; - } - if ( tracked.status == TransactionStatus::VERIFYING ) - { - tracked.status = TransactionStatus::CONFIRMED; - verifying_count_.fetch_sub( 1, std::memory_order_relaxed ); - m_logger->debug( "[{} - full: {}] Transaction {} (nonce {}) set to CONFIRMED", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key, - nonce ); - } - } - } - } + TransactionManagerLogger()->debug( "[{} - full: {}] No outgoing tx found with nonce {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + nonce ); } - - return outcome::success(); + return ret; } std::optional> TransactionManager::FilterTransaction( const crdt::pb::Element &element ) { std::optional> maybe_tombstones; - bool should_delete = false; + bool should_delete = true; std::shared_ptr new_tx; do { auto maybe_new_tx = DeSerializeTransaction( element.value() ); if ( maybe_new_tx.has_error() ) { - m_logger->error( "[{} - full: {}] Failed to deserialize incoming transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); - should_delete = true; + TransactionManagerLogger()->error( "[{} - full: {}] Failed to deserialize incoming transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + element.key() ); break; } new_tx = maybe_new_tx.value(); - if ( !new_tx->CheckSignature() ) + if ( !CheckTransactionAuthorization( *new_tx ) ) { - if ( !new_tx->CheckDAGSignatureLegacy() ) - { - m_logger->error( "[{} - full: {}] Could not validate signature of transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); - should_delete = true; - break; - } - m_logger->debug( "[{} - full: {}] Legacy transaction validated: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); + TransactionManagerLogger()->error( "[{} - full: {}] Could not validate signature of transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + element.key() ); + break; } - std::shared_ptr conflicting_tx; - - auto conflicting_tx_res = GetConflictingTransaction( *new_tx ); - if ( !conflicting_tx_res.has_value() ) + if ( IsGoingToOverwrite( GetTransactionPath( *new_tx ) ) ) { - //maybe it's not been processed yet, but it's on CRDT - auto maybe_existing_value = globaldb_m->Get( element.key() ); - if ( !maybe_existing_value.has_value() ) - { - m_logger->trace( "[{} - full: {}] No existing transaction, accepting new transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); - - break; - } - m_logger->debug( - "[{} - full: {}] Found transaction with the same key {}, checking mutability window and timestamps", + TransactionManagerLogger()->debug( + "[{} - full: {}] New transaction {} would overwrite an existing one. Preventing that", account_m->GetAddress().substr( 0, 8 ), full_node_m, - element.key() ); - - conflicting_tx_res = DeSerializeTransaction( maybe_existing_value.value() ); - if ( conflicting_tx_res.has_error() ) - { - m_logger->warn( "[{} - full: {}] Failed to deserialize existing transaction {}, accepting new one", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); - break; - } + new_tx->GetHash() ); + break; } - conflicting_tx = std::move( conflicting_tx_res.value() ); - - m_logger->debug( "[{} - full: {}] Checking if new tx {} is the correct one", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_tx->GetHash() ); - - should_delete = !ShouldReplaceTransaction( *conflicting_tx, *new_tx ); + should_delete = false; } while ( 0 ); @@ -2396,10 +2812,10 @@ namespace sgns auto maybe_has_value = globaldb_m->Get( element.key() ); if ( maybe_has_value.has_value() ) { - m_logger->debug( "[{} - full: {}] Already have the proof {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); + TransactionManagerLogger()->debug( "[{} - full: {}] Already have the proof {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + element.key() ); valid_proof = true; break; } @@ -2410,16 +2826,16 @@ namespace sgns if ( maybe_valid_proof.has_error() || ( !maybe_valid_proof.value() ) ) { // TODO: kill reputation point of the node. - m_logger->error( "[{} - full: {}] Could not verify proof {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); + TransactionManagerLogger()->error( "[{} - full: {}] Could not verify proof {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + element.key() ); break; } - m_logger->trace( "[{} - full: {}] Valid proof of {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); + TransactionManagerLogger()->trace( "[{} - full: {}] Valid proof of {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + element.key() ); valid_proof = true; } while ( 0 ); @@ -2444,70 +2860,15 @@ namespace sgns bool TransactionManager::ShouldReplaceTransaction( const IGeniusTransactions &existing_tx, const IGeniusTransactions &new_tx ) const { - // First check if the existing transaction is immutable - if ( existing_tx.GetHash() == new_tx.GetHash() ) - { - m_logger->info( "[{} - full: {}] Already have the same transaction, rejecting replacement attempt", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return false; - } - if ( IsTransactionImmutable( existing_tx ) ) - { - m_logger->info( "[{} - full: {}] Existing transaction is immutable, rejecting replacement attempt", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return false; - } - - // Get timestamps and elapsed times - auto existing_timestamp = existing_tx.GetTimestamp(); - auto new_timestamp = new_tx.GetTimestamp(); - auto time_diff = GetElapsedTime( new_timestamp, existing_timestamp ); // preserve original semantics - - // If new tx is earlier than existing (time_diff > 0) allow replacement. - // If timestamp_tolerance_m > 0 enforce the tolerance window; otherwise only the sign of time_diff is considered. - if ( time_diff > 0 ) - { - if ( timestamp_tolerance_m.count() == 0 ) - { - m_logger->debug( - "[{} - full: {}] Timestamp tolerance disabled — new tx earlier (diff {} ms): allowing replacement", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - time_diff ); - return true; - } - - if ( time_diff < timestamp_tolerance_m.count() ) - { - m_logger->debug( - "[{} - full: {}] Timestamps within tolerance ({} ms). Existing: {} , New: {} , Diff: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - timestamp_tolerance_m.count(), - existing_timestamp, - new_timestamp, - time_diff ); - - m_logger->info( "[{} - full: {}] New transaction is earlier (ts: {} vs {}), will replace existing", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_timestamp, - existing_timestamp ); - return true; - } - } - - m_logger->warn( - "[{} - full: {}] New transaction not eligible for replacement. Existing: {} , New: {} , Diff: {} ms, Tolerance: {} ms", + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Checking if new transaction {} should replace existing one {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, - existing_timestamp, - new_timestamp, - time_diff, - timestamp_tolerance_m.count() ); - return false; + __func__, + new_tx.GetHash(), + existing_tx.GetHash() ); + + return blockchain_->BestHash( existing_tx.GetHash(), new_tx.GetHash() ) == new_tx.GetHash(); } uint64_t TransactionManager::GetCurrentTimestamp() @@ -2525,20 +2886,21 @@ namespace sgns if ( elapsed < 0 ) { - m_logger->debug( "[{} - full: {}] Transaction timestamp {} is in the future (current: {}), elapsed: {} ms", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - timestamp, - current_timestamp, - elapsed ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Transaction timestamp {} is in the future (current: {}), elapsed: {} ms", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + timestamp, + current_timestamp, + elapsed ); } else { - m_logger->trace( "[{} - full: {}] Transaction timestamp {} elapsed: {} ms", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - timestamp, - elapsed ); + TransactionManagerLogger()->trace( "[{} - full: {}] Transaction timestamp {} elapsed: {} ms", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + timestamp, + elapsed ); } return elapsed; @@ -2563,10 +2925,11 @@ namespace sgns // If elapsed is negative, the transaction is from the future - not immutable if ( elapsed < 0 ) { - m_logger->debug( "[{} - full: {}] Transaction from future is not immutable (elapsed: {} ms)", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - elapsed ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Transaction from future is not immutable (elapsed: {} ms)", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + elapsed ); return false; } @@ -2574,19 +2937,21 @@ namespace sgns if ( is_immutable ) { - m_logger->debug( "[{} - full: {}] Transaction is immutable (elapsed: {} ms, window: {} ms)", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - elapsed, - mutability_window_m.count() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Transaction is immutable (elapsed: {} ms, window: {} ms)", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + elapsed, + mutability_window_m.count() ); } else { - m_logger->trace( "[{} - full: {}] Transaction is still mutable (elapsed: {} ms, window: {} ms)", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - elapsed, - mutability_window_m.count() ); + TransactionManagerLogger()->trace( + "[{} - full: {}] Transaction is still mutable (elapsed: {} ms, window: {} ms)", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + elapsed, + mutability_window_m.count() ); } return is_immutable; @@ -2596,39 +2961,39 @@ namespace sgns { timestamp_tolerance_m = std::chrono::milliseconds( timeframe_tolerance ); - m_logger->info( "[{} - full: {}] Updated timeframe tolerance to {} ms", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - timeframe_tolerance ); + TransactionManagerLogger()->info( "[{} - full: {}] Updated timeframe tolerance to {} ms", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + timeframe_tolerance ); } void TransactionManager::SetMutabilityWindowMs( uint64_t mutability_window ) { mutability_window_m = std::chrono::milliseconds( mutability_window ); - m_logger->info( "[{} - full: {}] Updated mutability window to {} ms", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - mutability_window ); + TransactionManagerLogger()->info( "[{} - full: {}] Updated mutability window to {} ms", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + mutability_window ); } outcome::result TransactionManager::RemoveTransactionFromProcessedMaps( const std::string &transaction_key, bool delete_from_crdt ) { - m_logger->debug( "[{} - full: {}] Removing transaction from processed maps: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Removing transaction from processed maps: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_key ); bool found = false; { std::unique_lock tx_lock( tx_mutex_m ); auto it = tx_processed_m.find( transaction_key ); if ( it != tx_processed_m.end() ) { - m_logger->debug( "[{} - full: {}] Removing from processed: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Removing from processed: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_key ); if ( it->second.tx ) { @@ -2638,12 +3003,8 @@ namespace sgns auto topics = it->second.tx->GetTopics(); BOOST_OUTCOME_TRY( DeleteTransaction( transaction_key, topics ) ); } - account_m->RollBackPeerConfirmedNonce( it->second.tx->dag_st.nonce(), + account_m->RollBackPeerConfirmedNonce( it->second.cached_nonce, it->second.tx->dag_st.source_addr() ); - if ( it->second.status == TransactionStatus::VERIFYING ) - { - verifying_count_.fetch_sub( 1, std::memory_order_relaxed ); - } } tx_processed_m.erase( it ); found = true; @@ -2652,10 +3013,10 @@ namespace sgns if ( !found ) { - m_logger->debug( "[{} - full: {}] Transaction not found in processed maps: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Transaction not found in processed maps: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_key ); } return outcome::success(); } @@ -2665,101 +3026,133 @@ namespace sgns { auto [key, value] = new_data; - m_logger->debug( "[{} - full: {}] Trying to deserialize {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Trying to deserialize {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); BOOST_OUTCOME_TRY( auto new_tx, DeSerializeTransaction( value ) ); - m_logger->debug( "[{} - full: {}] Deserialized transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); - bool should_add_transaction = false; - auto tx_hash = new_tx->GetHash(); - if ( tx_hash.empty() ) - { - m_logger->error( "[{} - full: {}] Empty hash on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Deserialized transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); + + if ( new_tx->GetHash().empty() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] Empty hash on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); return outcome::failure( boost::system::error_code{} ); } - m_logger->debug( "[{} - full: {}] Checking if we already have this transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Verifying if we have a conflicting transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); - std::unique_lock tx_lock( tx_mutex_m ); - auto it = tx_processed_m.find( key ); - - if ( it != tx_processed_m.end() ) - { - m_logger->debug( "[{} - full: {}] Already have the transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); - return outcome::success(); - } - m_logger->debug( "[{} - full: {}] Verifying if we have a conflicting transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); - tx_lock.unlock(); auto conflicting_tx = GetConflictingTransaction( *new_tx ); - tx_lock.lock(); if ( conflicting_tx.has_value() ) { - m_logger->debug( "[{} - full: {}] Found conflicting transaction with hash: {}, removing it", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - conflicting_tx.value()->GetHash() ); + TransactionManagerLogger()->warn( + "[{} - full: {}] Found conflicting transaction that passed the FILTER with hash: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx.value()->GetHash() ); + std::unique_lock tx_lock( tx_mutex_m ); + auto it = tx_processed_m.find( GetTransactionPath( conflicting_tx.value()->GetHash() ) ); - const auto conflict_hash = conflicting_tx.value()->GetHash(); + // No need to check if not found because we already found it on GetConflictingTransaction + + if ( it->second.status == TransactionStatus::CONFIRMED ) + { + TransactionManagerLogger()->debug( + "[{} - full: {}] Conflicting transaction is already CONFIRMED, not adding incoming transaction{}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); + tx_lock.unlock(); + BOOST_OUTCOME_TRY( ChangeTransactionState( new_tx, TransactionStatus::FAILED ) ); + tx_lock.lock(); + return outcome::failure( boost::system::error_code{} ); + } + TransactionManagerLogger()->warn( + "[{} - full: {}] Setting conflicting transaction to VERIFYING since it's not confirmed: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx.value()->GetHash() ); tx_lock.unlock(); - BOOST_OUTCOME_TRY( RemoveTransactionFromProcessedMaps( GetTransactionPath( conflict_hash ), true ) ); - tx_lock.lock(); + BOOST_OUTCOME_TRY( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::VERIFYING ) ); } - m_logger->debug( "[{} - full: {}] Parsing new transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); - BOOST_OUTCOME_TRY( ParseTransaction( new_tx ) ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Checking if the transaction has a valid certificate to be confirmed {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); - const auto nonce = new_tx->dag_st.nonce(); + auto next_tx_state = TransactionStatus::VERIFYING; + auto has_cert = blockchain_->CheckCertificate( new_tx->GetHash() ); + if ( has_cert ) { - std::lock_guard missing_lock( missing_tx_mutex_ ); - missing_tx_hashes_.erase( new_tx->GetHash() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Transaction has a valid certificate, marking as CONFIRMED {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); + next_tx_state = TransactionStatus::CONFIRMED; + if ( conflicting_tx.has_value() ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] Setting conflicting transaction to FAILED because the new has a certificate and it doesn't: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx.value()->GetHash() ); + BOOST_OUTCOME_TRY( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ) ); + } } - account_m->SetPeerConfirmedNonce( nonce, new_tx->dag_st.source_addr() ); + auto maybe_existing = GetTrackedTxByHash( new_tx->GetHash() ); + if ( maybe_existing.has_value() && next_tx_state == TransactionStatus::VERIFYING ) + { + const auto current_status = maybe_existing->status; + if ( current_status == TransactionStatus::FAILED || current_status == TransactionStatus::CONFIRMED ) + { + TransactionManagerLogger()->debug( + "[{} - full: {}] Keeping terminal status {} for tx {}, skipping downgrade to VERIFYING (has_cert={})", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + static_cast( current_status ), + new_tx->GetHash(), + has_cert ); + return outcome::success(); + } + } - tx_processed_m[key] = TrackedTx{ new_tx, TransactionStatus::CONFIRMED, nonce }; + BOOST_OUTCOME_TRY( ChangeTransactionState( new_tx, next_tx_state ) ); return outcome::success(); } void TransactionManager::ProcessDeletion( std::string key ) { - m_logger->debug( "[{} - full: {}] Processing deletion of {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + TransactionManagerLogger()->debug( "[{} - full: {}] Processing deletion of {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); auto remove_res = RemoveTransactionFromProcessedMaps( key ); if ( remove_res.has_error() ) { - m_logger->error( "[{} - full: {}] Error removing transaction {}: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key, - remove_res.error().message() ); + TransactionManagerLogger()->error( "[{} - full: {}] Error removing transaction {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key, + remove_res.error().message() ); } } @@ -2773,10 +3166,11 @@ namespace sgns auto datastore = globaldb_m ? globaldb_m->GetDataStore() : nullptr; if ( !datastore ) { - m_logger->error( "[{} - full: {}] RocksDB datastore unavailable, cannot store CID for tx {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + TransactionManagerLogger()->error( + "[{} - full: {}] RocksDB datastore unavailable, cannot store CID for tx {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); return outcome::failure( std::errc::bad_file_descriptor ); } @@ -2797,20 +3191,20 @@ namespace sgns void TransactionManager::ProcessNewData( crdt::CRDTCallbackManager::NewDataPair new_data ) { - m_logger->debug( "[{} - full: {}] Processing new data with key {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_data.first ); + TransactionManagerLogger()->debug( "[{} - full: {}] Processing new data with key {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_data.first ); auto add_res = AddTransactionToProcessedMaps( new_data ); if ( add_res.has_error() ) { - m_logger->error( "[{} - full: {}] Error adding transaction {}: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_data.first, - add_res.error().message() ); + TransactionManagerLogger()->error( "[{} - full: {}] Error adding transaction {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_data.first, + add_res.error().message() ); } else { @@ -2819,7 +3213,7 @@ namespace sgns if ( !received_first_periodic_sync_response_.load() ) { received_first_periodic_sync_response_.store( true ); - m_logger->info( + TransactionManagerLogger()->info( "[{} - full: {}] First transaction data received from network, switching to 10-minute periodic sync interval", account_m->GetAddress().substr( 0, 8 ), full_node_m ); @@ -2832,11 +3226,11 @@ namespace sgns auto store_cid_res = StoreTransactionCID( new_data.first, cid ); if ( store_cid_res.has_error() ) { - m_logger->error( "[{} - full: {}] Failed to store CID for key {}: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_data.first, - store_cid_res.error().message() ); + TransactionManagerLogger()->error( "[{} - full: {}] Failed to store CID for key {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_data.first, + store_cid_res.error().message() ); } auto key = new_data.first; @@ -2850,11 +3244,11 @@ namespace sgns cv_.notify_one(); - m_logger->debug( "[{} - full: {}] CRDT new data queued, {} - (queue size: {})", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key, - queue_size ); + TransactionManagerLogger()->debug( "[{} - full: {}] CRDT new data queued, {} - (queue size: {})", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key, + queue_size ); } void TransactionManager::DeleteElementCallback( std::string deleted_key ) @@ -2867,11 +3261,11 @@ namespace sgns } cv_.notify_one(); - m_logger->debug( "[{} - full: {}] CRDT deleted key queued, {} - (queue size: {})", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - deleted_key, - queue_size ); + TransactionManagerLogger()->debug( "[{} - full: {}] CRDT deleted key queued, {} - (queue size: {})", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + deleted_key, + queue_size ); } void TransactionManager::RegisterStateChangeCallback( StateChangeCallback callback ) @@ -2892,11 +3286,11 @@ namespace sgns std::lock_guard lock( state_change_callback_mutex_ ); if ( state_m != new_state ) { - m_logger->info( "[{} - full: {}] State changed from {} to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - state_m, - new_state ); + TransactionManagerLogger()->info( "[{} - full: {}] State changed from {} to {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + state_m, + new_state ); auto old_state = state_m; state_m = new_state; if ( state_change_callback_ ) @@ -2918,11 +3312,11 @@ namespace sgns auto monitored_networks = GetMonitoredNetworkIDs(); for ( auto network_id : monitored_networks ) { - m_logger->debug( "[{} - full: {}] Looking for CID of tx {} in network {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_hash, - network_id ); + TransactionManagerLogger()->debug( "[{} - full: {}] Looking for CID of tx {} in network {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_hash, + network_id ); auto key = GetTransactionPath( network_id, tx_hash ); crdt::GlobalDB::Buffer key_buffer; @@ -2941,14 +3335,1461 @@ namespace sgns outcome::result> TransactionManager::GetConflictingTransaction( const IGeniusTransactions &element ) const { - auto tx = GetTransactionByNonceAndAddress( element.dag_st.nonce(), element.GetSrcAddress() ); - if ( tx ) + auto tx = GetTransactionByNonceAndAddress( element.GetNonce(), element.GetSrcAddress() ); + if ( tx && tx->GetHash() != element.GetHash() ) { return tx; } return outcome::failure( std::errc::no_such_file_or_directory ); } + + bool TransactionManager::HasConfirmedInputConflict( const std::shared_ptr &candidate_tx ) const + { + if ( !candidate_tx || !candidate_tx->HasUTXOParameters() ) + { + return false; + } + + auto candidate_params = candidate_tx->GetUTXOParametersOpt(); + if ( !candidate_params.has_value() ) + { + return false; + } + + std::unordered_set candidate_inputs; + candidate_inputs.reserve( candidate_params->first.size() ); + for ( const auto &input : candidate_params->first ) + { + candidate_inputs.insert( OutPointKey( input.txid_hash_, input.output_idx_ ) ); + } + + std::shared_lock tx_lock( tx_mutex_m ); + for ( const auto &[_, tracked] : tx_processed_m ) + { + if ( !tracked.tx || tracked.status != TransactionStatus::CONFIRMED || + tracked.tx->GetHash() == candidate_tx->GetHash() || !tracked.tx->HasUTXOParameters() ) + { + continue; + } + + auto other_params = tracked.tx->GetUTXOParametersOpt(); + if ( !other_params.has_value() ) + { + continue; + } + + for ( const auto &other_input : other_params->first ) + { + if ( candidate_inputs.find( OutPointKey( other_input.txid_hash_, other_input.output_idx_ ) ) != + candidate_inputs.end() ) + { + return true; + } + } + } + return false; + } + + outcome::result TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Consensus certificate arrived for transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + auto tx = GetTransactionByHash( tx_hash ); + if ( !tx ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Transaction not found for hash {}. Accepting certificate callback and waiting for tx ingestion path", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::Check::Approve; + } + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking for conflicting transaction with {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + + auto conflicting_tx = GetConflictingTransaction( *tx ); + + if ( conflicting_tx.has_value() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] Found conflicting transaction: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx.value()->GetHash() ); + std::unique_lock tx_lock( tx_mutex_m ); + auto it = tx_processed_m.find( GetTransactionPath( conflicting_tx.value()->GetHash() ) ); + + // No need to check if not found because we already found it on GetConflictingTransaction + + if ( it->second.status == TransactionStatus::CONFIRMED ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] Conflicting transaction {} is CONFIRMED as well as incoming {}, not sure what to do {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx.value()->GetHash(), + tx_hash ); + tx_lock.unlock(); + if ( ShouldReplaceTransaction( *conflicting_tx.value(), *tx ) ) + { + auto result = ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ); + if ( result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Failed to change conflicting transaction state to FAILED for current tx {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + conflicting_tx.value()->GetHash(), + result.error().message() ); + } + } + else + { + auto result = ChangeTransactionState( tx, TransactionStatus::FAILED ); + if ( result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Failed to change transaction state to FAILED for new tx {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + result.error().message() ); + } + return outcome::failure( result.error() ); + } + } + else + { + TransactionManagerLogger()->warn( + "[{} - full: {}] Setting conflicting transaction {} to FAILED since the new one {} is confirmed: ", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx.value()->GetHash(), + tx_hash ); + tx_lock.unlock(); + auto result = ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ); + if ( result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Failed to change transaction state to FAILED for hash {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + result.error().message() ); + } + } + } + + auto result = ChangeTransactionState( tx, TransactionStatus::CONFIRMED ); + if ( result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Failed to change transaction state to CONFIRMED for hash {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + result.error().message() ); + return outcome::failure( result.error() ); + } + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Transaction {} confirmed by consensus", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + + auto tx_hash_bin = base::Hash256::fromReadableString( tx_hash ); + if ( tx_hash_bin.has_error() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Could not parse tx hash for checkpoint tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return outcome::failure( tx_hash_bin.error() ); + } + + auto validator_registry = blockchain_->GetValidatorRegistry(); + if ( !validator_registry ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: No validator registry, skipping checkpoint", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + return outcome::failure( std::errc::no_such_device ); + } + + const uint64_t registry_epoch = validator_registry->GetRegistryEpoch(); + const auto registry_cid = validator_registry->GetRegistryCid(); + auto registry_hash = hasher_m->sha2_256( + gsl::span( reinterpret_cast( registry_cid.data() ), registry_cid.size() ) ); + + if ( auto checkpoint_res = account_m->GetUTXOManager().CreateCheckpoint( registry_epoch, + tx_hash_bin.value(), + registry_hash ); + checkpoint_res.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Failed to create UTXO checkpoint tx={} epoch={} err={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + registry_epoch, + checkpoint_res.error().message() ); + } + return ConsensusManager::Check::Approve; + } + + outcome::result TransactionManager::HandleNonceConsensusSubject( + const ConsensusManager::Subject &subject ) + { + if ( subject.type() != SubjectType::SUBJECT_NONCE ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Received unexpected subject type: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + static_cast( subject.type() ) ); + return outcome::failure( std::errc::invalid_argument ); + } + + const std::string tx_hash = subject.nonce().tx_hash(); + const auto key = GetTransactionPath( tx_hash ); + + std::shared_ptr tracked_tx; + uint64_t tracked_nonce = 0; + TransactionStatus tracked_status = TransactionStatus::INVALID; + { + std::shared_lock tx_lock( tx_mutex_m ); + auto it = tx_processed_m.find( key ); + if ( it == tx_processed_m.end() ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Transaction not found for hash {}, pending", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::Check::Pending; + } + + tracked_tx = it->second.tx; + tracked_nonce = it->second.cached_nonce; + tracked_status = it->second.status; + } + + if ( !tracked_tx ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Tracked transaction missing for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return outcome::failure( std::errc::invalid_argument ); + } + + auto reject_and_maybe_fail_local = [&]( const char *reason ) -> ConsensusManager::Check + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Rejecting nonce subject for hash {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + reason ); + + // Ensure local outgoing invalid transactions don't stay in VERIFYING forever. + if ( tracked_tx->GetSrcAddress() == account_m->GetAddress() ) + { + auto current_out_status = GetOutgoingStatusByTxId( tracked_tx->GetHash() ); + if ( current_out_status != TransactionStatus::FAILED && + current_out_status != TransactionStatus::CONFIRMED ) + { + if ( auto fail_result = ChangeTransactionState( tracked_tx, TransactionStatus::FAILED ); + fail_result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Failed to mark rejected local tx as FAILED for hash {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + fail_result.error().message() ); + } + } + } + + return ConsensusManager::Check::Reject; + }; + + if ( tracked_nonce != subject.nonce().nonce() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Nonce mismatch for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return reject_and_maybe_fail_local( "nonce mismatch" ); + } + + if ( !subject.account_id().empty() && tracked_tx->GetSrcAddress() != subject.account_id() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Account mismatch for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return reject_and_maybe_fail_local( "account mismatch" ); + } + + if ( tracked_status == TransactionStatus::FAILED ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Transaction status invalid for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return reject_and_maybe_fail_local( "transaction already failed" ); + } + + if ( HasConfirmedInputConflict( tracked_tx ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Outpoint conflict against finalized transaction " + "for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return reject_and_maybe_fail_local( "input outpoint already finalized by another transaction" ); + } + + const auto witness_validation = ValidateWitnessForConsensus( subject, tracked_tx ); + if ( witness_validation == WitnessValidationResult::INVALID ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Witness validation failed for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return reject_and_maybe_fail_local( "witness validation failed" ); + } + + if ( auto migration_tx = std::dynamic_pointer_cast( tracked_tx ) ) + { + MigrationAllowList allow_list( globaldb_m->GetDataStore(), migration_tx->GetFromVersion() ); + auto eligibility_result = + allow_list.IsEligible( migration_tx->GetSrcAddress(), migration_tx->GetAmount() ); + if ( eligibility_result.has_error() ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Failed to evaluate local migration allowlist tx={} src={} err={}, pending", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + migration_tx->GetSrcAddress(), + eligibility_result.error().message() ); + return ConsensusManager::Check::Pending; + } + if ( !eligibility_result.value() ) + { + return reject_and_maybe_fail_local( "migration source address not locally eligible" ); + } + } + + auto validate_result = ValidateTransactionForConsensus( tracked_tx ); + + if ( !validate_result ) + { + return reject_and_maybe_fail_local( "transaction validation failed" ); + } + + return ConsensusManager::Check::Approve; + } + + bool TransactionManager::ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, + const std::string &address ) const + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Validating UTXO params for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + address ); + if ( params.first.empty() || params.second.empty() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Empty inputs or outputs", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + return false; + } + + if ( !account_m->GetUTXOManager().VerifyParameters( params, address ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: VerifyParameters failed for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + address ); + return false; + } + + TransactionManagerLogger()->debug( "[{} - full: {}] {}: UTXO params valid for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + address ); + return true; + } + + bool TransactionManager::ValidateTransactionForConsensus( const std::shared_ptr &tx ) const + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Validating transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + if ( !tx ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Null transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + return false; + } + + if ( !CheckTransactionWellFormed( *tx ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Well-formed check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + if ( !CheckTransactionAuthorization( *tx ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Authorization check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + if ( !CheckTransactionTimestamp( *tx ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Timestamp check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + if ( !CheckTransactionReplayProtection( *tx ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Replay protection failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + //TODO - Deal with checking the Mint + if ( !CheckTransactionTypeRules( tx ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Type rules failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Transaction valid tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return true; + } + + bool TransactionManager::CheckTransactionWellFormed( const IGeniusTransactions &tx ) const + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking well-formed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + if ( tx.GetHash().empty() || !tx.CheckHash() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Hash invalid tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return false; + } + + if ( tx.GetSrcAddress().empty() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Empty source address tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return false; + } + + if ( tx.GetTimestamp() == 0 ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing timestamp tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return false; + } + + if ( transaction_parsers.find( tx.GetType() ) == transaction_parsers.end() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Unknown tx type {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetType() ); + return false; + } + + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Well-formed ok tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return true; + } + + bool TransactionManager::CheckTransactionAuthorization( const IGeniusTransactions &tx ) const + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking authorization tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + if ( tx.CheckSignature() || tx.CheckDAGSignatureLegacy() ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Authorization ok tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return true; + } + TransactionManagerLogger()->error( "[{} - full: {}] {}: Authorization failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return false; + } + + bool TransactionManager::CheckTransactionTimestamp( const IGeniusTransactions &tx ) const + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking timestamp tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + const auto ts = tx.GetTimestamp(); + if ( ts == 0 ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing timestamp tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return false; + } + + const auto elapsed = GetElapsedTime( ts ); + if ( elapsed < 0 && timestamp_tolerance_m.count() > 0 && + ( -elapsed ) > static_cast( timestamp_tolerance_m.count() ) ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Timestamp out of tolerance tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return false; + } + + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Timestamp ok tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return true; + } + + bool TransactionManager::CheckTransactionReplayProtection( const IGeniusTransactions &tx ) const + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking replay protection tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + + if ( tx.GetNonce() > 0 ) + { + const auto previous_hash = tx.GetPreviousHash(); + if ( previous_hash.empty() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing previous hash tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return false; + } + auto previous_cert_result = blockchain_->GetCertificateBySubjectHash( previous_hash ); + if ( previous_cert_result.has_error() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing previous certificate for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + previous_hash ); + return false; + } + const auto &previous_subject = previous_cert_result.value().proposal().subject(); + if ( !previous_subject.has_nonce() ) + { + return false; + } + if ( previous_subject.account_id() != tx.GetSrcAddress() ) + { + return false; + } + if ( ( previous_subject.nonce().nonce() + 1 ) != tx.GetNonce() ) + { + return false; + } + } + + auto nonce_result = account_m->GetPeerNonce( tx.GetSrcAddress() ); + if ( nonce_result.has_error() ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: No confirmed nonce for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetSrcAddress() ); + return true; + } + + const auto confirmed_nonce = nonce_result.value(); + const auto tx_nonce = tx.GetNonce(); + + if ( tx_nonce <= confirmed_nonce ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Nonce too low tx={} nonce={} confirmed={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash(), + tx_nonce, + confirmed_nonce ); + return false; + } + + if ( tx_nonce > confirmed_nonce + nonce_window_m ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Nonce too high tx={} nonce={} confirmed={} window={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash(), + tx_nonce, + confirmed_nonce, + nonce_window_m ); + return false; + } + + if ( tx_nonce > confirmed_nonce + 1 ) + { + for ( uint64_t n = confirmed_nonce + 1; n < tx_nonce; ++n ) + { + auto tracked = GetTrackedTxByNonceAndAddress( n, tx.GetSrcAddress() ); + if ( !tracked.has_value() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Missing intermediate nonce {} for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + n, + tx.GetSrcAddress() ); + return false; + } + if ( tracked->status == TransactionStatus::FAILED ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Intermediate nonce {} invalid for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + n, + tx.GetSrcAddress() ); + return false; + } + } + } + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Replay protection ok tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return true; + } + + bool TransactionManager::CheckTransactionTypeRules( const std::shared_ptr &tx ) const + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking type rules", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + if ( !tx ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Null transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + return false; + } + + if ( tx->HasUTXOParameters() ) + { + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing UTXO parameters for tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + const auto chain_id = GetValidationChainId( tx ); + const auto &validator = GetInputValidator( chain_id ); + return validator.ValidateUTXOParameters( params_opt.value(), + tx->GetSrcAddress(), + account_m->GetUTXOManager() ); + } + + return true; + } + + TransactionManager::WitnessValidationResult TransactionManager::ValidateWitnessForConsensus( + const ConsensusSubject &subject, + const std::shared_ptr &tx ) const + { + if ( !tx ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Null transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + return WitnessValidationResult::INVALID; + } + + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Start tx={} src={} nonce={} subject_nonce={} has_nonce={} " + "has_utxo_params={} has_commitment={} has_witness={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + tx->GetSrcAddress(), + tx->GetNonce(), + subject.has_nonce() ? subject.nonce().nonce() : 0, + subject.has_nonce(), + tx->HasUTXOParameters(), + subject.has_nonce() && subject.nonce().has_utxo_commitment(), + subject.has_nonce() && subject.nonce().has_utxo_witness() ); + + if ( !subject.has_nonce() ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Subject has no nonce payload, accepting tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return WitnessValidationResult::VALID; + } + + const auto chain_id = GetValidationChainId( tx ); + const auto &validator = GetInputValidator( chain_id ); + + if ( !tx->HasUTXOParameters() ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Tx has no UTXO params, accepting tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return WitnessValidationResult::VALID; + } + + if ( !subject.nonce().has_utxo_commitment() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing UTXO commitment tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return WitnessValidationResult::INVALID; + } + + const auto &commitment = subject.nonce().utxo_commitment(); + if ( commitment.consumed_outpoints_root().size() != base::Hash256::size() || + commitment.produced_outputs_root().size() != base::Hash256::size() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Invalid commitment root sizes tx={} consumed_size={} " + "produced_size={} expected={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + commitment.consumed_outpoints_root().size(), + commitment.produced_outputs_root().size(), + base::Hash256::size() ); + return WitnessValidationResult::INVALID; + } + auto consumed_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( commitment.consumed_outpoints_root().data() ) ), + commitment.consumed_outpoints_root().size() ) ); + if ( consumed_root_result.has_error() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to parse commitment consumed root tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return WitnessValidationResult::INVALID; + } + + if ( validator.RequiresConsensusUTXOData() && !subject.nonce().has_utxo_witness() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Missing required UTXO witness tx={} chain_id={} validator_requires_witness={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + chain_id, + validator.RequiresConsensusUTXOData() ); + return WitnessValidationResult::INVALID; + } + + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing UTXO params payload tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return WitnessValidationResult::INVALID; + } + (void)consumed_root_result; + const bool witness_ok = validator.ValidateWitness( subject, tx, params_opt.value(), blockchain_ ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Validator witness result tx={} chain_id={} result={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + chain_id, + witness_ok ); + return witness_ok ? WitnessValidationResult::VALID : WitnessValidationResult::INVALID; + } + + std::optional TransactionManager::BuildUTXOTransitionCommitment( + const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return std::nullopt; + } + if ( !tx->HasUTXOParameters() ) + { + return std::nullopt; + } + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + return std::nullopt; + } + const auto &inputs = params_opt->first; + if ( inputs.empty() ) + { + return std::nullopt; + } + auto tx_hash = base::Hash256::fromReadableString( tx->GetHash() ); + if ( tx_hash.has_error() ) + { + return std::nullopt; + } + + UTXOTransitionCommitment commitment; + std::vector> consumed_payloads; + consumed_payloads.reserve( inputs.size() ); + for ( const auto &input : inputs ) + { + auto *committed_input = commitment.add_consumed_outpoints(); + committed_input->set_tx_id_hash( input.txid_hash_.data(), input.txid_hash_.size() ); + committed_input->set_output_index( input.output_idx_ ); + + std::vector leaf_payload; + leaf_payload.reserve( 32 + 4 ); + leaf_payload.insert( leaf_payload.end(), input.txid_hash_.begin(), input.txid_hash_.end() ); + utxo_merkle::AppendUInt32BE( leaf_payload, input.output_idx_ ); + consumed_payloads.push_back( std::move( leaf_payload ) ); + } + const auto consumed_outpoints_root = utxo_merkle::ComputeMerkleRootFromPayloads( + std::move( consumed_payloads ) ); + + std::vector produced_outputs; + if ( !ExtractProducedUTXOs( tx, produced_outputs ) ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Could not extract produced outputs for tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; + } + std::vector> produced_payloads; + produced_payloads.reserve( produced_outputs.size() ); + for ( size_t i = 0; i < produced_outputs.size(); ++i ) + { + const auto &produced_output = produced_outputs[i]; + auto *committed_output = commitment.add_produced_outputs(); + committed_output->set_tx_id_hash( tx_hash.value().data(), tx_hash.value().size() ); + committed_output->set_output_index( static_cast( i ) ); + committed_output->set_owner_address( produced_output.GetOwnerAddress() ); + const auto token_bytes = produced_output.GetTokenID().bytes(); + committed_output->set_token_id( token_bytes.data(), token_bytes.size() ); + committed_output->set_amount( produced_output.GetAmount() ); + + produced_payloads.push_back( SerializeUTXOLeafPayload( produced_output ) ); + } + const auto produced_outputs_root = account_m->GetUTXOManager().ComputeUTXOMerkleRootFromSnapshot( + produced_outputs ); + const auto produced_outputs_root_from_payloads = utxo_merkle::ComputeMerkleRootFromPayloads( + std::move( produced_payloads ) ); + if ( produced_outputs_root != produced_outputs_root_from_payloads ) + { + return std::nullopt; + } + + commitment.set_consumed_outpoints_root( consumed_outpoints_root.data(), consumed_outpoints_root.size() ); + commitment.set_produced_outputs_root( produced_outputs_root.data(), produced_outputs_root.size() ); + return commitment; + } + + std::optional TransactionManager::BuildUTXOWitness( + const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return std::nullopt; + } + + if ( !tx->HasUTXOParameters() ) + { + return std::nullopt; + } + + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + return std::nullopt; + } + const auto &inputs = params_opt->first; + + struct SnapshotLeaf + { + std::string outpoint_key; + std::vector payload; + }; + + std::vector leaves; + auto utxos = account_m->GetUTXOManager().GetUTXOsForReservation( tx->GetSrcAddress(), tx->GetHash() ); + leaves.reserve( utxos.size() ); + for ( const auto &utxo : utxos ) + { + leaves.push_back( + { OutPointKey( utxo.GetTxID(), utxo.GetOutputIdx() ), SerializeUTXOLeafPayload( utxo ) } ); + } + + std::sort( leaves.begin(), + leaves.end(), + []( const SnapshotLeaf &a, const SnapshotLeaf &b ) { return a.payload < b.payload; } ); + + std::unordered_map outpoint_to_index; + outpoint_to_index.reserve( leaves.size() ); + std::vector level_hashes; + level_hashes.reserve( leaves.size() ); + for ( size_t i = 0; i < leaves.size(); ++i ) + { + outpoint_to_index.emplace( leaves[i].outpoint_key, i ); + level_hashes.push_back( HashLeaf( leaves[i].payload ) ); + } + + UTXOWitness witness; + for ( const auto &input : inputs ) + { + const auto key = OutPointKey( input.txid_hash_, input.output_idx_ ); + auto it = outpoint_to_index.find( key ); + if ( it == outpoint_to_index.end() ) + { + return std::nullopt; + } + + const size_t leaf_index = it->second; + auto *proof = witness.add_consumed_inputs(); + proof->set_tx_id_hash( input.txid_hash_.data(), input.txid_hash_.size() ); + proof->set_output_index( input.output_idx_ ); + proof->set_leaf_payload( leaves[leaf_index].payload.data(), leaves[leaf_index].payload.size() ); + + size_t current_index = leaf_index; + std::vector current_level = level_hashes; + while ( current_level.size() > 1 ) + { + if ( ( current_level.size() % 2 ) != 0 ) + { + current_level.push_back( current_level.back() ); + } + + const size_t sibling_index = current_index ^ 1U; + auto *step = proof->add_branch(); + step->set_sibling_hash( current_level[sibling_index].data(), current_level[sibling_index].size() ); + step->set_is_left_sibling( sibling_index < current_index ); + + std::vector next_level; + next_level.reserve( current_level.size() / 2 ); + for ( size_t i = 0; i < current_level.size(); i += 2 ) + { + next_level.push_back( HashNode( current_level[i], current_level[i + 1] ) ); + } + + current_index = current_index / 2; + current_level = std::move( next_level ); + } + + auto producer_tx = GetTransactionByHash( input.txid_hash_.toReadableString() ); + if ( !producer_tx ) + { + return std::nullopt; + } + std::vector produced_outputs; + if ( !ExtractProducedUTXOs( producer_tx, produced_outputs ) ) + { + return std::nullopt; + } + + std::vector produced_leaves; + produced_leaves.reserve( produced_outputs.size() ); + for ( const auto &output_utxo : produced_outputs ) + { + produced_leaves.push_back( { OutPointKey( output_utxo.GetTxID(), output_utxo.GetOutputIdx() ), + SerializeUTXOLeafPayload( output_utxo ) } ); + } + std::sort( produced_leaves.begin(), + produced_leaves.end(), + []( const SnapshotLeaf &a, const SnapshotLeaf &b ) { return a.payload < b.payload; } ); + + std::unordered_map produced_outpoint_to_index; + produced_outpoint_to_index.reserve( produced_leaves.size() ); + std::vector produced_level_hashes; + produced_level_hashes.reserve( produced_leaves.size() ); + for ( size_t i = 0; i < produced_leaves.size(); ++i ) + { + produced_outpoint_to_index.emplace( produced_leaves[i].outpoint_key, i ); + produced_level_hashes.push_back( HashLeaf( produced_leaves[i].payload ) ); + } + + auto produced_it = produced_outpoint_to_index.find( key ); + if ( produced_it == produced_outpoint_to_index.end() ) + { + return std::nullopt; + } + if ( produced_leaves[produced_it->second].payload != leaves[leaf_index].payload ) + { + return std::nullopt; + } + + size_t produced_index = produced_it->second; + std::vector produced_level = produced_level_hashes; + while ( produced_level.size() > 1 ) + { + if ( ( produced_level.size() % 2 ) != 0 ) + { + produced_level.push_back( produced_level.back() ); + } + + const size_t sibling_index = produced_index ^ 1U; + auto *step = proof->add_produced_branch(); + step->set_sibling_hash( produced_level[sibling_index].data(), produced_level[sibling_index].size() ); + step->set_is_left_sibling( sibling_index < produced_index ); + + std::vector next_level; + next_level.reserve( produced_level.size() / 2 ); + for ( size_t i = 0; i < produced_level.size(); i += 2 ) + { + next_level.push_back( HashNode( produced_level[i], produced_level[i + 1] ) ); + } + + produced_index = produced_index / 2; + produced_level = std::move( next_level ); + } + } + + return witness; + } + + bool TransactionManager::ApplyTransactionToUTXOSnapshot( const std::shared_ptr &tx, + std::vector &snapshot ) const + { + if ( !tx ) + { + return false; + } + const auto remove_inputs = [&]( const std::vector &inputs ) + { + for ( const auto &input : inputs ) + { + auto it = std::find_if( + snapshot.begin(), + snapshot.end(), + [&]( const GeniusUTXO &u ) + { return u.GetTxID() == input.txid_hash_ && u.GetOutputIdx() == input.output_idx_; } ); + if ( it != snapshot.end() ) + { + snapshot.erase( it ); + } + } + }; + const auto tx_hash = base::Hash256::fromReadableString( tx->GetHash() ); + if ( tx_hash.has_error() ) + { + return false; + } + + if ( !tx->HasUTXOParameters() ) + { + return false; + } + + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + return false; + } + const auto &[inputs, outputs] = params_opt.value(); + remove_inputs( inputs ); + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) + { + if ( outputs[i].dest_address == tx->GetSrcAddress() ) + { + snapshot.emplace_back( tx_hash.value(), + i, + outputs[i].encrypted_amount, + outputs[i].token_id, + tx->GetSrcAddress() ); + } + } + return true; + } + + void TransactionManager::SetNonceWindow( uint64_t window ) + { + if ( window == 0 ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Nonce window 0, using default {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + DEFAULT_NONCE_WINDOW ); + nonce_window_m = DEFAULT_NONCE_WINDOW; + return; + } + TransactionManagerLogger()->info( "[{} - full: {}] {}: Setting nonce window to {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + window ); + nonce_window_m = window; + } + + outcome::result TransactionManager::ChangeTransactionState( const std::shared_ptr &tx, + TransactionStatus new_status ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Changing transaction state to {} for transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + static_cast( new_status ), + tx->GetHash() ); + switch ( new_status ) + { + case TransactionStatus::CREATED: + { + std::unique_lock tx_lock( tx_mutex_m ); + const auto key = GetTransactionPath( *tx ); + auto it = tx_processed_m.find( key ); + if ( it != tx_processed_m.end() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Trying to CREATE a transaction that already exists {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return outcome::failure( std::errc::file_exists ); + } + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of CREATE to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + tx_processed_m.emplace( key, TrackedTx{ tx, TransactionStatus::CREATED, tx->GetNonce() } ); + } + break; + case TransactionStatus::SENDING: + { + std::unique_lock tx_lock( tx_mutex_m ); + const auto key = GetTransactionPath( *tx ); + auto it = tx_processed_m.find( key ); + if ( it == tx_processed_m.end() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Trying to SEND a transaction that doesn't exist {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return outcome::failure( std::errc::no_such_file_or_directory ); + } + if ( it->second.status != TransactionStatus::CREATED ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Trying to SEND a transaction that is not in CREATED status {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return outcome::failure( std::errc::invalid_argument ); + } + it->second.status = TransactionStatus::SENDING; + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of SENDING to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + } + break; + case TransactionStatus::VERIFYING: + { + std::unique_lock tx_lock( tx_mutex_m ); + const auto key = GetTransactionPath( *tx ); + auto it = tx_processed_m.find( key ); + + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::VERIFYING ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Trying to VERIFY a transaction that is already in VERIFY {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + break; + } + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Unconfirming transaction {} and verifying it again", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + BOOST_OUTCOME_TRY( RevertTransaction( tx ) ); + + BOOST_OUTCOME_TRY( DeleteTransaction( key, tx->GetTopics() ) ); + + account_m->RollBackPeerConfirmedNonce( it->second.cached_nonce, tx->GetSrcAddress() ); + } + tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::VERIFYING, tx->GetNonce() }; + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of VERIFYING to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Attempting to resume the proposal handling to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + tx_lock.unlock(); + BOOST_OUTCOME_TRY( blockchain_->TryResumeProposal( tx->GetHash() ) ); + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Resumed the proposal handling to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + } + + break; + case TransactionStatus::CONFIRMED: + { + std::unique_lock tx_lock( tx_mutex_m ); + const auto key = GetTransactionPath( *tx ); + auto it = tx_processed_m.find( key ); + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Trying to CONFIRM a transaction that is already CONFIRMED {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + break; + } + tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::CONFIRMED, tx->GetNonce() }; + + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of CONFIRMED to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); + account_m->SetPeerConfirmedNonce( tx->GetNonce(), tx->GetSrcAddress(), tx->GetHash() ); + { + std::lock_guard missing_lock( missing_tx_mutex_ ); + missing_tx_hashes_.erase( tx->GetHash() ); + } + } + + break; + case TransactionStatus::INVALID: + case TransactionStatus::FAILED: + { + std::unique_lock tx_lock( tx_mutex_m ); + const auto key = GetTransactionPath( *tx ); + auto it = tx_processed_m.find( key ); + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::FAILED ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Trying to FAIL a transaction that is already FAILED {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + break; + } + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Unconfirming transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + BOOST_OUTCOME_TRY( RevertTransaction( tx ) ); + + BOOST_OUTCOME_TRY( DeleteTransaction( key, tx->GetTopics() ) ); + + account_m->RollBackPeerConfirmedNonce( it->second.cached_nonce, tx->GetSrcAddress() ); + } + else if ( tx->GetSrcAddress() == account_m->GetAddress() && tx->HasUTXOParameters() ) + { + // Local outgoing tx failed before confirmation: release locally reserved inputs. + auto params_opt = tx->GetUTXOParametersOpt(); + if ( params_opt.has_value() ) + { + account_m->GetUTXOManager().RollbackUTXOs( params_opt->first, tx->GetHash() ); + } + } + tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::FAILED, tx->GetNonce() }; + account_m->ReleaseNonce( tx->GetNonce() ); + + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of FAILED to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + { + std::lock_guard missing_lock( missing_tx_mutex_ ); + missing_tx_hashes_.erase( tx->GetHash() ); + } + } + + break; + default: + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Invalid transaction status {} for transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + static_cast( new_status ), + tx->GetHash() ); + return outcome::failure( std::errc::invalid_argument ); + } + + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Transaction {} state changed to {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + static_cast( new_status ) ); + return outcome::success(); + } + + bool TransactionManager::IsGoingToOverwrite( const std::string &key ) const + { + auto existing_data_result = globaldb_m->Get( key ); + if ( existing_data_result.has_value() ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Key {} already exists in global DB, will overwrite", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + key ); + auto maybe_old_tx = DeSerializeTransaction( existing_data_result.value() ); + if ( maybe_old_tx.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] Failed to deserialize existing transaction, allow to replace it {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); + return false; + } + return true; + } + return false; + } + } fmt::format_context::iterator fmt::formatter::format( diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 623e8f7d9..973f05ef1 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -1,6 +1,6 @@ /** * @file TransactionManager.hpp - * @brief + * @brief Transaction coordination, CRDT sync, and lifecycle tracking for outgoing and incoming account activity. * @date 2024-03-13 * @author Henrique A. Klein (hklein@gnus.ai) */ @@ -22,9 +22,12 @@ #include "account/proto/SGTransaction.pb.h" #include "account/IGeniusTransactions.hpp" #include "account/GeniusAccount.hpp" +#include "account/InputValidators.hpp" #include "base/logger.hpp" #include "base/buffer.hpp" #include "crypto/hasher.hpp" + +#include "blockchain/Blockchain.hpp" #include "processing/proto/SGProcessing.pb.h" #include "outcome/outcome.hpp" @@ -33,6 +36,9 @@ namespace sgns using namespace boost::multiprecision; using EscrowDataPair = std::pair; + /** + * @brief Coordinates transaction creation, CRDT propagation, verification, and status tracking. + */ class TransactionManager : public std::enable_shared_from_this { public: @@ -90,6 +96,7 @@ namespace sgns std::shared_ptr ctx, std::shared_ptr account, std::shared_ptr hasher, + std::shared_ptr blockchain, bool full_node = false, std::chrono::milliseconds timestamp_tolerance = std::chrono::milliseconds( 0 ), std::chrono::milliseconds mutability_window = std::chrono::milliseconds( 0 ) ); @@ -97,11 +104,17 @@ namespace sgns ~TransactionManager(); void Start(); + void RegisterTopicNames(); + void StartListeningTopics(); + void StartCore(); void PrintAccountInfo() const; std::vector> GetOutTransactions() const; std::vector> GetInTransactions() const; + std::vector> GetTransactions( + std::optional tx_status = std::nullopt ) const; + std::vector> GetTransactions() const; /** * @brief Creates and enqueues a transfer transaction. @@ -127,6 +140,19 @@ namespace sgns TokenID tokenid, std::string destination = "" ); + /** + * @brief Creates and enqueues a one-time migration mint transaction. + * @param[in] amount Amount to migrate. + * @param[in] from_version Legacy version namespace for the migration source key. + * @param[in] tokenid Token to mint. + * @param[in] destination Recipient address; defaults to the local account address when empty. + * @return Transaction hash on success. + */ + outcome::result MigrationFunds( uint64_t amount, + std::string from_version, + TokenID tokenid, + std::string destination = "" ); + /** * @brief Creates and enqueues an escrow-hold transaction. * @@ -143,24 +169,9 @@ namespace sgns const std::string &dev_addr, uint64_t peers_cut, const std::string &job_id ); - - /** - * @brief Releases an escrow by paying out subtask workers and the developer. - * - * Fetches the original EscrowTransaction from the CRDT, splits the escrowed - * funds among subtask result nodes (equal shares) with any remainder going to - * the developer address. Enqueues a TransferTransaction and an - * EscrowReleaseTransaction as an atomic batch together with the supplied - * @p crdt_transaction. - * - * @param[in] escrow_path CRDT key of the original escrow transaction. - * @param[in] task_result Processing result containing subtask payees. - * @param[in] crdt_transaction Existing CRDT atomic transaction to attach the batch to. - * @return Hash of the payout TransferTransaction on success. - */ - outcome::result PayEscrow( const std::string &escrow_path, - const SGProcessing::TaskResult &task_result, - std::shared_ptr crdt_transaction ); + outcome::result PayEscrow( const std::string &escrow_path, + const SGProcessing::TaskResult &task_result, + std::shared_ptr crdt_transaction ); // Wait for an incoming transaction to be processed with a timeout TransactionStatus WaitForTransactionIncoming( const std::string &txId, @@ -259,6 +270,7 @@ namespace sgns protected: friend class GeniusNode; + friend class Migration3_6_0To3_7_0; void EnqueueTransaction( TransactionPair element ); void EnqueueTransaction( TransactionItem element ); @@ -268,10 +280,25 @@ namespace sgns private: static constexpr std::string_view TRANSACTION_BASE_FORMAT = "/bc-%hu/"; + struct TrackedTx + { + std::shared_ptr tx; + TransactionStatus status; + uint64_t cached_nonce; // Cache nonce to avoid dereferencing tx + }; + + struct AccountUTXOState + { + uint64_t version{ 0 }; + base::Hash256 root{}; + bool initialized{ false }; + }; + TransactionManager( std::shared_ptr processing_db, std::shared_ptr ctx, std::shared_ptr account, std::shared_ptr hasher, + std::shared_ptr blockchain, bool full_node, std::chrono::milliseconds timestamp_tolerance, std::chrono::milliseconds mutability_window ); @@ -280,7 +307,11 @@ namespace sgns using TransactionParserFn = outcome::result ( TransactionManager::* )( const std::shared_ptr & ); - SGTransaction::DAGStruct FillDAGStruct( std::string transaction_hash = "" ) const; + SGTransaction::DAGStruct FillDAGStruct( std::optional other_chain_hash = std::nullopt ); + std::string GetOutgoingPreviousHash( uint64_t nonce ) const; + std::string GetTrackedOutgoingPreviousHash( uint64_t nonce ) const; + std::string GetPersistedOutgoingPreviousHash( uint64_t nonce ) const; + std::string QueryOutgoingPreviousHashFromCRDT( uint64_t nonce ) const; /** * @brief Commits a TransactionItem to the CRDT. @@ -295,8 +326,7 @@ namespace sgns * * @return Set of nonces that were successfully sent. */ - outcome::result> SendTransactionItem( TransactionItem &item ); - outcome::result ConfirmTransactions(); + outcome::result SendTransactionItem( TransactionItem &item ); /** * @brief Rolls back a failed TransactionItem. @@ -312,8 +342,7 @@ namespace sgns * @brief Returns the set of network IDs to monitor. * On DEV_NET, also includes TEST_NET and MAIN_NET. */ - static std::vector GetMonitoredNetworkIDs(); - + static std::vector GetMonitoredNetworkIDs(); static outcome::result> DeSerializeTransaction( std::string tx_data ); /** @@ -342,6 +371,10 @@ namespace sgns * @brief Dispatches to the type-specific reverter registered in transaction_parsers. */ outcome::result RevertTransaction( const std::shared_ptr &tx ); + bool DoesTransactionMutateUTXOState( const std::shared_ptr &tx ) const; + std::unordered_set CollectTouchedAccounts( const std::shared_ptr &tx ) const; + AccountUTXOState GetOrInitAccountUTXOState( const std::string &address ) const; + void UpdateAccountUTXOState( const std::unordered_set &addresses, bool increment_version ); /** * @brief Loads UTXOs from local storage and/or the network, then processes @@ -403,6 +436,8 @@ namespace sgns std::shared_ptr GetTransactionByNonceAndAddress( uint64_t nonce, const std::string &address ) const; + std::optional GetTrackedTxByNonceAndAddress( uint64_t nonce, const std::string &address ) const; + std::optional GetTrackedTxByHash( const std::string &tx_hash ) const; bool SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ); @@ -416,11 +451,14 @@ namespace sgns */ void TickOnce(); + outcome::result OnConsensusCertificate( const std::string &tx_hash ); + std::shared_ptr globaldb_m; std::shared_ptr ctx_m; std::shared_ptr account_m; std::shared_ptr hasher_m; + std::shared_ptr blockchain_; bool full_node_m; std::string full_node_topic_m; ///< formatted full-node topic State state_m; @@ -442,23 +480,21 @@ namespace sgns mutable std::mutex mutex_m; std::deque tx_queue_m; - struct TrackedTx - { - std::shared_ptr tx; - TransactionStatus status; - uint64_t cached_nonce; // Cache nonce to avoid dereferencing tx - }; - - mutable std::shared_mutex tx_mutex_m; - std::unordered_map tx_processed_m; - std::atomic verifying_count_{ 0 }; // Count of VERIFYING transactions - std::function task_m; - std::atomic stopped_{ false }; - std::chrono::milliseconds timestamp_tolerance_m; - std::chrono::milliseconds mutability_window_m; - - static constexpr std::chrono::milliseconds TIMESTAMP_TOLERANCE = std::chrono::seconds( 10 ); - static constexpr std::chrono::milliseconds MUTABILITY_WINDOW = std::chrono::minutes( 15 ); + mutable std::shared_mutex tx_mutex_m; + std::unordered_map tx_processed_m; + mutable std::shared_mutex account_utxo_state_mutex_; + mutable std::unordered_map account_utxo_state_; + std::atomic utxo_state_tracking_suppression_{ 0 }; + std::unordered_map pending_proposals_; + std::function task_m; + std::atomic stopped_{ false }; + std::chrono::milliseconds timestamp_tolerance_m; + std::chrono::milliseconds mutability_window_m; + uint64_t nonce_window_m = DEFAULT_NONCE_WINDOW; + + static constexpr std::chrono::milliseconds TIMESTAMP_TOLERANCE = std::chrono::seconds( 10 ); + static constexpr std::chrono::milliseconds MUTABILITY_WINDOW = std::chrono::minutes( 15 ); + static constexpr uint64_t DEFAULT_NONCE_WINDOW = 5; std::mutex cv_mutex_; std::condition_variable cv_; @@ -466,6 +502,9 @@ namespace sgns std::queue deleted_data_queue_; std::chrono::steady_clock::time_point last_loop_time_; + std::atomic topic_names_registered_{ false }; + std::atomic listening_topics_started_{ false }; + std::atomic core_started_{ false }; std::mutex missing_tx_mutex_; std::unordered_set missing_tx_hashes_; @@ -475,12 +514,9 @@ namespace sgns outcome::result ParseTransferTransaction( const std::shared_ptr &tx ); outcome::result ParseMintTransaction( const std::shared_ptr &tx ); outcome::result ParseEscrowTransaction( const std::shared_ptr &tx ); - outcome::result ParseEscrowReleaseTransaction( const std::shared_ptr &tx ); - outcome::result RevertTransferTransaction( const std::shared_ptr &tx ); outcome::result RevertMintTransaction( const std::shared_ptr &tx ); outcome::result RevertEscrowTransaction( const std::shared_ptr &tx ); - outcome::result RevertEscrowReleaseTransaction( const std::shared_ptr &tx ); static inline const std::unordered_map> transaction_parsers = { @@ -489,11 +525,10 @@ namespace sgns { "mint", { &TransactionManager::ParseMintTransaction, &TransactionManager::RevertMintTransaction } }, { "mint-v2", { &TransactionManager::ParseMintTransaction, &TransactionManager::RevertMintTransaction } }, + { "migration", + { &TransactionManager::ParseMintTransaction, &TransactionManager::RevertMintTransaction } }, { "escrow-hold", - { &TransactionManager::ParseEscrowTransaction, &TransactionManager::RevertEscrowTransaction } }, - { "escrow-release", - { &TransactionManager::ParseEscrowReleaseTransaction, - &TransactionManager::RevertEscrowReleaseTransaction } } }; + { &TransactionManager::ParseEscrowTransaction, &TransactionManager::RevertEscrowTransaction } } }; base::Logger m_logger = base::createLogger( "TransactionManager" ); @@ -592,11 +627,49 @@ namespace sgns void ChangeState( State new_state ); public: + + enum class WitnessValidationResult : uint8_t + { + VALID, + DRIFT, + INVALID + }; + /** * @brief Looks up the CID associated with a transaction hash in RocksDB, * searching across all monitored networks. */ - outcome::result GetTransactionCID( const std::string &tx_hash ) const; + outcome::result GetTransactionCID( const std::string &tx_hash ) const; + outcome::result HandleNonceConsensusSubject( + const ConsensusManager::Subject &subject ); + bool ValidateTransactionForConsensus( const std::shared_ptr &tx ) const; + bool CheckTransactionWellFormed( const IGeniusTransactions &tx ) const; + bool CheckTransactionAuthorization( const IGeniusTransactions &tx ) const; + bool CheckTransactionTimestamp( const IGeniusTransactions &tx ) const; + bool CheckTransactionReplayProtection( const IGeniusTransactions &tx ) const; + bool CheckTransactionTypeRules( const std::shared_ptr &tx ) const; + std::optional BuildUTXOTransitionCommitment( + const std::shared_ptr &tx ) const; + std::optional BuildUTXOWitness( const std::shared_ptr &tx ) const; + bool ApplyTransactionToUTXOSnapshot( const std::shared_ptr &tx, + std::vector &snapshot ) const; + WitnessValidationResult ValidateWitnessForConsensus( const ConsensusSubject &subject, + const std::shared_ptr &tx ) const; + bool ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, const std::string &address ) const; + void SetNonceWindow( uint64_t window ); + outcome::result ChangeTransactionState( const std::shared_ptr &tx, + TransactionStatus new_status ); + bool HasConfirmedInputConflict( const std::shared_ptr &candidate_tx ) const; + + bool IsGoingToOverwrite( const std::string &key ) const; + + private: + static constexpr std::string_view GENIUS_CHAIN_ID = "supergenius"; + + std::string GetValidationChainId( const std::shared_ptr &tx ) const; + const IInputValidator &GetInputValidator( const std::string &chain_id ) const; + GeniusInputValidator genius_input_validator_; + PublicChainInputValidator public_chain_input_validator_; }; } diff --git a/src/account/TransferTransaction.cpp b/src/account/TransferTransaction.cpp index e912461b6..9b401e298 100644 --- a/src/account/TransferTransaction.cpp +++ b/src/account/TransferTransaction.cpp @@ -29,10 +29,10 @@ namespace sgns return instance; } - std::vector TransferTransaction::SerializeByteVector() + std::vector TransferTransaction::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const { SGTransaction::TransferTx tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( this->dag_st ); + tx_struct.mutable_dag_struct()->CopyFrom( dag ); SGTransaction::UTXOTxParams *utxo_proto_params = tx_struct.mutable_utxo_params(); for ( const auto &[txid_hash_, output_idx_, signature_] : input_tx_ ) diff --git a/src/account/TransferTransaction.hpp b/src/account/TransferTransaction.hpp index ab10f5d57..a4aef5202 100644 --- a/src/account/TransferTransaction.hpp +++ b/src/account/TransferTransaction.hpp @@ -31,7 +31,8 @@ namespace sgns * @brief Serializes the transaction into a byte vector. * @return Serialized bytes. */ - std::vector SerializeByteVector() override; + using IGeniusTransactions::SerializeByteVector; + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; /** * @brief Deserializes a TransferTransaction from bytes. diff --git a/src/account/UTXOManager.cpp b/src/account/UTXOManager.cpp index c7347ffb4..49e083b13 100644 --- a/src/account/UTXOManager.cpp +++ b/src/account/UTXOManager.cpp @@ -1,5 +1,8 @@ #include "UTXOManager.hpp" +#include "UTXOMerkle.hpp" +#include +#include #include #include @@ -9,6 +12,68 @@ namespace sgns { + namespace + { + void RemoveOutPointFromVector( std::vector &outpoints, const OutPoint &target ) + { + outpoints.erase( std::remove( outpoints.begin(), outpoints.end(), target ), outpoints.end() ); + } + + std::string BuildUTXORecordKey( const std::string &owner_address, const OutPoint &outpoint ) + { + return fmt::format( "/utxo/{}/{}:{}", + owner_address, + outpoint.txid_hash_.toReadableString(), + outpoint.output_idx_ ); + } + + std::string BuildCheckpointRecordKey( const std::string &owner_address, uint64_t epoch ) + { + return fmt::format( "/utxo-checkpoint/{}/{}", owner_address, epoch ); + } + + std::string BuildLatestCheckpointPointerKey( const std::string &owner_address ) + { + return fmt::format( "/utxo-checkpoint/{}/latest", owner_address ); + } + + std::optional ParseOwnerAddrFromUTXORecordKey( std::string_view key ) + { + constexpr std::string_view prefix = "/utxo/"; + if ( key.substr( 0, prefix.size() ) != prefix ) + { + return std::nullopt; + } + + auto remainder = key.substr( prefix.size() ); + auto slash_pos = remainder.find( '/' ); + if ( slash_pos == std::string_view::npos || slash_pos == 0 ) + { + return std::nullopt; + } + + return std::string( remainder.substr( 0, slash_pos ) ); + } + + SGTransaction::UTXOEntryState ToProtoState( UTXOManager::UTXOState state ) + { + return state == UTXOManager::UTXOState::UTXO_CONSUMED ? SGTransaction::UTXO_ENTRY_CONSUMED + : SGTransaction::UTXO_ENTRY_READY; + } + + UTXOManager::UTXOState FromProtoState( SGTransaction::UTXOEntryState state ) + { + return state == SGTransaction::UTXO_ENTRY_CONSUMED ? UTXOManager::UTXOState::UTXO_CONSUMED + : UTXOManager::UTXOState::UTXO_READY; + } + + base::Hash256 ComputeMerkleRootFromUTXOList( std::vector unspent ) + { + return utxo_merkle::ComputeMerkleRootFromUTXOs( unspent ); + } + + } // namespace + uint64_t UTXOManager::GetBalance() const { return GetBalance( address_ ); @@ -26,13 +91,23 @@ namespace sgns } std::shared_lock lock( utxos_mutex_ ); - if ( auto it = utxos_.find( address ); it != utxos_.end() ) + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { - for ( const auto &[state, curr] : it->second ) + for ( const auto &outpoint : address_it->second ) { - if ( !curr.GetLock() && state == UTXOState::UTXO_READY ) + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) { - retval += curr.GetAmount(); + continue; + } + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + if ( reserved_outpoints_.find( outpoint ) == reserved_outpoints_.end() ) + { + //TODO - This should return in Genius Tokens but it's not taking into consideration the tokenID. It needs to multiply by the ratio of it + retval += utxo_it->second.utxo.GetAmount(); } } } @@ -57,19 +132,33 @@ namespace sgns } std::shared_lock lock( utxos_mutex_ ); - if ( auto it = utxos_.find( address ); it != utxos_.end() ) + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { - for ( const auto &[state, utxo] : it->second ) + for ( const auto &outpoint : address_it->second ) { - if ( !utxo.GetLock() && token_id.Equals( utxo.GetTokenID() ) && state == UTXOState::UTXO_READY ) + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) + { + continue; + } + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + if ( !token_id.Equals( utxo_it->second.utxo.GetTokenID() ) ) + { + continue; + } + if ( reserved_outpoints_.find( outpoint ) == reserved_outpoints_.end() ) { - balance += utxo.GetAmount(); + balance += utxo_it->second.utxo.GetAmount(); } } } return balance; } + //TODO - Remove the GeniusUTXO from parameters, instead add the necessary fields or IGeniusTransactions outcome::result UTXOManager::PutUTXO( GeniusUTXO new_utxo, const std::string &address ) { // If not a full node and trying to store UTXOs for other addresses, reject @@ -79,42 +168,28 @@ namespace sgns return false; } - std::unique_lock lock( utxos_mutex_ ); - auto &utxo_list = utxos_[address]; + new_utxo.SetOwnerAddress( address ); + const OutPoint outpoint{ new_utxo.GetTxID(), new_utxo.GetOutputIdx() }; - bool is_new = true; - for ( auto it = utxo_list.begin(); it != utxo_list.end(); ) { - auto &[state, curr] = *it; - if ( new_utxo.GetTxID() != curr.GetTxID() ) - { - ++it; - continue; - } - if ( new_utxo.GetOutputIdx() != curr.GetOutputIdx() ) - { - ++it; - continue; - } - if ( state == UTXOState::UTXO_CONSUMED ) + std::unique_lock lock( utxos_mutex_ ); + if ( auto existing = utxo_outpoints_.find( outpoint ); existing != utxo_outpoints_.end() ) { - utxo_list.erase( it ); - is_new = false; - break; + return false; } - //TODO - If it's the same, might be locked, then unlock - is_new = false; - break; - } - if ( is_new ) - { - utxo_list.emplace_back( UTXOState::UTXO_READY, std::move( new_utxo ) ); - BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); + + utxo_outpoints_[outpoint] = + UTXOEntry{ UTXOState::UTXO_READY, new_utxo, 0, std::nullopt, std::nullopt }; + address_outpoints_[address].push_back( outpoint ); } - return is_new; + + BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); + return true; } - outcome::result UTXOManager::DeleteUTXO( const base::Hash256 &utxo_id, const std::string &address ) + outcome::result UTXOManager::DeleteUTXO( const base::Hash256 &utxo_id, + uint32_t output_idx, + const std::string &address ) { // If not a full node and trying to delete UTXOs for other addresses, reject if ( !is_full_node_ && address != address_ ) @@ -122,66 +197,66 @@ namespace sgns logger_->warn( "Non-full node deleting UTXOs for other addresses" ); } - std::unique_lock lock( utxos_mutex_ ); - if ( auto it = utxos_.find( address ); it != utxos_.end() ) { - bool deleted = false; - auto &utxo_list = it->second; - for ( auto utxo_it = utxo_list.begin(); utxo_it != utxo_list.end(); ) + std::unique_lock lock( utxos_mutex_ ); + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { - auto &[state, curr] = *utxo_it; - if ( curr.GetTxID() == utxo_id ) + auto &outpoints = address_it->second; + auto outpoint_it = std::find_if( + outpoints.begin(), + outpoints.end(), + [&]( const OutPoint &outpoint ) + { return outpoint.txid_hash_ == utxo_id && outpoint.output_idx_ == output_idx; } ); + if ( outpoint_it != outpoints.end() ) { - utxo_it = utxo_list.erase( utxo_it ); - deleted = true; - continue; + const OutPoint outpoint = *outpoint_it; + reserved_outpoints_.erase( outpoint ); + utxo_outpoints_.erase( outpoint ); + outpoints.erase( outpoint_it ); } - ++utxo_it; - } - if ( deleted ) - { - BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); } } + BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); return outcome::success(); } outcome::result UTXOManager::ConsumeUTXOs( const std::vector &infos, const std::string &address ) { - bool consumed = true; - std::unique_lock lock( utxos_mutex_ ); - auto &utxo_list = utxos_[address]; - for ( auto &input_info : infos ) + bool consumed = true; { - bool utxo_found = false; - auto utxo_it = utxo_list.end(); - for ( auto it = utxo_list.begin(); it != utxo_list.end(); ++it ) + std::unique_lock lock( utxos_mutex_ ); + for ( auto &input_info : infos ) { - auto &[state, curr] = *it; - if ( input_info.txid_hash_ != curr.GetTxID() ) + const OutPoint outpoint{ input_info.txid_hash_, input_info.output_idx_ }; + bool utxo_found = false; + + if ( auto canonical_it = utxo_outpoints_.find( outpoint ); canonical_it != utxo_outpoints_.end() ) { - continue; + auto &entry = canonical_it->second; + if ( entry.state == UTXOState::UTXO_READY && entry.utxo.GetOwnerAddress() == address ) + { + utxo_found = true; + entry.state = UTXOState::UTXO_CONSUMED; + } } - if ( input_info.output_idx_ != curr.GetOutputIdx() ) + + reserved_outpoints_.erase( outpoint ); + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { - continue; + RemoveOutPointFromVector( address_it->second, outpoint ); } - utxo_found = true; - utxo_it = it; - break; - } - if ( utxo_found ) - { - utxo_list.erase( utxo_it ); - } - else - { - GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID() ); - utxo_list.emplace_back( UTXOState::UTXO_CONSUMED, consumed_utxo ); + + if ( !utxo_found ) + { + GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID(), address ); + utxo_outpoints_[outpoint] = + UTXOEntry{ UTXOState::UTXO_CONSUMED, consumed_utxo, 0, std::nullopt, std::nullopt }; + } + + consumed = consumed && utxo_found; } - consumed = consumed && utxo_found; } BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); @@ -192,27 +267,72 @@ namespace sgns std::vector UTXOManager::GetUTXOs( const std::string &address ) const { std::shared_lock lock( utxos_mutex_ ); - if ( auto it = utxos_.find( address ); it != utxos_.end() ) + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { std::vector result; - result.reserve( it->second.size() ); - for ( const auto &[state, utxo] : it->second ) + result.reserve( address_it->second.size() ); + for ( const auto &outpoint : address_it->second ) { - if ( state == UTXOState::UTXO_CONSUMED ) + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) { continue; } - result.push_back( utxo ); + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + result.push_back( utxo_it->second.utxo ); } return result; } return {}; } - std::unordered_map> UTXOManager::GetAllUTXOs() const + std::vector UTXOManager::GetUTXOsForReservation( const std::string &address, + const std::string &reservation_id ) const { std::shared_lock lock( utxos_mutex_ ); - return utxos_; + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) + { + std::vector result; + result.reserve( address_it->second.size() ); + for ( const auto &outpoint : address_it->second ) + { + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) + { + continue; + } + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + + auto reservation_it = reserved_outpoints_.find( outpoint ); + if ( reservation_it != reserved_outpoints_.end() && reservation_it->second != reservation_id ) + { + continue; + } + + result.push_back( utxo_it->second.utxo ); + } + return result; + } + return {}; + } + + std::unordered_map> UTXOManager::GetAllUTXOs() const + { + std::shared_lock lock( utxos_mutex_ ); + std::unordered_map> result; + for ( const auto &[outpoint, entry] : utxo_outpoints_ ) + { + (void)outpoint; + const auto &owner = entry.utxo.GetOwnerAddress(); + result[owner].emplace_back( entry.state, entry.utxo ); + } + return result; } outcome::result UTXOManager::SetUTXOs( const std::vector &utxos, const std::string &address ) @@ -224,13 +344,31 @@ namespace sgns return std::errc::permission_denied; } - std::unique_lock lock( utxos_mutex_ ); - auto &utxo_list = utxos_[address]; - utxo_list.clear(); - utxo_list.reserve( utxos.size() ); - for ( const auto &utxo : utxos ) { - utxo_list.emplace_back( UTXOState::UTXO_READY, utxo ); + std::unique_lock lock( utxos_mutex_ ); + + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) + { + for ( const auto &outpoint : address_it->second ) + { + utxo_outpoints_.erase( outpoint ); + reserved_outpoints_.erase( outpoint ); + } + address_it->second.clear(); + } + + auto &outpoints = address_outpoints_[address]; + outpoints.clear(); //TODO - Evaluate if this is necessary, since it already clears on the loop above. + outpoints.reserve( utxos.size() ); + for ( const auto &utxo : utxos ) + { + auto owned_utxo = utxo; + owned_utxo.SetOwnerAddress( address ); + const OutPoint outpoint{ owned_utxo.GetTxID(), owned_utxo.GetOutputIdx() }; + utxo_outpoints_[outpoint] = + UTXOEntry{ UTXOState::UTXO_READY, owned_utxo, 0, std::nullopt, std::nullopt }; + outpoints.push_back( outpoint ); + } } if ( auto res = StoreUTXOs( address ); res.has_error() ) @@ -294,75 +432,111 @@ namespace sgns return std::make_pair( inputs, outputs ); } - void UTXOManager::ReserveUTXOs( const std::vector &inputs ) + void UTXOManager::ReserveUTXOs( const std::vector &inputs, const std::string &reservation_id ) { std::unique_lock lock( utxos_mutex_ ); - for ( auto &[state, utxo] : utxos_[address_] ) + for ( const auto &input_utxo : inputs ) { - for ( auto &input_utxo : inputs ) + const OutPoint outpoint{ input_utxo.txid_hash_, input_utxo.output_idx_ }; + auto it = reserved_outpoints_.find( outpoint ); + if ( it == reserved_outpoints_.end() ) { - if ( input_utxo.txid_hash_ == utxo.GetTxID() ) - { - utxo.SetLocked( true ); - } + reserved_outpoints_.emplace( outpoint, reservation_id ); + continue; + } + if ( it->second != reservation_id ) + { + logger_->warn( "Outpoint {}:{} already reserved by another tx", + input_utxo.txid_hash_.toReadableString(), + input_utxo.output_idx_ ); } } } - void UTXOManager::RollbackUTXOs( const std::vector &inputs ) + void UTXOManager::RollbackUTXOs( const std::vector &inputs, const std::string &reservation_id ) { std::unique_lock lock( utxos_mutex_ ); - for ( auto &[state, utxo] : utxos_[address_] ) + for ( const auto &input_utxo : inputs ) { - for ( auto &input_utxo : inputs ) + const OutPoint outpoint{ input_utxo.txid_hash_, input_utxo.output_idx_ }; + auto it = reserved_outpoints_.find( outpoint ); + if ( it == reserved_outpoints_.end() ) { - if ( input_utxo.txid_hash_ == utxo.GetTxID() ) - { - utxo.SetLocked( false ); - } + continue; + } + if ( reservation_id.empty() || it->second == reservation_id ) + { + reserved_outpoints_.erase( it ); } } } bool UTXOManager::VerifyParameters( const UTXOTxParameters ¶ms, const std::string &address ) const { - size_t input_amount = 0; uint64_t expected_amount = 0; std::shared_lock lock( utxos_mutex_ ); - try + std::unordered_set seen_inputs; + seen_inputs.reserve( params.first.size() ); + + for ( const auto &input : params.first ) { - for ( const auto &[state, utxo] : utxos_.at( address ) ) + if ( !verify_signature_( address, input.signature_, input.SerializeForSigning() ) ) { - for ( auto &input : params.first ) - { - if ( state == UTXOState::UTXO_CONSUMED || state == UTXOState::UTXO_RESERVED ) - { - continue; - } - if ( input.txid_hash_ == utxo.GetTxID() ) - { - expected_amount += utxo.GetAmount(); - input_amount += 1; - } - if ( !verify_signature_( address, input.signature_, input.SerializeForSigning() ) ) - { - logger_->warn( "UTXO {} signing does not match", fmt::join( input.txid_hash_, "" ) ); - return false; - } - } + logger_->warn( "UTXO {} signing does not match", fmt::join( input.txid_hash_, "" ) ); + return false; + } + + const OutPoint outpoint{ input.txid_hash_, input.output_idx_ }; + if ( !seen_inputs.insert( outpoint ).second ) + { + logger_->warn( "Duplicate input outpoint detected for {}", input.txid_hash_.toReadableString() ); + return false; + } + + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) + { + logger_->warn( "Unknown outpoint {}:{}", input.txid_hash_.toReadableString(), input.output_idx_ ); + return false; + } + + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + logger_->warn( "Outpoint {}:{} is not spendable", + input.txid_hash_.toReadableString(), + input.output_idx_ ); + return false; } - } - catch ( const std::out_of_range & ) - { - logger_->warn( "Could not find UTXOs from address {}", address ); - return false; - } - lock.unlock(); + const auto &owner_address = utxo_it->second.utxo.GetOwnerAddress(); + const bool delegated_escrow_spend = owner_address != address && + input.output_idx_ == 0 && + utxo_address::IsEscrowLockAddress( owner_address ); + + if ( owner_address != address && !delegated_escrow_spend ) + { + logger_->warn( "Outpoint {}:{} does not belong to {}", + input.txid_hash_.toReadableString(), + input.output_idx_, + address ); + return false; + } + + if ( delegated_escrow_spend ) + { + logger_->debug( "Allowing delegated escrow spend for outpoint {}:{} by {} (lock owner: {})", + input.txid_hash_.toReadableString(), + input.output_idx_, + address.substr( 0, 8 ), + owner_address ); + } + + expected_amount += utxo_it->second.utxo.GetAmount(); + } uint64_t real_amount = std::accumulate( params.second.cbegin(), params.second.cend(), @@ -370,7 +544,72 @@ namespace sgns []( const uint64_t s, const OutputDestInfo &o ) { return o.encrypted_amount + s; } ); - return real_amount == expected_amount && input_amount == params.first.size(); + return real_amount == expected_amount && seen_inputs.size() == params.first.size(); + } + + std::optional UTXOManager::GetOutPointState( const base::Hash256 &utxo_id, + uint32_t output_idx ) const + { + std::shared_lock lock( utxos_mutex_ ); + const OutPoint outpoint{ utxo_id, output_idx }; + auto it = utxo_outpoints_.find( outpoint ); + if ( it == utxo_outpoints_.end() ) + { + return std::nullopt; + } + return it->second.state; + } + + bool UTXOManager::IsOutPointConsumed( const base::Hash256 &utxo_id, uint32_t output_idx ) const + { + auto state = GetOutPointState( utxo_id, output_idx ); + return state.has_value() && state.value() == UTXOState::UTXO_CONSUMED; + } + + base::Hash256 UTXOManager::ComputeUTXOMerkleRoot() const + { + return ComputeUTXOMerkleRoot( address_ ); + } + + base::Hash256 UTXOManager::ComputeUTXOMerkleRoot( const std::string &address ) const + { + if ( !is_full_node_ && address != address_ ) + { + logger_->warn( "Non-full node cannot compute UTXO Merkle root for other addresses" ); + return utxo_merkle::EmptyUTXOMerkleRoot(); + } + + std::vector unspent; + { + std::shared_lock lock( utxos_mutex_ ); + auto it = address_outpoints_.find( address ); + if ( it == address_outpoints_.end() ) + { + return utxo_merkle::EmptyUTXOMerkleRoot(); + } + + unspent.reserve( it->second.size() ); + for ( const auto &outpoint : it->second ) + { + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) + { + continue; + } + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + unspent.push_back( utxo_it->second.utxo ); + } + } + + return ComputeMerkleRootFromUTXOList( std::move( unspent ) ); + } + + base::Hash256 UTXOManager::ComputeUTXOMerkleRootFromSnapshot( const std::vector &utxos ) const + { + return ComputeMerkleRootFromUTXOList( utxos ); } outcome::result UTXOManager::LoadUTXOs( std::shared_ptr db ) @@ -381,16 +620,28 @@ namespace sgns return std::errc::invalid_argument; } - if ( db_ != nullptr ) { - logger_->warn( "UTXOs were already loaded" ); + std::unique_lock lock( utxos_mutex_ ); + if ( db_ != nullptr ) + { + logger_->warn( "UTXOs were already loaded" ); + } + db_ = std::move( db ); + utxo_outpoints_.clear(); + address_outpoints_.clear(); + reserved_outpoints_.clear(); + } + + auto db_handle = AcquireStorage(); + if ( db_handle == nullptr ) + { + logger_->error( "Tried to query UTXOs without loading DB" ); + return storage::DatabaseError::UNITIALIZED; } - db_ = std::move( db ); - utxos_.clear(); base::Buffer key_buf; key_buf.put( DB_PREFIX ); - auto utxo_list = db_->query( key_buf ); + auto utxo_list = db_handle->query( key_buf ); if ( utxo_list.has_error() ) { @@ -409,93 +660,341 @@ namespace sgns return false; } - for ( const auto &[key, params] : utxo_list.value() ) { - std::string address( key.subbuffer( DB_PREFIX.size() + 1 ).toString() ); - logger_->info( "Loading UTXOs of address {}", address ); - - SGTransaction::UTXOList utxos; - - if ( !utxos.ParseFromArray( params.data(), params.size() ) ) + std::unique_lock lock( utxos_mutex_ ); + for ( const auto &[key, params] : utxo_list.value() ) { - logger_->error( "Failed to deserialize UTXOs" ); - return std::errc::bad_message; - } + auto owner_addr_opt = ParseOwnerAddrFromUTXORecordKey( key.toString() ); + if ( !owner_addr_opt.has_value() ) + { + logger_->warn( "Skipping malformed UTXO key {}", key.toString() ); + continue; + } + const auto &address = owner_addr_opt.value(); - utxos_[address].reserve( utxos.utxos_size() ); + SGTransaction::UTXOEntryRecord entry_record; + if ( !entry_record.ParseFromArray( params.data(), params.size() ) ) + { + logger_->error( "Failed to deserialize UTXO record for address {}", address ); + return std::errc::bad_message; + } - for ( int i = 0; i < utxos.utxos_size(); ++i ) - { - const auto &utxo = utxos.utxos( i ); - BOOST_OUTCOME_TRY( auto hash, - base::Hash256::fromSpan( gsl::span( - reinterpret_cast( const_cast( utxo.hash().data() ) ), - utxo.hash().size() ) ) ); + if ( !entry_record.owner_address().empty() && entry_record.owner_address() != address ) + { + logger_->warn( "UTXO owner mismatch in key/value for {}", address ); + } - auto token_id = TokenID::FromBytes( utxo.token().data(), utxo.token().size() ); + const auto state = FromProtoState( entry_record.state() ); + + BOOST_OUTCOME_TRY( + auto hash, + base::Hash256::fromSpan( gsl::span( + reinterpret_cast( const_cast( entry_record.utxo().hash().data() ) ), + entry_record.utxo().hash().size() ) ) ); + + auto token_id = TokenID::FromBytes( entry_record.utxo().token().data(), + entry_record.utxo().token().size() ); + GeniusUTXO loaded_utxo( hash, + entry_record.utxo().output_idx(), + entry_record.utxo().amount(), + token_id, + address ); + const auto outpoint = loaded_utxo.GetOutPoint(); + UTXOEntry loaded_entry; + loaded_entry.state = state; + loaded_entry.utxo = loaded_utxo; + loaded_entry.created_epoch = entry_record.created_epoch(); + if ( entry_record.has_spent_epoch() ) + { + loaded_entry.spent_epoch = entry_record.spent_epoch(); + } + if ( entry_record.has_spent_by_txid() ) + { + BOOST_OUTCOME_TRY( auto spent_by_hash, + base::Hash256::fromSpan( gsl::span( + reinterpret_cast( const_cast( entry_record.spent_by_txid().data() ) ), + entry_record.spent_by_txid().size() ) ) ); + loaded_entry.spent_by_txid = spent_by_hash; + } - utxos_[address].emplace_back( UTXOState::UTXO_READY, - GeniusUTXO( hash, utxo.output_idx(), utxo.amount(), token_id ) ); + utxo_outpoints_[outpoint] = std::move( loaded_entry ); + address_outpoints_[address].push_back( outpoint ); } } - return true; + return !utxo_outpoints_.empty(); + } + + std::shared_ptr UTXOManager::AcquireStorage() const + { + std::shared_lock lock( utxos_mutex_ ); + return db_; + } + + void UTXOManager::ReleaseStorage() + { + std::unique_lock lock( utxos_mutex_ ); + db_.reset(); } outcome::result UTXOManager::StoreUTXOs( const std::string &address ) { - if ( db_ == nullptr ) + auto db = AcquireStorage(); + if ( db == nullptr ) { logger_->error( "Tried to store UTXOs without loading DB" ); return storage::DatabaseError::UNITIALIZED; } - SGTransaction::UTXOList utxos; + base::Buffer existing_prefix; + existing_prefix.put( fmt::format( "{}/{}/", DB_PREFIX, address ) ); + + auto existing_records = db->query( existing_prefix ); + if ( existing_records.has_error() && existing_records.error() != storage::DatabaseError::NOT_FOUND ) + { + logger_->error( "Failed to query existing UTXO records for address {}", address ); + return existing_records.error(); + } + + if ( existing_records.has_value() ) + { + //TODO - not great because it's not atomic, so we lose the record and if we shutdown before we record it is gone. + for ( const auto &[existing_key, _] : existing_records.value() ) + { + if ( auto rem_res = db->remove( existing_key ); rem_res.has_error() ) + { + logger_->error( "Failed to remove old UTXO record for address {}", address ); + return rem_res.error(); + } + } + } - try + std::vector> entries_to_store; { - for ( const auto &[state, utxo] : utxos_.at( address ) ) + std::shared_lock lock( utxos_mutex_ ); + entries_to_store.reserve( utxo_outpoints_.size() ); + for ( const auto &[outpoint, entry] : utxo_outpoints_ ) { - if ( state != UTXOState::UTXO_READY ) + if ( entry.utxo.GetOwnerAddress() != address ) { continue; } - auto new_utxo = utxos.add_utxos(); - new_utxo->set_hash( utxo.GetTxID().data(), utxo.GetTxID().size() ); - new_utxo->set_token( utxo.GetTokenID().bytes().data(), utxo.GetTokenID().size() ); - new_utxo->set_amount( utxo.GetAmount() ); - new_utxo->set_output_idx( utxo.GetOutputIdx() ); + entries_to_store.emplace_back( outpoint, entry ); + } + } + + uint64_t stored = 0; + for ( const auto &[outpoint, entry] : entries_to_store ) + { + SGTransaction::UTXOEntryRecord entry_record; + auto *utxo_proto = entry_record.mutable_utxo(); + const auto txid = entry.utxo.GetTxID(); + const auto token_id = entry.utxo.GetTokenID(); + utxo_proto->set_hash( txid.data(), txid.size() ); + utxo_proto->set_token( token_id.bytes().data(), token_id.size() ); + utxo_proto->set_amount( entry.utxo.GetAmount() ); + utxo_proto->set_output_idx( entry.utxo.GetOutputIdx() ); + entry_record.set_owner_address( address ); + entry_record.set_state( ToProtoState( entry.state ) ); + entry_record.set_created_epoch( entry.created_epoch ); + entry_record.set_has_spent_epoch( entry.spent_epoch.has_value() ); + if ( entry.spent_epoch.has_value() ) + { + entry_record.set_spent_epoch( entry.spent_epoch.value() ); + } + entry_record.set_has_spent_by_txid( entry.spent_by_txid.has_value() ); + if ( entry.spent_by_txid.has_value() ) + { + entry_record.set_spent_by_txid( entry.spent_by_txid.value().data(), + entry.spent_by_txid.value().size() ); + } + + base::Buffer value_buf( std::vector( entry_record.ByteSizeLong() ) ); + if ( !entry_record.SerializeToArray( value_buf.data(), value_buf.size() ) ) + { + logger_->error( "Failed to serialize UTXO record for address {}", address ); + return std::errc::bad_message; + } + + base::Buffer key_buf; + key_buf.put( BuildUTXORecordKey( address, outpoint ) ); + + if ( auto put_res = db->put( key_buf, value_buf ); put_res.has_error() ) + { + logger_->error( "Error when storing UTXO record for address {}", address ); + return put_res.error(); } + ++stored; + } + + logger_->info( "Stored {} UTXOs for address {}", stored, address ); + return outcome::success(); + } + + outcome::result UTXOManager::CreateCheckpoint( uint64_t epoch, + const base::Hash256 &last_finalized_tx, + const base::Hash256 ®istry_hash ) + { + return CreateCheckpoint( address_, epoch, last_finalized_tx, registry_hash ); + } + + outcome::result UTXOManager::CreateCheckpoint( const std::string &address, + uint64_t epoch, + const base::Hash256 &last_finalized_tx, + const base::Hash256 ®istry_hash ) + { + auto db = AcquireStorage(); + if ( db == nullptr ) + { + logger_->error( "Tried to create checkpoint without loading DB" ); + return storage::DatabaseError::UNITIALIZED; } - catch ( const std::out_of_range & ) + + if ( !is_full_node_ && address != address_ ) { - logger_->error( "There are no UTXOs in cache for address {}", address ); - return std::errc::bad_address; + logger_->warn( "Non-full node cannot create checkpoint for other addresses" ); + return std::errc::permission_denied; } - base::Buffer buf( std::vector( utxos.ByteSizeLong() ) ); - if ( !utxos.SerializeToArray( buf.data(), buf.size() ) ) + std::vector unspent_snapshot; { - logger_->error( "Failed to serialize to array" ); + std::shared_lock lock( utxos_mutex_ ); + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) + { + unspent_snapshot.reserve( address_it->second.size() ); + for ( const auto &outpoint : address_it->second ) + { + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) + { + continue; + } + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + unspent_snapshot.push_back( utxo_it->second.utxo ); + } + } + } + + SGTransaction::UTXOCheckpointRecord checkpoint_record; + checkpoint_record.set_owner_address( address ); + checkpoint_record.set_epoch( epoch ); + checkpoint_record.set_last_finalized_tx( last_finalized_tx.data(), last_finalized_tx.size() ); + checkpoint_record.set_registry_hash( registry_hash.data(), registry_hash.size() ); + const auto utxo_root = ComputeMerkleRootFromUTXOList( unspent_snapshot ); + checkpoint_record.set_utxo_merkle_root( utxo_root.data(), utxo_root.size() ); + checkpoint_record.set_utxo_count( unspent_snapshot.size() ); + const auto now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ); + checkpoint_record.set_created_at_ms( static_cast( now_ms.count() ) ); + + base::Buffer checkpoint_value_buf( std::vector( checkpoint_record.ByteSizeLong() ) ); + if ( !checkpoint_record.SerializeToArray( checkpoint_value_buf.data(), checkpoint_value_buf.size() ) ) + { + logger_->error( "Failed to serialize checkpoint for address {}", address ); return std::errc::bad_message; } - std::string key( DB_PREFIX ); - key.push_back( '/' ); - key.append( address ); - base::Buffer key_buf; - key_buf.put( key ); + const auto checkpoint_key = BuildCheckpointRecordKey( address, epoch ); + base::Buffer checkpoint_key_buf; + checkpoint_key_buf.put( checkpoint_key ); + if ( auto put_res = db->put( checkpoint_key_buf, checkpoint_value_buf ); put_res.has_error() ) + { + logger_->error( "Failed to store checkpoint record for address {}", address ); + return put_res.error(); + } - if ( auto result = db_->put( key_buf, buf ); result.has_error() ) + base::Buffer latest_pointer_key_buf; + latest_pointer_key_buf.put( BuildLatestCheckpointPointerKey( address ) ); + base::Buffer latest_pointer_value_buf; + latest_pointer_value_buf.put( checkpoint_key ); + if ( auto put_latest_res = db->put( latest_pointer_key_buf, latest_pointer_value_buf ); + put_latest_res.has_error() ) { - logger_->error( "Error when storing UTXOs" ); - return result.error(); + logger_->error( "Failed to store checkpoint latest pointer for address {}", address ); + return put_latest_res.error(); } - logger_->info( "Stored {} UTXOs for address {}", utxos.utxos_size(), address ); + logger_->info( "Created checkpoint owner={} epoch={} utxo_count={}", address, epoch, unspent_snapshot.size() ); return outcome::success(); } + outcome::result> UTXOManager::LoadLatestCheckpoint( + const std::string &address ) const + { + auto db = AcquireStorage(); + if ( db == nullptr ) + { + logger_->error( "Tried to load checkpoint without loading DB" ); + return storage::DatabaseError::UNITIALIZED; + } + + if ( !is_full_node_ && address != address_ ) + { + logger_->warn( "Non-full node cannot load checkpoint for other addresses" ); + return std::errc::permission_denied; + } + + base::Buffer latest_pointer_key_buf; + latest_pointer_key_buf.put( BuildLatestCheckpointPointerKey( address ) ); + auto latest_pointer_value = db->get( latest_pointer_key_buf ); + if ( latest_pointer_value.has_error() ) + { + if ( latest_pointer_value.error() == storage::DatabaseError::NOT_FOUND ) + { + return std::optional{}; + } + logger_->error( "Failed to load latest checkpoint pointer for address {}", address ); + return latest_pointer_value.error(); + } + + base::Buffer checkpoint_key_buf; + checkpoint_key_buf.put( latest_pointer_value.value().toString() ); + auto checkpoint_value = db->get( checkpoint_key_buf ); + if ( checkpoint_value.has_error() ) + { + if ( checkpoint_value.error() == storage::DatabaseError::NOT_FOUND ) + { + return std::optional{}; + } + logger_->error( "Failed to load checkpoint record for address {}", address ); + return checkpoint_value.error(); + } + + SGTransaction::UTXOCheckpointRecord checkpoint_record; + if ( !checkpoint_record.ParseFromArray( checkpoint_value.value().data(), checkpoint_value.value().size() ) ) + { + logger_->error( "Failed to deserialize checkpoint record for address {}", address ); + return std::errc::bad_message; + } + + BOOST_OUTCOME_TRY( auto last_finalized_tx_hash, + base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( + checkpoint_record.last_finalized_tx().data() ) ), + checkpoint_record.last_finalized_tx().size() ) ) ); + BOOST_OUTCOME_TRY( auto registry_hash, + base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( + checkpoint_record.registry_hash().data() ) ), + checkpoint_record.registry_hash().size() ) ) ); + BOOST_OUTCOME_TRY( auto utxo_root_hash, + base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( + checkpoint_record.utxo_merkle_root().data() ) ), + checkpoint_record.utxo_merkle_root().size() ) ) ); + + UTXOCheckpoint checkpoint; + checkpoint.owner_address = checkpoint_record.owner_address(); + checkpoint.epoch = checkpoint_record.epoch(); + checkpoint.last_finalized_tx = last_finalized_tx_hash; + checkpoint.registry_hash = registry_hash; + checkpoint.utxo_merkle_root = utxo_root_hash; + checkpoint.utxo_count = checkpoint_record.utxo_count(); + checkpoint.created_at_ms = checkpoint_record.created_at_ms(); + + return std::optional{ checkpoint }; + } + outcome::result, uint64_t>> UTXOManager::SelectUTXOs( uint64_t required_amount, const TokenID &token_id ) { @@ -503,29 +1002,38 @@ namespace sgns uint64_t selected_amount = 0; std::shared_lock lock( utxos_mutex_ ); - for ( const auto &[state, utxo] : utxos_[address_] ) + if ( auto address_it = address_outpoints_.find( address_ ); address_it != address_outpoints_.end() ) { - if ( selected_amount >= required_amount ) - { - break; - } - if ( utxo.GetLock() ) - { - continue; - } - if ( state == UTXOState::UTXO_CONSUMED || state == UTXOState::UTXO_RESERVED ) + for ( const auto &outpoint : address_it->second ) { - continue; - } - if ( !token_id.Equals( utxo.GetTokenID() ) ) - { - continue; - } + if ( selected_amount >= required_amount ) + { + break; + } - inputs.push_back( { utxo.GetTxID(), utxo.GetOutputIdx(), {} } ); - selected_amount += utxo.GetAmount(); + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) + { + continue; + } + const auto &entry = utxo_it->second; + if ( entry.state != UTXOState::UTXO_READY ) + { + continue; + } + if ( reserved_outpoints_.find( outpoint ) != reserved_outpoints_.end() ) + { + continue; + } + if ( !token_id.Equals( entry.utxo.GetTokenID() ) ) + { + continue; + } + + inputs.push_back( { entry.utxo.GetTxID(), entry.utxo.GetOutputIdx(), {} } ); + selected_amount += entry.utxo.GetAmount(); + } } - lock.unlock(); // Abort if insufficient funds if ( selected_amount < required_amount || inputs.empty() ) diff --git a/src/account/UTXOManager.hpp b/src/account/UTXOManager.hpp index 08c0752f5..351273bdc 100644 --- a/src/account/UTXOManager.hpp +++ b/src/account/UTXOManager.hpp @@ -1,3 +1,9 @@ +/** + * @file UTXOManager.hpp + * @brief In-memory and persisted UTXO state manager with reservation and checkpoint helpers. + * @date 2026-01-20 + * @author Henrique A. Klein (hklein@gnus.ai) + */ #pragma once #include "GeniusUTXO.hpp" @@ -7,26 +13,97 @@ #include "crdt/globaldb/globaldb.hpp" #include "storage/rocksdb/rocksdb.hpp" +#include #include +#include +#include namespace sgns { + /** + * @brief Hash functor for using OutPoint keys in unordered containers. + */ + struct OutPointHash + { + /** + * @brief Computes a combined hash for a transaction id and output index pair. + * @param[in] outpoint The OutPoint to hash, containing a transaction id and output index. + * @return Size of the combined hash + */ + size_t operator()( const OutPoint &outpoint ) const + { + size_t seed = std::hash{}( outpoint.txid_hash_ ); + boost::hash_combine( seed, outpoint.output_idx_ ); + return seed; + } + }; + + /** + * @brief Owns the local UTXO set, supports coin selection, validation, persistence, + * reservations, and deterministic snapshot hashing. + */ class UTXOManager { public: + /** + * @brief Lifecycle state stored for each tracked UTXO. + */ enum class UTXOState : uint8_t { - UTXO_READY, - UTXO_RESERVED, - UTXO_CONSUMED + UTXO_READY, ///< UTXO is unspent and available for use + UTXO_CONSUMED ///< UTXO has been consumed by a transaction and is no longer available }; - using UTXOData = std::pair; - using SignFunc = std::function( const std::vector &data )>; + /** + * @brief UTXO state paired with the actual UTXO + */ + using UTXOData = std::pair; + + /** + * @brief Metadata tracked for each outpoint in the local registry. + */ + struct UTXOEntry + { + UTXOState state{ UTXOState::UTXO_READY }; ///< Current lifecycle state of the UTXO + GeniusUTXO utxo; ///< The actual UTXO data + uint64_t created_epoch{ 0 }; ///< Epoch when the UTXO was created + std::optional spent_epoch; ///< Epoch when the UTXO was consumed, if applicable + std::optional spent_by_txid; ///< Transaction ID that consumed this UTXO, if applicable + }; + + /** + * @brief Persisted checkpoint snapshot used to audit finalized UTXO state at a given epoch. + */ + struct UTXOCheckpoint + { + std::string owner_address; ///< Owner address associated with this checkpoint + uint64_t epoch{ 0 }; ///< Epoch number when the checkpoint was created + base::Hash256 + last_finalized_tx{}; ///< Transaction ID of the last finalized transaction at the time of checkpointing + base::Hash256 registry_hash{}; ///< Hash of the full UTXO registry state at the time of checkpointing. + base::Hash256 utxo_merkle_root{}; ///< Merkle root of the unspent UTXOs at the time of checkpointing. + uint64_t utxo_count{ 0 }; ///< Total number of UTXOs included in the checkpoint + uint64_t created_at_ms{ 0 }; ///< Timestamp in milliseconds when the checkpoint was created + }; + + /// @brief Maps an outpoint to its UTXO Entry + using UTXOOutPointMap = std::unordered_map; + /// @brief Maps an owner address to a list of outpoints they own + using AddressOutPointList = std::unordered_map>; + /// @brief Method to sign a vector of bytes, returning the signature bytes + using SignFunc = std::function( const std::vector &data )>; + /// @brief Method to verify a signature given an address, signature bytes, and original data using VerifySignatureFunc = std::function &signature, const std::vector &data )>; + /** + * @brief Construct a new UTXOManager object + * @param[in] is_full_node True if the node is a full node + * @param[in] address The address of the node + * @param[in] sign The signer method + * @param[in] verify_signature The verifier method + */ UTXOManager( const bool is_full_node, std::string address, SignFunc sign, @@ -44,6 +121,11 @@ namespace sgns */ [[nodiscard]] uint64_t GetBalance() const; + /** + * @brief Get the informed address balance + * @param[in] address The address to get the balance for + * @return The total balance of the account + */ [[nodiscard]] uint64_t GetBalance( const std::string &address ) const; /** @@ -53,6 +135,12 @@ namespace sgns */ uint64_t GetBalance( const TokenID &token_id ) const; + /** + * @brief Get the balance of the informed address for a specific token + * @param[in] token_id The token ID to get the balance for + * @param[in] address The address to get the balance for + * @return The balance of the account for the specific token and address + */ uint64_t GetBalance( const TokenID &token_id, const std::string &address ) const; /** @@ -63,6 +151,11 @@ namespace sgns */ outcome::result PutUTXO( GeniusUTXO new_utxo, const std::string &address ); + /** + * @brief Adds a new UTXO to the account using the manager's default address. + * @param[in] new_utxo The UTXO to be added + * @return true if added successfully, false otherwise + */ outcome::result PutUTXO( const GeniusUTXO &new_utxo ) { return PutUTXO( new_utxo, address_ ); @@ -71,9 +164,12 @@ namespace sgns /** * @brief Delete a UTXO from the account * @param[in] utxo_id The ID of the UTXO to be deleted + * @param[in] output_idx The output index of the UTXO * @param address Address to remove the UTXO from */ - outcome::result DeleteUTXO( const base::Hash256 &utxo_id, const std::string &address ); + outcome::result DeleteUTXO( const base::Hash256 &utxo_id, + uint32_t output_idx, + const std::string &address ); /** * @brief Consume UTXOs from the account @@ -83,6 +179,11 @@ namespace sgns */ outcome::result ConsumeUTXOs( const std::vector &infos, const std::string &address ); + /** + * @brief Consume UTXOs from the default owner address tracked by this manager. + * @param[in] infos Vector of UTXO information to be consumed + * @return true if all UTXOs were consumed, false otherwise + */ outcome::result ConsumeUTXOs( const std::vector &infos ) { return ConsumeUTXOs( infos, address_ ); @@ -95,11 +196,28 @@ namespace sgns */ std::vector GetUTXOs( const std::string &address ) const; + /** + * @brief Returns spendable UTXOs owned by the manager's default address. + * @return The vector of UTXOs for the manager's default address + */ std::vector GetUTXOs() const { return GetUTXOs( address_ ); } + /** + * @brief Get UTXOs for a specific address that are reserved under a given reservation ID + * @param[in] address The address to get UTXOs for + * @param[in] reservation_id The reservation ID to filter UTXOs by + * @return The vector of UTXOs for the address under the reservation ID + */ + std::vector GetUTXOsForReservation( const std::string &address, + const std::string &reservation_id ) const; + + /** + * @brief Get all UTXOs tracked by the manager, grouped by owner address + * @return A map of owner addresses to their corresponding vectors of UTXOs + */ std::unordered_map> GetAllUTXOs() const; /** @@ -109,54 +227,201 @@ namespace sgns */ outcome::result SetUTXOs( const std::vector &utxos, const std::string &address ); + /** + * @brief Set UTXOs for the manager's default address (replaces existing UTXOs) + * @param[in] utxos Vector of UTXOs to set for the manager's default address + */ outcome::result SetUTXOs( const std::vector &utxos ) { return SetUTXOs( utxos, address_ ); } + /** + * @brief Create the input and output parameters for a single-output transfer, selecting from available UTXOs. + * @param[in] amount The amount to transfer + * @param[in] dest_address The destination address for the transfer + * @param[in] token_id The token ID to transfer + * @return The combined input and output parameters for the transaction if successful, or an error if selection or signing failed + */ outcome::result CreateTxParameter( uint64_t amount, std::string dest_address, TokenID token_id ); + /** + * @brief Selects and signs inputs for a multi-output transfer. + * @param[in] destinations The list of destination addresses and amounts for the transfer + * @param[in] token_id The token ID to transfer + * @return The combined input and output parameters for the transaction if successful, or an error if selection or signing failed + */ outcome::result CreateTxParameter( const std::vector &destinations, const TokenID &token_id ); - void ReserveUTXOs( const std::vector &inputs ); + /** + * @brief Marks inputs as reserved so they are not reused by concurrent transaction assembly. + * @param[in] inputs The list of UTXOs to reserve + * @param[in] reservation_id The ID for the reservation + */ + void ReserveUTXOs( const std::vector &inputs, const std::string &reservation_id ); - void RollbackUTXOs( const std::vector &inputs ); + /** + * @brief Releases a previous reservation without consuming the inputs. + * @param[in] inputs The list of UTXOs to release + * @param[in] reservation_id The ID for the reservation + */ + void RollbackUTXOs( const std::vector &inputs, const std::string &reservation_id ); + /** + * @brief Verifies ownership and signatures for UTXO transaction parameters using the default address. + * @param[in] params The transaction parameters to verify, including inputs and outputs + * @return true if all signatures are valid and correspond to the default address, false otherwise + */ bool VerifyParameters( const UTXOTxParameters ¶ms ) const { return VerifyParameters( params, address_ ); } + /** + * @brief Verifies ownership and signatures for UTXO transaction parameters using an explicit address. + * @param[in] params The transaction parameters to verify, including inputs and outputs + * @param[in] address The address to verify ownership against + * @return true if all signatures are valid and correspond to the specified address, false otherwise + */ bool VerifyParameters( const UTXOTxParameters ¶ms, const std::string &address ) const; + /** + * @brief Returns the tracked state of a specific outpoint when present. + * @param[in] utxo_id The transaction hash that created the UTXO + * @param[in] output_idx The output index of the UTXO within the transaction + * @return If the outpoint is tracked, returns its current state (e.g. ready or consumed); if not tracked, returns std::nullopt + */ + std::optional GetOutPointState( const base::Hash256 &utxo_id, uint32_t output_idx ) const; + + /** + * @brief Indicates whether a specific outpoint has already been consumed. + * @param[in] utxo_id The transaction hash that created the UTXO + * @param[in] output_idx The output index of the UTXO within the transaction + * @return true if the outpoint is consumed, false otherwise + */ + bool IsOutPointConsumed( const base::Hash256 &utxo_id, uint32_t output_idx ) const; + + /** + * @brief Compute a deterministic Merkle root for unspent UTXOs owned by this node address + * @return The computed UTXO Merkle root for this node address + */ + [[nodiscard]] base::Hash256 ComputeUTXOMerkleRoot() const; + + /** + * @brief Compute a deterministic Merkle root for unspent UTXOs from a specific address + * @param[in] address The address to compute the UTXO Merkle root for + * @return The computed UTXO Merkle root for the specified address + */ + [[nodiscard]] base::Hash256 ComputeUTXOMerkleRoot( const std::string &address ) const; + + /** + * @brief Compute deterministic UTXO Merkle root from an explicit UTXO snapshot + * @param[in] utxos The list of UTXOs to include in the Merkle root computation + * @return The computed UTXO Merkle root + */ + [[nodiscard]] base::Hash256 ComputeUTXOMerkleRootFromSnapshot( const std::vector &utxos ) const; + + /** + * @brief Loads the UTXO state for the manager's default address from persistent storage. + * @param[in] db The RocksDB instance to load from + * @return true if loaded successfully, false if no UTXOs were found, or an error if loading failed + */ outcome::result LoadUTXOs( std::shared_ptr db ); /** - * @return True if loaded any UTXOs, false if loaded 0 UTXOs and error if one occurred + * @brief Releases the current RocksDB handle used for persistence. + */ + void ReleaseStorage(); + + /** + * @brief Stores the current UTXO state for the manager's default address to persistent storage. + * @param[in] address The address to store UTXOs for */ outcome::result StoreUTXOs( const std::string &address ); + /** + * @brief Creates a checkpoint for the manager's default address. + * @param[in] epoch The epoch number associated with the checkpoint + * @param[in] last_finalized_tx The transaction ID of the last finalized transaction at the time of checkpointing + * @param[in] registry_hash The hash of the full registry state at the time of checkpointing + + */ + outcome::result CreateCheckpoint( uint64_t epoch, + const base::Hash256 &last_finalized_tx, + const base::Hash256 ®istry_hash ); + + /** + * @brief Creates a checkpoint for an explicit owner address. + * @param[in] address The address for which to create a checkpoint + * @param[in] epoch The epoch number associated with the checkpoint + * @param[in] last_finalized_tx The transaction ID of the last finalized transaction at the time of checkpointing + * @param[in] registry_hash The hash of the full registry state at the time of checkpointing + */ + outcome::result CreateCheckpoint( const std::string &address, + uint64_t epoch, + const base::Hash256 &last_finalized_tx, + const base::Hash256 ®istry_hash ); + + /** + * @brief Loads the latest checkpoint for the default owner address. + * @return If successful, returns the latest checkpoint for the manager's default address; if no checkpoint is found, returns std::nullopt; if an error occurs during loading, returns the error + */ + outcome::result> LoadLatestCheckpoint() const + { + return LoadLatestCheckpoint( address_ ); + } + + /** + * @brief Loads the latest checkpoint for the provided owner address. + * @param[in] address The owner address to load the checkpoint for + * @return If successful, returns the latest checkpoint for the manager's default address; if no checkpoint is found, returns std::nullopt; if an error occurs during loading, returns the error + */ + outcome::result> LoadLatestCheckpoint( const std::string &address ) const; + private: + /// Prefix for UTXO-related keys in RocksDB static constexpr std::string_view DB_PREFIX = "/utxo"; + ///< Prefix for UTXO checkpoint keys in RocksDB + static constexpr std::string_view CHECKPOINT_PREFIX = "/utxo-checkpoint"; + + /** + * @brief Grabs the current storage as a shared pointer copy + * @return The database handle as a shared pointer + */ + std::shared_ptr AcquireStorage() const; + /** + * @brief Selects UTXOs to cover a required amount for a specific token, excluding reserved outpoints, and returns the selected inputs along with the total selected amount. + * @param[in] required_amount The total amount that needs to be covered by the selected UTXOs + * @param[in] token_id The token ID that the selected UTXOs must match + * @return If successful, returns a pair containing the vector of selected UTXO input information and the total amount covered by those inputs; if selection fails (e.g., insufficient funds), returns an error + */ outcome::result, uint64_t>> SelectUTXOs( uint64_t required_amount, const TokenID &token_id ); + /** + * @brief Signs the provided UTXO inputs using the configured signing function. + * @param[in] inputs The vector of UTXO input information to sign + */ void SignInputs( std::vector &inputs ) const; + /// Logger instance for UTXOManager base::Logger logger_ = base::createLogger( "UTXOManager" ); - bool is_full_node_; - std::string address_; - SignFunc sign_; - VerifySignatureFunc verify_signature_; - std::shared_ptr db_; - - mutable std::shared_mutex utxos_mutex_; ///< Mutex for the UTXOs map - std::unordered_map> utxos_; ///< Map of UTXOs by address + bool is_full_node_; ///< Flag that indicates if this is a full node + std::string address_; ///< Address of the account this manager is responsible for + SignFunc sign_; ///< Signer method for authorizing UTXO spends + VerifySignatureFunc verify_signature_; ///< Verifier method for validating signatures on UTXO spends + std::shared_ptr db_; ///< Database handle for persisting UTXO state and checkpoints + + mutable std::shared_mutex utxos_mutex_; ///< Mutex for UTXO state structures + UTXOOutPointMap utxo_outpoints_; ///< Maps outpoints to their UTXO entries for efficient lookup + AddressOutPointList address_outpoints_; ///< Maps owner addresses to their outpoints for efficient lookup + /// Maps outpoints to their reservation IDs + std::unordered_map reserved_outpoints_; }; } diff --git a/src/account/UTXOMerkle.hpp b/src/account/UTXOMerkle.hpp new file mode 100644 index 000000000..5d8c81b1a --- /dev/null +++ b/src/account/UTXOMerkle.hpp @@ -0,0 +1,228 @@ +/** + * @file UTXOMerkle.hpp + * @brief Helpers for deterministic serialization and Merkle hashing of UTXO snapshots. + * @date 2026-03-18 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#pragma once + +#include "account/GeniusUTXO.hpp" +#include "crypto/sha/sha256.hpp" + +#include +#include +#include + +/** + * @brief Utilities for building deterministic Merkle roots over ordered UTXO payloads. + */ +namespace sgns::utxo_merkle +{ + /// Domain separator prefix used for hashed leaf payloads. + constexpr uint8_t kLeafPrefix = 0x00; + /// Domain separator prefix used for hashed internal nodes. + constexpr uint8_t kNodePrefix = 0x01; + + /** + * @brief Appends a 32-bit unsigned integer in big-endian order. + * @param[out] out The vector to append to + * @param[in] value the value to append + */ + inline void AppendUInt32BE( std::vector &out, uint32_t value ) + { + out.push_back( static_cast( ( value >> 24 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 16 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 8 ) & 0xFF ) ); + out.push_back( static_cast( value & 0xFF ) ); + } + + /** + * @brief Appends a 64-bit unsigned integer in big-endian order. + * @param[out] out The vector to append to + * @param[in] value the value to append + */ + inline void AppendUInt64BE( std::vector &out, uint64_t value ) + { + out.push_back( static_cast( ( value >> 56 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 48 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 40 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 32 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 24 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 16 ) & 0xFF ) ); + out.push_back( static_cast( ( value >> 8 ) & 0xFF ) ); + out.push_back( static_cast( value & 0xFF ) ); + } + + /** + * @brief Reads a 32-bit unsigned integer from big-endian bytes. + * @param[in] data A pointer to the byte array + * @return the 32 bit unsigned integer represented by the bytes + */ + inline uint32_t ReadUInt32BE( const uint8_t *data ) + { + return ( static_cast( data[0] ) << 24 ) | ( static_cast( data[1] ) << 16 ) | + ( static_cast( data[2] ) << 8 ) | static_cast( data[3] ); + } + + /** + * @brief Reads a 64-bit unsigned integer from big-endian bytes. + * @param[in] data A pointer to the byte array + * @return the 64 bit unsigned integer represented by the bytes + */ + inline uint64_t ReadUInt64BE( const uint8_t *data ) + { + return ( static_cast( data[0] ) << 56 ) | ( static_cast( data[1] ) << 48 ) | + ( static_cast( data[2] ) << 40 ) | ( static_cast( data[3] ) << 32 ) | + ( static_cast( data[4] ) << 24 ) | ( static_cast( data[5] ) << 16 ) | + ( static_cast( data[6] ) << 8 ) | static_cast( data[7] ); + } + + /** + * @brief Generates a canonical key for a UTXO outpoint, used for deterministic ordering in Merkle tree construction. + * @param[in] txid The transaction hash that created the UTXO + * @param[in] idx The output index of the UTXO within the transaction + * @return Canonical string key in the format "txid:idx" where txid is the readable hex representation of the transaction hash + */ + inline std::string OutPointKey( const base::Hash256 &txid, uint32_t idx ) + { + return txid.toReadableString() + ":" + std::to_string( idx ); + } + + /** + * @brief Serializes a UTXO into the canonical leaf payload used for Merkle hashing. + * @param[in] utxo The UTXO to serialize + * @return The serialized leaf payload + */ + inline std::vector SerializeUTXOLeafPayload( const GeniusUTXO &utxo ) + { + std::vector payload; + const auto &owner_address = utxo.GetOwnerAddress(); + const auto txid = utxo.GetTxID(); + const auto token_id = utxo.GetTokenID(); + const auto &token_bytes = token_id.bytes(); + payload.reserve( 32 + 4 + 4 + owner_address.size() + token_bytes.size() + 8 ); + + payload.insert( payload.end(), txid.begin(), txid.end() ); + AppendUInt32BE( payload, utxo.GetOutputIdx() ); + AppendUInt32BE( payload, static_cast( owner_address.size() ) ); + payload.insert( payload.end(), owner_address.begin(), owner_address.end() ); + payload.insert( payload.end(), token_bytes.begin(), token_bytes.end() ); + AppendUInt64BE( payload, utxo.GetAmount() ); + return payload; + } + + /** + * @brief Hashes a serialized UTXO leaf payload with the leaf domain separator. + * @param[in] payload The payload to hash + * @return The hash of the payload as a leaf node in the Merkle tree + */ + inline base::Hash256 HashLeaf( const std::vector &payload ) + { + std::vector bytes; + bytes.reserve( payload.size() + 1 ); + bytes.push_back( kLeafPrefix ); + bytes.insert( bytes.end(), payload.begin(), payload.end() ); + return crypto::sha256( gsl::span( bytes.data(), bytes.size() ) ); + } + + /** + * @brief Hashes two child nodes with the internal-node domain separator. + * @param[in] left The hash of the left child node + * @param[in] right The hash of the right child node + * @return The hash of the parent node + */ + inline base::Hash256 HashNode( const base::Hash256 &left, const base::Hash256 &right ) + { + std::vector bytes; + bytes.reserve( 1 + left.size() + right.size() ); + bytes.push_back( kNodePrefix ); + bytes.insert( bytes.end(), left.begin(), left.end() ); + bytes.insert( bytes.end(), right.begin(), right.end() ); + return crypto::sha256( gsl::span( bytes.data(), bytes.size() ) ); + } + + /** + * @brief Returns the canonical root used for an empty UTXO set. + * @return The canonical root for an empty UTXO set + */ + inline base::Hash256 EmptyUTXOMerkleRoot() + { + static const base::Hash256 empty_root = crypto::sha256( std::string_view( "UTXO_EMPTY_V1" ) ); + return empty_root; + } + + /** + * @brief Reduces a list of leaf hashes into a single Merkle root. + * @param[in] level_hashes The list of leaf hashes + * @return The computed Merkle root + */ + inline base::Hash256 ComputeMerkleRootFromLeafHashes( std::vector level_hashes ) + { + if ( level_hashes.empty() ) + { + return EmptyUTXOMerkleRoot(); + } + + while ( level_hashes.size() > 1 ) + { + if ( ( level_hashes.size() % 2 ) != 0 ) + { + level_hashes.push_back( level_hashes.back() ); + } + + std::vector next_level; + next_level.reserve( level_hashes.size() / 2 ); + for ( size_t i = 0; i < level_hashes.size(); i += 2 ) + { + next_level.push_back( HashNode( level_hashes[i], level_hashes[i + 1] ) ); + } + level_hashes = std::move( next_level ); + } + + return level_hashes.front(); + } + + /** + * @brief Sorts canonical payloads, hashes them as leaves, and computes the Merkle root. + * @param[in] payloads The list of serialized UTXO payloads to include in the Merkle tree + * @return The computed Merkle root of the payloads + */ + inline base::Hash256 ComputeMerkleRootFromPayloads( std::vector> payloads ) + { + if ( payloads.empty() ) + { + return EmptyUTXOMerkleRoot(); + } + + std::sort( payloads.begin(), payloads.end() ); + + std::vector leaf_hashes; + leaf_hashes.reserve( payloads.size() ); + for ( const auto &payload : payloads ) + { + leaf_hashes.push_back( HashLeaf( payload ) ); + } + + return ComputeMerkleRootFromLeafHashes( std::move( leaf_hashes ) ); + } + + /** + * @brief Computes the Merkle root for a given set of UTXOs by serializing them into canonical payloads and hashing them. + * @param[in] utxos The list of UTXOs to include in the Merkle tree + * @return The computed Merkle root of the UTXOs + */ + inline base::Hash256 ComputeMerkleRootFromUTXOs( const std::vector &utxos ) + { + if ( utxos.empty() ) + { + return EmptyUTXOMerkleRoot(); + } + std::vector> payloads; + payloads.reserve( utxos.size() ); + for ( const auto &utxo : utxos ) + { + payloads.push_back( SerializeUTXOLeafPayload( utxo ) ); + } + return ComputeMerkleRootFromPayloads( std::move( payloads ) ); + } +} diff --git a/src/account/UTXOStructs.cpp b/src/account/UTXOStructs.cpp index f57d8f206..73ce94ee2 100644 --- a/src/account/UTXOStructs.cpp +++ b/src/account/UTXOStructs.cpp @@ -3,6 +3,8 @@ #include "account/proto/SGTransaction.pb.h" #include "base/endian.h" +#include +#include std::vector sgns::InputUTXOInfo::SerializeForSigning() const { @@ -15,3 +17,24 @@ std::vector sgns::InputUTXOInfo::SerializeForSigning() const return vec; } + +bool sgns::utxo_address::IsEscrowLockAddress( std::string_view address ) +{ + if ( address.size() != 66 || address.substr( 0, 2 ) != "0x" ) + { + return false; + } + + return std::all_of( + address.begin() + 2, address.end(), []( unsigned char c ) { return std::isxdigit( c ) != 0; } ); +} + +bool sgns::utxo_address::IsAccountPublicKeyAddress( std::string_view address ) +{ + if ( address.size() != 128 ) + { + return false; + } + + return std::all_of( address.begin(), address.end(), []( unsigned char c ) { return std::isxdigit( c ) != 0; } ); +} diff --git a/src/account/UTXOStructs.hpp b/src/account/UTXOStructs.hpp index 153fb6218..5c9f9fe87 100644 --- a/src/account/UTXOStructs.hpp +++ b/src/account/UTXOStructs.hpp @@ -1,3 +1,9 @@ +/** + * @file UTXOStructs.hpp + * @brief Shared UTXO transaction input and output data structures. + * @date 2026-01-20 + * @author Henrique A. Klein (hklein@gnus.ai) + */ #pragma once #include "base/blob.hpp" @@ -5,20 +11,40 @@ namespace sgns { + namespace utxo_address + { + /** + * @brief Checks if the address is a valid escrow lock address (0x-prefixed 64 hex chars) + * @param[in] address The address to check + * @return true if the address is a valid escrow lock address, false otherwise + */ + bool IsEscrowLockAddress( std::string_view address ); + + /** + * @brief Checks if the address is a public key + * @param[in] address The address to check + * @return true if the address is a public key address, false otherwise + */ + bool IsAccountPublicKeyAddress( std::string_view address ); + } // namespace utxo_address + /** - * @brief Raw UTXO input data for a transaction + * @brief Raw UTXO input data included in a spend request. */ struct InputUTXOInfo { + /** + * @brief Serializes the input fields that must be signed by the owner. + */ std::vector SerializeForSigning() const; - base::Hash256 txid_hash_; //< Hash of the related transaction - uint32_t output_idx_; //< Index of the related output in the output vector - std::vector signature_; //< Signature of the hash and index + base::Hash256 txid_hash_; ///< Hash of the transaction that created the output. + uint32_t output_idx_; ///< Zero-based output index within the originating transaction. + std::vector signature_; ///< Signature authorizing the spend of this outpoint. }; /** - * @brief Single output entry + * @brief Single UTXO output destination entry. */ struct OutputDestInfo { @@ -27,5 +53,8 @@ namespace sgns TokenID token_id; ///< Token identifier }; + /** + * @brief Pair of signed inputs and destination outputs that make up a UTXO transaction payload. + */ using UTXOTxParameters = std::pair, std::vector>; } diff --git a/src/account/proto/SGTransaction.proto b/src/account/proto/SGTransaction.proto index bb4a5ff32..7b45bf70d 100644 --- a/src/account/proto/SGTransaction.proto +++ b/src/account/proto/SGTransaction.proto @@ -43,6 +43,36 @@ message UTXO bytes hash = 3; bytes token = 4; } + +enum UTXOEntryState +{ + UTXO_ENTRY_READY = 0; + UTXO_ENTRY_CONSUMED = 1; +} + +message UTXOEntryRecord +{ + UTXO utxo = 1; + string owner_address = 2; + UTXOEntryState state = 3; + uint64 created_epoch = 4; + bool has_spent_epoch = 5; + uint64 spent_epoch = 6; + bool has_spent_by_txid = 7; + bytes spent_by_txid = 8; +} + +message UTXOCheckpointRecord +{ + string owner_address = 1; + uint64 epoch = 2; + bytes last_finalized_tx = 3; + bytes registry_hash = 4; + bytes utxo_merkle_root = 5; + uint64 utxo_count = 6; + uint64 created_at_ms = 7; +} + message UTXOList { repeated UTXO utxos = 1; @@ -78,6 +108,14 @@ message MintTxV2 uint64 amount = 4; // UTXOTxParams utxo_params = 5; } +message MigrationTx +{ + DAGStruct dag_struct = 1;// + bytes token_id = 2; + uint64 amount = 3; // + UTXOTxParams utxo_params = 4; + string from_version = 5; +} message EscrowTx { DAGStruct dag_struct = 1;// diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index b5f081cf5..fefab9c2c 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -20,18 +20,17 @@ #include "crdt/proto/delta.pb.h" #include "account/GeniusAccount.hpp" #include "blockchain/impl/proto/SGBlockchain.pb.h" +#include "blockchain/Consensus.hpp" #include "base/buffer.hpp" #include "crdt/crdt_callback_manager.hpp" #include "base/sgns_version.hpp" namespace sgns { - namespace blockchain - { - class ValidatorRegistry; - } + class ValidatorRegistry; class Migration3_5_0To3_6_0; + class Migration3_6_0To3_7_0; class Blockchain : public std::enable_shared_from_this { @@ -65,9 +64,10 @@ namespace sgns * @param callback Called when initialization completes. * @return shared_ptr to Blockchain instance. */ - static std::shared_ptr New( std::shared_ptr global_db, - std::shared_ptr account, - BlockchainCallback callback ); + static std::shared_ptr New( std::shared_ptr global_db, + std::shared_ptr account, + std::shared_ptr pubsub, + BlockchainCallback callback ); ~Blockchain(); @@ -101,13 +101,40 @@ namespace sgns */ static const std::string &GetAuthorizedFullNodeAddress(); - outcome::result GetGenesisCID() const; - outcome::result GetAccountCreationCID() const; + outcome::result GetGenesisCID() const; + outcome::result GetAccountCreationCID() const; + std::shared_ptr GetValidatorRegistry() const; void SetFullNodeMode(); + bool RegisterSubjectHandler( SubjectType type, ConsensusManager::SubjectHandler handler ); + void UnregisterSubjectHandler( SubjectType type ); + bool RegisterCertificateHandler( SubjectType type, ConsensusManager::CertificateSubjectHandler handler ); + void UnregisterCertificateHandler( SubjectType type ); + + outcome::result CreateConsensusNonceSubject( const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash, + const std::optional &utxo_commitment, + const std::optional &utxo_witness ); + + outcome::result CreateConsensusProposal( const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash, + const std::optional &utxo_commitment, + const std::optional &utxo_witness ); + + outcome::result SubmitProposal( const ConsensusManager::Proposal &proposal ); + + outcome::result TryResumeProposal( const std::string &hash ); + bool CheckCertificate( const std::string &subject_hash ) const; + bool CheckCertificateStrict( const ConsensusManager::Subject &subject ) const; + outcome::result GetCertificateBySubjectHash( const std::string &subject_hash ) const; + const std::string &BestHash( const std::string &a, const std::string &b ) const; + protected: friend class Migration3_5_0To3_6_0; + friend class Migration3_6_0To3_7_0; static outcome::result MigrateCids( const std::shared_ptr &old_db, const std::shared_ptr &new_db ); @@ -123,10 +150,10 @@ namespace sgns outcome::result SaveGenesisCID( const std::string &cid ); outcome::result SaveAccountCreationCID( const std::string &address, const std::string &cid ); - std::vector ComputeSignatureData( const sgns::blockchain::GenesisBlock &g ) const; - std::vector ComputeSignatureData( const sgns::blockchain::AccountCreationBlock &ac ) const; - bool VerifySignature( const sgns::blockchain::GenesisBlock &g ) const; - bool VerifySignature( const sgns::blockchain::AccountCreationBlock &ac ) const; + std::vector ComputeSignatureData( const GenesisBlock &g ) const; + std::vector ComputeSignatureData( const AccountCreationBlock &ac ) const; + bool VerifySignature( const GenesisBlock &g ) const; + bool VerifySignature( const AccountCreationBlock &ac ) const; outcome::result CreateGenesisBlock(); outcome::result VerifyGenesisBlock( const std::string &serialized_genesis ); @@ -136,11 +163,9 @@ namespace sgns std::optional> FilterGenesis( const crdt::pb::Element &element ); std::optional> FilterAccountCreation( const crdt::pb::Element &element ); - - static bool ShouldReplaceGenesis( const blockchain::GenesisBlock &existing, - const blockchain::GenesisBlock &candidate ); - static bool ShouldReplaceAccountCreation( const blockchain::AccountCreationBlock &existing, - const blockchain::AccountCreationBlock &candidate ); + bool ShouldReplaceGenesis( const GenesisBlock &existing, const GenesisBlock &candidate ) const; + bool ShouldReplaceAccountCreation( const AccountCreationBlock &existing, + const AccountCreationBlock &candidate ) const; outcome::result GenesisReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair &new_data, const std::string &cid ); @@ -167,9 +192,9 @@ namespace sgns std::shared_ptr db_; ///< CRDT database instance std::shared_ptr account_; ///< GeniusAccount instance - BlockchainCallback blockchain_processed_callback_; ///< Callback when the processing of the blockchain is done - sgns::blockchain::GenesisBlock genesis_block_; ///< Cached genesis block for easy access - sgns::blockchain::AccountCreationBlock account_creation_block_; ///< Cached account creation block + BlockchainCallback blockchain_processed_callback_; ///< Callback when the processing of the blockchain is done + GenesisBlock genesis_block_; ///< Cached genesis block for easy access + AccountCreationBlock account_creation_block_; ///< Cached account creation block struct BlockchainCIDs { @@ -201,7 +226,7 @@ namespace sgns static std::string &AuthorizedFullNodeAddressStorage(); - std::shared_ptr validator_registry_; + std::shared_ptr validator_registry_; base::Logger logger_ = base::createLogger( "Blockchain" ); ///< Logger instance @@ -211,6 +236,8 @@ namespace sgns std::atomic validator_registry_initialized_{ false }; bool genesis_ready_ = false; bool account_creation_ready_ = false; + + std::shared_ptr consensus_manager_; }; } diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp new file mode 100644 index 000000000..0c32811e3 --- /dev/null +++ b/src/blockchain/Consensus.cpp @@ -0,0 +1,2598 @@ +/** + * @file Consensus.cpp + * @brief Consensus proposal/vote/certificate helpers. + * @date 2025-10-16 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#include "blockchain/Consensus.hpp" + +#include +#include +#include +#include +#include + +#include + +#include "base/hexutil.hpp" +#include "base/sgns_version.hpp" +#include "crypto/hasher/hasher_impl.hpp" +#include "account/GeniusAccount.hpp" +#include "blockchain/ConsensusAuth.hpp" + +namespace sgns +{ + + base::Logger ConsensusManagerLogger() + { + // Always call base::createLogger to get the current logger + // This will return existing logger or create new one as needed + return base::createLogger( "ConsensusManager" ); + } + + std::shared_ptr ConsensusManager::New( std::shared_ptr registry, + std::shared_ptr db, + std::shared_ptr pubsub, + Signer signer, + std::string address, + std::string consensus_topic ) + { + if ( !registry ) + { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: registry is null", __func__ ); + return nullptr; + } + if ( !db ) + { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: db is null", __func__ ); + return nullptr; + } + if ( !pubsub ) + { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: pubsub is null", __func__ ); + return nullptr; + } + if ( !signer ) + { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: signer is null", __func__ ); + return nullptr; + } + if ( address.empty() ) + { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: address is empty", __func__ ); + return nullptr; + } + + auto instance = std::shared_ptr( new ConsensusManager( std::move( registry ), + std::move( db ), + std::move( pubsub ), + std::move( signer ), + address, + consensus_topic ) ); + instance->certificate_work_journal_ = instance->db_->GetWorkJournal(); + + if ( !instance->certificate_work_journal_ ) + { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: crdt work journal is empty", + __func__ ); + return nullptr; + } + + instance->consensus_subs_future_ = std::move( instance->pubsub_->Subscribe( + instance->consensus_messages_topic_, + [weakptr( std::weak_ptr( instance ) )]( + boost::optional message ) + { + if ( auto self = weakptr.lock() ) + { + ConsensusManagerLogger()->trace( "{}: Received Consensus Message on topic {}", + __func__, + self->consensus_messages_topic_ ); + self->OnConsensusMessage( message ); + } + } ) ); + ConsensusManagerLogger()->debug( "{}: Subscribed to Consensus topic {}", + __func__, + instance->consensus_messages_topic_ ); + instance->StartRoundTimer(); + if ( !instance->RegisterCertificateFilter() ) + { + ConsensusManagerLogger()->error( "{}: Failed to register certificate filter", __func__ ); + } + instance->RecoverPendingCertificateWork(); + + return instance; + } + + ConsensusManager::ConsensusManager( std::shared_ptr registry, + std::shared_ptr db, + std::shared_ptr pubsub, + Signer signer, + std::string address, + std::string consensus_topic ) : + registry_( std::move( registry ) ), // + db_( std::move( db ) ), // + pubsub_( std::move( pubsub ) ), // + signer_( std::move( signer ) ), // + account_address_( address ), // + consensus_messages_topic_( std::string( CONSENSUS_CHANNEL_PREFIX ) + sgns::version::GetNetAndVersionAppendix() + + consensus_topic ), + consensus_datastore_topic_( consensus_messages_topic_ + "#datastore" ) + { + } + + ConsensusManager::~ConsensusManager() + { + stop_timer_.store( true ); + timer_cv_.notify_all(); + } + + void ConsensusManager::Close() + { + stop_timer_.store( true ); + timer_cv_.notify_all(); + if ( round_timer_.joinable() ) + { + round_timer_.join(); + } + } + + void ConsensusManager::StartRoundTimer() + { + if ( round_timer_.joinable() ) + { + return; + } + if ( stop_timer_.load() ) + { + return; + } + + std::weak_ptr weak_self = shared_from_this(); + round_timer_ = std::thread( + [weak_self]() + { + constexpr auto min_interval = std::chrono::milliseconds( 500 ); + while ( true ) + { + auto self = weak_self.lock(); + if ( !self ) + { + return; + } + + std::unique_lock lock( self->timer_mutex_ ); + auto interval = self->round_duration_ / 2; + if ( interval.count() <= 0 ) + { + interval = DEFAULT_ROUND_DURATION / 2; + } + if ( interval < min_interval ) + { + interval = min_interval; + } + if ( self->certificates_pending_.load() ) + { + // Work is pending: run on cadence, only interrupt for shutdown. + self->timer_cv_.wait_for( lock, interval, [self]() { return self->stop_timer_.load(); } ); + } + else + { + // No pending work: wait up to interval, but wake immediately when new work appears. + self->timer_cv_.wait_for( + lock, + interval, + [self]() { return self->stop_timer_.load() || self->certificates_pending_.load(); } ); + } + if ( self->stop_timer_.load() ) + { + return; + } + lock.unlock(); + if ( self->certificates_pending_.load() ) + { + self->ProcessCertificates(); + self->UpdateCertificatesPending(); + } + // Keep replaying unfinished certificate work while the node is running. + self->RecoverPendingCertificateWork(); + } + } ); + } + + outcome::result ConsensusManager::Publish( const ConsensusMessage &message ) + { + std::vector serialized_proto( message.ByteSizeLong() ); + if ( !message.SerializeToArray( serialized_proto.data(), serialized_proto.size() ) ) + { + ConsensusManagerLogger()->error( "{}: Failed to serialize consensus message", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + ConsensusManagerLogger()->debug( "{}: Sending consensus packet to {}", __func__, consensus_messages_topic_ ); + pubsub_->Publish( consensus_messages_topic_, serialized_proto ); + ConsensusManagerLogger()->debug( "{}: Consensus packet published (bytes={})", + __func__, + serialized_proto.size() ); + + return outcome::success(); + } + + bool ConsensusManager::RegisterSubjectHandler( SubjectType type, SubjectHandler handler ) + { + if ( !handler ) + { + ConsensusManagerLogger()->error( "{}: ignored empty handler type={}", __func__, static_cast( type ) ); + return false; + } + ConsensusManagerLogger()->debug( "{}: Registering subject handler type={}", + __func__, + static_cast( type ) ); + std::unique_lock lock( subject_handlers_mutex_ ); + subject_handlers_[static_cast( type )] = std::move( handler ); + return true; + } + + void ConsensusManager::UnregisterSubjectHandler( SubjectType type ) + { + ConsensusManagerLogger()->debug( "{}: Removing Subject handler with type={}", + __func__, + static_cast( type ) ); + std::unique_lock lock( subject_handlers_mutex_ ); + subject_handlers_.erase( static_cast( type ) ); + } + + bool ConsensusManager::RegisterCertificateHandler( SubjectType type, CertificateSubjectHandler handler ) + { + if ( !handler ) + { + ConsensusManagerLogger()->error( "{}: ignored empty certificate handler type={}", + __func__, + static_cast( type ) ); + return false; + } + ConsensusManagerLogger()->debug( "{}: Registering certificate handler type={}", + __func__, + static_cast( type ) ); + std::unique_lock lock( certificate_handlers_mutex_ ); + certificate_subject_handlers_[static_cast( type )] = std::move( handler ); + return true; + } + + void ConsensusManager::UnregisterCertificateHandler( SubjectType type ) + { + ConsensusManagerLogger()->debug( "{}: Removing Certificate handler with type={}", + __func__, + static_cast( type ) ); + std::unique_lock lock( certificate_handlers_mutex_ ); + certificate_subject_handlers_.erase( static_cast( type ) ); + } + + void ConsensusManager::ConfigureTimestampWindow( std::chrono::milliseconds window ) + { + if ( window.count() <= 0 ) + { + ConsensusManagerLogger()->warn( "{}: using default window", __func__ ); + timestamp_window_ = DEFAULT_TIMESTAMP_WINDOW; + return; + } + timestamp_window_ = window; + } + + void ConsensusManager::ConfigureRoundDuration( std::chrono::milliseconds duration ) + { + if ( duration.count() <= 0 ) + { + ConsensusManagerLogger()->warn( "{}: using default round duration", __func__ ); + round_duration_ = DEFAULT_ROUND_DURATION; + return; + } + round_duration_ = duration; + } + + void ConsensusManager::ConfigureRoundSkew( std::chrono::milliseconds skew ) + { + if ( skew.count() < 0 ) + { + ConsensusManagerLogger()->warn( "{}: using default round skew", __func__ ); + round_skew_ = DEFAULT_ROUND_SKEW; + return; + } + round_skew_ = skew; + } + + void ConsensusManager::ConfigureCertificateDelay( std::chrono::milliseconds delay ) + { + if ( delay.count() < 0 ) + { + ConsensusManagerLogger()->warn( "{}: using zero delay", __func__ ); + certificate_delay_ = std::chrono::milliseconds( 0 ); + return; + } + certificate_delay_ = delay; + } + + bool ConsensusManager::IsTimestampSane( uint64_t timestamp_ms ) const + { + if ( timestamp_ms == 0 ) + { + return false; + } + const auto now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count(); + const auto window_ms = timestamp_window_.count(); + const auto ts_ms = static_cast( timestamp_ms ); + return ( ts_ms >= now_ms - window_ms ) && ( ts_ms <= now_ms + window_ms ); + } + + uint64_t ConsensusManager::GetCurrentRound( uint64_t proposal_ts_ms ) const + { + if ( proposal_ts_ms == 0 || round_duration_.count() <= 0 ) + { + return 0; + } + const auto now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count(); + const auto elapsed = static_cast( now_ms ) - static_cast( proposal_ts_ms ); + if ( elapsed <= 0 ) + { + return 0; + } + const auto skew_ms = static_cast( round_skew_.count() ); + if ( elapsed <= skew_ms ) + { + return 0; + } + const auto round_ms = static_cast( round_duration_.count() ); + auto round = static_cast( ( elapsed - skew_ms ) / round_ms ); + ConsensusManagerLogger()->debug( "{}: Returning round={}", __func__, round ); + return round; + } + + std::vector ConsensusManager::GetOrderedActiveValidators( + const ValidatorRegistry::Registry ®istry ) const + { + std::vector validators; + validators.reserve( registry.validators_size() ); + for ( const auto &entry : registry.validators() ) + { + if ( entry.status() == ValidatorRegistry::Status::ACTIVE ) + { + validators.push_back( entry.validator_id() ); + } + } + std::sort( validators.begin(), validators.end() ); + ConsensusManagerLogger()->trace( "{}: Returning validators with size ={}", __func__, validators.size() ); + return validators; + } + + bool ConsensusManager::IsCurrentAggregator( const Proposal &proposal, + const ValidatorRegistry::Registry ®istry ) const + { + ConsensusManagerLogger()->trace( "{}: Checking if is current aggregator for proposal", __func__ ); + auto ordered = GetOrderedActiveValidators( registry ); + if ( ordered.empty() ) + { + return false; + } + + sgns::crypto::HasherImpl hasher; + auto hash = hasher.sha2_256( + gsl::span( reinterpret_cast( proposal.proposal_id().data() ), + proposal.proposal_id().size() ) ); + uint64_t base_index = 0; + for ( size_t i = 0; i < sizeof( uint64_t ) && i < hash.size(); ++i ) + { + base_index = ( base_index << 8 ) | hash[i]; + } + base_index = base_index % ordered.size(); + + const auto round = GetCurrentRound( proposal.timestamp() ); + const auto index = ( base_index + round ) % ordered.size(); + + return ordered[index] == account_address_; + } + + outcome::result ConsensusManager::GetSubjectHash( const Subject &subject ) + { + if ( subject.type() == SubjectType::SUBJECT_NONCE ) + { + if ( !subject.has_nonce() || subject.nonce().tx_hash().empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return subject.nonce().tx_hash(); + } + if ( subject.type() == SubjectType::SUBJECT_TASK_RESULT ) + { + if ( !subject.has_task_result() || subject.task_result().task_result_hash().empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return subject.task_result().task_result_hash(); + } + if ( subject.type() == SubjectType::SUBJECT_REGISTRY_BATCH ) + { + if ( !subject.has_registry_batch() || subject.registry_batch().batch_root().empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return std::string( subject.registry_batch().batch_root() ); + } + return outcome::failure( std::errc::invalid_argument ); + } + + void ConsensusManager::ContinueProposalAfterSubject( const Proposal &proposal ) + { + ConsensusManagerLogger()->debug( "{}: Continuing proposal: hash {}, id {}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + const auto slot_key = GetSlotKey( proposal ); + bool should_vote = false; + + ConsensusManagerLogger()->debug( "{}: Slot key acquired: hash {}, id {}, slot key {}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); + { + std::lock_guard lock( proposals_mutex_ ); + if ( proposals_.find( proposal.proposal_id() ) == proposals_.end() ) + { + ConsensusManagerLogger()->debug( + "{}: No proposal state found. Creating... : hash {}, id {}, slot key {}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); + ProposalState state; + state.proposal = proposal; + state.slot_key = slot_key; + proposals_.emplace( proposal.proposal_id(), std::move( state ) ); + } + + auto &slot_state = slot_states_[slot_key]; + if ( slot_state.best_proposal_id.empty() ) + { + ConsensusManagerLogger()->debug( "{}: Configuring best proposal for hash {}, id={}, slot key {}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); + slot_state.best_proposal_id = proposal.proposal_id(); + if ( proposal.subject().has_nonce() ) + { + slot_state.best_tx_hash = proposal.subject().nonce().tx_hash(); + } + } + else + { + const auto ¤t = proposals_.at( slot_state.best_proposal_id ).proposal; + ConsensusManagerLogger()->debug( + "{}: Already have a best proposal for hash {}, id={}, slot key {}. Seeing if {} is better ", + __func__, + GetPrintableSubjectHash( current.subject() ), + current.proposal_id().substr( 0, 8 ), + slot_key, + proposal.proposal_id().substr( 0, 8 ) ); + if ( IsBetterProposal( proposal, current ) ) + { + ConsensusManagerLogger()->debug( "{}: Better proposal for hash {}, id={}, slot key {}. ", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); + slot_state.best_proposal_id = proposal.proposal_id(); + if ( proposal.subject().has_nonce() ) + { + slot_state.best_tx_hash = proposal.subject().nonce().tx_hash(); + } + } + } + + if ( slot_state.best_proposal_id == proposal.proposal_id() && !slot_state.voted ) + { + ConsensusManagerLogger()->debug( + "{}: My proposal for hash {}, id={}, slot key {} is better so let's vote on it. ", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); + slot_state.voted = true; + should_vote = true; + } + } + + auto pending_votes = TakePendingVotes( proposal.proposal_id() ); + for ( const auto &vote : pending_votes ) + { + HandleVote( vote ); + } + + if ( should_vote ) + { + auto vote_result = CreateVote( proposal.proposal_id(), account_address_, true, signer_ ); + if ( vote_result.has_value() ) + { + (void)SubmitVote( vote_result.value() ); + ConsensusManagerLogger()->debug( "{}: self-vote submitted for hash {}, id={}, slot key {}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); + } + else + { + ConsensusManagerLogger()->error( "{}: self-vote failed for hash {}, id={}, slot key {} error={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key, + vote_result.error().message() ); + } + } + } + + void ConsensusManager::AddPendingProposal( const Proposal &proposal, const std::string &subject_hash ) + { + std::lock_guard lock( proposals_mutex_ ); + if ( pending_proposals_.find( proposal.proposal_id() ) != pending_proposals_.end() ) + { + ConsensusManagerLogger()->error( + "{}: Failed adding pending proposal for {}: already have a proposal with id {}", + __func__, + subject_hash.substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + ConsensusManagerLogger()->debug( "{}: Adding pending proposal for {}: proposal with id {}", + __func__, + subject_hash.substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); + pending_proposals_.emplace( proposal.proposal_id(), proposal ); + pending_by_subject_hash_[subject_hash].push_back( proposal.proposal_id() ); + } + + std::vector ConsensusManager::TakePendingProposals( const std::string &subject_hash ) + { + std::vector result; + std::lock_guard lock( proposals_mutex_ ); + auto it = pending_by_subject_hash_.find( subject_hash ); + if ( it == pending_by_subject_hash_.end() ) + { + ConsensusManagerLogger()->trace( "{}: No pending proposals for {}", __func__, subject_hash.substr( 0, 8 ) ); + return result; + } + for ( const auto &proposal_id : it->second ) + { + auto prop_it = pending_proposals_.find( proposal_id ); + if ( prop_it != pending_proposals_.end() ) + { + result.push_back( prop_it->second ); + pending_proposals_.erase( prop_it ); + } + } + ConsensusManagerLogger()->debug( "{}: Taking pending proposals for {}", __func__, subject_hash.substr( 0, 8 ) ); + pending_by_subject_hash_.erase( it ); + return result; + } + + void ConsensusManager::AddPendingVote( const Vote &vote ) + { + std::lock_guard lock( proposals_mutex_ ); + pending_votes_[vote.proposal_id()].push_back( vote ); + } + + std::vector ConsensusManager::TakePendingVotes( const std::string &proposal_id ) + { + std::vector result; + std::lock_guard lock( proposals_mutex_ ); + auto it = pending_votes_.find( proposal_id ); + if ( it == pending_votes_.end() ) + { + return result; + } + result = std::move( it->second ); + pending_votes_.erase( it ); + return result; + } + + outcome::result ConsensusManager::CreateProposal( const Subject &subject, + const std::string &proposer_id, + const std::string ®istry_cid, + uint64_t registry_epoch ) + { + return CreateProposal( subject, proposer_id, registry_cid, registry_epoch, signer_ ); + } + + outcome::result ConsensusManager::CreateProposal( const Subject &subject, + const std::string &proposer_id, + const std::string ®istry_cid, + uint64_t registry_epoch, + Signer sign ) + { + ConsensusManagerLogger()->trace( "{}: called by {} with hash {}, registry CID {} and epoch {}", + __func__, + proposer_id.substr( 0, 8 ), + GetPrintableSubjectHash( subject ), + registry_cid, + registry_epoch ); + if ( !sign ) + { + ConsensusManagerLogger()->error( "{}: failed for hash {}: signer is empty", + __func__, + GetPrintableSubjectHash( subject ) ); + return outcome::failure( std::errc::invalid_argument ); + } + + if ( !ValidateSubject( subject ) ) + { + ConsensusManagerLogger()->error( "{}: failed for hash {}: subject validation failed", + __func__, + GetPrintableSubjectHash( subject ) ); + return outcome::failure( std::errc::invalid_argument ); + } + + Proposal proposal; + *proposal.mutable_subject() = subject; + proposal.set_proposer_id( proposer_id ); + proposal.set_registry_cid( registry_cid ); + proposal.set_registry_epoch( registry_epoch ); + proposal.set_timestamp( + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count() ); + + if ( proposal.subject().subject_id().empty() ) + { + auto subject_id_result = ComputeSubjectId( proposal.subject() ); + if ( subject_id_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed for hash {}: subject id computation error={}", + __func__, + GetPrintableSubjectHash( subject ), + subject_id_result.error().message() ); + return outcome::failure( subject_id_result.error() ); + } + proposal.mutable_subject()->set_subject_id( subject_id_result.value() ); + } + + proposal.set_proposal_id( CreateProposalId( proposal ) ); + auto signing_bytes = ProposalSigningBytes( proposal ); + if ( signing_bytes.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: signing bytes error={}", + __func__, + signing_bytes.error().message() ); + return outcome::failure( signing_bytes.error() ); + } + ConsensusManagerLogger()->debug( "{}: Creating proposal ID {} for hash {}", + __func__, + proposal.proposal_id().substr( 0, 8 ), + GetPrintableSubjectHash( subject ) ); + BOOST_OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); + proposal.set_signature( signature.data(), signature.size() ); + + ConsensusManagerLogger()->debug( "{}: success for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( subject ), + proposal.proposal_id().substr( 0, 8 ) ); + return proposal; + } + + outcome::result ConsensusManager::CreateVote( const std::string &proposal_id, + const std::string &voter_id, + bool approve, + Signer sign ) + { + ConsensusManagerLogger()->trace( "{}: called by {}: proposal_id={} approve={}", + __func__, + voter_id.substr( 0, 8 ), + proposal_id.substr( 0, 8 ), + approve ); + if ( !sign ) + { + ConsensusManagerLogger()->error( "{}: failed: signer is empty", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + Vote vote; + vote.set_proposal_id( proposal_id ); + vote.set_voter_id( voter_id ); + vote.set_approve( approve ); + vote.set_timestamp( + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count() ); + + auto signing_bytes = VoteSigningBytes( vote ); + if ( signing_bytes.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: signing bytes error={}", + __func__, + signing_bytes.error().message() ); + return outcome::failure( signing_bytes.error() ); + } + + BOOST_OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); + vote.set_signature( signature.data(), signature.size() ); + + ConsensusManagerLogger()->debug( "{}: {} voted for proposal_id={}", + __func__, + voter_id.substr( 0, 8 ), + proposal_id.substr( 0, 8 ) ); + return vote; + } + + outcome::result ConsensusManager::CreateVoteBundle( const std::string &proposal_id, + const std::string &aggregator_id, + const std::vector &votes, + Signer sign ) + { + ConsensusManagerLogger()->trace( "{}: called by {}: proposal_id={} votes={}", + __func__, + aggregator_id.substr( 0, 8 ), + proposal_id.substr( 0, 8 ), + votes.size() ); + if ( !sign ) + { + ConsensusManagerLogger()->error( "{}: failed: signer is empty", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + VoteBundle bundle; + bundle.set_proposal_id( proposal_id ); + bundle.set_aggregator_id( aggregator_id ); + bundle.set_timestamp( + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count() ); + for ( const auto &vote : votes ) + { + *bundle.add_votes() = vote; + } + + auto signing_bytes = VoteBundleSigningBytes( bundle ); + if ( signing_bytes.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: signing bytes error={}", + __func__, + signing_bytes.error().message() ); + return outcome::failure( signing_bytes.error() ); + } + + BOOST_OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); + bundle.set_signature( signature.data(), signature.size() ); + + ConsensusManagerLogger()->debug( + "{}: Vote bundle created successfully by {}: proposal_id={} number of votes={}", + __func__, + aggregator_id.substr( 0, 8 ), + proposal_id.substr( 0, 8 ), + votes.size() ); + return bundle; + } + + outcome::result ConsensusManager::CreateCertificate( const Proposal &proposal, + const std::vector &votes ) + { + ConsensusManagerLogger()->trace( + "{}: Creating certificate for hash {}: proposal_id={} number of votes={} registry CID={}, epoch={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + votes.size(), + proposal.registry_cid(), + proposal.registry_epoch() ); + auto tally_result = TallyVotes( proposal, votes ); + if ( tally_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: tally error={}", __func__, tally_result.error().message() ); + return outcome::failure( tally_result.error() ); + } + + const auto &tally = tally_result.value(); + Certificate cert; + cert.set_proposal_id( proposal.proposal_id() ); + cert.set_registry_cid( proposal.registry_cid() ); + cert.set_registry_epoch( proposal.registry_epoch() ); + cert.set_total_weight( tally.total_weight ); + cert.set_approved_weight( tally.approved_weight ); + uint64_t max_vote_ts = 0; + for ( const auto &vote : votes ) + { + if ( vote.timestamp() > max_vote_ts ) + { + max_vote_ts = vote.timestamp(); + } + } + if ( max_vote_ts == 0 ) + { + max_vote_ts = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count(); + } + cert.set_timestamp( max_vote_ts ); + for ( const auto &vote : votes ) + { + *cert.add_votes() = vote; + } + *cert.mutable_proposal() = proposal; + + ConsensusManagerLogger()->debug( "{}: Success creating certificate for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return cert; + } + + outcome::result ConsensusManager::TallyVotes( + const Proposal &proposal, + const std::vector &votes, + const ValidatorRegistry::Registry ®istry, + const std::string ®istry_cid ) const + { + if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) + { + ConsensusManagerLogger()->error( + "{}: failed: registry cid mismatch hash {}, proposal CID ={} registry CID={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.registry_cid(), + registry_cid ); + return outcome::failure( std::errc::invalid_argument ); + } + if ( proposal.registry_epoch() != registry.epoch() ) + { + ConsensusManagerLogger()->error( + "{}: failed: registry epoch mismatch hash {}, proposal Epoch={} registry Epoch={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.registry_epoch(), + registry.epoch() ); + return outcome::failure( std::errc::invalid_argument ); + } + + uint64_t total_weight = ValidatorRegistry::TotalWeight( registry ); + uint64_t approved_weight = 0; + std::set seen; + + for ( const auto &vote : votes ) + { + ConsensusManagerLogger()->trace( "{}: processing vote for hash {}: voter_id={} approve={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + vote.voter_id().substr( 0, 8 ), + vote.approve() ); + if ( vote.proposal_id() != proposal.proposal_id() ) + { + continue; + } + if ( !seen.insert( vote.voter_id() ).second ) + { + continue; + } + + const auto *validator = ValidatorRegistry::FindValidator( registry, vote.voter_id() ); + if ( !validator || validator->status() != ValidatorRegistry::Status::ACTIVE ) + { + ConsensusManagerLogger()->error( "{}: processing vote for hash {}: voter_id={} approve={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + vote.voter_id().substr( 0, 8 ), + vote.approve() ); + continue; + } + + auto signing_bytes = VoteSigningBytes( vote ); + if ( signing_bytes.has_error() ) + { + continue; + } + + if ( !GeniusAccount::VerifySignature( vote.voter_id(), vote.signature(), signing_bytes.value() ) ) + { + continue; + } + + ConsensusManagerLogger()->debug( "{}: Valid voter signature for hash {}: voter_id={} approve={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + vote.voter_id().substr( 0, 8 ), + vote.approve() ); + if ( vote.approve() ) + { + ConsensusManagerLogger()->debug( "{}: Adding weight for hash {}: voter_id={} weight={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + vote.voter_id().substr( 0, 8 ), + validator->weight() ); + approved_weight += validator->weight(); + } + } + + QuorumTally tally; + tally.total_weight = total_weight; + tally.approved_weight = approved_weight; + tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); + ConsensusManagerLogger()->debug( + "{}: Votes tallied for hash {} proposal_id={} approved_weight={} total_weight={} quorum={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + approved_weight, + total_weight, + tally.has_quorum ); + return tally; + } + + outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, + const std::vector &votes ) const + { + ConsensusManagerLogger()->trace( + "{}: Tallying with current registry for hash {}, proposal_id={} number of votes={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + votes.size() ); + + if ( proposal.registry_cid().empty() ) + { + ConsensusManagerLogger()->error( "{}: failed: proposal registry CID is empty", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + auto registry_result = registry_->LoadRegistry( proposal.registry_cid() ); + if ( registry_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: registry load error={} cid={}", + __func__, + registry_result.error().message(), + proposal.registry_cid() ); + return outcome::failure( registry_result.error() ); + } + return TallyVotes( proposal, votes, registry_result.value(), proposal.registry_cid() ); + } + + outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) + { + ConsensusManagerLogger()->trace( "{}: called for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return sgns::ProposalSigningBytes( proposal ); + } + + outcome::result> ConsensusManager::VoteSigningBytes( const Vote &vote ) + { + ConsensusManagerLogger()->trace( "{}: called with voter address {} proposal_id={}", + __func__, + vote.voter_id().substr( 0, 8 ), + vote.proposal_id() ); + return sgns::VoteSigningBytes( vote ); + } + + outcome::result> ConsensusManager::VoteBundleSigningBytes( const VoteBundle &bundle ) + { + ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", + __func__, + bundle.proposal_id().substr( 0, 8 ), + bundle.votes_size() ); + return sgns::VoteBundleSigningBytes( bundle ); + } + + outcome::result ConsensusManager::SubmitProposal( const Proposal &proposal, bool self_vote ) + { + ConsensusManagerLogger()->trace( "{}: called for hash {} proposal_id={} self_vote={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + self_vote ); + const auto slot_key = GetSlotKey( proposal ); + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( proposal.proposal_id() ); + if ( it == proposals_.end() ) + { + ConsensusManagerLogger()->debug( "{}: Creating proposal state for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + ProposalState state; + state.proposal = proposal; + state.slot_key = slot_key; + proposals_.emplace( proposal.proposal_id(), std::move( state ) ); + } + } + + ConsensusMessage message; + *message.mutable_proposal() = proposal; + auto publish_result = Publish( message ); + if ( publish_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: publish error={}", + __func__, + publish_result.error().message() ); + return publish_result; + } + ConsensusManagerLogger()->debug( "{}: success for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + + if ( self_vote ) + { + HandleProposal( proposal ); + } + + return outcome::success(); + } + + outcome::result ConsensusManager::SubmitVote( const Vote &vote, bool self_handle ) + { + ConsensusManagerLogger()->trace( "{}: called by {} proposal_id={}", + __func__, + vote.voter_id().substr( 0, 8 ), + vote.proposal_id().substr( 0, 8 ) ); + ConsensusMessage message; + *message.mutable_vote() = vote; + auto result = Publish( message ); + if ( result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); + return result; + } + ConsensusManagerLogger()->debug( "{}: success voter_id={} proposal_id={} ", + __func__, + vote.voter_id().substr( 0, 8 ), + vote.proposal_id().substr( 0, 8 ) ); + if ( self_handle ) + { + HandleVote( vote ); + } + return result; + } + + outcome::result ConsensusManager::SubmitCertificate( const Certificate &certificate ) + { + ConsensusManagerLogger()->trace( "{}: called for hash {} and proposal_id={}", + __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ), + certificate.proposal_id().substr( 0, 8 ) ); + ConsensusMessage message; + *message.mutable_certificate() = certificate; + auto result = Publish( message ); + if ( result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); + return result; + } + + auto subject_hash_result = GetSubjectHash( certificate.proposal().subject() ); + if ( subject_hash_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: subject hash {} error proposal_id={}", + __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ), + certificate.proposal_id().substr( 0, 8 ) ); + return outcome::failure( subject_hash_result.error() ); + } + + std::string serialized; + if ( !certificate.SerializeToString( &serialized ) ) + { + ConsensusManagerLogger()->error( "{}: failed: certificate serialize error", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + const auto key = std::string{ CERTIFICATE_BASE_PATH_KEY } + subject_hash_result.value(); + crdt::HierarchicalKey cert_key( key ); + crdt::GlobalDB::Buffer cert_value; + cert_value.put( serialized ); + + auto cert_put = db_->Put( cert_key, cert_value, { consensus_datastore_topic_ } ); + if ( cert_put.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: cert put for hash {} error={}", + __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ), + cert_put.error().message() ); + return outcome::failure( cert_put.error() ); + } + + ConsensusManagerLogger()->debug( "{}: success submitting certificate for {} and proposal_id={}", + __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ), + certificate.proposal_id().substr( 0, 8 ) ); + return result; + } + + void ConsensusManager::HandleProposal( const Proposal &proposal ) + { + ConsensusManagerLogger()->trace( "{}: called for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + + if ( !CheckProposal( proposal ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: Invalid proposal for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + + if ( !IsTimestampSane( proposal.timestamp() ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: timestamp out of bounds for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + + if ( proposal.registry_cid().empty() ) + { + ConsensusManagerLogger()->error( "{}: rejected: proposal registry CID missing for hash {}. proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + + auto subject_hash = GetSubjectHash( proposal.subject() ); + if ( subject_hash.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject hash missing proposal_id={}", + __func__, + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + + auto proposal_registry_result = registry_->LoadRegistry( proposal.registry_cid() ); + if ( proposal_registry_result.has_error() ) + { + ConsensusManagerLogger()->warn( + "{}: deferred: registry load error={} proposal={} proposal_id={} hash={}. Keeping proposal pending", + __func__, + proposal_registry_result.error().message(), + proposal.registry_cid(), + proposal.proposal_id().substr( 0, 8 ), + subject_hash.value().substr( 0, 8 ) ); + + { + std::lock_guard lock( proposals_mutex_ ); + if ( proposals_.find( proposal.proposal_id() ) == proposals_.end() ) + { + ProposalState state; + state.proposal = proposal; + state.slot_key = GetSlotKey( proposal ); + proposals_.emplace( proposal.proposal_id(), std::move( state ) ); + } + } + + AddPendingProposal( proposal, subject_hash.value() ); + return; + } + if ( proposal.registry_epoch() != proposal_registry_result.value().epoch() ) + { + ConsensusManagerLogger()->error( "{}: rejected: registry epoch mismatch proposal={} registry={}", + __func__, + proposal.registry_epoch(), + proposal_registry_result.value().epoch() ); + return; + } + + if ( !CheckSubject( proposal.subject() ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + + if ( CheckCertificateForSubject( subject_hash.value() ) ) + { + ConsensusManagerLogger()->debug( "{}: ignored: subject already certified hash={} proposal_id={}", + __func__, + subject_hash.value().substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); + std::lock_guard lock( proposals_mutex_ ); + pending_votes_.erase( proposal.proposal_id() ); + pending_proposals_.erase( proposal.proposal_id() ); + return; + } + + SubjectHandler subject_handler; + { + std::shared_lock lock( subject_handlers_mutex_ ); + auto handler_it = subject_handlers_.find( static_cast( proposal.subject().type() ) ); + if ( handler_it == subject_handlers_.end() ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject handler missing type={}", + __func__, + static_cast( proposal.subject().type() ) ); + return; + } + subject_handler = handler_it->second; + } + + auto subject_result = subject_handler( proposal.subject() ); + if ( subject_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject handler error for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + + if ( subject_result.value() == Check::Reject ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + return; + } + + if ( subject_result.value() == Check::Pending ) + { + { + std::lock_guard lock( proposals_mutex_ ); + if ( proposals_.find( proposal.proposal_id() ) == proposals_.end() ) + { + ProposalState state; + state.proposal = proposal; + state.slot_key = GetSlotKey( proposal ); + proposals_.emplace( proposal.proposal_id(), std::move( state ) ); + } + } + ConsensusManagerLogger()->debug( "{}: Adding pending proposal for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); + AddPendingProposal( proposal, subject_hash.value() ); + return; + } + + ContinueProposalAfterSubject( proposal ); + } + + outcome::result ConsensusManager::ResumeProposalHandling( const std::string &subject_hash ) + { + if ( subject_hash.empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + ConsensusManagerLogger()->trace( "{}: Attempting to resume proposals for hash={}", + __func__, + subject_hash.substr( 0, 8 ) ); + + auto to_process = TakePendingProposals( subject_hash ); + + for ( const auto &proposal : to_process ) + { + SubjectHandler subject_handler; + { + std::shared_lock lock( subject_handlers_mutex_ ); + auto handler_it = subject_handlers_.find( static_cast( proposal.subject().type() ) ); + if ( handler_it == subject_handlers_.end() ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject handler missing type={}", + __func__, + static_cast( proposal.subject().type() ) ); + continue; + } + subject_handler = handler_it->second; + } + + auto subject_result = subject_handler( proposal.subject() ); + if ( subject_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject handler error for hash {} proposal_id={}", + __func__, + subject_hash.substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); + continue; + } + + if ( subject_result.value() == Check::Reject ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", + __func__, + subject_hash.substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); + continue; + } + + if ( subject_result.value() == Check::Pending ) + { + auto subject_hash_result = GetSubjectHash( proposal.subject() ); + if ( subject_hash_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject hash missing proposal_id={}", + __func__, + proposal.proposal_id() ); + continue; + } + ConsensusManagerLogger()->debug( "{}: Adding pending proposal for hash {} proposal_id={}", + __func__, + subject_hash.substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); + AddPendingProposal( proposal, subject_hash_result.value() ); + continue; + } + + ContinueProposalAfterSubject( proposal ); + } + return outcome::success(); + } + + void ConsensusManager::ProcessCertificates() + { + std::vector to_process; + { + std::lock_guard lock( proposals_mutex_ ); + for ( auto &kv : proposals_ ) + { + auto &state = kv.second; + if ( !state.quorum_reached ) + { + ConsensusManagerLogger()->debug( + "{}: Found proposal without quorum reached for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); + + continue; + } + to_process.push_back( state ); + } + } + + for ( auto &state : to_process ) + { + auto subject_hash = GetSubjectHash( state.proposal.subject() ); + if ( subject_hash.has_value() && CheckCertificateForSubject( subject_hash.value() ) ) + { + ConsensusManagerLogger()->debug( "{}: hash {} already certified, clearing proposal_id={}", + __func__, + subject_hash.value().substr( 0, 8 ), + state.proposal.proposal_id().substr( 0, 8 ) ); + ClearProposalSlot( state.proposal ); + continue; + } + ConsensusManagerLogger()->debug( "{}: Processing proposal with quorum reached for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); + const auto now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count(); + if ( state.quorum_reached_ts_ms != 0 && certificate_delay_.count() > 0 ) + { + const auto elapsed_ms = static_cast( now_ms ) - + static_cast( state.quorum_reached_ts_ms ); + if ( elapsed_ms < static_cast( certificate_delay_.count() ) ) + { + continue; + } + } + + const auto round = GetCurrentRound( state.proposal.timestamp() ); + if ( state.last_attempt_round != NO_ROUND && round == state.last_attempt_round ) + { + ConsensusManagerLogger()->debug( + "{}: proposal already attempted in round for hash {} proposal_id={} round={}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ), + round ); + continue; + } + auto proposal_registry_result = registry_->LoadRegistry( state.proposal.registry_cid() ); + if ( proposal_registry_result.has_error() ) + { + ConsensusManagerLogger()->debug( "{}: skipping proposal due to registry load error={} proposal_id={}", + __func__, + proposal_registry_result.error().message(), + state.proposal.proposal_id().substr( 0, 8 ) ); + continue; + } + const auto &proposal_registry = proposal_registry_result.value(); + if ( state.proposal.registry_epoch() != proposal_registry.epoch() ) + { + ConsensusManagerLogger()->debug( "{}: skipping proposal due to registry epoch mismatch proposal_id={}", + __func__, + state.proposal.proposal_id().substr( 0, 8 ) ); + continue; + } + + if ( !IsCurrentAggregator( state.proposal, proposal_registry ) ) + { + ConsensusManagerLogger()->debug( "{}: not aggregator for proposal for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); + continue; + } + + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( state.proposal.proposal_id() ); + if ( it != proposals_.end() ) + { + it->second.last_attempt_round = round; + } + } + ConsensusManagerLogger()->debug( "{}: Attempting to create certificate for hash {} proposal_id={} round={}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ), + round ); + auto certificate_result = CreateCertificate( state.proposal, state.votes ); + if ( certificate_result.has_error() ) + { + ConsensusManagerLogger()->error( + "{}: failed: certificate creation error for hash {} proposal_id {}: {}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ), + certificate_result.error().message() ); + continue; + } + + (void)SubmitCertificate( certificate_result.value() ); + ClearProposalSlot( state.proposal ); + ConsensusManagerLogger()->debug( "{}: certificate submitted for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); + } + } + + void ConsensusManager::UpdateCertificatesPending() + { + bool has_pending = false; + { + std::lock_guard lock( proposals_mutex_ ); + for ( const auto &kv : proposals_ ) + { + if ( kv.second.quorum_reached ) + { + has_pending = true; + break; + } + } + } + certificates_pending_.store( has_pending ); + if ( !has_pending ) + { + timer_cv_.notify_all(); + } + } + + bool ConsensusManager::RegisterCertificateFilter() + { + const std::string pattern = std::string( CERT_KEY_PATTERN ); + + auto weak_self = weak_from_this(); + const bool filter_registered = db_->RegisterElementFilter( + pattern, + [weak_self]( const crdt::pb::Element &element ) -> std::optional> + { + if ( auto strong = weak_self.lock() ) + { + return strong->FilterCertificate( element ); + } + return std::nullopt; + } ); + + const bool callback_registered = db_->RegisterNewElementCallback( + pattern, + [weak_self]( crdt::CRDTCallbackManager::NewDataPair new_data, const std::string &cid ) + { + if ( auto strong = weak_self.lock() ) + { + strong->CertificateReceived( std::move( new_data ), cid ); + } + } ); + + db_->AddListenTopic( consensus_datastore_topic_ ); + + return filter_registered && callback_registered; + } + + std::optional> ConsensusManager::FilterCertificate( + const crdt::pb::Element &element ) + { + ConsensusManagerLogger()->trace( "{}: entry key={}", __func__, element.key() ); + Certificate certificate; + if ( !certificate.ParseFromString( element.value() ) ) + { + ConsensusManagerLogger()->error( "{}: parse failed, rejecting: {}", __func__, element.key() ); + return std::vector{}; + } + + if ( certificate.proposal_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: missing proposal_id, rejecting: {}", __func__, element.key() ); + return std::vector{}; + } + + if ( ValidateCertificate( certificate ) == Check::Reject ) + { + ConsensusManagerLogger()->error( "{}: validation failed, rejecting: {}", __func__, element.key() ); + return std::vector{}; + } + + ConsensusManagerLogger()->debug( "{}: certificate accepted key={}", __func__, element.key() ); + return std::nullopt; + } + + void ConsensusManager::CertificateReceived( crdt::CRDTCallbackManager::NewDataPair new_data, + const std::string &cid ) + { + auto [key, value] = new_data; + (void)cid; + Certificate certificate; + if ( !certificate.ParseFromArray( value.data(), value.size() ) ) + { + ConsensusManagerLogger()->error( "{}: invalid certificate payload key={}", __func__, key ); + return; + } + + auto subject_hash = GetSubjectHash( certificate.proposal().subject() ); + if ( subject_hash.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed getting subject hash proposal_id={} error={}", + __func__, + certificate.proposal_id().substr( 0, 8 ), + subject_hash.error().message() ); + return; + } + + auto certificate_check = ValidateCertificate( certificate ); + + if ( certificate_check == Check::Stalled ) + { + ConsensusManagerLogger()->error( + "{}: Validation of the certificate pending for key {}, certificate handler not called ", + __func__, + key ); + certificate_work_journal_->MarkStalled( key ); + return; + } + + registry_->OnFinalizedCertificate( certificate ); + + CertificateSubjectHandler handler; + { + std::shared_lock lock( certificate_handlers_mutex_ ); + auto it = certificate_subject_handlers_.find( static_cast( certificate.proposal().subject().type() ) ); + if ( it == certificate_subject_handlers_.end() ) + { + (void)certificate_work_journal_->MarkDone( key ); + return; + } + handler = it->second; + } + + auto certificate_handler_result = handler( subject_hash.value(), certificate ); + + if ( certificate_handler_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: certificate handler error proposal_id={} error={}", + __func__, + certificate.proposal_id().substr( 0, 8 ), + certificate_handler_result.error().message() ); + return; + } + auto certificate_result = certificate_handler_result.value(); + + if ( certificate_result == Check::Stalled ) + { + ConsensusManagerLogger()->error( "{}: certificate rejected by handler proposal_id={}", + __func__, + certificate.proposal_id().substr( 0, 8 ) ); + ConsensusManagerLogger()->debug( "{}: Key {} is not Done yet", __func__, key ); + certificate_work_journal_->MarkStalled( key ); + return; + } + (void)certificate_work_journal_->MarkDone( key ); + } + + ConsensusManager::Check ConsensusManager::ValidateCertificate( const Certificate &certificate ) const + { + if ( certificate.proposal_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: Certificate proposal ID missing ", __func__ ); + return Check::Reject; + } + if ( !certificate.has_proposal() ) + { + ConsensusManagerLogger()->error( "{}: Certificate missing proposal ", __func__ ); + return Check::Reject; + } + + const auto &proposal = certificate.proposal(); + if ( proposal.proposal_id() != certificate.proposal_id() ) + { + ConsensusManagerLogger()->error( "{}: rejected: proposal_id mismatch cert={} proposal={}", + __func__, + certificate.proposal_id(), + proposal.proposal_id() ); + return Check::Reject; + } + if ( proposal.registry_cid() != certificate.registry_cid() || + proposal.registry_epoch() != certificate.registry_epoch() ) + { + ConsensusManagerLogger()->error( "{}: rejected: registry mismatch proposal_id={}", + __func__, + certificate.proposal_id() ); + return Check::Reject; + } + auto registry_ret = registry_->LoadRegistry( certificate.registry_cid() ); + if ( registry_ret.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: registry load error={} for registry cid {} proposal_id={}", + __func__, + registry_ret.error().message(), + certificate.registry_cid(), + certificate.proposal_id() ); + return Check::Stalled; + } + auto ®istry = registry_ret.value(); + if ( !ValidateSubject( proposal.subject() ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: invalid subject proposal_id={}", + __func__, + proposal.proposal_id() ); + return Check::Reject; + } + if ( !CheckProposal( proposal ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: invalid proposal proposal_id={}", + __func__, + proposal.proposal_id() ); + return Check::Reject; + } + + const auto computed_id = CreateProposalId( proposal ); + if ( computed_id.empty() ) + { + ConsensusManagerLogger()->error( "{}: rejected: computed_id empty", __func__ ); + return Check::Reject; + } + if ( computed_id != certificate.proposal_id() ) + { + ConsensusManagerLogger()->error( "{}: rejected: computed_id mismatch cert={} computed={}", + __func__, + certificate.proposal_id(), + computed_id ); + return Check::Reject; + } + + std::vector votes; + votes.reserve( static_cast( certificate.votes_size() ) ); + for ( const auto &vote : certificate.votes() ) + { + votes.push_back( vote ); + } + auto tally = TallyVotes( proposal, votes, registry, certificate.registry_cid() ); + if ( tally.has_error() || !tally.value().has_quorum ) + { + return Check::Reject; + } + + return Check::Approve; + } + + void ConsensusManager::HandleVote( const Vote &vote ) + { + ConsensusManagerLogger()->trace( "{}: called. Vote by {} on proposal_id={} ", + __func__, + vote.voter_id().substr( 0, 8 ), + vote.proposal_id().substr( 0, 8 ) ); + if ( !CheckVote( vote ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: Invalid vote proposal_id={} voter_id={}", + __func__, + vote.proposal_id(), + vote.voter_id() ); + return; + } + if ( !vote.approve() ) + { + ConsensusManagerLogger()->debug( "{}: ignored: vote not approved voter_id={}", + __func__, + vote.voter_id().substr( 0, 8 ) ); + //TODO - maybe see reputation? + return; + } + + auto signing_bytes = VoteSigningBytes( vote ); + if ( signing_bytes.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: signing bytes error={}", + __func__, + signing_bytes.error().message() ); + return; + } + if ( !GeniusAccount::VerifySignature( vote.voter_id(), vote.signature(), signing_bytes.value() ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: signature verification failed voter_id={}", + __func__, + vote.voter_id().substr( 0, 8 ) ); + return; + } + + bool has_quorum = false; + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( vote.proposal_id() ); + if ( it == proposals_.end() ) + { + pending_votes_[vote.proposal_id()].push_back( vote ); + ConsensusManagerLogger()->debug( "{}: queued pending vote proposal_id={}", + __func__, + vote.proposal_id().substr( 0, 8 ) ); + return; + } + auto &proposal_state = it->second; + auto subject_hash = GetSubjectHash( proposal_state.proposal.subject() ); + if ( subject_hash.has_value() && CheckCertificateForSubject( subject_hash.value() ) ) + { + ConsensusManagerLogger()->debug( "{}: ignored: vote for already certified hash {} proposal_id={}", + __func__, + subject_hash.value().substr( 0, 8 ), + vote.proposal_id().substr( 0, 8 ) ); + pending_votes_.erase( vote.proposal_id() ); + return; + } + auto slot_it = slot_states_.find( proposal_state.slot_key ); + if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id != vote.proposal_id() ) + { + ConsensusManagerLogger()->error( "{}: ignored: not best proposal proposal_id={}", + __func__, + vote.proposal_id().substr( 0, 8 ) ); + return; + } + + if ( proposal_state.seen_voters.find( vote.voter_id() ) != proposal_state.seen_voters.end() ) + { + ConsensusManagerLogger()->trace( "{}: ignored: duplicate vote voter_id={}", + __func__, + vote.voter_id().substr( 0, 8 ) ); + return; + } + + auto proposal_registry_result = registry_->LoadRegistry( proposal_state.proposal.registry_cid() ); + if ( proposal_registry_result.has_error() ) + { + ConsensusManagerLogger()->warn( "{}: deferred vote: registry load error={} proposal_id={}", + __func__, + proposal_registry_result.error().message(), + vote.proposal_id().substr( 0, 8 ) ); + pending_votes_[vote.proposal_id()].push_back( vote ); + return; + } + const auto &proposal_registry = proposal_registry_result.value(); + if ( proposal_state.proposal.registry_epoch() != proposal_registry.epoch() ) + { + ConsensusManagerLogger()->error( "{}: rejected: registry mismatch proposal_id={}", + __func__, + vote.proposal_id().substr( 0, 8 ) ); + return; + } + + const auto *validator = registry_->FindValidator( proposal_registry, vote.voter_id() ); + const bool is_active_validator = validator && validator->status() == ValidatorRegistry::Status::ACTIVE; + + if ( it->second.total_weight == 0 ) + { + it->second.total_weight = registry_->TotalWeight( proposal_registry ); + } + + it->second.votes.push_back( vote ); + it->second.seen_voters.insert( vote.voter_id() ); + if ( is_active_validator ) + { + it->second.approved_weight += validator->weight(); + has_quorum = registry_->IsQuorum( it->second.approved_weight, it->second.total_weight ); + if ( has_quorum ) + { + if ( !it->second.quorum_reached ) + { + it->second.quorum_reached = true; + it->second.quorum_reached_ts_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count(); + } + ConsensusManagerLogger()->debug( + "{}: quorum reached; certificate will be created by timer proposal_id={}", + __func__, + vote.proposal_id() ); + } + } + else + { + ConsensusManagerLogger()->debug( "{}: accepted vote from non-validator voter_id={}", + __func__, + vote.voter_id().substr( 0, 8 ) ); + } + } + if ( has_quorum ) + { + certificates_pending_.store( true ); + timer_cv_.notify_all(); + } + } + + void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) + { + ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", + __func__, + bundle.proposal_id().substr( 0, 8 ), + bundle.votes_size() ); + + for ( const auto &vote : bundle.votes() ) + { + ConsensusManagerLogger()->trace( "{}: processing voter_id={}", __func__, vote.voter_id().substr( 0, 8 ) ); + HandleVote( vote ); + } + } + + void ConsensusManager::HandleCertificate( const Certificate &certificate ) + { + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); + + if ( ValidateCertificate( certificate ) == Check::Reject ) + { + ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", + __func__, + certificate.proposal_id() ); + return; + } + + ProposalState proposal_state; + auto fetch_proposal_state_ret = FetchProposalState( certificate ); + if ( fetch_proposal_state_ret.has_value() ) + { + proposal_state = fetch_proposal_state_ret.value(); + ConsensusManagerLogger()->debug( "{}: fetched proposal state, proposal_id={}", + __func__, + certificate.proposal_id() ); + } + else + { + ConsensusManagerLogger()->debug( "{}: proposal state not found, creating new one proposal_id={}", + __func__, + certificate.proposal_id() ); + proposal_state = CreateProposalState( certificate ); + } + + if ( !ValidateCertificateBestProposal( proposal_state, certificate ) ) + { + return; + } + + ClearProposalSlot( certificate.proposal() ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); + } + + outcome::result ConsensusManager::FetchProposalState( + const Certificate &certificate ) + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( certificate.proposal_id() ); + if ( it == proposals_.end() ) + { + return outcome::failure( std::errc::no_such_device ); + } + return it->second; + } + + ConsensusManager::ProposalState ConsensusManager::CreateProposalState( const Certificate &certificate ) + { + ProposalState new_state; + new_state.proposal = certificate.proposal(); + new_state.slot_key = GetSlotKey( new_state.proposal ); + proposals_.emplace( new_state.proposal.proposal_id(), new_state ); + + auto &slot_state = slot_states_[new_state.slot_key]; + if ( slot_state.best_proposal_id.empty() ) + { + slot_state.best_proposal_id = new_state.proposal.proposal_id(); + if ( new_state.proposal.subject().has_nonce() ) + { + slot_state.best_tx_hash = new_state.proposal.subject().nonce().tx_hash(); + } + } + + return new_state; + } + + bool ConsensusManager::ValidateCertificateBestProposal( const ProposalState &state, + const Certificate &certificate ) const + { + if ( certificate.has_proposal() && certificate.proposal().has_subject() && + certificate.proposal().subject().type() == SubjectType::SUBJECT_REGISTRY_BATCH ) + { + // Registry-batch subjects can have multiple competing proposals for the same deterministic batch root. + // Once a valid certificate exists, accept it even if local best_proposal_id changed due proposal races. + return true; + } + std::lock_guard lock( proposals_mutex_ ); + auto slot_it = slot_states_.find( state.slot_key ); + if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id != certificate.proposal_id() ) + { + ConsensusManagerLogger()->error( "{}: rejected: not best proposal proposal_id={}", + __func__, + certificate.proposal_id() ); + return false; + } + return true; + } + + std::vector ConsensusManager::CollectCertificateVotes( + const Certificate &certificate ) const + { + std::vector votes; + votes.reserve( static_cast( certificate.votes_size() ) ); + for ( const auto &vote : certificate.votes() ) + { + ConsensusManagerLogger()->trace( "{}: processing vote voter_id={}", __func__, vote.voter_id() ); + votes.push_back( vote ); + } + return votes; + } + + void ConsensusManager::ClearProposalSlot( const Proposal &proposal ) + { + std::lock_guard lock( proposals_mutex_ ); + + std::string slot_key; + auto it = proposals_.find( proposal.proposal_id() ); + if ( it != proposals_.end() ) + { + slot_key = it->second.slot_key; + } + else + { + slot_key = GetSlotKey( proposal ); + } + + std::unordered_set ids_to_remove; + ids_to_remove.insert( proposal.proposal_id() ); + for ( const auto &kv : proposals_ ) + { + if ( kv.second.slot_key == slot_key ) + { + ids_to_remove.insert( kv.first ); + } + } + + for ( const auto &proposal_id : ids_to_remove ) + { + proposals_.erase( proposal_id ); + pending_proposals_.erase( proposal_id ); + pending_votes_.erase( proposal_id ); + } + + for ( auto it_hash = pending_by_subject_hash_.begin(); it_hash != pending_by_subject_hash_.end(); ) + { + auto &vec = it_hash->second; + vec.erase( std::remove_if( vec.begin(), + vec.end(), + [&]( const std::string &proposal_id ) + { return ids_to_remove.find( proposal_id ) != ids_to_remove.end(); } ), + vec.end() ); + if ( vec.empty() ) + { + it_hash = pending_by_subject_hash_.erase( it_hash ); + } + else + { + ++it_hash; + } + } + + slot_states_.erase( slot_key ); + + bool has_pending = false; + for ( const auto &kv : proposals_ ) + { + if ( kv.second.quorum_reached ) + { + has_pending = true; + break; + } + } + certificates_pending_.store( has_pending ); + if ( !has_pending ) + { + timer_cv_.notify_all(); + } + } + + std::string ConsensusManager::GetSlotKey( const Proposal &proposal ) const + { + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); + if ( proposal.subject().type() == SubjectType::SUBJECT_NONCE && proposal.subject().has_nonce() ) + { + return proposal.subject().account_id() + ":" + std::to_string( proposal.subject().nonce().nonce() ); + } + if ( !proposal.subject().subject_id().empty() ) + { + return proposal.subject().subject_id(); + } + return proposal.proposal_id(); + } + + bool ConsensusManager::IsBetterProposal( const Proposal &candidate, const Proposal ¤t ) const + { + ConsensusManagerLogger()->trace( "{}: called candidate={} current={}", + __func__, + candidate.proposal_id(), + current.proposal_id() ); + const bool candidate_nonce = candidate.subject().type() == SubjectType::SUBJECT_NONCE && + candidate.subject().has_nonce(); + const bool current_nonce = current.subject().type() == SubjectType::SUBJECT_NONCE && + current.subject().has_nonce(); + if ( candidate_nonce && current_nonce ) + { + const auto &cand_hash = candidate.subject().nonce().tx_hash(); + const auto &curr_hash = current.subject().nonce().tx_hash(); + if ( cand_hash == curr_hash ) + { + return candidate.proposal_id() < current.proposal_id(); + } + return BestHash( curr_hash, cand_hash ) == cand_hash; + } + + return candidate.proposal_id() < current.proposal_id(); + } + + const std::string &ConsensusManager::BestHash( const std::string &a, const std::string &b ) + { + return ( a <= b ) ? a : b; + } + + outcome::result ConsensusManager::ComputeSubjectId( const Subject &subject ) + { + ConsensusManagerLogger()->trace( "{}: called subject_type={}", __func__, static_cast( subject.type() ) ); + Subject copy = subject; + copy.clear_subject_id(); + std::string serialized; + if ( !copy.SerializeToString( &serialized ) ) + { + ConsensusManagerLogger()->error( "{}: failed: serialization error", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + sgns::crypto::HasherImpl hasher; + auto hash = hasher.sha2_256( + gsl::span( reinterpret_cast( serialized.data() ), serialized.size() ) ); + ConsensusManagerLogger()->debug( "{}: success", __func__ ); + return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); + } + + outcome::result ConsensusManager::CreateNonceSubject( + const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash, + const std::optional &utxo_commitment, + const std::optional &utxo_witness ) + { + ConsensusManagerLogger()->trace( "{}: called account_id={} nonce={}", __func__, account_id, nonce ); + Subject subject; + subject.set_type( SubjectType::SUBJECT_NONCE ); + subject.set_account_id( account_id ); + auto *payload = subject.mutable_nonce(); + payload->set_nonce( nonce ); + payload->set_tx_hash( tx_hash.data(), tx_hash.size() ); + if ( utxo_commitment.has_value() ) + { + *payload->mutable_utxo_commitment() = utxo_commitment.value(); + } + if ( utxo_witness.has_value() ) + { + *payload->mutable_utxo_witness() = utxo_witness.value(); + } + + auto subject_id = ComputeSubjectId( subject ); + if ( subject_id.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: subject id error={}", + __func__, + subject_id.error().message() ); + return outcome::failure( subject_id.error() ); + } + subject.set_subject_id( subject_id.value() ); + ConsensusManagerLogger()->debug( "{}: success subject_id={}", __func__, subject.subject_id() ); + return subject; + } + + outcome::result ConsensusManager::CreateTaskResultSubject( + const std::string &account_id, + const std::string &escrow_path, + const std::string &task_result_hash, + uint64_t result_epoch ) + { + ConsensusManagerLogger()->trace( "{}: called account_id={} result_epoch={}", + __func__, + account_id, + result_epoch ); + Subject subject; + subject.set_type( SubjectType::SUBJECT_TASK_RESULT ); + subject.set_account_id( account_id ); + auto *payload = subject.mutable_task_result(); + payload->set_escrow_path( escrow_path ); + payload->set_task_result_hash( task_result_hash.data(), task_result_hash.size() ); + payload->set_result_epoch( result_epoch ); + + auto subject_id = ComputeSubjectId( subject ); + if ( subject_id.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: subject id error={}", + __func__, + subject_id.error().message() ); + return outcome::failure( subject_id.error() ); + } + subject.set_subject_id( subject_id.value() ); + ConsensusManagerLogger()->debug( "{}: success subject_id={}", __func__, subject.subject_id() ); + return subject; + } + + outcome::result ConsensusManager::CreateRegistryBatchSubject( + const std::string &account_id, + const std::string &base_registry_cid, + uint64_t base_registry_epoch, + uint64_t target_registry_epoch, + uint32_t certificate_count, + const std::string &batch_root ) + { + ConsensusManagerLogger()->trace( "{}: called account_id={} base_epoch={} target_epoch={} certificates={}", + __func__, + account_id.substr( 0, 8 ), + base_registry_epoch, + target_registry_epoch, + certificate_count ); + Subject subject; + subject.set_type( SubjectType::SUBJECT_REGISTRY_BATCH ); + subject.set_account_id( account_id ); + auto *payload = subject.mutable_registry_batch(); + payload->set_base_registry_cid( base_registry_cid ); + payload->set_base_registry_epoch( base_registry_epoch ); + payload->set_target_registry_epoch( target_registry_epoch ); + payload->set_certificate_count( certificate_count ); + payload->set_batch_root( batch_root.data(), batch_root.size() ); + + auto subject_id = ComputeSubjectId( subject ); + if ( subject_id.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: subject id error={}", + __func__, + subject_id.error().message() ); + return outcome::failure( subject_id.error() ); + } + subject.set_subject_id( subject_id.value() ); + ConsensusManagerLogger()->debug( "{}: success subject_id={}", __func__, subject.subject_id() ); + return subject; + } + + std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) + { + ConsensusManagerLogger()->trace( "{}: Creating proposal ID", __func__ ); + // Proposal ID must be derived from the proposal contents excluding the proposal_id itself. + Proposal copy = proposal; + copy.clear_proposal_id(); + auto signing_bytes = ProposalSigningBytes( copy ); + if ( signing_bytes.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed, no proposal ID created: signing bytes error={}", + __func__, + signing_bytes.error().message() ); + return {}; + } + + sgns::crypto::HasherImpl hasher; + auto hash = hasher.sha2_256( + gsl::span( signing_bytes.value().data(), signing_bytes.value().size() ) ); + auto proposal_id = base::hex_lower( gsl::span( hash.data(), hash.size() ) ); + ConsensusManagerLogger()->debug( "{}: Proposal ID {} created", __func__, proposal_id.substr( 0, 8 ) ); + return proposal_id; + } + + bool ConsensusManager::ValidateSubject( const Subject &subject ) + { + ConsensusManagerLogger()->trace( "{}: called subject_type={}", __func__, static_cast( subject.type() ) ); + if ( subject.account_id().empty() ) + { + return false; + } + if ( subject.subject_id().empty() ) + { + return false; + } + + const auto expected_subject_id = ComputeSubjectId( subject ); + if ( expected_subject_id.has_error() || expected_subject_id.value() != subject.subject_id() ) + { + return false; + } + + switch ( subject.type() ) + { + case SubjectType::SUBJECT_NONCE: + if ( !subject.has_nonce() || subject.nonce().tx_hash().empty() ) + { + return false; + } + // Allow commitment-only subjects (public-chain flow). Witness remains optional. + // But a witness without a commitment is always invalid. + if ( subject.nonce().has_utxo_witness() && !subject.nonce().has_utxo_commitment() ) + { + return false; + } + return true; + case SubjectType::SUBJECT_TASK_RESULT: + return subject.has_task_result() && !subject.task_result().task_result_hash().empty(); + case SubjectType::SUBJECT_REGISTRY_BATCH: + return subject.has_registry_batch() && !subject.registry_batch().base_registry_cid().empty() && + subject.registry_batch().target_registry_epoch() == + subject.registry_batch().base_registry_epoch() + 1 && + subject.registry_batch().certificate_count() > 0 && + !subject.registry_batch().batch_root().empty(); + case SubjectType::SUBJECT_UNSPECIFIED: + default: + return false; + } + } + + void ConsensusManager::OnConsensusMessage( boost::optional message ) + { + ConsensusManagerLogger()->trace( "{}: called", __func__ ); + if ( !message ) + { + ConsensusManagerLogger()->error( "{}: ignored: message is empty", __func__ ); + return; + } + + ConsensusMessage decoded; + if ( !decoded.ParseFromArray( message->data.data(), static_cast( message->data.size() ) ) ) + { + ConsensusManagerLogger()->error( "{}: Failed to decode consensus message", __func__ ); + return; + } + + if ( decoded.has_proposal() ) + { + ConsensusManagerLogger()->debug( "{}: decoded proposal", __func__ ); + HandleProposal( decoded.proposal() ); + return; + } + if ( decoded.has_vote() ) + { + ConsensusManagerLogger()->debug( "{}: decoded vote", __func__ ); + HandleVote( decoded.vote() ); + return; + } + if ( decoded.has_vote_bundle() ) + { + ConsensusManagerLogger()->debug( "{}: decoded vote bundle", __func__ ); + HandleVoteBundle( decoded.vote_bundle() ); + return; + } + if ( decoded.has_certificate() ) + { + ConsensusManagerLogger()->debug( "{}: decoded certificate", __func__ ); + HandleCertificate( decoded.certificate() ); + } + } + + bool ConsensusManager::CheckSubject( const Subject &subject ) + { + ConsensusManagerLogger()->trace( "{}: subject_type={}", __func__, static_cast( subject.type() ) ); + + if ( subject.account_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: subject account_id is empty", __func__ ); + return false; + } + + if ( subject.subject_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: subject subject_id is empty", __func__ ); + return false; + } + auto expected_subject_id = ComputeSubjectId( subject ); + if ( expected_subject_id.has_error() || expected_subject_id.value() != subject.subject_id() ) + { + ConsensusManagerLogger()->error( "{}: subject subject_id mismatch", __func__ ); + return false; + } + + if ( subject.type() != SubjectType::SUBJECT_NONCE && subject.type() != SubjectType::SUBJECT_TASK_RESULT && + subject.type() != SubjectType::SUBJECT_REGISTRY_BATCH ) + { + ConsensusManagerLogger()->error( "{}: Invalid Subject type {}", + __func__, + static_cast( subject.type() ) ); + return false; + } + if ( subject.type() == SubjectType::SUBJECT_NONCE ) + { + if ( !subject.has_nonce() ) + { + ConsensusManagerLogger()->error( "{}: subject missing nonce payload", __func__ ); + return false; + } + if ( subject.nonce().tx_hash().empty() ) + { + ConsensusManagerLogger()->error( "{}: subject nonce tx_hash is empty", __func__ ); + return false; + } + } + + if ( subject.type() == SubjectType::SUBJECT_TASK_RESULT ) + { + if ( !subject.has_task_result() ) + { + ConsensusManagerLogger()->error( "{}: subject missing task_result payload", __func__ ); + return false; + } + if ( subject.task_result().escrow_path().empty() ) + { + ConsensusManagerLogger()->error( "{}: subject task_result escrow_path is empty", __func__ ); + return false; + } + if ( subject.task_result().task_result_hash().empty() ) + { + ConsensusManagerLogger()->error( "{}: subject task_result task_result_hash is empty", __func__ ); + return false; + } + } + + if ( subject.type() == SubjectType::SUBJECT_REGISTRY_BATCH ) + { + if ( !subject.has_registry_batch() ) + { + ConsensusManagerLogger()->error( "{}: subject missing registry_batch payload", __func__ ); + return false; + } + if ( subject.registry_batch().base_registry_cid().empty() ) + { + ConsensusManagerLogger()->error( "{}: subject registry_batch base_registry_cid is empty", __func__ ); + return false; + } + if ( subject.registry_batch().target_registry_epoch() != + subject.registry_batch().base_registry_epoch() + 1 ) + { + ConsensusManagerLogger()->error( "{}: subject registry_batch target epoch mismatch", __func__ ); + return false; + } + if ( subject.registry_batch().certificate_count() == 0 ) + { + ConsensusManagerLogger()->error( "{}: subject registry_batch certificate_count is zero", __func__ ); + return false; + } + if ( subject.registry_batch().batch_root().empty() ) + { + ConsensusManagerLogger()->error( "{}: subject registry_batch batch_root is empty", __func__ ); + return false; + } + } + + return true; + } + + bool ConsensusManager::CheckProposal( const Proposal &proposal ) + { + if ( proposal.proposal_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: Proposal ID missing ", __func__ ); + return false; + } + if ( proposal.proposer_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: Proposer ID missing ", __func__ ); + return false; + } + if ( proposal.registry_cid().empty() ) + { + ConsensusManagerLogger()->error( "{}: Registry CID missing ", __func__ ); + return false; + } + if ( !proposal.has_subject() ) + { + ConsensusManagerLogger()->error( "{}: Proposal without subject ", __func__ ); + return false; + } + auto signing_bytes = ProposalSigningBytes( proposal ); + if ( signing_bytes.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: signing bytes error={}", + __func__, + signing_bytes.error().message() ); + return false; + } + if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: signature verification failed proposer_id={}", + __func__, + proposal.proposer_id() ); + return false; + } + return true; + } + + bool ConsensusManager::CheckVote( const Vote &vote ) + { + if ( vote.proposal_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: Vote proposal ID missing ", __func__ ); + return false; + } + if ( vote.voter_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: Vote voter ID missing ", __func__ ); + return false; + } + return true; + } + + void ConsensusManager::RecoverPendingCertificateWork() + { + auto recovered = certificate_work_journal_->RecoverStaleProcessing( CERT_KEY_PATTERN, std::chrono::seconds( 15 ) ); + if ( recovered > 0 ) + { + ConsensusManagerLogger()->info( "{}: recovered {} stale certificate work items", __func__, recovered ); + } + + auto unfinished = certificate_work_journal_->ListUnfinished( CERT_KEY_PATTERN ); + const auto now_ms = + static_cast( std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count() ); + + for ( const auto &entry : unfinished ) + { + if ( entry.key.empty() ) + { + continue; + } + if ( entry.state != crdt::CRDTWorkJournal::State::Stalled ) + { + continue; + } + if ( entry.lease_until_ms != 0 && entry.lease_until_ms > now_ms ) + { + continue; + } + auto value = db_->Get( { entry.key } ); + if ( value.has_error() ) + { + continue; + } + CertificateReceived( { entry.key, value.value() }, std::string{} ); + } + } + + outcome::result ConsensusManager::GetCertificateBySubjectHash( + const std::string &subject_hash ) const + { + const auto key = std::string{ CERTIFICATE_BASE_PATH_KEY } + subject_hash; + + BOOST_OUTCOME_TRY( auto certificate_data, db_->Get( { key } ) ); + + Certificate certificate; + if ( !certificate.ParseFromArray( certificate_data.data(), certificate_data.size() ) ) + { + ConsensusManagerLogger()->error( "{}: invalid certificate payload key={}", __func__, key ); + return outcome::failure( std::errc::invalid_argument ); + } + + auto current_hash = GetSubjectHash( certificate.proposal().subject() ); + if ( current_hash.has_error() ) + { + return outcome::failure( current_hash.error() ); + } + if ( current_hash.value() != subject_hash ) + { + ConsensusManagerLogger()->error( "{}: certificate subject hash mismatch expected={} actual={}", + __func__, + subject_hash, + current_hash.value() ); + return outcome::failure( std::errc::invalid_argument ); + } + return certificate; + } + + bool ConsensusManager::CheckCertificateForSubject( const std::string &subject_hash ) const + { + auto certificate_result = GetCertificateBySubjectHash( subject_hash ); + if ( certificate_result.has_error() ) + { + return false; + } + auto certificate_check = ValidateCertificate( certificate_result.value() ); + return certificate_check == Check::Approve; + } + + bool ConsensusManager::CheckCertificateForSubject( const ConsensusManager::Subject &subject ) const + { + auto current_hash = GetSubjectHash( subject ); + if ( current_hash.has_error() ) + { + ConsensusManagerLogger()->error( "{}: Failed to get the hash for the subject with ID {}, error: {}", + __func__, + subject.subject_id().substr( 0, 8 ), + current_hash.error().message() ); + return false; + } + auto certificate_result = GetCertificateBySubjectHash( current_hash.value() ); + if ( certificate_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: Failed to get the certificate for the hash {}, error: {}", + __func__, + GetPrintableSubjectHash( subject ), + certificate_result.error().message() ); + return false; + } + auto &certificate = certificate_result.value(); + auto certificate_subject_id_result = ComputeSubjectId( certificate.proposal().subject() ); + if ( certificate_subject_id_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed for hash {}: certificate subject id computation error={}", + __func__, + GetPrintableSubjectHash( subject ), + certificate_subject_id_result.error().message() ); + return false; + } + auto &certificate_subject_id = certificate_subject_id_result.value(); + auto subject_id_result = ComputeSubjectId( subject ); + if ( subject_id_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed for hash {}: subject id computation error={}", + __func__, + GetPrintableSubjectHash( subject ), + subject_id_result.error().message() ); + return false; + } + auto proposed_subject_id = subject_id_result.value(); + bool equal = proposed_subject_id == certificate_subject_id; + if ( !equal ) + { + ConsensusManagerLogger()->debug( "{}: Match for subject and certificate (hash {}): MISMATCH", + __func__, + GetPrintableSubjectHash( subject ) ); + return false; + } + auto certificate_check = ValidateCertificate( certificate ); + if ( certificate_check != Check::Approve ) + { + ConsensusManagerLogger()->error( "{}: certificate failed validation for hash {}", + __func__, + GetPrintableSubjectHash( subject ) ); + return false; + } + ConsensusManagerLogger()->debug( "{}: Match for subject and certificate (hash {}): {}", + __func__, + GetPrintableSubjectHash( subject ), + equal ? "Match" : "MISMATCH" ); + return true; + } + + std::string ConsensusManager::GetPrintableSubjectHash( const Subject &subject ) + { + auto subject_hash = GetSubjectHash( subject ); + const std::string short_hash = subject_hash.has_value() ? subject_hash.value().substr( 0, 8 ) : "Invalid"; + return short_hash; + } + +} diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp new file mode 100644 index 000000000..fba2787a5 --- /dev/null +++ b/src/blockchain/Consensus.hpp @@ -0,0 +1,276 @@ +/** + * @file Consensus.hpp + * @brief Consensus proposal/vote/certificate helpers. + * @date 2025-10-16 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "blockchain/ValidatorRegistry.hpp" +#include "blockchain/impl/proto/Consensus.pb.h" +#include "crdt/globaldb/crdt_work_journal.hpp" +#include "crdt/globaldb/globaldb.hpp" +#include "crdt/proto/delta.pb.h" +#include "ipfs_pubsub/gossip_pubsub.hpp" +#include "outcome/outcome.hpp" + +namespace sgns +{ + /** + * @brief Implements Consensus with weighted voting. + * + * This class implements a consensus algorithm using pubsub messages. + * A subject needs to be created and with it a proposal as well. The proposal gets sent to the network + * and gets voted by peers who receive it. This class has hooks to be filled by the caller to register methods + * to handle subject and proposal. The idea is to leave out the validation of specific data (transaction, job result and etc) + * for whomever creates the subject. It relies on @ref ValidatorRegistry class to get the voters and their weights. + * Once consensus is reached a round scheme determines who amongst the validators will create the certificate which is + * the finality of the subject. The certificate also enabled registry updates to register new validators according to peer who voted + * correctly or penalize people who votes incorrectly. + */ + class ConsensusManager : public std::enable_shared_from_this + { + public: + /** + * @brief Destroys the Consensus Manager object + */ + ~ConsensusManager(); + /** + * @brief Close and cleanup members of the Consensus Manager + */ + void Close(); + + using Proposal = ConsensusProposal; ///< Alias for Consensus Proposal protobuf type + using Vote = ConsensusVote; ///< Alias for Consensus Vote protobuf type + using VoteBundle = ConsensusVoteBundle; ///< Alias for Consensus Vote Bundle protobuf type + using Certificate = ConsensusCertificate; ///< Alias for Consensus Certificate protobuf type + using Subject = ConsensusSubject; ///< Alias for Consensus Subject protobuf type + + /// @brief Alias for a signer method type + using Signer = std::function>( std::vector payload )>; + + /** + * @brief Object checking values + */ + enum class Check + { + Approve, ///< Object is approved + Reject, ///< Object is rejected + Pending, ///< Object evaluation is pending + Stalled ///< Object evaluation is stalled + }; + + /// @brief Alias for a subject handler method type + using SubjectHandler = std::function( const Subject &subject )>; + /// @brief Alias for a certificate handler method type + using CertificateSubjectHandler = + std::function( const std::string &subject_hash, const Certificate &certificate )>; + + /** + * @brief Quorum tally structure + */ + struct QuorumTally + { + uint64_t total_weight = 0; ///< The total maximum weight of the quorum + uint64_t approved_weight = 0; ///< The weight which was already approved + bool has_quorum = false; ///< Flag indicating if quorum was reached + }; + + static std::shared_ptr New( std::shared_ptr registry, + std::shared_ptr db, + std::shared_ptr pubsub, + Signer signer, + std::string address, + std::string consensus_topic = "" ); + + bool RegisterSubjectHandler( SubjectType type, SubjectHandler handler ); + void UnregisterSubjectHandler( SubjectType type ); + bool RegisterCertificateHandler( SubjectType type, CertificateSubjectHandler handler ); + void UnregisterCertificateHandler( SubjectType type ); + + outcome::result Publish( const ConsensusMessage &message ); + + outcome::result CreateProposal( const Subject &subject, + const std::string &proposer_id, + const std::string ®istry_cid, + uint64_t registry_epoch ); + static outcome::result CreateProposal( const Subject &subject, + const std::string &proposer_id, + const std::string ®istry_cid, + uint64_t registry_epoch, + Signer sign ); + + outcome::result CreateVote( const std::string &proposal_id, + const std::string &voter_id, + bool approve, + Signer sign ); + + outcome::result CreateVoteBundle( const std::string &proposal_id, + const std::string &aggregator_id, + const std::vector &votes, + Signer sign ); + + outcome::result CreateCertificate( const Proposal &proposal, const std::vector &votes ); + + outcome::result TallyVotes( const Proposal &proposal, + const std::vector &votes, + const ValidatorRegistry::Registry ®istry, + const std::string ®istry_cid ) const; + outcome::result TallyVotes( const Proposal &proposal, const std::vector &votes ) const; + + static outcome::result> ProposalSigningBytes( const Proposal &proposal ); + static outcome::result> VoteSigningBytes( const Vote &vote ); + static outcome::result> VoteBundleSigningBytes( const VoteBundle &bundle ); + static outcome::result ComputeSubjectId( const Subject &subject ); + static outcome::result CreateNonceSubject( + const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash, + const std::optional &utxo_commitment, + const std::optional &utxo_witness ); + static outcome::result CreateTaskResultSubject( const std::string &account_id, + const std::string &escrow_path, + const std::string &task_result_hash, + uint64_t result_epoch ); + static outcome::result CreateRegistryBatchSubject( const std::string &account_id, + const std::string &base_registry_cid, + uint64_t base_registry_epoch, + uint64_t target_registry_epoch, + uint32_t certificate_count, + const std::string &batch_root ); + static const std::string &BestHash( const std::string &a, const std::string &b ); + outcome::result SubmitProposal( const Proposal &proposal, bool self_vote = true ); + outcome::result SubmitVote( const Vote &vote, bool self_handle = true ); + outcome::result SubmitCertificate( const Certificate &certificate ); + outcome::result ResumeProposalHandling( const std::string &subject_hash ); + void ProcessCertificates(); + void ConfigureCertificateDelay( std::chrono::milliseconds delay ); + + outcome::result GetCertificateBySubjectHash( const std::string &subject_hash ) const; + bool CheckCertificateForSubject( const std::string &subject_hash ) const; + bool CheckCertificateForSubject( const Subject &subject ) const; + + protected: + void ConfigureTimestampWindow( std::chrono::milliseconds window ); + void ConfigureRoundDuration( std::chrono::milliseconds duration ); + void ConfigureRoundSkew( std::chrono::milliseconds skew ); + + private: + explicit ConsensusManager( std::shared_ptr registry, + std::shared_ptr db, + std::shared_ptr pubsub, + Signer signer, + std::string address, + std::string consensus_topic ); + void StartRoundTimer(); + + static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; + static constexpr std::string_view CERTIFICATE_BASE_PATH_KEY = "/cert/"; + static constexpr std::chrono::milliseconds DEFAULT_TIMESTAMP_WINDOW = std::chrono::minutes( 5 ); + static constexpr std::chrono::milliseconds DEFAULT_ROUND_DURATION = std::chrono::milliseconds( 500 ); + static constexpr std::chrono::milliseconds DEFAULT_ROUND_SKEW = std::chrono::milliseconds( 250 ); + static constexpr uint64_t NO_ROUND = std::numeric_limits::max(); + static constexpr std::string_view CERT_KEY_PATTERN = "^/?cert/[^/]+"; + + struct ProposalState + { + Proposal proposal; + std::vector votes; + std::string slot_key; + uint64_t total_weight = 0; + uint64_t approved_weight = 0; + std::unordered_set seen_voters; + bool quorum_reached = false; + uint64_t quorum_reached_ts_ms = 0; + uint64_t last_attempt_round = NO_ROUND; + }; + + struct SlotState + { + std::string best_proposal_id; + std::string best_tx_hash; + bool voted = false; + }; + + void HandleProposal( const Proposal &proposal ); + void HandleVote( const Vote &vote ); + void HandleVoteBundle( const VoteBundle &bundle ); + void HandleCertificate( const Certificate &certificate ); + std::string GetSlotKey( const Proposal &proposal ) const; + bool IsBetterProposal( const Proposal &candidate, const Proposal ¤t ) const; + bool IsTimestampSane( uint64_t timestamp_ms ) const; + bool IsCurrentAggregator( const Proposal &proposal, const ValidatorRegistry::Registry ®istry ) const; + std::vector GetOrderedActiveValidators( const ValidatorRegistry::Registry ®istry ) const; + uint64_t GetCurrentRound( uint64_t proposal_ts_ms ) const; + outcome::result FetchProposalState( const Certificate &certificate ); + ProposalState CreateProposalState( const Certificate &certificate ); + bool ValidateCertificateBestProposal( const ProposalState &state, const Certificate &certificate ) const; + std::vector CollectCertificateVotes( const Certificate &certificate ) const; + void ClearProposalSlot( const Proposal &proposal ); + static outcome::result GetSubjectHash( const Subject &subject ); + void ContinueProposalAfterSubject( const Proposal &proposal ); + void AddPendingProposal( const Proposal &proposal, const std::string &subject_hash ); + std::vector TakePendingProposals( const std::string &subject_hash ); + void AddPendingVote( const Vote &vote ); + std::vector TakePendingVotes( const std::string &proposal_id ); + bool RegisterCertificateFilter(); + std::optional> FilterCertificate( const crdt::pb::Element &element ); + void CertificateReceived( crdt::CRDTCallbackManager::NewDataPair new_data, const std::string &cid ); + void RecoverPendingCertificateWork(); + ConsensusManager::Check ValidateCertificate( const Certificate &certificate ) const; + static std::string CreateProposalId( const Proposal &proposal ); + static bool ValidateSubject( const Subject &subject ); + + void OnConsensusMessage( boost::optional message ); + void UpdateCertificatesPending(); + static bool CheckSubject( const Subject &subject ); + static bool CheckProposal( const Proposal &proposal ); + static bool CheckVote( const Vote &vote ); + static std::string GetPrintableSubjectHash( const Subject &subject ); + std::shared_ptr registry_; + std::shared_ptr db_; + std::shared_ptr certificate_work_journal_; + std::unordered_map subject_handlers_; + mutable std::shared_mutex subject_handlers_mutex_; + std::unordered_map certificate_subject_handlers_; + mutable std::shared_mutex certificate_handlers_mutex_; + Signer signer_; + std::string account_address_; + std::unordered_map proposals_; + std::unordered_map slot_states_; + std::unordered_map pending_proposals_; + std::unordered_map> pending_by_subject_hash_; + std::unordered_map> pending_votes_; + mutable std::mutex proposals_mutex_; + std::shared_ptr pubsub_; + + std::string consensus_messages_topic_; + std::string consensus_datastore_topic_; + std::shared_future> consensus_subs_future_; + std::chrono::milliseconds timestamp_window_{ DEFAULT_TIMESTAMP_WINDOW }; + std::chrono::milliseconds certificate_delay_{ std::chrono::milliseconds( 2000 ) }; + std::chrono::milliseconds round_duration_{ DEFAULT_ROUND_DURATION }; + std::chrono::milliseconds round_skew_{ DEFAULT_ROUND_SKEW }; + std::atomic stop_timer_{ false }; + std::atomic certificates_pending_{ false }; + std::condition_variable timer_cv_; + std::mutex timer_mutex_; + std::thread round_timer_; + }; +} diff --git a/src/blockchain/ConsensusAuth.hpp b/src/blockchain/ConsensusAuth.hpp new file mode 100644 index 000000000..56205c141 --- /dev/null +++ b/src/blockchain/ConsensusAuth.hpp @@ -0,0 +1,101 @@ +/** + * @file ConsensusAuth.hpp + * @brief Header-only helpers for consensus signing and validation. + * @date 2026-02-07 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#pragma once + +#include +#include + +#include "account/GeniusAccount.hpp" +#include "base/hexutil.hpp" +#include "blockchain/impl/proto/Consensus.pb.h" +#include "crypto/hasher/hasher_impl.hpp" +#include +#include "outcome/outcome.hpp" + +namespace sgns +{ + inline outcome::result> ProposalSigningBytes( const ConsensusProposal &proposal ) + { + ConsensusProposal copy = proposal; + copy.clear_signature(); + std::string serialized; + if ( !copy.SerializeToString( &serialized ) ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return std::vector( serialized.begin(), serialized.end() ); + } + + inline outcome::result> VoteSigningBytes( const ConsensusVote &vote ) + { + ConsensusVote copy = vote; + copy.clear_signature(); + std::string serialized; + if ( !copy.SerializeToString( &serialized ) ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return std::vector( serialized.begin(), serialized.end() ); + } + + inline outcome::result> VoteBundleSigningBytes( const ConsensusVoteBundle &bundle ) + { + ConsensusVoteBundle copy = bundle; + copy.clear_signature(); + std::string serialized; + if ( !copy.SerializeToString( &serialized ) ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return std::vector( serialized.begin(), serialized.end() ); + } + + inline outcome::result ComputeProposalId( const ConsensusProposal &proposal ) + { + ConsensusProposal copy = proposal; + copy.clear_proposal_id(); + auto signing_bytes = ProposalSigningBytes( copy ); + if ( signing_bytes.has_error() ) + { + return outcome::failure( signing_bytes.error() ); + } + + sgns::crypto::HasherImpl hasher; + auto hash = hasher.sha2_256( + gsl::span( signing_bytes.value().data(), signing_bytes.value().size() ) ); + return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); + } + + inline bool ValidateProposal( const ConsensusProposal &proposal ) + { + if ( proposal.proposer_id().empty() || proposal.signature().empty() || proposal.proposal_id().empty() ) + { + return false; + } + + auto signing_bytes = ProposalSigningBytes( proposal ); + if ( signing_bytes.has_error() ) + { + return false; + } + + if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), + proposal.signature(), + signing_bytes.value() ) ) + { + return false; + } + + auto computed_id = ComputeProposalId( proposal ); + if ( computed_id.has_error() ) + { + return false; + } + + return computed_id.value() == proposal.proposal_id(); + } +} diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 2d26f8121..f9604438a 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -8,21 +8,29 @@ #include #include +#include +#include #include #include +#include +#include #include #include #include "account/GeniusAccount.hpp" +#include "base/hexutil.hpp" +#include "blockchain/Consensus.hpp" +#include "blockchain/ConsensusAuth.hpp" #include "blockchain/impl/proto/ValidatorRegistry.pb.h" +#include "crypto/hasher/hasher_impl.hpp" #include "crdt/graphsync_dagsyncer.hpp" -namespace sgns::blockchain +namespace sgns { namespace { - base::Logger validator_registry_logger() + base::Logger ValidatorRegistryLogger() { return base::createLogger( "ValidatorRegistry" ); } @@ -33,7 +41,7 @@ namespace sgns::blockchain crdt::pb::Delta delta; if ( !delta.ParseFromArray( buffer.data(), buffer.size() ) ) { - validator_registry_logger()->error( "{}: Failed to parse Delta from IPLD node", __func__ ); + ValidatorRegistryLogger()->error( "{}: Failed to parse Delta from IPLD node", __func__ ); return outcome::failure( std::errc::invalid_argument ); } @@ -43,17 +51,47 @@ namespace sgns::blockchain validator::RegistryUpdate update; if ( !update.ParseFromString( element.value() ) ) { - validator_registry_logger()->error( "{}: Can't parse the registry update {}", - __func__, - element.key() ); + ValidatorRegistryLogger()->error( "{}: Can't parse the registry update {}", + __func__, + element.key() ); return outcome::failure( std::errc::invalid_argument ); } return update.prev_registry_hash(); } - validator_registry_logger()->error( "{}: NO SUCH FILE ", __func__ ); + ValidatorRegistryLogger()->error( "{}: NO SUCH FILE ", __func__ ); return outcome::failure( std::errc::no_such_file_or_directory ); } + + outcome::result ExtractConsensusSubjectHash( const ConsensusSubject &subject ) + { + if ( subject.type() == SubjectType::SUBJECT_NONCE ) + { + if ( !subject.has_nonce() || subject.nonce().tx_hash().empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return subject.nonce().tx_hash(); + } + if ( subject.type() == SubjectType::SUBJECT_TASK_RESULT ) + { + if ( !subject.has_task_result() || subject.task_result().task_result_hash().empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return subject.task_result().task_result_hash(); + } + if ( subject.type() == SubjectType::SUBJECT_REGISTRY_BATCH ) + { + if ( !subject.has_registry_batch() || subject.registry_batch().batch_root().empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return std::string( subject.registry_batch().batch_root() ); + } + return outcome::failure( std::errc::invalid_argument ); + } + } ValidatorRegistry::ValidatorRegistry( std::shared_ptr db, @@ -74,6 +112,17 @@ namespace sgns::blockchain logger_->trace( "{}: constructed", __func__ ); } + ValidatorRegistry::~ValidatorRegistry() + { + const std::string pattern = "/?" + std::string( RegistryKey() ); + if ( db_ ) + { + db_->UnregisterNewElementCallback( pattern ); + db_->UnregisterElementFilter( pattern ); + } + logger_->trace( "{}: destroyed", __func__ ); + } + std::shared_ptr ValidatorRegistry::New( std::shared_ptr db, uint64_t quorum_numerator, uint64_t quorum_denominator, @@ -132,28 +181,28 @@ namespace sgns::blockchain auto new_crdt = new_db->GetCRDTDataStore(); if ( !new_crdt ) { - validator_registry_logger()->error( "{}: Missing broadcaster while migrating Validator CIDs", __func__ ); + ValidatorRegistryLogger()->error( "{}: Missing broadcaster while migrating Validator CIDs", __func__ ); return outcome::failure( std::errc::no_such_device ); } if ( !old_syncer ) { - validator_registry_logger()->error( "{}: Missing DAG syncer while migrating Validator CIDs", __func__ ); + ValidatorRegistryLogger()->error( "{}: Missing DAG syncer while migrating Validator CIDs", __func__ ); return outcome::failure( std::errc::no_such_device ); } auto old_store = old_db->GetDataStore(); auto new_store = new_db->GetDataStore(); - validator_registry_logger()->debug( "{}: Getting the registry CID from the datastore", __func__ ); + ValidatorRegistryLogger()->debug( "{}: Getting the registry CID from the datastore", __func__ ); crdt::GlobalDB::Buffer registry_cid_key; registry_cid_key.put( std::string( RegistryCidKey() ) ); auto registry_cid = old_store->get( registry_cid_key ); if ( registry_cid.has_value() ) { - validator_registry_logger()->debug( "{}: Latest Validator CID: {}", - __func__, - registry_cid.value().toString() ); + ValidatorRegistryLogger()->debug( "{}: Latest Validator CID: {}", + __func__, + registry_cid.value().toString() ); std::vector registry_chain; std::vector> nodes; @@ -168,9 +217,9 @@ namespace sgns::blockchain nodes.push_back( std::move( node ) ); if ( prev_result.has_error() ) { - validator_registry_logger()->error( "{}: Failed to extract previous registry CID from {}", - __func__, - current_cid ); + ValidatorRegistryLogger()->error( "{}: Failed to extract previous registry CID from {}", + __func__, + current_cid ); break; } current_cid = prev_result.value(); @@ -184,9 +233,9 @@ namespace sgns::blockchain { continue; } - validator_registry_logger()->debug( "{}: Adding Validator CID: {}", - __func__, - registry_cid.value().toString() ); + ValidatorRegistryLogger()->debug( "{}: Adding Validator CID: {}", + __func__, + registry_cid.value().toString() ); crdt::GlobalDB::Buffer registry_cid_value; registry_cid_value.put( cid_string ); (void)new_store->put( registry_cid_key, std::move( registry_cid_value ) ); @@ -194,54 +243,54 @@ namespace sgns::blockchain BOOST_OUTCOME_TRY( new_crdt->AddDAGNode( node ) ); } } - validator_registry_logger()->debug( "{}: Finished migrating validator registry: ", __func__ ); + ValidatorRegistryLogger()->debug( "{}: Finished migrating validator registry: ", __func__ ); return outcome::success(); } uint64_t ValidatorRegistry::ComputeWeight( Role role ) const { logger_->trace( "{}: entry role={}", __func__, static_cast( role ) ); - const uint64_t base_weight = weight_config_.base_weight_; - uint64_t multiplier = 1; + uint64_t weight = weight_config_.regular_weight_; + uint64_t cap = weight_config_.regular_max_weight_; switch ( role ) { case Role::GENESIS: - multiplier = weight_config_.genesis_multiplier_; + weight = weight_config_.genesis_weight_; + cap = weight_config_.genesis_max_weight_; break; case Role::FULL: - multiplier = weight_config_.full_multiplier_; + weight = weight_config_.full_weight_; + cap = weight_config_.full_max_weight_; break; case Role::SHARDED: - multiplier = weight_config_.sharded_multiplier_; + weight = weight_config_.sharded_weight_; + cap = weight_config_.sharded_max_weight_; break; case Role::REGULAR: default: - multiplier = 1; break; } - if ( multiplier == 0 ) + if ( weight == 0 ) { - logger_->debug( "{}: multiplier is zero, weight=0", __func__ ); + logger_->debug( "{}: weight is zero", __func__ ); return 0; } - if ( base_weight > weight_config_.max_weight_ / multiplier ) + if ( weight > cap ) { - logger_->debug( "{}: weight clamped to max {}", __func__, weight_config_.max_weight_ ); - return weight_config_.max_weight_; + logger_->debug( "{}: weight clamped to max {}", __func__, cap ); + return cap; } - const uint64_t weighted = base_weight * multiplier; - const uint64_t result = std::min( weighted, weight_config_.max_weight_ ); - logger_->debug( "{}: computed weight={}", __func__, result ); - return result; + logger_->debug( "{}: computed weight={}", __func__, weight ); + return weight; } - uint64_t ValidatorRegistry::TotalWeight( const Registry ®istry ) const + uint64_t ValidatorRegistry::TotalWeight( const Registry ®istry ) { - logger_->trace( "{}: entry validators={}", __func__, registry.validators().size() ); + ValidatorRegistryLogger()->trace( "{}: entry validators={}", __func__, registry.validators().size() ); uint64_t total_weight = 0; for ( const auto &entry : registry.validators() ) { @@ -251,29 +300,32 @@ namespace sgns::blockchain } total_weight += entry.weight(); } - logger_->debug( "{}: total_weight={}", __func__, total_weight ); + ValidatorRegistryLogger()->debug( "{}: total_weight={}", __func__, total_weight ); return total_weight; } uint64_t ValidatorRegistry::QuorumThreshold( uint64_t total_weight ) const { - logger_->trace( "{}: entry total_weight={}", __func__, total_weight ); + ValidatorRegistryLogger()->trace( "{}: entry total_weight={}", __func__, total_weight ); if ( total_weight == 0 ) { - logger_->debug( "{}: total_weight is zero, threshold=0", __func__ ); + ValidatorRegistryLogger()->debug( "{}: total_weight is zero, threshold=0", __func__ ); return 0; } const uint64_t numerator = total_weight * quorum_numerator_; const uint64_t threshold = ( numerator + quorum_denominator_ - 1 ) / quorum_denominator_; - logger_->debug( "{}: threshold={}", __func__, threshold ); + ValidatorRegistryLogger()->debug( "{}: threshold={}", __func__, threshold ); return threshold; } bool ValidatorRegistry::IsQuorum( uint64_t accumulated_weight, uint64_t total_weight ) const { - logger_->trace( "{}: entry accumulated={} total={}", __func__, accumulated_weight, total_weight ); + ValidatorRegistryLogger()->trace( "{}: entry accumulated={} total={}", + __func__, + accumulated_weight, + total_weight ); const bool is_quorum = accumulated_weight >= QuorumThreshold( total_weight ); - logger_->debug( "{}: is_quorum={}", __func__, is_quorum ); + ValidatorRegistryLogger()->debug( "{}: is_quorum={}", __func__, is_quorum ); return is_quorum; } @@ -288,6 +340,8 @@ namespace sgns::blockchain entry->set_role( Role::GENESIS ); entry->set_status( Status::ACTIVE ); entry->set_weight( ComputeWeight( entry->role() ) ); + entry->set_penalty_score( 0 ); + entry->set_missed_epochs( 0 ); logger_->debug( "{}: registry created with weight={}", __func__, entry->weight() ); return registry; } @@ -420,12 +474,10 @@ namespace sgns::blockchain outcome::result ValidatorRegistry::LoadRegistry() const { - logger_->trace( "{}: entry", __func__ ); { std::shared_lock lock( cache_mutex_ ); if ( cached_registry_ ) { - logger_->debug( "{}: returning cached registry", __func__ ); return cached_registry_.value(); } } @@ -440,6 +492,39 @@ namespace sgns::blockchain return update_result.value().registry(); } + outcome::result ValidatorRegistry::LoadRegistry( const std::string &cid ) const + { + ValidatorRegistryLogger()->trace( "{}: entry cid={}", __func__, cid ); + + BOOST_OUTCOME_TRY( auto cid_content, db_->GetCIDContent( cid ) ); + ValidatorRegistryLogger()->trace( "{}: Got CID content with {} entries ", __func__, cid_content.size() ); + crdt::HierarchicalKey registry_key{ std::string( RegistryKey() ) }; + for ( auto &[key, registry_content] : cid_content ) + { + ValidatorRegistryLogger()->trace( "{}: Processing CID content key={}", __func__, key ); + if ( key != registry_key.GetKey() ) + { + ValidatorRegistryLogger()->debug( "{}: Skipping non-registry content key={}, registry_key={}", + __func__, + key, + registry_key.GetKey() ); + continue; + } + std::vector bytes( registry_content.begin(), registry_content.end() ); + auto decoded = DeserializeRegistryUpdate( bytes ); + if ( decoded.has_error() ) + { + ValidatorRegistryLogger()->error( "{}: failed to parse registry update ", __func__ ); + continue; + } + + ValidatorRegistryLogger()->debug( "{}: Grabbing registry from cid {} and key={}", __func__, cid, key ); + return decoded.value().registry(); + } + + return outcome::failure( std::errc::no_such_file_or_directory ); + } + outcome::result ValidatorRegistry::LoadRegistryUpdate() const { logger_->trace( "{}: entry", __func__ ); @@ -456,204 +541,851 @@ namespace sgns::blockchain return outcome::failure( std::errc::no_such_file_or_directory ); } - outcome::result> ValidatorRegistry::GetValidatorWeight( - const std::string &validator_id ) const + outcome::result ValidatorRegistry::CreateUpdateFromCertificate( + const sgns::ConsensusCertificate &certificate ) { - std::shared_lock lock( cache_mutex_ ); - if ( !cache_initialized_ || !cached_registry_ ) + logger_->trace( "{}: entry proposal_id={}", __func__, certificate.proposal_id() ); + auto registry_result = LoadRegistry(); + if ( registry_result.has_error() ) { - return outcome::success( std::optional{} ); + logger_->error( "{}: failed to load registry: {}", __func__, registry_result.error().message() ); + return outcome::failure( registry_result.error() ); } - const auto *validator = FindValidator( cached_registry_.value(), validator_id ); - if ( !validator || validator->status() != Status::ACTIVE ) + auto current_registry = registry_result.value(); + if ( !ValidateCertificateForUpdate( certificate, current_registry ) ) { - return outcome::success( std::optional{} ); + logger_->error( "{}: invalid certificate", __func__ ); + return outcome::failure( std::errc::invalid_argument ); } - return outcome::success( std::optional{ validator->weight() } ); + auto votes = ExtractCertificateVotes( certificate, current_registry ); + + RegistryUpdate update; + update.set_prev_registry_hash( GetRegistryCid() ); + *update.mutable_registry() = BuildRegistryFromCertificate( current_registry, + certificate, + votes.registered_votes, + votes.unregistered_votes ); + + std::string serialized_cert; + if ( !certificate.SerializeToString( &serialized_cert ) ) + { + logger_->error( "{}: failed to serialize certificate", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + update.set_certificate( serialized_cert ); + + logger_->debug( "{}: update created epoch={}", __func__, update.registry().epoch() ); + return update; } - bool ValidatorRegistry::RegisterFilter() + outcome::result ValidatorRegistry::StoreRegistryUpdate( const RegistryUpdate &update ) { - logger_->trace( "{}: entry", __func__ ); - const std::string pattern = "/?" + std::string( RegistryKey() ); - auto weak_self = weak_from_this(); - const bool filter_registered = db_->RegisterElementFilter( - pattern, - [weak_self]( const crdt::pb::Element &element ) -> std::optional> - { - if ( auto strong = weak_self.lock() ) - { - return strong->FilterRegistryUpdate( element ); - } - return std::nullopt; - } ); - const bool callback_registered = db_->RegisterNewElementCallback( - pattern, - [weak_self]( crdt::CRDTCallbackManager::NewDataPair new_data, const std::string &cid ) - { - if ( auto strong = weak_self.lock() ) - { - strong->RegistryUpdateReceived( std::move( new_data ), cid ); - } - } ); + logger_->trace( "{}: entry epoch={}", __func__, update.registry().epoch() ); + auto serialized_update = SerializeRegistryUpdate( update ); + if ( serialized_update.has_error() ) + { + logger_->error( "{}: failed to serialize registry update", __func__ ); + return outcome::failure( serialized_update.error() ); + } - db_->AddListenTopic( std::string( ValidatorTopic() ) ); + base::Buffer update_buffer( + gsl::span( serialized_update.value().data(), serialized_update.value().size() ) ); - const bool result = filter_registered && callback_registered; - logger_->info( "{}: result={}", __func__, result ); - return result; + crdt::HierarchicalKey registry_key{ std::string( RegistryKey() ) }; + auto registry_put = db_->Put( registry_key, update_buffer, { std::string( ValidatorTopic() ) } ); + if ( registry_put.has_error() ) + { + logger_->error( "{}: failed to store registry update in CRDT", __func__ ); + return outcome::failure( registry_put.error() ); + } + + auto cid_string = registry_put.value().toString(); + if ( cid_string.has_value() ) + { + logger_->info( "{}: stored registry update CID {}", __func__, cid_string.value() ); + } + else + { + logger_->error( "{}: registry update stored but CID missing", __func__ ); + } + + logger_->info( "{}: success", __func__ ); + return outcome::success(); } - std::optional> ValidatorRegistry::FilterRegistryUpdate( - const crdt::pb::Element &element ) + outcome::result> ValidatorRegistry::BeginRegistryUpdateTransaction( + const RegistryUpdate &update ) { - logger_->trace( "{}: entry key={}", __func__, element.key() ); - std::vector bytes( element.value().begin(), element.value().end() ); - auto decoded_update = DeserializeRegistryUpdate( bytes ); - if ( decoded_update.has_error() ) + logger_->trace( "{}: entry epoch={}", __func__, update.registry().epoch() ); + auto serialized_update = SerializeRegistryUpdate( update ); + if ( serialized_update.has_error() ) { - logger_->error( "{}: parse failed, rejecting: {}", __func__, element.key() ); - return std::vector{}; + logger_->error( "{}: failed to serialize registry update", __func__ ); + return outcome::failure( serialized_update.error() ); } - RegistryUpdate update = decoded_update.value(); - const Registry *current_ptr = nullptr; + base::Buffer update_buffer( + gsl::span( serialized_update.value().data(), serialized_update.value().size() ) ); + auto tx = db_->BeginTransaction(); + if ( !tx ) { - std::shared_lock lock( cache_mutex_ ); - if ( cached_registry_ ) - { - current_ptr = &cached_registry_.value(); - } + logger_->error( "{}: failed to begin atomic transaction", __func__ ); + return outcome::failure( std::errc::not_enough_memory ); } - if ( !VerifyUpdate( update, current_ptr ) ) + crdt::HierarchicalKey registry_key{ std::string( RegistryKey() ) }; + auto registry_put = tx->Put( registry_key, update_buffer ); + if ( registry_put.has_error() ) { - logger_->error( "{}: verification failed, rejecting: {}", __func__, element.key() ); - return std::vector{}; + logger_->error( "{}: failed to stage registry update in transaction", __func__ ); + return outcome::failure( registry_put.error() ); } - logger_->debug( "{}: update accepted", __func__ ); - return std::nullopt; + logger_->debug( "{}: staged registry update in transaction", __func__ ); + return tx; } - void ValidatorRegistry::RegistryUpdateReceived( const crdt::CRDTCallbackManager::NewDataPair &new_data, - const std::string &cid ) + void ValidatorRegistry::SetMaxNewValidatorsPerUpdate( size_t max_new ) { - logger_->trace( "{}: entry cid={}", __func__, cid ); - const auto &buffer = new_data.second; - auto decoded = DeserializeRegistryUpdate( buffer.toVector() ); - if ( decoded.has_error() ) + logger_->trace( "{}: entry max_new={}", __func__, max_new ); + max_new_validators_per_update_ = max_new; + } + + std::string ValidatorRegistry::GetRegistryCid() const + { + std::shared_lock lock( cache_mutex_ ); + return cached_registry_id_; + } + + uint64_t ValidatorRegistry::GetRegistryEpoch() const + { + std::shared_lock lock( cache_mutex_ ); + if ( cached_registry_ ) { - logger_->error( "{}: failed to parse registry update for cache refresh", __func__ ); - return; + return cached_registry_->epoch(); } + return 0; + } + void ValidatorRegistry::SetCertificatesPerBatch( size_t batch_size ) + { + if ( batch_size == 0 ) { - std::unique_lock lock( cache_mutex_ ); - cached_update_ = decoded.value(); - cached_registry_ = decoded.value().registry(); - cached_registry_id_ = cid; - cache_initialized_ = true; + logger_->warn( "{}: ignored zero batch size", __func__ ); + return; } + std::lock_guard lock( batch_mutex_ ); + certificates_per_batch_ = batch_size; + } - PersistLocalState( cid ); - NotifyInitialized( true ); - logger_->info( "{}: cache updated and initialized", __func__ ); + void ValidatorRegistry::SetBatchSubjectSubmitter( + std::function( const ConsensusSubject &subject )> submitter ) + { + std::lock_guard lock( batch_mutex_ ); + submit_batch_subject_ = std::move( submitter ); } - outcome::result> ValidatorRegistry::ComputeUpdateSigningBytes( - const RegistryUpdate &update ) const + std::string ValidatorRegistry::BuildBatchKey( const std::string &base_registry_cid, uint64_t base_registry_epoch ) { - logger_->trace( "{}: entry validators={}", __func__, update.registry().validators().size() ); - validator::RegistrySigningPayload payload; - *payload.mutable_registry() = update.registry(); - payload.set_prev_registry_hash( update.prev_registry_hash() ); + return base_registry_cid + ":" + std::to_string( base_registry_epoch ); + } - std::string serialized; - if ( !payload.SerializeToString( &serialized ) ) + outcome::result ValidatorRegistry::ComputeBatchRoot( + const std::vector &subject_hashes ) const + { + if ( subject_hashes.empty() ) { - logger_->error( "{}: serialization failed", __func__ ); return outcome::failure( std::errc::invalid_argument ); } - - logger_->debug( "{}: payload size={}", __func__, serialized.size() ); - return std::vector( serialized.begin(), serialized.end() ); + std::string payload; + for ( size_t i = 0; i < subject_hashes.size(); ++i ) + { + if ( i > 0 ) + { + payload.push_back( '\n' ); + } + payload += subject_hashes[i]; + } + sgns::crypto::HasherImpl hasher; + auto hash = hasher.sha2_256( + gsl::span( reinterpret_cast( payload.data() ), payload.size() ) ); + return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); } - bool ValidatorRegistry::VerifyUpdate( const RegistryUpdate &update, const Registry *current_registry ) const + outcome::result> ValidatorRegistry::SelectBatchSubjects( + const std::string &base_registry_cid, + uint64_t base_registry_epoch, + uint32_t certificate_count, + std::optional expected_root ) const { - logger_->trace( "{}: entry validators={}", __func__, update.registry().validators().size() ); - if ( update.registry().validators().empty() ) + if ( certificate_count == 0 ) { - logger_->error( "{}: empty registry update", __func__ ); - return false; + return outcome::failure( std::errc::invalid_argument ); } + std::vector selected; + { + std::lock_guard lock( batch_mutex_ ); + const auto key = BuildBatchKey( base_registry_cid, base_registry_epoch ); + auto it = pending_certificate_subjects_by_base_.find( key ); + if ( it == pending_certificate_subjects_by_base_.end() || + it->second.size() < static_cast( certificate_count ) ) + { + return outcome::failure( std::errc::resource_unavailable_try_again ); + } + selected.assign( it->second.begin(), it->second.end() ); + } + if ( selected.size() > static_cast( certificate_count ) ) + { + selected.resize( certificate_count ); + } + auto root_result = ComputeBatchRoot( selected ); + if ( root_result.has_error() ) + { + return outcome::failure( root_result.error() ); + } + if ( expected_root.has_value() && root_result.value() != expected_root.value() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return selected; + } - auto signing_bytes = ComputeUpdateSigningBytes( update ); - if ( signing_bytes.has_error() ) + outcome::result ValidatorRegistry::LoadCertificateBySubjectHash( + const std::string &subject_hash ) const + { + const auto cert_key = std::string( "/cert/" ) + subject_hash; + auto cert_get = db_->Get( crdt::HierarchicalKey( cert_key ) ); + if ( cert_get.has_error() ) { - logger_->error( "{}: signing bytes computation failed", __func__ ); - return false; + return outcome::failure( cert_get.error() ); } - if ( !current_registry ) + sgns::ConsensusCertificate certificate; + std::string serialized = std::string( cert_get.value().toString() ); + if ( !certificate.ParseFromString( serialized ) ) { - logger_->debug( "{}: verifying genesis update", __func__ ); - if ( update.prev_registry_hash().empty() ) - { - for ( const auto &signature : update.signatures() ) - { - if ( signature.validator_id() != genesis_authority_ ) - { - continue; - } - if ( GeniusAccount::VerifySignature( signature.validator_id(), - signature.signature(), - signing_bytes.value() ) ) - { - logger_->info( "{}: genesis update verified", __func__ ); - return true; - } - } - } - logger_->error( "{}: genesis update verification failed", __func__ ); - return false; + return outcome::failure( std::errc::invalid_argument ); } + return certificate; + } - const std::string prev_registry_cid = update.prev_registry_hash(); - std::string current_id; + void ValidatorRegistry::OnFinalizedCertificate( const sgns::ConsensusCertificate &certificate ) + { + if ( !certificate.has_proposal() ) { - std::shared_lock lock( cache_mutex_ ); - current_id = cached_registry_id_; + return; } - if ( current_id.empty() || prev_registry_cid != current_id ) + if ( certificate.proposal().subject().type() == SubjectType::SUBJECT_REGISTRY_BATCH ) { - //TODO - Check if the CID checking is necessary, because we could receive out-of-order updates - logger_->error( "{}: prev registry CID mismatch", __func__ ); - return false; + return; } - if ( update.registry().epoch() <= current_registry->epoch() ) + auto subject_hash_result = ExtractConsensusSubjectHash( certificate.proposal().subject() ); + if ( subject_hash_result.has_error() ) { - logger_->error( "{}: epoch not increasing", __func__ ); - return false; + return; } - uint64_t total_weight = TotalWeight( *current_registry ); - uint64_t accumulated_weight = 0; - std::set seen; - - for ( const auto &signature : update.signatures() ) + const auto key = BuildBatchKey( certificate.registry_cid(), certificate.registry_epoch() ); { - if ( !seen.insert( signature.validator_id() ).second ) - { - continue; - } + std::lock_guard lock( batch_mutex_ ); + pending_certificate_subjects_by_base_[key].insert( subject_hash_result.value() ); + } - const auto *validator = FindValidator( *current_registry, signature.validator_id() ); + (void)TryCreateAndSubmitBatchProposal( certificate.registry_cid(), certificate.registry_epoch() ); + } + + outcome::result ValidatorRegistry::TryCreateAndSubmitBatchProposal( const std::string &base_registry_cid, + uint64_t base_registry_epoch ) + { + std::function( const ConsensusSubject &subject )> submitter; + size_t threshold = 0; + { + std::lock_guard lock( batch_mutex_ ); + submitter = submit_batch_subject_; + threshold = certificates_per_batch_; + } + if ( !submitter || threshold == 0 ) + { + return outcome::success(); + } + + if ( GetRegistryCid() != base_registry_cid || GetRegistryEpoch() != base_registry_epoch ) + { + return outcome::failure( std::errc::operation_canceled ); + } + + auto selected_result = SelectBatchSubjects( base_registry_cid, + base_registry_epoch, + static_cast( threshold ), + std::nullopt ); + if ( selected_result.has_error() ) + { + return outcome::failure( selected_result.error() ); + } + + auto root_result = ComputeBatchRoot( selected_result.value() ); + if ( root_result.has_error() ) + { + return outcome::failure( root_result.error() ); + } + + auto subject_result = ConsensusManager::CreateRegistryBatchSubject( genesis_authority_, + base_registry_cid, + base_registry_epoch, + base_registry_epoch + 1, + static_cast( threshold ), + root_result.value() ); + if ( subject_result.has_error() ) + { + return outcome::failure( subject_result.error() ); + } + + { + std::lock_guard lock( batch_mutex_ ); + auto batch_hash_result = ExtractConsensusSubjectHash( subject_result.value() ); + if ( batch_hash_result.has_error() ) + { + return outcome::failure( batch_hash_result.error() ); + } + if ( pending_batch_subject_ids_.find( batch_hash_result.value() ) != pending_batch_subject_ids_.end() ) + { + return outcome::success(); + } + pending_batch_subject_ids_.insert( batch_hash_result.value() ); + } + + return submitter( subject_result.value() ); + } + + outcome::result ValidatorRegistry::EvaluateBatchSubject( + const ConsensusSubject &subject ) + { + if ( subject.type() != SubjectType::SUBJECT_REGISTRY_BATCH || !subject.has_registry_batch() ) + { + return outcome::success( BatchSubjectDecision::Reject ); + } + + const auto &payload = subject.registry_batch(); + auto selected_result = SelectBatchSubjects( payload.base_registry_cid(), + payload.base_registry_epoch(), + payload.certificate_count(), + std::string( payload.batch_root() ) ); + if ( selected_result.has_error() ) + { + if ( selected_result.error() == std::errc::resource_unavailable_try_again ) + { + return outcome::success( BatchSubjectDecision::Pending ); + } + return outcome::success( BatchSubjectDecision::Reject ); + } + + auto registry_result = LoadRegistry(); + if ( registry_result.has_error() ) + { + return outcome::success( BatchSubjectDecision::Pending ); + } + + if ( registry_result.value().epoch() != payload.base_registry_epoch() || + GetRegistryCid() != payload.base_registry_cid() ) + { + return outcome::success( BatchSubjectDecision::Reject ); + } + + return outcome::success( BatchSubjectDecision::Approve ); + } + + outcome::result ValidatorRegistry::HandleBatchCertificate( + const std::string &subject_hash, + const sgns::ConsensusCertificate &certificate ) + { + { + std::lock_guard lock( batch_mutex_ ); + if ( finalized_batch_subject_ids_.find( subject_hash ) != finalized_batch_subject_ids_.end() ) + { + return BatchCertificateDecision::Approve; + } + if ( applying_batch_subject_ids_.find( subject_hash ) != applying_batch_subject_ids_.end() ) + { + return BatchCertificateDecision::Approve; + } + applying_batch_subject_ids_.insert( subject_hash ); + } + if ( !certificate.has_proposal() || !certificate.proposal().has_subject() || + certificate.proposal().subject().type() != SubjectType::SUBJECT_REGISTRY_BATCH || + !certificate.proposal().subject().has_registry_batch() ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return BatchCertificateDecision::Reject; + } + + auto current_registry_result = LoadRegistry(); + if ( current_registry_result.has_error() ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return BatchCertificateDecision::Reject; + } + if ( !ValidateCertificate( certificate, current_registry_result.value() ) ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return BatchCertificateDecision::Reject; + } + + const auto &payload = certificate.proposal().subject().registry_batch(); + auto selected_result = SelectBatchSubjects( payload.base_registry_cid(), + payload.base_registry_epoch(), + payload.certificate_count(), + std::string( payload.batch_root() ) ); + if ( selected_result.has_error() ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return BatchCertificateDecision::Reject; + } + + auto base_registry_result = LoadRegistry( payload.base_registry_cid() ); + if ( base_registry_result.has_error() ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return BatchCertificateDecision::Stalled; + } + + std::vector certificates; + certificates.reserve( selected_result.value().size() ); + for ( const auto &tx_subject_hash : selected_result.value() ) + { + auto cert_result = LoadCertificateBySubjectHash( tx_subject_hash ); + if ( cert_result.has_error() ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return BatchCertificateDecision::Reject; + } + certificates.push_back( cert_result.value() ); + } + + std::unordered_map registered_scores; + std::unordered_map unregistered_scores; + for ( const auto &tx_cert : certificates ) + { + auto votes = ExtractCertificateVotes( tx_cert, base_registry_result.value() ); + for ( const auto &[validator_id, approve] : votes.registered_votes ) + { + registered_scores[validator_id] += approve ? 1 : -1; + } + for ( const auto &[validator_id, approve] : votes.unregistered_votes ) + { + unregistered_scores[validator_id] += approve ? 1 : -1; + } + } + + std::unordered_map registered_votes; + std::unordered_map unregistered_votes; + for ( const auto &[validator_id, score] : registered_scores ) + { + registered_votes[validator_id] = score >= 0; + } + for ( const auto &[validator_id, score] : unregistered_scores ) + { + unregistered_votes[validator_id] = score >= 0; + } + + RegistryUpdate update; + update.set_prev_registry_hash( payload.base_registry_cid() ); + *update.mutable_registry() = BuildRegistryFromAggregatedVotes( base_registry_result.value(), + registered_votes, + unregistered_votes ); + std::string serialized_cert; + if ( !certificate.SerializeToString( &serialized_cert ) ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return outcome::failure( std::errc::invalid_argument ); + } + update.set_certificate( serialized_cert ); + for ( const auto &tx_subject_hash : selected_result.value() ) + { + update.add_batch_certificate_subject_hashes( tx_subject_hash ); + } + + std::thread( + [weak_self = weak_from_this(), subject_hash, update = std::move( update )]() mutable + { + auto self = weak_self.lock(); + if ( !self ) + { + return; + } + auto store_result = self->StoreRegistryUpdate( update ); + std::lock_guard lock( self->batch_mutex_ ); + self->applying_batch_subject_ids_.erase( subject_hash ); + if ( store_result.has_error() ) + { + self->logger_->error( "{}: failed storing batch registry update subject_hash={} error={}", + __func__, + subject_hash.substr( 0, 8 ), + store_result.error().message() ); + return; + } + self->pending_batch_subject_ids_.erase( subject_hash ); + self->finalized_batch_subject_ids_.insert( subject_hash ); + } ) + .detach(); + return BatchCertificateDecision::Approve; + } + + outcome::result> ValidatorRegistry::GetValidatorWeight( + const std::string &validator_id ) const + { + std::shared_lock lock( cache_mutex_ ); + if ( !cache_initialized_ || !cached_registry_ ) + { + return outcome::success( std::optional{} ); + } + + const auto *validator = FindValidator( cached_registry_.value(), validator_id ); + if ( !validator || validator->status() != Status::ACTIVE ) + { + return outcome::success( std::optional{} ); + } + + return outcome::success( std::optional{ validator->weight() } ); + } + + bool ValidatorRegistry::RegisterFilter() + { + logger_->trace( "{}: entry", __func__ ); + const std::string pattern = "/?" + std::string( RegistryKey() ); + auto weak_self = weak_from_this(); + const bool filter_registered = db_->RegisterElementFilter( + pattern, + [weak_self]( const crdt::pb::Element &element ) -> std::optional> + { + if ( auto strong = weak_self.lock() ) + { + return strong->FilterRegistryUpdate( element ); + } + return std::nullopt; + } ); + const bool callback_registered = db_->RegisterNewElementCallback( + pattern, + [weak_self]( crdt::CRDTCallbackManager::NewDataPair new_data, const std::string &cid ) + { + if ( auto strong = weak_self.lock() ) + { + strong->RegistryUpdateReceived( std::move( new_data ), cid ); + } + } ); + + db_->AddListenTopic( std::string( ValidatorTopic() ) ); + + const bool result = filter_registered && callback_registered; + logger_->info( "{}: result={}", __func__, result ); + return result; + } + + std::optional> ValidatorRegistry::FilterRegistryUpdate( + const crdt::pb::Element &element ) + { + logger_->trace( "{}: entry key={}", __func__, element.key() ); + std::vector bytes( element.value().begin(), element.value().end() ); + auto decoded_update = DeserializeRegistryUpdate( bytes ); + if ( decoded_update.has_error() ) + { + logger_->error( "{}: parse failed, rejecting: {}", __func__, element.key() ); + return std::vector{}; + } + + RegistryUpdate update = decoded_update.value(); + const Registry *current_ptr = nullptr; + + { + std::shared_lock lock( cache_mutex_ ); + if ( cached_registry_ ) + { + current_ptr = &cached_registry_.value(); + } + } + + if ( !VerifyUpdate( update, current_ptr, false ) ) + { + logger_->error( "{}: verification failed, rejecting: {}", __func__, element.key() ); + return std::vector{}; + } + + logger_->debug( "{}: update accepted", __func__ ); + return std::nullopt; + } + + void ValidatorRegistry::RegistryUpdateReceived( const crdt::CRDTCallbackManager::NewDataPair &new_data, + const std::string &cid ) + { + logger_->trace( "{}: entry cid={}", __func__, cid ); + const auto &buffer = new_data.second; + auto decoded = DeserializeRegistryUpdate( buffer.toVector() ); + if ( decoded.has_error() ) + { + logger_->error( "{}: failed to parse registry update for cache refresh", __func__ ); + return; + } + + { + std::unique_lock lock( cache_mutex_ ); + cached_update_ = decoded.value(); + cached_registry_ = decoded.value().registry(); + cached_registry_id_ = cid; + cache_initialized_ = true; + } + + PersistLocalState( cid ); + NotifyInitialized( true ); + logger_->info( "{}: cache updated and initialized", __func__ ); + } + + outcome::result> ValidatorRegistry::ComputeUpdateSigningBytes( + const RegistryUpdate &update ) const + { + logger_->trace( "{}: entry validators={}", __func__, update.registry().validators().size() ); + validator::RegistrySigningPayload payload; + *payload.mutable_registry() = update.registry(); + payload.set_prev_registry_hash( update.prev_registry_hash() ); + + std::string serialized; + if ( !payload.SerializeToString( &serialized ) ) + { + logger_->error( "{}: serialization failed", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + logger_->debug( "{}: payload size={}", __func__, serialized.size() ); + return std::vector( serialized.begin(), serialized.end() ); + } + + bool ValidatorRegistry::VerifyUpdate( const RegistryUpdate &update, + const Registry *current_registry, + bool enforce_time_window ) const + { + logger_->trace( "{}: entry validators={}", __func__, update.registry().validators().size() ); + if ( update.registry().validators().empty() ) + { + logger_->error( "{}: empty registry update", __func__ ); + return false; + } + + auto signing_bytes = ComputeUpdateSigningBytes( update ); + if ( signing_bytes.has_error() ) + { + logger_->error( "{}: signing bytes computation failed", __func__ ); + return false; + } + + if ( !current_registry ) + { + logger_->debug( "{}: verifying genesis update", __func__ ); + if ( update.prev_registry_hash().empty() ) + { + for ( const auto &signature : update.signatures() ) + { + if ( signature.validator_id() != genesis_authority_ ) + { + continue; + } + if ( GeniusAccount::VerifySignature( signature.validator_id(), + signature.signature(), + signing_bytes.value() ) ) + { + logger_->info( "{}: genesis update verified", __func__ ); + return true; + } + } + } + logger_->error( "{}: genesis update verification failed", __func__ ); + return false; + } + + if ( !update.certificate().empty() ) + { + sgns::ConsensusCertificate certificate; + if ( !certificate.ParseFromString( update.certificate() ) ) + { + logger_->error( "{}: invalid certificate payload", __func__ ); + return false; + } + + if ( enforce_time_window ) + { + if ( !ValidateCertificateForUpdate( certificate, *current_registry ) ) + { + logger_->error( "{}: certificate verification failed", __func__ ); + return false; + } + } + else + { + if ( !ValidateCertificate( certificate, *current_registry ) ) + { + logger_->error( "{}: certificate verification failed", __func__ ); + return false; + } + } + + Registry expected; + if ( certificate.has_proposal() && certificate.proposal().has_subject() && + certificate.proposal().subject().type() == SubjectType::SUBJECT_REGISTRY_BATCH && + certificate.proposal().subject().has_registry_batch() ) + { + const auto &payload = certificate.proposal().subject().registry_batch(); + if ( payload.base_registry_cid() != update.prev_registry_hash() || + payload.base_registry_epoch() != current_registry->epoch() || + payload.target_registry_epoch() != current_registry->epoch() + 1 ) + { + logger_->error( "{}: batch subject metadata mismatch", __func__ ); + return false; + } + if ( update.batch_certificate_subject_hashes_size() != static_cast( payload.certificate_count() ) ) + { + logger_->error( "{}: batch subject certificate count mismatch", __func__ ); + return false; + } + std::vector subject_hashes; + subject_hashes.reserve( static_cast( update.batch_certificate_subject_hashes_size() ) ); + for ( const auto &subject_hash : update.batch_certificate_subject_hashes() ) + { + subject_hashes.push_back( subject_hash ); + } + std::sort( subject_hashes.begin(), subject_hashes.end() ); + auto root_result = ComputeBatchRoot( subject_hashes ); + if ( root_result.has_error() ) + { + return false; + } + const auto payload_root = std::string( payload.batch_root() ); + if ( payload_root != root_result.value() ) + { + logger_->error( "{}: batch root mismatch", __func__ ); + return false; + } + + std::unordered_map registered_scores; + std::unordered_map unregistered_scores; + for ( const auto &subject_hash : subject_hashes ) + { + auto certificate_result = LoadCertificateBySubjectHash( subject_hash ); + if ( certificate_result.has_error() ) + { + logger_->error( "{}: missing certificate for batch hash={}", + __func__, + subject_hash.substr( 0, 8 ) ); + return false; + } + const auto &tx_cert = certificate_result.value(); + if ( tx_cert.registry_cid() != payload.base_registry_cid() || + tx_cert.registry_epoch() != payload.base_registry_epoch() ) + { + logger_->error( "{}: batch certificate registry mismatch", __func__ ); + return false; + } + auto votes = ExtractCertificateVotes( tx_cert, *current_registry ); + for ( const auto &[validator_id, approve] : votes.registered_votes ) + { + registered_scores[validator_id] += approve ? 1 : -1; + } + for ( const auto &[validator_id, approve] : votes.unregistered_votes ) + { + unregistered_scores[validator_id] += approve ? 1 : -1; + } + } + + std::unordered_map registered_votes; + std::unordered_map unregistered_votes; + for ( const auto &[validator_id, score] : registered_scores ) + { + registered_votes[validator_id] = score >= 0; + } + for ( const auto &[validator_id, score] : unregistered_scores ) + { + unregistered_votes[validator_id] = score >= 0; + } + expected = BuildRegistryFromAggregatedVotes( *current_registry, registered_votes, unregistered_votes ); + } + else + { + auto votes = ExtractCertificateVotes( certificate, *current_registry ); + expected = BuildRegistryFromCertificate( *current_registry, + certificate, + votes.registered_votes, + votes.unregistered_votes ); + } + Registry provided = update.registry(); + NormalizeRegistry( provided ); + NormalizeRegistry( expected ); + + if ( provided.epoch() != current_registry->epoch() + 1 ) + { + logger_->error( "{}: epoch not next expected", __func__ ); + return false; + } + + if ( provided.SerializeAsString() != expected.SerializeAsString() ) + { + logger_->error( "{}: registry mismatch against certificate", __func__ ); + return false; + } + + const std::string prev_registry_cid = update.prev_registry_hash(); + std::string current_id; + { + std::shared_lock lock( cache_mutex_ ); + current_id = cached_registry_id_; + } + if ( current_id.empty() || prev_registry_cid != current_id ) + { + logger_->error( "{}: prev registry CID mismatch", __func__ ); + return false; + } + + logger_->info( "{}: certificate-based update verified", __func__ ); + return true; + } + + const std::string prev_registry_cid = update.prev_registry_hash(); + std::string current_id; + { + std::shared_lock lock( cache_mutex_ ); + current_id = cached_registry_id_; + } + if ( current_id.empty() || prev_registry_cid != current_id ) + { + //TODO - Check if the CID checking is necessary, because we could receive out-of-order updates + logger_->error( "{}: prev registry CID mismatch", __func__ ); + return false; + } + + if ( update.registry().epoch() != current_registry->epoch() + 1 ) + { + logger_->error( "{}: epoch not next expected", __func__ ); + return false; + } + + uint64_t total_weight = TotalWeight( *current_registry ); + uint64_t accumulated_weight = 0; + std::set seen; + + for ( const auto &signature : update.signatures() ) + { + if ( !seen.insert( signature.validator_id() ).second ) + { + continue; + } + + const auto *validator = FindValidator( *current_registry, signature.validator_id() ); if ( !validator || validator->status() != Status::ACTIVE ) { continue; @@ -678,19 +1410,570 @@ namespace sgns::blockchain return false; } + bool ValidatorRegistry::ValidateCertificate( const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry ) const + { + logger_->trace( "{}: entry proposal_id={}", __func__, certificate.proposal_id() ); + if ( !certificate.has_proposal() ) + { + logger_->error( "{}: missing proposal in certificate", __func__ ); + return false; + } + + const auto &proposal = certificate.proposal(); + if ( !ValidateProposal( proposal ) ) + { + logger_->error( "{}: invalid proposal signature", __func__ ); + return false; + } + if ( proposal.proposal_id() != certificate.proposal_id() ) + { + logger_->error( "{}: proposal_id mismatch cert={} proposal={}", + __func__, + certificate.proposal_id(), + proposal.proposal_id() ); + return false; + } + if ( proposal.registry_epoch() != certificate.registry_epoch() || + proposal.registry_cid() != certificate.registry_cid() ) + { + logger_->error( "{}: registry metadata mismatch proposal_id={}", __func__, proposal.proposal_id() ); + return false; + } + if ( proposal.registry_epoch() != current_registry.epoch() ) + { + logger_->error( "{}: registry epoch mismatch cert={} registry={}", + __func__, + proposal.registry_epoch(), + current_registry.epoch() ); + return false; + } + + const std::string current_id = GetRegistryCid(); + if ( !current_id.empty() && !proposal.registry_cid().empty() && proposal.registry_cid() != current_id ) + { + logger_->error( "{}: registry CID mismatch cert={} registry={}", + __func__, + proposal.registry_cid(), + current_id ); + return false; + } + + if ( certificate.proposal_id().empty() ) + { + logger_->error( "{}: empty proposal_id", __func__ ); + return false; + } + + return true; + } + + bool ValidatorRegistry::ValidateCertificateForUpdate( const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry ) const + { + const uint64_t window_ms = weight_config_.certificate_timestamp_window_ms_; + if ( window_ms > 0 ) + { + const auto now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count(); + const auto cert_ms = static_cast( certificate.timestamp() ); + const auto diff = std::llabs( now_ms - cert_ms ); + if ( cert_ms == 0 || static_cast( diff ) > window_ms ) + { + logger_->error( "{}: certificate timestamp outside window", __func__ ); + return false; + } + } + return ValidateCertificate( certificate, current_registry ); + } + + ValidatorRegistry::CertificateVotes ValidatorRegistry::ExtractCertificateVotes( + const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry ) const + { + CertificateVotes result; + uint64_t total_weight = TotalWeight( current_registry ); + uint64_t approved_weight = 0; + std::unordered_set seen; + + for ( const auto &vote : certificate.votes() ) + { + if ( vote.proposal_id() != certificate.proposal_id() ) + { + continue; + } + if ( !seen.insert( vote.voter_id() ).second ) + { + continue; + } + + auto signing_bytes = VoteSigningBytes( vote ); + if ( signing_bytes.has_error() ) + { + continue; + } + + if ( !GeniusAccount::VerifySignature( vote.voter_id(), vote.signature(), signing_bytes.value() ) ) + { + continue; + } + const auto *validator = FindValidator( current_registry, vote.voter_id() ); + if ( !validator ) + { + result.unregistered.insert( vote.voter_id() ); + result.unregistered_votes[vote.voter_id()] = vote.approve(); + continue; + } + + result.registered_votes[vote.voter_id()] = vote.approve(); + + if ( vote.approve() && validator->status() == Status::ACTIVE ) + { + approved_weight += validator->weight(); + result.approved.insert( vote.voter_id() ); + } + } + + if ( !IsQuorum( approved_weight, total_weight ) ) + { + logger_->error( "{}: quorum not reached approved={} total={}", __func__, approved_weight, total_weight ); + result.approved.clear(); + result.unregistered.clear(); + result.registered_votes.clear(); + result.unregistered_votes.clear(); + return result; + } + + logger_->debug( "{}: quorum verified approved={} total={}", __func__, approved_weight, total_weight ); + return result; + } + + ValidatorRegistry::Registry ValidatorRegistry::BuildRegistryFromCertificate( + const Registry ¤t_registry, + const sgns::ConsensusCertificate &certificate, + const std::unordered_map ®istered_votes, + const std::unordered_map &unregistered_votes ) const + { + logger_->debug( + "{}: building registry update proposal_id={} epoch={} current_validators={} registered_votes={} unregistered_votes={}", + __func__, + certificate.proposal_id().substr( 0, 8 ), + current_registry.epoch(), + current_registry.validators_size(), + registered_votes.size(), + unregistered_votes.size() ); + if ( !unregistered_votes.empty() ) + { + std::vector unregistered_ids; + unregistered_ids.reserve( unregistered_votes.size() ); + for ( const auto &pair : unregistered_votes ) + { + unregistered_ids.push_back( pair.first.substr( 0, 8 ) ); + } + std::sort( unregistered_ids.begin(), unregistered_ids.end() ); + logger_->debug( "{}: unregistered voter ids (prefixes)={}", __func__, fmt::join( unregistered_ids, "," ) ); + } + + Registry next = current_registry; + next.set_epoch( current_registry.epoch() + 1 ); + + const int before_count = next.validators_size(); + InsertNewValidators( next, unregistered_votes ); + const int after_insert = next.validators_size(); + if ( after_insert > before_count ) + { + std::vector new_ids; + new_ids.reserve( static_cast( after_insert - before_count ) ); + for ( const auto &entry : next.validators() ) + { + if ( !FindValidator( current_registry, entry.validator_id() ) ) + { + new_ids.push_back( entry.validator_id().substr( 0, 8 ) ); + } + } + std::sort( new_ids.begin(), new_ids.end() ); + logger_->debug( "{}: inserted {} new validators (prefixes)={}", + __func__, + new_ids.size(), + fmt::join( new_ids, "," ) ); + } + + std::vector entries; + entries.reserve( static_cast( next.validators_size() ) ); + for ( const auto &entry : next.validators() ) + { + entries.push_back( entry ); + } + + ApplyVoteEffects( entries, registered_votes ); + std::unordered_set participants; + participants.reserve( registered_votes.size() + unregistered_votes.size() ); + for ( const auto &pair : registered_votes ) + { + participants.insert( pair.first ); + } + for ( const auto &pair : unregistered_votes ) + { + participants.insert( pair.first ); + } + ApplyInactivityDecay( entries, participants ); + ApplyTotalWeightCap( entries ); + + std::sort( entries.begin(), + entries.end(), + []( const ValidatorEntry &a, const ValidatorEntry &b ) + { return a.validator_id() < b.validator_id(); } ); + + next.clear_validators(); + for ( const auto &entry : entries ) + { + *next.add_validators() = entry; + } + + logger_->debug( "{}: built registry from certificate proposal_id={} epoch={} validators={}", + __func__, + certificate.proposal_id().substr( 0, 8 ), + next.epoch(), + next.validators_size() ); + return next; + } + + ValidatorRegistry::Registry ValidatorRegistry::BuildRegistryFromAggregatedVotes( + const Registry ¤t_registry, + const std::unordered_map ®istered_votes, + const std::unordered_map &unregistered_votes ) const + { + Registry next = current_registry; + next.set_epoch( current_registry.epoch() + 1 ); + + InsertNewValidators( next, unregistered_votes ); + + std::vector entries; + entries.reserve( static_cast( next.validators_size() ) ); + for ( const auto &entry : next.validators() ) + { + entries.push_back( entry ); + } + + ApplyVoteEffects( entries, registered_votes ); + std::unordered_set participants; + participants.reserve( registered_votes.size() + unregistered_votes.size() ); + for ( const auto &pair : registered_votes ) + { + participants.insert( pair.first ); + } + for ( const auto &pair : unregistered_votes ) + { + participants.insert( pair.first ); + } + ApplyInactivityDecay( entries, participants ); + ApplyTotalWeightCap( entries ); + + std::sort( entries.begin(), + entries.end(), + []( const ValidatorEntry &a, const ValidatorEntry &b ) + { return a.validator_id() < b.validator_id(); } ); + + next.clear_validators(); + for ( const auto &entry : entries ) + { + *next.add_validators() = entry; + } + return next; + } + + void ValidatorRegistry::InsertNewValidators( Registry ®istry, + const std::unordered_map &unregistered_votes ) const + { + std::vector new_ids; + new_ids.reserve( unregistered_votes.size() ); + for ( const auto &pair : unregistered_votes ) + { + new_ids.push_back( pair.first ); + } + std::sort( new_ids.begin(), new_ids.end() ); + size_t added = 0; + for ( const auto &validator_id : new_ids ) + { + if ( added >= max_new_validators_per_update_ ) + { + logger_->debug( "{}: new validator cap reached {}", __func__, max_new_validators_per_update_ ); + break; + } + if ( FindValidator( registry, validator_id ) ) + { + continue; + } + auto *entry = registry.add_validators(); + entry->set_validator_id( validator_id ); + entry->set_role( Role::REGULAR ); + entry->set_status( Status::ACTIVE ); + entry->set_weight( ComputeWeight( entry->role() ) ); + auto it = unregistered_votes.find( validator_id ); + const bool approve = ( it != unregistered_votes.end() ) ? it->second : true; + entry->set_penalty_score( approve ? 0 : 1 ); + entry->set_missed_epochs( 0 ); + logger_->debug( "{}: added validator id={} weight={} approve={} penalty={} status={}", + __func__, + validator_id.substr( 0, 8 ), + entry->weight(), + approve, + entry->penalty_score(), + static_cast( entry->status() ) ); + ++added; + } + } + + void ValidatorRegistry::ApplyVoteEffects( std::vector &entries, + const std::unordered_map ®istered_votes ) const + { + for ( auto &entry : entries ) + { + auto vote_it = registered_votes.find( entry.validator_id() ); + if ( vote_it == registered_votes.end() ) + { + continue; + } + + const bool approve = vote_it->second; + uint32_t penalty = static_cast( entry.penalty_score() ); + const uint32_t cap = weight_config_.penalty_cap_; + const uint64_t old_weight = entry.weight(); + const uint32_t old_penalty = penalty; + const auto old_status = entry.status(); + entry.set_missed_epochs( 0 ); + + if ( approve ) + { + if ( penalty > 0 ) + { + penalty -= 1; + } + entry.set_penalty_score( penalty ); + + if ( entry.status() == Status::ACTIVE ) + { + const uint64_t increment = weight_config_.approval_increment_; + if ( increment > 0 ) + { + uint64_t role_cap = weight_config_.regular_max_weight_; + switch ( entry.role() ) + { + case Role::GENESIS: + role_cap = weight_config_.genesis_max_weight_; + break; + case Role::FULL: + role_cap = weight_config_.full_max_weight_; + break; + case Role::SHARDED: + role_cap = weight_config_.sharded_max_weight_; + break; + case Role::REGULAR: + default: + role_cap = weight_config_.regular_max_weight_; + break; + } + const uint64_t clamped = std::min( entry.weight() + increment, role_cap ); + entry.set_weight( clamped ); + } + } + else if ( penalty == 0 ) + { + entry.set_status( Status::ACTIVE ); + } + } + else + { + if ( entry.status() == Status::BLACKLISTED ) + { + const uint32_t bumped = std::min( + cap, + static_cast( penalty + weight_config_.blacklist_bump_ ) ); + penalty = bumped; + } + else + { + if ( penalty < cap ) + { + penalty += 1; + } + if ( penalty >= weight_config_.penalty_threshold_ ) + { + entry.set_status( Status::BLACKLISTED ); + const uint32_t bumped = std::min( + cap, + static_cast( penalty + weight_config_.blacklist_bump_ ) ); + penalty = bumped; + } + } + entry.set_penalty_score( penalty ); + } + + logger_->debug( "{}: vote effect id={} approve={} weight {}->{} penalty {}->{} status {}->{}", + __func__, + entry.validator_id().substr( 0, 8 ), + approve, + old_weight, + entry.weight(), + old_penalty, + entry.penalty_score(), + static_cast( old_status ), + static_cast( entry.status() ) ); + } + } + + void ValidatorRegistry::ApplyInactivityDecay( std::vector &entries, + const std::unordered_set &participants ) const + { + for ( auto &entry : entries ) + { + if ( entry.status() != Status::ACTIVE ) + { + continue; + } + if ( participants.find( entry.validator_id() ) != participants.end() ) + { + continue; + } + uint32_t missed = static_cast( entry.missed_epochs() ); + if ( missed < std::numeric_limits::max() ) + { + missed += 1; + } + entry.set_missed_epochs( missed ); + + if ( missed >= weight_config_.missed_epoch_threshold_ ) + { + const uint32_t dec = weight_config_.inactivity_decrement_; + if ( dec > 0 && entry.weight() > 0 ) + { + const uint64_t old_weight = entry.weight(); + const uint64_t new_weight = ( entry.weight() > dec ) ? ( entry.weight() - dec ) : 0; + entry.set_weight( new_weight ); + if ( new_weight == 0 ) + { + entry.set_status( Status::SUSPENDED ); + } + logger_->debug( "{}: inactivity decay id={} missed={} weight {}->{} status={}", + __func__, + entry.validator_id().substr( 0, 8 ), + missed, + old_weight, + new_weight, + static_cast( entry.status() ) ); + } + } + } + } + + void ValidatorRegistry::ApplyTotalWeightCap( std::vector &entries ) const + { + uint64_t total_active = 0; + for ( const auto &entry : entries ) + { + if ( entry.status() == Status::ACTIVE ) + { + total_active += entry.weight(); + } + } + + const uint64_t weight_cap = weight_config_.genesis_weight_ * weight_config_.total_weight_cap_multiplier_; + if ( weight_cap == 0 || total_active <= weight_cap ) + { + return; + } + + logger_->debug( "{}: applying total weight cap total_active={} cap={}", __func__, total_active, weight_cap ); + + uint64_t scaled_sum = 0; + std::vector active_indices; + active_indices.reserve( entries.size() ); + for ( size_t i = 0; i < entries.size(); ++i ) + { + if ( entries[i].status() != Status::ACTIVE ) + { + continue; + } + const uint64_t old_weight = entries[i].weight(); + const uint64_t scaled = ( entries[i].weight() * weight_cap ) / total_active; + entries[i].set_weight( scaled ); + scaled_sum += scaled; + active_indices.push_back( i ); + logger_->debug( "{}: cap scale id={} weight {}->{}", + __func__, + entries[i].validator_id().substr( 0, 8 ), + old_weight, + scaled ); + } + + uint64_t remainder = ( scaled_sum <= weight_cap ) ? ( weight_cap - scaled_sum ) : 0; + if ( remainder == 0 || active_indices.empty() ) + { + return; + } + + std::sort( active_indices.begin(), + active_indices.end(), + [&entries]( size_t a, size_t b ) + { + if ( entries[a].weight() != entries[b].weight() ) + { + return entries[a].weight() > entries[b].weight(); + } + return entries[a].validator_id() < entries[b].validator_id(); + } ); + size_t idx = 0; + while ( remainder > 0 ) + { + entries[active_indices[idx]].set_weight( entries[active_indices[idx]].weight() + 1 ); + remainder -= 1; + idx = ( idx + 1 ) % active_indices.size(); + } + + for ( const auto active_idx : active_indices ) + { + logger_->debug( "{}: cap final id={} weight={}", + __func__, + entries[active_idx].validator_id().substr( 0, 8 ), + entries[active_idx].weight() ); + } + } + + void ValidatorRegistry::NormalizeRegistry( Registry ®istry ) + { + std::vector entries; + entries.reserve( static_cast( registry.validators_size() ) ); + for ( const auto &entry : registry.validators() ) + { + entries.push_back( entry ); + } + + std::sort( entries.begin(), + entries.end(), + []( const ValidatorEntry &a, const ValidatorEntry &b ) + { return a.validator_id() < b.validator_id(); } ); + + registry.clear_validators(); + for ( const auto &entry : entries ) + { + *registry.add_validators() = entry; + } + } + const ValidatorRegistry::ValidatorEntry *ValidatorRegistry::FindValidator( const Registry ®istry, - const std::string &validator_id ) const + const std::string &validator_id ) { - logger_->trace( "{}: entry id={}", __func__, validator_id.substr( 0, 8 ) ); + ValidatorRegistryLogger()->trace( "{}: entry id={}", __func__, validator_id.substr( 0, 8 ) ); for ( const auto &validator : registry.validators() ) { if ( validator.validator_id() == validator_id ) { - logger_->debug( "{}: validator found", __func__ ); + ValidatorRegistryLogger()->debug( "{}: validator found", __func__ ); return &validator; } } - logger_->debug( "{}: validator not found", __func__ ); + ValidatorRegistryLogger()->debug( "{}: validator not found", __func__ ); return nullptr; } diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index 359c8a96e..cb48f5b66 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -13,11 +13,14 @@ #include #include #include +#include +#include #include #include #include "base/buffer.hpp" #include "base/logger.hpp" +#include "blockchain/impl/proto/Consensus.pb.h" #include "blockchain/impl/proto/ValidatorRegistry.pb.h" #include "crdt/crdt_callback_manager.hpp" #include "crdt/proto/delta.pb.h" @@ -30,28 +33,41 @@ namespace sgns class Migration3_5_0To3_6_0; } -namespace sgns::blockchain +namespace sgns { class ValidatorRegistry : public std::enable_shared_from_this { public: - using ValidatorEntry = validator::ValidatorEntry; - using Registry = validator::Registry; - using SignatureEntry = validator::SignatureEntry; - using RegistryUpdate = validator::RegistryUpdate; - using Role = validator::Role; - using Status = validator::Status; - using InitCallback = std::function; + static constexpr size_t DefaultMaxNewValidatorsPerUpdate = 10; + static constexpr size_t DefaultCertificatesPerBatch = 5; + using ValidatorEntry = validator::ValidatorEntry; + using Registry = validator::Registry; + using SignatureEntry = validator::SignatureEntry; + using RegistryUpdate = validator::RegistryUpdate; + using Role = validator::Role; + using Status = validator::Status; + using InitCallback = std::function; using BlockRequestMethod = std::function )> )>; struct WeightConfig { - uint64_t base_weight_ = 1; - uint64_t full_multiplier_ = 3; - uint64_t genesis_multiplier_ = 5; - uint64_t sharded_multiplier_ = 1; - uint64_t max_weight_ = 10; + uint64_t genesis_weight_ = 50000; + uint64_t full_weight_ = 1000; + uint64_t regular_weight_ = 1; + uint64_t sharded_weight_ = 1; + uint64_t genesis_max_weight_ = 50000; + uint64_t full_max_weight_ = 5000; + uint64_t regular_max_weight_ = 100; + uint64_t sharded_max_weight_ = 100; + uint64_t approval_increment_ = 1; + uint32_t penalty_threshold_ = 10; + uint32_t penalty_cap_ = 100; + uint32_t blacklist_bump_ = 10; + uint32_t missed_epoch_threshold_ = 500; + uint32_t inactivity_decrement_ = 1; + uint64_t total_weight_cap_multiplier_ = 4; + uint64_t certificate_timestamp_window_ms_ = 300000; }; static std::shared_ptr New( std::shared_ptr db, @@ -61,24 +77,55 @@ namespace sgns::blockchain std::string genesis_authority, BlockRequestMethod block_request_method, InitCallback init_callback = nullptr ); - - uint64_t ComputeWeight( Role role ) const; - uint64_t TotalWeight( const Registry ®istry ) const; - uint64_t QuorumThreshold( uint64_t total_weight ) const; - bool IsQuorum( uint64_t accumulated_weight, uint64_t total_weight ) const; - - Registry CreateGenesisRegistry( const std::string &genesis_validator_id ) const; - outcome::result StoreGenesisRegistry( const std::string &genesis_validator_id, - std::function( std::vector )> sign ); - outcome::result LoadRegistry() const; - outcome::result LoadRegistryUpdate() const; + ~ValidatorRegistry(); + + uint64_t ComputeWeight( Role role ) const; + static uint64_t TotalWeight( const Registry ®istry ); + uint64_t QuorumThreshold( uint64_t total_weight ) const; + bool IsQuorum( uint64_t accumulated_weight, uint64_t total_weight ) const; + + Registry CreateGenesisRegistry( const std::string &genesis_validator_id ) const; + outcome::result StoreGenesisRegistry( const std::string &genesis_validator_id, + std::function( std::vector )> sign ); + outcome::result LoadRegistry() const; + outcome::result LoadRegistry( const std::string &cid ) const; + outcome::result LoadRegistryUpdate() const; outcome::result> GetValidatorWeight( const std::string &validator_id ) const; - bool RegisterFilter(); + bool RegisterFilter(); + outcome::result CreateUpdateFromCertificate( const sgns::ConsensusCertificate &certificate ); + outcome::result StoreRegistryUpdate( const RegistryUpdate &update ); + outcome::result> BeginRegistryUpdateTransaction( + const RegistryUpdate &update ); + void SetMaxNewValidatorsPerUpdate( size_t max_new ); outcome::result> SerializeRegistry( const Registry ®istry ) const; outcome::result DeserializeRegistry( const std::vector &buffer ) const; outcome::result> SerializeRegistryUpdate( const RegistryUpdate &update ) const; outcome::result DeserializeRegistryUpdate( const std::vector &buffer ) const; + std::string GetRegistryCid() const; + uint64_t GetRegistryEpoch() const; + void SetCertificatesPerBatch( size_t batch_size ); + void SetBatchSubjectSubmitter( + std::function( const ConsensusSubject &subject )> submitter ); + void OnFinalizedCertificate( const sgns::ConsensusCertificate &certificate ); + + enum class BatchSubjectDecision + { + Approve, + Reject, + Pending + }; + enum class BatchCertificateDecision + { + Approve, + Reject, + Pending, + Stalled + }; + outcome::result EvaluateBatchSubject( const ConsensusSubject &subject ); + outcome::result HandleBatchCertificate( + const std::string &subject_hash, + const sgns::ConsensusCertificate &certificate ); static constexpr std::string_view RegistryKey() { @@ -95,6 +142,8 @@ namespace sgns::blockchain return "gnus-validator-registry-cid"; } + static const ValidatorEntry *FindValidator( const Registry ®istry, const std::string &validator_id ); + protected: friend class sgns::Migration3_5_0To3_6_0; @@ -102,6 +151,14 @@ namespace sgns::blockchain const std::shared_ptr &new_db ); private: + struct CertificateVotes + { + std::unordered_set approved; + std::unordered_set unregistered; + std::unordered_map registered_votes; + std::unordered_map unregistered_votes; + }; + ValidatorRegistry( std::shared_ptr db, uint64_t quorum_numerator, uint64_t quorum_denominator, @@ -113,9 +170,43 @@ namespace sgns::blockchain std::optional> FilterRegistryUpdate( const crdt::pb::Element &element ); void RegistryUpdateReceived( const crdt::CRDTCallbackManager::NewDataPair &new_data, const std::string &cid ); outcome::result> ComputeUpdateSigningBytes( const RegistryUpdate &update ) const; - bool VerifyUpdate( const RegistryUpdate &update, const Registry *current_registry ) const; - const ValidatorEntry *FindValidator( const Registry ®istry, const std::string &validator_id ) const; - void InitializeCache(); + bool VerifyUpdate( const RegistryUpdate &update, + const Registry *current_registry, + bool enforce_time_window ) const; + bool ValidateCertificate( const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry ) const; + bool ValidateCertificateForUpdate( const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry ) const; + CertificateVotes ExtractCertificateVotes( const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry ) const; + Registry BuildRegistryFromCertificate( const Registry ¤t_registry, + const sgns::ConsensusCertificate &certificate, + const std::unordered_map ®istered_votes, + const std::unordered_map &unregistered_votes ) const; + Registry BuildRegistryFromAggregatedVotes( + const Registry ¤t_registry, + const std::unordered_map ®istered_votes, + const std::unordered_map &unregistered_votes ) const; + void InsertNewValidators( Registry ®istry, + const std::unordered_map &unregistered_votes ) const; + void ApplyVoteEffects( std::vector &entries, + const std::unordered_map ®istered_votes ) const; + void ApplyInactivityDecay( std::vector &entries, + const std::unordered_set &participants ) const; + void ApplyTotalWeightCap( std::vector &entries ) const; + static void NormalizeRegistry( Registry ®istry ); + + void InitializeCache(); + static std::string BuildBatchKey( const std::string &base_registry_cid, uint64_t base_registry_epoch ); + outcome::result ComputeBatchRoot( const std::vector &subject_hashes ) const; + outcome::result> SelectBatchSubjects( const std::string &base_registry_cid, + uint64_t base_registry_epoch, + uint32_t certificate_count, + std::optional expected_root ) const; + outcome::result LoadCertificateBySubjectHash( + const std::string &subject_hash ) const; + outcome::result TryCreateAndSubmitBatchProposal( const std::string &base_registry_cid, + uint64_t base_registry_epoch ); void NotifyInitialized( bool success ) const; void PersistLocalState( const std::string &cid ) const; void RequestHeadCids( const std::set &cids ); @@ -130,7 +221,15 @@ namespace sgns::blockchain std::optional cached_registry_; std::optional cached_update_; std::string cached_registry_id_; - bool cache_initialized_ = false; + bool cache_initialized_ = false; + size_t max_new_validators_per_update_ = DefaultMaxNewValidatorsPerUpdate; + size_t certificates_per_batch_ = DefaultCertificatesPerBatch; + mutable std::mutex batch_mutex_; + std::unordered_map> pending_certificate_subjects_by_base_; + std::unordered_set pending_batch_subject_ids_; + std::unordered_set finalized_batch_subject_ids_; + std::unordered_set applying_batch_subject_ids_; + std::function( const ConsensusSubject &subject )> submit_batch_subject_; InitCallback init_callback_; std::function )> callback )> diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 66374efc8..441752743 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -64,9 +64,10 @@ namespace sgns return address; } - std::shared_ptr Blockchain::New( std::shared_ptr global_db, - std::shared_ptr account, - BlockchainCallback callback ) + std::shared_ptr Blockchain::New( std::shared_ptr global_db, + std::shared_ptr account, + std::shared_ptr pubsub, + BlockchainCallback callback ) { auto instance = std::shared_ptr( new Blockchain( std::move( global_db ), std::move( account ), std::move( callback ) ) ); @@ -127,11 +128,11 @@ namespace sgns return std::nullopt; } ); - instance->validator_registry_ = blockchain::ValidatorRegistry::New( + instance->validator_registry_ = ValidatorRegistry::New( instance->db_, 2, 3, - blockchain::ValidatorRegistry::WeightConfig{}, + ValidatorRegistry::WeightConfig{}, GetAuthorizedFullNodeAddress(), [weak_ptr( std::weak_ptr( @@ -163,6 +164,106 @@ namespace sgns return nullptr; } + instance->consensus_manager_ = ConsensusManager::New( + instance->validator_registry_, + instance->db_, + std::move( pubsub ), + [weak_ptr( std::weak_ptr( instance ) )]( + std::vector payload ) -> outcome::result> + { + if ( auto strong = weak_ptr.lock() ) + { + return strong->account_->Sign( std::move( payload ) ); + } + return outcome::failure( std::errc::owner_dead ); + }, + instance->account_->GetAddress() ); + + instance->validator_registry_->SetBatchSubjectSubmitter( + [weak_ptr( std::weak_ptr( instance ) )]( + const ConsensusSubject &subject ) -> outcome::result + { + if ( auto strong = weak_ptr.lock() ) + { + auto weight_result = strong->validator_registry_->GetValidatorWeight( + strong->account_->GetAddress() ); + if ( weight_result.has_error() ) + { + return outcome::failure( weight_result.error() ); + } + if ( !weight_result.value().has_value() ) + { + return outcome::success(); + } + auto proposal_result = strong->consensus_manager_->CreateProposal( + subject, + strong->account_->GetAddress(), + strong->validator_registry_->GetRegistryCid(), + strong->validator_registry_->GetRegistryEpoch() ); + if ( proposal_result.has_error() ) + { + return outcome::failure( proposal_result.error() ); + } + return strong->consensus_manager_->SubmitProposal( proposal_result.value(), true ); + } + return outcome::failure( std::errc::owner_dead ); + } ); + + instance->consensus_manager_->RegisterSubjectHandler( + SubjectType::SUBJECT_REGISTRY_BATCH, + [weak_ptr( std::weak_ptr( instance ) )]( + const ConsensusManager::Subject &subject ) -> outcome::result + { + if ( auto strong = weak_ptr.lock() ) + { + auto decision_result = strong->validator_registry_->EvaluateBatchSubject( subject ); + if ( decision_result.has_error() ) + { + return outcome::failure( decision_result.error() ); + } + switch ( decision_result.value() ) + { + case ValidatorRegistry::BatchSubjectDecision::Approve: + return ConsensusManager::Check::Approve; + case ValidatorRegistry::BatchSubjectDecision::Pending: + return ConsensusManager::Check::Pending; + case ValidatorRegistry::BatchSubjectDecision::Reject: + default: + return ConsensusManager::Check::Reject; + } + } + return outcome::failure( std::errc::owner_dead ); + } ); + + instance->consensus_manager_->RegisterCertificateHandler( + SubjectType::SUBJECT_REGISTRY_BATCH, + [weak_ptr( std::weak_ptr( instance ) )]( + const std::string &subject_hash, + const ConsensusCertificate &certificate ) -> outcome::result + { + if ( auto strong = weak_ptr.lock() ) + { + auto decision = strong->validator_registry_->HandleBatchCertificate( subject_hash, certificate ); + if ( decision.has_error() ) + { + return outcome::failure( decision.error() ); + } + switch ( decision.value() ) + { + case ValidatorRegistry::BatchCertificateDecision::Approve: + return ConsensusManager::Check::Approve; + case ValidatorRegistry::BatchCertificateDecision::Pending: + return ConsensusManager::Check::Pending; + case ValidatorRegistry::BatchCertificateDecision::Stalled: + return ConsensusManager::Check::Stalled; + case ValidatorRegistry::BatchCertificateDecision::Reject: + default: + return ConsensusManager::Check::Reject; + } + } + return outcome::failure( std::errc::owner_dead ); + } ); + auto ensure_registry_result = instance->EnsureValidatorRegistry(); if ( ensure_registry_result.has_error() ) { @@ -237,7 +338,7 @@ namespace sgns case 2: { sgns::crdt::GlobalDB::Buffer registry_cid_key; - registry_cid_key.put( std::string( blockchain::ValidatorRegistry::RegistryCidKey() ) ); + registry_cid_key.put( std::string( ValidatorRegistry::RegistryCidKey() ) ); auto registry_cid = strong->db_->GetDataStore()->get( registry_cid_key ); if ( registry_cid.has_value() ) { @@ -360,7 +461,21 @@ namespace sgns Blockchain::~Blockchain() { logger_->debug( "[{}] ~Blockchain destructor called", account_->GetAddress().substr( 0, 8 ) ); + if ( consensus_manager_ ) + { + consensus_manager_->Close(); + } + if ( db_ ) + { + const std::string genesis_pattern = "/?" + std::string( GENESIS_KEY ); + const std::string account_pattern = "/?" + std::string( ACCOUNT_CREATION_KEY_PREFIX ) + ".*"; + db_->UnregisterNewElementCallback( genesis_pattern ); + db_->UnregisterElementFilter( genesis_pattern ); + db_->UnregisterNewElementCallback( account_pattern ); + db_->UnregisterElementFilter( account_pattern ); + } account_->ClearGetBlockChainCIDMethod(); + account_->ClearGetValidatorWeightMethod(); } void Blockchain::SetAuthorizedFullNodeAddress( const std::string &pub_address ) @@ -839,8 +954,8 @@ namespace sgns account_->GetAddress().substr( 0, 8 ), GetAuthorizedFullNodeAddress().substr( 0, 8 ) ); - sgns::blockchain::GenesisBlock g; - auto timestamp = std::chrono::system_clock::now(); + GenesisBlock g; + auto timestamp = std::chrono::system_clock::now(); g.set_chain_id( "supergenius" ); g.set_timestamp( @@ -933,7 +1048,7 @@ namespace sgns account_->GetAddress().substr( 0, 8 ), GetAuthorizedFullNodeAddress().substr( 0, 8 ) ); - sgns::blockchain::GenesisBlock g; + GenesisBlock g; // Convert string back to byte vector for ParseFromArray std::vector data( serialized_genesis.begin(), serialized_genesis.end() ); @@ -971,12 +1086,12 @@ namespace sgns return outcome::success(); } - std::vector Blockchain::ComputeSignatureData( const blockchain::GenesisBlock &g ) const + std::vector Blockchain::ComputeSignatureData( const GenesisBlock &g ) const { logger_->trace( "[{}] Computing signature data for genesis block", account_->GetAddress().substr( 0, 8 ) ); // Create a copy without signature for deterministic signing - blockchain::GenesisBlock g_copy = g; + GenesisBlock g_copy = g; g_copy.clear_signature(); // Serialize the unsigned block @@ -993,10 +1108,10 @@ namespace sgns return signature_data; } - std::vector Blockchain::ComputeSignatureData( const blockchain::AccountCreationBlock &ac ) const + std::vector Blockchain::ComputeSignatureData( const AccountCreationBlock &ac ) const { // Create a copy without signature for deterministic signing - blockchain::AccountCreationBlock ac_copy = ac; + AccountCreationBlock ac_copy = ac; ac_copy.clear_signature(); size_t size = ac_copy.ByteSizeLong(); @@ -1009,7 +1124,7 @@ namespace sgns return signature_data; } - bool Blockchain::VerifySignature( const blockchain::GenesisBlock &g ) const + bool Blockchain::VerifySignature( const GenesisBlock &g ) const { logger_->trace( "[{}] Verifying genesis block signature", account_->GetAddress().substr( 0, 8 ) ); @@ -1043,7 +1158,7 @@ namespace sgns return verification_result; } - bool Blockchain::VerifySignature( const blockchain::AccountCreationBlock &ac ) const + bool Blockchain::VerifySignature( const AccountCreationBlock &ac ) const { logger_->trace( "[{}] Verifying account creation block signature", account_->GetAddress().substr( 0, 8 ) ); @@ -1091,8 +1206,8 @@ namespace sgns account_->GetAddress().substr( 0, 8 ), cids_.genesis_.value() ); - sgns::blockchain::AccountCreationBlock ac; - auto timestamp = std::chrono::system_clock::now(); + AccountCreationBlock ac; + auto timestamp = std::chrono::system_clock::now(); ac.set_account_address( account_->GetAddress() ); ac.set_genesis_block_cid( cids_.genesis_.value() ); @@ -1162,7 +1277,7 @@ namespace sgns { logger_->debug( "[{}] Verifying account creation block", account_->GetAddress().substr( 0, 8 ) ); - sgns::blockchain::AccountCreationBlock ac; + AccountCreationBlock ac; // Convert string back to byte vector for ParseFromArray std::vector data( serialized_account_creation.begin(), serialized_account_creation.end() ); @@ -1213,7 +1328,7 @@ namespace sgns do { - sgns::blockchain::GenesisBlock new_genesis; + GenesisBlock new_genesis; if ( !new_genesis.ParseFromArray( reinterpret_cast( element.value().data() ), static_cast( element.value().size() ) ) ) { @@ -1247,7 +1362,7 @@ namespace sgns break; } - sgns::blockchain::GenesisBlock existing_genesis; + GenesisBlock existing_genesis; if ( !existing_genesis.ParseFromArray( reinterpret_cast( existing_serialized.data() ), static_cast( existing_serialized.size() ) ) ) { @@ -1295,7 +1410,7 @@ namespace sgns do { - sgns::blockchain::AccountCreationBlock new_block; + AccountCreationBlock new_block; if ( !new_block.ParseFromArray( reinterpret_cast( element.value().data() ), static_cast( element.value().size() ) ) ) { @@ -1349,7 +1464,7 @@ namespace sgns break; } - sgns::blockchain::AccountCreationBlock existing_block; + AccountCreationBlock existing_block; if ( !existing_block.ParseFromArray( reinterpret_cast( existing_serialized.data() ), static_cast( existing_serialized.size() ) ) ) { @@ -1399,8 +1514,7 @@ namespace sgns return std::nullopt; } - bool Blockchain::ShouldReplaceGenesis( const blockchain::GenesisBlock &existing, - const blockchain::GenesisBlock &candidate ) + bool Blockchain::ShouldReplaceGenesis( const GenesisBlock &existing, const GenesisBlock &candidate ) const { if ( candidate.timestamp() == existing.timestamp() ) { @@ -1409,8 +1523,8 @@ namespace sgns return candidate.timestamp() < existing.timestamp(); } - bool Blockchain::ShouldReplaceAccountCreation( const blockchain::AccountCreationBlock &existing, - const blockchain::AccountCreationBlock &candidate ) + bool Blockchain::ShouldReplaceAccountCreation( const AccountCreationBlock &existing, + const AccountCreationBlock &candidate ) const { if ( candidate.timestamp() == existing.timestamp() ) { @@ -1422,12 +1536,17 @@ namespace sgns outcome::result Blockchain::Stop() { logger_->info( "[{}] Stopping blockchain", account_->GetAddress().substr( 0, 8 ) ); + if ( consensus_manager_ ) + { + consensus_manager_->Close(); + } //db_->RemoveListenTopic( std::string( BLOCKCHAIN_TOPIC ) ); return outcome::success(); } - outcome::result Blockchain::AccountCreationReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair &new_data, - const std::string &cid ) + outcome::result Blockchain::AccountCreationReceivedCallback( + const crdt::CRDTCallbackManager::NewDataPair &new_data, + const std::string &cid ) { logger_->debug( "[{}] Account creation received callback triggered with CID: {}", account_->GetAddress().substr( 0, 8 ), @@ -1536,9 +1655,98 @@ namespace sgns return it->second; } + std::shared_ptr Blockchain::GetValidatorRegistry() const + { + return validator_registry_; + } + void Blockchain::SetFullNodeMode() { db_->AddListenTopic( std::string( BLOCKCHAIN_TOPIC ) ); //This will not trigger the broadcaster, but it will grab links on CRDT } + + bool Blockchain::RegisterSubjectHandler( SubjectType type, ConsensusManager::SubjectHandler handler ) + { + return consensus_manager_->RegisterSubjectHandler( type, std::move( handler ) ); + } + + void Blockchain::UnregisterSubjectHandler( SubjectType type ) + { + consensus_manager_->UnregisterSubjectHandler( type ); + } + + bool Blockchain::RegisterCertificateHandler( SubjectType type, ConsensusManager::CertificateSubjectHandler handler ) + { + return consensus_manager_->RegisterCertificateHandler( type, std::move( handler ) ); + } + + void Blockchain::UnregisterCertificateHandler( SubjectType type ) + { + consensus_manager_->UnregisterCertificateHandler( type ); + } + + outcome::result Blockchain::CreateConsensusNonceSubject( + const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash, + const std::optional &utxo_commitment, + const std::optional &utxo_witness ) + { + return consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ); + } + + outcome::result Blockchain::CreateConsensusProposal( + const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash, + const std::optional &utxo_commitment, + const std::optional &utxo_witness ) + { + BOOST_OUTCOME_TRY( auto &&nonce_subject, + CreateConsensusNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ) ); + BOOST_OUTCOME_TRY( auto &&nonce_proposal, + consensus_manager_->CreateProposal( nonce_subject, + account_id, + validator_registry_->GetRegistryCid(), + validator_registry_->GetRegistryEpoch() ) ); + + return nonce_proposal; + } + + outcome::result Blockchain::SubmitProposal( const ConsensusManager::Proposal &proposal ) + { + return consensus_manager_->SubmitProposal( std::move( proposal ) ); + } + + outcome::result Blockchain::TryResumeProposal( const std::string &hash ) + { + if ( consensus_manager_->CheckCertificateForSubject( hash ) ) + { + return outcome::success(); + } + return consensus_manager_->ResumeProposalHandling( hash ); + } + + bool Blockchain::CheckCertificate( const std::string &subject_hash ) const + { + return consensus_manager_->CheckCertificateForSubject( subject_hash ); + } + + bool Blockchain::CheckCertificateStrict( const ConsensusManager::Subject &subject ) const + { + return consensus_manager_->CheckCertificateForSubject( subject ); + } + + outcome::result Blockchain::GetCertificateBySubjectHash( + const std::string &subject_hash ) const + { + return consensus_manager_->GetCertificateBySubjectHash( subject_hash ); + } + + const std::string &Blockchain::BestHash( const std::string &a, const std::string &b ) const + { + return consensus_manager_->BestHash( a, b ); + } + } diff --git a/src/blockchain/impl/CMakeLists.txt b/src/blockchain/impl/CMakeLists.txt index 790340cfb..47ce294cd 100644 --- a/src/blockchain/impl/CMakeLists.txt +++ b/src/blockchain/impl/CMakeLists.txt @@ -1,5 +1,6 @@ add_proto_library(SGBlocksProto proto/SGBlocks.proto) add_proto_library(SGBlockchainProto proto/SGBlockchain.proto) +add_proto_library(ConsensusProto proto/Consensus.proto) add_proto_library(ValidatorRegistryProto proto/ValidatorRegistry.proto) add_library(blockchain_common @@ -25,6 +26,7 @@ supergenius_install(blockchain_common) add_library(blockchain_genesis Blockchain.cpp ../ValidatorRegistry.cpp + ../Consensus.cpp ) target_link_libraries(blockchain_genesis @@ -37,8 +39,10 @@ target_link_libraries(blockchain_genesis hexutil logger sgns_genius_account + ipfs-pubsub PRIVATE SGBlockchainProto + ConsensusProto ValidatorRegistryProto ) diff --git a/src/blockchain/impl/proto/Consensus.proto b/src/blockchain/impl/proto/Consensus.proto new file mode 100644 index 000000000..e44ffd314 --- /dev/null +++ b/src/blockchain/impl/proto/Consensus.proto @@ -0,0 +1,126 @@ +syntax = "proto3"; + +package sgns; + +message ConsensusSubject { + string subject_id = 1; + SubjectType type = 2; + string account_id = 3; + + oneof payload { + NonceSubject nonce = 10; + TaskResultSubject task_result = 11; + RegistryBatchSubject registry_batch = 12; + } +} + +enum SubjectType { + SUBJECT_UNSPECIFIED = 0; + SUBJECT_NONCE = 1; + SUBJECT_TASK_RESULT = 2; + SUBJECT_REGISTRY_BATCH = 3; +} + +message UTXOTransitionCommitment { + message CommittedOutPoint { + bytes tx_id_hash = 1; + uint32 output_index = 2; + } + + message CommittedOutput { + bytes tx_id_hash = 1; + uint32 output_index = 2; + string owner_address = 3; + bytes token_id = 4; + uint64 amount = 5; + } + + repeated CommittedOutPoint consumed_outpoints = 1; + repeated CommittedOutput produced_outputs = 2; + bytes consumed_outpoints_root = 3; + bytes produced_outputs_root = 4; +} + +message MerkleProofStep { + bytes sibling_hash = 1; + bool is_left_sibling = 2; +} + +message ConsumedInputProof { + bytes tx_id_hash = 1; + uint32 output_index = 2; + bytes leaf_payload = 3; + repeated MerkleProofStep branch = 4; + repeated MerkleProofStep produced_branch = 5; +} + +message UTXOWitness { + repeated ConsumedInputProof consumed_inputs = 1; +} + +message NonceSubject { + uint64 nonce = 1; + bytes tx_hash = 2; + UTXOTransitionCommitment utxo_commitment = 3; + UTXOWitness utxo_witness = 4; +} + +message TaskResultSubject { + string escrow_path = 1; + bytes task_result_hash = 2; + uint64 result_epoch = 3; +} + +message RegistryBatchSubject { + string base_registry_cid = 1; + uint64 base_registry_epoch = 2; + uint64 target_registry_epoch = 3; + uint32 certificate_count = 4; + bytes batch_root = 5; +} + +message ConsensusProposal { + string proposal_id = 1; + string proposer_id = 2; + uint64 timestamp = 3; + string registry_cid = 4; + uint64 registry_epoch = 5; + ConsensusSubject subject = 6; + bytes signature = 7; +} + +message ConsensusVote { + string proposal_id = 1; + string voter_id = 2; + bool approve = 3; + uint64 timestamp = 4; + bytes signature = 5; +} + +message ConsensusVoteBundle { + string proposal_id = 1; + string aggregator_id = 2; + uint64 timestamp = 3; + repeated ConsensusVote votes = 4; + bytes signature = 5; +} + +message ConsensusCertificate { + string proposal_id = 1; + string registry_cid = 2; + uint64 registry_epoch = 3; + uint64 total_weight = 4; + uint64 approved_weight = 5; + uint64 timestamp = 6; + repeated ConsensusVote votes = 7; + ConsensusProposal proposal = 8; +} + +message ConsensusMessage { + oneof payload { + ConsensusProposal proposal = 1; + ConsensusVote vote = 2; + ConsensusVoteBundle vote_bundle = 3; + ConsensusCertificate certificate = 4; + } +} diff --git a/src/blockchain/impl/proto/SGBlockchain.proto b/src/blockchain/impl/proto/SGBlockchain.proto index 64fafb370..018dea6d0 100644 --- a/src/blockchain/impl/proto/SGBlockchain.proto +++ b/src/blockchain/impl/proto/SGBlockchain.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package sgns.blockchain; +package sgns; message GenesisBlock { diff --git a/src/blockchain/impl/proto/ValidatorRegistry.proto b/src/blockchain/impl/proto/ValidatorRegistry.proto index 449848c9c..e35c59436 100644 --- a/src/blockchain/impl/proto/ValidatorRegistry.proto +++ b/src/blockchain/impl/proto/ValidatorRegistry.proto @@ -1,12 +1,14 @@ syntax = "proto3"; -package sgns.blockchain.validator; +package sgns.validator; message ValidatorEntry { string validator_id = 1; uint64 weight = 2; Role role = 3; Status status = 4; + uint32 penalty_score = 5; + uint32 missed_epochs = 6; } message Registry { @@ -36,6 +38,8 @@ message RegistryUpdate { Registry registry = 1; string prev_registry_hash = 2; repeated SignatureEntry signatures = 3; + bytes certificate = 4; + repeated string batch_certificate_subject_hashes = 5; } message RegistrySigningPayload { diff --git a/src/crdt/CMakeLists.txt b/src/crdt/CMakeLists.txt index 3e1359534..78c8805bd 100644 --- a/src/crdt/CMakeLists.txt +++ b/src/crdt/CMakeLists.txt @@ -70,6 +70,7 @@ supergenius_install(crdt_data_filter) add_library(crdt_datastore impl/crdt_datastore.cpp + impl/crdt_work_journal.cpp impl/atomic_transaction.cpp ) target_link_libraries(crdt_datastore diff --git a/src/crdt/atomic_transaction.hpp b/src/crdt/atomic_transaction.hpp index ef4ddb0d7..7437999ec 100644 --- a/src/crdt/atomic_transaction.hpp +++ b/src/crdt/atomic_transaction.hpp @@ -7,6 +7,7 @@ #include "outcome/outcome.hpp" #include "primitives/cid/cid.hpp" #include +#include #include #include #include @@ -75,6 +76,20 @@ namespace sgns::crdt */ bool HasKey( const HierarchicalKey &key ) const; + /** + * @brief Add a single topic to this transaction's internal topic set. + * @param topic topic name to add + * @return outcome::success or failure if already committed + */ + outcome::result AddTopic( const std::string &topic ); + + /** + * @brief Add multiple topics to this transaction's internal topic set. + * @param topics topic names to add + * @return outcome::success or failure if already committed + */ + outcome::result AddTopics( const std::unordered_set &topics ); + /** * @brief Commits all pending operations atomically. * Combines all pending operations into a single Delta and publishes it. @@ -112,7 +127,9 @@ namespace sgns::crdt std::shared_ptr datastore_; std::vector operations_; std::unordered_set modified_keys_; // Track which keys have been modified + std::unordered_set stored_topics_; // Topics accumulated before commit bool is_committed_; + mutable std::mutex mutex_; }; } // namespace sgns::crdt diff --git a/src/crdt/crdt_callback_manager.hpp b/src/crdt/crdt_callback_manager.hpp index a4ef77c96..f199f4800 100644 --- a/src/crdt/crdt_callback_manager.hpp +++ b/src/crdt/crdt_callback_manager.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -16,6 +17,7 @@ namespace sgns::crdt { + class CRDTWorkJournal; class CRDTCallbackManager { @@ -30,7 +32,7 @@ namespace sgns::crdt /** * @brief Construct a new CRDTCallbackManager object */ - explicit CRDTCallbackManager(); + explicit CRDTCallbackManager( std::shared_ptr work_journal ); /** * @brief Destroy the CRDTCallbackManager object */ @@ -76,6 +78,7 @@ namespace sgns::crdt void DeleteDataCallback( const std::string &deleted_key, const std::string &cid ); private: + std::shared_ptr work_journal_; std::shared_mutex new_data_callback_registry_mutex_; ///< Mutex to manipulate @ref new_data_callback_registry_ NewDataCallbackRegistry new_data_callback_registry_; ///< New data callback registry std::shared_mutex diff --git a/src/crdt/crdt_data_filter.hpp b/src/crdt/crdt_data_filter.hpp index ae244c6d8..5f0b8ff38 100644 --- a/src/crdt/crdt_data_filter.hpp +++ b/src/crdt/crdt_data_filter.hpp @@ -19,6 +19,8 @@ namespace sgns::crdt { + class CRDTWorkJournal; + class CRDTDataFilter { public: @@ -33,7 +35,7 @@ namespace sgns::crdt * @param[in] accept_by_default: if true, every delta that doesn't have a filter gets accepted. * if false, rejects by default. */ - explicit CRDTDataFilter( bool accept_by_default = true ); + explicit CRDTDataFilter( std::shared_ptr work_journal, bool accept_by_default = true ); /** * @brief Destroy the CRDTDataFilter object @@ -81,11 +83,12 @@ namespace sgns::crdt void FilterTombstonesOnDelta( pb::Delta &delta ); private: - const bool accept_by_default_; ///< The default behavior for values not matching any filter - mutable std::shared_mutex element_registry_mutex_; ///< Mutex for the element registry - std::shared_mutex tombstone_registry_mutex_; ///< Mutex for the tombstone registry - FilterCallbackRegistry element_registry_; ///< Element filter callback registry - FilterCallbackRegistry tombstone_registry_; ///< Tombstone filter callback registry + std::shared_ptr work_journal_; + const bool accept_by_default_; ///< The default behavior for values not matching any filter + mutable std::shared_mutex element_registry_mutex_; ///< Mutex for the element registry + std::shared_mutex tombstone_registry_mutex_; ///< Mutex for the tombstone registry + FilterCallbackRegistry element_registry_; ///< Element filter callback registry + FilterCallbackRegistry tombstone_registry_; ///< Tombstone filter callback registry }; } diff --git a/src/crdt/crdt_datastore.hpp b/src/crdt/crdt_datastore.hpp index 5a0bc21ef..c2b8b814d 100644 --- a/src/crdt/crdt_datastore.hpp +++ b/src/crdt/crdt_datastore.hpp @@ -30,15 +30,12 @@ #include "crdt/crdt_options.hpp" #include "crdt/crdt_data_filter.hpp" #include "crdt/crdt_callback_manager.hpp" +#include "crdt/globaldb/crdt_work_journal.hpp" #include "storage/rocksdb/rocksdb.hpp" namespace sgns { class Blockchain; -} - -namespace sgns::blockchain -{ class ValidatorRegistry; } @@ -217,6 +214,10 @@ namespace sgns::crdt bool RegisterElementFilter( const std::string &pattern, CRDTElementFilterCallback filter ); bool RegisterNewElementCallback( const std::string &pattern, CRDTNewElementCallback callback ); bool RegisterDeletedElementCallback( const std::string &pattern, CRDTDeletedElementCallback callback ); + void UnregisterElementFilter( const std::string &pattern ); + void UnregisterNewElementCallback( const std::string &pattern ); + void UnregisterDeletedElementCallback( const std::string &pattern ); + std::shared_ptr GetWorkJournal() const; /** * @brief Configure which topic this datastore should filter on. @@ -252,10 +253,13 @@ namespace sgns::crdt std::unordered_set GetTopicNames() const; + outcome::result>> GetILPDNodeContent( + const std::string &cid_string ); + protected: friend class PubSubBroadcasterExt; friend class ::sgns::Blockchain; - friend class ::sgns::blockchain::ValidatorRegistry; + friend class ::sgns::ValidatorRegistry; struct RootCIDJob { @@ -399,7 +403,7 @@ namespace sgns::crdt outcome::result WaitForJob( const CID &cid ); private: - CrdtDatastore() = default; + CrdtDatastore() = delete; CrdtDatastore( std::shared_ptr aDatastore, const HierarchicalKey &aKey, @@ -452,6 +456,7 @@ namespace sgns::crdt std::queue pendingRootQueue_; std::optional activeRootCID_; + std::shared_ptr work_journal_; CRDTDataFilter crdt_filter_; bool started_ = false; bool broadcast_enabled_ = false; diff --git a/src/crdt/globaldb/crdt_work_journal.hpp b/src/crdt/globaldb/crdt_work_journal.hpp new file mode 100644 index 000000000..fbc3f972a --- /dev/null +++ b/src/crdt/globaldb/crdt_work_journal.hpp @@ -0,0 +1,69 @@ +#ifndef SUPERGENIUS_CRDT_WORK_JOURNAL_HPP +#define SUPERGENIUS_CRDT_WORK_JOURNAL_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sgns::storage +{ + class rocksdb; +} + +namespace sgns::crdt +{ + class CRDTWorkJournal + { + public: + enum class State : uint8_t + { + Seen = 0, + Processing = 1, + Stalled = 2, + }; + + struct Entry + { + std::string key; + State state = State::Seen; + uint64_t attempt_count = 0; + uint64_t updated_at_ms = 0; + uint64_t lease_until_ms = 0; + }; + + static std::shared_ptr New( std::shared_ptr datastore ); + + void MarkSeen( const std::string &key ); + void MarkProcessing( const std::string &key, std::chrono::milliseconds lease = std::chrono::minutes( 5 ) ); + void MarkStalled( const std::string &key, std::chrono::milliseconds lease = std::chrono::minutes( 5 ) ); + bool MarkDone( const std::string &key ); + + std::optional GetEntry( const std::string &key ) const; + std::vector ListUnfinished( std::string_view key_pattern = {} ) const; + size_t RecoverStaleProcessing( std::string_view key_pattern, + std::chrono::milliseconds stale = std::chrono::milliseconds( 0 ) ); + + private: + static constexpr std::string_view NAMESPACE_PREFIX = "/crdt/work/"; + + explicit CRDTWorkJournal( std::shared_ptr datastore ); + static uint64_t NowMs(); + std::string BuildStorageKey( const std::string &key ) const; + static std::optional DeserializeEntry( std::string_view storage_key, std::string_view value ); + static std::string SerializeEntry( const Entry &entry ); + static std::vector Split( const std::string &value, char separator ); + + std::optional GetEntryUnlocked( const std::string &key ) const; + bool PutEntryUnlocked( const Entry &entry ) const; + + std::shared_ptr datastore_; + mutable std::mutex mutex_; + }; +} + +#endif // SUPERGENIUS_CRDT_WORK_JOURNAL_HPP diff --git a/src/crdt/globaldb/globaldb.cpp b/src/crdt/globaldb/globaldb.cpp index bce382fd3..d705a23a9 100644 --- a/src/crdt/globaldb/globaldb.cpp +++ b/src/crdt/globaldb/globaldb.cpp @@ -345,10 +345,15 @@ namespace sgns::crdt return m_broadcaster->AddBroadcastTopic( topicName ); } + void GlobalDB::AddTopicName( const std::string &topicName ) + { + m_crdtDatastore->AddTopicName( topicName ); + } + void GlobalDB::AddListenTopic( const std::string &topicName ) { m_broadcaster->AddListenTopic( topicName ); - m_crdtDatastore->AddTopicName( topicName ); + AddTopicName( topicName ); } bool GlobalDB::RegisterElementFilter( const std::string &pattern, GlobalDBFilterCallback filter ) @@ -366,11 +371,31 @@ namespace sgns::crdt return m_crdtDatastore->RegisterDeletedElementCallback( pattern, std::move( callback ) ); } + void GlobalDB::UnregisterElementFilter( const std::string &pattern ) + { + m_crdtDatastore->UnregisterElementFilter( pattern ); + } + + void GlobalDB::UnregisterNewElementCallback( const std::string &pattern ) + { + m_crdtDatastore->UnregisterNewElementCallback( pattern ); + } + + void GlobalDB::UnregisterDeletedElementCallback( const std::string &pattern ) + { + m_crdtDatastore->UnregisterDeletedElementCallback( pattern ); + } + std::shared_ptr GlobalDB::GetDataStore() { return m_datastore; } + std::shared_ptr GlobalDB::GetWorkJournal() const + { + return m_crdtDatastore ? m_crdtDatastore->GetWorkJournal() : nullptr; + } + outcome::result GlobalDB::GetCRDTHeadList() { return m_crdtDatastore->GetHeadList(); @@ -449,4 +474,15 @@ namespace sgns::crdt return m_crdtDatastore; } + outcome::result>> GlobalDB::GetCIDContent( + const std::string &cid_string ) + { + if ( !m_crdtDatastore ) + { + m_logger->error( "{}: CRDT datastore not initialized", __func__ ); + return outcome::failure( Error::CRDT_DATASTORE_NOT_CREATED ); + } + return m_crdtDatastore->GetILPDNodeContent( cid_string ); + } + } diff --git a/src/crdt/globaldb/globaldb.hpp b/src/crdt/globaldb/globaldb.hpp index 4ad52047f..08c97d0c7 100644 --- a/src/crdt/globaldb/globaldb.hpp +++ b/src/crdt/globaldb/globaldb.hpp @@ -140,16 +140,21 @@ namespace sgns::crdt std::shared_ptr BeginTransaction(); outcome::result AddBroadcastTopic( const std::string &topicName ); + void AddTopicName( const std::string &topicName ); void AddListenTopic( const std::string &topicName ); void PrintDataStore(); std::shared_ptr GetDataStore(); std::shared_ptr GetBroadcaster(); + std::shared_ptr GetWorkJournal() const; bool RegisterElementFilter( const std::string &pattern, GlobalDBFilterCallback filter ); bool RegisterNewElementCallback( const std::string &pattern, GlobalDBNewElementCallback callback ); bool RegisterDeletedElementCallback( const std::string &pattern, GlobalDBDeletedElementCallback callback ); + void UnregisterElementFilter( const std::string &pattern ); + void UnregisterNewElementCallback( const std::string &pattern ); + void UnregisterDeletedElementCallback( const std::string &pattern ); void Start(); void StartCIDReceiving(); @@ -178,6 +183,9 @@ namespace sgns::crdt std::shared_ptr GetCRDTDataStore(); + outcome::result>> GetCIDContent( + const std::string &cid_string ); + private: /** * @brief Constructs a new Global D B object diff --git a/src/crdt/impl/atomic_transaction.cpp b/src/crdt/impl/atomic_transaction.cpp index d0c6af7e0..07dd4902a 100644 --- a/src/crdt/impl/atomic_transaction.cpp +++ b/src/crdt/impl/atomic_transaction.cpp @@ -12,14 +12,12 @@ namespace sgns::crdt AtomicTransaction::~AtomicTransaction() { - if ( !is_committed_ ) - { - Rollback(); - } + Rollback(); } outcome::result AtomicTransaction::Put( HierarchicalKey key, Buffer value ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -31,6 +29,7 @@ namespace sgns::crdt outcome::result AtomicTransaction::Remove( const HierarchicalKey &key ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -41,6 +40,7 @@ namespace sgns::crdt outcome::result AtomicTransaction::Get( const HierarchicalKey &key ) const { + std::lock_guard lock( mutex_ ); // First, check pending operations in reverse order (most recent first) auto latest_op = FindLatestOperation( key ); if ( latest_op.has_value() ) @@ -63,6 +63,7 @@ namespace sgns::crdt outcome::result AtomicTransaction::Erase( const HierarchicalKey &key ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -86,11 +87,38 @@ namespace sgns::crdt bool AtomicTransaction::HasKey( const HierarchicalKey &key ) const { + std::lock_guard lock( mutex_ ); return modified_keys_.find( key.GetKey() ) != modified_keys_.end(); } + outcome::result AtomicTransaction::AddTopic( const std::string &topic ) + { + std::lock_guard lock( mutex_ ); + if ( is_committed_ ) + { + return outcome::failure( boost::system::error_code{} ); + } + if ( !topic.empty() ) + { + stored_topics_.insert( topic ); + } + return outcome::success(); + } + + outcome::result AtomicTransaction::AddTopics( const std::unordered_set &topics ) + { + std::lock_guard lock( mutex_ ); + if ( is_committed_ ) + { + return outcome::failure( boost::system::error_code{} ); + } + stored_topics_.insert( topics.begin(), topics.end() ); + return outcome::success(); + } + outcome::result AtomicTransaction::Commit( const std::unordered_set &topics ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -130,7 +158,10 @@ namespace sgns::crdt } combined_delta->set_priority( max_priority ); - auto result = datastore_->Publish( combined_delta, topics ); + auto merged_topics = stored_topics_; + merged_topics.insert( topics.begin(), topics.end() ); + + auto result = datastore_->Publish( combined_delta, merged_topics ); if ( !result.has_failure() ) { is_committed_ = true; @@ -141,8 +172,10 @@ namespace sgns::crdt void AtomicTransaction::Rollback() { + std::lock_guard lock( mutex_ ); operations_.clear(); modified_keys_.clear(); + stored_topics_.clear(); } std::optional AtomicTransaction::FindLatestOperation( diff --git a/src/crdt/impl/crdt_callback_manager.cpp b/src/crdt/impl/crdt_callback_manager.cpp index fb9275686..bc4e2ea59 100644 --- a/src/crdt/impl/crdt_callback_manager.cpp +++ b/src/crdt/impl/crdt_callback_manager.cpp @@ -6,39 +6,40 @@ */ #include #include "crdt/crdt_callback_manager.hpp" +#include "crdt/globaldb/crdt_work_journal.hpp" namespace sgns::crdt { - CRDTCallbackManager::CRDTCallbackManager() + CRDTCallbackManager::CRDTCallbackManager( std::shared_ptr work_journal ) : + work_journal_( std::move( work_journal ) ) { - - logger_->debug("CRDTCallbackManager constructed"); + logger_->debug( "CRDTCallbackManager constructed" ); } - CRDTCallbackManager::~CRDTCallbackManager() + CRDTCallbackManager::~CRDTCallbackManager() { - logger_->debug("CRDTCallbackManager destroyed"); + logger_->debug( "CRDTCallbackManager destroyed" ); } bool CRDTCallbackManager::RegisterNewDataCallback( const std::string &pattern, NewDataCallback callback ) { bool ret = false; std::lock_guard lock( new_data_callback_registry_mutex_ ); - - logger_->debug("Attempting to register new data callback for pattern: '{}'", pattern); - + + logger_->debug( "Attempting to register new data callback for pattern: '{}'", pattern ); + if ( new_data_callback_registry_.find( pattern ) == new_data_callback_registry_.end() ) { new_data_callback_registry_[pattern] = std::move( callback ); - ret = true; - logger_->info("Successfully registered new data callback for pattern: '{}'", pattern); + ret = true; + logger_->info( "Successfully registered new data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Pattern '{}' already exists in new data callback registry", pattern); + logger_->warn( "Pattern '{}' already exists in new data callback registry", pattern ); } - - logger_->debug("Total registered new data callbacks: {}", new_data_callback_registry_.size()); + + logger_->debug( "Total registered new data callbacks: {}", new_data_callback_registry_.size() ); return ret; } @@ -46,166 +47,186 @@ namespace sgns::crdt { bool ret = false; std::lock_guard lock( deleted_data_callback_registry_mutex_ ); - - logger_->debug("Attempting to register deleted data callback for pattern: '{}'", pattern); - + + logger_->debug( "Attempting to register deleted data callback for pattern: '{}'", pattern ); + if ( deleted_data_callback_registry_.find( pattern ) == deleted_data_callback_registry_.end() ) { deleted_data_callback_registry_[pattern] = std::move( callback ); - ret = true; - logger_->info("Successfully registered deleted data callback for pattern: '{}'", pattern); + ret = true; + logger_->info( "Successfully registered deleted data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Pattern '{}' already exists in deleted data callback registry", pattern); + logger_->warn( "Pattern '{}' already exists in deleted data callback registry", pattern ); } - - logger_->debug("Total registered deleted data callbacks: {}", deleted_data_callback_registry_.size()); + + logger_->debug( "Total registered deleted data callbacks: {}", deleted_data_callback_registry_.size() ); return ret; } void CRDTCallbackManager::UnregisterNewDataCallback( const std::string &pattern ) { std::lock_guard lock( new_data_callback_registry_mutex_ ); - - auto it = new_data_callback_registry_.find(pattern); - if (it != new_data_callback_registry_.end()) + + auto it = new_data_callback_registry_.find( pattern ); + if ( it != new_data_callback_registry_.end() ) { new_data_callback_registry_.erase( pattern ); - logger_->info("Successfully unregistered new data callback for pattern: '{}'", pattern); + logger_->info( "Successfully unregistered new data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Attempted to unregister non-existent pattern: '{}'", pattern); + logger_->warn( "Attempted to unregister non-existent pattern: '{}'", pattern ); } - - logger_->debug("Total registered new data callbacks after unregister: {}", new_data_callback_registry_.size()); + + logger_->debug( "Total registered new data callbacks after unregister: {}", + new_data_callback_registry_.size() ); } void CRDTCallbackManager::UnregisterDeletedDataCallback( const std::string &pattern ) { std::lock_guard lock( deleted_data_callback_registry_mutex_ ); - - auto it = deleted_data_callback_registry_.find(pattern); - if (it != deleted_data_callback_registry_.end()) + + auto it = deleted_data_callback_registry_.find( pattern ); + if ( it != deleted_data_callback_registry_.end() ) { deleted_data_callback_registry_.erase( pattern ); - logger_->info("Successfully unregistered deleted data callback for pattern: '{}'", pattern); + logger_->info( "Successfully unregistered deleted data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Attempted to unregister non-existent pattern: '{}'", pattern); + logger_->warn( "Attempted to unregister non-existent pattern: '{}'", pattern ); } - - logger_->debug("Total registered deleted data callbacks after unregister: {}", deleted_data_callback_registry_.size()); + + logger_->debug( "Total registered deleted data callbacks after unregister: {}", + deleted_data_callback_registry_.size() ); } void CRDTCallbackManager::PutDataCallback( const std::string &key, const base::Buffer &value, const std::string &cid ) { - logger_->debug("PutDataCallback triggered for key: '{}', cid: '{}', value size: {} bytes", - key, cid, value.size()); - + logger_->debug( "PutDataCallback triggered for key: '{}', cid: '{}', value size: {} bytes", + key, + cid, + value.size() ); + NewDataCallbackRegistry registry_copy; { std::shared_lock lock( new_data_callback_registry_mutex_ ); registry_copy = new_data_callback_registry_; - logger_->debug("Copied {} registered patterns for matching", registry_copy.size()); + logger_->debug( "Copied {} registered patterns for matching", registry_copy.size() ); } - - if (registry_copy.empty()) + work_journal_->MarkProcessing( key ); + + if ( registry_copy.empty() ) { - logger_->warn("No new data callbacks registered - key '{}' will not trigger any callbacks", key); + logger_->warn( "No new data callbacks registered - key '{}' will not trigger any callbacks", key ); return; } - + bool callback_triggered = false; for ( const auto &[pattern, callback] : registry_copy ) { - logger_->debug("Testing key '{}' against pattern '{}'", key, pattern); - - try + logger_->debug( "Testing key '{}' against pattern '{}'", key, pattern ); + + try { std::regex regex( pattern ); - bool matches = std::regex_match( key, regex ); - - logger_->debug("Regex match result for key '{}' vs pattern '{}': {}", - key, pattern, matches ? "MATCH" : "NO MATCH"); - + bool matches = std::regex_match( key, regex ); + + logger_->debug( "Regex match result for key '{}' vs pattern '{}': {}", + key, + pattern, + matches ? "MATCH" : "NO MATCH" ); + if ( matches ) { - logger_->info("Executing callback for key '{}' matching pattern '{}'", key, pattern); + logger_->info( "Executing callback for key '{}' matching pattern '{}'", key, pattern ); callback( std::make_pair( key, value ), cid ); + if ( auto entry = work_journal_->GetEntry( key ); + entry.has_value() && entry->state == CRDTWorkJournal::State::Processing ) + { + if ( !work_journal_->MarkDone( key ) ) + { + logger_->error( "Failed to auto-complete CRDT work for key '{}'", key ); + } + } callback_triggered = true; } } - catch (const std::regex_error& e) + catch ( const std::regex_error &e ) { - logger_->error("Regex error for pattern '{}': {}", pattern, e.what()); + logger_->error( "Regex error for pattern '{}': {}", pattern, e.what() ); } } - - if (!callback_triggered) + + if ( !callback_triggered ) { - logger_->warn("No callbacks were triggered for key '{}' - no pattern matches found", key); + logger_->warn( "No callbacks were triggered for key '{}' - no pattern matches found", key ); } else { - logger_->debug("Successfully triggered callbacks for key '{}'", key); + logger_->debug( "Successfully triggered callbacks for key '{}'", key ); } } void CRDTCallbackManager::DeleteDataCallback( const std::string &deleted_key, const std::string &cid ) { - logger_->debug("DeleteDataCallback triggered for key: '{}', cid: '{}'", deleted_key, cid); - + logger_->debug( "DeleteDataCallback triggered for key: '{}', cid: '{}'", deleted_key, cid ); + DeletedDataCallbackRegistry registry_copy; { std::shared_lock lock( deleted_data_callback_registry_mutex_ ); registry_copy = deleted_data_callback_registry_; - logger_->debug("Copied {} registered delete patterns for matching", registry_copy.size()); + logger_->debug( "Copied {} registered delete patterns for matching", registry_copy.size() ); } - - if (registry_copy.empty()) + + if ( registry_copy.empty() ) { - logger_->warn("No deleted data callbacks registered - key '{}' will not trigger any callbacks", deleted_key); + logger_->warn( "No deleted data callbacks registered - key '{}' will not trigger any callbacks", + deleted_key ); return; } - + bool callback_triggered = false; for ( const auto &[pattern, callback] : registry_copy ) { - logger_->debug("Testing deleted key '{}' against pattern '{}'", deleted_key, pattern); - - try + logger_->debug( "Testing deleted key '{}' against pattern '{}'", deleted_key, pattern ); + + try { std::regex regex( pattern ); - bool matches = std::regex_match( deleted_key, regex ); - - logger_->debug("Regex match result for deleted key '{}' vs pattern '{}': {}", - deleted_key, pattern, matches ? "MATCH" : "NO MATCH"); - + bool matches = std::regex_match( deleted_key, regex ); + + logger_->debug( "Regex match result for deleted key '{}' vs pattern '{}': {}", + deleted_key, + pattern, + matches ? "MATCH" : "NO MATCH" ); + if ( matches ) { - logger_->info("Executing delete callback for key '{}' matching pattern '{}'", deleted_key, pattern); + logger_->info( "Executing delete callback for key '{}' matching pattern '{}'", + deleted_key, + pattern ); callback( deleted_key, cid ); callback_triggered = true; } } - catch (const std::regex_error& e) + catch ( const std::regex_error &e ) { - logger_->error("Regex error for delete pattern '{}': {}", pattern, e.what()); + logger_->error( "Regex error for delete pattern '{}': {}", pattern, e.what() ); } } - - if (!callback_triggered) + + if ( !callback_triggered ) { - logger_->warn("No delete callbacks were triggered for key '{}' - no pattern matches found", deleted_key); + logger_->warn( "No delete callbacks were triggered for key '{}' - no pattern matches found", deleted_key ); } else { - logger_->debug("Successfully triggered delete callbacks for key '{}'", deleted_key); + logger_->debug( "Successfully triggered delete callbacks for key '{}'", deleted_key ); } } diff --git a/src/crdt/impl/crdt_data_filter.cpp b/src/crdt/impl/crdt_data_filter.cpp index d3a2c2c58..e7dcb0957 100644 --- a/src/crdt/impl/crdt_data_filter.cpp +++ b/src/crdt/impl/crdt_data_filter.cpp @@ -5,11 +5,16 @@ * @author Henrique A. Klein (hklein@gnus.ai) */ #include "crdt/crdt_data_filter.hpp" +#include "crdt/globaldb/crdt_work_journal.hpp" #include namespace sgns::crdt { - CRDTDataFilter::CRDTDataFilter( bool accept_by_default ) : accept_by_default_( std::move( accept_by_default ) ) {} + CRDTDataFilter::CRDTDataFilter( std::shared_ptr work_journal, bool accept_by_default ) : + work_journal_( std::move( work_journal ) ), // + accept_by_default_( std::move( accept_by_default ) ) // + { + } bool CRDTDataFilter::RegisterElementFilter( const std::string &pattern, ElementFilterCallback filter ) { @@ -73,6 +78,10 @@ namespace sgns::crdt } } } + else + { + work_journal_->MarkSeen( element.key() ); + } filter_matched = true; break; } diff --git a/src/crdt/impl/crdt_datastore.cpp b/src/crdt/impl/crdt_datastore.cpp index c263f78aa..207379a74 100644 --- a/src/crdt/impl/crdt_datastore.cpp +++ b/src/crdt/impl/crdt_datastore.cpp @@ -371,8 +371,9 @@ namespace sgns::crdt namespaceKey_( aKey ), broadcaster_( std::move( aBroadcaster ) ), dagSyncer_( std::move( aDagSyncer ) ), - crdt_filter_( true ), - crdt_cb_manager_() + work_journal_( CRDTWorkJournal::New( dataStore_ ) ), + crdt_filter_( work_journal_, true ), + crdt_cb_manager_( work_journal_ ) { logger_ = options_->logger; numberOfDagWorkers = options_->numWorkers; @@ -1611,6 +1612,21 @@ namespace sgns::crdt return crdt_cb_manager_.RegisterDeletedDataCallback( pattern, std::move( callback ) ); } + void CrdtDatastore::UnregisterElementFilter( const std::string &pattern ) + { + crdt_filter_.UnregisterElementFilter( pattern ); + } + + void CrdtDatastore::UnregisterNewElementCallback( const std::string &pattern ) + { + crdt_cb_manager_.UnregisterNewDataCallback( pattern ); + } + + void CrdtDatastore::UnregisterDeletedElementCallback( const std::string &pattern ) + { + crdt_cb_manager_.UnregisterDeletedDataCallback( pattern ); + } + void CrdtDatastore::PutElementsCallback( const std::string &key, const Buffer &value, const std::string &cid ) { crdt_cb_manager_.PutDataCallback( key, value, cid ); @@ -1621,6 +1637,11 @@ namespace sgns::crdt crdt_cb_manager_.DeleteDataCallback( key, cid ); } + std::shared_ptr CrdtDatastore::GetWorkJournal() const + { + return work_journal_; + } + void CrdtDatastore::UpdateCRDTHeads( const CID &rootCID, uint64_t rootPriority, bool add_topics_to_broadcast ) { std::lock_guard lock( pendingHeadsMutex_ ); @@ -1789,4 +1810,27 @@ namespace sgns::crdt std::lock_guard lock( topicNamesMutex_ ); return topicNames_; } + + outcome::result>> CrdtDatastore::GetILPDNodeContent( + const std::string &cid_string ) + { + BOOST_OUTCOME_TRY( auto cid, CID::fromString( cid_string ) ); + + BOOST_OUTCOME_TRY( auto node, dagSyncer_->GetNodeWithoutRequest( cid ) ); + + //TODO - Check if filtering is needed here. Currently not filtering. + BOOST_OUTCOME_TRY( auto delta, GetDeltaFromNode( *node, true ) ); + + //TODO - Maybe check tombstones, right now just grabbing elements. + std::vector elements( delta.elements().begin(), delta.elements().end() ); + + std::vector> result; + for ( const auto &elem : elements ) + { + Buffer valueBuffer; + valueBuffer.put( elem.value() ); + result.emplace_back( elem.key(), valueBuffer ); + } + return result; + } } diff --git a/src/crdt/impl/crdt_work_journal.cpp b/src/crdt/impl/crdt_work_journal.cpp new file mode 100644 index 000000000..8ba84bdd2 --- /dev/null +++ b/src/crdt/impl/crdt_work_journal.cpp @@ -0,0 +1,326 @@ +#include "crdt/globaldb/crdt_work_journal.hpp" + +#include +#include + +#include "base/buffer.hpp" +#include "storage/rocksdb/rocksdb.hpp" + +namespace sgns::crdt +{ + std::shared_ptr CRDTWorkJournal::New( std::shared_ptr datastore ) + { + if ( !datastore ) + { + return nullptr; + } + return std::shared_ptr( new CRDTWorkJournal( std::move( datastore ) ) ); + } + + CRDTWorkJournal::CRDTWorkJournal( std::shared_ptr datastore ) : + datastore_( std::move( datastore ) ) + { + } + + void CRDTWorkJournal::MarkSeen( const std::string &key ) + { + if ( key.empty() ) + { + return; + } + std::lock_guard lock( mutex_ ); + auto maybe_entry = GetEntryUnlocked( key ); + + Entry entry; + if ( maybe_entry.has_value() ) + { + entry = maybe_entry.value(); + if ( entry.state == State::Processing ) + { + return; + } + } + entry.key = key; + entry.state = State::Seen; + entry.updated_at_ms = NowMs(); + entry.lease_until_ms = 0; + PutEntryUnlocked( entry ); + } + + void CRDTWorkJournal::MarkProcessing( const std::string &key, std::chrono::milliseconds lease ) + { + if ( key.empty() ) + { + return; + } + std::lock_guard lock( mutex_ ); + auto maybe_entry = GetEntryUnlocked( key ); + if ( !maybe_entry.has_value() ) + { + return; + } + + auto entry = maybe_entry.value(); + entry.state = State::Processing; + entry.updated_at_ms = NowMs(); + entry.lease_until_ms = entry.updated_at_ms + static_cast( std::max( 0, lease.count() ) ); + entry.attempt_count += 1; + PutEntryUnlocked( entry ); + } + + void CRDTWorkJournal::MarkStalled( const std::string &key, std::chrono::milliseconds lease ) + { + if ( key.empty() ) + { + return; + } + std::lock_guard lock( mutex_ ); + auto maybe_entry = GetEntryUnlocked( key ); + if ( !maybe_entry.has_value() ) + { + return; + } + + auto entry = maybe_entry.value(); + entry.state = State::Stalled; + entry.updated_at_ms = NowMs(); + entry.lease_until_ms = entry.updated_at_ms + static_cast( std::max( 0, lease.count() ) ); + entry.attempt_count += 1; + PutEntryUnlocked( entry ); + } + + bool CRDTWorkJournal::MarkDone( const std::string &key ) + { + if ( key.empty() ) + { + return false; + } + std::lock_guard lock( mutex_ ); + if ( !datastore_ ) + { + return false; + } + base::Buffer key_buf; + key_buf.put( BuildStorageKey( key ) ); + return datastore_->remove( key_buf ).has_value(); + } + + std::optional CRDTWorkJournal::GetEntry( const std::string &key ) const + { + if ( key.empty() ) + { + return std::nullopt; + } + std::lock_guard lock( mutex_ ); + return GetEntryUnlocked( key ); + } + + std::vector CRDTWorkJournal::ListUnfinished( std::string_view key_pattern ) const + { + std::lock_guard lock( mutex_ ); + std::vector out; + if ( !datastore_ ) + { + return out; + } + + base::Buffer prefix_buf; + prefix_buf.put( NAMESPACE_PREFIX ); + auto result = datastore_->query( prefix_buf ); + if ( result.has_error() ) + { + return out; + } + + std::optional pattern_regex; + if ( !key_pattern.empty() ) + { + try + { + pattern_regex.emplace( std::string( key_pattern ) ); + } + catch ( const std::regex_error & ) + { + return out; + } + } + + out.reserve( result.value().size() ); + for ( const auto &[raw_key, raw_value] : result.value() ) + { + auto parsed = DeserializeEntry( raw_key.toString(), raw_value.toString() ); + if ( parsed.has_value() ) + { + if ( pattern_regex.has_value() && !std::regex_match( parsed->key, pattern_regex.value() ) ) + { + continue; + } + out.push_back( std::move( parsed.value() ) ); + } + } + return out; + } + + size_t CRDTWorkJournal::RecoverStaleProcessing( std::string_view key_pattern, std::chrono::milliseconds stale ) + { + std::lock_guard lock( mutex_ ); + if ( !datastore_ ) + { + return 0; + } + + const uint64_t now_ms = NowMs(); + const uint64_t grace_ms = static_cast( std::max( 0, stale.count() ) ); + size_t recovered = 0; + + base::Buffer prefix_buf; + prefix_buf.put( NAMESPACE_PREFIX ); + auto result = datastore_->query( prefix_buf ); + if ( result.has_error() ) + { + return recovered; + } + std::optional pattern_regex; + if ( !key_pattern.empty() ) + { + try + { + pattern_regex.emplace( std::string( key_pattern ) ); + } + catch ( const std::regex_error & ) + { + return recovered; + } + } + + for ( const auto &[raw_key, raw_value] : result.value() ) + { + auto parsed = DeserializeEntry( raw_key.toString(), raw_value.toString() ); + if ( !parsed.has_value() ) + { + continue; + } + auto &entry = parsed.value(); + if ( pattern_regex.has_value() && !std::regex_match( entry.key, pattern_regex.value() ) ) + { + continue; + } + if ( entry.state != State::Processing ) + { + continue; + } + if ( entry.lease_until_ms != 0 && entry.lease_until_ms + grace_ms > now_ms ) + { + continue; + } + entry.state = State::Stalled; + entry.updated_at_ms = now_ms; + entry.lease_until_ms = 0; + PutEntryUnlocked( entry ); + recovered += 1; + } + return recovered; + } + + uint64_t CRDTWorkJournal::NowMs() + { + return static_cast( + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count() ); + } + + std::string CRDTWorkJournal::BuildStorageKey( const std::string &key ) const + { + return std::string( NAMESPACE_PREFIX ) + key; + } + + std::optional CRDTWorkJournal::DeserializeEntry( std::string_view storage_key, + std::string_view value ) + { + auto fields = Split( std::string( value ), '|' ); + if ( fields.size() != 5 || fields[0] != "v1" ) + { + return std::nullopt; + } + Entry entry; + try + { + const auto state = std::stoi( fields[1] ); + if ( state != static_cast( State::Seen ) && state != static_cast( State::Processing ) && + state != static_cast( State::Stalled ) ) + { + return std::nullopt; + } + entry.state = static_cast( state ); + entry.attempt_count = static_cast( std::stoull( fields[2] ) ); + entry.updated_at_ms = static_cast( std::stoull( fields[3] ) ); + entry.lease_until_ms = static_cast( std::stoull( fields[4] ) ); + } + catch ( ... ) + { + return std::nullopt; + } + + const auto key_str = std::string( storage_key ); + const auto pos = key_str.find( "/crdt/work/" ); + if ( pos == std::string::npos ) + { + return std::nullopt; + } + entry.key = key_str.substr( pos + std::string( "/crdt/work/" ).size() ); + return entry; + } + + std::string CRDTWorkJournal::SerializeEntry( const Entry &entry ) + { + return "v1|" + std::to_string( static_cast( entry.state ) ) + "|" + std::to_string( entry.attempt_count ) + + "|" + std::to_string( entry.updated_at_ms ) + "|" + std::to_string( entry.lease_until_ms ); + } + + std::vector CRDTWorkJournal::Split( const std::string &value, char separator ) + { + std::vector out; + size_t start = 0; + while ( true ) + { + const auto pos = value.find( separator, start ); + if ( pos == std::string::npos ) + { + out.push_back( value.substr( start ) ); + break; + } + out.push_back( value.substr( start, pos - start ) ); + start = pos + 1; + } + return out; + } + + std::optional CRDTWorkJournal::GetEntryUnlocked( const std::string &key ) const + { + if ( !datastore_ ) + { + return std::nullopt; + } + base::Buffer key_buf; + key_buf.put( BuildStorageKey( key ) ); + auto maybe_value = datastore_->get( key_buf ); + if ( maybe_value.has_error() ) + { + return std::nullopt; + } + return DeserializeEntry( BuildStorageKey( key ), maybe_value.value().toString() ); + } + + bool CRDTWorkJournal::PutEntryUnlocked( const Entry &entry ) const + { + if ( !datastore_ ) + { + return false; + } + base::Buffer key_buf; + key_buf.put( BuildStorageKey( entry.key ) ); + base::Buffer value_buf; + value_buf.put( SerializeEntry( entry ) ); + return datastore_->put( key_buf, value_buf ).has_value(); + } +} diff --git a/src/processing/impl/processing_task_queue_impl.cpp b/src/processing/impl/processing_task_queue_impl.cpp index 90dc2c9a8..e7ca4e593 100644 --- a/src/processing/impl/processing_task_queue_impl.cpp +++ b/src/processing/impl/processing_task_queue_impl.cpp @@ -198,6 +198,7 @@ namespace sgns::processing auto job_completion_transaction = m_db->BeginTransaction(); data.put( taskResult.SerializeAsString() ); BOOST_OUTCOME_TRY( job_completion_transaction->Put( std::move( result_key ), std::move( data ) ) ); + BOOST_OUTCOME_TRY( job_completion_transaction->AddTopic( m_processing_topic ) ); m_logger->debug( "TASK_COMPLETED: {}, results stored", taskKey ); return job_completion_transaction; diff --git a/src/processing/processing_subtask_queue_accessor_impl.cpp b/src/processing/processing_subtask_queue_accessor_impl.cpp index 411a74640..64d2a360b 100644 --- a/src/processing/processing_subtask_queue_accessor_impl.cpp +++ b/src/processing/processing_subtask_queue_accessor_impl.cpp @@ -151,6 +151,7 @@ namespace sgns::processing { std::lock_guard guard( m_mutexResults ); auto queue = m_subTaskQueueManager->GetQueueSnapshot(); + auto finalization_ret = FinalizationRetVal::NOT_FINALIZED; std::set subTaskIds; for ( size_t itemIdx = 0; itemIdx < static_cast( queue->subtasks().items_size() ); ++itemIdx ) @@ -171,8 +172,8 @@ namespace sgns::processing if ( isFullyProcessed ) { std::set invalidSubTaskIds; - auto finalized_ret = FinalizeQueueProcessing( queue->subtasks(), invalidSubTaskIds ); - if ( finalized_ret == FinalizationRetVal::NOT_FINALIZED ) + finalization_ret = FinalizeQueueProcessing( queue->subtasks(), invalidSubTaskIds ); + if ( finalization_ret == FinalizationRetVal::NOT_FINALIZED ) { m_subTaskQueueManager->ChangeSubTaskProcessingStates( processedSubTaskIds, false ); isFullyProcessed = false; @@ -183,6 +184,13 @@ namespace sgns::processing if ( !isFullyProcessed ) { m_subTaskQueueManager->GrabSubTask( onSubTaskGrabbedCallback ); + return; + } + + if ( finalization_ret == FinalizationRetVal::FINALIZED_BUT_NOT_OWNER ) + { + // The owner finalized using the received results; signal completion so this worker can shut down cleanly. + onSubTaskGrabbedCallback( boost::none ); } } diff --git a/test/src/CMakeLists.txt b/test/src/CMakeLists.txt index 38103675f..3f5fa4a05 100644 --- a/test/src/CMakeLists.txt +++ b/test/src/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(account_creation) add_subdirectory(account) add_subdirectory(base) +add_subdirectory(blockchain) add_subdirectory(crdt) add_subdirectory(crypto) add_subdirectory(graphsync) diff --git a/test/src/account/account_management_test.cpp b/test/src/account/account_management_test.cpp index 6bdeec296..80f94dcbc 100644 --- a/test/src/account/account_management_test.cpp +++ b/test/src/account/account_management_test.cpp @@ -10,6 +10,7 @@ #include "account/GeniusNode.hpp" #include "account/TransactionManager.hpp" #include "testutil/wait_condition.hpp" +#include "testutil/mint_source_hash.hpp" using namespace sgns::test; @@ -73,7 +74,7 @@ TEST_F( AccountManagement, CanSelectAccountThatWasAdded ) TEST_F( AccountManagement, TransferAccount ) { - ASSERT_TRUE( node_->MintTokens( 200, "", "", TOKEN_ID, "", GeniusNode::TIMEOUT_MINT ).has_value() ); + ASSERT_TRUE( node_->MintTokens( 200, sgns::test::NextMintSourceHash(), "", TOKEN_ID, "", GeniusNode::TIMEOUT_MINT ).has_value() ); auto balance = node_->GetBalance(); TW::HDWallet wallet( 128, "" ); auto other_account_address = GeniusAccount::NewFromMnemonic( TOKEN_ID, wallet.getMnemonic(), path, true ) @@ -287,7 +288,7 @@ TEST_F( AccountManagement, SetPayoutAddress ) boost::replace_all( json_data, "[basepath]", bin_path ); auto mint_result = node_requester->MintTokens( 50000000000, - "", + sgns::test::NextMintSourceHash(), "", TOKEN_ID, "", diff --git a/test/src/account/utxo_manager_test.cpp b/test/src/account/utxo_manager_test.cpp index f1dc09808..263cca4db 100644 --- a/test/src/account/utxo_manager_test.cpp +++ b/test/src/account/utxo_manager_test.cpp @@ -1,5 +1,7 @@ #include +#include + #include "base/blob.hpp" // for sgns::base::Hash256 #include "account/UTXOManager.hpp" #include "account/GeniusUTXO.hpp" @@ -181,6 +183,76 @@ TEST_F( UTXOManagerTest, Storage ) EXPECT_EQ( utxos.size(), 2 ); } +TEST_F( UTXOManagerTest, MerkleRootDeterministicAcrossInsertionOrder ) +{ + const std::array seed_a{ 0xA1 }; + const std::array seed_b{ 0xB2 }; + const auto hash_a = HASHER.sha2_256( gsl::span( seed_a ) ); + const auto hash_b = HASHER.sha2_256( gsl::span( seed_b ) ); + + std::vector ordered_a{ + GeniusUTXO( hash_a, 0, 100, TOKEN_1 ), + GeniusUTXO( hash_b, 1, 200, sgns::TokenID::FromBytes( { 0x02 } ) ), + }; + + std::vector ordered_b{ + GeniusUTXO( hash_b, 1, 200, sgns::TokenID::FromBytes( { 0x02 } ) ), + GeniusUTXO( hash_a, 0, 100, TOKEN_1 ), + }; + + ASSERT_TRUE( utxo_manager->SetUTXOs( ordered_a ).has_value() ); + auto root_a = utxo_manager->ComputeUTXOMerkleRoot(); + + ASSERT_TRUE( utxo_manager->SetUTXOs( ordered_b ).has_value() ); + auto root_b = utxo_manager->ComputeUTXOMerkleRoot(); + + EXPECT_EQ( root_a, root_b ); +} + +TEST_F( UTXOManagerTest, MerkleRootChangesWhenUTXOSetChanges ) +{ + const std::array seed_a{ 0xC3 }; + const std::array seed_b{ 0xD4 }; + const auto hash_a = HASHER.sha2_256( gsl::span( seed_a ) ); + const auto hash_b = HASHER.sha2_256( gsl::span( seed_b ) ); + + EXPECT_TRUE( utxo_manager->PutUTXO( GeniusUTXO( hash_a, 0, 55, TOKEN_1 ) ) ); + EXPECT_TRUE( utxo_manager->PutUTXO( GeniusUTXO( hash_b, 1, 77, sgns::TokenID::FromBytes( { 0x03 } ) ) ) ); + + const auto root_before = utxo_manager->ComputeUTXOMerkleRoot(); + + InputUTXOInfo spent; + spent.txid_hash_ = hash_a; + spent.output_idx_ = 0; + utxo_manager->ConsumeUTXOs( { spent } ); + + const auto root_after = utxo_manager->ComputeUTXOMerkleRoot(); + EXPECT_NE( root_before, root_after ); +} + +TEST_F( UTXOManagerTest, CheckpointRoundtrip ) +{ + const std::array seed_tx{ 0x11 }; + const std::array seed_registry{ 0x22 }; + const auto tx_hash = HASHER.sha2_256( gsl::span( seed_tx ) ); + const auto registry_hash = HASHER.sha2_256( gsl::span( seed_registry ) ); + + EXPECT_TRUE( utxo_manager->PutUTXO( GeniusUTXO( tx_hash, 0, 123, TOKEN_1 ) ) ); + + ASSERT_TRUE( utxo_manager->CreateCheckpoint( 7, tx_hash, registry_hash ).has_value() ); + auto checkpoint_res = utxo_manager->LoadLatestCheckpoint(); + ASSERT_TRUE( checkpoint_res.has_value() ); + ASSERT_TRUE( checkpoint_res.value().has_value() ); + + const auto &checkpoint = checkpoint_res.value().value(); + EXPECT_EQ( checkpoint.owner_address, std::string( PRIV_KEY ) ); + EXPECT_EQ( checkpoint.epoch, 7u ); + EXPECT_EQ( checkpoint.last_finalized_tx, tx_hash ); + EXPECT_EQ( checkpoint.registry_hash, registry_hash ); + EXPECT_EQ( checkpoint.utxo_count, 1u ); + EXPECT_GT( checkpoint.created_at_ms, 0u ); +} + TEST( GeniusUTXO, PropertyAccessors ) { uint32_t idx = 5; @@ -191,7 +263,7 @@ TEST( GeniusUTXO, PropertyAccessors ) EXPECT_EQ( utxo.GetOutputIdx(), idx ); EXPECT_EQ( utxo.GetAmount(), amt ); EXPECT_EQ( utxo.GetTokenID(), tok ); - EXPECT_FALSE( utxo.GetLock() ); + EXPECT_TRUE( utxo.GetOwnerAddress().empty() ); } TEST( InputUTXOInfo, FieldAssignment ) diff --git a/test/src/blockchain/CMakeLists.txt b/test/src/blockchain/CMakeLists.txt index 2be885a68..740f73136 100644 --- a/test/src/blockchain/CMakeLists.txt +++ b/test/src/blockchain/CMakeLists.txt @@ -1,47 +1,22 @@ -addtest(block_header_repository_test - block_header_repository_test.cpp -) -target_link_libraries(block_header_repository_test - block_header_repository - base_rocksdb_test - base_crdt_test - hasher - blockchain_common -) - -addtest(block_tree_test - block_tree_test.cpp -) -target_link_libraries(block_tree_test - block_tree - block_header_repository - extrinsic_observer -) -if(FORCE_MULTIPLE) - set_target_properties(block_tree_test PROPERTIES LINK_FLAGS "${MULTIPLE_OPTION}") -endif() +addtest(blockchain_genesis_test + blockchain_genesis_test.cpp + ) -addtest(block_storage_test - block_storage_test.cpp +addtest(consensus_certificate_test + consensus_certificate_test.cpp ) -target_link_libraries(block_storage_test - block_storage +target_link_libraries(consensus_certificate_test + blockchain_genesis + rapidjson base_crdt_test ) -if(FORCE_MULTIPLE) - set_target_properties(block_storage_test PROPERTIES LINK_FLAGS "${MULTIPLE_OPTION}") -endif() - -addtest(blockchain_genesis_test - blockchain_genesis_test.cpp - ) - target_include_directories(blockchain_genesis_test PRIVATE ${AsyncIOManager_INCLUDE_DIR}) target_link_libraries(blockchain_genesis_test genius_node + rapidjson ) if(WIN32) target_link_options(blockchain_genesis_test PUBLIC /WHOLEARCHIVE:$) diff --git a/test/src/blockchain/blockchain_genesis_test.cpp b/test/src/blockchain/blockchain_genesis_test.cpp index cd27f6b97..c2c10dcdb 100644 --- a/test/src/blockchain/blockchain_genesis_test.cpp +++ b/test/src/blockchain/blockchain_genesis_test.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef _WIN32 //#include @@ -22,11 +23,12 @@ #include #include #include -#include "local_secure_storage/impl/json/JSONSecureStorage.hpp" +#include "local_secure_storage/SecureStorage.hpp" #include "account/GeniusNode.hpp" #include "FileManager.hpp" #include #include +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" class BlockchainGenesisTest : public ::testing::Test @@ -296,11 +298,11 @@ TEST_F( BlockchainGenesisTest, WithAuthorizationCanSyncAndProcessTransactions ) // Mint tokens on the first regular node after sync is confirmed auto mint_result = node_regular_1->MintTokens( mint_amount, - "", + sgns::test::NextMintSourceHash(), "", token_id, "", - std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); + std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; auto [mint_tx_id, mint_duration] = mint_result.value(); diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp new file mode 100644 index 000000000..4964ab7cc --- /dev/null +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -0,0 +1,624 @@ +#include + +#define private public +#include "blockchain/Consensus.hpp" +#undef private + +#include "account/GeniusAccount.hpp" +#include "blockchain/ValidatorRegistry.hpp" +#include "testutil/storage/base_crdt_test.hpp" +#include "testutil/wait_condition.hpp" + +namespace +{ + constexpr const char *kTestPrivateKey = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce8b1a6f0d4f3b9b7f0a1b2"; + constexpr const char *kTestPrivateKey2 = "0x6c3e7b1a8d3f2c9b0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2"; + + std::shared_ptr MakeAccount( const std::string &path ) + { + auto account = sgns::GeniusAccount::New( sgns::TokenID::FromBytes( { 0x00 } ), kTestPrivateKey, path, false ); + EXPECT_TRUE( account ); + if ( !account ) + { + return nullptr; + } + return account; + } + + std::shared_ptr MakeAccount( const std::string &path, const char *private_key ) + { + auto account = sgns::GeniusAccount::New( sgns::TokenID::FromBytes( { 0x00 } ), private_key, path, false ); + EXPECT_TRUE( account ); + if ( !account ) + { + return nullptr; + } + return account; + } + + std::shared_ptr MakeRegistry( const std::shared_ptr &db, + const std::shared_ptr &account ) + { + using sgns::ValidatorRegistry; + if ( !db || !account ) + { + ADD_FAILURE() << "MakeRegistry received null dependency"; + return nullptr; + } + auto registry = ValidatorRegistry::New( + db, + 1, + 1, + ValidatorRegistry::WeightConfig{}, + account->GetAddress(), + []( const std::string &, std::function )> cb ) + { cb( outcome::failure( std::errc::not_supported ) ); } ); + EXPECT_TRUE( registry ); + + auto store_result = registry->StoreGenesisRegistry( account->GetAddress(), + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + EXPECT_FALSE( store_result.has_error() ); + + ASSERT_WAIT_FOR_CONDITION( + [®istry]() + { + auto load = registry->LoadRegistry(); + return load.has_value() && !registry->GetRegistryCid().empty(); + }, + std::chrono::milliseconds( 2000 ), + "registry initialized", + nullptr ); + + return registry; + } + + std::shared_ptr MakeManager( const std::shared_ptr ®istry, + const std::shared_ptr &db, + const std::shared_ptr &pubs, + const std::shared_ptr &account ) + { + if ( !registry || !db || !pubs || !account ) + { + ADD_FAILURE() << "MakeManager received null dependency"; + return nullptr; + } + auto manager = sgns::ConsensusManager::New( + registry, + db, + pubs, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + account->GetAddress() ); + EXPECT_TRUE( manager ); + return manager; + } + + sgns::UTXOTransitionCommitment MakeTestCommitment() + { + sgns::UTXOTransitionCommitment commitment; + auto *consumed = commitment.add_consumed_outpoints(); + consumed->set_tx_id_hash( std::string( 32, '\x01' ) ); + consumed->set_output_index( 0 ); + auto *produced = commitment.add_produced_outputs(); + produced->set_tx_id_hash( std::string( 32, '\x02' ) ); + produced->set_output_index( 0 ); + produced->set_owner_address( "owner" ); + produced->set_token_id( std::string( 32, '\x03' ) ); + produced->set_amount( 1 ); + commitment.set_consumed_outpoints_root( std::string( 32, '\x05' ) ); + commitment.set_produced_outputs_root( std::string( 32, '\x04' ) ); + return commitment; + } + + sgns::UTXOWitness MakeTestWitness() + { + sgns::UTXOWitness witness; + return witness; + } +} + +namespace sgns::test +{ + class ConsensusCertificateTest : public ::test::CRDTFixture + { + public: + ConsensusCertificateTest() : CRDTFixture( "ConsensusCertificateTest" ) {} + + static void SetUpTestSuite() + { + CRDTFixture::SetUpTestSuite(); + } + }; + + TEST_F( ConsensusCertificateTest, CreateCertificateEmbedsProposal ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + std::string tx_hash = "0x010203"; + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 1, + tx_hash, + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + + auto proposal_result = manager->CreateProposal( subject_result.value(), + account->GetAddress(), + registry->GetRegistryCid(), + registry->GetRegistryEpoch() ); + ASSERT_TRUE( proposal_result.has_value() ); + + auto vote_result = manager->CreateVote( proposal_result.value().proposal_id(), + account->GetAddress(), + true, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( vote_result.has_value() ); + + auto cert_result = manager->CreateCertificate( proposal_result.value(), { vote_result.value() } ); + ASSERT_TRUE( cert_result.has_value() ); + + const auto &cert = cert_result.value(); + EXPECT_TRUE( cert.has_proposal() ); + EXPECT_EQ( cert.proposal().proposal_id(), proposal_result.value().proposal_id() ); + } + + TEST_F( ConsensusCertificateTest, HandleCertificateRejectsMismatchedProposal ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + std::string tx_hash = "0x010203"; + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 7, + tx_hash, + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + + auto proposal_result = manager->CreateProposal( subject_result.value(), + account->GetAddress(), + registry->GetRegistryCid(), + registry->GetRegistryEpoch(), + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( proposal_result.has_value() ); + + auto vote_result = manager->CreateVote( proposal_result.value().proposal_id(), + account->GetAddress(), + true, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( vote_result.has_value() ); + + auto cert_result = manager->CreateCertificate( proposal_result.value(), { vote_result.value() } ); + ASSERT_TRUE( cert_result.has_value() ); + + auto cert = cert_result.value(); + + manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, + []( const ConsensusManager::Subject & ) + { return ConsensusManager::Check::Approve; } ); + manager->HandleProposal( proposal_result.value() ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) != manager->proposals_.end() ); + manager->HandleCertificate( cert ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) == manager->proposals_.end() ); + + manager->HandleProposal( proposal_result.value() ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) != manager->proposals_.end() ); + auto *bad_subject = cert.mutable_proposal()->mutable_subject()->mutable_nonce(); + bad_subject->set_nonce( bad_subject->nonce() + 1 ); + + manager->HandleCertificate( cert ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) != manager->proposals_.end() ); + } + + TEST_F( ConsensusCertificateTest, NewRejectsInvalidInputs ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + + EXPECT_EQ( ConsensusManager::New( + nullptr, + db_, + pubs_, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + account->GetAddress() ), + nullptr ); + EXPECT_EQ( ConsensusManager::New( + registry, + nullptr, + pubs_, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + account->GetAddress() ), + nullptr ); + EXPECT_EQ( ConsensusManager::New( + registry, + db_, + nullptr, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + account->GetAddress() ), + nullptr ); + EXPECT_EQ( ConsensusManager::New( registry, db_, pubs_, nullptr, account->GetAddress() ), nullptr ); + EXPECT_EQ( ConsensusManager::New( + registry, + db_, + pubs_, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + "" ), + nullptr ); + } + + TEST_F( ConsensusCertificateTest, RegisterAndUnregisterHandlers ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + EXPECT_TRUE( manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, + []( const ConsensusManager::Subject & ) + { return ConsensusManager::Check::Approve; } ) ); + EXPECT_TRUE( + manager->RegisterCertificateHandler( SubjectType::SUBJECT_NONCE, + []( const std::string &, const ConsensusManager::Certificate & ) + { return outcome::success( ConsensusManager::Check::Approve ); } ) ); + EXPECT_TRUE( manager->subject_handlers_.find( static_cast( SubjectType::SUBJECT_NONCE ) ) != + manager->subject_handlers_.end() ); + EXPECT_TRUE( manager->certificate_subject_handlers_.find( static_cast( SubjectType::SUBJECT_NONCE ) ) != + manager->certificate_subject_handlers_.end() ); + + manager->UnregisterSubjectHandler( SubjectType::SUBJECT_NONCE ); + manager->UnregisterCertificateHandler( SubjectType::SUBJECT_NONCE ); + EXPECT_TRUE( manager->subject_handlers_.find( static_cast( SubjectType::SUBJECT_NONCE ) ) == + manager->subject_handlers_.end() ); + EXPECT_TRUE( manager->certificate_subject_handlers_.find( static_cast( SubjectType::SUBJECT_NONCE ) ) == + manager->certificate_subject_handlers_.end() ); + } + + TEST_F( ConsensusCertificateTest, CreateVoteBundleAndSigningBytes ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 2, + "0x0a0b0c", + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + + auto proposal_result = manager->CreateProposal( subject_result.value(), + account->GetAddress(), + registry->GetRegistryCid(), + registry->GetRegistryEpoch() ); + ASSERT_TRUE( proposal_result.has_value() ); + + auto vote_result = manager->CreateVote( proposal_result.value().proposal_id(), + account->GetAddress(), + true, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( vote_result.has_value() ); + + auto bundle_result = manager->CreateVoteBundle( proposal_result.value().proposal_id(), + account->GetAddress(), + { vote_result.value() }, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( bundle_result.has_value() ); + EXPECT_EQ( bundle_result.value().votes_size(), 1 ); + + auto proposal_bytes = ConsensusManager::ProposalSigningBytes( proposal_result.value() ); + ASSERT_TRUE( proposal_bytes.has_value() ); + EXPECT_FALSE( proposal_bytes.value().empty() ); + + auto vote_bytes = ConsensusManager::VoteSigningBytes( vote_result.value() ); + ASSERT_TRUE( vote_bytes.has_value() ); + EXPECT_FALSE( vote_bytes.value().empty() ); + + auto bundle_bytes = ConsensusManager::VoteBundleSigningBytes( bundle_result.value() ); + ASSERT_TRUE( bundle_bytes.has_value() ); + EXPECT_FALSE( bundle_bytes.value().empty() ); + } + + TEST_F( ConsensusCertificateTest, CreateTaskResultSubjectAndComputeSubjectId ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto subject_result = ConsensusManager::CreateTaskResultSubject( account->GetAddress(), + "escrow/path", + "0xdeadbeef", + 12 ); + ASSERT_TRUE( subject_result.has_value() ); + EXPECT_FALSE( subject_result.value().subject_id().empty() ); + + auto computed = ConsensusManager::ComputeSubjectId( subject_result.value() ); + ASSERT_TRUE( computed.has_value() ); + EXPECT_EQ( computed.value(), subject_result.value().subject_id() ); + } + + TEST_F( ConsensusCertificateTest, TallyVotesWithRegistry ) + { + auto account = MakeAccount( getPathString() ); + auto account2 = MakeAccount( getPathString() + "/acc2", kTestPrivateKey2 ); + ASSERT_TRUE( account ); + ASSERT_TRUE( account2 ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 3, + "0x111213", + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + + auto proposal_result = manager->CreateProposal( subject_result.value(), + account->GetAddress(), + registry->GetRegistryCid(), + registry->GetRegistryEpoch() ); + ASSERT_TRUE( proposal_result.has_value() ); + + auto vote_result = manager->CreateVote( proposal_result.value().proposal_id(), + account->GetAddress(), + true, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( vote_result.has_value() ); + + auto vote2_result = manager->CreateVote( proposal_result.value().proposal_id(), + account2->GetAddress(), + true, + [account2]( std::vector payload ) + { return account2->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( vote2_result.has_value() ); + + auto registry_result = registry->LoadRegistry(); + ASSERT_TRUE( registry_result.has_value() ); + + auto tally = manager->TallyVotes( proposal_result.value(), + { vote_result.value(), vote2_result.value() }, + registry_result.value(), + registry->GetRegistryCid() ); + ASSERT_TRUE( tally.has_value() ); + EXPECT_TRUE( tally.value().has_quorum ); + EXPECT_EQ( tally.value().total_weight, ValidatorRegistry::TotalWeight( registry_result.value() ) ); + auto *validator = ValidatorRegistry::FindValidator( registry_result.value(), account->GetAddress() ); + ASSERT_TRUE( validator ); + EXPECT_EQ( tally.value().approved_weight, validator->weight() ); + + auto tally_mismatch = manager->TallyVotes( proposal_result.value(), + { vote_result.value() }, + registry_result.value(), + "bad-cid" ); + EXPECT_TRUE( tally_mismatch.has_error() ); + } + + TEST_F( ConsensusCertificateTest, SubmitProposalVoteCertificateAndProcess ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 4, + "0x222324", + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + + auto proposal_result = manager->CreateProposal( subject_result.value(), + account->GetAddress(), + registry->GetRegistryCid(), + registry->GetRegistryEpoch() ); + ASSERT_TRUE( proposal_result.has_value() ); + + manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, + []( const ConsensusManager::Subject & ) + { return ConsensusManager::Check::Approve; } ); + + auto submit_prop = manager->SubmitProposal( proposal_result.value(), false ); + EXPECT_FALSE( submit_prop.has_error() ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) != manager->proposals_.end() ); + + auto vote_result = manager->CreateVote( proposal_result.value().proposal_id(), + account->GetAddress(), + true, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( vote_result.has_value() ); + + auto submit_vote = manager->SubmitVote( vote_result.value() ); + EXPECT_FALSE( submit_vote.has_error() ); + + manager->HandleProposal( proposal_result.value() ); + manager->HandleVote( vote_result.value() ); + EXPECT_TRUE( manager->proposals_.at( proposal_result.value().proposal_id() ).quorum_reached ); + + manager->ProcessCertificates(); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) == manager->proposals_.end() ); + } + + TEST_F( ConsensusCertificateTest, ResumeProposalHandlingFromPending ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 5, + "0x333435", + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + + auto proposal_result = manager->CreateProposal( subject_result.value(), + account->GetAddress(), + registry->GetRegistryCid(), + registry->GetRegistryEpoch() ); + ASSERT_TRUE( proposal_result.has_value() ); + + manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, + []( const ConsensusManager::Subject & ) + { return ConsensusManager::Check::Pending; } ); + manager->HandleProposal( proposal_result.value() ); + EXPECT_TRUE( manager->pending_proposals_.find( proposal_result.value().proposal_id() ) != + manager->pending_proposals_.end() ); + + manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, + []( const ConsensusManager::Subject & ) + { return ConsensusManager::Check::Approve; } ); + + auto resume = manager->ResumeProposalHandling( subject_result.value().nonce().tx_hash() ); + EXPECT_FALSE( resume.has_error() ); + EXPECT_TRUE( manager->pending_proposals_.find( proposal_result.value().proposal_id() ) == + manager->pending_proposals_.end() ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) != manager->proposals_.end() ); + } + + TEST_F( ConsensusCertificateTest, SubmitCertificateStoresInCrdt ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + std::string tx_hash = "0x444546"; + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 6, + tx_hash, + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + + auto proposal_result = manager->CreateProposal( subject_result.value(), + account->GetAddress(), + registry->GetRegistryCid(), + registry->GetRegistryEpoch() ); + ASSERT_TRUE( proposal_result.has_value() ); + + auto vote_result = manager->CreateVote( proposal_result.value().proposal_id(), + account->GetAddress(), + true, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); + ASSERT_TRUE( vote_result.has_value() ); + + auto cert_result = manager->CreateCertificate( proposal_result.value(), { vote_result.value() } ); + ASSERT_TRUE( cert_result.has_value() ); + + std::atomic handler_called{ false }; + manager->RegisterCertificateHandler( + SubjectType::SUBJECT_NONCE, + [&handler_called, &tx_hash]( const std::string &subject_hash, const ConsensusManager::Certificate & ) + { + if ( subject_hash == tx_hash ) + { + handler_called.store( true ); + } + return outcome::success( ConsensusManager::Check::Approve ); + } ); + + auto submit_result = manager->SubmitCertificate( cert_result.value() ); + EXPECT_FALSE( submit_result.has_error() ); + + crdt::HierarchicalKey cert_key( "/cert/" + tx_hash ); + auto cert_get = db_->Get( cert_key ); + EXPECT_TRUE( cert_get.has_value() ); + + ASSERT_WAIT_FOR_CONDITION( [&handler_called]() { return handler_called.load(); }, + std::chrono::milliseconds( 2000 ), + "certificate handler", + nullptr ); + } + + TEST_F( ConsensusCertificateTest, ValidateSubjectRejectsTamperedSubjectIdBinding ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 11, + "0xabc123", + MakeTestCommitment(), + MakeTestWitness() ); + ASSERT_TRUE( subject_result.has_value() ); + auto subject = subject_result.value(); + + ASSERT_TRUE( manager->ValidateSubject( subject ) ); + + subject.mutable_nonce()->set_nonce( subject.nonce().nonce() + 1 ); + EXPECT_FALSE( manager->ValidateSubject( subject ) ); + } + + TEST_F( ConsensusCertificateTest, ValidateSubjectRejectsTamperedWitnessWithStaleSubjectId ) + { + auto account = MakeAccount( getPathString() ); + ASSERT_TRUE( account ); + auto registry = MakeRegistry( db_, account ); + ASSERT_TRUE( registry ); + auto manager = MakeManager( registry, db_, pubs_, account ); + ASSERT_TRUE( manager ); + + UTXOTransitionCommitment commitment; + auto *consumed = commitment.add_consumed_outpoints(); + consumed->set_tx_id_hash( std::string( 32, '\x01' ) ); + consumed->set_output_index( 0 ); + commitment.set_consumed_outpoints_root( std::string( 32, '\x02' ) ); + commitment.set_produced_outputs_root( std::string( 32, '\x03' ) ); + + UTXOWitness witness; + auto *proof = witness.add_consumed_inputs(); + proof->set_tx_id_hash( std::string( 32, '\x03' ) ); + proof->set_output_index( 0 ); + proof->set_leaf_payload( "leaf" ); + + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 1, + "0xdeadbeef", + commitment, + witness ); + ASSERT_TRUE( subject_result.has_value() ); + auto subject = subject_result.value(); + + ASSERT_TRUE( manager->ValidateSubject( subject ) ); + + auto *tampered = subject.mutable_nonce()->mutable_utxo_witness()->mutable_consumed_inputs( 0 ); + tampered->set_output_index( 9 ); + EXPECT_FALSE( manager->ValidateSubject( subject ) ); + } +} // namespace sgns::test diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index ae14169d7..5d33a4da6 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef _WIN32 //#include @@ -23,11 +24,17 @@ #include #include #include "local_secure_storage/impl/json/JSONSecureStorage.hpp" +#define private public +#define protected public #include "account/GeniusNode.hpp" +#undef private +#undef protected #include "FileManager.hpp" #include #include +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" +#include "blockchain/ValidatorRegistry.hpp" class MultiAccountTest : public ::testing::Test { @@ -136,7 +143,7 @@ class MultiAccountTest : public ::testing::Test } }; -TEST_F( MultiAccountTest, SyncThroughEachOther ) +TEST_F( MultiAccountTest, DISABLED_SyncThroughEachOther ) { // Create nodes dynamically auto node_full = CreateNode( "node_multi_full", @@ -167,7 +174,7 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) auto balance_original_start = node_original->GetBalance(); // Mint some tokens auto mint_result = node_original->MintTokens( 100, - "", + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", @@ -175,14 +182,14 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_original"; mint_result = node_original->MintTokens( 2000, - "", + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_original"; mint_result = node_original->MintTokens( 30, - "", + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", @@ -207,7 +214,7 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) "node_duplicated not synced" ); mint_result = node_duplicated->MintTokens( 60000, - "", + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", @@ -218,11 +225,15 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) [&] { return ( balance_original_start + 60000 + 2000 + 100 + 30 ) == node_duplicated->GetBalance(); }, std::chrono::milliseconds( 30000 ), "node_duplicated balance not synced" ); + test::assertWaitForCondition( + [&] { return ( balance_original_start + 60000 + 2000 + 100 + 30 ) == node_original->GetBalance(); }, + std::chrono::milliseconds( 30000 ), + "node_duplicated balance not synced" ); ASSERT_EQ( node_duplicated->GetBalance(), node_original->GetBalance() ); } -TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) +TEST_F( MultiAccountTest, DISABLED_CRDTFilterDuplicateTx ) { // Create 3 nodes - 2 with the same address, 1 different (full node for network) auto node_full = CreateNode( "full_node_address_unique", // different self_address @@ -285,9 +296,11 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) balance_full_start ); // Get initial transaction counts - auto tx_count_node1_start = node_same_addr_1->GetOutTransactions().size(); - auto tx_count_node2_start = node_same_addr_2->GetOutTransactions().size(); - auto tx_count_full_start = node_full->GetOutTransactions().size(); + auto tx_count_node1_start = node_same_addr_1->GetTransactions( TransactionManager::TransactionStatus::CONFIRMED ) + .size(); + auto tx_count_node2_start = node_same_addr_2->GetTransactions( TransactionManager::TransactionStatus::CONFIRMED ) + .size(); + auto tx_count_full_start = node_full->GetTransactions( TransactionManager::TransactionStatus::CONFIRMED ).size(); fmt::println( "Initial tx counts - Node1: {}, Node2: {}, Full: {}", tx_count_node1_start, @@ -298,9 +311,9 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) std::cout << "Minting tokens on isolated nodes..." << std::endl; auto mint_result_1 = node_same_addr_1->MintTokens( 50000000000, // 50 GNUS + sgns::test::NextMintSourceHash(), "", - "", - sgns::TokenID::FromBytes( { 0x00 } ), + sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( mint_result_1.has_value() ) << "Mint transaction failed on node_same_addr_1"; @@ -349,14 +362,43 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) transfer1_res.value(), std::chrono::milliseconds( INCOMING_TIMEOUT_MILLISECONDS ) ); + fmt::println( "Waiting for the conflict resolution" ); + + uint64_t correct_tokens_transferred = 0; test::assertWaitForCondition( - [&]() { return node_same_addr_2->GetBalance() == ( balance_node1_after_mint - 10000000000 ); }, + [&]() + { + auto status1 = node_same_addr_1->GetTransactionStatus( transfer1_res.value() ); + if ( status1 == TransactionManager::TransactionStatus::CONFIRMED ) + { + correct_tokens_transferred = 10000000000; + return true; + } + + auto status2 = node_same_addr_2->GetTransactionStatus( transfer2_res.value() ); + if ( status2 == TransactionManager::TransactionStatus::CONFIRMED ) + { + correct_tokens_transferred = 13000000000; + return true; + } + + return false; + }, + std::chrono::milliseconds( 50000 ), + "Neither transfer was confirmed" ); + + test::assertWaitForCondition( + [&]() { return node_same_addr_1->GetBalance() == ( balance_node1_after_mint - correct_tokens_transferred ); }, std::chrono::milliseconds( 50000 ), - "node_same_addr_2 balance not synced" ); + "node_same_addr_1 balance not synced" ); test::assertWaitForCondition( [&]() { return node_same_addr_2->GetBalance() == node_same_addr_1->GetBalance(); }, std::chrono::milliseconds( 50000 ), "node_same_addr_2 balance not synced" ); + fmt::println( "Balances after bootstrap - Node1: {}, Node2: {}", + node_same_addr_2->GetBalance(), + node_same_addr_1->GetBalance() ); + std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); // Get final balances after CRDT resolution @@ -370,8 +412,10 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) balance_full_final ); // Get final transaction counts - auto tx_count_node1_final = node_same_addr_1->GetOutTransactions().size(); - auto tx_count_node2_final = node_same_addr_2->GetOutTransactions().size(); + auto tx_count_node1_final = node_same_addr_1->GetTransactions( TransactionManager::TransactionStatus::CONFIRMED ) + .size(); + auto tx_count_node2_final = node_same_addr_2->GetTransactions( TransactionManager::TransactionStatus::CONFIRMED ) + .size(); fmt::println( "Final tx counts - Node1: {}, Node2: {}", tx_count_node1_final, tx_count_node2_final ); @@ -383,3 +427,423 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) std::cout << "CRDT Filter test completed successfully!" << std::endl; } + +TEST_F( MultiAccountTest, NodeConsensusTest ) +{ + constexpr size_t kCertificatesPerBatch = 1; + const auto kCertificateDelay = std::chrono::seconds( 7 ); + + auto configure_consensus_batch_and_delay = [&]( const std::shared_ptr &node ) + { + test::assertWaitForCondition( + [&]() + { + return node && node->blockchain_ && node->blockchain_->consensus_manager_ && + node->GetTransactionManagerState() == TransactionManager::State::READY; + }, + std::chrono::milliseconds( 30000 ), + "node blockchain not ready for consensus configuration" ); + + auto node_registry = node->blockchain_->GetValidatorRegistry(); + ASSERT_TRUE( node_registry ); + + node_registry->SetCertificatesPerBatch( kCertificatesPerBatch ); + node->blockchain_->consensus_manager_->ConfigureCertificateDelay( kCertificateDelay ); + }; + + auto node_full = CreateNode( "node_consensus_full", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + true, // is full node + true, // is processor + true ); // is genesis authorized + + test::assertWaitForCondition( + [&]() { return node_full->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_full not synced" ); + + auto node_client = CreateNode( "node_consensus_client", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, // not full node + false // not processor + ); + + auto node_peer1 = CreateNode( "node_consensus_peer1", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + auto node_peer2 = CreateNode( "node_consensus_peer2", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + auto node_peer3 = CreateNode( "node_consensus_peer3", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + + node_client->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer1->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer2->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer3->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + test::assertWaitForCondition( + [&]() { return node_client->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_client not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer1->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer1 not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer2->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer2 not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer3->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer3 not synced" ); + + configure_consensus_batch_and_delay( node_full ); + configure_consensus_batch_and_delay( node_client ); + configure_consensus_batch_and_delay( node_peer1 ); + configure_consensus_batch_and_delay( node_peer2 ); + configure_consensus_batch_and_delay( node_peer3 ); + + ASSERT_TRUE( node_full->blockchain_ ); + auto registry = node_full->blockchain_->GetValidatorRegistry(); + ASSERT_TRUE( registry ); + + fmt::println( "Nodes created. Registry loaded" ); + test::assertWaitForCondition( + [&]() + { + auto load = registry->LoadRegistry(); + return load.has_value() && !registry->GetRegistryCid().empty(); + }, + std::chrono::milliseconds( 30000 ), + "validator registry not initialized" ); + + fmt::println( "Registry CID: {}", registry->GetRegistryCid() ); + auto assert_registry_updated = [&]( uint64_t epoch_before, const std::string &cid_before ) + { + test::assertWaitForCondition( + [&]() + { + auto load = registry->LoadRegistry(); + return load.has_value() && + ( load.value().epoch() > epoch_before || registry->GetRegistryCid() != cid_before ); + }, + std::chrono::milliseconds( 30000 ), + "validator registry did not update" ); + + auto registry_after = registry->LoadRegistry(); + ASSERT_TRUE( registry_after.has_value() ); + EXPECT_GT( registry_after.value().epoch(), epoch_before ); + EXPECT_NE( registry->GetRegistryCid(), cid_before ); + + if ( registry_after.value().validators().size() > 0 ) + { + auto *full_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_full->GetAddress() ); + ASSERT_TRUE( full_validator ); + EXPECT_GT( full_validator->weight(), 0 ); + } + if ( registry_after.value().validators().size() > 1 ) + { + auto *client_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_client->GetAddress() ); + ASSERT_TRUE( client_validator ); + EXPECT_GT( client_validator->weight(), 0 ); + } + if ( registry_after.value().validators().size() > 2 ) + { + auto *peer1_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_peer1->GetAddress() ); + ASSERT_TRUE( peer1_validator ); + EXPECT_GT( peer1_validator->weight(), 0 ); + } + if ( registry_after.value().validators().size() > 3 ) + { + auto *peer2_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_peer2->GetAddress() ); + ASSERT_TRUE( peer2_validator ); + EXPECT_GT( peer2_validator->weight(), 0 ); + } + if ( registry_after.value().validators().size() > 4 ) + { + auto *peer3_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_peer3->GetAddress() ); + ASSERT_TRUE( peer3_validator ); + + EXPECT_GT( peer3_validator->weight(), 0 ); + } + }; + + auto wait_client_registry_caught_up = [&]() + { + ASSERT_TRUE( node_client->blockchain_ ); + auto client_registry = node_client->blockchain_->GetValidatorRegistry(); + ASSERT_TRUE( client_registry ); + + test::assertWaitForCondition( + [&]() + { + auto full_load = registry->LoadRegistry(); + auto client_load = client_registry->LoadRegistry(); + return full_load.has_value() && client_load.has_value() && + client_registry->GetRegistryCid() == registry->GetRegistryCid() && + client_load.value().epoch() >= full_load.value().epoch(); + }, + std::chrono::milliseconds( 30000 ), + "node_client validator registry not caught up" ); + }; + + auto registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + auto epoch_before = registry_state.value().epoch(); + auto cid_before = registry->GetRegistryCid(); + + auto mint1 = node_client->MintTokens( 100, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; + fmt::println( "Mint 1 succeeded" ); + + assert_registry_updated( epoch_before, cid_before ); + wait_client_registry_caught_up(); + + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + + auto mint2 = node_client->MintTokens( 250, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + ASSERT_TRUE( mint2.has_value() ) << "Mint 2 failed on node_client"; + fmt::println( "Mint 2 succeeded" ); + assert_registry_updated( epoch_before, cid_before ); + wait_client_registry_caught_up(); + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + + auto transfer1 = node_client->TransferFunds( 75, + node_peer1->GetAddress(), + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( transfer1.has_value() ) << "Transfer 1 failed on node_client"; + fmt::println( "Transfer 1 succeeded" ); + assert_registry_updated( epoch_before, cid_before ); + wait_client_registry_caught_up(); + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + + auto transfer2 = node_client->TransferFunds( 40, + node_peer2->GetAddress(), + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( transfer2.has_value() ) << "Transfer 2 failed on node_client"; + fmt::println( "Transfer 2 succeeded" ); + assert_registry_updated( epoch_before, cid_before ); + wait_client_registry_caught_up(); + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + + auto transfer3 = node_client->TransferFunds( 10, + node_peer3->GetAddress(), + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( transfer3.has_value() ) << "Transfer 3 failed on node_client"; + + fmt::println( "Transfer 3 succeeded" ); + assert_registry_updated( epoch_before, cid_before ); +} + +TEST_F( MultiAccountTest, NodeConsensusBatch5Test ) +{ + constexpr size_t kCertificatesPerBatch = 5; + const auto kCertificateDelay = std::chrono::seconds( 7 ); + + auto node_full = CreateNode( "node_consensus_batch5_full", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + true, // is full node + true, // is processor + true ); // is genesis authorized + + test::assertWaitForCondition( + [&]() { return node_full->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_full not synced" ); + + auto node_client = CreateNode( "node_consensus_batch5_client", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, // not full node + false // not processor + ); + + auto node_peer1 = CreateNode( "node_consensus_batch5_peer1", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + auto node_peer2 = CreateNode( "node_consensus_batch5_peer2", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + auto node_peer3 = CreateNode( "node_consensus_batch5_peer3", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + + auto configure_consensus_batch_and_delay = [&]( const std::shared_ptr &node ) + { + test::assertWaitForCondition( + [&]() + { + return node && node->blockchain_ && node->blockchain_->consensus_manager_ && + node->GetTransactionManagerState() == TransactionManager::State::READY; + }, + std::chrono::milliseconds( 30000 ), + "node blockchain not ready for consensus configuration" ); + + auto node_registry = node->blockchain_->GetValidatorRegistry(); + ASSERT_TRUE( node_registry ); + + node_registry->SetCertificatesPerBatch( kCertificatesPerBatch ); + node->blockchain_->consensus_manager_->ConfigureCertificateDelay( kCertificateDelay ); + }; + + node_client->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer1->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer2->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer3->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + + test::assertWaitForCondition( + [&]() { return node_client->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_client not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer1->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer1 not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer2->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer2 not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer3->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer3 not synced" ); + + configure_consensus_batch_and_delay( node_full ); + configure_consensus_batch_and_delay( node_client ); + configure_consensus_batch_and_delay( node_peer1 ); + configure_consensus_batch_and_delay( node_peer2 ); + configure_consensus_batch_and_delay( node_peer3 ); + + ASSERT_TRUE( node_full->blockchain_ ); + auto registry = node_full->blockchain_->GetValidatorRegistry(); + ASSERT_TRUE( registry ); + + test::assertWaitForCondition( + [&]() + { + auto load = registry->LoadRegistry(); + return load.has_value() && !registry->GetRegistryCid().empty(); + }, + std::chrono::milliseconds( 30000 ), + "validator registry not initialized" ); + + auto registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + const auto initial_epoch = registry_state.value().epoch(); + const auto initial_cid = registry->GetRegistryCid(); + + auto assert_registry_immutable = [&]( const char *step ) + { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 10 ); + while ( std::chrono::steady_clock::now() < deadline ) + { + auto load = registry->LoadRegistry(); + ASSERT_TRUE( load.has_value() ) << "registry load failed during " << step; + EXPECT_EQ( load.value().epoch(), initial_epoch ) << "registry epoch changed unexpectedly at " << step; + EXPECT_EQ( registry->GetRegistryCid(), initial_cid ) << "registry CID changed unexpectedly at " << step; + std::this_thread::sleep_for( std::chrono::milliseconds( 250 ) ); + } + }; + + auto mint1 = node_client->MintTokens( 100, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; + assert_registry_immutable( "tx1" ); + + auto mint2 = node_client->MintTokens( 250, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + ASSERT_TRUE( mint2.has_value() ) << "Mint 2 failed on node_client"; + assert_registry_immutable( "tx2" ); + + auto transfer1 = node_client->TransferFunds( 75, + node_peer1->GetAddress(), + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( transfer1.has_value() ) << "Transfer 1 failed on node_client"; + assert_registry_immutable( "tx3" ); + + auto transfer2 = node_client->TransferFunds( 40, + node_peer2->GetAddress(), + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( transfer2.has_value() ) << "Transfer 2 failed on node_client"; + assert_registry_immutable( "tx4" ); + + auto transfer3 = node_client->TransferFunds( 10, + node_peer3->GetAddress(), + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( transfer3.has_value() ) << "Transfer 3 failed on node_client"; + + test::assertWaitForCondition( + [&]() + { + auto load = registry->LoadRegistry(); + return load.has_value() && + ( load.value().epoch() > initial_epoch || registry->GetRegistryCid() != initial_cid ); + }, + std::chrono::milliseconds( 60000 ), + "validator registry did not update after 5th certificate" ); + + auto registry_after = registry->LoadRegistry(); + ASSERT_TRUE( registry_after.has_value() ); + EXPECT_GT( registry_after.value().epoch(), initial_epoch ); + EXPECT_NE( registry->GetRegistryCid(), initial_cid ); + + const std::vector expected_validators = { node_full->GetAddress(), + node_client->GetAddress(), + node_peer1->GetAddress(), + node_peer2->GetAddress(), + node_peer3->GetAddress() }; + for ( const auto &validator_id : expected_validators ) + { + auto *validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), validator_id ); + ASSERT_TRUE( validator ) << "missing validator in registry: " << validator_id; + EXPECT_GT( validator->weight(), 0 ) << "validator has non-positive weight: " << validator_id; + } +} diff --git a/test/src/processing_multi/processing_multi_test.cpp b/test/src/processing_multi/processing_multi_test.cpp index 9eaf694de..0261c117d 100644 --- a/test/src/processing_multi/processing_multi_test.cpp +++ b/test/src/processing_multi/processing_multi_test.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef _WIN32 //#include @@ -24,6 +25,7 @@ #include "FileManager.hpp" #include #include +#include "testutil/mint_source_hash.hpp" class ProcessingMultiTest : public ::testing::Test { @@ -136,13 +138,13 @@ std::string ProcessingMultiTest::binary_path = ""; TEST_F( ProcessingMultiTest, MintTokens ) { node_main->MintTokens( 50000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); node_main->MintTokens( 50000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/processing_nodes/child_tokens_test.cpp b/test/src/processing_nodes/child_tokens_test.cpp index 50e3dec88..99c40a3a0 100644 --- a/test/src/processing_nodes/child_tokens_test.cpp +++ b/test/src/processing_nodes/child_tokens_test.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include "account/GeniusNode.hpp" #include "account/GeniusAccount.hpp" #include "account/TokenID.hpp" +#include "testutil/mint_source_hash.hpp" #include "blockchain/Blockchain.hpp" #include "testutil/wait_condition.hpp" #include @@ -174,8 +176,9 @@ TEST( TransferTokenValue, ThreeNodeTransferTest ) } // Ensure enough balance with +1 change - auto mintRes51 = node51->MintTokens( totalMint51 + 1, - "", + auto mintRes51 = + node51->MintTokens( totalMint51 + 1, + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x51 } ), "", @@ -183,8 +186,9 @@ TEST( TransferTokenValue, ThreeNodeTransferTest ) ASSERT_TRUE( mintRes51.has_value() ) << "Grouped mint failed on token51"; std::cout << "Minted total " << ( totalMint51 + 1 ) << " of token51 on node51\n"; - auto mintRes52 = node52->MintTokens( totalMint52 + 1, - "", + auto mintRes52 = + node52->MintTokens( totalMint52 + 1, + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x52 } ), "", @@ -277,7 +281,7 @@ TEST_P( GeniusNodeMintMainTest, MintMainBalance ) ASSERT_TRUE( parsedInitialChild.has_value() ); auto res = node->MintTokens( p.mintMain, - "", + sgns::test::NextMintSourceHash(), "", p.TokenID, "", @@ -354,7 +358,7 @@ TEST_P( GeniusNodeMintChildTest, MintChildBalance ) ASSERT_TRUE( parsedMint.has_value() ); auto res = node->MintTokens( parsedMint.value(), - "", + sgns::test::NextMintSourceHash(), "", p.TokenID, "", @@ -433,10 +437,7 @@ TEST( GeniusNodeMultiTokenMintTest, MintMultipleTokenIds ) for ( const auto &tm : mints ) { - auto res = node->MintTokens( tm.amount, - "", - "", - tm.tokenId, + auto res = node->MintTokens( tm.amount, sgns::test::NextMintSourceHash(), "", tm.tokenId, "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( res.has_value() ); // << "MintTokens failed for token=" << tm.tokenId << " amount=" << tm.amount; @@ -490,8 +491,9 @@ TEST_F( ProcessingNodesModuleTest, SinglePostProcessing ) std::chrono::milliseconds( 30000 ), "node_proc2 not synced" ); - auto mintResMain = node_main->MintTokens( 1000, - "", + auto mintResMain = + node_main->MintTokens( 1000, + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/processing_nodes/full_node_test.cpp b/test/src/processing_nodes/full_node_test.cpp index adf5d73a2..f07ddf4e0 100644 --- a/test/src/processing_nodes/full_node_test.cpp +++ b/test/src/processing_nodes/full_node_test.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include #include "account/GeniusNode.hpp" #include "account/TokenID.hpp" +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" #include "local_secure_storage/impl/json/JSONSecureStorage.hpp" @@ -96,7 +98,7 @@ TEST( NodeBalancePersistenceTest, BalancePersistsAfterRecreation ) for ( size_t i = 0; i < mintAmount; ++i ) { auto mintRes = originalNode->MintTokens( 500000, - "", + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/processing_nodes/processing_nodes_test.cpp b/test/src/processing_nodes/processing_nodes_test.cpp index 2553c36b1..d99aa7e54 100644 --- a/test/src/processing_nodes/processing_nodes_test.cpp +++ b/test/src/processing_nodes/processing_nodes_test.cpp @@ -3,12 +3,14 @@ #include #include #include +#include #include #include #include "account/GeniusNode.hpp" #include #include +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" using namespace sgns::test; @@ -148,21 +150,21 @@ TEST_F( ProcessingNodesTest, DISABLED_ProcessNodesTransactionsCount ) std::chrono::milliseconds( 20000 ), "Node proc 2 not synced" ); node_main->MintTokens( 50000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); node_main->MintTokens( 50000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); std::this_thread::sleep_for( std::chrono::milliseconds( 10000 ) ); - int transcount_main = node_main->GetOutTransactions().size(); - int transcount_node1 = node_proc1->GetOutTransactions().size(); - int transcount_node2 = node_proc2->GetOutTransactions().size(); + int transcount_main = node_main->GetTransactions(TransactionManager::TransactionStatus::CONFIRMED).size(); + int transcount_node1 = node_proc1->GetTransactions(TransactionManager::TransactionStatus::CONFIRMED).size(); + int transcount_node2 = node_proc2->GetTransactions(TransactionManager::TransactionStatus::CONFIRMED).size(); std::cout << "Count 1" << transcount_main << std::endl; //std::cout << "Count 2" << transcount_node1 << std::endl; std::cout << "Count 3" << transcount_node2 << std::endl; @@ -463,8 +465,9 @@ TEST_F( ProcessingNodesTest, PostProcessing ) auto procmgr = sgns::sgprocessing::ProcessingManager::Create( json_data ); auto cost = node_main->GetProcessCost( procmgr.value() ); - auto mint_result = node_main->MintTokens( 50000000000, - "", + auto mint_result = + node_main->MintTokens( 50000000000, + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -504,7 +507,7 @@ TEST_F( ProcessingNodesTest, PostProcessing ) { auto result = node_proc1->GetBalance() + node_proc2->GetBalance(); auto expected_peer_gain = ( ( cost * 65 ) / 100 ) / 2; - return balance_node1 + balance_node2 + 2 * expected_peer_gain; + return result == balance_node1 + balance_node2 + 2 * expected_peer_gain; }, std::chrono::milliseconds( 40000 ), "Balances not updated in time" ); diff --git a/test/src/transaction_sync/migration_sync_test.cpp b/test/src/transaction_sync/migration_sync_test.cpp index 2402e478d..ff9d91178 100644 --- a/test/src/transaction_sync/migration_sync_test.cpp +++ b/test/src/transaction_sync/migration_sync_test.cpp @@ -9,8 +9,10 @@ #include #include +#include "account/MigrationAllowList.hpp" #include "account/GeniusNode.hpp" #include "account/TokenID.hpp" +#include "storage/rocksdb/rocksdb.hpp" #include "testutil/wait_condition.hpp" namespace fs = std::filesystem; @@ -45,6 +47,21 @@ class MigrationParamTest : public ::testing::TestWithParam "16fc3a9c86b42bd7e02b4c3276704948211a034b6cddfe024bfaf39dfb51d95a9649c5b149d18956991cc116f148f6441fc8fc60205d499dad35421c1279dd93"; static constexpr uint16_t FULL_NODE_BASEPORT = 43001; + void SetEligibilityCheckEnabled( bool enabled ) + { + sgns::MigrationAllowList::SetEligibilityCheckEnabledForTests( enabled ); + } + + void SetUp() override + { + SetEligibilityCheckEnabled( true ); + } + + void TearDown() override + { + SetEligibilityCheckEnabled( true ); + } + static void RemovePrefixedSubdirs( const fs::path &baseDir ) { if ( !fs::exists( baseDir ) || !fs::is_directory( baseDir ) ) @@ -123,6 +140,8 @@ class MigrationParamTest : public ::testing::TestWithParam TEST_P( MigrationParamTest, BalanceAfterMigration ) { + SetEligibilityCheckEnabled( false ); + std::string full_node_pub_address{ FULL_NODE_PUB_ADDRESS }; Blockchain::SetAuthorizedFullNodeAddress( full_node_pub_address ); auto params = GetParam(); @@ -131,7 +150,7 @@ TEST_P( MigrationParamTest, BalanceAfterMigration ) test::assertWaitForCondition( [full_node] { return full_node && full_node->GetState() == GeniusNode::NodeState::READY; }, - std::chrono::milliseconds( 30000 ), + std::chrono::milliseconds( 80000 ), "Full node not synced" ); auto binaryParent = boost::dll::program_location().parent_path().string(); auto node = CreateNodeInstance( binaryParent, params.subdir, params.key_hex ); @@ -147,6 +166,36 @@ TEST_P( MigrationParamTest, BalanceAfterMigration ) EXPECT_EQ( node->GetBalance(), params.expected_balance ); } +TEST_F( MigrationParamTest, RejectsOverclaimWhenAllowListEnabled ) +{ + namespace fs = std::filesystem; + using sgns::MigrationAllowList; + using sgns::storage::rocksdb; + + const auto unique_suffix = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count() ); + const fs::path db_path = fs::temp_directory_path() / ( "migration_allowlist_rejects_test_" + unique_suffix ); + std::error_code ec; + fs::remove_all( db_path, ec ); + fs::create_directories( db_path, ec ); + ASSERT_FALSE( ec ) << "Failed to create temp DB directory: " << ec.message(); + + rocksdb::Options options; + options.create_if_missing = true; + + auto db_result = rocksdb::create( db_path.string(), options ); + ASSERT_TRUE( db_result.has_value() ) << db_result.error().message(); + + MigrationAllowList allow_list( db_result.value(), "3.6.0" ); + ASSERT_TRUE( allow_list.StoreObservedBalance( "eligible-address", 100 ).has_value() ); + + auto eligible = allow_list.IsEligible( "eligible-address", 201 ); + ASSERT_TRUE( eligible.has_value() ) << eligible.error().message(); + EXPECT_FALSE( eligible.value() ); + + fs::remove_all( db_path, ec ); +} + INSTANTIATE_TEST_SUITE_P( Nodes, MigrationParamTest, diff --git a/test/src/transaction_sync/transaction_crash_test.cpp b/test/src/transaction_sync/transaction_crash_test.cpp index c8fad486f..79d2cc0bd 100644 --- a/test/src/transaction_sync/transaction_crash_test.cpp +++ b/test/src/transaction_sync/transaction_crash_test.cpp @@ -7,12 +7,13 @@ #endif #include #include +#include #include #include "account/GeniusNode.hpp" +#include "testutil/mint_source_hash.hpp" namespace sgns { - /** * @file transaction_crash_sync_test_updated.cpp * @brief Verifies transaction synchronization after a node crash and recovery, @@ -111,7 +112,7 @@ namespace sgns std::cout << "Minting the required tokens" << std::endl; auto mint_result = node1->MintTokens( total_amount, - "", + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/transaction_sync/transaction_sync_test.cpp b/test/src/transaction_sync/transaction_sync_test.cpp index c822ea8fb..b4c6847d0 100644 --- a/test/src/transaction_sync/transaction_sync_test.cpp +++ b/test/src/transaction_sync/transaction_sync_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #ifdef _WIN32 //#include @@ -20,6 +21,7 @@ #include #include "account/TransferTransaction.hpp" #include "proof/TransferProof.hpp" +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" namespace sgns @@ -111,7 +113,8 @@ namespace sgns outcome::result CreateTransfer( sgns::GeniusAccount &account, uint64_t amount, - const std::string &destination ) + const std::string &destination, + const std::string &previous_hash = "" ) { BOOST_OUTCOME_TRY( auto params, account.GetUTXOManager().CreateTxParameter( amount, @@ -121,15 +124,17 @@ namespace sgns auto timestamp = std::chrono::system_clock::now(); SGTransaction::DAGStruct dag; - dag.set_previous_hash( "" ); + dag.set_previous_hash( previous_hash ); dag.set_nonce( account.ReserveNextNonce() ); dag.set_source_addr( account.GetAddress() ); - dag.set_timestamp( timestamp.time_since_epoch().count() ); + dag.set_timestamp( + std::chrono::duration_cast( timestamp.time_since_epoch() ).count() ); dag.set_uncle_hash( "" ); dag.set_data_hash( "" ); //filled by transaction class auto transfer_transaction = std::make_shared( sgns::TransferTransaction::New( params.first, params.second, dag ) ); + transfer_transaction->MakeSignature( account ); std::optional> maybe_proof; TransferProof prover( account.GetUTXOManager().GetBalance(), amount ); @@ -137,7 +142,7 @@ namespace sgns maybe_proof = std::move( proof_result ); - account.GetUTXOManager().ReserveUTXOs( params.first ); + account.GetUTXOManager().ReserveUTXOs( params.first, transfer_transaction->GetHash() ); return std::make_pair( transfer_transaction, maybe_proof ); } @@ -163,7 +168,7 @@ TEST_F( TransactionSyncTest, TransactionSimpleTransfer ) auto balance_1_before = node_proc1->GetBalance(); auto balance_2_before = node_proc2->GetBalance(); auto mint_result = node_proc1->MintTokens( 10000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -238,7 +243,7 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) for ( auto amount : mint_amounts ) { auto mint_result = node_proc1->MintTokens( amount, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -251,7 +256,7 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) // Mint tokens on node_proc2 auto mint_result1 = node_proc2->MintTokens( 10000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -259,7 +264,7 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) ASSERT_TRUE( mint_result1.has_value() ) << "Mint transaction failed or timed out"; auto mint_result2 = node_proc2->MintTokens( 20000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -354,9 +359,9 @@ TEST_F( TransactionSyncTest, TransactionTransferSync ) for ( size_t index = 0; index < xfer_amounts[0].size(); index++ ) { - auto xfer_amount = xfer_amounts[0][index]; - xfer_amount_1 += xfer_amount; - auto transfer_result1 = node_proc1->TransferFunds( xfer_amount, + auto xfer_amount = xfer_amounts[0][index]; + xfer_amount_1 += xfer_amount; + auto transfer_result1 = node_proc1->TransferFunds( xfer_amount, node_proc2->GetAddress(), sgns::TokenID::FromBytes( { 0x00 } ), std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); @@ -366,9 +371,9 @@ TEST_F( TransactionSyncTest, TransactionTransferSync ) txIDs[0].push_back( transfer_tx_id1 ); - xfer_amount = xfer_amounts[1][index]; - xfer_amount_2 += xfer_amount; - auto transfer_result2 = node_proc2->TransferFunds( xfer_amount, + xfer_amount = xfer_amounts[1][index]; + xfer_amount_2 += xfer_amount; + auto transfer_result2 = node_proc2->TransferFunds( xfer_amount, node_proc1->GetAddress(), sgns::TokenID::FromBytes( { 0x00 } ), std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); @@ -435,14 +440,14 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) // Mint tokens with timeout auto mint_result = node_proc1->MintTokens( 10000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; mint_result = node_proc1->MintTokens( 10000000000, - "", + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -457,7 +462,8 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) // Verify balance after minting EXPECT_EQ( node_proc1->GetBalance(), balance_1_before_invalid ) << "Correct Balance of outgoing transactions"; - auto tx_pair = CreateTransfer( *GetAccountFromNode( *node_proc1 ), 10000000000, node_proc2->GetAddress() ); + auto tx_pair = CreateTransfer( *GetAccountFromNode( *node_proc1 ), 10000000000, node_proc2->GetAddress(), + mint_tx_id ); if ( !tx_pair.has_value() ) { } @@ -475,46 +481,14 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) auto invalid_tx_id = tx->dag_st.data_hash(); SendPair( *node_proc1, tx, proof_vect ); - test::assertWaitForCondition( - [&] - { - return node_proc1->GetTransactionStatus( invalid_tx_id ) == - TransactionManager::TransactionStatus::VERIFYING; - }, - std::chrono::milliseconds( 20000 ), - "Invalid transaction didn't get sent" ); - - EXPECT_EQ( node_proc1->GetBalance(), balance_1_before_invalid - 10000000000 ) - << "Correct Balance of outgoing transactions"; - - std::cout << "Invalid tx confirmed " << std::endl; - - // Transfer funds with timeout - auto transfer_result = node_proc1->TransferFunds( 10000000000, - node_proc2->GetAddress(), - sgns::TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); - ASSERT_FALSE( transfer_result.has_value() ) << "Transfer transaction succeeded when it should fail"; - - std::cout << "subsequent tx failed" << std::endl; - - test::assertWaitForCondition( - [&]() { return node_proc1->GetTransactionManagerState() == TransactionManager::State::SYNCING; }, - std::chrono::milliseconds( 20000 ), - "Node didn't went into synching" ); - - EXPECT_EQ( node_proc1->GetTransactionManagerState(), - TransactionManager::State::SYNCING ); //confirms it's invalid - auto invalid_tx_result_sent = node_proc1->WaitForTransactionOutgoing( invalid_tx_id, std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + EXPECT_EQ( invalid_tx_result_sent, TransactionManager::TransactionStatus::FAILED ); - std::cout << "waited again for the invalid tx" << std::endl; - - EXPECT_EQ( invalid_tx_result_sent, TransactionManager::TransactionStatus::FAILED ); //confirms it's invalid + EXPECT_EQ( node_proc1->GetBalance(), balance_1_before_invalid ) << "Correct Balance of outgoing transactions"; - std::cout << "now it's invalid" << std::endl; + std::cout << "Invalid tx failed" << std::endl; test::assertWaitForCondition( [&]() { return node_proc1->GetTransactionManagerState() == TransactionManager::State::READY; }, @@ -523,12 +497,10 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) std::cout << "wait until its ready" << std::endl; - EXPECT_EQ( node_proc1->GetBalance(), balance_1_before_invalid ) << "Correct Balance of outgoing transactions"; - - transfer_result = node_proc1->TransferFunds( 10000000000, - node_proc2->GetAddress(), - sgns::TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + auto transfer_result = node_proc1->TransferFunds( 10000000000, + node_proc2->GetAddress(), + sgns::TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); ASSERT_TRUE( transfer_result.has_value() ) << "Transfer transaction failed when it should succeed"; auto [transfer_tx_id, transfer_duration] = transfer_result.value(); @@ -547,3 +519,72 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) EXPECT_EQ( node_proc2->GetBalance(), balance_2_before + 10000000000 ) << "Transfer should increase node_proc2's balance"; } + +TEST_F( TransactionSyncTest, InvalidPreviousHashTest ) +{ + // Ensure nodes are connected and ready + node_proc1->GetPubSub()->AddPeers( + { node_proc2->GetPubSub()->GetInterfaceAddress(), full_node->GetPubSub()->GetInterfaceAddress() } ); + node_proc2->GetPubSub()->AddPeers( { full_node->GetPubSub()->GetInterfaceAddress() } ); + + test::assertWaitForCondition( + [&]() { return node_proc1->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 20000 ), + "node_proc1 not synched" ); + test::assertWaitForCondition( + [&]() { return node_proc2->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 20000 ), + "node_proc2 not synched" ); + + // Mint tokens to ensure sufficient balance + auto mint_result = node_proc1->MintTokens( 20000000000, + sgns::test::NextMintSourceHash(), + "", + TokenID::FromBytes( { 0x00 } ), + "", + std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); + ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; + + // Create and send a valid first transfer using the normal flow + auto transfer_result = node_proc1->TransferFunds( 10000000000, + node_proc2->GetAddress(), + sgns::TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( transfer_result.has_value() ) << "Transfer transaction failed or timed out"; + auto [tx1_id, transfer_duration] = transfer_result.value(); + std::cout << "Transfer transaction completed in " << transfer_duration << " ms" << std::endl; + + auto tx1_status = node_proc1->WaitForTransactionOutgoing( + tx1_id, + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + EXPECT_EQ( tx1_status, TransactionManager::TransactionStatus::CONFIRMED ); + + // Create a second transfer with an invalid previous hash + auto tx_pair2 = CreateTransfer( *GetAccountFromNode( *node_proc1 ), + 10000000000, + node_proc2->GetAddress(), + tx1_id ); + ASSERT_TRUE( tx_pair2.has_value() ); + + auto [tx2, proof2] = tx_pair2.value(); + std::string bad_prev = tx1_id; + if ( !bad_prev.empty() ) + { + bad_prev[0] = ( bad_prev[0] == 'a' ) ? 'b' : 'a'; + } + tx2->dag_st.set_previous_hash( bad_prev ); + tx2->FillHash(); + tx2->MakeSignature( *GetAccountFromNode( *node_proc1 ) ); + + std::vector proof_vect2; + if ( proof2.has_value() ) + { + proof_vect2 = proof2.value(); + } + SendPair( *node_proc1, tx2, proof_vect2 ); + + auto tx2_status = node_proc1->WaitForTransactionOutgoing( + tx2->GetHash(), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + EXPECT_EQ( tx2_status, TransactionManager::TransactionStatus::FAILED ); +} diff --git a/test/testutil/mint_source_hash.hpp b/test/testutil/mint_source_hash.hpp new file mode 100644 index 000000000..c5b0eb8ad --- /dev/null +++ b/test/testutil/mint_source_hash.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace sgns::test +{ + inline std::string NextMintSourceHash() + { + static std::atomic counter{ 0 }; + static std::mutex rng_mutex; + static std::mt19937_64 rng( [] + { + const auto now = static_cast( + std::chrono::high_resolution_clock::now().time_since_epoch().count() ); + std::random_device rd; + std::seed_seq seed{ static_cast( now ), + static_cast( now >> 32 ), + rd(), + rd(), + rd(), + rd() }; + return std::mt19937_64( seed ); + }() ); + + const uint64_t sequence = counter.fetch_add( 1, std::memory_order_relaxed ); + uint64_t random_hi; + uint64_t random_lo; + + { + std::lock_guard lock( rng_mutex ); + random_hi = rng(); + random_lo = rng(); + } + + char buffer[65] = {}; + std::snprintf( buffer, + sizeof( buffer ), + "%016llx%016llx%016llx%016llx", + static_cast( random_hi ), + static_cast( random_lo ), + static_cast( sequence ), + static_cast( sequence ^ random_hi ^ random_lo ) ); + return std::string( buffer ); + } +} // namespace sgns::test diff --git a/test/testutil/storage/base_crdt_test.cpp b/test/testutil/storage/base_crdt_test.cpp index da88bc758..dd3f55fe6 100644 --- a/test/testutil/storage/base_crdt_test.cpp +++ b/test/testutil/storage/base_crdt_test.cpp @@ -49,12 +49,18 @@ namespace test { const std::string CRDTFixture::basePath = "CRDT.Datastore.TEST"; std::shared_ptr CRDTFixture::logging_system_; + std::atomic CRDTFixture::fixture_counter_{ 0 }; CRDTFixture::CRDTFixture( fs::path path ) : FSFixture( std::move( path ) ) { + const auto fixture_id = fixture_counter_.fetch_add( 1, std::memory_order_relaxed ) + 1; + const auto suffix = std::to_string( fixture_id ); + keypair_path_ = basePath + "/unit_test_" + suffix; + db_path_ = basePath + ".unit_" + suffix; + io_ = std::make_shared(); - pubs_ = std::make_shared( KeyPairFileStorage( basePath + "/unit_test" ).GetKeyPair().value() ); + pubs_ = std::make_shared( KeyPairFileStorage( keypair_path_ ).GetKeyPair().value() ); BOOST_ASSERT_MSG( pubs_ != nullptr, "could not create GossibPubSub for some reason" ); auto crdtOptions = sgns::crdt::CrdtOptions::DefaultOptions(); @@ -63,8 +69,7 @@ namespace test auto graphsyncnetwork = std::make_shared( pubs_->GetHost(), scheduler ); - auto globaldb_ret = - GlobalDB::New( io_, basePath + ".unit", pubs_, crdtOptions, graphsyncnetwork, scheduler, generator ); + auto globaldb_ret = GlobalDB::New( io_, db_path_, pubs_, crdtOptions, graphsyncnetwork, scheduler, generator ); BOOST_ASSERT( globaldb_ret.has_value() ); db_ = std::move( globaldb_ret.value() ); @@ -80,10 +85,18 @@ namespace test CRDTFixture::~CRDTFixture() { + if ( pubs_ ) + { + pubs_->Stop(); + } + db_.reset(); + pubs_.reset(); + io_.reset(); + try { - fs::remove_all( basePath ); - fs::remove_all( basePath + ".unit" ); + fs::remove_all( keypair_path_ ); + fs::remove_all( db_path_ ); } catch ( const fs::filesystem_error &err ) { diff --git a/test/testutil/storage/base_crdt_test.hpp b/test/testutil/storage/base_crdt_test.hpp index 273cae818..cd37f19cc 100644 --- a/test/testutil/storage/base_crdt_test.hpp +++ b/test/testutil/storage/base_crdt_test.hpp @@ -10,6 +10,7 @@ #include "testutil/storage/base_fs_test.hpp" #include +#include #include #include #include @@ -39,7 +40,10 @@ namespace test std::shared_ptr pubs_; std::shared_ptr db_; + std::string keypair_path_; + std::string db_path_; static std::shared_ptr<::soralog::LoggingSystem> logging_system_; + static std::atomic fixture_counter_; }; }