diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index f45d19126..264c7df05 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -5,7 +5,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 fdfed98e1..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( io_, libp2p::protocol::SchedulerConfig{} ); - 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_, utxo_manager_, 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 - libp2p::protocol::kademlia::ContentId key( hash ); - pubsub_->GetDHT()->Start(); - pubsub_->GetDHT()->ProvideCID( key, true ); - - auto cidtest = libp2p::multi::ContentIdentifierCodec::decode( key.data ); - - auto cidstring = libp2p::multi::ContentIdentifierCodec::toString( cidtest.value() ); - std::cout << "CID Test::" << cidstring.value() << std::endl; - - //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 0d33eeef2..3e948e1e7 100644 --- a/src/account/CMakeLists.txt +++ b/src/account/CMakeLists.txt @@ -60,6 +60,7 @@ add_library(genius_node ProcessingTransaction.cpp EscrowTransaction.cpp EscrowReleaseTransaction.cpp + InputValidators.cpp TransactionManager.cpp TokenAmount.cpp MigrationManager.cpp diff --git a/src/account/EscrowReleaseTransaction.cpp b/src/account/EscrowReleaseTransaction.cpp index 28884feba..69b6efa7f 100644 --- a/src/account/EscrowReleaseTransaction.cpp +++ b/src/account/EscrowReleaseTransaction.cpp @@ -44,10 +44,10 @@ namespace sgns return instance; } - std::vector EscrowReleaseTransaction::SerializeByteVector() + std::vector EscrowReleaseTransaction::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const { SGTransaction::EscrowReleaseTx tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( this->dag_st ); + 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 ) { diff --git a/src/account/EscrowReleaseTransaction.hpp b/src/account/EscrowReleaseTransaction.hpp index a8b9fe28a..6c5a61684 100644 --- a/src/account/EscrowReleaseTransaction.hpp +++ b/src/account/EscrowReleaseTransaction.hpp @@ -56,7 +56,8 @@ namespace sgns * * @return A vector of bytes representing the serialized transaction. */ - std::vector SerializeByteVector() override; + using IGeniusTransactions::SerializeByteVector; + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; /** * @brief Gets the UTXO parameters. 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..4db5f92e7 100644 --- a/src/account/EscrowTransaction.hpp +++ b/src/account/EscrowTransaction.hpp @@ -26,7 +26,8 @@ namespace sgns ~EscrowTransaction() override = default; - std::vector SerializeByteVector() override; + using IGeniusTransactions::SerializeByteVector; + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; uint64_t GetNumChunks() const; std::string GetTransactionSpecificPath() const override diff --git a/src/account/GeniusAccount.cpp b/src/account/GeniusAccount.cpp index 5a9f46715..a7cc0942e 100644 --- a/src/account/GeniusAccount.cpp +++ b/src/account/GeniusAccount.cpp @@ -598,11 +598,21 @@ 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 ) diff --git a/src/account/GeniusAccount.hpp b/src/account/GeniusAccount.hpp index b0ba51baf..0febb73d0 100644 --- a/src/account/GeniusAccount.hpp +++ b/src/account/GeniusAccount.hpp @@ -28,6 +28,8 @@ #include #include "account/TokenID.hpp" +#include "account/GeniusUTXO.hpp" +#include "account/UTXOStructs.hpp" #include "local_secure_storage/ISecureStorage.hpp" #include "outcome/outcome.hpp" @@ -154,10 +156,11 @@ 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 diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index 1590347a9..f042439fd 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -369,6 +369,7 @@ namespace sgns blockchain_ = Blockchain::New( tx_globaldb_, account_, + pubsub_, [weak_self]( outcome::result result ) { if ( auto strong = weak_self.lock() ) @@ -379,7 +380,7 @@ namespace sgns result.error().message() ); strong->node_logger_->info( "Scheduling blockchain retry after failure" ); strong->account_->RequestHeads( - { std::string( blockchain::ValidatorRegistry::ValidatorTopic() ) } ); + { std::string( ValidatorRegistry::ValidatorTopic() ) } ); strong->ScheduleBlockchainRetry(); return; } @@ -433,6 +434,7 @@ namespace sgns utxo_manager_, account_, std::make_shared(), + blockchain_, is_full_node_ ); transaction_manager_->RegisterStateChangeCallback( @@ -492,15 +494,15 @@ namespace sgns // Debug mode node_logger_ = ConfigureLogger( "SuperGeniusNode", logdir, spdlog::level::debug ); auto loggerGeniusNode = ConfigureLogger( "GeniusNode", logdir, spdlog::level::debug ); - auto loggerGlobalDB = ConfigureLogger( "GlobalDB", logdir, spdlog::level::debug ); + auto loggerGlobalDB = ConfigureLogger( "GlobalDB", logdir, spdlog::level::err ); 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 loggerCRDTHeads = ConfigureLogger( "CrdtHeads", logdir, spdlog::level::trace ); + 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::err ); + auto loggerMigrationStep = ConfigureLogger( "MigrationStep", logdir, spdlog::level::err ); auto loggerQueue = ConfigureLogger( "ProcessingTaskQueueImpl", logdir, spdlog::level::err ); auto loggerRocksDB = ConfigureLogger( "rocksdb", logdir, spdlog::level::err ); auto logkad = ConfigureLogger( "Kademlia", logdir, spdlog::level::err ); @@ -512,16 +514,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 ); @@ -546,7 +549,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 ); @@ -568,16 +571,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 ); diff --git a/src/account/GeniusNode.hpp b/src/account/GeniusNode.hpp index 93190db7f..be452cf77 100644 --- a/src/account/GeniusNode.hpp +++ b/src/account/GeniusNode.hpp @@ -176,6 +176,17 @@ namespace sgns return manager_result.value()->GetOutTransactions(); } + [[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 ); + } + std::string GetAddress() const { return account_->GetAddress(); diff --git a/src/account/GeniusUTXO.hpp b/src/account/GeniusUTXO.hpp index acb8c9794..8f508eb7d 100644 --- a/src/account/GeniusUTXO.hpp +++ b/src/account/GeniusUTXO.hpp @@ -10,33 +10,71 @@ #include "base/blob.hpp" #include "account/TokenID.hpp" +#include +#include + namespace sgns { + struct OutPoint + { + base::Hash256 txid_hash_; + uint32_t output_idx_{ 0 }; + + bool operator==( const OutPoint &other ) const + { + return txid_hash_ == other.txid_hash_ && output_idx_ == other.output_idx_; + } + }; + class GeniusUTXO { public: + GeniusUTXO() : outpoint_{}, amount_( 0 ), token_id_(), owner_address_() + { + } + 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 ) // + { + } + + 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 ) ) + { + } + + void SetOwnerAddress( std::string owner_address ) + { + owner_address_ = std::move( owner_address ); + } + + const std::string &GetOwnerAddress() const { + return owner_address_; } - void SetLocked( const bool locked ) + OutPoint GetOutPoint() const { - locked_ = locked; + return outpoint_; } base::Hash256 GetTxID() const { - return txid_hash_; + return outpoint_.txid_hash_; } uint32_t GetOutputIdx() const { - return output_idx_; + return outpoint_.output_idx_; } uint64_t GetAmount() const @@ -44,22 +82,16 @@ namespace sgns return amount_; } - bool GetLock() const - { - return locked_; - } - TokenID GetTokenID() const { return token_id_; } private: - base::Hash256 txid_hash_; - uint32_t output_idx_; + OutPoint outpoint_; uint64_t amount_; - bool locked_; TokenID token_id_; + std::string owner_address_; }; } 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..1fbbf150c --- /dev/null +++ b/src/account/InputValidators.cpp @@ -0,0 +1,437 @@ +/** + * @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 ); + if ( payload_owner != tx->GetSrcAddress() ) + { + 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..45059dfa0 --- /dev/null +++ b/src/account/InputValidators.hpp @@ -0,0 +1,75 @@ +/** + * @file InputValidators.hpp + * @brief Input validation strategies for different source chains + * @date 2026-03-23 + */ +#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 +{ + class IInputValidator + { + public: + virtual ~IInputValidator() = default; + + virtual bool ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const = 0; + + virtual bool ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const = 0; + + virtual bool RequiresConsensusUTXOData() const = 0; + }; + + class GeniusInputValidator final : public IInputValidator + { + public: + bool ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const override; + + bool ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const override; + + bool RequiresConsensusUTXOData() const override + { + return true; + } + }; + + class PublicChainInputValidator final : public IInputValidator + { + public: + bool ValidateUTXOParameters( const UTXOTxParameters ¶ms, + const std::string &address, + const UTXOManager &utxo_manager ) const override; + + bool ValidateWitness( const ConsensusSubject &subject, + const std::shared_ptr &tx, + const UTXOTxParameters ¶ms, + const std::shared_ptr &blockchain ) const override; + + bool RequiresConsensusUTXOData() const override + { + return false; + } + + private: + bool VerifyPublicChainSmartContract( const std::shared_ptr &tx, + const std::string &source_reference ) const; + }; +} // namespace sgns diff --git a/src/account/Migration3_4_0To3_5_0.cpp b/src/account/Migration3_4_0To3_5_0.cpp index 8f3835766..d72dfe8e1 100644 --- a/src/account/Migration3_4_0To3_5_0.cpp +++ b/src/account/Migration3_4_0To3_5_0.cpp @@ -130,6 +130,7 @@ namespace sgns blockchain_ = Blockchain::New( db_3_5_0_, account_, + pubSub_, [wptr( weak_from_this() )]( outcome::result result ) { if ( auto strong = wptr.lock() ) @@ -138,7 +139,7 @@ namespace sgns { strong->logger_->error( "Error starting blockchain: {}", result.error().message() ); strong->account_->RequestHeads( - { std::string( sgns::blockchain::ValidatorRegistry::ValidatorTopic() ) } ); + { std::string( sgns::ValidatorRegistry::ValidatorTopic() ) } ); strong->blockchain_status_.store( Status::ST_ERROR ); return; } diff --git a/src/account/Migration3_5_0To3_6_0.cpp b/src/account/Migration3_5_0To3_6_0.cpp index 3e818d5f0..8b3d7016b 100644 --- a/src/account/Migration3_5_0To3_6_0.cpp +++ b/src/account/Migration3_5_0To3_6_0.cpp @@ -95,7 +95,7 @@ namespace sgns logger_->info( "Starting migration from {} to {}", FromVersion(), ToVersion() ); - OUTCOME_TRY( blockchain::ValidatorRegistry::MigrateCids( db_3_5_1_, db_3_6_0_ ) ); + OUTCOME_TRY( ValidatorRegistry::MigrateCids( db_3_5_1_, db_3_6_0_ ) ); OUTCOME_TRY( Blockchain::MigrateCids( db_3_5_1_, db_3_6_0_ ) ); auto crdt_transaction_ = db_3_6_0_->BeginTransaction(); 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..8192b9813 100644 --- a/src/account/MintTransaction.hpp +++ b/src/account/MintTransaction.hpp @@ -27,12 +27,15 @@ namespace sgns TokenID token_id, SGTransaction::DAGStruct dag ); - std::vector SerializeByteVector() override; + using IGeniusTransactions::SerializeByteVector; + std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; uint64_t GetAmount() const; TokenID GetTokenID() const; + std::string GetChainId() const override; + std::string GetTransactionSpecificPath() const override { return GetType(); 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/TransactionManager.cpp b/src/account/TransactionManager.cpp index 93b845fd8..8133af8ea 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -9,8 +9,7 @@ #include #include #include -#include -#include +#include #include #include @@ -21,6 +20,7 @@ #include "MintTransactionV2.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,11 +32,66 @@ 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, UTXOManager &utxo_manager, 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 ) @@ -46,10 +101,34 @@ namespace sgns utxo_manager, 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 ) + { + (void)certificate; + if ( auto strong = weak_ptr.lock() ) + { + strong->OnConsensusCertificate( subject_hash ); + } + } ); + 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 ) { @@ -137,6 +216,7 @@ namespace sgns UTXOManager &utxo_manager, 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 ) : @@ -145,6 +225,7 @@ namespace sgns account_m( std::move( account ) ), utxo_manager_( utxo_manager ), 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() ), @@ -157,9 +238,24 @@ 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 ); + } + } Stop(); } @@ -180,23 +276,23 @@ namespace sgns return; } - m_logger->info( "[{} - full: {}] Starting Transaction Manager", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->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->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 ); } @@ -247,25 +343,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() ) { @@ -273,9 +369,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; @@ -283,7 +380,7 @@ namespace sgns break; case State::SYNCING: - this->SyncNonce(); + SyncNonce(); break; case State::READY: @@ -300,17 +397,18 @@ namespace sgns // Immediately switch to SYNCING so no new transactions are created while we roll back. 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, + send_result.error().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; } @@ -318,9 +416,10 @@ namespace sgns // when full node becomes available 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 ); + TransactionManagerLogger()->info( + "[{} - full: {}] Network timeout - keeping transaction in queue for retry", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); // Don't pop - transaction stays in queue for retry when we return to READY } else @@ -330,29 +429,11 @@ namespace sgns } 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 ); - } - // Periodic sync - request heads every 10 minutes to stay synchronized across devices/instances // Use 30 second interval until we get first response, then switch to 10 minutes bool should_sync = false; @@ -372,33 +453,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 ); } } @@ -468,7 +549,7 @@ namespace sgns transfer_transaction->MakeSignature( *account_m ); - utxo_manager_.ReserveUTXOs( inputs ); + utxo_manager_.ReserveUTXOs( inputs, transfer_transaction->GetHash() ); EnqueueTransaction( std::make_pair( transfer_transaction, std::nullopt ) ); @@ -489,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 ), std::move( tokenid ), FillDAGStruct( std::move( transaction_hash ) ), + std::move( mint_inputs ), destination ) ); mint_transaction->MakeSignature( *account_m ); @@ -521,13 +629,12 @@ namespace sgns utxo_manager_.CreateTxParameter( amount, "0x" + hash_data.toReadableString(), TokenID::FromBytes( { 0x00 } ) ) ); - auto [inputs, outputs] = params; - utxo_manager_.ReserveUTXOs( inputs ); - + auto [inputs, outputs] = params; auto escrow_transaction = std::make_shared( EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct() ) ); escrow_transaction->MakeSignature( *account_m ); + utxo_manager_.ReserveUTXOs( inputs, escrow_transaction->GetHash() ); // Get the transaction ID for tracking auto txId = escrow_transaction->GetHash(); @@ -549,21 +656,23 @@ 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 outcome::failure( boost::system::error_code{} ); } 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 outcome::failure( boost::system::error_code{} ); } - 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 ); OUTCOME_TRY( ( auto &&, transaction ), FetchTransaction( globaldb_m, escrow_path ) ); std::shared_ptr escrow_tx = std::dynamic_pointer_cast( transaction ); @@ -584,11 +693,11 @@ namespace sgns for ( auto &subtask : task_result.subtask_results() ) { std::cout << "Subtask Result " << subtask.subtaskid() << "from " << subtask.node_address() << std::endl; - 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(), @@ -596,10 +705,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; @@ -610,19 +719,22 @@ namespace sgns auto transfer_transaction = std::make_shared( TransferTransaction::New( std::vector{ escrow_utxo_input }, payout_peers, FillDAGStruct() ) ); + transfer_transaction->MakeSignature( *account_m ); + auto escrow_release_dag = FillDAGStruct(); + escrow_release_dag.set_previous_hash( transfer_transaction->GetHash() ); + 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() ) ); - - TransactionBatch tx_batch; + escrow_release_dag ) ); - transfer_transaction->MakeSignature( *account_m ); escrow_release_tx->MakeSignature( *account_m ); + TransactionBatch tx_batch; + tx_batch.push_back( std::make_pair( transfer_transaction, std::nullopt ) ); tx_batch.push_back( std::make_pair( escrow_release_tx, std::nullopt ) ); @@ -632,19 +744,20 @@ namespace sgns 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 ); @@ -657,29 +770,94 @@ 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 + { + if ( nonce == 0 ) + { + return ""; + } + + 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(); + } + return ""; + } + + 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 ) { - std::unordered_set nonces_set; auto [transaction_batch, maybe_crdt_transaction] = item; std::shared_ptr crdt_transaction = nullptr; - m_logger->trace( "{} called", __func__ ); + TransactionManagerLogger()->trace( "{} called", __func__ ); if ( maybe_crdt_transaction.has_value() && maybe_crdt_transaction.value() ) { @@ -689,54 +867,48 @@ namespace sgns { crdt_transaction = globaldb_m->BeginTransaction(); } - auto nonce_result = account_m->GetConfirmedNonce( NONCE_REQUEST_TIMEOUT_MS ); - uint64_t expected_next_nonce = 0; - int64_t confirmed_nonce = -1; - - if ( nonce_result.has_value() ) + std::optional expected_next_nonce; + if ( auto local_confirmed = account_m->GetLocalConfirmedNonce(); local_confirmed.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 ); - expected_next_nonce = static_cast( confirmed_nonce ) + 1; + 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 ( nonce_result.has_error() && nonce_result.error() == AccountMessenger::Error::NO_RESPONSE_RECEIVED ) + else if ( !transaction_batch.empty() ) { - if ( !full_node_m ) - { - 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 ) ); - } - - 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() ) - { - confirmed_nonce = static_cast( local_confirmed.value() ); - - 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; - } + // 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() ); + 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 ) ); @@ -746,10 +918,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_TRYV2( auto &&, crdt_transaction->Put( std::move( tx_key ), std::move( data_transaction ) ) ); @@ -760,138 +932,88 @@ 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_TRYV2( auto &&, 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 ); - std::unordered_set topicSet; - if ( !transaction_batch.empty() ) - { - topicSet.emplace( full_node_topic_m ); - topicSet.emplace( account_m->GetAddress() ); + topicSet.merge( transaction->GetTopics() ); + transactions_sent.insert( transaction ); + + expected_next_nonce = expected_next_nonce.value() + 1; } - for ( auto &[tx, _] : transaction_batch ) + + OUTCOME_TRY( crdt_transaction->Commit( topicSet ) ); + + for ( auto &transaction : transactions_sent ) { - 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_TRYV2( auto &&, crdt_transaction->Commit( topicSet ) ); + OUTCOME_TRY( auto &&proposal, + blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), + transaction->GetNonce(), + transaction->GetHash(), + utxo_commitment, + utxo_witness ) ); + OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); + 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() ); + OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::FAILED ) ); } return outcome::success(); } @@ -1006,13 +1128,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 ); + 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 ) @@ -1020,96 +1147,216 @@ 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 ); - } - - outcome::result> TransactionManager::FetchTransaction( - const std::shared_ptr &db, - std::string_view transaction_key ) - { - OUTCOME_TRY( auto transaction_data, db->Get( { std::string( transaction_key ) } ) ); - - return DeSerializeTransaction( transaction_data ); - } - - outcome::result> TransactionManager::DeSerializeTransaction( - const base::Buffer &tx_data ) - { - const auto &transaction_data_vector = tx_data.toVector(); - - OUTCOME_TRY( ( auto &&, dag ), IGeniusTransactions::DeSerializeDAGStruct( transaction_data_vector ) ); - - auto it = IGeniusTransactions::GetDeSerializers().find( dag.type() ); - if ( it == IGeniusTransactions::GetDeSerializers().end() ) + utxo_state_tracking_suppression_.fetch_add( 1 ); + auto revert_result = ( this->*( it->second.second ) )( tx ); + utxo_state_tracking_suppression_.fetch_sub( 1 ); + OUTCOME_TRY( revert_result ); + if ( DoesTransactionMutateUTXOState( tx ) && utxo_state_tracking_suppression_.load() == 0 ) { - return std::errc::invalid_argument; + UpdateAccountUTXOState( CollectTouchedAccounts( tx ), false ); } - return it->second( transaction_data_vector ); + return outcome::success(); } - outcome::result TransactionManager::CheckProof( const std::shared_ptr &tx ) + bool TransactionManager::DoesTransactionMutateUTXOState( const std::shared_ptr &tx ) const { - auto proof_path = GetTransactionProofPath( *tx ); - m_logger->debug( "[{} - full: {}] Checking the proof in {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - proof_path ); - OUTCOME_TRY( ( auto &&, proof_data ), globaldb_m->Get( { proof_path } ) ); + if ( !tx ) + { + return false; + } - auto proof_data_vector = proof_data.toVector(); + if ( tx->HasUTXOParameters() ) + { + return true; + } - m_logger->debug( "[{} - full: {}] Proof data acquired. Verifying...", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return IBasicProof::VerifyFullProof( proof_data_vector ); + // Legacy mint transactions still create UTXOs for the source account. + return tx->GetType() == "mint"; } - outcome::result TransactionManager::QueryTransactions() + std::unordered_set TransactionManager::CollectTouchedAccounts( + const std::shared_ptr &tx ) const { - auto monitored_networks = GetMonitoredNetworkIDs(); - - for ( auto network_id : monitored_networks ) + std::unordered_set addresses; + if ( !tx ) { - 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 ); - 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() ); + return addresses; + } - for ( const auto &[key, value] : transaction_list ) + if ( tx->HasUTXOParameters() ) + { + auto params_opt = tx->GetUTXOParametersOpt(); + if ( params_opt.has_value() ) { - auto transaction_key = globaldb_m->KeyToString( key ); - if ( !transaction_key.has_value() ) + const auto &[inputs, outputs] = params_opt.value(); + if ( !inputs.empty() ) { - m_logger->error( "[{} - full: {}] Unable to convert a key to string", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - continue; + if ( full_node_m || tx->GetSrcAddress() == account_m->GetAddress() ) + { + addresses.insert( tx->GetSrcAddress() ); + } } - auto process_result = FetchAndProcessTransaction( transaction_key.value(), value ); - if ( !transaction_key.has_value() ) + for ( const auto &output : outputs ) { - m_logger->error( "[{} - full: {}] Unable to fetch and process transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_key.value() ); + 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 = utxo_manager_.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, utxo_manager_.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( + const std::shared_ptr &db, + std::string_view transaction_key ) + { + OUTCOME_TRY( auto transaction_data, db->Get( { std::string( transaction_key ) } ) ); + + return DeSerializeTransaction( transaction_data ); + } + + outcome::result> TransactionManager::DeSerializeTransaction( + const base::Buffer &tx_data ) + { + const auto &transaction_data_vector = tx_data.toVector(); + + OUTCOME_TRY( ( auto &&, dag ), IGeniusTransactions::DeSerializeDAGStruct( transaction_data_vector ) ); + + auto it = IGeniusTransactions::GetDeSerializers().find( dag.type() ); + if ( it == IGeniusTransactions::GetDeSerializers().end() ) + { + return std::errc::invalid_argument; + } + return it->second( transaction_data_vector ); + } + + outcome::result TransactionManager::CheckProof( const std::shared_ptr &tx ) + { + auto proof_path = GetTransactionProofPath( *tx ); + TransactionManagerLogger()->debug( "[{} - full: {}] Checking the proof in {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + proof_path ); + OUTCOME_TRY( ( auto &&, proof_data ), globaldb_m->Get( { proof_path } ) ); + + auto proof_data_vector = proof_data.toVector(); + + TransactionManagerLogger()->debug( "[{} - full: {}] Proof data acquired. Verifying...", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); + return IBasicProof::VerifyFullProof( proof_data_vector ); + } + + outcome::result TransactionManager::QueryTransactions() + { + auto monitored_networks = GetMonitoredNetworkIDs(); + + for ( auto network_id : monitored_networks ) + { + std::string blockchain_base = GetBlockChainBase( network_id ); + std::string query_path = blockchain_base + "tx"; + TransactionManagerLogger()->trace( "[{} - full: {}] Probing transactions on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + query_path ); + OUTCOME_TRY( auto transaction_list, globaldb_m->QueryKeyValues( query_path ) ); + + 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() ) + { + 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() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] Unable to fetch and process transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_key.value() ); + } + } + } + return outcome::success(); } @@ -1121,15 +1368,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(); } } @@ -1138,61 +1380,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; } + OUTCOME_TRY( ChangeTransactionState( transaction, next_tx_state ) ); return outcome::success(); } @@ -1208,23 +1446,23 @@ namespace sgns GeniusUTXO new_utxo( hash, i, dest_infos[i].encrypted_amount, dest_infos[i].token_id ); BOOST_OUTCOME_TRY( utxo_manager_.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( utxo_manager_.ConsumeUTXOs( transfer_tx->GetInputInfos(), transfer_tx->GetSrcAddress() ) ); return outcome::success(); @@ -1247,11 +1485,11 @@ namespace sgns utxo_manager_.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( utxo_manager_.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( utxo_manager_.GetBalance() ) ); return outcome::success(); } @@ -1264,11 +1502,11 @@ namespace sgns auto hash = ( base::Hash256::fromReadableString( mint_tx->GetHash() ) ).value(); BOOST_OUTCOME_TRY( utxo_manager_.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( utxo_manager_.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( utxo_manager_.GetBalance() ) ); return outcome::success(); } @@ -1305,17 +1543,17 @@ namespace sgns if ( !escrowReleaseTx ) { - m_logger->error( "[{} - full: {}] Failed to cast transaction to EscrowReleaseTransaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->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 ); + TransactionManagerLogger()->debug( "[{} - full: {}] Successfully fetched release for escrow: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowHash ); return outcome::success(); } @@ -1326,43 +1564,44 @@ 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( utxo_manager_.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( utxo_manager_.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() ); OUTCOME_TRY( ParseTransaction( tx ) ); } } - utxo_manager_.RollbackUTXOs( transfer_tx->GetInputInfos() ); + utxo_manager_.RollbackUTXOs( transfer_tx->GetInputInfos(), transfer_tx->GetHash() ); return outcome::success(); } @@ -1374,21 +1613,23 @@ namespace sgns 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 ) { - utxo_manager_.DeleteUTXO( hash, dest_info.dest_address ); + const auto &dest_info = outputs[i]; + OUTCOME_TRY( utxo_manager_.DeleteUTXO( hash, i, dest_info.dest_address ) ) } if ( !inputs.empty() ) { - utxo_manager_.RollbackUTXOs( inputs ); + utxo_manager_.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( utxo_manager_.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( utxo_manager_.GetBalance() ) ); return outcome::success(); } @@ -1399,13 +1640,13 @@ namespace sgns } auto hash = ( base::Hash256::fromReadableString( mint_tx->GetHash() ) ).value(); - BOOST_OUTCOME_TRY( utxo_manager_.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( utxo_manager_.GetBalance() ) ); + BOOST_OUTCOME_TRY( utxo_manager_.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( utxo_manager_.GetBalance() ) ); return outcome::success(); } @@ -1422,21 +1663,21 @@ namespace sgns auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); if ( outputs.size() > 1 ) { - BOOST_OUTCOME_TRY( utxo_manager_.DeleteUTXO( hash, outputs[1].dest_address ) ); + BOOST_OUTCOME_TRY( utxo_manager_.DeleteUTXO( hash, 1, outputs[1].dest_address ) ); } for ( auto &input : inputs ) { 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() ); OUTCOME_TRY( ParseTransaction( tx ) ); } } - utxo_manager_.RollbackUTXOs( inputs ); + utxo_manager_.RollbackUTXOs( inputs, escrow_tx->GetHash() ); } } @@ -1450,17 +1691,17 @@ namespace sgns if ( !escrowReleaseTx ) { - m_logger->error( "[{} - full: {}] Failed to cast transaction to EscrowReleaseTransaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->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 ); + TransactionManagerLogger()->debug( "[{} - full: {}] Successfully fetched release for escrow: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowHash ); return outcome::success(); } @@ -1499,6 +1740,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 @@ -1521,9 +1780,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 ) ); @@ -1542,10 +1801,10 @@ namespace sgns do { 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 ) { @@ -1553,29 +1812,29 @@ 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; } 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 ) ); @@ -1607,10 +1866,11 @@ namespace sgns 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() ); + TransactionManagerLogger()->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; @@ -1636,47 +1896,88 @@ namespace sgns 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 = utxo_manager_.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 = utxo_manager_.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 = utxo_manager_.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 = utxo_manager_.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; } @@ -1687,30 +1988,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; } @@ -1721,10 +2023,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; } @@ -1750,10 +2052,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; } @@ -1789,18 +2091,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; @@ -1808,45 +2110,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 ); @@ -1857,9 +2160,9 @@ 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; } @@ -1867,37 +2170,37 @@ namespace sgns auto local_nonce_result = account_m->GetPeerNonce( account_m->GetAddress() ); 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; @@ -1924,27 +2227,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 ); @@ -1952,12 +2256,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 @@ -1979,10 +2284,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; } } @@ -1990,29 +2296,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 ); } } @@ -2022,10 +2328,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 ) { @@ -2036,53 +2342,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 { @@ -2105,24 +2401,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 ); 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 ); 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(); } @@ -2138,12 +2434,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; } @@ -2158,7 +2453,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; } @@ -2166,6 +2461,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 ); @@ -2194,7 +2519,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 ) @@ -2205,211 +2532,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 ); @@ -2441,10 +2626,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; } @@ -2455,16 +2640,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 ); @@ -2489,70 +2674,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() @@ -2570,20 +2700,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; @@ -2608,10 +2739,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; } @@ -2619,19 +2751,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; @@ -2641,39 +2775,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 ) { @@ -2683,12 +2817,8 @@ namespace sgns auto topics = it->second.tx->GetTopics(); 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; @@ -2697,10 +2827,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(); } @@ -2710,101 +2840,114 @@ 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 ); 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(); + 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(); - OUTCOME_TRY( RemoveTransactionFromProcessedMaps( GetTransactionPath( conflict_hash ), true ) ); - tx_lock.lock(); + 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 ); - 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; + if ( blockchain_->CheckCertificate( new_tx->GetHash() ) ) { - 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() ); + OUTCOME_TRY( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ) ); + } } - - account_m->SetPeerConfirmedNonce( nonce, new_tx->dag_st.source_addr() ); - - tx_processed_m[key] = TrackedTx{ new_tx, TransactionStatus::CONFIRMED, nonce }; + 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() ); } } @@ -2818,10 +2961,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 ); } @@ -2842,20 +2986,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 { @@ -2864,7 +3008,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 ); @@ -2877,11 +3021,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; @@ -2890,11 +3034,11 @@ namespace sgns new_data_queue_.push( std::move( new_data ) ); } - m_logger->debug( "[{} - full: {}] CRDT new data queued, {} - (queue size: {})", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key, - new_data_queue_.size() ); + TransactionManagerLogger()->debug( "[{} - full: {}] CRDT new data queued, {} - (queue size: {})", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key, + new_data_queue_.size() ); // Notify the condition variable to wake up the main loop cv_.notify_one(); @@ -2909,11 +3053,11 @@ namespace sgns deleted_data_queue_.push( deleted_key ); } - m_logger->debug( "[{} - full: {}] CRDT deleted key queued, {} - (queue size: {})", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - deleted_key, - deleted_data_queue_.size() ); + TransactionManagerLogger()->debug( "[{} - full: {}] CRDT deleted key queued, {} - (queue size: {})", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + deleted_key, + deleted_data_queue_.size() ); // Notify the condition variable to wake up the main loop cv_.notify_one(); @@ -2937,11 +3081,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_ ) @@ -2963,11 +3107,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; @@ -2986,14 +3130,1423 @@ 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; + } + + void 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()->error( "[{} - full: {}] {}: Transaction not found for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return; + } + 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; + } + } + 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; + } + 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; + } + + 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; + } + + 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 = utxo_manager_.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() ); + } + } + + 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::SubjectCheck::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::SubjectCheck + { + 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::SubjectCheck::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" ); + } + + auto validate_result = ValidateTransactionForConsensus( tracked_tx ); + + if ( !validate_result ) + { + return reject_and_maybe_fail_local( "transaction validation failed" ); + } + + return ConsensusManager::SubjectCheck::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 ( !utxo_manager_.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() ); + auto nonce_result = account_m->GetPeerNonce( tx.GetSrcAddress() ); + if ( nonce_result.has_error() ) + { + if ( tx.GetNonce() == 0 ) + { + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Transaction Nonce 0 for {}. No need to check previous transactions", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetSrcAddress() ); + //TODO - Possibly check account creation + return true; + } + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: No confirmed nonce for address {}. Checking certificate and nonce from previous hash", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetSrcAddress() ); + const auto previous_hash = tx.GetPreviousHash(); + if ( previous_hash.empty() ) + { + return false; + } + auto previous_cert_result = blockchain_->GetCertificateBySubjectHash( previous_hash ); + if ( previous_cert_result.has_error() ) + { + 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; + } + return ( previous_subject.nonce().nonce() + 1 ) == tx.GetNonce(); + } + + 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(), utxo_manager_ ); + } + + 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 = utxo_manager_.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 = utxo_manager_.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() ); + OUTCOME_TRY( RevertTransaction( tx ) ); + + 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(); + 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() ); + OUTCOME_TRY( ParseTransaction( tx ) ); + account_m->SetPeerConfirmedNonce( tx->GetNonce(), tx->GetSrcAddress() ); + { + 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() ); + OUTCOME_TRY( RevertTransaction( tx ) ); + + 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() ) + { + utxo_manager_.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 d223e9757..6e0e8afb5 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -24,10 +24,13 @@ #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 "proof/proto/SGProof.pb.h" #include "processing/proto/SGProcessing.pb.h" @@ -96,6 +99,7 @@ namespace sgns UTXOManager &utxo_manager, 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 ) ); @@ -109,6 +113,9 @@ namespace sgns std::vector> GetOutTransactions() const; std::vector> GetInTransactions() const; + std::vector> GetTransactions( + std::optional tx_status = std::nullopt ) const; + std::vector> GetTransactions() const; outcome::result TransferFunds( uint64_t amount, const std::string &destination, TokenID token_id ); outcome::result MintFunds( uint64_t amount, @@ -197,11 +204,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, UTXOManager &utxo_manager, 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 ); @@ -210,10 +231,10 @@ namespace sgns using TransactionParserFn = outcome::result ( TransactionManager::* )( const std::shared_ptr & ); - SGTransaction::DAGStruct FillDAGStruct( std::string transaction_hash = "" ) const; - outcome::result> SendTransactionItem( TransactionItem &item ); - outcome::result ConfirmTransactions(); - outcome::result RollbackTransactions( TransactionItem &item_to_rollback ); + SGTransaction::DAGStruct FillDAGStruct( std::optional other_chain_hash = std::nullopt ); + std::string GetOutgoingPreviousHash( uint64_t nonce ) const; + outcome::result SendTransactionItem( TransactionItem &item ); + outcome::result RollbackTransactions( TransactionItem &item_to_rollback ); static std::vector GetMonitoredNetworkIDs(); static std::string GetBlockChainBase(); @@ -226,6 +247,11 @@ namespace sgns outcome::result CheckProof( const std::shared_ptr &tx ); outcome::result ParseTransaction( const std::shared_ptr &tx ); 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 ); void InitializeUTXOs(); void InitTransactions(); @@ -244,15 +270,20 @@ namespace sgns std::shared_ptr GetTransactionByHashNoLock( const std::string &tx_hash ) const; 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 ); + void OnConsensusCertificate( const std::string &tx_hash ); + std::shared_ptr globaldb_m; std::shared_ptr ctx_m; std::shared_ptr account_m; UTXOManager &utxo_manager_; std::shared_ptr hasher_m; + std::shared_ptr blockchain_; bool full_node_m; std::string full_node_topic_m; ///< formatted full-node topic void TickOnce(); @@ -275,23 +306,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_; @@ -330,8 +359,6 @@ namespace sgns { &TransactionManager::ParseEscrowReleaseTransaction, &TransactionManager::RevertEscrowReleaseTransaction } } }; - base::Logger m_logger = base::createLogger( "TransactionManager" ); - std::optional> FilterTransaction( const crdt::pb::Element &element ); std::optional> FilterProof( const crdt::pb::Element &element ); @@ -358,7 +385,44 @@ namespace sgns void ChangeState( State new_state ); public: - outcome::result GetTransactionCID( const std::string &tx_hash ) const; + enum class WitnessValidationResult : uint8_t + { + VALID, + DRIFT, + INVALID + }; + + 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 73ad74d45..cd71956e9 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,64 @@ 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 +87,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 +128,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() ) ) { - balance += utxo.GetAmount(); + continue; + } + if ( reserved_outpoints_.find( outpoint ) == reserved_outpoints_.end() ) + { + 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 +164,23 @@ 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 ) - { - utxo_list.erase( it ); - is_new = false; - break; - } - //TODO - If it's the same, might be locked, then unlock - is_new = false; - break; - } - if ( is_new ) + std::unique_lock lock( utxos_mutex_ ); + if ( auto existing = utxo_outpoints_.find( outpoint ); existing != utxo_outpoints_.end() ) { - utxo_list.emplace_back( UTXOState::UTXO_READY, std::move( new_utxo ) ); - BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); + return false; } - return is_new; + + utxo_outpoints_[outpoint] = UTXOEntry{ UTXOState::UTXO_READY, new_utxo }; + address_outpoints_[address].push_back( outpoint ); + + 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_ ) @@ -123,23 +189,20 @@ namespace sgns } 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(); ) - { - auto &[state, curr] = *utxo_it; - if ( curr.GetTxID() == utxo_id ) - { - utxo_it = utxo_list.erase( utxo_it ); - deleted = true; - continue; - } - ++utxo_it; - } - if ( deleted ) + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) + { + 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() ) { + const OutPoint outpoint = *outpoint_it; + reserved_outpoints_.erase( outpoint ); + utxo_outpoints_.erase( outpoint ); + outpoints.erase( outpoint_it ); BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); } } @@ -152,35 +215,33 @@ namespace sgns { bool consumed = true; std::unique_lock lock( utxos_mutex_ ); - auto &utxo_list = utxos_[address]; for ( auto &input_info : infos ) { - bool utxo_found = false; - auto utxo_it = utxo_list.end(); - for ( auto it = utxo_list.begin(); it != utxo_list.end(); ++it ) + 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() ) { - auto &[state, curr] = *it; - if ( input_info.txid_hash_ != curr.GetTxID() ) + auto &entry = canonical_it->second; + if ( entry.state == UTXOState::UTXO_READY && entry.utxo.GetOwnerAddress() == address ) { - continue; + utxo_found = true; + entry.state = UTXOState::UTXO_CONSUMED; } - if ( input_info.output_idx_ != curr.GetOutputIdx() ) - { - continue; - } - utxo_found = true; - utxo_it = it; - break; } - if ( utxo_found ) + + reserved_outpoints_.erase( outpoint ); + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { - utxo_list.erase( utxo_it ); + RemoveOutPointFromVector( address_it->second, outpoint ); } - else + + if ( !utxo_found ) { - GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID() ); - utxo_list.emplace_back( UTXOState::UTXO_CONSUMED, consumed_utxo ); + GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID(), address ); + utxo_outpoints_[outpoint] = UTXOEntry{ UTXOState::UTXO_CONSUMED, consumed_utxo }; } + consumed = consumed && utxo_found; } @@ -192,17 +253,55 @@ 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; + } + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + result.push_back( utxo_it->second.utxo ); + } + return result; + } + return {}; + } + + std::vector UTXOManager::GetUTXOsForReservation( const std::string &address, + const std::string &reservation_id ) const + { + std::shared_lock lock( utxos_mutex_ ); + 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 ); + + result.push_back( utxo_it->second.utxo ); } return result; } @@ -212,7 +311,14 @@ namespace sgns std::unordered_map> UTXOManager::GetAllUTXOs() const { std::shared_lock lock( utxos_mutex_ ); - return utxos_; + 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 ) @@ -225,12 +331,27 @@ namespace sgns } std::unique_lock lock( utxos_mutex_ ); - auto &utxo_list = utxos_[address]; - utxo_list.clear(); - utxo_list.reserve( utxos.size() ); + + 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 ) { - utxo_list.emplace_back( UTXOState::UTXO_READY, utxo ); + 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 }; + outpoints.push_back( outpoint ); } if ( auto res = StoreUTXOs( address ); res.has_error() ) @@ -294,75 +415,98 @@ 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 - { - for ( const auto &[state, utxo] : utxos_.at( address ) ) - { - 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; - } - } - } - } - catch ( const std::out_of_range & ) + if ( address_outpoints_.find( address ) == address_outpoints_.end() ) { logger_->warn( "Could not find UTXOs from address {}", address ); return false; } - lock.unlock(); + std::unordered_set seen_inputs; + seen_inputs.reserve( params.first.size() ); + + for ( const auto &input : params.first ) + { + if ( !verify_signature_( address, input.signature_, input.SerializeForSigning() ) ) + { + 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; + } + + if ( utxo_it->second.utxo.GetOwnerAddress() != address ) + { + logger_->warn( "Outpoint {}:{} does not belong to {}", input.txid_hash_.toReadableString(), input.output_idx_, address ); + return false; + } + + expected_amount += utxo_it->second.utxo.GetAmount(); + } uint64_t real_amount = std::accumulate( params.second.cbegin(), params.second.cend(), @@ -370,7 +514,53 @@ 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(); + } + + 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 ) @@ -386,7 +576,9 @@ namespace sgns logger_->warn( "UTXOs were already loaded" ); } db_ = std::move( db ); - utxos_.clear(); + utxo_outpoints_.clear(); + address_outpoints_.clear(); + reserved_outpoints_.clear(); base::Buffer key_buf; key_buf.put( DB_PREFIX ); @@ -411,35 +603,62 @@ namespace sgns 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; + 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(); - if ( !utxos.ParseFromArray( params.data(), params.size() ) ) + SGTransaction::UTXOEntryRecord entry_record; + if ( !entry_record.ParseFromArray( params.data(), params.size() ) ) { - logger_->error( "Failed to deserialize UTXOs" ); + logger_->error( "Failed to deserialize UTXO record for address {}", address ); return std::errc::bad_message; } - utxos_[address].reserve( utxos.utxos_size() ); - - for ( int i = 0; i < utxos.utxos_size(); ++i ) + if ( !entry_record.owner_address().empty() && entry_record.owner_address() != address ) { - const auto &utxo = utxos.utxos( i ); - OUTCOME_TRY( auto hash, - base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( utxo.hash().data() ) ), - utxo.hash().size() ) ) ); - - auto token_id = TokenID::FromBytes( utxo.token().data(), utxo.token().size() ); + logger_->warn( "UTXO owner mismatch in key/value for {}", address ); + } - utxos_[address].emplace_back( UTXOState::UTXO_READY, - GeniusUTXO( hash, utxo.output_idx(), utxo.amount(), token_id ) ); + const auto state = FromProtoState( entry_record.state() ); + + 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() ) + { + 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; } + + utxo_outpoints_[outpoint] = std::move( loaded_entry ); + address_outpoints_[address].push_back( outpoint ); } - return true; + return !utxo_outpoints_.empty(); } outcome::result UTXOManager::StoreUTXOs( const std::string &address ) @@ -450,52 +669,245 @@ namespace sgns return storage::DatabaseError::UNITIALIZED; } - SGTransaction::UTXOList utxos; + base::Buffer existing_prefix; + existing_prefix.put( fmt::format( "{}/{}/", DB_PREFIX, address ) ); - try + auto existing_records = db_->query( existing_prefix ); + if ( existing_records.has_error() && existing_records.error() != storage::DatabaseError::NOT_FOUND ) { - for ( const auto &[state, utxo] : utxos_.at( address ) ) + 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 ( state != UTXOState::UTXO_READY ) + if ( auto rem_res = db_->remove( existing_key ); rem_res.has_error() ) { - continue; + logger_->error( "Failed to remove old UTXO record for address {}", address ); + return rem_res.error(); } - 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() ); } } - catch ( const std::out_of_range & ) + + uint64_t stored = 0; + for ( const auto &[outpoint, entry] : utxo_outpoints_ ) { - logger_->error( "There are no UTXOs in cache for address {}", address ); - return std::errc::bad_address; + if ( entry.utxo.GetOwnerAddress() != address ) + { + continue; + } + + 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; } - base::Buffer buf( std::vector( utxos.ByteSizeLong() ) ); - if ( !utxos.SerializeToArray( buf.data(), buf.size() ) ) + 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 ) + { + if ( db_ == nullptr ) { - logger_->error( "Failed to serialize to array" ); + logger_->error( "Tried to create checkpoint without loading DB" ); + return storage::DatabaseError::UNITIALIZED; + } + + if ( !is_full_node_ && address != address_ ) + { + logger_->warn( "Non-full node cannot create checkpoint for other addresses" ); + return std::errc::permission_denied; + } + + std::vector unspent_snapshot; + { + 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 + { + 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; + } + + 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() ) ) ); + OUTCOME_TRY( auto registry_hash, + base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( checkpoint_record.registry_hash().data() ) ), + checkpoint_record.registry_hash().size() ) ) ); + 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 +915,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 ) - { - continue; - } - if ( !token_id.Equals( utxo.GetTokenID() ) ) + for ( const auto &outpoint : address_it->second ) { - continue; - } + if ( selected_amount >= required_amount ) + { + break; + } + + 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( { utxo.GetTxID(), utxo.GetOutputIdx(), {} } ); - selected_amount += utxo.GetAmount(); + 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 b4bf979fd..62b593ea5 100644 --- a/src/account/UTXOManager.hpp +++ b/src/account/UTXOManager.hpp @@ -7,21 +7,54 @@ #include "crdt/globaldb/globaldb.hpp" #include "storage/rocksdb/rocksdb.hpp" +#include #include +#include +#include namespace sgns { + struct OutPointHash + { + size_t operator()( const OutPoint &outpoint ) const + { + size_t seed = std::hash{}( outpoint.txid_hash_ ); + boost::hash_combine( seed, outpoint.output_idx_ ); + return seed; + } + }; + class UTXOManager { public: enum class UTXOState : uint8_t { UTXO_READY, - UTXO_RESERVED, UTXO_CONSUMED }; using UTXOData = std::pair; + struct UTXOEntry + { + UTXOState state{ UTXOState::UTXO_READY }; + GeniusUTXO utxo; + uint64_t created_epoch{ 0 }; + std::optional spent_epoch; + std::optional spent_by_txid; + }; + struct UTXOCheckpoint + { + std::string owner_address; + uint64_t epoch{ 0 }; + base::Hash256 last_finalized_tx{}; + base::Hash256 registry_hash{}; + base::Hash256 utxo_merkle_root{}; + uint64_t utxo_count{ 0 }; + uint64_t created_at_ms{ 0 }; + }; + + using UTXOOutPointMap = std::unordered_map; + using AddressOutPointList = std::unordered_map>; using SignFunc = std::function( const std::vector &data )>; using VerifySignatureFunc = std::function &signature, @@ -71,9 +104,10 @@ 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 @@ -100,6 +134,9 @@ namespace sgns return GetUTXOs( address_ ); } + std::vector GetUTXOsForReservation( const std::string &address, + const std::string &reservation_id ) const; + std::unordered_map> GetAllUTXOs() const; /** @@ -121,9 +158,9 @@ namespace sgns outcome::result CreateTxParameter( const std::vector &destinations, const TokenID &token_id ); - void ReserveUTXOs( const std::vector &inputs ); + void ReserveUTXOs( const std::vector &inputs, const std::string &reservation_id ); - void RollbackUTXOs( const std::vector &inputs ); + void RollbackUTXOs( const std::vector &inputs, const std::string &reservation_id ); bool VerifyParameters( const UTXOTxParameters ¶ms ) const { @@ -132,6 +169,21 @@ namespace sgns bool VerifyParameters( const UTXOTxParameters ¶ms, const std::string &address ) const; + /** + * @brief Compute a deterministic Merkle root for unspent UTXOs owned by this node address + */ + [[nodiscard]] base::Hash256 ComputeUTXOMerkleRoot() const; + + /** + * @brief Compute a deterministic Merkle root for unspent UTXOs from a specific address + */ + [[nodiscard]] base::Hash256 ComputeUTXOMerkleRoot( const std::string &address ) const; + + /** + * @brief Compute deterministic UTXO Merkle root from an explicit UTXO snapshot + */ + [[nodiscard]] base::Hash256 ComputeUTXOMerkleRootFromSnapshot( const std::vector &utxos ) const; + outcome::result LoadUTXOs( std::shared_ptr db ); /** @@ -139,8 +191,25 @@ namespace sgns */ outcome::result StoreUTXOs( const std::string &address ); + outcome::result CreateCheckpoint( uint64_t epoch, + const base::Hash256 &last_finalized_tx, + const base::Hash256 ®istry_hash ); + + outcome::result CreateCheckpoint( const std::string &address, + uint64_t epoch, + const base::Hash256 &last_finalized_tx, + const base::Hash256 ®istry_hash ); + + outcome::result> LoadLatestCheckpoint() const + { + return LoadLatestCheckpoint( address_ ); + } + + outcome::result> LoadLatestCheckpoint( const std::string &address ) const; + private: static constexpr std::string_view DB_PREFIX = "/utxo"; + static constexpr std::string_view CHECKPOINT_PREFIX = "/utxo-checkpoint"; outcome::result, uint64_t>> SelectUTXOs( uint64_t required_amount, const TokenID &token_id ); @@ -155,8 +224,10 @@ namespace sgns 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 + mutable std::shared_mutex utxos_mutex_; ///< Mutex for UTXO state structures + UTXOOutPointMap utxo_outpoints_; + AddressOutPointList address_outpoints_; + std::unordered_map reserved_outpoints_; }; } diff --git a/src/account/UTXOMerkle.hpp b/src/account/UTXOMerkle.hpp new file mode 100644 index 000000000..83e01fcb5 --- /dev/null +++ b/src/account/UTXOMerkle.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include "account/GeniusUTXO.hpp" +#include "crypto/sha/sha256.hpp" + +#include +#include +#include + +namespace sgns::utxo_merkle +{ + constexpr uint8_t kLeafPrefix = 0x00; + constexpr uint8_t kNodePrefix = 0x01; + + 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 ) ); + } + + 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 ) ); + } + + 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] ); + } + + 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] ); + } + + inline std::string OutPointKey( const base::Hash256 &txid, uint32_t idx ) + { + return txid.toReadableString() + ":" + std::to_string( idx ); + } + + 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; + } + + 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() ) ); + } + + 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() ) ); + } + + inline base::Hash256 EmptyUTXOMerkleRoot() + { + static const base::Hash256 empty_root = crypto::sha256( std::string_view( "UTXO_EMPTY_V1" ) ); + return empty_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(); + } + + 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 ) ); + } + + 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/proto/SGTransaction.proto b/src/account/proto/SGTransaction.proto index bb4a5ff32..2df6f5ea8 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; diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index b5f081cf5..147b8bd4a 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -20,16 +20,14 @@ #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; @@ -65,9 +63,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,11 +100,37 @@ 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; @@ -123,10 +148,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 +161,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 +190,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 +224,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 +234,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..d5d8905c5 --- /dev/null +++ b/src/blockchain/Consensus.cpp @@ -0,0 +1,2483 @@ +/** + * @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->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__ ); + } + + 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]() + { + 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; + } + self->timer_cv_.wait( lock, + [self]() + { return self->stop_timer_.load() || self->certificates_pending_.load(); } ); + if ( self->stop_timer_.load() ) + { + return; + } + lock.unlock(); + self->ProcessCertificates(); + self->UpdateCertificatesPending(); + lock.lock(); + if ( self->certificates_pending_.load() && !self->stop_timer_.load() ) + { + self->timer_cv_.wait_for( + lock, + interval, + [self]() { return self->stop_timer_.load() || !self->certificates_pending_.load(); } ); + } + if ( self->stop_timer_.load() ) + { + return; + } + } + } ); + } + + 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 ) ); + 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() ); + } + + 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() ); + } + + 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() == SubjectCheck::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() == SubjectCheck::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() == SubjectCheck::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() == SubjectCheck::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 = "^/?cert/[^/]+"; + + 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 ( !ValidateCertificate( certificate ) ) + { + 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() ) + { + 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() ) + { + return; + } + handler = it->second; + } + + handler( subject_hash.value(), certificate ); + } + + bool ConsensusManager::ValidateCertificate( const Certificate &certificate ) const + { + if ( certificate.proposal_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: Certificate proposal ID missing ", __func__ ); + return false; + } + if ( !certificate.has_proposal() ) + { + ConsensusManagerLogger()->error( "{}: Certificate missing proposal ", __func__ ); + return false; + } + + 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 false; + } + 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 false; + } + 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 false; + } + auto ®istry = registry_ret.value(); + if ( !ValidateSubject( proposal.subject() ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: invalid subject proposal_id={}", + __func__, + proposal.proposal_id() ); + return false; + } + if ( !CheckProposal( proposal ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: invalid proposal proposal_id={}", + __func__, + proposal.proposal_id() ); + return false; + } + + const auto computed_id = CreateProposalId( proposal ); + if ( computed_id.empty() ) + { + ConsensusManagerLogger()->error( "{}: rejected: computed_id empty", __func__ ); + return false; + } + if ( computed_id != certificate.proposal_id() ) + { + ConsensusManagerLogger()->error( "{}: rejected: computed_id mismatch cert={} computed={}", + __func__, + certificate.proposal_id(), + computed_id ); + return false; + } + + 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 false; + } + + return true; + } + + 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 ) ) + { + 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; + } + + outcome::result ConsensusManager::GetCertificateBySubjectHash( + const std::string &subject_hash ) const + { + const auto key = std::string{ CERTIFICATE_BASE_PATH_KEY } + subject_hash; + + 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; + } + //TODO - Check if we need to call ValidateCertificate here. I don't think so because it was validated before. + return true; + } + + 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; + ConsensusManagerLogger()->debug( "{}: Match for subject and certificate (hash {}): {}", + __func__, + GetPrintableSubjectHash( subject ), + equal ? "Match" : "MISMATCH" ); + return equal; + } + + 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..6f3fa43be --- /dev/null +++ b/src/blockchain/Consensus.hpp @@ -0,0 +1,270 @@ +/** + * @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/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 Subject checking values + */ + enum class SubjectCheck + { + Approve, ///< Subject is approved + Reject, ///< Subject is rejected + Pending ///< Subject evaluation is pending + }; + + /// @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; + + /** + * @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(); + + 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 ); + bool 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::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 24617b824..a9012e3b8 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 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 ); + + 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,846 @@ 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 ); + } + + void 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; + } + if ( applying_batch_subject_ids_.find( subject_hash ) != applying_batch_subject_ids_.end() ) + { + return; + } + 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; + } + + 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; + } + if ( !ValidateCertificate( certificate, current_registry_result.value() ) ) + { + std::lock_guard lock( batch_mutex_ ); + applying_batch_subject_ids_.erase( subject_hash ); + return; + } + + 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; + } + + 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; + } + + 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; + } + 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; + } + 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(); + } + + 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 +1405,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..ade047717 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,47 @@ namespace sgns::blockchain std::string genesis_authority, BlockRequestMethod block_request_method, InitCallback init_callback = nullptr ); + ~ValidatorRegistry(); - 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; + 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(); + 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 + }; + outcome::result EvaluateBatchSubject( const ConsensusSubject &subject ); + void HandleBatchCertificate( const std::string &subject_hash, + const sgns::ConsensusCertificate &certificate ); static constexpr std::string_view RegistryKey() { @@ -95,6 +134,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 +143,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,12 +162,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(); - void NotifyInitialized( bool success ) const; - void PersistLocalState( const std::string &cid ) const; - void RequestHeadCids( const std::set &cids ); + 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 ); std::shared_ptr db_; uint64_t quorum_numerator_; @@ -130,7 +210,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 d72471bef..517f7485f 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 ) ) ); @@ -98,11 +99,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( @@ -133,6 +134,86 @@ 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::SubjectCheck::Approve; + case ValidatorRegistry::BatchSubjectDecision::Pending: + return ConsensusManager::SubjectCheck::Pending; + case ValidatorRegistry::BatchSubjectDecision::Reject: + default: + return ConsensusManager::SubjectCheck::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 ) + { + if ( auto strong = weak_ptr.lock() ) + { + strong->validator_registry_->HandleBatchCertificate( subject_hash, certificate ); + } + } ); + auto ensure_registry_result = instance->EnsureValidatorRegistry(); if ( ensure_registry_result.has_error() ) { @@ -315,6 +396,19 @@ 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(); } @@ -794,8 +888,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( @@ -888,7 +982,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() ); @@ -926,12 +1020,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 @@ -948,10 +1042,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(); @@ -964,7 +1058,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 ) ); @@ -998,7 +1092,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 ) ); @@ -1046,8 +1140,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() ); @@ -1117,7 +1211,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() ); @@ -1168,7 +1262,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() ) ) ) { @@ -1202,7 +1296,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() ) ) ) { @@ -1250,7 +1344,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() ) ) ) { @@ -1304,7 +1398,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() ) ) ) { @@ -1354,8 +1448,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() ) { @@ -1364,8 +1457,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() ) { @@ -1377,6 +1470,10 @@ 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(); } @@ -1491,9 +1588,96 @@ 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 ) + { + OUTCOME_TRY( auto &&nonce_subject, + CreateConsensusNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ) ); + 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/crdt_datastore.hpp b/src/crdt/crdt_datastore.hpp index e5e5b25dd..a77c3cceb 100644 --- a/src/crdt/crdt_datastore.hpp +++ b/src/crdt/crdt_datastore.hpp @@ -35,10 +35,6 @@ namespace sgns { class Blockchain; -} - -namespace sgns::blockchain -{ class ValidatorRegistry; } @@ -215,6 +211,9 @@ 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 ); /** * @brief Configure which topic this datastore should filter on. @@ -243,10 +242,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 { diff --git a/src/crdt/globaldb/globaldb.cpp b/src/crdt/globaldb/globaldb.cpp index b86369e8c..1a2cbe579 100644 --- a/src/crdt/globaldb/globaldb.cpp +++ b/src/crdt/globaldb/globaldb.cpp @@ -356,6 +356,21 @@ 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; @@ -429,4 +444,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 b00f91958..5cfad94c5 100644 --- a/src/crdt/globaldb/globaldb.hpp +++ b/src/crdt/globaldb/globaldb.hpp @@ -150,6 +150,9 @@ namespace sgns::crdt 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(); @@ -175,6 +178,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/crdt_datastore.cpp b/src/crdt/impl/crdt_datastore.cpp index 8da8e4b59..a9d29dabf 100644 --- a/src/crdt/impl/crdt_datastore.cpp +++ b/src/crdt/impl/crdt_datastore.cpp @@ -1574,6 +1574,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 ); @@ -1752,4 +1767,27 @@ namespace sgns::crdt std::lock_guard lock( topicNamesMutex_ ); return topicNames_; } + + outcome::result>> CrdtDatastore::GetILPDNodeContent( + const std::string &cid_string ) + { + OUTCOME_TRY( auto cid, CID::fromString( cid_string ) ); + + OUTCOME_TRY( auto node, dagSyncer_->GetNodeWithoutRequest( cid ) ); + + //TODO - Check if filtering is needed here. Currently not filtering. + 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/test/src/CMakeLists.txt b/test/src/CMakeLists.txt index 4a8ee65e8..c332e991a 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/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 264d9ee0a..04d0905f6 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,13 +23,25 @@ #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/wait_condition.hpp" +namespace +{ + std::string NextMintSourceHash() + { + static std::atomic mint_counter{ 1 }; + const auto value = mint_counter.fetch_add( 1 ); + char buf[65] = {}; + std::snprintf( buf, sizeof( buf ), "%064llx", static_cast( value ) ); + return std::string( buf ); + } +} // namespace + class BlockchainGenesisTest : public ::testing::Test { protected: @@ -304,7 +317,7 @@ TEST_F( BlockchainGenesisTest, WithAuthorizationCanSyncAndProcessTransactions ) auto balance_regular_2_before = node_regular_2->GetBalance(); // Mint tokens on the first regular node after sync is confirmed - auto mint_result = node_regular_1->MintTokens( mint_amount, "", "", token_id ); + auto mint_result = node_regular_1->MintTokens( mint_amount, NextMintSourceHash(), "", token_id ); 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..94ffc4447 --- /dev/null +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -0,0 +1,549 @@ +#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 ); + 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 ); + return account; + } + + std::shared_ptr MakeRegistry( const std::shared_ptr &db, + const std::shared_ptr &account ) + { + using sgns::ValidatorRegistry; + 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 ) + { + 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() ); + auto registry = MakeRegistry( db_, account ); + + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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() ); + auto registry = MakeRegistry( db_, account ); + + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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::SubjectCheck::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() ); + auto registry = MakeRegistry( db_, account ); + + 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() ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + EXPECT_TRUE( manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, + []( const ConsensusManager::Subject & ) + { return ConsensusManager::SubjectCheck::Approve; } ) ); + EXPECT_TRUE( manager->RegisterCertificateHandler( SubjectType::SUBJECT_NONCE, + []( const std::string &, + const ConsensusManager::Certificate & ) + { } ) ); + 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() ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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() ); + 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 ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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() ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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::SubjectCheck::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() ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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::SubjectCheck::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::SubjectCheck::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() ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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 ); + } + } ); + + 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() ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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() ); + auto registry = MakeRegistry( db_, account ); + auto manager = MakeManager( registry, db_, pubs_, account ); + + 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 52d3e754f..e9a11ffc4 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,15 +24,31 @@ #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/wait_condition.hpp" +#include "blockchain/ValidatorRegistry.hpp" class MultiAccountTest : public ::testing::Test { protected: + static std::string NextMintSourceHash() + { + static std::atomic mint_counter{ 1 }; + const auto value = mint_counter.fetch_add( 1 ); + + char suffix[17] = {}; + std::snprintf( suffix, sizeof( suffix ), "%016llx", static_cast( value ) ); + + return std::string( 48, '0' ) + suffix; + } + std::shared_ptr CreateNode( const std::string &self_address, const std::string &dev_addr, const std::string &tokenValue, @@ -125,6 +142,8 @@ class MultiAccountTest : public ::testing::Test removeWithRetry( binaryPath + "/node_multi_account_0/" ); removeWithRetry( binaryPath + "/node_multi_account_1/" ); removeWithRetry( binaryPath + "/node_multi_account_2/" ); + removeWithRetry( binaryPath + "/node_multi_account_3/" ); + removeWithRetry( binaryPath + "/node_multi_account_4/" ); } void TearDown() override @@ -135,7 +154,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", @@ -165,12 +184,12 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) auto balance_original_start = node_original->GetBalance(); // Mint some tokens - auto mint_result = node_original->MintTokens( 100, "", "", TokenID::FromBytes( { 0x00 } ) ); + auto mint_result = node_original->MintTokens( 100, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_original"; - mint_result = node_original->MintTokens( 2000, "", "", TokenID::FromBytes( { 0x00 } ) ); + mint_result = node_original->MintTokens( 2000, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_original"; - mint_result = node_original->MintTokens( 30, "", "", TokenID::FromBytes( { 0x00 } ) ); + mint_result = node_original->MintTokens( 30, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_original"; @@ -190,18 +209,22 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) std::chrono::milliseconds( 30000 ), "node_duplicated not synced" ); - mint_result = node_duplicated->MintTokens( 60000, "", "", TokenID::FromBytes( { 0x00 } ) ); + mint_result = node_duplicated->MintTokens( 60000, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_duplicated"; test::assertWaitForCondition( [&] { 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 @@ -264,9 +287,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, @@ -277,7 +302,7 @@ 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 - "", + NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result_1.has_value() ) << "Mint transaction failed on node_same_addr_1"; @@ -326,14 +351,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 ), - "node_same_addr_2 balance not synced" ); + "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_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 @@ -347,8 +401,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 ); @@ -360,3 +416,412 @@ 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 ) + { + ASSERT_TRUE( node ); + ASSERT_TRUE( node->blockchain_ ); + ASSERT_TRUE( node->blockchain_->consensus_manager_ ); + + 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 ); + + 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 ); + + 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" ); + + 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, 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, 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 ) + { + ASSERT_TRUE( node ); + ASSERT_TRUE( node->blockchain_ ); + ASSERT_TRUE( node->blockchain_->consensus_manager_ ); + + auto node_registry = node->blockchain_->GetValidatorRegistry(); + ASSERT_TRUE( node_registry ); + + node_registry->SetCertificatesPerBatch( kCertificatesPerBatch ); + node->blockchain_->consensus_manager_->ConfigureCertificateDelay( kCertificateDelay ); + }; + + 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 ); + + 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" ); + + 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, 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, 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 aeeaef497..eb7615e6f 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 @@ -25,6 +26,17 @@ #include #include +namespace +{ + std::string NextMintSourceHash() + { + static uint64_t mint_counter = 1; + char buf[65] = {}; + std::snprintf( buf, sizeof( buf ), "%064llx", static_cast( mint_counter++ ) ); + return std::string( buf ); + } +} // namespace + class ProcessingMultiTest : public ::testing::Test { protected: @@ -135,8 +147,8 @@ std::string ProcessingMultiTest::binary_path = ""; TEST_F( ProcessingMultiTest, MintTokens ) { - node_main->MintTokens( 50000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); - node_main->MintTokens( 50000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + node_main->MintTokens( 50000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); + node_main->MintTokens( 50000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); std::this_thread::sleep_for( std::chrono::milliseconds( 10000 ) ); } diff --git a/test/src/processing_nodes/child_tokens_test.cpp b/test/src/processing_nodes/child_tokens_test.cpp index 7b03ab63c..bb5735bd3 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 @@ -22,6 +23,15 @@ using boost::multiprecision::cpp_dec_float_50; namespace { + std::string NextMintSourceHash() + { + static std::atomic mint_counter{ 1 }; + const auto value = mint_counter.fetch_add( 1 ); + char buf[65] = {}; + std::snprintf( buf, sizeof( buf ), "%064llx", static_cast( value ) ); + return std::string( buf ); + } + /** * @brief Helper to create a GeniusNode with its own directory and cleanup. * @param tokenValue TokenValueInGNUS to initialize DevConfig. @@ -181,11 +191,13 @@ TEST( TransferTokenValue, ThreeNodeTransferTest ) } // Ensure enough balance with +1 change - auto mintRes51 = node51->MintTokens( totalMint51 + 1, "", "", sgns::TokenID::FromBytes( { 0x51 } ) ); + auto mintRes51 = + node51->MintTokens( totalMint51 + 1, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x51 } ) ); 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, "", "", sgns::TokenID::FromBytes( { 0x52 } ) ); + auto mintRes52 = + node52->MintTokens( totalMint52 + 1, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x52 } ) ); ASSERT_TRUE( mintRes52.has_value() ) << "Grouped mint failed on token52"; std::cout << "Minted total " << ( totalMint52 + 1 ) << " of token52 on node52\n"; @@ -275,7 +287,7 @@ TEST_P( GeniusNodeMintMainTest, MintMainBalance ) auto parsedInitialChild = node->ParseTokens( initialChildStr, p.TokenID ); ASSERT_TRUE( parsedInitialChild.has_value() ); - auto res = node->MintTokens( p.mintMain, "", "", p.TokenID ); + auto res = node->MintTokens( p.mintMain, NextMintSourceHash(), "", p.TokenID ); ASSERT_TRUE( res.has_value() ); auto finalFmtRes = node->FormatTokens( node->GetBalance(), p.TokenID ); @@ -349,7 +361,7 @@ TEST_P( GeniusNodeMintChildTest, MintChildBalance ) auto parsedMint = node->ParseTokens( p.mintChild, p.TokenID ); ASSERT_TRUE( parsedMint.has_value() ); - auto res = node->MintTokens( parsedMint.value(), "", "", p.TokenID ); + auto res = node->MintTokens( parsedMint.value(), NextMintSourceHash(), "", p.TokenID ); ASSERT_TRUE( res.has_value() ); auto finalFmtRes = node->FormatTokens( node->GetBalance(), p.TokenID ); @@ -426,7 +438,7 @@ TEST( GeniusNodeMultiTokenMintTest, MintMultipleTokenIds ) for ( const auto &tm : mints ) { - auto res = node->MintTokens( tm.amount, "", "", tm.tokenId ); + auto res = node->MintTokens( tm.amount, NextMintSourceHash(), "", tm.tokenId ); ASSERT_TRUE( res.has_value() ); // << "MintTokens failed for token=" << tm.tokenId << " amount=" << tm.amount; expectedTotals[tm.tokenId] += tm.amount; @@ -481,7 +493,8 @@ TEST_F( ProcessingNodesModuleTest, SinglePostProcessing ) std::chrono::milliseconds( 30000 ), "node_proc2 not synched" ); - auto mintResMain = node_main->MintTokens( 1000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + auto mintResMain = + node_main->MintTokens( 1000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mintResMain.has_value() ) << "Mint failed on node_main"; std::string bin_path = boost::dll::program_location().parent_path().string() + "/"; diff --git a/test/src/processing_nodes/full_node_test.cpp b/test/src/processing_nodes/full_node_test.cpp index b52499cc0..bd6588b25 100644 --- a/test/src/processing_nodes/full_node_test.cpp +++ b/test/src/processing_nodes/full_node_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "account/GeniusNode.hpp" #include "account/TokenID.hpp" @@ -12,6 +13,18 @@ using namespace sgns; +namespace +{ + std::string NextMintSourceHash() + { + static std::atomic mint_counter{ 1 }; + const auto value = mint_counter.fetch_add( 1 ); + char buf[65] = {}; + std::snprintf( buf, sizeof( buf ), "%064llx", static_cast( value ) ); + return std::string( buf ); + } +} // namespace + /** * @brief Helper to create a GeniusNode with explicit full-node flag, custom folder, and fixed private key. * @param self_address Address for this node @@ -101,7 +114,7 @@ TEST( NodeBalancePersistenceTest, BalancePersistsAfterRecreation ) constexpr size_t mintAmount = 10; for ( size_t i = 0; i < mintAmount; ++i ) { - auto mintRes = originalNode->MintTokens( 500000, "", "", TokenID::FromBytes( { 0x00 } ) ); + auto mintRes = originalNode->MintTokens( 500000, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mintRes.has_value() ) << "MintTokens failed on original node"; afterMint = originalNode->GetBalance(); ASSERT_GT( afterMint, beforeMint ); diff --git a/test/src/processing_nodes/processing_nodes_test.cpp b/test/src/processing_nodes/processing_nodes_test.cpp index 9405ca4f9..c72fc3731 100644 --- a/test/src/processing_nodes/processing_nodes_test.cpp +++ b/test/src/processing_nodes/processing_nodes_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,17 @@ using namespace sgns::test; +namespace +{ + std::string NextMintSourceHash() + { + static uint64_t mint_counter = 1; + char buf[65] = {}; + std::snprintf( buf, sizeof( buf ), "%064llx", static_cast( mint_counter++ ) ); + return std::string( buf ); + } +} // namespace + class ProcessingNodesTest : public ::testing::Test { protected: @@ -166,12 +178,12 @@ TEST_F( ProcessingNodesTest, DISABLED_ProcessNodesTransactionsCount ) [&]() { return node_proc2->GetTransactionManagerState() == TransactionManager::State::READY; }, std::chrono::milliseconds( 20000 ), "Node proc 2 not synched" ); - node_main->MintTokens( 50000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); - node_main->MintTokens( 50000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + node_main->MintTokens( 50000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); + node_main->MintTokens( 50000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); 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; @@ -472,7 +484,8 @@ 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, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + auto mint_result = + node_main->MintTokens( 50000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; diff --git a/test/src/transaction_sync/transaction_crash_test.cpp b/test/src/transaction_sync/transaction_crash_test.cpp index 34aa7a80e..dba51ce91 100644 --- a/test/src/transaction_sync/transaction_crash_test.cpp +++ b/test/src/transaction_sync/transaction_crash_test.cpp @@ -7,11 +7,22 @@ #endif #include #include +#include #include #include "account/GeniusNode.hpp" namespace sgns { + namespace + { + std::string NextMintSourceHash() + { + static uint64_t mint_counter = 1; + char buf[65] = {}; + std::snprintf( buf, sizeof( buf ), "%064llx", static_cast( mint_counter++ ) ); + return std::string( buf ); + } + } // namespace /** * @file transaction_crash_sync_test_updated.cpp @@ -115,7 +126,7 @@ namespace sgns } std::cout << "Minting the required tokens" << std::endl; - auto mint_result = node1->MintTokens( total_amount, "", "", TokenID::FromBytes( { 0x00 } ) ); + auto mint_result = node1->MintTokens( total_amount, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; auto [mint_tx_id, mint_duration] = mint_result.value(); std::cout << "Mint transaction " << mint_tx_id << " completed in " << mint_duration << " ms" << std::endl; diff --git a/test/src/transaction_sync/transaction_sync_test.cpp b/test/src/transaction_sync/transaction_sync_test.cpp index 01a2efd8a..8fc5dbe29 100644 --- a/test/src/transaction_sync/transaction_sync_test.cpp +++ b/test/src/transaction_sync/transaction_sync_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #ifdef _WIN32 //#include @@ -21,6 +22,17 @@ #include "proof/TransferProof.hpp" #include "testutil/wait_condition.hpp" +namespace +{ + std::string NextMintSourceHash() + { + static uint64_t mint_counter = 1; + char buf[65] = {}; + std::snprintf( buf, sizeof( buf ), "%064llx", static_cast( mint_counter++ ) ); + return std::string( buf ); + } +} // namespace + namespace sgns { class TransactionSyncTest : public ::testing::Test @@ -119,7 +131,8 @@ namespace sgns std::shared_ptr account, UTXOManager &utxo_manager, uint64_t amount, - const std::string &destination ) + const std::string &destination, + const std::string &previous_hash = "" ) { OUTCOME_TRY( auto &¶ms, utxo_manager.CreateTxParameter( amount, destination, sgns::TokenID::FromBytes( { 0x00 } ) ) ); @@ -127,15 +140,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( static_cast( utxo_manager.GetBalance() ), static_cast( amount ) ); @@ -143,7 +158,7 @@ namespace sgns maybe_proof = std::move( proof_result ); - utxo_manager.ReserveUTXOs( params.first ); + utxo_manager.ReserveUTXOs( params.first, transfer_transaction->GetHash() ); return std::make_pair( transfer_transaction, maybe_proof ); } @@ -170,7 +185,8 @@ TEST_F( TransactionSyncTest, TransactionSimpleTransfer ) auto balance_2_before = node_proc2->GetBalance(); // Mint tokens with timeout - auto mint_result = node_proc1->MintTokens( 10000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + auto mint_result = + node_proc1->MintTokens( 10000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; auto [mint_tx_id, mint_duration] = mint_result.value(); @@ -243,7 +259,7 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) for ( auto amount : mint_amounts ) { - auto mint_result = node_proc1->MintTokens( amount, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + auto mint_result = node_proc1->MintTokens( amount, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction of " << amount << " failed or timed out"; auto [tx_id, duration] = mint_result.value(); @@ -251,10 +267,12 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) } // Mint tokens on node_proc2 - auto mint_result1 = node_proc2->MintTokens( 10000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + auto mint_result1 = + node_proc2->MintTokens( 10000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result1.has_value() ) << "Mint transaction failed or timed out"; - auto mint_result2 = node_proc2->MintTokens( 20000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); + auto mint_result2 = + node_proc2->MintTokens( 20000000000, NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result2.has_value() ) << "Mint transaction failed or timed out"; // Verify balances after minting @@ -423,9 +441,10 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) "full_node not synched" ); // Mint tokens with timeout - auto mint_result = node_proc1->MintTokens( 10000000000, "", "", TokenID::FromBytes( { 0x00 } ) ); + auto mint_result = + node_proc1->MintTokens( 10000000000, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; - mint_result = node_proc1->MintTokens( 10000000000, "", "", TokenID::FromBytes( { 0x00 } ) ); + mint_result = node_proc1->MintTokens( 10000000000, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; auto [mint_tx_id, mint_duration] = mint_result.value(); @@ -439,7 +458,8 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) auto tx_pair = CreateTransfer( GetAccountFromNode( *node_proc1 ), *GetUTXOManagerFromNode( *node_proc1 ), 10000000000, - node_proc2->GetAddress() ); + node_proc2->GetAddress(), + mint_tx_id ); if ( !tx_pair.has_value() ) { } @@ -457,46 +477,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; }, @@ -505,12 +493,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(); @@ -529,3 +515,69 @@ 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, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + 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 ), + *GetUTXOManagerFromNode( *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 ); +}