From a1c121880cfce2e813a56075f0598f0de1204726 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 15 Jan 2026 14:50:31 -0300 Subject: [PATCH 001/114] WIP: Creating the base of consensus --- src/blockchain/Consensus.cpp | 396 ++++++++++++++++++++++ src/blockchain/Consensus.hpp | 94 +++++ src/blockchain/impl/CMakeLists.txt | 3 + src/blockchain/impl/proto/Consensus.proto | 76 +++++ 4 files changed, 569 insertions(+) create mode 100644 src/blockchain/Consensus.cpp create mode 100644 src/blockchain/Consensus.hpp create mode 100644 src/blockchain/impl/proto/Consensus.proto diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp new file mode 100644 index 000000000..e9786cf76 --- /dev/null +++ b/src/blockchain/Consensus.cpp @@ -0,0 +1,396 @@ +/** + * @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 "base/hexutil.hpp" +#include "crypto/hasher/hasher_impl.hpp" + +namespace sgns::blockchain +{ + ConsensusManager::ConsensusManager( std::shared_ptr registry ) : + registry_( std::move( registry ) ) + { + } + + void ConsensusManager::SetRegistry( std::shared_ptr registry ) + { + registry_ = std::move( registry ); + } + + outcome::result ConsensusManager::CreateProposal( + const Subject &subject, + const std::string &proposer_id, + const std::string ®istry_cid, + uint64_t registry_epoch, + Signer sign ) + { + if ( !sign ) + { + return outcome::failure( std::errc::invalid_argument ); + } + + if ( !ValidateSubject( 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() ) + { + 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() ) + { + return outcome::failure( signing_bytes.error() ); + } + + auto signature = sign( signing_bytes.value() ); + proposal.set_signature( signature.data(), signature.size() ); + + return proposal; + } + + outcome::result ConsensusManager::CreateVote( + const std::string &proposal_id, + const std::string &voter_id, + bool approve, + Signer sign ) + { + if ( !sign ) + { + 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() ) + { + return outcome::failure( signing_bytes.error() ); + } + + auto signature = sign( signing_bytes.value() ); + vote.set_signature( signature.data(), signature.size() ); + + return vote; + } + + outcome::result ConsensusManager::CreateVoteBundle( + const std::string &proposal_id, + const std::string &aggregator_id, + const std::vector &votes, + Signer sign ) + { + if ( !sign ) + { + 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() ) + { + return outcome::failure( signing_bytes.error() ); + } + + auto signature = sign( signing_bytes.value() ); + bundle.set_signature( signature.data(), signature.size() ); + + return bundle; + } + + outcome::result ConsensusManager::CreateCertificate( + const Proposal &proposal, + const std::vector &votes, + Verifier verify ) + { + auto tally_result = TallyVotes( proposal, votes, std::move( verify ) ); + if ( tally_result.has_error() ) + { + 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 ); + cert.set_timestamp( std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count() ); + for ( const auto &vote : votes ) + { + *cert.add_votes() = vote; + } + + return cert; + } + + outcome::result ConsensusManager::TallyVotes( + const Proposal &proposal, + const std::vector &votes, + Verifier verify ) + { + if ( !registry_ ) + { + return outcome::failure( std::errc::not_supported ); + } + + auto registry_result = registry_->LoadRegistry(); + if ( registry_result.has_error() ) + { + return outcome::failure( registry_result.error() ); + } + + const auto ®istry = registry_result.value(); + if ( proposal.registry_epoch() != registry.epoch() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + + uint64_t total_weight = registry_->TotalWeight( registry ); + uint64_t approved_weight = 0; + std::set seen; + + for ( const auto &vote : votes ) + { + if ( vote.proposal_id() != proposal.proposal_id() ) + { + continue; + } + if ( !seen.insert( vote.voter_id() ).second ) + { + continue; + } + + const auto *validator = FindValidator( registry, vote.voter_id() ); + if ( !validator || validator->status() != ValidatorRegistry::Status::ACTIVE ) + { + continue; + } + + if ( verify ) + { + auto signing_bytes = VoteSigningBytes( vote ); + if ( signing_bytes.has_error() ) + { + continue; + } + if ( !verify( vote.voter_id(), vote.signature(), signing_bytes.value() ) ) + { + continue; + } + } + + if ( vote.approve() ) + { + 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 ); + return tally; + } + + outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) + { + Proposal 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() ); + } + + outcome::result> ConsensusManager::VoteSigningBytes( const Vote &vote ) + { + Vote 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() ); + } + + outcome::result> ConsensusManager::VoteBundleSigningBytes( const VoteBundle &bundle ) + { + VoteBundle 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() ); + } + + outcome::result ConsensusManager::ComputeSubjectId( const Subject &subject ) + { + Subject copy = subject; + copy.clear_subject_id(); + std::string serialized; + if ( !copy.SerializeToString( &serialized ) ) + { + return outcome::failure( std::errc::invalid_argument ); + } + + sgns::crypto::HasherImpl hasher; + auto hash = hasher.sha2_256( + gsl::span( reinterpret_cast( serialized.data() ), + serialized.size() ) ); + return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); + } + + outcome::result ConsensusManager::CreateNonceSubject( + const std::string &account_id, + uint64_t nonce, + const std::vector &tx_hash ) + { + 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() ); + + auto subject_id = ComputeSubjectId( subject ); + if ( subject_id.has_error() ) + { + return outcome::failure( subject_id.error() ); + } + subject.set_subject_id( subject_id.value() ); + return subject; + } + + outcome::result ConsensusManager::CreateTaskResultSubject( + const std::string &account_id, + const std::string &escrow_path, + const std::vector &task_result_hash, + uint64_t 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() ) + { + return outcome::failure( subject_id.error() ); + } + subject.set_subject_id( subject_id.value() ); + return subject; + } + + std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) const + { + auto signing_bytes = ProposalSigningBytes( proposal ); + if ( signing_bytes.has_error() ) + { + return {}; + } + + 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() ) ); + } + + bool ConsensusManager::ValidateSubject( const Subject &subject ) const + { + if ( subject.account_id().empty() ) + { + return false; + } + + switch ( subject.type() ) + { + case SubjectType::SUBJECT_NONCE: + return subject.has_nonce() && !subject.nonce().tx_hash().empty(); + case SubjectType::SUBJECT_TASK_RESULT: + return subject.has_task_result() && !subject.task_result().task_result_hash().empty(); + case SubjectType::SUBJECT_UNSPECIFIED: + default: + break; + } + + if ( subject.has_nonce() ) + { + return !subject.nonce().tx_hash().empty(); + } + if ( subject.has_task_result() ) + { + return !subject.task_result().task_result_hash().empty(); + } + + return false; + } + + const ValidatorRegistry::ValidatorEntry *ConsensusManager::FindValidator( + const ValidatorRegistry::Registry ®istry, + const std::string &validator_id ) const + { + for ( const auto &validator : registry.validators() ) + { + if ( validator.validator_id() == validator_id ) + { + return &validator; + } + } + return nullptr; + } +} diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp new file mode 100644 index 000000000..f0d110afd --- /dev/null +++ b/src/blockchain/Consensus.hpp @@ -0,0 +1,94 @@ +/** + * @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 "blockchain/ValidatorRegistry.hpp" +#include "blockchain/impl/proto/Consensus.pb.h" +#include "outcome/outcome.hpp" + +namespace sgns::blockchain +{ + class ConsensusManager + { + public: + using Proposal = ConsensusProposal; + using Vote = ConsensusVote; + using VoteBundle = ConsensusVoteBundle; + using Certificate = ConsensusCertificate; + using Subject = ConsensusSubject; + + using Signer = + std::function( std::vector payload )>; + using Verifier = + std::function &payload )>; + + struct QuorumTally + { + uint64_t total_weight = 0; + uint64_t approved_weight = 0; + bool has_quorum = false; + }; + + explicit ConsensusManager( std::shared_ptr registry ); + + void SetRegistry( std::shared_ptr registry ); + + 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, + Verifier verify ); + + outcome::result TallyVotes( const Proposal &proposal, + const std::vector &votes, + Verifier verify ); + + 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::vector &tx_hash ); + static outcome::result CreateTaskResultSubject( const std::string &account_id, + const std::string &escrow_path, + const std::vector &task_result_hash, + uint64_t result_epoch ); + + private: + std::string CreateProposalId( const Proposal &proposal ) const; + bool ValidateSubject( const Subject &subject ) const; + const ValidatorRegistry::ValidatorEntry *FindValidator( + const ValidatorRegistry::Registry ®istry, + const std::string &validator_id ) const; + + std::shared_ptr registry_; + }; +} diff --git a/src/blockchain/impl/CMakeLists.txt b/src/blockchain/impl/CMakeLists.txt index 790340cfb..f820a2789 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 @@ -39,6 +41,7 @@ target_link_libraries(blockchain_genesis sgns_genius_account 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..f654de2f8 --- /dev/null +++ b/src/blockchain/impl/proto/Consensus.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +package sgns.blockchain; + +message ConsensusSubject { + string subject_id = 1; + SubjectType type = 2; + string account_id = 3; + + oneof payload { + NonceSubject nonce = 10; + TaskResultSubject task_result = 11; + } +} + +enum SubjectType { + SUBJECT_UNSPECIFIED = 0; + SUBJECT_NONCE = 1; + SUBJECT_TASK_RESULT = 2; +} + +message NonceSubject { + uint64 nonce = 1; + bytes tx_hash = 2; +} + +message TaskResultSubject { + string escrow_path = 1; + bytes task_result_hash = 2; + uint64 result_epoch = 3; +} + +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; +} + +message ConsensusMessage { + oneof payload { + ConsensusProposal proposal = 1; + ConsensusVote vote = 2; + ConsensusVoteBundle vote_bundle = 3; + ConsensusCertificate certificate = 4; + } +} From 665a83d15dd1ab540238c5fecfff4e7c789aa03d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 29 Jan 2026 16:12:02 -0300 Subject: [PATCH 002/114] Feat: Including proposal in certificate to prevent tampering --- src/blockchain/impl/proto/Consensus.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blockchain/impl/proto/Consensus.proto b/src/blockchain/impl/proto/Consensus.proto index f654de2f8..93baae190 100644 --- a/src/blockchain/impl/proto/Consensus.proto +++ b/src/blockchain/impl/proto/Consensus.proto @@ -64,6 +64,7 @@ message ConsensusCertificate { uint64 approved_weight = 5; uint64 timestamp = 6; repeated ConsensusVote votes = 7; + ConsensusProposal proposal = 8; } message ConsensusMessage { From 0c590f2e5e95eeebcb4a60dde0141748da32d45d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 29 Jan 2026 16:25:45 -0300 Subject: [PATCH 003/114] Feat: Adding consensus unit test --- test/src/CMakeLists.txt | 1 + test/src/blockchain/CMakeLists.txt | 8 + .../blockchain/consensus_certificate_test.cpp | 164 ++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 test/src/blockchain/consensus_certificate_test.cpp 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/blockchain/CMakeLists.txt b/test/src/blockchain/CMakeLists.txt index 2be885a68..a26072612 100644 --- a/test/src/blockchain/CMakeLists.txt +++ b/test/src/blockchain/CMakeLists.txt @@ -38,6 +38,14 @@ addtest(blockchain_genesis_test blockchain_genesis_test.cpp ) +addtest(consensus_certificate_test + consensus_certificate_test.cpp +) +target_link_libraries(consensus_certificate_test + blockchain_genesis + base_crdt_test +) + target_include_directories(blockchain_genesis_test PRIVATE ${AsyncIOManager_INCLUDE_DIR}) target_link_libraries(blockchain_genesis_test diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp new file mode 100644 index 000000000..05e91c05e --- /dev/null +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -0,0 +1,164 @@ +#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"; + + 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 MakeRegistry( + const std::shared_ptr &db, + const std::shared_ptr &account ) + { + using sgns::blockchain::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; + } +} + +namespace sgns::test +{ + class ConsensusCertificateTest : public 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 = std::make_shared( registry ); + manager->SetVerifier( + []( const std::string &signer, const std::string &signature, const std::vector &payload ) + { return GeniusAccount::VerifySignature( signer, signature, payload ); } ); + + std::vector tx_hash{ 0x01, 0x02, 0x03 }; + auto subject_result = + blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); + 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() }, manager->verifier_ ); + 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 = std::make_shared( registry ); + manager->SetVerifier( + []( const std::string &signer, const std::string &signature, const std::vector &payload ) + { return GeniusAccount::VerifySignature( signer, signature, payload ); } ); + + std::vector tx_hash{ 0x0a, 0x0b, 0x0c }; + auto subject_result = + blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); + 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() }, manager->verifier_ ); + ASSERT_TRUE( cert_result.has_value() ); + + auto cert = cert_result.value(); + + bool notified = false; + manager->SetCertificateCallback( + [¬ified]( const blockchain::ConsensusProposal &, const blockchain::ConsensusCertificate & ) + { notified = true; } ); + + manager->HandleCertificate( cert ); + EXPECT_TRUE( notified ); + + notified = false; + auto *bad_subject = cert.mutable_proposal()->mutable_subject()->mutable_nonce(); + bad_subject->set_nonce( bad_subject->nonce() + 1 ); + + manager->HandleCertificate( cert ); + EXPECT_FALSE( notified ); + } +} // namespace sgns::test From f606b1a2a878a41fb8885551898aa606da8f9c44 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 30 Jan 2026 08:30:04 -0300 Subject: [PATCH 004/114] Feat: Adding method to get the validator CID and epoch --- src/blockchain/ValidatorRegistry.cpp | 16 ++++++++++++++++ src/blockchain/ValidatorRegistry.hpp | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 24617b824..ed7d332fc 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -456,6 +456,22 @@ namespace sgns::blockchain return outcome::failure( std::errc::no_such_file_or_directory ); } + 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_ ) + { + return cached_registry_->epoch(); + } + return 0; + } + outcome::result> ValidatorRegistry::GetValidatorWeight( const std::string &validator_id ) const { diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index e85009305..50d6067bc 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -79,6 +79,8 @@ namespace sgns::blockchain 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; static constexpr std::string_view RegistryKey() { From 74562fa4a501c1f0c7a59d30dccf27222c404a54 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 30 Jan 2026 08:31:26 -0300 Subject: [PATCH 005/114] Feat: Adding method to grab the validator registry from blockchain --- src/blockchain/Blockchain.hpp | 1 + src/blockchain/impl/Blockchain.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index a4f399ef6..633de4a8c 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -103,6 +103,7 @@ namespace sgns outcome::result GetGenesisCID() const; outcome::result GetAccountCreationCID() const; + std::shared_ptr GetValidatorRegistry() const; void SetFullNodeMode(); diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index e3ceee06d..46ff7a81a 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1492,6 +1492,11 @@ namespace sgns return it->second; } + std::shared_ptr Blockchain::GetValidatorRegistry() const + { + return validator_registry_; + } + void Blockchain::SetFullNodeMode() { db_->AddListenTopic( From 5f55ac778eda33bb97337fd5276a9fc2acbb167b Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 30 Jan 2026 12:06:27 -0300 Subject: [PATCH 006/114] WIP: Organizing consensus on blockchain --- src/account/IGeniusTransactions.hpp | 2 +- src/blockchain/Blockchain.hpp | 7 +++++-- src/blockchain/impl/Blockchain.cpp | 11 +++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/account/IGeniusTransactions.hpp b/src/account/IGeniusTransactions.hpp index 890f96bb9..4820b45ea 100644 --- a/src/account/IGeniusTransactions.hpp +++ b/src/account/IGeniusTransactions.hpp @@ -79,7 +79,7 @@ namespace sgns return dag_st.source_addr(); } - std::string GetHash() const; + [[nodiscard]] std::string GetHash() const; uint64_t GetTimestamp() const { diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index 633de4a8c..febeb6531 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -20,6 +20,7 @@ #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" @@ -101,8 +102,8 @@ 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(); @@ -210,6 +211,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/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 46ff7a81a..eaf57b812 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -132,6 +132,17 @@ namespace sgns return nullptr; } + instance->consensus_manager_ = blockchain::ConsensusManager::New( + instance->validator_registry_, + [weak_ptr( std::weak_ptr( instance ) )]( std::vector payload ) + { + if ( auto strong = weak_ptr.lock() ) + { + return strong->account_->Sign( std::move( payload ) ); + } + return outcome::failure( std::errc::owner_dead ); + } ); + auto ensure_registry_result = instance->EnsureValidatorRegistry(); if ( ensure_registry_result.has_error() ) { From f17b81d7b0d80380bcdfb77a5c330c89462a8793 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 30 Jan 2026 15:25:46 -0300 Subject: [PATCH 007/114] WIP: Creating proposal on transaction manager --- src/account/TransactionManager.cpp | 72 +++++++++++++++++++++++++++--- src/account/TransactionManager.hpp | 22 ++++++--- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 7bd57f217..d8f2ebf8e 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -9,8 +9,6 @@ #include #include #include -#include -#include #include #include @@ -35,6 +33,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 ) @@ -44,10 +43,22 @@ namespace sgns utxo_manager, std::move( account ), std::move( hasher ), + std::move( blockchain ), full_node, timestamp_tolerance, mutability_window ) ); + instance->blockchain_->SetCertificateCallback( + [weak_ptr( std::weak_ptr( instance ) )]( + const blockchain::ConsensusProposal &proposal, + const blockchain::ConsensusCertificate &certificate ) + { + if ( auto strong = weak_ptr.lock() ) + { + strong->OnConsensusCertificate( proposal, certificate ); + } + } ); + auto monitored_networks = GetMonitoredNetworkIDs(); for ( auto network_id : monitored_networks ) { @@ -135,6 +146,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 ) : @@ -143,6 +155,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() ), @@ -757,11 +770,33 @@ namespace sgns 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 ) ) ); + 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() ); + m_logger->debug( "[{} - full: {}] Creating Consensus Proposal for tx {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_path ); + OUTCOME_TRY( auto &&proposal, + blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), + transaction->dag_st.nonce(), + transaction->GetHash() ) ); + pending_proposals_[transaction->GetHash()] = proposal.proposal_id(); + } + else + { + 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() ); + + return outcome::failure( + boost::system::errc::make_error_code( boost::system::errc::invalid_argument ) ); } - nonces_set.insert( transaction->dag_st.nonce() ); expected_next_nonce++; } @@ -2712,6 +2747,25 @@ namespace sgns key ); OUTCOME_TRY( ParseTransaction( new_tx ) ); + auto proposal_it = pending_proposals_.find( tx_hash ); + if ( proposal_it != pending_proposals_.end() ) + { + m_logger->debug( "[{} - full: {}] Found pending proposal for transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); + auto submit_result = blockchain_->SendConsensusProposal( *proposal_it ); + if ( submit_result.has_error() ) + { + m_logger->error( "[{} - full: {}] Failed to submit Proposal for tx {}: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_hash, + submit_result.error().message() ); + return outcome::failure( submit_result.error() ); + } + } + const auto nonce = new_tx->dag_st.nonce(); { @@ -2931,8 +2985,14 @@ namespace sgns return outcome::failure( std::errc::no_such_file_or_directory ); } + + void TransactionManager::OnConsensusCertificate( const blockchain::ConsensusProposal &proposal, + const blockchain::ConsensusCertificate &certificate ) + { + } } + fmt::format_context::iterator fmt::formatter::format( sgns::TransactionManager::State s, format_context &ctx ) const diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index ce949db79..4a365e8ae 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -28,6 +28,8 @@ #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 +98,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 ) ); @@ -201,6 +204,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 ); @@ -246,6 +250,9 @@ namespace sgns bool SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ); + void OnConsensusCertificate( const blockchain::ConsensusProposal &proposal, + const blockchain::ConsensusCertificate &certificate ); + std::shared_ptr globaldb_m; std::shared_ptr ctx_m; @@ -281,13 +288,14 @@ namespace sgns 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; + mutable std::shared_mutex tx_mutex_m; + std::unordered_map tx_processed_m; + std::atomic verifying_count_{ 0 }; // Count of VERIFYING transactions + 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; static constexpr std::chrono::milliseconds TIMESTAMP_TOLERANCE = std::chrono::seconds( 10 ); static constexpr std::chrono::milliseconds MUTABILITY_WINDOW = std::chrono::minutes( 15 ); From f6fb2db7364b3bb0022eb2aa9d36847aae50ae08 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 30 Jan 2026 15:31:24 -0300 Subject: [PATCH 008/114] Fix: Removing old tests --- test/src/blockchain/CMakeLists.txt | 35 ------------------- .../blockchain/consensus_certificate_test.cpp | 6 ---- 2 files changed, 41 deletions(-) diff --git a/test/src/blockchain/CMakeLists.txt b/test/src/blockchain/CMakeLists.txt index a26072612..a190efb12 100644 --- a/test/src/blockchain/CMakeLists.txt +++ b/test/src/blockchain/CMakeLists.txt @@ -1,38 +1,3 @@ -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(block_storage_test - block_storage_test.cpp -) -target_link_libraries(block_storage_test - block_storage - 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 diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index 05e91c05e..8c84f2b4c 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -77,9 +77,6 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = std::make_shared( registry ); - manager->SetVerifier( - []( const std::string &signer, const std::string &signature, const std::vector &payload ) - { return GeniusAccount::VerifySignature( signer, signature, payload ); } ); std::vector tx_hash{ 0x01, 0x02, 0x03 }; auto subject_result = @@ -116,9 +113,6 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = std::make_shared( registry ); - manager->SetVerifier( - []( const std::string &signer, const std::string &signature, const std::vector &payload ) - { return GeniusAccount::VerifySignature( signer, signature, payload ); } ); std::vector tx_hash{ 0x0a, 0x0b, 0x0c }; auto subject_result = From 2474af43a42f5114f17b0d39c5510ccb355ed7d7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 4 Feb 2026 11:07:23 -0300 Subject: [PATCH 009/114] Chore: Removing unused example --- example/CMakeLists.txt | 1 - example/account_handling/AccountHandling.cpp | 159 ---------------- example/account_handling/AccountHelper.cpp | 180 ------------------- example/account_handling/AccountHelper.hpp | 87 --------- example/account_handling/CMakeLists.txt | 40 ----- 5 files changed, 467 deletions(-) delete mode 100644 example/account_handling/AccountHandling.cpp delete mode 100644 example/account_handling/AccountHelper.cpp delete mode 100644 example/account_handling/AccountHelper.hpp delete mode 100644 example/account_handling/CMakeLists.txt 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 908eb9861..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 195f161a2..000000000 --- a/example/account_handling/CMakeLists.txt +++ /dev/null @@ -1,40 +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 -# ipfs-lite-cpp::ipfs_datastore_rocksdb -# ipfs-lite-cpp::buffer -# ipfs-lite-cpp::ipld_node -# ipfs-lite-cpp::ipfs_merkledag_service -# ipfs-lite-cpp::graphsync - 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 - ipfs-pubsub - rapidjson - ${WIN_CRYPT_LIBRARY} -) From 4f7c62046426d8d4c1b2826d9fb9b5bc6ff3dd8d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 4 Feb 2026 11:24:18 -0300 Subject: [PATCH 010/114] Feat: Adding pubsub communication on consensus --- src/account/GeniusNode.cpp | 2 + src/account/Migration3_4_0To3_5_0.cpp | 1 + src/account/TransactionManager.cpp | 4 +- src/account/TransactionManager.hpp | 17 +- src/blockchain/Blockchain.hpp | 16 +- src/blockchain/Consensus.cpp | 645 ++++++++++++++++-- src/blockchain/Consensus.hpp | 145 ++-- src/blockchain/impl/Blockchain.cpp | 37 +- src/blockchain/impl/CMakeLists.txt | 1 + test/src/blockchain/CMakeLists.txt | 2 + .../blockchain/blockchain_genesis_test.cpp | 2 +- .../blockchain/consensus_certificate_test.cpp | 77 +-- 12 files changed, 776 insertions(+), 173 deletions(-) diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index eedc91ef0..a7f7b8e4e 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -355,6 +355,7 @@ namespace sgns blockchain_ = Blockchain::New( tx_globaldb_, account_, + pubsub_, [weak_self]( outcome::result result ) { if ( auto strong = weak_self.lock() ) @@ -416,6 +417,7 @@ namespace sgns utxo_manager_, account_, std::make_shared(), + blockchain_, is_full_node_ ); transaction_manager_->RegisterStateChangeCallback( diff --git a/src/account/Migration3_4_0To3_5_0.cpp b/src/account/Migration3_4_0To3_5_0.cpp index 8f3835766..eebb2ba41 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() ) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index d8f2ebf8e..92cf0f955 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -784,7 +784,7 @@ namespace sgns blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), transaction->dag_st.nonce(), transaction->GetHash() ) ); - pending_proposals_[transaction->GetHash()] = proposal.proposal_id(); + pending_proposals_[transaction->GetHash()] = proposal; } else { @@ -2754,7 +2754,7 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m, key ); - auto submit_result = blockchain_->SendConsensusProposal( *proposal_it ); + auto submit_result = blockchain_->SubmitProposal( proposal_it->second ); if ( submit_result.has_error() ) { m_logger->error( "[{} - full: {}] Failed to submit Proposal for tx {}: {}", diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 4a365e8ae..5236f8d3e 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -259,6 +259,7 @@ namespace sgns 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(); @@ -288,14 +289,14 @@ namespace sgns 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::unordered_map pending_proposals_; - std::function task_m; - std::atomic stopped_{ false }; - std::chrono::milliseconds timestamp_tolerance_m; - std::chrono::milliseconds mutability_window_m; + mutable std::shared_mutex tx_mutex_m; + std::unordered_map tx_processed_m; + std::atomic verifying_count_{ 0 }; // Count of VERIFYING transactions + 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; static constexpr std::chrono::milliseconds TIMESTAMP_TOLERANCE = std::chrono::seconds( 10 ); static constexpr std::chrono::milliseconds MUTABILITY_WINDOW = std::chrono::minutes( 15 ); diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index febeb6531..b47d4576d 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -66,9 +66,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(); @@ -108,6 +109,15 @@ namespace sgns void SetFullNodeMode(); + void SetCertificateCallback( blockchain::ConsensusManager::CertificateCallback callback ); + + outcome::result CreateConsensusProposal( + const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash ); + + outcome::result SubmitProposal( const blockchain::ConsensusManager::Proposal &proposal ); + protected: friend class Migration3_5_1To3_6_0; diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index e9786cf76..943ff4521 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -6,6 +6,7 @@ */ #include "blockchain/Consensus.hpp" +#include #include #include #include @@ -13,26 +14,120 @@ #include #include "base/hexutil.hpp" +#include "base/sgns_version.hpp" #include "crypto/hasher/hasher_impl.hpp" +#include "account/GeniusAccount.hpp" namespace sgns::blockchain { - ConsensusManager::ConsensusManager( std::shared_ptr registry ) : - registry_( std::move( registry ) ) + + 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 pubsub, + Signer signer, + std::string consensus_topic ) + { + if ( !pubsub ) + { + return nullptr; + } + + auto instance = std::shared_ptr( new ConsensusManager( std::move( registry ), + std::move( pubsub ), + std::move( signer ), + std::move( consensus_topic ) ) ); + + instance->consensus_subs_future_ = std::move( instance->pubsub_->Subscribe( + instance->consensus_topic_, + [weakptr( std::weak_ptr( instance ) )]( + boost::optional message ) + { + if ( auto self = weakptr.lock() ) + { + ConsensusManagerLogger()->trace( "Received Consensus Message on topic {}", self->consensus_topic_ ); + self->OnConsensusMessage( message ); + } + } ) ); + ConsensusManagerLogger()->debug( "Subscribed to Consensus topic {}", instance->consensus_topic_ ); + + return instance; + } + + ConsensusManager::ConsensusManager( std::shared_ptr registry, + std::shared_ptr pubsub, + Signer signer, + std::string consensus_topic ) : + registry_( std::move( registry ) ), // + pubsub_( std::move( pubsub ) ), // + signer_( std::move( signer ) ), // + consensus_topic_( std::string( CONSENSUS_CHANNEL_PREFIX ) + sgns::version::GetNetAndVersionAppendix() + + consensus_topic ) + { + } + + void ConsensusManager::SetProposalValidator( ProposalValidator validator ) + { + proposal_validator_ = std::move( validator ); + } + + void ConsensusManager::SetCertificateCallback( CertificateCallback callback ) + { + certificate_callback_ = std::move( callback ); + } + + outcome::result ConsensusManager::Publish( const ConsensusMessage &message ) + { + std::vector serialized_proto( message.ByteSizeLong() ); + if ( !message.SerializeToArray( serialized_proto.data(), serialized_proto.size() ) ) + { + return outcome::failure( std::errc::invalid_argument ); + } + + ConsensusManagerLogger()->debug( "Sending consensus packet to {}", consensus_topic_ ); + pubsub_->Publish( consensus_topic_, serialized_proto ); + + return outcome::success(); + } + + void ConsensusManager::SetProposalHandler( ProposalHandler handler ) { + proposal_handler_ = std::move( handler ); } - void ConsensusManager::SetRegistry( std::shared_ptr registry ) + void ConsensusManager::SetVoteHandler( VoteHandler handler ) { - registry_ = std::move( registry ); + vote_handler_ = std::move( handler ); } - outcome::result ConsensusManager::CreateProposal( - const Subject &subject, - const std::string &proposer_id, - const std::string ®istry_cid, - uint64_t registry_epoch, - Signer sign ) + void ConsensusManager::SetVoteBundleHandler( VoteBundleHandler handler ) + { + vote_bundle_handler_ = std::move( handler ); + } + + void ConsensusManager::SetCertificateHandler( CertificateHandler handler ) + { + certificate_handler_ = std::move( handler ); + } + + 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 ) { if ( !sign ) { @@ -49,9 +144,9 @@ namespace sgns::blockchain 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() ); + proposal.set_timestamp( + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count() ); if ( proposal.subject().subject_id().empty() ) { @@ -69,18 +164,16 @@ namespace sgns::blockchain { return outcome::failure( signing_bytes.error() ); } - - auto signature = sign( signing_bytes.value() ); + OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); proposal.set_signature( signature.data(), signature.size() ); return proposal; } - outcome::result ConsensusManager::CreateVote( - const std::string &proposal_id, - const std::string &voter_id, - bool approve, - Signer sign ) + outcome::result ConsensusManager::CreateVote( const std::string &proposal_id, + const std::string &voter_id, + bool approve, + Signer sign ) { if ( !sign ) { @@ -91,9 +184,9 @@ namespace sgns::blockchain 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() ); + 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() ) @@ -101,17 +194,16 @@ namespace sgns::blockchain return outcome::failure( signing_bytes.error() ); } - auto signature = sign( signing_bytes.value() ); + OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); vote.set_signature( signature.data(), signature.size() ); return vote; } - outcome::result ConsensusManager::CreateVoteBundle( - const std::string &proposal_id, - const std::string &aggregator_id, - const std::vector &votes, - Signer sign ) + outcome::result ConsensusManager::CreateVoteBundle( const std::string &proposal_id, + const std::string &aggregator_id, + const std::vector &votes, + Signer sign ) { if ( !sign ) { @@ -121,9 +213,9 @@ namespace sgns::blockchain 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() ); + 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; @@ -135,18 +227,16 @@ namespace sgns::blockchain return outcome::failure( signing_bytes.error() ); } - auto signature = sign( signing_bytes.value() ); + OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); bundle.set_signature( signature.data(), signature.size() ); return bundle; } - outcome::result ConsensusManager::CreateCertificate( - const Proposal &proposal, - const std::vector &votes, - Verifier verify ) + outcome::result ConsensusManager::CreateCertificate( const Proposal &proposal, + const std::vector &votes ) { - auto tally_result = TallyVotes( proposal, votes, std::move( verify ) ); + auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() ) { return outcome::failure( tally_result.error() ); @@ -159,21 +249,20 @@ namespace sgns::blockchain cert.set_registry_epoch( proposal.registry_epoch() ); cert.set_total_weight( tally.total_weight ); cert.set_approved_weight( tally.approved_weight ); - cert.set_timestamp( std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() ) - .count() ); + cert.set_timestamp( + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count() ); for ( const auto &vote : votes ) { *cert.add_votes() = vote; } + *cert.mutable_proposal() = proposal; return cert; } - outcome::result ConsensusManager::TallyVotes( - const Proposal &proposal, - const std::vector &votes, - Verifier verify ) + outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, + const std::vector &votes ) { if ( !registry_ ) { @@ -186,14 +275,19 @@ namespace sgns::blockchain return outcome::failure( registry_result.error() ); } - const auto ®istry = registry_result.value(); + const auto ®istry = registry_result.value(); + const auto registry_cid = registry_->GetRegistryCid(); + if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) + { + return outcome::failure( std::errc::invalid_argument ); + } if ( proposal.registry_epoch() != registry.epoch() ) { return outcome::failure( std::errc::invalid_argument ); } - uint64_t total_weight = registry_->TotalWeight( registry ); - uint64_t approved_weight = 0; + uint64_t total_weight = registry_->TotalWeight( registry ); + uint64_t approved_weight = 0; std::set seen; for ( const auto &vote : votes ) @@ -213,17 +307,15 @@ namespace sgns::blockchain continue; } - if ( verify ) + auto signing_bytes = VoteSigningBytes( vote ); + if ( signing_bytes.has_error() ) { - auto signing_bytes = VoteSigningBytes( vote ); - if ( signing_bytes.has_error() ) - { - continue; - } - if ( !verify( vote.voter_id(), vote.signature(), signing_bytes.value() ) ) - { - continue; - } + continue; + } + + if ( !GeniusAccount::VerifySignature( vote.voter_id(), vote.signature(), signing_bytes.value() ) ) + { + continue; } if ( vote.approve() ) @@ -233,9 +325,9 @@ namespace sgns::blockchain } QuorumTally tally; - tally.total_weight = total_weight; + tally.total_weight = total_weight; tally.approved_weight = approved_weight; - tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); + tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); return tally; } @@ -275,6 +367,378 @@ namespace sgns::blockchain return std::vector( serialized.begin(), serialized.end() ); } + outcome::result ConsensusManager::SubmitProposal( const Proposal &proposal, bool 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() ) + { + 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() ) + { + return publish_result; + } + + if ( self_vote ) + { + HandleProposal( proposal ); + } + + return outcome::success(); + } + + outcome::result ConsensusManager::SubmitVote( const Vote &vote ) + { + ConsensusMessage message; + *message.mutable_vote() = vote; + return Publish( message ); + } + + outcome::result ConsensusManager::SubmitCertificate( const Certificate &certificate ) + { + ConsensusMessage message; + *message.mutable_certificate() = certificate; + return Publish( message ); + } + + void ConsensusManager::HandleProposal( const Proposal &proposal ) + { + if ( proposal_handler_ ) + { + proposal_handler_( proposal ); + } + + if ( !registry_ ) + { + return; + } + + const auto registry_cid = registry_->GetRegistryCid(); + if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) + { + return; + } + if ( proposal.registry_epoch() != registry_->GetRegistryEpoch() ) + { + return; + } + + auto signing_bytes = ProposalSigningBytes( proposal ); + if ( signing_bytes.has_error() ) + { + return; + } + if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) + { + return; + } + + if ( proposal_validator_ && !proposal_validator_( proposal ) ) + { + return; + } + + const auto slot_key = GetSlotKey( proposal ); + bool should_vote = false; + { + std::lock_guard lock( proposals_mutex_ ); + if ( proposals_.find( proposal.proposal_id() ) == proposals_.end() ) + { + 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() ) + { + 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; + if ( IsBetterProposal( proposal, current ) ) + { + 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 ) + { + slot_state.voted = true; + should_vote = true; + } + } + + if ( should_vote && signer_ && !account_address_.empty() ) + { + auto vote_result = CreateVote( proposal.proposal_id(), account_address_, true, signer_ ); + if ( vote_result.has_value() ) + { + (void)SubmitVote( vote_result.value() ); + } + } + } + + void ConsensusManager::HandleVote( const Vote &vote ) + { + if ( vote_handler_ ) + { + vote_handler_( vote ); + } + + if ( !registry_ ) + { + return; + } + + ProposalState state; + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( vote.proposal_id() ); + if ( it == proposals_.end() ) + { + return; + } + it->second.votes.push_back( vote ); + state = it->second; + + auto slot_it = slot_states_.find( state.slot_key ); + if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id != vote.proposal_id() ) + { + return; + } + } + + auto tally_result = TallyVotes( state.proposal, state.votes ); + if ( tally_result.has_error() || !tally_result.value().has_quorum ) + { + return; + } + + if ( state.certificate.has_value() ) + { + return; + } + + auto certificate_result = CreateCertificate( state.proposal, state.votes ); + if ( certificate_result.has_error() ) + { + return; + } + + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( vote.proposal_id() ); + if ( it != proposals_.end() ) + { + it->second.certificate = certificate_result.value(); + } + } + + (void)SubmitCertificate( certificate_result.value() ); + NotifyCertificate( state.proposal, certificate_result.value() ); + } + + void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) + { + if ( vote_bundle_handler_ ) + { + vote_bundle_handler_( bundle ); + } + for ( const auto &vote : bundle.votes() ) + { + HandleVote( vote ); + } + } + + void ConsensusManager::HandleCertificate( const Certificate &certificate ) + { + if ( certificate_handler_ ) + { + certificate_handler_( certificate ); + } + + if ( !registry_ ) + { + return; + } + + ProposalState state; + Proposal proposal; + bool have_proposal = false; + if ( certificate.has_proposal() ) + { + proposal = certificate.proposal(); + + if ( proposal.proposal_id() != certificate.proposal_id() ) + { + return; + } + + if ( proposal.registry_cid() != certificate.registry_cid() || + proposal.registry_epoch() != certificate.registry_epoch() ) + { + return; + } + + if ( !ValidateSubject( proposal.subject() ) ) + { + return; + } + + auto signing_bytes = ProposalSigningBytes( proposal ); + if ( signing_bytes.has_error() ) + { + return; + } + if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), + proposal.signature(), + signing_bytes.value() ) ) + { + return; + } + + const auto computed_id = CreateProposalId( proposal ); + if ( computed_id.empty() || computed_id != certificate.proposal_id() ) + { + return; + } + + have_proposal = true; + } + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( certificate.proposal_id() ); + if ( it != proposals_.end() ) + { + if ( it->second.certificate.has_value() ) + { + return; + } + state = it->second; + proposal = state.proposal; + have_proposal = true; + } + else if ( have_proposal ) + { + ProposalState new_state; + new_state.proposal = proposal; + new_state.slot_key = GetSlotKey( proposal ); + proposals_.emplace( proposal.proposal_id(), new_state ); + state = std::move( new_state ); + + auto &slot_state = slot_states_[state.slot_key]; + if ( slot_state.best_proposal_id.empty() ) + { + slot_state.best_proposal_id = proposal.proposal_id(); + if ( proposal.subject().has_nonce() ) + { + slot_state.best_tx_hash = proposal.subject().nonce().tx_hash(); + } + } + } + else + { + return; + } + + auto slot_it = slot_states_.find( state.slot_key ); + if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id != certificate.proposal_id() ) + { + return; + } + } + + std::vector votes; + votes.reserve( static_cast( certificate.votes_size() ) ); + for ( const auto &vote : certificate.votes() ) + { + votes.push_back( vote ); + } + + auto tally_result = TallyVotes( proposal, votes ); + if ( tally_result.has_error() || !tally_result.value().has_quorum ) + { + return; + } + + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( certificate.proposal_id() ); + if ( it != proposals_.end() ) + { + it->second.certificate = certificate; + } + } + + NotifyCertificate( proposal, certificate ); + } + + void ConsensusManager::NotifyCertificate( const Proposal &proposal, const Certificate &certificate ) + { + if ( certificate_callback_ ) + { + certificate_callback_( proposal, certificate ); + } + } + + std::string ConsensusManager::GetSlotKey( const Proposal &proposal ) const + { + 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 + { + 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 std::lexicographical_compare( cand_hash.begin(), + cand_hash.end(), + curr_hash.begin(), + curr_hash.end() ); + } + + return candidate.proposal_id() < current.proposal_id(); + } + outcome::result ConsensusManager::ComputeSubjectId( const Subject &subject ) { Subject copy = subject; @@ -286,16 +750,14 @@ namespace sgns::blockchain } sgns::crypto::HasherImpl hasher; - auto hash = hasher.sha2_256( - gsl::span( reinterpret_cast( serialized.data() ), - serialized.size() ) ); + auto hash = hasher.sha2_256( + gsl::span( reinterpret_cast( serialized.data() ), serialized.size() ) ); return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); } - outcome::result ConsensusManager::CreateNonceSubject( - const std::string &account_id, - uint64_t nonce, - const std::vector &tx_hash ) + outcome::result ConsensusManager::CreateNonceSubject( const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash ) { Subject subject; subject.set_type( SubjectType::SUBJECT_NONCE ); @@ -314,10 +776,10 @@ namespace sgns::blockchain } outcome::result ConsensusManager::CreateTaskResultSubject( - const std::string &account_id, - const std::string &escrow_path, - const std::vector &task_result_hash, - uint64_t result_epoch ) + const std::string &account_id, + const std::string &escrow_path, + const std::string &task_result_hash, + uint64_t result_epoch ) { Subject subject; subject.set_type( SubjectType::SUBJECT_TASK_RESULT ); @@ -336,7 +798,7 @@ namespace sgns::blockchain return subject; } - std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) const + std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) { auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) @@ -345,12 +807,12 @@ namespace sgns::blockchain } sgns::crypto::HasherImpl hasher; - auto hash = hasher.sha2_256( + 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() ) ); } - bool ConsensusManager::ValidateSubject( const Subject &subject ) const + bool ConsensusManager::ValidateSubject( const Subject &subject ) { if ( subject.account_id().empty() ) { @@ -393,4 +855,39 @@ namespace sgns::blockchain } return nullptr; } + + void ConsensusManager::OnConsensusMessage( boost::optional message ) + { + if ( !message ) + { + return; + } + + ConsensusMessage decoded; + if ( !decoded.ParseFromArray( message->data.data(), static_cast( message->data.size() ) ) ) + { + ConsensusManagerLogger()->debug( "Failed to decode consensus message" ); + return; + } + + if ( decoded.has_proposal() ) + { + HandleProposal( decoded.proposal() ); + return; + } + if ( decoded.has_vote() ) + { + HandleVote( decoded.vote() ); + return; + } + if ( decoded.has_vote_bundle() ) + { + HandleVoteBundle( decoded.vote_bundle() ); + return; + } + if ( decoded.has_certificate() ) + { + HandleCertificate( decoded.certificate() ); + } + } } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index f0d110afd..6645dd70f 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -6,51 +6,71 @@ */ #pragma once +#include #include #include #include #include #include #include +#include +#include #include "blockchain/ValidatorRegistry.hpp" #include "blockchain/impl/proto/Consensus.pb.h" +#include "ipfs_pubsub/gossip_pubsub.hpp" #include "outcome/outcome.hpp" namespace sgns::blockchain { - class ConsensusManager + class ConsensusManager : public std::enable_shared_from_this { public: - using Proposal = ConsensusProposal; - using Vote = ConsensusVote; - using VoteBundle = ConsensusVoteBundle; + using Proposal = ConsensusProposal; + using Vote = ConsensusVote; + using VoteBundle = ConsensusVoteBundle; using Certificate = ConsensusCertificate; - using Subject = ConsensusSubject; + using Subject = ConsensusSubject; - using Signer = - std::function( std::vector payload )>; - using Verifier = - std::function &payload )>; + using Signer = std::function>( std::vector payload )>; + using ProposalHandler = std::function; + using VoteHandler = std::function; + using VoteBundleHandler = std::function; + using CertificateHandler = std::function; + using CertificateCallback = std::function; + using ProposalValidator = std::function; struct QuorumTally { - uint64_t total_weight = 0; + uint64_t total_weight = 0; uint64_t approved_weight = 0; - bool has_quorum = false; + bool has_quorum = false; }; - explicit ConsensusManager( std::shared_ptr registry ); + static std::shared_ptr New( std::shared_ptr registry, + std::shared_ptr pubsub, + Signer signer, + std::string consensus_topic = "" ); - void SetRegistry( std::shared_ptr registry ); + void SetProposalValidator( ProposalValidator validator ); + void SetCertificateCallback( CertificateCallback callback ); - outcome::result CreateProposal( const Subject &subject, - const std::string &proposer_id, - const std::string ®istry_cid, - uint64_t registry_epoch, - Signer sign ); + outcome::result Publish( const ConsensusMessage &message ); + + void SetProposalHandler( ProposalHandler handler ); + void SetVoteHandler( VoteHandler handler ); + void SetVoteBundleHandler( VoteBundleHandler handler ); + void SetCertificateHandler( CertificateHandler handler ); + + 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, @@ -62,33 +82,78 @@ namespace sgns::blockchain const std::vector &votes, Signer sign ); - outcome::result CreateCertificate( const Proposal &proposal, - const std::vector &votes, - Verifier verify ); + outcome::result CreateCertificate( const Proposal &proposal, const std::vector &votes ); - outcome::result TallyVotes( const Proposal &proposal, - const std::vector &votes, - Verifier verify ); + outcome::result TallyVotes( const Proposal &proposal, const std::vector &votes ); 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::vector &tx_hash ); - static outcome::result CreateTaskResultSubject( const std::string &account_id, - const std::string &escrow_path, - const std::vector &task_result_hash, - uint64_t result_epoch ); + static outcome::result ComputeSubjectId( const Subject &subject ); + static outcome::result CreateNonceSubject( const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash ); + static outcome::result CreateTaskResultSubject( const std::string &account_id, + const std::string &escrow_path, + const std::string &task_result_hash, + uint64_t result_epoch ); + outcome::result SubmitProposal( const Proposal &proposal, bool self_vote = true ); + outcome::result SubmitVote( const Vote &vote ); + outcome::result SubmitCertificate( const Certificate &certificate ); private: - std::string CreateProposalId( const Proposal &proposal ) const; - bool ValidateSubject( const Subject &subject ) const; - const ValidatorRegistry::ValidatorEntry *FindValidator( - const ValidatorRegistry::Registry ®istry, - const std::string &validator_id ) const; + explicit ConsensusManager( std::shared_ptr registry, + std::shared_ptr pubsub, + Signer signer, + std::string consensus_topic ); + + static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; + + struct ProposalState + { + Proposal proposal; + std::vector votes; + std::optional certificate; + std::string slot_key; + }; + + 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 ); + void NotifyCertificate( const Proposal &proposal, const Certificate &certificate ); + std::string GetSlotKey( const Proposal &proposal ) const; + bool IsBetterProposal( const Proposal &candidate, const Proposal ¤t ) const; + + static std::string CreateProposalId( const Proposal &proposal ); + static bool ValidateSubject( const Subject &subject ); + const ValidatorRegistry::ValidatorEntry *FindValidator( const ValidatorRegistry::Registry ®istry, + const std::string &validator_id ) const; + + void OnConsensusMessage( boost::optional message ); + + std::shared_ptr registry_; + ProposalHandler proposal_handler_; + VoteHandler vote_handler_; + VoteBundleHandler vote_bundle_handler_; + CertificateHandler certificate_handler_; + CertificateCallback certificate_callback_; + ProposalValidator proposal_validator_; + Signer signer_; + std::string account_address_; + std::unordered_map proposals_; + std::unordered_map slot_states_; + mutable std::mutex proposals_mutex_; + std::shared_ptr pubsub_; - std::shared_ptr registry_; + std::string consensus_topic_; + std::shared_future> consensus_subs_future_; }; } diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index eaf57b812..c9208edd9 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -63,9 +63,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 ) ) ); @@ -134,7 +135,9 @@ namespace sgns instance->consensus_manager_ = blockchain::ConsensusManager::New( instance->validator_registry_, - [weak_ptr( std::weak_ptr( instance ) )]( std::vector payload ) + std::move( pubsub ), + [weak_ptr( std::weak_ptr( instance ) )]( + std::vector payload ) -> outcome::result> { if ( auto strong = weak_ptr.lock() ) { @@ -1513,4 +1516,30 @@ namespace sgns db_->AddListenTopic( std::string( BLOCKCHAIN_TOPIC ) ); //This will not trigger the broadcaster, but it will grab links on CRDT } + + void Blockchain::SetCertificateCallback( blockchain::ConsensusManager::CertificateCallback callback ) + { + consensus_manager_->SetCertificateCallback( std::move( callback ) ); + } + + outcome::result Blockchain::CreateConsensusProposal( + const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash ) + { + OUTCOME_TRY( auto &&nonce_subject, consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash ) ); + 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 blockchain::ConsensusManager::Proposal &proposal ) + { + return consensus_manager_->SubmitProposal( std::move( proposal ) ); + } + } diff --git a/src/blockchain/impl/CMakeLists.txt b/src/blockchain/impl/CMakeLists.txt index f820a2789..47ce294cd 100644 --- a/src/blockchain/impl/CMakeLists.txt +++ b/src/blockchain/impl/CMakeLists.txt @@ -39,6 +39,7 @@ target_link_libraries(blockchain_genesis hexutil logger sgns_genius_account + ipfs-pubsub PRIVATE SGBlockchainProto ConsensusProto diff --git a/test/src/blockchain/CMakeLists.txt b/test/src/blockchain/CMakeLists.txt index a190efb12..740f73136 100644 --- a/test/src/blockchain/CMakeLists.txt +++ b/test/src/blockchain/CMakeLists.txt @@ -8,6 +8,7 @@ addtest(consensus_certificate_test ) target_link_libraries(consensus_certificate_test blockchain_genesis + rapidjson base_crdt_test ) @@ -15,6 +16,7 @@ target_include_directories(blockchain_genesis_test PRIVATE ${AsyncIOManager_INCL 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 a4a3c762e..1eca74e69 100644 --- a/test/src/blockchain/blockchain_genesis_test.cpp +++ b/test/src/blockchain/blockchain_genesis_test.cpp @@ -22,7 +22,7 @@ #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 diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index 8c84f2b4c..e4549f319 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -11,15 +11,11 @@ namespace { - constexpr const char *kTestPrivateKey = - "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce8b1a6f0d4f3b9b7f0a1b2"; + constexpr const char *kTestPrivateKey = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce8b1a6f0d4f3b9b7f0a1b2"; std::shared_ptr MakeAccount( const std::string &path ) { - auto account = sgns::GeniusAccount::New( sgns::TokenID::FromBytes( { 0x00 } ), - kTestPrivateKey, - path, - false ); + auto account = sgns::GeniusAccount::New( sgns::TokenID::FromBytes( { 0x00 } ), kTestPrivateKey, path, false ); EXPECT_TRUE( account ); return account; } @@ -39,9 +35,9 @@ namespace { 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 ) ); } ); + 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( @@ -60,7 +56,7 @@ namespace namespace sgns::test { - class ConsensusCertificateTest : public CRDTFixture + class ConsensusCertificateTest : public ::test::CRDTFixture { public: ConsensusCertificateTest() : CRDTFixture( "ConsensusCertificateTest" ) {} @@ -73,33 +69,32 @@ namespace sgns::test TEST_F( ConsensusCertificateTest, CreateCertificateEmbedsProposal ) { - auto account = MakeAccount( getPathString() ); + auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = std::make_shared( registry ); + auto manager = blockchain::ConsensusManager::New( registry, + pubs_, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); - std::vector tx_hash{ 0x01, 0x02, 0x03 }; - auto subject_result = - blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); + std::string tx_hash = "0x010203"; + auto subject_result = blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); 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 ) ); } ); + 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 ) ); } ); + 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() }, manager->verifier_ ); + auto cert_result = manager->CreateCertificate( proposal_result.value(), { vote_result.value() } ); ASSERT_TRUE( cert_result.has_value() ); const auto &cert = cert_result.value(); @@ -109,14 +104,16 @@ namespace sgns::test TEST_F( ConsensusCertificateTest, HandleCertificateRejectsMismatchedProposal ) { - auto account = MakeAccount( getPathString() ); + auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = std::make_shared( registry ); + auto manager = blockchain::ConsensusManager::New( registry, + pubs_, + [account]( std::vector payload ) + { return account->Sign( std::move( payload ) ); } ); - std::vector tx_hash{ 0x0a, 0x0b, 0x0c }; - auto subject_result = - blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); + std::string tx_hash = "0x010203"; + auto subject_result = blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); ASSERT_TRUE( subject_result.has_value() ); auto proposal_result = manager->CreateProposal( subject_result.value(), @@ -127,28 +124,26 @@ namespace sgns::test { 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 ) ); } ); + 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() }, manager->verifier_ ); + auto cert_result = manager->CreateCertificate( proposal_result.value(), { vote_result.value() } ); ASSERT_TRUE( cert_result.has_value() ); auto cert = cert_result.value(); bool notified = false; - manager->SetCertificateCallback( - [¬ified]( const blockchain::ConsensusProposal &, const blockchain::ConsensusCertificate & ) - { notified = true; } ); + manager->SetCertificateCallback( [¬ified]( const blockchain::ConsensusProposal &, + const blockchain::ConsensusCertificate & ) { notified = true; } ); manager->HandleCertificate( cert ); EXPECT_TRUE( notified ); - notified = false; + notified = false; auto *bad_subject = cert.mutable_proposal()->mutable_subject()->mutable_nonce(); bad_subject->set_nonce( bad_subject->nonce() + 1 ); From f69cb4c3ec74c65c9a69e42dbbabc76581dc26a7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 4 Feb 2026 15:29:56 -0300 Subject: [PATCH 011/114] Fix: Destructor removes registered callbacks --- src/account/TransactionManager.cpp | 15 +++++++++++++++ src/blockchain/ValidatorRegistry.cpp | 11 +++++++++++ src/blockchain/ValidatorRegistry.hpp | 1 + src/blockchain/impl/Blockchain.cpp | 9 +++++++++ src/crdt/crdt_datastore.hpp | 3 +++ src/crdt/globaldb/globaldb.cpp | 15 +++++++++++++++ src/crdt/globaldb/globaldb.hpp | 3 +++ src/crdt/impl/crdt_datastore.cpp | 15 +++++++++++++++ 8 files changed, 72 insertions(+) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 92cf0f955..c0c1540ce 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -171,6 +171,21 @@ namespace sgns m_logger->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(); } diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index ed7d332fc..cf3c4f288 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -74,6 +74,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, diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index 50d6067bc..38bf9c418 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -61,6 +61,7 @@ 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; diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index c9208edd9..a0e011f31 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -328,6 +328,15 @@ namespace sgns Blockchain::~Blockchain() { logger_->debug( "[{}] ~Blockchain destructor called", account_->GetAddress().substr( 0, 8 ) ); + 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(); } diff --git a/src/crdt/crdt_datastore.hpp b/src/crdt/crdt_datastore.hpp index e5e5b25dd..a2cd0a2b2 100644 --- a/src/crdt/crdt_datastore.hpp +++ b/src/crdt/crdt_datastore.hpp @@ -215,6 +215,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. diff --git a/src/crdt/globaldb/globaldb.cpp b/src/crdt/globaldb/globaldb.cpp index b86369e8c..a8a673050 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; diff --git a/src/crdt/globaldb/globaldb.hpp b/src/crdt/globaldb/globaldb.hpp index b00f91958..5ba455912 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(); diff --git a/src/crdt/impl/crdt_datastore.cpp b/src/crdt/impl/crdt_datastore.cpp index 6650494dd..efdd167d3 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 ); From 1a286adec791634c635864f97e9ba0b4eaf472ee Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 4 Feb 2026 15:49:52 -0300 Subject: [PATCH 012/114] Chore: Adding logs --- src/blockchain/Consensus.cpp | 206 ++++++++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 6 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 943ff4521..9c39b8754 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -35,6 +35,7 @@ namespace sgns::blockchain { if ( !pubsub ) { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: pubsub is null", __func__ ); return nullptr; } @@ -50,11 +51,11 @@ namespace sgns::blockchain { if ( auto self = weakptr.lock() ) { - ConsensusManagerLogger()->trace( "Received Consensus Message on topic {}", self->consensus_topic_ ); + ConsensusManagerLogger()->trace( "{}: Received Consensus Message on topic {}", __func__, self->consensus_topic_ ); self->OnConsensusMessage( message ); } } ) ); - ConsensusManagerLogger()->debug( "Subscribed to Consensus topic {}", instance->consensus_topic_ ); + ConsensusManagerLogger()->debug( "{}: Subscribed to Consensus topic {}", __func__, instance->consensus_topic_ ); return instance; } @@ -86,11 +87,13 @@ namespace sgns::blockchain 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 {}", consensus_topic_ ); + ConsensusManagerLogger()->debug( "{}: Sending consensus packet to {}", __func__, consensus_topic_ ); pubsub_->Publish( consensus_topic_, serialized_proto ); + ConsensusManagerLogger()->debug( "{}: Consensus packet published (bytes={})", __func__, serialized_proto.size() ); return outcome::success(); } @@ -129,13 +132,16 @@ namespace sgns::blockchain uint64_t registry_epoch, Signer sign ) { + ConsensusManagerLogger()->trace( "{}: CreateProposal called for proposer_id={}", __func__, proposer_id ); if ( !sign ) { + ConsensusManagerLogger()->error( "{}: CreateProposal failed: signer is empty", __func__ ); return outcome::failure( std::errc::invalid_argument ); } if ( !ValidateSubject( subject ) ) { + ConsensusManagerLogger()->error( "{}: CreateProposal failed: subject validation failed", __func__ ); return outcome::failure( std::errc::invalid_argument ); } @@ -153,6 +159,8 @@ namespace sgns::blockchain auto subject_id_result = ComputeSubjectId( proposal.subject() ); if ( subject_id_result.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateProposal failed: subject id computation error={}", __func__, + subject_id_result.error().message() ); return outcome::failure( subject_id_result.error() ); } proposal.mutable_subject()->set_subject_id( subject_id_result.value() ); @@ -162,11 +170,14 @@ namespace sgns::blockchain auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateProposal failed: signing bytes error={}", __func__, + signing_bytes.error().message() ); return outcome::failure( signing_bytes.error() ); } OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); proposal.set_signature( signature.data(), signature.size() ); + ConsensusManagerLogger()->debug( "{}: CreateProposal success proposal_id={}", __func__, proposal.proposal_id() ); return proposal; } @@ -175,8 +186,13 @@ namespace sgns::blockchain bool approve, Signer sign ) { + ConsensusManagerLogger()->trace( "{}: CreateVote called proposal_id={} voter_id={} approve={}", __func__, + proposal_id, + voter_id, + approve ); if ( !sign ) { + ConsensusManagerLogger()->error( "{}: CreateVote failed: signer is empty", __func__ ); return outcome::failure( std::errc::invalid_argument ); } @@ -191,12 +207,15 @@ namespace sgns::blockchain auto signing_bytes = VoteSigningBytes( vote ); if ( signing_bytes.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateVote 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( "{}: CreateVote success proposal_id={} voter_id={}", __func__, proposal_id, voter_id ); return vote; } @@ -205,8 +224,13 @@ namespace sgns::blockchain const std::vector &votes, Signer sign ) { + ConsensusManagerLogger()->trace( "{}: CreateVoteBundle called proposal_id={} aggregator_id={} votes={}", __func__, + proposal_id, + aggregator_id, + votes.size() ); if ( !sign ) { + ConsensusManagerLogger()->error( "{}: CreateVoteBundle failed: signer is empty", __func__ ); return outcome::failure( std::errc::invalid_argument ); } @@ -224,21 +248,31 @@ namespace sgns::blockchain auto signing_bytes = VoteBundleSigningBytes( bundle ); if ( signing_bytes.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateVoteBundle 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( "{}: CreateVoteBundle success proposal_id={} votes={}", __func__, + proposal_id, + votes.size() ); return bundle; } outcome::result ConsensusManager::CreateCertificate( const Proposal &proposal, const std::vector &votes ) { + ConsensusManagerLogger()->trace( "{}: CreateCertificate called proposal_id={} votes={}", __func__, + proposal.proposal_id(), + votes.size() ); auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateCertificate failed: tally error={}", __func__, + tally_result.error().message() ); return outcome::failure( tally_result.error() ); } @@ -258,20 +292,27 @@ namespace sgns::blockchain } *cert.mutable_proposal() = proposal; + ConsensusManagerLogger()->debug( "{}: CreateCertificate success proposal_id={}", __func__, proposal.proposal_id() ); return cert; } outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, const std::vector &votes ) { + ConsensusManagerLogger()->trace( "{}: TallyVotes called proposal_id={} votes={}", __func__, + proposal.proposal_id(), + votes.size() ); if ( !registry_ ) { + ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry is null", __func__ ); return outcome::failure( std::errc::not_supported ); } auto registry_result = registry_->LoadRegistry(); if ( registry_result.has_error() ) { + ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry load error={}", __func__, + registry_result.error().message() ); return outcome::failure( registry_result.error() ); } @@ -279,10 +320,16 @@ namespace sgns::blockchain const auto registry_cid = registry_->GetRegistryCid(); if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { + ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry cid mismatch proposal={} registry={}", __func__, + proposal.registry_cid(), + registry_cid ); return outcome::failure( std::errc::invalid_argument ); } if ( proposal.registry_epoch() != registry.epoch() ) { + ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry epoch mismatch proposal={} registry={}", __func__, + proposal.registry_epoch(), + registry.epoch() ); return outcome::failure( std::errc::invalid_argument ); } @@ -292,6 +339,9 @@ namespace sgns::blockchain for ( const auto &vote : votes ) { + ConsensusManagerLogger()->trace( "{}: TallyVotes processing vote voter_id={} approve={}", __func__, + vote.voter_id(), + vote.approve() ); if ( vote.proposal_id() != proposal.proposal_id() ) { continue; @@ -328,16 +378,23 @@ namespace sgns::blockchain tally.total_weight = total_weight; tally.approved_weight = approved_weight; tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); + ConsensusManagerLogger()->debug( "{}: TallyVotes success proposal_id={} approved_weight={} total_weight={} quorum={}", __func__, + proposal.proposal_id(), + approved_weight, + total_weight, + tally.has_quorum ); return tally; } outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) { + ConsensusManagerLogger()->trace( "{}: ProposalSigningBytes called proposal_id={}", __func__, proposal.proposal_id() ); Proposal copy = proposal; copy.clear_signature(); std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { + ConsensusManagerLogger()->error( "{}: ProposalSigningBytes failed: serialization error", __func__ ); return outcome::failure( std::errc::invalid_argument ); } return std::vector( serialized.begin(), serialized.end() ); @@ -345,11 +402,15 @@ namespace sgns::blockchain outcome::result> ConsensusManager::VoteSigningBytes( const Vote &vote ) { + ConsensusManagerLogger()->trace( "{}: VoteSigningBytes called voter_id={} proposal_id={}", __func__, + vote.voter_id(), + vote.proposal_id() ); Vote copy = vote; copy.clear_signature(); std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { + ConsensusManagerLogger()->error( "{}: VoteSigningBytes failed: serialization error", __func__ ); return outcome::failure( std::errc::invalid_argument ); } return std::vector( serialized.begin(), serialized.end() ); @@ -357,11 +418,15 @@ namespace sgns::blockchain outcome::result> ConsensusManager::VoteBundleSigningBytes( const VoteBundle &bundle ) { + ConsensusManagerLogger()->trace( "{}: VoteBundleSigningBytes called proposal_id={} votes={}", __func__, + bundle.proposal_id(), + bundle.votes_size() ); VoteBundle copy = bundle; copy.clear_signature(); std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { + ConsensusManagerLogger()->error( "{}: VoteBundleSigningBytes failed: serialization error", __func__ ); return outcome::failure( std::errc::invalid_argument ); } return std::vector( serialized.begin(), serialized.end() ); @@ -369,6 +434,9 @@ namespace sgns::blockchain outcome::result ConsensusManager::SubmitProposal( const Proposal &proposal, bool self_vote ) { + ConsensusManagerLogger()->trace( "{}: SubmitProposal called proposal_id={} self_vote={}", __func__, + proposal.proposal_id(), + self_vote ); const auto slot_key = GetSlotKey( proposal ); { std::lock_guard lock( proposals_mutex_ ); @@ -387,8 +455,11 @@ namespace sgns::blockchain auto publish_result = Publish( message ); if ( publish_result.has_error() ) { + ConsensusManagerLogger()->error( "{}: SubmitProposal failed: publish error={}", __func__, + publish_result.error().message() ); return publish_result; } + ConsensusManagerLogger()->debug( "{}: SubmitProposal success proposal_id={}", __func__, proposal.proposal_id() ); if ( self_vote ) { @@ -400,20 +471,41 @@ namespace sgns::blockchain outcome::result ConsensusManager::SubmitVote( const Vote &vote ) { + ConsensusManagerLogger()->trace( "{}: SubmitVote called proposal_id={} voter_id={}", __func__, + vote.proposal_id(), + vote.voter_id() ); ConsensusMessage message; *message.mutable_vote() = vote; - return Publish( message ); + auto result = Publish( message ); + if ( result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: SubmitVote failed: publish error={}", __func__, result.error().message() ); + return result; + } + ConsensusManagerLogger()->debug( "{}: SubmitVote success proposal_id={} voter_id={}", __func__, + vote.proposal_id(), + vote.voter_id() ); + return result; } outcome::result ConsensusManager::SubmitCertificate( const Certificate &certificate ) { + ConsensusManagerLogger()->trace( "{}: SubmitCertificate called proposal_id={}", __func__, certificate.proposal_id() ); ConsensusMessage message; *message.mutable_certificate() = certificate; - return Publish( message ); + auto result = Publish( message ); + if ( result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: SubmitCertificate failed: publish error={}", __func__, result.error().message() ); + return result; + } + ConsensusManagerLogger()->debug( "{}: SubmitCertificate success proposal_id={}", __func__, certificate.proposal_id() ); + return result; } void ConsensusManager::HandleProposal( const Proposal &proposal ) { + ConsensusManagerLogger()->trace( "{}: HandleProposal called proposal_id={}", __func__, proposal.proposal_id() ); if ( proposal_handler_ ) { proposal_handler_( proposal ); @@ -421,31 +513,44 @@ namespace sgns::blockchain if ( !registry_ ) { + ConsensusManagerLogger()->error( "{}: HandleProposal aborted: registry is null", __func__ ); return; } const auto registry_cid = registry_->GetRegistryCid(); if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry cid mismatch proposal={} registry={}", __func__, + proposal.registry_cid(), + registry_cid ); return; } if ( proposal.registry_epoch() != registry_->GetRegistryEpoch() ) { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry epoch mismatch proposal={} registry={}", __func__, + proposal.registry_epoch(), + registry_->GetRegistryEpoch() ); return; } auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signing bytes error={}", __func__, + signing_bytes.error().message() ); return; } if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signature verification failed proposer_id={}", __func__, + proposal.proposer_id() ); return; } if ( proposal_validator_ && !proposal_validator_( proposal ) ) { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: proposal validator failed proposal_id={}", __func__, + proposal.proposal_id() ); return; } @@ -496,12 +601,23 @@ namespace sgns::blockchain if ( vote_result.has_value() ) { (void)SubmitVote( vote_result.value() ); + ConsensusManagerLogger()->debug( "{}: HandleProposal self-vote submitted proposal_id={}", __func__, + proposal.proposal_id() ); + } + else + { + ConsensusManagerLogger()->error( "{}: HandleProposal self-vote failed proposal_id={} error={}", __func__, + proposal.proposal_id(), + vote_result.error().message() ); } } } void ConsensusManager::HandleVote( const Vote &vote ) { + ConsensusManagerLogger()->trace( "{}: HandleVote called proposal_id={} voter_id={}", __func__, + vote.proposal_id(), + vote.voter_id() ); if ( vote_handler_ ) { vote_handler_( vote ); @@ -509,6 +625,7 @@ namespace sgns::blockchain if ( !registry_ ) { + ConsensusManagerLogger()->error( "{}: HandleVote aborted: registry is null", __func__ ); return; } @@ -518,6 +635,8 @@ namespace sgns::blockchain auto it = proposals_.find( vote.proposal_id() ); if ( it == proposals_.end() ) { + ConsensusManagerLogger()->error( "{}: HandleVote ignored: proposal not found proposal_id={}", __func__, + vote.proposal_id() ); return; } it->second.votes.push_back( vote ); @@ -526,6 +645,8 @@ namespace sgns::blockchain auto slot_it = slot_states_.find( state.slot_key ); if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id != vote.proposal_id() ) { + ConsensusManagerLogger()->error( "{}: HandleVote ignored: not best proposal proposal_id={}", __func__, + vote.proposal_id() ); return; } } @@ -533,17 +654,26 @@ namespace sgns::blockchain auto tally_result = TallyVotes( state.proposal, state.votes ); if ( tally_result.has_error() || !tally_result.value().has_quorum ) { + if ( tally_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: HandleVote aborted: tally error={}", __func__, + tally_result.error().message() ); + } return; } if ( state.certificate.has_value() ) { + ConsensusManagerLogger()->debug( "{}: HandleVote skipped: certificate already present proposal_id={}", __func__, + vote.proposal_id() ); return; } auto certificate_result = CreateCertificate( state.proposal, state.votes ); if ( certificate_result.has_error() ) { + ConsensusManagerLogger()->error( "{}: HandleVote failed: certificate creation error={}", __func__, + certificate_result.error().message() ); return; } @@ -558,22 +688,28 @@ namespace sgns::blockchain (void)SubmitCertificate( certificate_result.value() ); NotifyCertificate( state.proposal, certificate_result.value() ); + ConsensusManagerLogger()->debug( "{}: HandleVote certificate submitted proposal_id={}", __func__, vote.proposal_id() ); } void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) { + ConsensusManagerLogger()->trace( "{}: HandleVoteBundle called proposal_id={} votes={}", __func__, + bundle.proposal_id(), + bundle.votes_size() ); if ( vote_bundle_handler_ ) { vote_bundle_handler_( bundle ); } for ( const auto &vote : bundle.votes() ) { + ConsensusManagerLogger()->trace( "{}: HandleVoteBundle processing voter_id={}", __func__, vote.voter_id() ); HandleVote( vote ); } } void ConsensusManager::HandleCertificate( const Certificate &certificate ) { + ConsensusManagerLogger()->trace( "{}: HandleCertificate called proposal_id={}", __func__, certificate.proposal_id() ); if ( certificate_handler_ ) { certificate_handler_( certificate ); @@ -581,6 +717,7 @@ namespace sgns::blockchain if ( !registry_ ) { + ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: registry is null", __func__ ); return; } @@ -593,35 +730,49 @@ namespace sgns::blockchain if ( proposal.proposal_id() != certificate.proposal_id() ) { + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: proposal_id mismatch cert={} proposal={}", __func__, + certificate.proposal_id(), + proposal.proposal_id() ); return; } if ( proposal.registry_cid() != certificate.registry_cid() || proposal.registry_epoch() != certificate.registry_epoch() ) { + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: registry mismatch proposal_id={}", __func__, + certificate.proposal_id() ); return; } if ( !ValidateSubject( proposal.subject() ) ) { + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: invalid subject proposal_id={}", __func__, + proposal.proposal_id() ); return; } auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: signing bytes error={}", __func__, + signing_bytes.error().message() ); return; } if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) { + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: signature verification failed proposer_id={}", __func__, + proposal.proposer_id() ); return; } const auto computed_id = CreateProposalId( proposal ); if ( computed_id.empty() || computed_id != certificate.proposal_id() ) { + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: computed_id mismatch cert={} computed={}", __func__, + certificate.proposal_id(), + computed_id ); return; } @@ -634,6 +785,8 @@ namespace sgns::blockchain { if ( it->second.certificate.has_value() ) { + ConsensusManagerLogger()->debug( "{}: HandleCertificate skipped: already have certificate proposal_id={}", __func__, + certificate.proposal_id() ); return; } state = it->second; @@ -660,12 +813,16 @@ namespace sgns::blockchain } else { + ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: missing proposal proposal_id={}", __func__, + certificate.proposal_id() ); return; } 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( "{}: HandleCertificate rejected: not best proposal proposal_id={}", __func__, + certificate.proposal_id() ); return; } } @@ -674,12 +831,18 @@ namespace sgns::blockchain votes.reserve( static_cast( certificate.votes_size() ) ); for ( const auto &vote : certificate.votes() ) { + ConsensusManagerLogger()->trace( "{}: HandleCertificate processing vote voter_id={}", __func__, vote.voter_id() ); votes.push_back( vote ); } auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() || !tally_result.value().has_quorum ) { + if ( tally_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: tally error={}", __func__, + tally_result.error().message() ); + } return; } @@ -693,10 +856,12 @@ namespace sgns::blockchain } NotifyCertificate( proposal, certificate ); + ConsensusManagerLogger()->debug( "{}: HandleCertificate success proposal_id={}", __func__, certificate.proposal_id() ); } void ConsensusManager::NotifyCertificate( const Proposal &proposal, const Certificate &certificate ) { + ConsensusManagerLogger()->trace( "{}: NotifyCertificate called proposal_id={}", __func__, proposal.proposal_id() ); if ( certificate_callback_ ) { certificate_callback_( proposal, certificate ); @@ -705,6 +870,7 @@ namespace sgns::blockchain std::string ConsensusManager::GetSlotKey( const Proposal &proposal ) const { + ConsensusManagerLogger()->trace( "{}: GetSlotKey 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() ); @@ -718,6 +884,9 @@ namespace sgns::blockchain bool ConsensusManager::IsBetterProposal( const Proposal &candidate, const Proposal ¤t ) const { + ConsensusManagerLogger()->trace( "{}: IsBetterProposal 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 && @@ -741,17 +910,20 @@ namespace sgns::blockchain outcome::result ConsensusManager::ComputeSubjectId( const Subject &subject ) { + ConsensusManagerLogger()->trace( "{}: ComputeSubjectId called subject_type={}", __func__, static_cast( subject.type() ) ); Subject copy = subject; copy.clear_subject_id(); std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { + ConsensusManagerLogger()->error( "{}: ComputeSubjectId 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( "{}: ComputeSubjectId success", __func__ ); return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); } @@ -759,6 +931,7 @@ namespace sgns::blockchain uint64_t nonce, const std::string &tx_hash ) { + ConsensusManagerLogger()->trace( "{}: CreateNonceSubject called account_id={} nonce={}", __func__, account_id, nonce ); Subject subject; subject.set_type( SubjectType::SUBJECT_NONCE ); subject.set_account_id( account_id ); @@ -769,9 +942,12 @@ namespace sgns::blockchain auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateNonceSubject failed: subject id error={}", __func__, + subject_id.error().message() ); return outcome::failure( subject_id.error() ); } subject.set_subject_id( subject_id.value() ); + ConsensusManagerLogger()->debug( "{}: CreateNonceSubject success subject_id={}", __func__, subject.subject_id() ); return subject; } @@ -781,6 +957,9 @@ namespace sgns::blockchain const std::string &task_result_hash, uint64_t result_epoch ) { + ConsensusManagerLogger()->trace( "{}: CreateTaskResultSubject 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 ); @@ -792,28 +971,36 @@ namespace sgns::blockchain auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateTaskResultSubject failed: subject id error={}", __func__, + subject_id.error().message() ); return outcome::failure( subject_id.error() ); } subject.set_subject_id( subject_id.value() ); + ConsensusManagerLogger()->debug( "{}: CreateTaskResultSubject success subject_id={}", __func__, subject.subject_id() ); return subject; } std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) { + ConsensusManagerLogger()->trace( "{}: CreateProposalId called proposal_id={}", __func__, proposal.proposal_id() ); auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { + ConsensusManagerLogger()->error( "{}: CreateProposalId failed: 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() ) ); + ConsensusManagerLogger()->debug( "{}: CreateProposalId success", __func__ ); return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); } bool ConsensusManager::ValidateSubject( const Subject &subject ) { + ConsensusManagerLogger()->trace( "{}: ValidateSubject called subject_type={}", __func__, static_cast( subject.type() ) ); if ( subject.account_id().empty() ) { return false; @@ -846,6 +1033,7 @@ namespace sgns::blockchain const ValidatorRegistry::Registry ®istry, const std::string &validator_id ) const { + ConsensusManagerLogger()->trace( "{}: FindValidator called validator_id={}", __func__, validator_id ); for ( const auto &validator : registry.validators() ) { if ( validator.validator_id() == validator_id ) @@ -858,35 +1046,41 @@ namespace sgns::blockchain void ConsensusManager::OnConsensusMessage( boost::optional message ) { + ConsensusManagerLogger()->trace( "{}: OnConsensusMessage called", __func__ ); if ( !message ) { + ConsensusManagerLogger()->error( "{}: OnConsensusMessage ignored: message is empty", __func__ ); return; } ConsensusMessage decoded; if ( !decoded.ParseFromArray( message->data.data(), static_cast( message->data.size() ) ) ) { - ConsensusManagerLogger()->debug( "Failed to decode consensus message" ); + ConsensusManagerLogger()->error( "{}: Failed to decode consensus message", __func__ ); return; } if ( decoded.has_proposal() ) { + ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded proposal", __func__ ); HandleProposal( decoded.proposal() ); return; } if ( decoded.has_vote() ) { + ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded vote", __func__ ); HandleVote( decoded.vote() ); return; } if ( decoded.has_vote_bundle() ) { + ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded vote bundle", __func__ ); HandleVoteBundle( decoded.vote_bundle() ); return; } if ( decoded.has_certificate() ) { + ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded certificate", __func__ ); HandleCertificate( decoded.certificate() ); } } From 4def0969ad35e209427f46df770d53df45899e5e Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 5 Feb 2026 09:42:52 -0300 Subject: [PATCH 013/114] Fix: Certificate test now pass --- src/blockchain/Consensus.cpp | 294 ++++++++++++++++++++++++----------- 1 file changed, 203 insertions(+), 91 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 9c39b8754..135a692d7 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -51,7 +51,9 @@ namespace sgns::blockchain { if ( auto self = weakptr.lock() ) { - ConsensusManagerLogger()->trace( "{}: Received Consensus Message on topic {}", __func__, self->consensus_topic_ ); + ConsensusManagerLogger()->trace( "{}: Received Consensus Message on topic {}", + __func__, + self->consensus_topic_ ); self->OnConsensusMessage( message ); } } ) ); @@ -93,7 +95,9 @@ namespace sgns::blockchain ConsensusManagerLogger()->debug( "{}: Sending consensus packet to {}", __func__, consensus_topic_ ); pubsub_->Publish( consensus_topic_, serialized_proto ); - ConsensusManagerLogger()->debug( "{}: Consensus packet published (bytes={})", __func__, serialized_proto.size() ); + ConsensusManagerLogger()->debug( "{}: Consensus packet published (bytes={})", + __func__, + serialized_proto.size() ); return outcome::success(); } @@ -159,7 +163,8 @@ namespace sgns::blockchain auto subject_id_result = ComputeSubjectId( proposal.subject() ); if ( subject_id_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateProposal failed: subject id computation error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateProposal failed: subject id computation error={}", + __func__, subject_id_result.error().message() ); return outcome::failure( subject_id_result.error() ); } @@ -170,14 +175,17 @@ namespace sgns::blockchain auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateProposal failed: signing bytes error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateProposal failed: signing bytes error={}", + __func__, signing_bytes.error().message() ); return outcome::failure( signing_bytes.error() ); } OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); proposal.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: CreateProposal success proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: CreateProposal success proposal_id={}", + __func__, + proposal.proposal_id() ); return proposal; } @@ -186,7 +194,8 @@ namespace sgns::blockchain bool approve, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: CreateVote called proposal_id={} voter_id={} approve={}", __func__, + ConsensusManagerLogger()->trace( "{}: CreateVote called proposal_id={} voter_id={} approve={}", + __func__, proposal_id, voter_id, approve ); @@ -207,7 +216,8 @@ namespace sgns::blockchain auto signing_bytes = VoteSigningBytes( vote ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateVote failed: signing bytes error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateVote failed: signing bytes error={}", + __func__, signing_bytes.error().message() ); return outcome::failure( signing_bytes.error() ); } @@ -215,7 +225,10 @@ namespace sgns::blockchain OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); vote.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: CreateVote success proposal_id={} voter_id={}", __func__, proposal_id, voter_id ); + ConsensusManagerLogger()->debug( "{}: CreateVote success proposal_id={} voter_id={}", + __func__, + proposal_id, + voter_id ); return vote; } @@ -224,7 +237,8 @@ namespace sgns::blockchain const std::vector &votes, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: CreateVoteBundle called proposal_id={} aggregator_id={} votes={}", __func__, + ConsensusManagerLogger()->trace( "{}: CreateVoteBundle called proposal_id={} aggregator_id={} votes={}", + __func__, proposal_id, aggregator_id, votes.size() ); @@ -248,7 +262,8 @@ namespace sgns::blockchain auto signing_bytes = VoteBundleSigningBytes( bundle ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateVoteBundle failed: signing bytes error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateVoteBundle failed: signing bytes error={}", + __func__, signing_bytes.error().message() ); return outcome::failure( signing_bytes.error() ); } @@ -256,7 +271,8 @@ namespace sgns::blockchain OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); bundle.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: CreateVoteBundle success proposal_id={} votes={}", __func__, + ConsensusManagerLogger()->debug( "{}: CreateVoteBundle success proposal_id={} votes={}", + __func__, proposal_id, votes.size() ); return bundle; @@ -265,13 +281,15 @@ namespace sgns::blockchain outcome::result ConsensusManager::CreateCertificate( const Proposal &proposal, const std::vector &votes ) { - ConsensusManagerLogger()->trace( "{}: CreateCertificate called proposal_id={} votes={}", __func__, + ConsensusManagerLogger()->trace( "{}: CreateCertificate called proposal_id={} votes={}", + __func__, proposal.proposal_id(), votes.size() ); auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateCertificate failed: tally error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateCertificate failed: tally error={}", + __func__, tally_result.error().message() ); return outcome::failure( tally_result.error() ); } @@ -292,14 +310,17 @@ namespace sgns::blockchain } *cert.mutable_proposal() = proposal; - ConsensusManagerLogger()->debug( "{}: CreateCertificate success proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: CreateCertificate success proposal_id={}", + __func__, + proposal.proposal_id() ); return cert; } outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, const std::vector &votes ) { - ConsensusManagerLogger()->trace( "{}: TallyVotes called proposal_id={} votes={}", __func__, + ConsensusManagerLogger()->trace( "{}: TallyVotes called proposal_id={} votes={}", + __func__, proposal.proposal_id(), votes.size() ); if ( !registry_ ) @@ -311,7 +332,8 @@ namespace sgns::blockchain auto registry_result = registry_->LoadRegistry(); if ( registry_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry load error={}", __func__, + ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry load error={}", + __func__, registry_result.error().message() ); return outcome::failure( registry_result.error() ); } @@ -320,14 +342,16 @@ namespace sgns::blockchain const auto registry_cid = registry_->GetRegistryCid(); if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { - ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry cid mismatch proposal={} registry={}", __func__, + ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry cid mismatch proposal={} registry={}", + __func__, proposal.registry_cid(), registry_cid ); return outcome::failure( std::errc::invalid_argument ); } if ( proposal.registry_epoch() != registry.epoch() ) { - ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry epoch mismatch proposal={} registry={}", __func__, + ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry epoch mismatch proposal={} registry={}", + __func__, proposal.registry_epoch(), registry.epoch() ); return outcome::failure( std::errc::invalid_argument ); @@ -339,7 +363,8 @@ namespace sgns::blockchain for ( const auto &vote : votes ) { - ConsensusManagerLogger()->trace( "{}: TallyVotes processing vote voter_id={} approve={}", __func__, + ConsensusManagerLogger()->trace( "{}: TallyVotes processing vote voter_id={} approve={}", + __func__, vote.voter_id(), vote.approve() ); if ( vote.proposal_id() != proposal.proposal_id() ) @@ -378,17 +403,21 @@ namespace sgns::blockchain tally.total_weight = total_weight; tally.approved_weight = approved_weight; tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); - ConsensusManagerLogger()->debug( "{}: TallyVotes success proposal_id={} approved_weight={} total_weight={} quorum={}", __func__, - proposal.proposal_id(), - approved_weight, - total_weight, - tally.has_quorum ); + ConsensusManagerLogger()->debug( + "{}: TallyVotes success proposal_id={} approved_weight={} total_weight={} quorum={}", + __func__, + proposal.proposal_id(), + approved_weight, + total_weight, + tally.has_quorum ); return tally; } outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: ProposalSigningBytes called proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: ProposalSigningBytes called proposal_id={}", + __func__, + proposal.proposal_id() ); Proposal copy = proposal; copy.clear_signature(); std::string serialized; @@ -402,7 +431,8 @@ namespace sgns::blockchain outcome::result> ConsensusManager::VoteSigningBytes( const Vote &vote ) { - ConsensusManagerLogger()->trace( "{}: VoteSigningBytes called voter_id={} proposal_id={}", __func__, + ConsensusManagerLogger()->trace( "{}: VoteSigningBytes called voter_id={} proposal_id={}", + __func__, vote.voter_id(), vote.proposal_id() ); Vote copy = vote; @@ -418,7 +448,8 @@ namespace sgns::blockchain outcome::result> ConsensusManager::VoteBundleSigningBytes( const VoteBundle &bundle ) { - ConsensusManagerLogger()->trace( "{}: VoteBundleSigningBytes called proposal_id={} votes={}", __func__, + ConsensusManagerLogger()->trace( "{}: VoteBundleSigningBytes called proposal_id={} votes={}", + __func__, bundle.proposal_id(), bundle.votes_size() ); VoteBundle copy = bundle; @@ -434,7 +465,8 @@ namespace sgns::blockchain outcome::result ConsensusManager::SubmitProposal( const Proposal &proposal, bool self_vote ) { - ConsensusManagerLogger()->trace( "{}: SubmitProposal called proposal_id={} self_vote={}", __func__, + ConsensusManagerLogger()->trace( "{}: SubmitProposal called proposal_id={} self_vote={}", + __func__, proposal.proposal_id(), self_vote ); const auto slot_key = GetSlotKey( proposal ); @@ -455,11 +487,14 @@ namespace sgns::blockchain auto publish_result = Publish( message ); if ( publish_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: SubmitProposal failed: publish error={}", __func__, + ConsensusManagerLogger()->error( "{}: SubmitProposal failed: publish error={}", + __func__, publish_result.error().message() ); return publish_result; } - ConsensusManagerLogger()->debug( "{}: SubmitProposal success proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: SubmitProposal success proposal_id={}", + __func__, + proposal.proposal_id() ); if ( self_vote ) { @@ -471,18 +506,22 @@ namespace sgns::blockchain outcome::result ConsensusManager::SubmitVote( const Vote &vote ) { - ConsensusManagerLogger()->trace( "{}: SubmitVote called proposal_id={} voter_id={}", __func__, + ConsensusManagerLogger()->trace( "{}: SubmitVote called proposal_id={} voter_id={}", + __func__, vote.proposal_id(), vote.voter_id() ); ConsensusMessage message; *message.mutable_vote() = vote; - auto result = Publish( message ); + auto result = Publish( message ); if ( result.has_error() ) { - ConsensusManagerLogger()->error( "{}: SubmitVote failed: publish error={}", __func__, result.error().message() ); + ConsensusManagerLogger()->error( "{}: SubmitVote failed: publish error={}", + __func__, + result.error().message() ); return result; } - ConsensusManagerLogger()->debug( "{}: SubmitVote success proposal_id={} voter_id={}", __func__, + ConsensusManagerLogger()->debug( "{}: SubmitVote success proposal_id={} voter_id={}", + __func__, vote.proposal_id(), vote.voter_id() ); return result; @@ -490,16 +529,22 @@ namespace sgns::blockchain outcome::result ConsensusManager::SubmitCertificate( const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: SubmitCertificate called proposal_id={}", __func__, certificate.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: SubmitCertificate called proposal_id={}", + __func__, + certificate.proposal_id() ); ConsensusMessage message; *message.mutable_certificate() = certificate; - auto result = Publish( message ); + auto result = Publish( message ); if ( result.has_error() ) { - ConsensusManagerLogger()->error( "{}: SubmitCertificate failed: publish error={}", __func__, result.error().message() ); + ConsensusManagerLogger()->error( "{}: SubmitCertificate failed: publish error={}", + __func__, + result.error().message() ); return result; } - ConsensusManagerLogger()->debug( "{}: SubmitCertificate success proposal_id={}", __func__, certificate.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: SubmitCertificate success proposal_id={}", + __func__, + certificate.proposal_id() ); return result; } @@ -520,36 +565,44 @@ namespace sgns::blockchain const auto registry_cid = registry_->GetRegistryCid(); if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry cid mismatch proposal={} registry={}", __func__, - proposal.registry_cid(), - registry_cid ); + ConsensusManagerLogger()->error( + "{}: HandleProposal rejected: registry cid mismatch proposal={} registry={}", + __func__, + proposal.registry_cid(), + registry_cid ); return; } if ( proposal.registry_epoch() != registry_->GetRegistryEpoch() ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry epoch mismatch proposal={} registry={}", __func__, - proposal.registry_epoch(), - registry_->GetRegistryEpoch() ); + ConsensusManagerLogger()->error( + "{}: HandleProposal rejected: registry epoch mismatch proposal={} registry={}", + __func__, + proposal.registry_epoch(), + registry_->GetRegistryEpoch() ); return; } auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signing bytes error={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signing bytes error={}", + __func__, signing_bytes.error().message() ); return; } if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signature verification failed proposer_id={}", __func__, - proposal.proposer_id() ); + ConsensusManagerLogger()->error( + "{}: HandleProposal rejected: signature verification failed proposer_id={}", + __func__, + proposal.proposer_id() ); return; } if ( proposal_validator_ && !proposal_validator_( proposal ) ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: proposal validator failed proposal_id={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: proposal validator failed proposal_id={}", + __func__, proposal.proposal_id() ); return; } @@ -601,12 +654,14 @@ namespace sgns::blockchain if ( vote_result.has_value() ) { (void)SubmitVote( vote_result.value() ); - ConsensusManagerLogger()->debug( "{}: HandleProposal self-vote submitted proposal_id={}", __func__, + ConsensusManagerLogger()->debug( "{}: HandleProposal self-vote submitted proposal_id={}", + __func__, proposal.proposal_id() ); } else { - ConsensusManagerLogger()->error( "{}: HandleProposal self-vote failed proposal_id={} error={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleProposal self-vote failed proposal_id={} error={}", + __func__, proposal.proposal_id(), vote_result.error().message() ); } @@ -615,7 +670,8 @@ namespace sgns::blockchain void ConsensusManager::HandleVote( const Vote &vote ) { - ConsensusManagerLogger()->trace( "{}: HandleVote called proposal_id={} voter_id={}", __func__, + ConsensusManagerLogger()->trace( "{}: HandleVote called proposal_id={} voter_id={}", + __func__, vote.proposal_id(), vote.voter_id() ); if ( vote_handler_ ) @@ -635,7 +691,8 @@ namespace sgns::blockchain auto it = proposals_.find( vote.proposal_id() ); if ( it == proposals_.end() ) { - ConsensusManagerLogger()->error( "{}: HandleVote ignored: proposal not found proposal_id={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleVote ignored: proposal not found proposal_id={}", + __func__, vote.proposal_id() ); return; } @@ -645,7 +702,8 @@ namespace sgns::blockchain auto slot_it = slot_states_.find( state.slot_key ); if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id != vote.proposal_id() ) { - ConsensusManagerLogger()->error( "{}: HandleVote ignored: not best proposal proposal_id={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleVote ignored: not best proposal proposal_id={}", + __func__, vote.proposal_id() ); return; } @@ -656,7 +714,8 @@ namespace sgns::blockchain { if ( tally_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleVote aborted: tally error={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleVote aborted: tally error={}", + __func__, tally_result.error().message() ); } return; @@ -664,7 +723,8 @@ namespace sgns::blockchain if ( state.certificate.has_value() ) { - ConsensusManagerLogger()->debug( "{}: HandleVote skipped: certificate already present proposal_id={}", __func__, + ConsensusManagerLogger()->debug( "{}: HandleVote skipped: certificate already present proposal_id={}", + __func__, vote.proposal_id() ); return; } @@ -672,7 +732,8 @@ namespace sgns::blockchain auto certificate_result = CreateCertificate( state.proposal, state.votes ); if ( certificate_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleVote failed: certificate creation error={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleVote failed: certificate creation error={}", + __func__, certificate_result.error().message() ); return; } @@ -688,12 +749,15 @@ namespace sgns::blockchain (void)SubmitCertificate( certificate_result.value() ); NotifyCertificate( state.proposal, certificate_result.value() ); - ConsensusManagerLogger()->debug( "{}: HandleVote certificate submitted proposal_id={}", __func__, vote.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: HandleVote certificate submitted proposal_id={}", + __func__, + vote.proposal_id() ); } void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) { - ConsensusManagerLogger()->trace( "{}: HandleVoteBundle called proposal_id={} votes={}", __func__, + ConsensusManagerLogger()->trace( "{}: HandleVoteBundle called proposal_id={} votes={}", + __func__, bundle.proposal_id(), bundle.votes_size() ); if ( vote_bundle_handler_ ) @@ -709,7 +773,9 @@ namespace sgns::blockchain void ConsensusManager::HandleCertificate( const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: HandleCertificate called proposal_id={}", __func__, certificate.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: HandleCertificate called proposal_id={}", + __func__, + certificate.proposal_id() ); if ( certificate_handler_ ) { certificate_handler_( certificate ); @@ -730,23 +796,27 @@ namespace sgns::blockchain if ( proposal.proposal_id() != certificate.proposal_id() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: proposal_id mismatch cert={} proposal={}", __func__, - certificate.proposal_id(), - proposal.proposal_id() ); + ConsensusManagerLogger()->error( + "{}: HandleCertificate rejected: proposal_id mismatch cert={} proposal={}", + __func__, + certificate.proposal_id(), + proposal.proposal_id() ); return; } if ( proposal.registry_cid() != certificate.registry_cid() || proposal.registry_epoch() != certificate.registry_epoch() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: registry mismatch proposal_id={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: registry mismatch proposal_id={}", + __func__, certificate.proposal_id() ); return; } if ( !ValidateSubject( proposal.subject() ) ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: invalid subject proposal_id={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: invalid subject proposal_id={}", + __func__, proposal.proposal_id() ); return; } @@ -754,7 +824,8 @@ namespace sgns::blockchain auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: signing bytes error={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: signing bytes error={}", + __func__, signing_bytes.error().message() ); return; } @@ -762,17 +833,26 @@ namespace sgns::blockchain proposal.signature(), signing_bytes.value() ) ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: signature verification failed proposer_id={}", __func__, - proposal.proposer_id() ); + ConsensusManagerLogger()->error( + "{}: HandleCertificate rejected: signature verification failed proposer_id={}", + __func__, + proposal.proposer_id() ); return; } const auto computed_id = CreateProposalId( proposal ); - if ( computed_id.empty() || computed_id != certificate.proposal_id() ) + if ( computed_id.empty() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: computed_id mismatch cert={} computed={}", __func__, - certificate.proposal_id(), - computed_id ); + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: computed_id empty", __func__ ); + return; + } + if ( computed_id != certificate.proposal_id() ) + { + ConsensusManagerLogger()->error( + "{}: HandleCertificate rejected: computed_id mismatch cert={} computed={}", + __func__, + certificate.proposal_id(), + computed_id ); return; } @@ -785,8 +865,10 @@ namespace sgns::blockchain { if ( it->second.certificate.has_value() ) { - ConsensusManagerLogger()->debug( "{}: HandleCertificate skipped: already have certificate proposal_id={}", __func__, - certificate.proposal_id() ); + ConsensusManagerLogger()->debug( + "{}: HandleCertificate skipped: already have certificate proposal_id={}", + __func__, + certificate.proposal_id() ); return; } state = it->second; @@ -813,7 +895,8 @@ namespace sgns::blockchain } else { - ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: missing proposal proposal_id={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: missing proposal proposal_id={}", + __func__, certificate.proposal_id() ); return; } @@ -821,7 +904,8 @@ namespace sgns::blockchain 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( "{}: HandleCertificate rejected: not best proposal proposal_id={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: not best proposal proposal_id={}", + __func__, certificate.proposal_id() ); return; } @@ -831,7 +915,9 @@ namespace sgns::blockchain votes.reserve( static_cast( certificate.votes_size() ) ); for ( const auto &vote : certificate.votes() ) { - ConsensusManagerLogger()->trace( "{}: HandleCertificate processing vote voter_id={}", __func__, vote.voter_id() ); + ConsensusManagerLogger()->trace( "{}: HandleCertificate processing vote voter_id={}", + __func__, + vote.voter_id() ); votes.push_back( vote ); } @@ -840,7 +926,8 @@ namespace sgns::blockchain { if ( tally_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: tally error={}", __func__, + ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: tally error={}", + __func__, tally_result.error().message() ); } return; @@ -856,12 +943,16 @@ namespace sgns::blockchain } NotifyCertificate( proposal, certificate ); - ConsensusManagerLogger()->debug( "{}: HandleCertificate success proposal_id={}", __func__, certificate.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: HandleCertificate success proposal_id={}", + __func__, + certificate.proposal_id() ); } void ConsensusManager::NotifyCertificate( const Proposal &proposal, const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: NotifyCertificate called proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: NotifyCertificate called proposal_id={}", + __func__, + proposal.proposal_id() ); if ( certificate_callback_ ) { certificate_callback_( proposal, certificate ); @@ -884,7 +975,8 @@ namespace sgns::blockchain bool ConsensusManager::IsBetterProposal( const Proposal &candidate, const Proposal ¤t ) const { - ConsensusManagerLogger()->trace( "{}: IsBetterProposal called candidate={} current={}", __func__, + ConsensusManagerLogger()->trace( "{}: IsBetterProposal called candidate={} current={}", + __func__, candidate.proposal_id(), current.proposal_id() ); const bool candidate_nonce = candidate.subject().type() == SubjectType::SUBJECT_NONCE && @@ -910,7 +1002,9 @@ namespace sgns::blockchain outcome::result ConsensusManager::ComputeSubjectId( const Subject &subject ) { - ConsensusManagerLogger()->trace( "{}: ComputeSubjectId called subject_type={}", __func__, static_cast( subject.type() ) ); + ConsensusManagerLogger()->trace( "{}: ComputeSubjectId called subject_type={}", + __func__, + static_cast( subject.type() ) ); Subject copy = subject; copy.clear_subject_id(); std::string serialized; @@ -931,7 +1025,10 @@ namespace sgns::blockchain uint64_t nonce, const std::string &tx_hash ) { - ConsensusManagerLogger()->trace( "{}: CreateNonceSubject called account_id={} nonce={}", __func__, account_id, nonce ); + ConsensusManagerLogger()->trace( "{}: CreateNonceSubject called account_id={} nonce={}", + __func__, + account_id, + nonce ); Subject subject; subject.set_type( SubjectType::SUBJECT_NONCE ); subject.set_account_id( account_id ); @@ -942,12 +1039,15 @@ namespace sgns::blockchain auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateNonceSubject failed: subject id error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateNonceSubject failed: subject id error={}", + __func__, subject_id.error().message() ); return outcome::failure( subject_id.error() ); } subject.set_subject_id( subject_id.value() ); - ConsensusManagerLogger()->debug( "{}: CreateNonceSubject success subject_id={}", __func__, subject.subject_id() ); + ConsensusManagerLogger()->debug( "{}: CreateNonceSubject success subject_id={}", + __func__, + subject.subject_id() ); return subject; } @@ -957,7 +1057,8 @@ namespace sgns::blockchain const std::string &task_result_hash, uint64_t result_epoch ) { - ConsensusManagerLogger()->trace( "{}: CreateTaskResultSubject called account_id={} result_epoch={}", __func__, + ConsensusManagerLogger()->trace( "{}: CreateTaskResultSubject called account_id={} result_epoch={}", + __func__, account_id, result_epoch ); Subject subject; @@ -971,22 +1072,31 @@ namespace sgns::blockchain auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateTaskResultSubject failed: subject id error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateTaskResultSubject failed: subject id error={}", + __func__, subject_id.error().message() ); return outcome::failure( subject_id.error() ); } subject.set_subject_id( subject_id.value() ); - ConsensusManagerLogger()->debug( "{}: CreateTaskResultSubject success subject_id={}", __func__, subject.subject_id() ); + ConsensusManagerLogger()->debug( "{}: CreateTaskResultSubject success subject_id={}", + __func__, + subject.subject_id() ); return subject; } std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: CreateProposalId called proposal_id={}", __func__, proposal.proposal_id() ); - auto signing_bytes = ProposalSigningBytes( proposal ); + ConsensusManagerLogger()->trace( "{}: CreateProposalId called proposal_id={}", + __func__, + proposal.proposal_id() ); + // 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( "{}: CreateProposalId failed: signing bytes error={}", __func__, + ConsensusManagerLogger()->error( "{}: CreateProposalId failed: signing bytes error={}", + __func__, signing_bytes.error().message() ); return {}; } @@ -1000,7 +1110,9 @@ namespace sgns::blockchain bool ConsensusManager::ValidateSubject( const Subject &subject ) { - ConsensusManagerLogger()->trace( "{}: ValidateSubject called subject_type={}", __func__, static_cast( subject.type() ) ); + ConsensusManagerLogger()->trace( "{}: ValidateSubject called subject_type={}", + __func__, + static_cast( subject.type() ) ); if ( subject.account_id().empty() ) { return false; From 8ba4c55261a3ab62d61c0d2b90491ee75535009c Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 5 Feb 2026 11:40:42 -0300 Subject: [PATCH 014/114] WIP: Fixing the handling of proposal --- src/blockchain/Consensus.cpp | 89 +++++++++++++++++++++++++++--------- src/blockchain/Consensus.hpp | 6 +++ 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 135a692d7..e3887da19 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -122,6 +122,31 @@ namespace sgns::blockchain certificate_handler_ = std::move( handler ); } + void ConsensusManager::ConfigureTimestampWindow( std::chrono::milliseconds window ) + { + if ( window.count() <= 0 ) + { + ConsensusManagerLogger()->warn( "{}: ConfigureTimestampWindow using default window", __func__ ); + timestamp_window_ = DEFAULT_TIMESTAMP_WINDOW; + return; + } + timestamp_window_ = window; + } + + 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 ); + } + outcome::result ConsensusManager::CreateProposal( const Subject &subject, const std::string &proposer_id, const std::string ®istry_cid, @@ -551,19 +576,47 @@ namespace sgns::blockchain void ConsensusManager::HandleProposal( const Proposal &proposal ) { ConsensusManagerLogger()->trace( "{}: HandleProposal called proposal_id={}", __func__, proposal.proposal_id() ); - if ( proposal_handler_ ) + + auto signing_bytes = ProposalSigningBytes( proposal ); + if ( signing_bytes.has_error() ) { - proposal_handler_( proposal ); + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signing bytes error={}", + __func__, + signing_bytes.error().message() ); + return; + } + if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) + { + ConsensusManagerLogger()->error( + "{}: HandleProposal rejected: signature verification failed proposer_id={}", + __func__, + proposal.proposer_id() ); + return; + } + + if ( !IsTimestampSane( proposal.timestamp() ) ) + { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: timestamp out of bounds proposal_id={}", + __func__, + proposal.proposal_id() ); + return; } if ( !registry_ ) { - ConsensusManagerLogger()->error( "{}: HandleProposal aborted: registry is null", __func__ ); + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry is null", __func__ ); return; } const auto registry_cid = registry_->GetRegistryCid(); - if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) + if ( proposal.registry_cid().empty() || registry_cid.empty() ) + { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry cid missing proposal_id={}", + __func__, + proposal.proposal_id() ); + return; + } + if ( proposal.registry_cid() != registry_cid ) { ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry cid mismatch proposal={} registry={}", @@ -582,23 +635,6 @@ namespace sgns::blockchain return; } - auto signing_bytes = ProposalSigningBytes( proposal ); - if ( signing_bytes.has_error() ) - { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signing bytes error={}", - __func__, - signing_bytes.error().message() ); - return; - } - if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) - { - ConsensusManagerLogger()->error( - "{}: HandleProposal rejected: signature verification failed proposer_id={}", - __func__, - proposal.proposer_id() ); - return; - } - if ( proposal_validator_ && !proposal_validator_( proposal ) ) { ConsensusManagerLogger()->error( "{}: HandleProposal rejected: proposal validator failed proposal_id={}", @@ -666,6 +702,17 @@ namespace sgns::blockchain vote_result.error().message() ); } } + + if ( proposal_handler_ ) + { + proposal_handler_( proposal ); + } + + if ( !registry_ ) + { + ConsensusManagerLogger()->error( "{}: HandleProposal aborted: registry is null", __func__ ); + return; + } } void ConsensusManager::HandleVote( const Vote &vote ) diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 6645dd70f..3fbe83815 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -101,6 +101,9 @@ namespace sgns::blockchain outcome::result SubmitVote( const Vote &vote ); outcome::result SubmitCertificate( const Certificate &certificate ); + protected: + void ConfigureTimestampWindow( std::chrono::milliseconds window ); + private: explicit ConsensusManager( std::shared_ptr registry, std::shared_ptr pubsub, @@ -108,6 +111,7 @@ namespace sgns::blockchain std::string consensus_topic ); static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; + static constexpr std::chrono::milliseconds DEFAULT_TIMESTAMP_WINDOW = std::chrono::minutes( 5 ); struct ProposalState { @@ -131,6 +135,7 @@ namespace sgns::blockchain void NotifyCertificate( const Proposal &proposal, 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; static std::string CreateProposalId( const Proposal &proposal ); static bool ValidateSubject( const Subject &subject ); @@ -155,5 +160,6 @@ namespace sgns::blockchain std::string consensus_topic_; std::shared_future> consensus_subs_future_; + std::chrono::milliseconds timestamp_window_{ DEFAULT_TIMESTAMP_WINDOW }; }; } From 3b2ca19ea2519e9b2dbf55a3ddd3187730b3eb1f Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 5 Feb 2026 14:29:21 -0300 Subject: [PATCH 015/114] Fix: Handling of the proposal with callback support --- src/blockchain/Consensus.cpp | 238 ++++++++++++++++++++++++++--------- src/blockchain/Consensus.hpp | 74 ++++++----- 2 files changed, 227 insertions(+), 85 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index e3887da19..b7456bc59 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -102,11 +102,6 @@ namespace sgns::blockchain return outcome::success(); } - void ConsensusManager::SetProposalHandler( ProposalHandler handler ) - { - proposal_handler_ = std::move( handler ); - } - void ConsensusManager::SetVoteHandler( VoteHandler handler ) { vote_handler_ = std::move( handler ); @@ -122,11 +117,31 @@ namespace sgns::blockchain certificate_handler_ = std::move( handler ); } + void ConsensusManager::RegisterSubjectHandler( SubjectType type, SubjectHandler handler ) + { + if ( !handler ) + { + ConsensusManagerLogger()->warn( "{}: ignored empty handler type={}", __func__, static_cast( type ) ); + return; + } + std::unique_lock lock( subject_handlers_mutex_ ); + subject_handlers_[static_cast( type )] = std::move( handler ); + } + + 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 ) ); + } + void ConsensusManager::ConfigureTimestampWindow( std::chrono::milliseconds window ) { if ( window.count() <= 0 ) { - ConsensusManagerLogger()->warn( "{}: ConfigureTimestampWindow using default window", __func__ ); + ConsensusManagerLogger()->warn( "{}: using default window", __func__ ); timestamp_window_ = DEFAULT_TIMESTAMP_WINDOW; return; } @@ -147,6 +162,123 @@ namespace sgns::blockchain return ( ts_ms >= now_ms - window_ms ) && ( ts_ms <= now_ms + window_ms ); } + outcome::result ConsensusManager::GetSubjectHash( const Subject &subject ) const + { + 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(); + } + return outcome::failure( std::errc::invalid_argument ); + } + + void ConsensusManager::ContinueProposalAfterSubject( const Proposal &proposal ) + { + const auto slot_key = GetSlotKey( proposal ); + bool should_vote = false; + { + std::lock_guard lock( proposals_mutex_ ); + if ( proposals_.find( proposal.proposal_id() ) == proposals_.end() ) + { + 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() ) + { + 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; + if ( IsBetterProposal( proposal, current ) ) + { + 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 ) + { + slot_state.voted = true; + should_vote = true; + } + } + + if ( should_vote && signer_ && !account_address_.empty() ) + { + auto vote_result = CreateVote( proposal.proposal_id(), account_address_, true, signer_ ); + if ( vote_result.has_value() ) + { + (void)SubmitVote( vote_result.value() ); + ConsensusManagerLogger()->debug( "{}: HandleProposal self-vote submitted proposal_id={}", + __func__, + proposal.proposal_id() ); + } + else + { + ConsensusManagerLogger()->error( "{}: HandleProposal self-vote failed proposal_id={} error={}", + __func__, + proposal.proposal_id(), + 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() ) + { + return; + } + 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() ) + { + 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 ); + } + } + pending_by_subject_hash_.erase( it ); + return result; + } + outcome::result ConsensusManager::CreateProposal( const Subject &subject, const std::string &proposer_id, const std::string ®istry_cid, @@ -643,76 +775,68 @@ namespace sgns::blockchain return; } - const auto slot_key = GetSlotKey( proposal ); - bool should_vote = false; + SubjectHandler subject_handler; { - std::lock_guard lock( proposals_mutex_ ); - if ( proposals_.find( proposal.proposal_id() ) == proposals_.end() ) + std::shared_lock lock( subject_handlers_mutex_ ); + auto handler_it = subject_handlers_.find( static_cast( proposal.subject().type() ) ); + if ( handler_it == subject_handlers_.end() ) { - ProposalState state; - state.proposal = proposal; - state.slot_key = slot_key; - proposals_.emplace( proposal.proposal_id(), std::move( state ) ); + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject handler missing type={}", + __func__, + static_cast( proposal.subject().type() ) ); + return; } + subject_handler = handler_it->second; + } - auto &slot_state = slot_states_[slot_key]; - if ( slot_state.best_proposal_id.empty() ) - { - 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; - if ( IsBetterProposal( proposal, current ) ) - { - slot_state.best_proposal_id = proposal.proposal_id(); - if ( proposal.subject().has_nonce() ) - { - slot_state.best_tx_hash = proposal.subject().nonce().tx_hash(); - } - } - } + auto subject_result = subject_handler( proposal.subject() ); + if ( subject_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject handler error proposal_id={}", + __func__, + proposal.proposal_id() ); + return; + } - if ( slot_state.best_proposal_id == proposal.proposal_id() && !slot_state.voted ) - { - slot_state.voted = true; - should_vote = true; - } + if ( subject_result.value() == SubjectCheck::Reject ) + { + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject check failed proposal_id={}", + __func__, + proposal.proposal_id() ); + return; } - if ( should_vote && signer_ && !account_address_.empty() ) + if ( subject_result.value() == SubjectCheck::Pending ) { - auto vote_result = CreateVote( proposal.proposal_id(), account_address_, true, signer_ ); - if ( vote_result.has_value() ) + auto subject_hash = GetSubjectHash( proposal.subject() ); + if ( subject_hash.has_error() ) { - (void)SubmitVote( vote_result.value() ); - ConsensusManagerLogger()->debug( "{}: HandleProposal self-vote submitted proposal_id={}", + ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject hash missing proposal_id={}", __func__, proposal.proposal_id() ); + return; } - else - { - ConsensusManagerLogger()->error( "{}: HandleProposal self-vote failed proposal_id={} error={}", - __func__, - proposal.proposal_id(), - vote_result.error().message() ); - } + AddPendingProposal( proposal, subject_hash.value() ); + return; } - if ( proposal_handler_ ) + ContinueProposalAfterSubject( proposal ); + } + + outcome::result ConsensusManager::ResumeProposalHandling( const std::string &subject_hash ) + { + if ( subject_hash.empty() ) { - proposal_handler_( proposal ); + return outcome::failure( std::errc::invalid_argument ); } - if ( !registry_ ) + auto to_process = TakePendingProposals( subject_hash ); + + for ( const auto &proposal : to_process ) { - ConsensusManagerLogger()->error( "{}: HandleProposal aborted: registry is null", __func__ ); - return; + ContinueProposalAfterSubject( proposal ); } + return outcome::success(); } void ConsensusManager::HandleVote( const Vote &vote ) diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 3fbe83815..5999492f6 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,13 @@ namespace sgns::blockchain using CertificateHandler = std::function; using CertificateCallback = std::function; using ProposalValidator = std::function; + enum class SubjectCheck + { + Approve, + Reject, + Pending + }; + using SubjectHandler = std::function( const Subject &subject )>; struct QuorumTally { @@ -53,6 +61,8 @@ namespace sgns::blockchain std::string consensus_topic = "" ); void SetProposalValidator( ProposalValidator validator ); + void RegisterSubjectHandler( SubjectType type, SubjectHandler handler ); + void UnregisterSubjectHandler( SubjectType type ); void SetCertificateCallback( CertificateCallback callback ); outcome::result Publish( const ConsensusMessage &message ); @@ -90,16 +100,17 @@ namespace sgns::blockchain 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, + static outcome::result CreateNonceSubject( const std::string &account_id, + uint64_t nonce, const std::string &tx_hash ); - static outcome::result CreateTaskResultSubject( const std::string &account_id, - const std::string &escrow_path, + static outcome::result CreateTaskResultSubject( const std::string &account_id, + const std::string &escrow_path, const std::string &task_result_hash, - uint64_t result_epoch ); + uint64_t result_epoch ); outcome::result SubmitProposal( const Proposal &proposal, bool self_vote = true ); outcome::result SubmitVote( const Vote &vote ); outcome::result SubmitCertificate( const Certificate &certificate ); + outcome::result ResumeProposalHandling( const std::string &subject_hash ); protected: void ConfigureTimestampWindow( std::chrono::milliseconds window ); @@ -110,7 +121,7 @@ namespace sgns::blockchain Signer signer, std::string consensus_topic ); - static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; + static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; static constexpr std::chrono::milliseconds DEFAULT_TIMESTAMP_WINDOW = std::chrono::minutes( 5 ); struct ProposalState @@ -128,14 +139,18 @@ namespace sgns::blockchain bool voted = false; }; - void HandleProposal( const Proposal &proposal ); - void HandleVote( const Vote &vote ); - void HandleVoteBundle( const VoteBundle &bundle ); - void HandleCertificate( const Certificate &certificate ); - void NotifyCertificate( const Proposal &proposal, 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; + void HandleProposal( const Proposal &proposal ); + void HandleVote( const Vote &vote ); + void HandleVoteBundle( const VoteBundle &bundle ); + void HandleCertificate( const Certificate &certificate ); + void NotifyCertificate( const Proposal &proposal, 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; + outcome::result GetSubjectHash( const Subject &subject ) const; + void ContinueProposalAfterSubject( const Proposal &proposal ); + void AddPendingProposal( const Proposal &proposal, const std::string &subject_hash ); + std::vector TakePendingProposals( const std::string &subject_hash ); static std::string CreateProposalId( const Proposal &proposal ); static bool ValidateSubject( const Subject &subject ); @@ -144,22 +159,25 @@ namespace sgns::blockchain void OnConsensusMessage( boost::optional message ); - std::shared_ptr registry_; - ProposalHandler proposal_handler_; - VoteHandler vote_handler_; - VoteBundleHandler vote_bundle_handler_; - CertificateHandler certificate_handler_; - CertificateCallback certificate_callback_; - ProposalValidator proposal_validator_; - Signer signer_; - std::string account_address_; - std::unordered_map proposals_; - std::unordered_map slot_states_; - mutable std::mutex proposals_mutex_; - std::shared_ptr pubsub_; + std::shared_ptr registry_; + VoteHandler vote_handler_; + VoteBundleHandler vote_bundle_handler_; + CertificateHandler certificate_handler_; + CertificateCallback certificate_callback_; + ProposalValidator proposal_validator_; + std::unordered_map subject_handlers_; + mutable std::shared_mutex subject_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_; + mutable std::mutex proposals_mutex_; + std::shared_ptr pubsub_; std::string consensus_topic_; std::shared_future> consensus_subs_future_; - std::chrono::milliseconds timestamp_window_{ DEFAULT_TIMESTAMP_WINDOW }; + std::chrono::milliseconds timestamp_window_{ DEFAULT_TIMESTAMP_WINDOW }; }; } From c626d7602f1ee0de81f9d0ef002e9b0ea0b169d6 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 5 Feb 2026 15:33:36 -0300 Subject: [PATCH 016/114] Chore: Fixing namespace of consensus and validator registry --- src/account/Migration3_5_1To3_6_0.cpp | 2 +- src/account/TransactionManager.cpp | 8 +-- src/account/TransactionManager.hpp | 6 +- src/blockchain/Blockchain.hpp | 51 ++++++++-------- src/blockchain/Consensus.cpp | 7 ++- src/blockchain/Consensus.hpp | 4 +- src/blockchain/ValidatorRegistry.cpp | 2 +- src/blockchain/ValidatorRegistry.hpp | 2 +- src/blockchain/impl/Blockchain.cpp | 61 +++++++++++-------- src/blockchain/impl/proto/Consensus.proto | 2 +- src/blockchain/impl/proto/SGBlockchain.proto | 2 +- .../impl/proto/ValidatorRegistry.proto | 2 +- src/crdt/crdt_datastore.hpp | 8 +-- .../blockchain/consensus_certificate_test.cpp | 16 ++--- 14 files changed, 89 insertions(+), 84 deletions(-) diff --git a/src/account/Migration3_5_1To3_6_0.cpp b/src/account/Migration3_5_1To3_6_0.cpp index 06f55e347..d20e62d09 100644 --- a/src/account/Migration3_5_1To3_6_0.cpp +++ b/src/account/Migration3_5_1To3_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/TransactionManager.cpp b/src/account/TransactionManager.cpp index c0c1540ce..9998cd2f9 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -50,8 +50,8 @@ namespace sgns instance->blockchain_->SetCertificateCallback( [weak_ptr( std::weak_ptr( instance ) )]( - const blockchain::ConsensusProposal &proposal, - const blockchain::ConsensusCertificate &certificate ) + const ConsensusProposal &proposal, + const ConsensusCertificate &certificate ) { if ( auto strong = weak_ptr.lock() ) { @@ -3001,8 +3001,8 @@ namespace sgns return outcome::failure( std::errc::no_such_file_or_directory ); } - void TransactionManager::OnConsensusCertificate( const blockchain::ConsensusProposal &proposal, - const blockchain::ConsensusCertificate &certificate ) + void TransactionManager::OnConsensusCertificate( const ConsensusProposal &proposal, + const ConsensusCertificate &certificate ) { } } diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 5236f8d3e..80f9a655e 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -250,8 +250,8 @@ namespace sgns bool SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ); - void OnConsensusCertificate( const blockchain::ConsensusProposal &proposal, - const blockchain::ConsensusCertificate &certificate ); + void OnConsensusCertificate( const ConsensusProposal &proposal, + const ConsensusCertificate &certificate ); std::shared_ptr globaldb_m; @@ -292,7 +292,7 @@ namespace sgns mutable std::shared_mutex tx_mutex_m; std::unordered_map tx_processed_m; std::atomic verifying_count_{ 0 }; // Count of VERIFYING transactions - std::unordered_map pending_proposals_; + std::unordered_map pending_proposals_; std::function task_m; std::atomic stopped_{ false }; std::chrono::milliseconds timestamp_tolerance_m; diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index b47d4576d..9091eb45d 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -27,10 +27,7 @@ namespace sgns { - namespace blockchain - { - class ValidatorRegistry; - } + class ValidatorRegistry; class Migration3_5_1To3_6_0; @@ -103,20 +100,22 @@ namespace sgns */ static const std::string &GetAuthorizedFullNodeAddress(); - outcome::result GetGenesisCID() const; - outcome::result GetAccountCreationCID() const; - std::shared_ptr GetValidatorRegistry() const; + outcome::result GetGenesisCID() const; + outcome::result GetAccountCreationCID() const; + std::shared_ptr GetValidatorRegistry() const; void SetFullNodeMode(); - void SetCertificateCallback( blockchain::ConsensusManager::CertificateCallback callback ); + bool RegisterSubjectHandler( SubjectType type, ConsensusManager::SubjectHandler handler ); + void UnregisterSubjectHandler( SubjectType type ); + + void SetCertificateCallback( ConsensusManager::CertificateCallback callback ); - outcome::result CreateConsensusProposal( - const std::string &account_id, - uint64_t nonce, - const std::string &tx_hash ); + outcome::result CreateConsensusProposal( const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash ); - outcome::result SubmitProposal( const blockchain::ConsensusManager::Proposal &proposal ); + outcome::result SubmitProposal( const ConsensusManager::Proposal &proposal ); protected: friend class Migration3_5_1To3_6_0; @@ -135,10 +134,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 ); @@ -148,11 +147,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; void GenesisReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair &new_data, const std::string &cid ); void AccountCreationReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair& new_data, const std::string &cid ); @@ -177,9 +174,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 { @@ -211,7 +208,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 @@ -222,7 +219,7 @@ namespace sgns bool genesis_ready_ = false; bool account_creation_ready_ = false; - std::shared_ptr consensus_manager_; + std::shared_ptr consensus_manager_; }; } diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index b7456bc59..9d57fe2b0 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -18,7 +18,7 @@ #include "crypto/hasher/hasher_impl.hpp" #include "account/GeniusAccount.hpp" -namespace sgns::blockchain +namespace sgns { base::Logger ConsensusManagerLogger() @@ -117,15 +117,16 @@ namespace sgns::blockchain certificate_handler_ = std::move( handler ); } - void ConsensusManager::RegisterSubjectHandler( SubjectType type, SubjectHandler handler ) + bool ConsensusManager::RegisterSubjectHandler( SubjectType type, SubjectHandler handler ) { if ( !handler ) { ConsensusManagerLogger()->warn( "{}: ignored empty handler type={}", __func__, static_cast( type ) ); - return; + return false; } std::unique_lock lock( subject_handlers_mutex_ ); subject_handlers_[static_cast( type )] = std::move( handler ); + return true; } void ConsensusManager::UnregisterSubjectHandler( SubjectType type ) diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 5999492f6..8024a4302 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -22,7 +22,7 @@ #include "ipfs_pubsub/gossip_pubsub.hpp" #include "outcome/outcome.hpp" -namespace sgns::blockchain +namespace sgns { class ConsensusManager : public std::enable_shared_from_this { @@ -61,7 +61,7 @@ namespace sgns::blockchain std::string consensus_topic = "" ); void SetProposalValidator( ProposalValidator validator ); - void RegisterSubjectHandler( SubjectType type, SubjectHandler handler ); + bool RegisterSubjectHandler( SubjectType type, SubjectHandler handler ); void UnregisterSubjectHandler( SubjectType type ); void SetCertificateCallback( CertificateCallback callback ); diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index cf3c4f288..25f904092 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -18,7 +18,7 @@ #include "blockchain/impl/proto/ValidatorRegistry.pb.h" #include "crdt/graphsync_dagsyncer.hpp" -namespace sgns::blockchain +namespace sgns { namespace { diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index 38bf9c418..b950d7506 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -30,7 +30,7 @@ namespace sgns class Migration3_5_1To3_6_0; } -namespace sgns::blockchain +namespace sgns { class ValidatorRegistry : public std::enable_shared_from_this { diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index a0e011f31..006283ba4 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -98,11 +98,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,7 +133,7 @@ namespace sgns return nullptr; } - instance->consensus_manager_ = blockchain::ConsensusManager::New( + instance->consensus_manager_ = ConsensusManager::New( instance->validator_registry_, std::move( pubsub ), [weak_ptr( std::weak_ptr( instance ) )]( @@ -826,7 +826,7 @@ namespace sgns account_->GetAddress().substr( 0, 8 ), GetAuthorizedFullNodeAddress().substr( 0, 8 ) ); - sgns::blockchain::GenesisBlock g; + GenesisBlock g; auto timestamp = std::chrono::system_clock::now(); g.set_chain_id( "supergenius" ); @@ -920,7 +920,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() ); @@ -958,12 +958,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 @@ -977,10 +977,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(); @@ -990,7 +990,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 ) ); @@ -1024,7 +1024,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 ) ); @@ -1072,7 +1072,7 @@ namespace sgns account_->GetAddress().substr( 0, 8 ), cids_.genesis_.value() ); - sgns::blockchain::AccountCreationBlock ac; + AccountCreationBlock ac; auto timestamp = std::chrono::system_clock::now(); ac.set_account_address( account_->GetAddress() ); @@ -1143,7 +1143,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() ); @@ -1194,7 +1194,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() ) ) ) { @@ -1228,7 +1228,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() ) ) ) { @@ -1276,7 +1276,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() ) ) ) { @@ -1330,7 +1330,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() ) ) ) { @@ -1380,8 +1380,8 @@ 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() ) { @@ -1390,8 +1390,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() ) { @@ -1515,7 +1515,7 @@ namespace sgns return it->second; } - std::shared_ptr Blockchain::GetValidatorRegistry() const + std::shared_ptr Blockchain::GetValidatorRegistry() const { return validator_registry_; } @@ -1526,12 +1526,23 @@ namespace sgns std::string( BLOCKCHAIN_TOPIC ) ); //This will not trigger the broadcaster, but it will grab links on CRDT } - void Blockchain::SetCertificateCallback( blockchain::ConsensusManager::CertificateCallback callback ) + 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 ); + } + + void Blockchain::SetCertificateCallback( ConsensusManager::CertificateCallback callback ) { consensus_manager_->SetCertificateCallback( std::move( callback ) ); } - outcome::result Blockchain::CreateConsensusProposal( + outcome::result Blockchain::CreateConsensusProposal( const std::string &account_id, uint64_t nonce, const std::string &tx_hash ) @@ -1546,7 +1557,7 @@ namespace sgns return nonce_proposal; } - outcome::result Blockchain::SubmitProposal( const blockchain::ConsensusManager::Proposal &proposal ) + outcome::result Blockchain::SubmitProposal( const ConsensusManager::Proposal &proposal ) { return consensus_manager_->SubmitProposal( std::move( proposal ) ); } diff --git a/src/blockchain/impl/proto/Consensus.proto b/src/blockchain/impl/proto/Consensus.proto index 93baae190..e3b1e93ca 100644 --- a/src/blockchain/impl/proto/Consensus.proto +++ b/src/blockchain/impl/proto/Consensus.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package sgns.blockchain; +package sgns; message ConsensusSubject { string subject_id = 1; 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..f9647a40a 100644 --- a/src/blockchain/impl/proto/ValidatorRegistry.proto +++ b/src/blockchain/impl/proto/ValidatorRegistry.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package sgns.blockchain.validator; +package sgns.validator; message ValidatorEntry { string validator_id = 1; diff --git a/src/crdt/crdt_datastore.hpp b/src/crdt/crdt_datastore.hpp index a2cd0a2b2..06859f0b9 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; } @@ -249,8 +245,8 @@ namespace sgns::crdt protected: friend class PubSubBroadcasterExt; friend class ::sgns::Blockchain; - friend class ::sgns::blockchain::ValidatorRegistry; - + friend class ::sgns::ValidatorRegistry; + struct RootCIDJob { std::shared_ptr node_; ///< Current node to process diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index e4549f319..042bf887c 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -20,11 +20,11 @@ namespace return account; } - std::shared_ptr MakeRegistry( + std::shared_ptr MakeRegistry( const std::shared_ptr &db, const std::shared_ptr &account ) { - using sgns::blockchain::ValidatorRegistry; + using sgns::ValidatorRegistry; auto registry = ValidatorRegistry::New( db, 1, @@ -72,13 +72,13 @@ namespace sgns::test auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = blockchain::ConsensusManager::New( registry, + auto manager = ConsensusManager::New( registry, pubs_, [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); } ); std::string tx_hash = "0x010203"; - auto subject_result = blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); ASSERT_TRUE( subject_result.has_value() ); auto proposal_result = manager->CreateProposal( subject_result.value(), @@ -107,13 +107,13 @@ namespace sgns::test auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = blockchain::ConsensusManager::New( registry, + auto manager = ConsensusManager::New( registry, pubs_, [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); } ); std::string tx_hash = "0x010203"; - auto subject_result = blockchain::ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); ASSERT_TRUE( subject_result.has_value() ); auto proposal_result = manager->CreateProposal( subject_result.value(), @@ -137,8 +137,8 @@ namespace sgns::test auto cert = cert_result.value(); bool notified = false; - manager->SetCertificateCallback( [¬ified]( const blockchain::ConsensusProposal &, - const blockchain::ConsensusCertificate & ) { notified = true; } ); + manager->SetCertificateCallback( [¬ified]( const ConsensusProposal &, + const ConsensusCertificate & ) { notified = true; } ); manager->HandleCertificate( cert ); EXPECT_TRUE( notified ); From 4226e7934653450eb14e95362d34496196f98265 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 14:13:34 -0300 Subject: [PATCH 017/114] Fix: CheckHash is now const --- src/account/EscrowReleaseTransaction.cpp | 4 ++-- src/account/EscrowReleaseTransaction.hpp | 3 ++- src/account/EscrowTransaction.cpp | 4 ++-- src/account/EscrowTransaction.hpp | 3 ++- src/account/IGeniusTransactions.cpp | 15 +++++++-------- src/account/IGeniusTransactions.hpp | 9 +++++++-- src/account/MintTransaction.cpp | 4 ++-- src/account/MintTransaction.hpp | 3 ++- src/account/ProcessingTransaction.cpp | 4 ++-- src/account/ProcessingTransaction.hpp | 3 ++- src/account/TransferTransaction.cpp | 4 ++-- src/account/TransferTransaction.hpp | 3 ++- 12 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/account/EscrowReleaseTransaction.cpp b/src/account/EscrowReleaseTransaction.cpp index 704ac4093..ec495bdb5 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 ed9aef592..24e5ced1c 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 bd99cbfa1..fbbe561bd 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 bdc46797d..5c98ac60b 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/IGeniusTransactions.cpp b/src/account/IGeniusTransactions.cpp index 9eda254d7..67d6c32dd 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(); } diff --git a/src/account/IGeniusTransactions.hpp b/src/account/IGeniusTransactions.hpp index 4820b45ea..3a006f10c 100644 --- a/src/account/IGeniusTransactions.hpp +++ b/src/account/IGeniusTransactions.hpp @@ -55,7 +55,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 ); + } virtual std::string GetTransactionSpecificPath() const = 0; @@ -89,7 +94,7 @@ namespace sgns virtual std::unordered_set GetTopics() const; void FillHash(); - bool CheckHash(); + bool CheckHash() const; std::vector MakeSignature( GeniusAccount &account ); bool CheckSignature(); diff --git a/src/account/MintTransaction.cpp b/src/account/MintTransaction.cpp index bebd2dcf7..15b1cdbfc 100644 --- a/src/account/MintTransaction.cpp +++ b/src/account/MintTransaction.cpp @@ -21,10 +21,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() ); diff --git a/src/account/MintTransaction.hpp b/src/account/MintTransaction.hpp index e5c7ab614..348d19024 100644 --- a/src/account/MintTransaction.hpp +++ b/src/account/MintTransaction.hpp @@ -27,7 +27,8 @@ 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; diff --git a/src/account/ProcessingTransaction.cpp b/src/account/ProcessingTransaction.cpp index 5ede92fae..637b9c0ed 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/TransferTransaction.cpp b/src/account/TransferTransaction.cpp index c50a5d1c9..ac9e6b84d 100644 --- a/src/account/TransferTransaction.cpp +++ b/src/account/TransferTransaction.cpp @@ -30,10 +30,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 273f3d577..30286511c 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. From 6b77a15e6093ea203760788f7263dec9b6ac33ea Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 14:23:48 -0300 Subject: [PATCH 018/114] Fix: CheckSignature is now const --- src/account/IGeniusTransactions.cpp | 21 +++++++++++---------- src/account/IGeniusTransactions.hpp | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/account/IGeniusTransactions.cpp b/src/account/IGeniusTransactions.cpp index 67d6c32dd..5ab47b6ee 100644 --- a/src/account/IGeniusTransactions.cpp +++ b/src/account/IGeniusTransactions.cpp @@ -71,24 +71,25 @@ 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 ); - dag_st.SerializeToArray( serialized.data(), size ); - dag_st.set_signature( str_signature ); + dag_copy.SerializeToArray( serialized.data(), size ); return GeniusAccount::VerifySignature( dag_st.source_addr(), str_signature, serialized ) && CheckHash(); } diff --git a/src/account/IGeniusTransactions.hpp b/src/account/IGeniusTransactions.hpp index 3a006f10c..8e593fed1 100644 --- a/src/account/IGeniusTransactions.hpp +++ b/src/account/IGeniusTransactions.hpp @@ -97,8 +97,8 @@ namespace sgns bool CheckHash() const; std::vector MakeSignature( GeniusAccount &account ); - bool CheckSignature(); - bool CheckDAGSignatureLegacy(); + bool CheckSignature() const; + bool CheckDAGSignatureLegacy() const; SGTransaction::DAGStruct dag_st; From 2d2d577b8286e692a85784413accb40e064036dc Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 14:24:29 -0300 Subject: [PATCH 019/114] Feat: Adding checking of the subject structure --- src/blockchain/Consensus.cpp | 111 +++++++++++++++++++++++++++-------- src/blockchain/Consensus.hpp | 1 + 2 files changed, 88 insertions(+), 24 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 9d57fe2b0..ff97b3436 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -708,28 +708,27 @@ namespace sgns void ConsensusManager::HandleProposal( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: HandleProposal called proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: signing bytes error={}", + ConsensusManagerLogger()->error( "{}: rejected: signing bytes error={}", __func__, signing_bytes.error().message() ); return; } if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) { - ConsensusManagerLogger()->error( - "{}: HandleProposal rejected: signature verification failed proposer_id={}", - __func__, - proposal.proposer_id() ); + ConsensusManagerLogger()->error( "{}: rejected: signature verification failed proposer_id={}", + __func__, + proposal.proposer_id() ); return; } if ( !IsTimestampSane( proposal.timestamp() ) ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: timestamp out of bounds proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: timestamp out of bounds proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -737,40 +736,45 @@ namespace sgns if ( !registry_ ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry is null", __func__ ); + ConsensusManagerLogger()->error( "{}: rejected: registry is null", __func__ ); return; } const auto registry_cid = registry_->GetRegistryCid(); if ( proposal.registry_cid().empty() || registry_cid.empty() ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: registry cid missing proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: registry cid missing proposal_id={}", __func__, proposal.proposal_id() ); return; } if ( proposal.registry_cid() != registry_cid ) { - ConsensusManagerLogger()->error( - "{}: HandleProposal rejected: registry cid mismatch proposal={} registry={}", - __func__, - proposal.registry_cid(), - registry_cid ); + ConsensusManagerLogger()->error( "{}: rejected: registry cid mismatch proposal={} registry={}", + __func__, + proposal.registry_cid(), + registry_cid ); return; } if ( proposal.registry_epoch() != registry_->GetRegistryEpoch() ) { - ConsensusManagerLogger()->error( - "{}: HandleProposal rejected: registry epoch mismatch proposal={} registry={}", - __func__, - proposal.registry_epoch(), - registry_->GetRegistryEpoch() ); + ConsensusManagerLogger()->error( "{}: rejected: registry epoch mismatch proposal={} registry={}", + __func__, + proposal.registry_epoch(), + registry_->GetRegistryEpoch() ); + return; + } + if ( !CheckSubject( proposal.subject() ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", + __func__, + proposal.proposal_id() ); return; } if ( proposal_validator_ && !proposal_validator_( proposal ) ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: proposal validator failed proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: proposal validator failed proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -782,7 +786,7 @@ namespace sgns auto handler_it = subject_handlers_.find( static_cast( proposal.subject().type() ) ); if ( handler_it == subject_handlers_.end() ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject handler missing type={}", + ConsensusManagerLogger()->error( "{}: rejected: subject handler missing type={}", __func__, static_cast( proposal.subject().type() ) ); return; @@ -793,7 +797,7 @@ namespace sgns auto subject_result = subject_handler( proposal.subject() ); if ( subject_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject handler error proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject handler error proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -801,7 +805,7 @@ namespace sgns if ( subject_result.value() == SubjectCheck::Reject ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject check failed proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -812,7 +816,7 @@ namespace sgns auto subject_hash = GetSubjectHash( proposal.subject() ); if ( subject_hash.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleProposal rejected: subject hash missing proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject hash missing proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -1368,4 +1372,63 @@ namespace sgns 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; + } + + if ( subject.type() != SubjectType::SUBJECT_NONCE && subject.type() != SubjectType::SUBJECT_TASK_RESULT ) + { + 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; + } + } + + return true; + } } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 8024a4302..0c25040cb 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -158,6 +158,7 @@ namespace sgns const std::string &validator_id ) const; void OnConsensusMessage( boost::optional message ); + bool CheckSubject( const Subject &subject ); std::shared_ptr registry_; VoteHandler vote_handler_; From 818d04395cbde93292a41810ac874ae8fb9e64b7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 14:30:16 -0300 Subject: [PATCH 020/114] Feat: Creating nonce subject handling --- src/account/TransactionManager.cpp | 367 +++++++++++++++++++++++++---- src/account/TransactionManager.hpp | 41 ++-- 2 files changed, 352 insertions(+), 56 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 9998cd2f9..ccee43424 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -49,15 +49,25 @@ namespace sgns mutability_window ) ); instance->blockchain_->SetCertificateCallback( - [weak_ptr( std::weak_ptr( instance ) )]( - const ConsensusProposal &proposal, - const ConsensusCertificate &certificate ) + [weak_ptr( std::weak_ptr( instance ) )]( const ConsensusProposal &proposal, + const ConsensusCertificate &certificate ) { if ( auto strong = weak_ptr.lock() ) { strong->OnConsensusCertificate( proposal, certificate ); } } ); + 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 ) @@ -176,9 +186,9 @@ namespace sgns 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/[^/]+"; + 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 ); @@ -2027,30 +2037,20 @@ namespace sgns __func__, account_m->GetAddress().substr( 0, 8 ), full_node_m, - tracked.tx->dag_st.nonce(), + 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 ); - } + 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 { @@ -2145,7 +2145,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; } @@ -2153,6 +2153,21 @@ 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; + } + TransactionManager::TransactionStatus TransactionManager::GetOutgoingStatusByTxId( const std::string &txId ) const { std::shared_lock tx_lock( tx_mutex_m ); @@ -2192,7 +2207,7 @@ namespace sgns { continue; } - if ( tracked.tx->dag_st.nonce() != nonce ) + if ( tracked.cached_nonce != nonce ) { continue; } @@ -2252,7 +2267,7 @@ namespace sgns } if ( tracked.status == TransactionStatus::VERIFYING ) { - verifying_nonces.push_back( tracked.tx->dag_st.nonce() ); + verifying_nonces.push_back( tracked.cached_nonce ); } } } @@ -2299,7 +2314,7 @@ namespace sgns { continue; } - if ( tracked.tx->dag_st.nonce() != nonce ) + if ( tracked.cached_nonce != nonce ) { continue; } @@ -2341,21 +2356,14 @@ namespace sgns } 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: {}", + 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; } std::shared_ptr conflicting_tx; @@ -2670,7 +2678,7 @@ 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 ) { @@ -3005,6 +3013,283 @@ namespace sgns const ConsensusCertificate &certificate ) { } + + outcome::result TransactionManager::HandleNonceConsensusSubject( + const ConsensusManager::Subject &subject ) + { + if ( subject.type() != SubjectType::SUBJECT_NONCE ) + { + m_logger->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_lock tx_lock( tx_mutex_m ); + auto it = tx_processed_m.find( key ); + if ( it == tx_processed_m.end() ) + { + m_logger->debug( "[{} - full: {}] {}: Transaction not found for hash {}, pending", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::SubjectCheck::Pending; + } + + auto &tracked = it->second; + if ( !tracked.tx ) + { + m_logger->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 ); + } + + if ( tracked.cached_nonce != subject.nonce().nonce() ) + { + m_logger->error( "[{} - full: {}] {}: Nonce mismatch for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::SubjectCheck::Reject; + } + + if ( !subject.account_id().empty() && tracked.tx->GetSrcAddress() != subject.account_id() ) + { + m_logger->error( "[{} - full: {}] {}: Account mismatch for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::SubjectCheck::Reject; + } + + if ( tracked.status == TransactionStatus::FAILED || tracked.status == TransactionStatus::INVALID ) + { + m_logger->error( "[{} - full: {}] {}: Transaction status invalid for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::SubjectCheck::Reject; + } + + auto validate_result = ValidateTransactionForConsensus( tracked.tx ); + + return validate_result ? ConsensusManager::SubjectCheck::Approve : ConsensusManager::SubjectCheck::Reject; + } + + bool TransactionManager::ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, + const std::string &address ) const + { + if ( params.first.empty() || params.second.empty() ) + { + return false; + } + + if ( !full_node_m && address != account_m->GetAddress() ) + { + return false; + } + + if ( !utxo_manager_.VerifyParameters( params, address ) ) + { + return false; + } + + return true; + } + + bool TransactionManager::ValidateTransactionForConsensus( const std::shared_ptr &tx ) const + { + bool ret = false; + do + { + if ( !CheckTransactionWellFormed( *tx ) ) + { + break; + } + if ( !CheckTransactionAuthorization( *tx ) ) + { + break; + } + if ( !CheckTransactionTimestamp( *tx ) ) + { + break; + } + if ( !CheckTransactionReplayProtection( *tx ) ) + { + break; + } + ret = CheckTransactionTypeRules( tx ); + } while ( 0 ); + + return ret; + } + + bool TransactionManager::CheckTransactionWellFormed( const IGeniusTransactions &tx ) const + { + if ( tx.GetHash().empty() || !tx.CheckHash() ) + { + return false; + } + + if ( tx.GetSrcAddress().empty() ) + { + return false; + } + + if ( tx.GetTimestamp() == 0 ) + { + return false; + } + + if ( transaction_parsers.find( tx.GetType() ) == transaction_parsers.end() ) + { + return false; + } + + return true; + } + + bool TransactionManager::CheckTransactionAuthorization( const IGeniusTransactions &tx ) const + { + return tx.CheckSignature() || tx.CheckDAGSignatureLegacy(); + } + + bool TransactionManager::CheckTransactionTimestamp( const IGeniusTransactions &tx ) const + { + const auto ts = tx.GetTimestamp(); + if ( ts == 0 ) + { + return false; + } + + const auto elapsed = GetElapsedTime( ts ); + if ( elapsed < 0 && timestamp_tolerance_m.count() > 0 && + ( -elapsed ) > static_cast( timestamp_tolerance_m.count() ) ) + { + return false; + } + + return true; + } + + bool TransactionManager::CheckTransactionReplayProtection( const IGeniusTransactions &tx ) const + { + auto nonce_result = account_m->GetPeerNonce( tx.GetSrcAddress() ); + if ( nonce_result.has_error() ) + { + return false; + } + + const auto confirmed_nonce = nonce_result.value(); + const auto tx_nonce = tx.dag_st.nonce(); + + if ( tx_nonce <= confirmed_nonce ) + { + return false; + } + + if ( 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() ) + { + return false; + } + if ( tracked->status == TransactionStatus::FAILED || tracked->status == TransactionStatus::INVALID ) + { + return false; + } + } + } + + return true; + } + + bool TransactionManager::CheckTransactionTypeRules( const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return false; + } + + if ( tx->GetType() == "transfer" ) + { + auto transfer_tx = std::dynamic_pointer_cast( tx ); + if ( !transfer_tx ) + { + return false; + } + return ValidateUTXOParametersForConsensus( + UTXOTxParameters{ transfer_tx->GetInputInfos(), transfer_tx->GetDstInfos() }, + transfer_tx->GetSrcAddress() ); + } + + if ( tx->GetType() == "escrow-hold" ) + { + auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return false; + } + return ValidateUTXOParametersForConsensus( escrow_tx->GetUTXOParameters(), escrow_tx->GetSrcAddress() ); + } + + if ( tx->GetType() == "escrow-release" ) + { + auto escrow_release_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_release_tx ) + { + return false; + } + return ValidateUTXOParametersForConsensus( escrow_release_tx->GetUTXOParameters(), + escrow_release_tx->GetSrcAddress() ); + } + + if ( tx->GetType() == "mint" ) + { + auto mint_tx = std::dynamic_pointer_cast( tx ); + if ( !mint_tx ) + { + return false; + } + if ( mint_tx->GetAmount() == 0 ) + { + return false; + } + return true; + } + + return true; + } + + void TransactionManager::SetNonceWindow( uint64_t window ) + { + if ( window == 0 ) + { + nonce_window_m = DEFAULT_NONCE_WINDOW; + return; + } + nonce_window_m = window; + } } diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 80f9a655e..03d212367 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -199,6 +199,13 @@ 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 + }; + TransactionManager( std::shared_ptr processing_db, std::shared_ptr ctx, UTXOManager &utxo_manager, @@ -247,11 +254,11 @@ 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; bool SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ); - void OnConsensusCertificate( const ConsensusProposal &proposal, - const ConsensusCertificate &certificate ); + void OnConsensusCertificate( const ConsensusProposal &proposal, const ConsensusCertificate &certificate ); std::shared_ptr globaldb_m; @@ -282,24 +289,19 @@ 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::unordered_map pending_proposals_; - std::function task_m; - std::atomic stopped_{ false }; - std::chrono::milliseconds timestamp_tolerance_m; - std::chrono::milliseconds mutability_window_m; + 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 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_; @@ -365,6 +367,15 @@ namespace sgns public: 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; + bool ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, const std::string &address ) const; + void SetNonceWindow( uint64_t window ); }; } From d4f3c3b1741efe24dd1541de3e365f0248c6fe4e Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 14:53:42 -0300 Subject: [PATCH 021/114] Chore: Creating logs on new methods --- src/account/TransactionManager.cpp | 259 ++++++++++++++++++++++++++--- 1 file changed, 237 insertions(+), 22 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index ccee43424..4326f97ca 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -3091,86 +3091,211 @@ namespace sgns bool TransactionManager::ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, const std::string &address ) const { + m_logger->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() ) { + m_logger->error( "[{} - full: {}] {}: Empty inputs or outputs", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } if ( !full_node_m && address != account_m->GetAddress() ) { + m_logger->error( "[{} - full: {}] {}: Non-full node cannot verify address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + address ); return false; } if ( !utxo_manager_.VerifyParameters( params, address ) ) { + m_logger->error( "[{} - full: {}] {}: VerifyParameters failed for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + address ); return false; } + m_logger->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 { - bool ret = false; - do + m_logger->debug( "[{} - full: {}] {}: Validating transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + if ( !tx ) { - if ( !CheckTransactionWellFormed( *tx ) ) - { - break; - } - if ( !CheckTransactionAuthorization( *tx ) ) - { - break; - } - if ( !CheckTransactionTimestamp( *tx ) ) - { - break; - } - if ( !CheckTransactionReplayProtection( *tx ) ) - { - break; - } - ret = CheckTransactionTypeRules( tx ); - } while ( 0 ); + m_logger->error( "[{} - full: {}] {}: Null transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + return false; + } - return ret; + if ( !CheckTransactionWellFormed( *tx ) ) + { + m_logger->error( "[{} - full: {}] {}: Well-formed check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + if ( !CheckTransactionAuthorization( *tx ) ) + { + m_logger->error( "[{} - full: {}] {}: Authorization check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + if ( !CheckTransactionTimestamp( *tx ) ) + { + m_logger->error( "[{} - full: {}] {}: Timestamp check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + if ( !CheckTransactionReplayProtection( *tx ) ) + { + m_logger->error( "[{} - full: {}] {}: Replay protection failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + if ( !CheckTransactionTypeRules( tx ) ) + { + m_logger->error( "[{} - full: {}] {}: Type rules failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return false; + } + + m_logger->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 { + m_logger->debug( "[{} - full: {}] {}: Checking well-formed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); if ( tx.GetHash().empty() || !tx.CheckHash() ) { + m_logger->error( "[{} - full: {}] {}: Hash invalid tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return false; } if ( tx.GetSrcAddress().empty() ) { + m_logger->error( "[{} - full: {}] {}: Empty source address tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return false; } if ( tx.GetTimestamp() == 0 ) { + m_logger->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() ) { + m_logger->error( "[{} - full: {}] {}: Unknown tx type {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetType() ); return false; } + m_logger->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 { - return tx.CheckSignature() || tx.CheckDAGSignatureLegacy(); + m_logger->debug( "[{} - full: {}] {}: Checking authorization tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + if ( tx.CheckSignature() || tx.CheckDAGSignatureLegacy() ) + { + m_logger->debug( "[{} - full: {}] {}: Authorization ok tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); + return true; + } + m_logger->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 { + m_logger->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 ) { + m_logger->error( "[{} - full: {}] {}: Missing timestamp tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return false; } @@ -3178,17 +3303,37 @@ namespace sgns if ( elapsed < 0 && timestamp_tolerance_m.count() > 0 && ( -elapsed ) > static_cast( timestamp_tolerance_m.count() ) ) { + m_logger->error( "[{} - full: {}] {}: Timestamp out of tolerance tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return false; } + m_logger->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 { + m_logger->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() ) { + m_logger->error( "[{} - full: {}] {}: Missing peer nonce for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetSrcAddress() ); return false; } @@ -3197,11 +3342,26 @@ namespace sgns if ( tx_nonce <= confirmed_nonce ) { + m_logger->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 ) { + m_logger->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; } @@ -3212,22 +3372,47 @@ namespace sgns auto tracked = GetTrackedTxByNonceAndAddress( n, tx.GetSrcAddress() ); if ( !tracked.has_value() ) { + m_logger->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 || tracked->status == TransactionStatus::INVALID ) { + m_logger->error( "[{} - full: {}] {}: Intermediate nonce {} invalid for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + n, + tx.GetSrcAddress() ); return false; } } } + m_logger->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 { + m_logger->debug( "[{} - full: {}] {}: Checking type rules", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); if ( !tx ) { + m_logger->error( "[{} - full: {}] {}: Null transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } @@ -3236,6 +3421,10 @@ namespace sgns auto transfer_tx = std::dynamic_pointer_cast( tx ); if ( !transfer_tx ) { + m_logger->error( "[{} - full: {}] {}: Failed to cast transfer tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return ValidateUTXOParametersForConsensus( @@ -3248,6 +3437,10 @@ namespace sgns auto escrow_tx = std::dynamic_pointer_cast( tx ); if ( !escrow_tx ) { + m_logger->error( "[{} - full: {}] {}: Failed to cast escrow-hold tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return ValidateUTXOParametersForConsensus( escrow_tx->GetUTXOParameters(), escrow_tx->GetSrcAddress() ); @@ -3258,6 +3451,10 @@ namespace sgns auto escrow_release_tx = std::dynamic_pointer_cast( tx ); if ( !escrow_release_tx ) { + m_logger->error( "[{} - full: {}] {}: Failed to cast escrow-release tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return ValidateUTXOParametersForConsensus( escrow_release_tx->GetUTXOParameters(), @@ -3269,10 +3466,18 @@ namespace sgns auto mint_tx = std::dynamic_pointer_cast( tx ); if ( !mint_tx ) { + m_logger->error( "[{} - full: {}] {}: Failed to cast mint tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } if ( mint_tx->GetAmount() == 0 ) { + m_logger->error( "[{} - full: {}] {}: Mint amount is zero", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return true; @@ -3285,9 +3490,19 @@ namespace sgns { if ( window == 0 ) { + m_logger->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; } + m_logger->info( "[{} - full: {}] {}: Setting nonce window to {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + window ); nonce_window_m = window; } } From 3a82ae6ba4af0c2880c4885b2225a5f68041a20a Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 15:05:32 -0300 Subject: [PATCH 022/114] Fix: When the proposal gets resumed it must check the subject again --- src/blockchain/Consensus.cpp | 54 +++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index ff97b3436..a972e60f0 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -764,17 +764,17 @@ namespace sgns registry_->GetRegistryEpoch() ); return; } - if ( !CheckSubject( proposal.subject() ) ) + if ( proposal_validator_ && !proposal_validator_( proposal ) ) { - ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: proposal validator failed proposal_id={}", __func__, proposal.proposal_id() ); return; } - if ( proposal_validator_ && !proposal_validator_( proposal ) ) + if ( !CheckSubject( proposal.subject() ) ) { - ConsensusManagerLogger()->error( "{}: rejected: proposal validator failed proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -839,6 +839,52 @@ namespace sgns 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 proposal_id={}", + __func__, + proposal.proposal_id() ); + continue; + } + + if ( subject_result.value() == SubjectCheck::Reject ) + { + ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", + __func__, + proposal.proposal_id() ); + 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; + } + AddPendingProposal( proposal, subject_hash_result.value() ); + continue; + } + ContinueProposalAfterSubject( proposal ); } return outcome::success(); From ce1c7eac59372d7a286fac3bab99625b4a86ea13 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 15:29:08 -0300 Subject: [PATCH 023/114] Feat: Adding hook to try to resume proposal handling/voting --- src/account/TransactionManager.cpp | 2 ++ src/blockchain/Blockchain.hpp | 2 ++ src/blockchain/impl/Blockchain.cpp | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 4326f97ca..4488e7205 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -2770,6 +2770,8 @@ namespace sgns key ); OUTCOME_TRY( ParseTransaction( new_tx ) ); + (void)blockchain_->TryResumeProposal( tx_hash ); + auto proposal_it = pending_proposals_.find( tx_hash ); if ( proposal_it != pending_proposals_.end() ) { diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index 9091eb45d..5eebdf7f7 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -117,6 +117,8 @@ namespace sgns outcome::result SubmitProposal( const ConsensusManager::Proposal &proposal ); + outcome::result TryResumeProposal( const std::string &hash ); + protected: friend class Migration3_5_1To3_6_0; diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 006283ba4..8fea90d1b 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1562,4 +1562,9 @@ namespace sgns return consensus_manager_->SubmitProposal( std::move( proposal ) ); } + outcome::result Blockchain::TryResumeProposal( const std::string &hash ) + { + return consensus_manager_->ResumeProposalHandling( hash ); + } + } From c42e0d0e1120faa7d2a237b0fa9b5d7ee7de2460 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Feb 2026 17:31:40 -0300 Subject: [PATCH 024/114] Fix: Submitting the consensus proposal before writing the transaction on CRDT --- src/account/TransactionManager.cpp | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 4488e7205..2c8ce5bb7 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -809,7 +809,7 @@ namespace sgns blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), transaction->dag_st.nonce(), transaction->GetHash() ) ); - pending_proposals_[transaction->GetHash()] = proposal; + OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); } else { @@ -2772,25 +2772,6 @@ namespace sgns (void)blockchain_->TryResumeProposal( tx_hash ); - auto proposal_it = pending_proposals_.find( tx_hash ); - if ( proposal_it != pending_proposals_.end() ) - { - m_logger->debug( "[{} - full: {}] Found pending proposal for transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); - auto submit_result = blockchain_->SubmitProposal( proposal_it->second ); - if ( submit_result.has_error() ) - { - m_logger->error( "[{} - full: {}] Failed to submit Proposal for tx {}: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_hash, - submit_result.error().message() ); - return outcome::failure( submit_result.error() ); - } - } - const auto nonce = new_tx->dag_st.nonce(); { From e90ef04eba60c6a2dc0207738db29ae451d911ef Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Sat, 7 Feb 2026 09:17:23 -0300 Subject: [PATCH 025/114] Fix: Incremental tTallying of votes doesn't recompute old votes --- src/blockchain/Consensus.cpp | 136 ++++++++++++++---- src/blockchain/Consensus.hpp | 6 + src/blockchain/impl/Blockchain.cpp | 20 ++- .../blockchain/consensus_certificate_test.cpp | 35 ++--- 4 files changed, 142 insertions(+), 55 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index a972e60f0..d367152b5 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -31,18 +31,35 @@ namespace sgns std::shared_ptr ConsensusManager::New( std::shared_ptr registry, 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 ( !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( pubsub ), std::move( signer ), - std::move( consensus_topic ) ) ); + address, + consensus_topic ) ); instance->consensus_subs_future_ = std::move( instance->pubsub_->Subscribe( instance->consensus_topic_, @@ -65,10 +82,12 @@ namespace sgns ConsensusManager::ConsensusManager( std::shared_ptr registry, std::shared_ptr pubsub, Signer signer, + std::string address, std::string consensus_topic ) : registry_( std::move( registry ) ), // pubsub_( std::move( pubsub ) ), // signer_( std::move( signer ) ), // + account_address_( address ), // consensus_topic_( std::string( CONSENSUS_CHANNEL_PREFIX ) + sgns::version::GetNetAndVersionAppendix() + consensus_topic ) { @@ -227,7 +246,7 @@ namespace sgns } } - if ( should_vote && signer_ && !account_address_.empty() ) + if ( should_vote ) { auto vote_result = CreateVote( proposal.proposal_id(), account_address_, true, signer_ ); if ( vote_result.has_value() ) @@ -477,20 +496,20 @@ namespace sgns outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, const std::vector &votes ) { - ConsensusManagerLogger()->trace( "{}: TallyVotes called proposal_id={} votes={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", __func__, proposal.proposal_id(), votes.size() ); if ( !registry_ ) { - ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry is null", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: registry is null", __func__ ); return outcome::failure( std::errc::not_supported ); } auto registry_result = registry_->LoadRegistry(); if ( registry_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry load error={}", + ConsensusManagerLogger()->error( "{}: failed: registry load error={}", __func__, registry_result.error().message() ); return outcome::failure( registry_result.error() ); @@ -500,7 +519,7 @@ namespace sgns const auto registry_cid = registry_->GetRegistryCid(); if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { - ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry cid mismatch proposal={} registry={}", + ConsensusManagerLogger()->error( "{}: failed: registry cid mismatch proposal={} registry={}", __func__, proposal.registry_cid(), registry_cid ); @@ -508,7 +527,7 @@ namespace sgns } if ( proposal.registry_epoch() != registry.epoch() ) { - ConsensusManagerLogger()->error( "{}: TallyVotes failed: registry epoch mismatch proposal={} registry={}", + ConsensusManagerLogger()->error( "{}: failed: registry epoch mismatch proposal={} registry={}", __func__, proposal.registry_epoch(), registry.epoch() ); @@ -521,7 +540,7 @@ namespace sgns for ( const auto &vote : votes ) { - ConsensusManagerLogger()->trace( "{}: TallyVotes processing vote voter_id={} approve={}", + ConsensusManagerLogger()->trace( "{}: processing vote voter_id={} approve={}", __func__, vote.voter_id(), vote.approve() ); @@ -562,7 +581,7 @@ namespace sgns tally.approved_weight = approved_weight; tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); ConsensusManagerLogger()->debug( - "{}: TallyVotes success proposal_id={} approved_weight={} total_weight={} quorum={}", + "{}: success proposal_id={} approved_weight={} total_weight={} quorum={}", __func__, proposal.proposal_id(), approved_weight, @@ -839,7 +858,6 @@ namespace sgns for ( const auto &proposal : to_process ) { - SubjectHandler subject_handler; { std::shared_lock lock( subject_handlers_mutex_ ); @@ -892,7 +910,7 @@ namespace sgns void ConsensusManager::HandleVote( const Vote &vote ) { - ConsensusManagerLogger()->trace( "{}: HandleVote called proposal_id={} voter_id={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={}", __func__, vote.proposal_id(), vote.voter_id() ); @@ -903,49 +921,113 @@ namespace sgns if ( !registry_ ) { - ConsensusManagerLogger()->error( "{}: HandleVote aborted: registry is null", __func__ ); + ConsensusManagerLogger()->error( "{}: aborted: registry is null", __func__ ); return; } + if ( !vote.approve() ) + { + ConsensusManagerLogger()->debug( "{}: ignored: vote not approved voter_id={}", + __func__, + vote.voter_id() ); + 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() ); + return; + } + + auto registry_result = registry_->LoadRegistry(); + if ( registry_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: rejected: registry load error={}", + __func__, + registry_result.error().message() ); + return; + } + const auto ®istry = registry_result.value(); + + bool has_quorum = false; ProposalState state; { std::lock_guard lock( proposals_mutex_ ); auto it = proposals_.find( vote.proposal_id() ); if ( it == proposals_.end() ) { - ConsensusManagerLogger()->error( "{}: HandleVote ignored: proposal not found proposal_id={}", + ConsensusManagerLogger()->error( "{}: ignored: proposal not found proposal_id={}", __func__, vote.proposal_id() ); return; } - it->second.votes.push_back( vote ); - state = it->second; - auto slot_it = slot_states_.find( state.slot_key ); + auto slot_it = slot_states_.find( it->second.slot_key ); if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id != vote.proposal_id() ) { - ConsensusManagerLogger()->error( "{}: HandleVote ignored: not best proposal proposal_id={}", + ConsensusManagerLogger()->error( "{}: ignored: not best proposal proposal_id={}", __func__, vote.proposal_id() ); return; } - } - auto tally_result = TallyVotes( state.proposal, state.votes ); - if ( tally_result.has_error() || !tally_result.value().has_quorum ) - { - if ( tally_result.has_error() ) + if ( it->second.seen_voters.find( vote.voter_id() ) != it->second.seen_voters.end() ) { - ConsensusManagerLogger()->error( "{}: HandleVote aborted: tally error={}", + ConsensusManagerLogger()->trace( "{}: ignored: duplicate vote voter_id={}", __func__, - tally_result.error().message() ); + vote.voter_id() ); + return; + } + + if ( it->second.proposal.registry_cid() != registry_->GetRegistryCid() || + it->second.proposal.registry_epoch() != registry.epoch() ) + { + ConsensusManagerLogger()->error( "{}: rejected: registry mismatch proposal_id={}", + __func__, + vote.proposal_id() ); + return; } + + const auto *validator = FindValidator( registry, vote.voter_id() ); + if ( !validator || validator->status() != ValidatorRegistry::Status::ACTIVE ) + { + ConsensusManagerLogger()->error( "{}: rejected: validator not active voter_id={}", + __func__, + vote.voter_id() ); + return; + } + + if ( it->second.total_weight == 0 ) + { + it->second.total_weight = registry_->TotalWeight( registry ); + } + + it->second.votes.push_back( vote ); + it->second.seen_voters.insert( vote.voter_id() ); + it->second.approved_weight += validator->weight(); + state = it->second; + has_quorum = + registry_->IsQuorum( it->second.approved_weight, it->second.total_weight ); + } + + if ( !has_quorum ) + { return; } if ( state.certificate.has_value() ) { - ConsensusManagerLogger()->debug( "{}: HandleVote skipped: certificate already present proposal_id={}", + ConsensusManagerLogger()->debug( "{}: skipped: certificate already present proposal_id={}", __func__, vote.proposal_id() ); return; @@ -954,7 +1036,7 @@ namespace sgns auto certificate_result = CreateCertificate( state.proposal, state.votes ); if ( certificate_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleVote failed: certificate creation error={}", + ConsensusManagerLogger()->error( "{}: failed: certificate creation error={}", __func__, certificate_result.error().message() ); return; @@ -971,7 +1053,7 @@ namespace sgns (void)SubmitCertificate( certificate_result.value() ); NotifyCertificate( state.proposal, certificate_result.value() ); - ConsensusManagerLogger()->debug( "{}: HandleVote certificate submitted proposal_id={}", + ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", __func__, vote.proposal_id() ); } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 0c25040cb..c6536997e 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include "blockchain/ValidatorRegistry.hpp" @@ -58,6 +59,7 @@ namespace sgns static std::shared_ptr New( std::shared_ptr registry, std::shared_ptr pubsub, Signer signer, + std::string address, std::string consensus_topic = "" ); void SetProposalValidator( ProposalValidator validator ); @@ -119,6 +121,7 @@ namespace sgns explicit ConsensusManager( std::shared_ptr registry, std::shared_ptr pubsub, Signer signer, + std::string address, std::string consensus_topic ); static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; @@ -130,6 +133,9 @@ namespace sgns std::vector votes; std::optional certificate; std::string slot_key; + uint64_t total_weight = 0; + uint64_t approved_weight = 0; + std::unordered_set seen_voters; }; struct SlotState diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 8fea90d1b..93f93ee52 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -144,7 +144,8 @@ namespace sgns return strong->account_->Sign( std::move( payload ) ); } return outcome::failure( std::errc::owner_dead ); - } ); + }, + instance->account_->GetAddress() ); auto ensure_registry_result = instance->EnsureValidatorRegistry(); if ( ensure_registry_result.has_error() ) @@ -827,7 +828,7 @@ namespace sgns GetAuthorizedFullNodeAddress().substr( 0, 8 ) ); GenesisBlock g; - auto timestamp = std::chrono::system_clock::now(); + auto timestamp = std::chrono::system_clock::now(); g.set_chain_id( "supergenius" ); g.set_timestamp( @@ -1073,7 +1074,7 @@ namespace sgns cids_.genesis_.value() ); AccountCreationBlock ac; - auto timestamp = std::chrono::system_clock::now(); + auto timestamp = std::chrono::system_clock::now(); ac.set_account_address( account_->GetAddress() ); ac.set_genesis_block_cid( cids_.genesis_.value() ); @@ -1380,8 +1381,7 @@ namespace sgns return std::nullopt; } - bool Blockchain::ShouldReplaceGenesis( const GenesisBlock &existing, - const GenesisBlock &candidate ) const + bool Blockchain::ShouldReplaceGenesis( const GenesisBlock &existing, const GenesisBlock &candidate ) const { if ( candidate.timestamp() == existing.timestamp() ) { @@ -1526,8 +1526,7 @@ namespace sgns 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 ) + bool Blockchain::RegisterSubjectHandler( SubjectType type, ConsensusManager::SubjectHandler handler ) { return consensus_manager_->RegisterSubjectHandler( type, std::move( handler ) ); } @@ -1542,10 +1541,9 @@ namespace sgns consensus_manager_->SetCertificateCallback( std::move( callback ) ); } - outcome::result Blockchain::CreateConsensusProposal( - const std::string &account_id, - uint64_t nonce, - const std::string &tx_hash ) + outcome::result Blockchain::CreateConsensusProposal( const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash ) { OUTCOME_TRY( auto &&nonce_subject, consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash ) ); OUTCOME_TRY( auto &&nonce_proposal, diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index 042bf887c..44460fe33 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -20,9 +20,8 @@ namespace return account; } - std::shared_ptr MakeRegistry( - const std::shared_ptr &db, - const std::shared_ptr &account ) + std::shared_ptr MakeRegistry( const std::shared_ptr &db, + const std::shared_ptr &account ) { using sgns::ValidatorRegistry; auto registry = ValidatorRegistry::New( @@ -72,13 +71,14 @@ namespace sgns::test auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = ConsensusManager::New( registry, - pubs_, - [account]( std::vector payload ) - { return account->Sign( std::move( payload ) ); } ); + auto manager = ConsensusManager::New( + registry, + pubs_, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + account->GetAddress() ); - std::string tx_hash = "0x010203"; - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); + std::string tx_hash = "0x010203"; + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); ASSERT_TRUE( subject_result.has_value() ); auto proposal_result = manager->CreateProposal( subject_result.value(), @@ -107,13 +107,14 @@ namespace sgns::test auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = ConsensusManager::New( registry, - pubs_, - [account]( std::vector payload ) - { return account->Sign( std::move( payload ) ); } ); + auto manager = ConsensusManager::New( + registry, + pubs_, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + account->GetAddress() ); - std::string tx_hash = "0x010203"; - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); + std::string tx_hash = "0x010203"; + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); ASSERT_TRUE( subject_result.has_value() ); auto proposal_result = manager->CreateProposal( subject_result.value(), @@ -137,8 +138,8 @@ namespace sgns::test auto cert = cert_result.value(); bool notified = false; - manager->SetCertificateCallback( [¬ified]( const ConsensusProposal &, - const ConsensusCertificate & ) { notified = true; } ); + manager->SetCertificateCallback( [¬ified]( const ConsensusProposal &, const ConsensusCertificate & ) + { notified = true; } ); manager->HandleCertificate( cert ); EXPECT_TRUE( notified ); From a014ea74619a87631dc053bc2371a6711a7f74dd Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Sat, 7 Feb 2026 09:21:44 -0300 Subject: [PATCH 026/114] Chore: Log clean --- src/blockchain/Consensus.cpp | 148 +++++++++++++++++------------------ 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index d367152b5..33e3c7f9f 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -252,13 +252,13 @@ namespace sgns if ( vote_result.has_value() ) { (void)SubmitVote( vote_result.value() ); - ConsensusManagerLogger()->debug( "{}: HandleProposal self-vote submitted proposal_id={}", + ConsensusManagerLogger()->debug( "{}: self-vote submitted proposal_id={}", __func__, proposal.proposal_id() ); } else { - ConsensusManagerLogger()->error( "{}: HandleProposal self-vote failed proposal_id={} error={}", + ConsensusManagerLogger()->error( "{}: self-vote failed proposal_id={} error={}", __func__, proposal.proposal_id(), vote_result.error().message() ); @@ -313,16 +313,16 @@ namespace sgns uint64_t registry_epoch, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: CreateProposal called for proposer_id={}", __func__, proposer_id ); + ConsensusManagerLogger()->trace( "{}: called for proposer_id={}", __func__, proposer_id ); if ( !sign ) { - ConsensusManagerLogger()->error( "{}: CreateProposal failed: signer is empty", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: signer is empty", __func__ ); return outcome::failure( std::errc::invalid_argument ); } if ( !ValidateSubject( subject ) ) { - ConsensusManagerLogger()->error( "{}: CreateProposal failed: subject validation failed", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: subject validation failed", __func__ ); return outcome::failure( std::errc::invalid_argument ); } @@ -340,7 +340,7 @@ namespace sgns auto subject_id_result = ComputeSubjectId( proposal.subject() ); if ( subject_id_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateProposal failed: subject id computation error={}", + ConsensusManagerLogger()->error( "{}: failed: subject id computation error={}", __func__, subject_id_result.error().message() ); return outcome::failure( subject_id_result.error() ); @@ -352,7 +352,7 @@ namespace sgns auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateProposal failed: signing bytes error={}", + ConsensusManagerLogger()->error( "{}: failed: signing bytes error={}", __func__, signing_bytes.error().message() ); return outcome::failure( signing_bytes.error() ); @@ -360,7 +360,7 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); proposal.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: CreateProposal success proposal_id={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); return proposal; @@ -371,14 +371,14 @@ namespace sgns bool approve, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: CreateVote called proposal_id={} voter_id={} approve={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={} approve={}", __func__, proposal_id, voter_id, approve ); if ( !sign ) { - ConsensusManagerLogger()->error( "{}: CreateVote failed: signer is empty", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: signer is empty", __func__ ); return outcome::failure( std::errc::invalid_argument ); } @@ -393,7 +393,7 @@ namespace sgns auto signing_bytes = VoteSigningBytes( vote ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateVote failed: signing bytes error={}", + ConsensusManagerLogger()->error( "{}: failed: signing bytes error={}", __func__, signing_bytes.error().message() ); return outcome::failure( signing_bytes.error() ); @@ -402,7 +402,7 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); vote.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: CreateVote success proposal_id={} voter_id={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={} voter_id={}", __func__, proposal_id, voter_id ); @@ -414,14 +414,14 @@ namespace sgns const std::vector &votes, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: CreateVoteBundle called proposal_id={} aggregator_id={} votes={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} aggregator_id={} votes={}", __func__, proposal_id, aggregator_id, votes.size() ); if ( !sign ) { - ConsensusManagerLogger()->error( "{}: CreateVoteBundle failed: signer is empty", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: signer is empty", __func__ ); return outcome::failure( std::errc::invalid_argument ); } @@ -439,7 +439,7 @@ namespace sgns auto signing_bytes = VoteBundleSigningBytes( bundle ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateVoteBundle failed: signing bytes error={}", + ConsensusManagerLogger()->error( "{}: failed: signing bytes error={}", __func__, signing_bytes.error().message() ); return outcome::failure( signing_bytes.error() ); @@ -448,7 +448,7 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); bundle.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: CreateVoteBundle success proposal_id={} votes={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={} votes={}", __func__, proposal_id, votes.size() ); @@ -458,14 +458,14 @@ namespace sgns outcome::result ConsensusManager::CreateCertificate( const Proposal &proposal, const std::vector &votes ) { - ConsensusManagerLogger()->trace( "{}: CreateCertificate called proposal_id={} votes={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", __func__, proposal.proposal_id(), votes.size() ); auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateCertificate failed: tally error={}", + ConsensusManagerLogger()->error( "{}: failed: tally error={}", __func__, tally_result.error().message() ); return outcome::failure( tally_result.error() ); @@ -487,7 +487,7 @@ namespace sgns } *cert.mutable_proposal() = proposal; - ConsensusManagerLogger()->debug( "{}: CreateCertificate success proposal_id={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); return cert; @@ -592,7 +592,7 @@ namespace sgns outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: ProposalSigningBytes called proposal_id={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); Proposal copy = proposal; @@ -600,7 +600,7 @@ namespace sgns std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { - ConsensusManagerLogger()->error( "{}: ProposalSigningBytes failed: serialization error", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: serialization error", __func__ ); return outcome::failure( std::errc::invalid_argument ); } return std::vector( serialized.begin(), serialized.end() ); @@ -608,7 +608,7 @@ namespace sgns outcome::result> ConsensusManager::VoteSigningBytes( const Vote &vote ) { - ConsensusManagerLogger()->trace( "{}: VoteSigningBytes called voter_id={} proposal_id={}", + ConsensusManagerLogger()->trace( "{}: called voter_id={} proposal_id={}", __func__, vote.voter_id(), vote.proposal_id() ); @@ -617,7 +617,7 @@ namespace sgns std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { - ConsensusManagerLogger()->error( "{}: VoteSigningBytes failed: serialization error", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: serialization error", __func__ ); return outcome::failure( std::errc::invalid_argument ); } return std::vector( serialized.begin(), serialized.end() ); @@ -625,7 +625,7 @@ namespace sgns outcome::result> ConsensusManager::VoteBundleSigningBytes( const VoteBundle &bundle ) { - ConsensusManagerLogger()->trace( "{}: VoteBundleSigningBytes called proposal_id={} votes={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", __func__, bundle.proposal_id(), bundle.votes_size() ); @@ -634,7 +634,7 @@ namespace sgns std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { - ConsensusManagerLogger()->error( "{}: VoteBundleSigningBytes failed: serialization error", __func__ ); + ConsensusManagerLogger()->error( "{}: failed: serialization error", __func__ ); return outcome::failure( std::errc::invalid_argument ); } return std::vector( serialized.begin(), serialized.end() ); @@ -642,7 +642,7 @@ namespace sgns outcome::result ConsensusManager::SubmitProposal( const Proposal &proposal, bool self_vote ) { - ConsensusManagerLogger()->trace( "{}: SubmitProposal called proposal_id={} self_vote={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} self_vote={}", __func__, proposal.proposal_id(), self_vote ); @@ -664,12 +664,12 @@ namespace sgns auto publish_result = Publish( message ); if ( publish_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: SubmitProposal failed: publish error={}", + ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, publish_result.error().message() ); return publish_result; } - ConsensusManagerLogger()->debug( "{}: SubmitProposal success proposal_id={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); @@ -683,7 +683,7 @@ namespace sgns outcome::result ConsensusManager::SubmitVote( const Vote &vote ) { - ConsensusManagerLogger()->trace( "{}: SubmitVote called proposal_id={} voter_id={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={}", __func__, vote.proposal_id(), vote.voter_id() ); @@ -692,12 +692,12 @@ namespace sgns auto result = Publish( message ); if ( result.has_error() ) { - ConsensusManagerLogger()->error( "{}: SubmitVote failed: publish error={}", + ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); return result; } - ConsensusManagerLogger()->debug( "{}: SubmitVote success proposal_id={} voter_id={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={} voter_id={}", __func__, vote.proposal_id(), vote.voter_id() ); @@ -706,7 +706,7 @@ namespace sgns outcome::result ConsensusManager::SubmitCertificate( const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: SubmitCertificate called proposal_id={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); ConsensusMessage message; @@ -714,12 +714,12 @@ namespace sgns auto result = Publish( message ); if ( result.has_error() ) { - ConsensusManagerLogger()->error( "{}: SubmitCertificate failed: publish error={}", + ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); return result; } - ConsensusManagerLogger()->debug( "{}: SubmitCertificate success proposal_id={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); return result; @@ -1060,7 +1060,7 @@ namespace sgns void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) { - ConsensusManagerLogger()->trace( "{}: HandleVoteBundle called proposal_id={} votes={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", __func__, bundle.proposal_id(), bundle.votes_size() ); @@ -1070,14 +1070,14 @@ namespace sgns } for ( const auto &vote : bundle.votes() ) { - ConsensusManagerLogger()->trace( "{}: HandleVoteBundle processing voter_id={}", __func__, vote.voter_id() ); + ConsensusManagerLogger()->trace( "{}: processing voter_id={}", __func__, vote.voter_id() ); HandleVote( vote ); } } void ConsensusManager::HandleCertificate( const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: HandleCertificate called proposal_id={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); if ( certificate_handler_ ) @@ -1087,7 +1087,7 @@ namespace sgns if ( !registry_ ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: registry is null", __func__ ); + ConsensusManagerLogger()->error( "{}: aborted: registry is null", __func__ ); return; } @@ -1101,7 +1101,7 @@ namespace sgns if ( proposal.proposal_id() != certificate.proposal_id() ) { ConsensusManagerLogger()->error( - "{}: HandleCertificate rejected: proposal_id mismatch cert={} proposal={}", + "{}: rejected: proposal_id mismatch cert={} proposal={}", __func__, certificate.proposal_id(), proposal.proposal_id() ); @@ -1111,7 +1111,7 @@ namespace sgns if ( proposal.registry_cid() != certificate.registry_cid() || proposal.registry_epoch() != certificate.registry_epoch() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: registry mismatch proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: registry mismatch proposal_id={}", __func__, certificate.proposal_id() ); return; @@ -1119,7 +1119,7 @@ namespace sgns if ( !ValidateSubject( proposal.subject() ) ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: invalid subject proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: invalid subject proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -1128,7 +1128,7 @@ namespace sgns auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: signing bytes error={}", + ConsensusManagerLogger()->error( "{}: rejected: signing bytes error={}", __func__, signing_bytes.error().message() ); return; @@ -1138,7 +1138,7 @@ namespace sgns signing_bytes.value() ) ) { ConsensusManagerLogger()->error( - "{}: HandleCertificate rejected: signature verification failed proposer_id={}", + "{}: rejected: signature verification failed proposer_id={}", __func__, proposal.proposer_id() ); return; @@ -1147,13 +1147,13 @@ namespace sgns const auto computed_id = CreateProposalId( proposal ); if ( computed_id.empty() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate rejected: computed_id empty", __func__ ); + ConsensusManagerLogger()->error( "{}: rejected: computed_id empty", __func__ ); return; } if ( computed_id != certificate.proposal_id() ) { ConsensusManagerLogger()->error( - "{}: HandleCertificate rejected: computed_id mismatch cert={} computed={}", + "{}: rejected: computed_id mismatch cert={} computed={}", __func__, certificate.proposal_id(), computed_id ); @@ -1170,7 +1170,7 @@ namespace sgns if ( it->second.certificate.has_value() ) { ConsensusManagerLogger()->debug( - "{}: HandleCertificate skipped: already have certificate proposal_id={}", + "{}: skipped: already have certificate proposal_id={}", __func__, certificate.proposal_id() ); return; @@ -1199,7 +1199,7 @@ namespace sgns } else { - ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: missing proposal proposal_id={}", + ConsensusManagerLogger()->error( "{}: aborted: missing proposal proposal_id={}", __func__, certificate.proposal_id() ); return; @@ -1208,7 +1208,7 @@ namespace sgns 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( "{}: HandleCertificate rejected: not best proposal proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: not best proposal proposal_id={}", __func__, certificate.proposal_id() ); return; @@ -1219,7 +1219,7 @@ namespace sgns votes.reserve( static_cast( certificate.votes_size() ) ); for ( const auto &vote : certificate.votes() ) { - ConsensusManagerLogger()->trace( "{}: HandleCertificate processing vote voter_id={}", + ConsensusManagerLogger()->trace( "{}: processing vote voter_id={}", __func__, vote.voter_id() ); votes.push_back( vote ); @@ -1230,7 +1230,7 @@ namespace sgns { if ( tally_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: HandleCertificate aborted: tally error={}", + ConsensusManagerLogger()->error( "{}: aborted: tally error={}", __func__, tally_result.error().message() ); } @@ -1247,14 +1247,14 @@ namespace sgns } NotifyCertificate( proposal, certificate ); - ConsensusManagerLogger()->debug( "{}: HandleCertificate success proposal_id={}", + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); } void ConsensusManager::NotifyCertificate( const Proposal &proposal, const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: NotifyCertificate called proposal_id={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); if ( certificate_callback_ ) @@ -1265,7 +1265,7 @@ namespace sgns std::string ConsensusManager::GetSlotKey( const Proposal &proposal ) const { - ConsensusManagerLogger()->trace( "{}: GetSlotKey called proposal_id={}", __func__, proposal.proposal_id() ); + 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() ); @@ -1279,7 +1279,7 @@ namespace sgns bool ConsensusManager::IsBetterProposal( const Proposal &candidate, const Proposal ¤t ) const { - ConsensusManagerLogger()->trace( "{}: IsBetterProposal called candidate={} current={}", + ConsensusManagerLogger()->trace( "{}: called candidate={} current={}", __func__, candidate.proposal_id(), current.proposal_id() ); @@ -1306,7 +1306,7 @@ namespace sgns outcome::result ConsensusManager::ComputeSubjectId( const Subject &subject ) { - ConsensusManagerLogger()->trace( "{}: ComputeSubjectId called subject_type={}", + ConsensusManagerLogger()->trace( "{}: called subject_type={}", __func__, static_cast( subject.type() ) ); Subject copy = subject; @@ -1314,14 +1314,14 @@ namespace sgns std::string serialized; if ( !copy.SerializeToString( &serialized ) ) { - ConsensusManagerLogger()->error( "{}: ComputeSubjectId failed: serialization error", __func__ ); + 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( "{}: ComputeSubjectId success", __func__ ); + ConsensusManagerLogger()->debug( "{}: success", __func__ ); return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); } @@ -1329,7 +1329,7 @@ namespace sgns uint64_t nonce, const std::string &tx_hash ) { - ConsensusManagerLogger()->trace( "{}: CreateNonceSubject called account_id={} nonce={}", + ConsensusManagerLogger()->trace( "{}: called account_id={} nonce={}", __func__, account_id, nonce ); @@ -1343,13 +1343,13 @@ namespace sgns auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateNonceSubject failed: subject id 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( "{}: CreateNonceSubject success subject_id={}", + ConsensusManagerLogger()->debug( "{}: success subject_id={}", __func__, subject.subject_id() ); return subject; @@ -1361,7 +1361,7 @@ namespace sgns const std::string &task_result_hash, uint64_t result_epoch ) { - ConsensusManagerLogger()->trace( "{}: CreateTaskResultSubject called account_id={} result_epoch={}", + ConsensusManagerLogger()->trace( "{}: called account_id={} result_epoch={}", __func__, account_id, result_epoch ); @@ -1376,13 +1376,13 @@ namespace sgns auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateTaskResultSubject failed: subject id 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( "{}: CreateTaskResultSubject success subject_id={}", + ConsensusManagerLogger()->debug( "{}: success subject_id={}", __func__, subject.subject_id() ); return subject; @@ -1390,7 +1390,7 @@ namespace sgns std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: CreateProposalId called proposal_id={}", + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); // Proposal ID must be derived from the proposal contents excluding the proposal_id itself. @@ -1399,7 +1399,7 @@ namespace sgns auto signing_bytes = ProposalSigningBytes( copy ); if ( signing_bytes.has_error() ) { - ConsensusManagerLogger()->error( "{}: CreateProposalId failed: signing bytes error={}", + ConsensusManagerLogger()->error( "{}: failed: signing bytes error={}", __func__, signing_bytes.error().message() ); return {}; @@ -1408,13 +1408,13 @@ namespace sgns sgns::crypto::HasherImpl hasher; auto hash = hasher.sha2_256( gsl::span( signing_bytes.value().data(), signing_bytes.value().size() ) ); - ConsensusManagerLogger()->debug( "{}: CreateProposalId success", __func__ ); + ConsensusManagerLogger()->debug( "{}: success", __func__ ); return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); } bool ConsensusManager::ValidateSubject( const Subject &subject ) { - ConsensusManagerLogger()->trace( "{}: ValidateSubject called subject_type={}", + ConsensusManagerLogger()->trace( "{}: called subject_type={}", __func__, static_cast( subject.type() ) ); if ( subject.account_id().empty() ) @@ -1449,7 +1449,7 @@ namespace sgns const ValidatorRegistry::Registry ®istry, const std::string &validator_id ) const { - ConsensusManagerLogger()->trace( "{}: FindValidator called validator_id={}", __func__, validator_id ); + ConsensusManagerLogger()->trace( "{}: called validator_id={}", __func__, validator_id ); for ( const auto &validator : registry.validators() ) { if ( validator.validator_id() == validator_id ) @@ -1462,10 +1462,10 @@ namespace sgns void ConsensusManager::OnConsensusMessage( boost::optional message ) { - ConsensusManagerLogger()->trace( "{}: OnConsensusMessage called", __func__ ); + ConsensusManagerLogger()->trace( "{}: called", __func__ ); if ( !message ) { - ConsensusManagerLogger()->error( "{}: OnConsensusMessage ignored: message is empty", __func__ ); + ConsensusManagerLogger()->error( "{}: ignored: message is empty", __func__ ); return; } @@ -1478,25 +1478,25 @@ namespace sgns if ( decoded.has_proposal() ) { - ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded proposal", __func__ ); + ConsensusManagerLogger()->debug( "{}: decoded proposal", __func__ ); HandleProposal( decoded.proposal() ); return; } if ( decoded.has_vote() ) { - ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded vote", __func__ ); + ConsensusManagerLogger()->debug( "{}: decoded vote", __func__ ); HandleVote( decoded.vote() ); return; } if ( decoded.has_vote_bundle() ) { - ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded vote bundle", __func__ ); + ConsensusManagerLogger()->debug( "{}: decoded vote bundle", __func__ ); HandleVoteBundle( decoded.vote_bundle() ); return; } if ( decoded.has_certificate() ) { - ConsensusManagerLogger()->debug( "{}: OnConsensusMessage decoded certificate", __func__ ); + ConsensusManagerLogger()->debug( "{}: decoded certificate", __func__ ); HandleCertificate( decoded.certificate() ); } } From 52fc1c25397d292eb692ab4cc18da3cb4c405acb Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Sat, 7 Feb 2026 14:54:03 -0300 Subject: [PATCH 027/114] Feat: Separating signing methods and some new check methods --- src/blockchain/Consensus.cpp | 303 +++++++++++----------------- src/blockchain/Consensus.hpp | 27 +-- src/blockchain/ConsensusSigning.hpp | 52 +++++ 3 files changed, 180 insertions(+), 202 deletions(-) create mode 100644 src/blockchain/ConsensusSigning.hpp diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 33e3c7f9f..cd7791cd0 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -17,6 +17,7 @@ #include "base/sgns_version.hpp" #include "crypto/hasher/hasher_impl.hpp" #include "account/GeniusAccount.hpp" +#include "blockchain/ConsensusSigning.hpp" namespace sgns { @@ -93,11 +94,6 @@ namespace sgns { } - void ConsensusManager::SetProposalValidator( ProposalValidator validator ) - { - proposal_validator_ = std::move( validator ); - } - void ConsensusManager::SetCertificateCallback( CertificateCallback callback ) { certificate_callback_ = std::move( callback ); @@ -121,11 +117,6 @@ namespace sgns return outcome::success(); } - void ConsensusManager::SetVoteHandler( VoteHandler handler ) - { - vote_handler_ = std::move( handler ); - } - void ConsensusManager::SetVoteBundleHandler( VoteBundleHandler handler ) { vote_bundle_handler_ = std::move( handler ); @@ -360,9 +351,7 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); proposal.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", - __func__, - proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); return proposal; } @@ -402,10 +391,7 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); vote.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={} voter_id={}", - __func__, - proposal_id, - voter_id ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={} voter_id={}", __func__, proposal_id, voter_id ); return vote; } @@ -448,10 +434,7 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); bundle.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={} votes={}", - __func__, - proposal_id, - votes.size() ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={} votes={}", __func__, proposal_id, votes.size() ); return bundle; } @@ -465,9 +448,7 @@ namespace sgns auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: tally error={}", - __func__, - tally_result.error().message() ); + ConsensusManagerLogger()->error( "{}: failed: tally error={}", __func__, tally_result.error().message() ); return outcome::failure( tally_result.error() ); } @@ -487,9 +468,7 @@ namespace sgns } *cert.mutable_proposal() = proposal; - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", - __func__, - proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); return cert; } @@ -553,7 +532,7 @@ namespace sgns continue; } - const auto *validator = FindValidator( registry, vote.voter_id() ); + const auto *validator = registry_->FindValidator( registry, vote.voter_id() ); if ( !validator || validator->status() != ValidatorRegistry::Status::ACTIVE ) { continue; @@ -580,30 +559,19 @@ namespace sgns tally.total_weight = total_weight; tally.approved_weight = approved_weight; tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); - ConsensusManagerLogger()->debug( - "{}: success proposal_id={} approved_weight={} total_weight={} quorum={}", - __func__, - proposal.proposal_id(), - approved_weight, - total_weight, - tally.has_quorum ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={} approved_weight={} total_weight={} quorum={}", + __func__, + proposal.proposal_id(), + approved_weight, + total_weight, + tally.has_quorum ); return tally; } outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", - __func__, - proposal.proposal_id() ); - Proposal copy = proposal; - copy.clear_signature(); - std::string serialized; - if ( !copy.SerializeToString( &serialized ) ) - { - ConsensusManagerLogger()->error( "{}: failed: serialization error", __func__ ); - return outcome::failure( std::errc::invalid_argument ); - } - return std::vector( serialized.begin(), serialized.end() ); + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); + return sgns::ProposalSigningBytes( proposal ); } outcome::result> ConsensusManager::VoteSigningBytes( const Vote &vote ) @@ -612,15 +580,7 @@ namespace sgns __func__, vote.voter_id(), vote.proposal_id() ); - Vote copy = vote; - copy.clear_signature(); - std::string serialized; - if ( !copy.SerializeToString( &serialized ) ) - { - ConsensusManagerLogger()->error( "{}: failed: serialization error", __func__ ); - return outcome::failure( std::errc::invalid_argument ); - } - return std::vector( serialized.begin(), serialized.end() ); + return sgns::VoteSigningBytes( vote ); } outcome::result> ConsensusManager::VoteBundleSigningBytes( const VoteBundle &bundle ) @@ -629,15 +589,7 @@ namespace sgns __func__, bundle.proposal_id(), bundle.votes_size() ); - VoteBundle copy = bundle; - copy.clear_signature(); - std::string serialized; - if ( !copy.SerializeToString( &serialized ) ) - { - ConsensusManagerLogger()->error( "{}: failed: serialization error", __func__ ); - return outcome::failure( std::errc::invalid_argument ); - } - return std::vector( serialized.begin(), serialized.end() ); + return sgns::VoteBundleSigningBytes( bundle ); } outcome::result ConsensusManager::SubmitProposal( const Proposal &proposal, bool self_vote ) @@ -669,9 +621,7 @@ namespace sgns publish_result.error().message() ); return publish_result; } - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", - __func__, - proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); if ( self_vote ) { @@ -692,9 +642,7 @@ namespace sgns auto result = Publish( message ); if ( result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: publish error={}", - __func__, - result.error().message() ); + ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); return result; } ConsensusManagerLogger()->debug( "{}: success proposal_id={} voter_id={}", @@ -706,22 +654,16 @@ namespace sgns outcome::result ConsensusManager::SubmitCertificate( const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", - __func__, - certificate.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); ConsensusMessage message; *message.mutable_certificate() = certificate; auto result = Publish( message ); if ( result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: publish error={}", - __func__, - result.error().message() ); + ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); return result; } - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", - __func__, - certificate.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); return result; } @@ -729,6 +671,14 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); + if ( !CheckProposal( proposal ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: Invalid proposal proposal_id={}", + __func__, + proposal.proposal_id() ); + return; + } + auto signing_bytes = ProposalSigningBytes( proposal ); if ( signing_bytes.has_error() ) { @@ -753,16 +703,10 @@ namespace sgns return; } - if ( !registry_ ) - { - ConsensusManagerLogger()->error( "{}: rejected: registry is null", __func__ ); - return; - } - const auto registry_cid = registry_->GetRegistryCid(); - if ( proposal.registry_cid().empty() || registry_cid.empty() ) + if ( registry_cid.empty() ) { - ConsensusManagerLogger()->error( "{}: rejected: registry cid missing proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: Local registry doesn't have a CID. proposal_id={}", __func__, proposal.proposal_id() ); return; @@ -783,13 +727,6 @@ namespace sgns registry_->GetRegistryEpoch() ); return; } - if ( proposal_validator_ && !proposal_validator_( proposal ) ) - { - ConsensusManagerLogger()->error( "{}: rejected: proposal validator failed proposal_id={}", - __func__, - proposal.proposal_id() ); - return; - } if ( !CheckSubject( proposal.subject() ) ) { @@ -914,22 +851,18 @@ namespace sgns __func__, vote.proposal_id(), vote.voter_id() ); - if ( vote_handler_ ) - { - vote_handler_( vote ); - } - - if ( !registry_ ) + if ( !CheckVote( vote ) ) { - ConsensusManagerLogger()->error( "{}: aborted: registry is null", __func__ ); + 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() ); + ConsensusManagerLogger()->debug( "{}: ignored: vote not approved voter_id={}", __func__, vote.voter_id() ); + //TODO - maybe see reputation? return; } @@ -971,8 +904,8 @@ namespace sgns vote.proposal_id() ); return; } - - auto slot_it = slot_states_.find( it->second.slot_key ); + auto &proposal_state = it->second; + 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={}", @@ -981,16 +914,14 @@ namespace sgns return; } - if ( it->second.seen_voters.find( vote.voter_id() ) != it->second.seen_voters.end() ) + 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() ); + ConsensusManagerLogger()->trace( "{}: ignored: duplicate vote voter_id={}", __func__, vote.voter_id() ); return; } - if ( it->second.proposal.registry_cid() != registry_->GetRegistryCid() || - it->second.proposal.registry_epoch() != registry.epoch() ) + if ( proposal_state.proposal.registry_cid() != registry_->GetRegistryCid() || + proposal_state.proposal.registry_epoch() != registry.epoch() ) { ConsensusManagerLogger()->error( "{}: rejected: registry mismatch proposal_id={}", __func__, @@ -998,7 +929,7 @@ namespace sgns return; } - const auto *validator = FindValidator( registry, vote.voter_id() ); + const auto *validator = registry_->FindValidator( registry, vote.voter_id() ); if ( !validator || validator->status() != ValidatorRegistry::Status::ACTIVE ) { ConsensusManagerLogger()->error( "{}: rejected: validator not active voter_id={}", @@ -1015,9 +946,8 @@ namespace sgns it->second.votes.push_back( vote ); it->second.seen_voters.insert( vote.voter_id() ); it->second.approved_weight += validator->weight(); - state = it->second; - has_quorum = - registry_->IsQuorum( it->second.approved_weight, it->second.total_weight ); + state = it->second; + has_quorum = registry_->IsQuorum( it->second.approved_weight, it->second.total_weight ); } if ( !has_quorum ) @@ -1053,9 +983,7 @@ namespace sgns (void)SubmitCertificate( certificate_result.value() ); NotifyCertificate( state.proposal, certificate_result.value() ); - ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", - __func__, - vote.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", __func__, vote.proposal_id() ); } void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) @@ -1077,9 +1005,7 @@ namespace sgns void ConsensusManager::HandleCertificate( const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", - __func__, - certificate.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); if ( certificate_handler_ ) { certificate_handler_( certificate ); @@ -1100,11 +1026,10 @@ namespace sgns if ( proposal.proposal_id() != certificate.proposal_id() ) { - ConsensusManagerLogger()->error( - "{}: rejected: proposal_id mismatch cert={} proposal={}", - __func__, - certificate.proposal_id(), - proposal.proposal_id() ); + ConsensusManagerLogger()->error( "{}: rejected: proposal_id mismatch cert={} proposal={}", + __func__, + certificate.proposal_id(), + proposal.proposal_id() ); return; } @@ -1137,10 +1062,9 @@ namespace sgns proposal.signature(), signing_bytes.value() ) ) { - ConsensusManagerLogger()->error( - "{}: rejected: signature verification failed proposer_id={}", - __func__, - proposal.proposer_id() ); + ConsensusManagerLogger()->error( "{}: rejected: signature verification failed proposer_id={}", + __func__, + proposal.proposer_id() ); return; } @@ -1152,11 +1076,10 @@ namespace sgns } if ( computed_id != certificate.proposal_id() ) { - ConsensusManagerLogger()->error( - "{}: rejected: computed_id mismatch cert={} computed={}", - __func__, - certificate.proposal_id(), - computed_id ); + ConsensusManagerLogger()->error( "{}: rejected: computed_id mismatch cert={} computed={}", + __func__, + certificate.proposal_id(), + computed_id ); return; } @@ -1169,10 +1092,9 @@ namespace sgns { if ( it->second.certificate.has_value() ) { - ConsensusManagerLogger()->debug( - "{}: skipped: already have certificate proposal_id={}", - __func__, - certificate.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: skipped: already have certificate proposal_id={}", + __func__, + certificate.proposal_id() ); return; } state = it->second; @@ -1219,9 +1141,7 @@ namespace sgns votes.reserve( static_cast( certificate.votes_size() ) ); for ( const auto &vote : certificate.votes() ) { - ConsensusManagerLogger()->trace( "{}: processing vote voter_id={}", - __func__, - vote.voter_id() ); + ConsensusManagerLogger()->trace( "{}: processing vote voter_id={}", __func__, vote.voter_id() ); votes.push_back( vote ); } @@ -1247,16 +1167,12 @@ namespace sgns } NotifyCertificate( proposal, certificate ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", - __func__, - certificate.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); } void ConsensusManager::NotifyCertificate( const Proposal &proposal, const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", - __func__, - proposal.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); if ( certificate_callback_ ) { certificate_callback_( proposal, certificate ); @@ -1306,9 +1222,7 @@ namespace sgns outcome::result ConsensusManager::ComputeSubjectId( const Subject &subject ) { - ConsensusManagerLogger()->trace( "{}: called subject_type={}", - __func__, - static_cast( subject.type() ) ); + ConsensusManagerLogger()->trace( "{}: called subject_type={}", __func__, static_cast( subject.type() ) ); Subject copy = subject; copy.clear_subject_id(); std::string serialized; @@ -1329,10 +1243,7 @@ namespace sgns uint64_t nonce, const std::string &tx_hash ) { - ConsensusManagerLogger()->trace( "{}: called account_id={} nonce={}", - __func__, - account_id, - nonce ); + ConsensusManagerLogger()->trace( "{}: called account_id={} nonce={}", __func__, account_id, nonce ); Subject subject; subject.set_type( SubjectType::SUBJECT_NONCE ); subject.set_account_id( account_id ); @@ -1349,9 +1260,7 @@ namespace sgns return outcome::failure( subject_id.error() ); } subject.set_subject_id( subject_id.value() ); - ConsensusManagerLogger()->debug( "{}: success subject_id={}", - __func__, - subject.subject_id() ); + ConsensusManagerLogger()->debug( "{}: success subject_id={}", __func__, subject.subject_id() ); return subject; } @@ -1382,17 +1291,13 @@ namespace sgns return outcome::failure( subject_id.error() ); } subject.set_subject_id( subject_id.value() ); - ConsensusManagerLogger()->debug( "{}: success subject_id={}", - __func__, - subject.subject_id() ); + ConsensusManagerLogger()->debug( "{}: success subject_id={}", __func__, subject.subject_id() ); return subject; } std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", - __func__, - proposal.proposal_id() ); + ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); // Proposal ID must be derived from the proposal contents excluding the proposal_id itself. Proposal copy = proposal; copy.clear_proposal_id(); @@ -1414,9 +1319,7 @@ namespace sgns bool ConsensusManager::ValidateSubject( const Subject &subject ) { - ConsensusManagerLogger()->trace( "{}: called subject_type={}", - __func__, - static_cast( subject.type() ) ); + ConsensusManagerLogger()->trace( "{}: called subject_type={}", __func__, static_cast( subject.type() ) ); if ( subject.account_id().empty() ) { return false; @@ -1445,21 +1348,6 @@ namespace sgns return false; } - const ValidatorRegistry::ValidatorEntry *ConsensusManager::FindValidator( - const ValidatorRegistry::Registry ®istry, - const std::string &validator_id ) const - { - ConsensusManagerLogger()->trace( "{}: called validator_id={}", __func__, validator_id ); - for ( const auto &validator : registry.validators() ) - { - if ( validator.validator_id() == validator_id ) - { - return &validator; - } - } - return nullptr; - } - void ConsensusManager::OnConsensusMessage( boost::optional message ) { ConsensusManagerLogger()->trace( "{}: called", __func__ ); @@ -1559,4 +1447,49 @@ namespace sgns 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.registry_epoch() == 0 ) + { + ConsensusManagerLogger()->error( "{}: Registry EPOCH is zero ", __func__ ); + return false; + } + if ( !proposal.has_subject() ) + { + ConsensusManagerLogger()->error( "{}: Proposal without subject ", __func__ ); + 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; + } } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index c6536997e..05f710228 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -40,7 +40,6 @@ namespace sgns using VoteBundleHandler = std::function; using CertificateHandler = std::function; using CertificateCallback = std::function; - using ProposalValidator = std::function; enum class SubjectCheck { Approve, @@ -62,15 +61,12 @@ namespace sgns std::string address, std::string consensus_topic = "" ); - void SetProposalValidator( ProposalValidator validator ); bool RegisterSubjectHandler( SubjectType type, SubjectHandler handler ); void UnregisterSubjectHandler( SubjectType type ); void SetCertificateCallback( CertificateCallback callback ); outcome::result Publish( const ConsensusMessage &message ); - void SetProposalHandler( ProposalHandler handler ); - void SetVoteHandler( VoteHandler handler ); void SetVoteBundleHandler( VoteBundleHandler handler ); void SetCertificateHandler( CertificateHandler handler ); @@ -129,12 +125,12 @@ namespace sgns struct ProposalState { - Proposal proposal; - std::vector votes; - std::optional certificate; - std::string slot_key; - uint64_t total_weight = 0; - uint64_t approved_weight = 0; + Proposal proposal; + std::vector votes; + std::optional certificate; + std::string slot_key; + uint64_t total_weight = 0; + uint64_t approved_weight = 0; std::unordered_set seen_voters; }; @@ -160,18 +156,15 @@ namespace sgns static std::string CreateProposalId( const Proposal &proposal ); static bool ValidateSubject( const Subject &subject ); - const ValidatorRegistry::ValidatorEntry *FindValidator( const ValidatorRegistry::Registry ®istry, - const std::string &validator_id ) const; - - void OnConsensusMessage( boost::optional message ); - bool CheckSubject( const Subject &subject ); + void OnConsensusMessage( boost::optional message ); + static bool CheckSubject( const Subject &subject ); + static bool CheckProposal( const Proposal &proposal ); + static bool CheckVote( const Vote &vote ); std::shared_ptr registry_; - VoteHandler vote_handler_; VoteBundleHandler vote_bundle_handler_; CertificateHandler certificate_handler_; CertificateCallback certificate_callback_; - ProposalValidator proposal_validator_; std::unordered_map subject_handlers_; mutable std::shared_mutex subject_handlers_mutex_; Signer signer_; diff --git a/src/blockchain/ConsensusSigning.hpp b/src/blockchain/ConsensusSigning.hpp new file mode 100644 index 000000000..5579e343d --- /dev/null +++ b/src/blockchain/ConsensusSigning.hpp @@ -0,0 +1,52 @@ +/** + * @file ConsensusSigning.hpp + * @brief Header-only helpers for consensus signing bytes. + * @date 2026-02-07 + * @author Henrique A. Klein (hklein@gnus.ai) + */ +#pragma once + +#include +#include + +#include "blockchain/impl/proto/Consensus.pb.h" +#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() ); + } +} From 4337cbe6858e6308486ffc66b3d34f6c1843742c Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 9 Feb 2026 18:28:38 -0300 Subject: [PATCH 028/114] WIP: Adding registry creation from certificate --- src/blockchain/ValidatorRegistry.cpp | 598 ++++++++++++++++-- src/blockchain/ValidatorRegistry.hpp | 76 ++- .../impl/proto/ValidatorRegistry.proto | 3 + 3 files changed, 620 insertions(+), 57 deletions(-) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 25f904092..62fd2a2e8 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -8,13 +8,16 @@ #include #include +#include #include #include +#include #include #include #include "account/GeniusAccount.hpp" +#include "blockchain/ConsensusSigning.hpp" #include "blockchain/impl/proto/ValidatorRegistry.pb.h" #include "crdt/graphsync_dagsyncer.hpp" @@ -22,7 +25,7 @@ namespace sgns { namespace { - base::Logger validator_registry_logger() + base::Logger ValidatorRegistryLogger() { return base::createLogger( "ValidatorRegistry" ); } @@ -33,7 +36,7 @@ namespace sgns 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 +46,18 @@ namespace sgns 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 ); } + } ValidatorRegistry::ValidatorRegistry( std::shared_ptr db, @@ -143,28 +147,28 @@ namespace sgns 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; @@ -179,9 +183,9 @@ namespace sgns 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(); @@ -195,9 +199,9 @@ namespace sgns { 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 ) ); @@ -205,49 +209,49 @@ namespace sgns 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 @@ -299,6 +303,8 @@ namespace sgns 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; } @@ -467,6 +473,88 @@ namespace sgns return outcome::failure( std::errc::no_such_file_or_directory ); } + outcome::result ValidatorRegistry::CreateUpdateFromCertificate( + const sgns::ConsensusCertificate &certificate ) + { + logger_->trace( "{}: entry proposal_id={}", __func__, certificate.proposal_id() ); + auto registry_result = LoadRegistry(); + if ( registry_result.has_error() ) + { + logger_->error( "{}: failed to load registry: {}", __func__, registry_result.error().message() ); + return outcome::failure( registry_result.error() ); + } + + auto current_registry = registry_result.value(); + std::unordered_set approved; + std::unordered_set unregistered; + std::unordered_map registered_votes; + std::unordered_map unregistered_votes; + if ( !VerifyCertificateForUpdate( certificate, current_registry, approved, unregistered, registered_votes, unregistered_votes ) ) + { + logger_->error( "{}: invalid certificate", __func__ ); + return outcome::failure( std::errc::invalid_argument ); + } + + RegistryUpdate update; + update.set_prev_registry_hash( GetRegistryCid() ); + *update.mutable_registry() = BuildRegistryFromCertificate( current_registry, + certificate, + registered_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; + } + + outcome::result ValidatorRegistry::StoreRegistryUpdate( const RegistryUpdate &update ) + { + 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() ); + } + + base::Buffer update_buffer( + gsl::span( serialized_update.value().data(), serialized_update.value().size() ) ); + + 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(); + } + + void ValidatorRegistry::SetMaxNewValidatorsPerUpdate( size_t max_new ) + { + 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_ ); @@ -650,6 +738,66 @@ namespace sgns return false; } + if ( !update.certificate().empty() ) + { + sgns::ConsensusCertificate certificate; + if ( !certificate.ParseFromString( update.certificate() ) ) + { + logger_->error( "{}: invalid certificate payload", __func__ ); + return false; + } + + std::unordered_set approved; + std::unordered_set unregistered; + std::unordered_map registered_votes; + std::unordered_map unregistered_votes; + if ( !VerifyCertificateForUpdate( certificate, + *current_registry, + approved, + unregistered, + registered_votes, + unregistered_votes ) ) + { + logger_->error( "{}: certificate verification failed", __func__ ); + return false; + } + + Registry expected = BuildRegistryFromCertificate( *current_registry, + certificate, + registered_votes, + unregistered_votes ); + Registry provided = update.registry(); + NormalizeRegistry( provided ); + NormalizeRegistry( expected ); + + if ( provided.epoch() <= current_registry->epoch() ) + { + logger_->error( "{}: epoch not increasing", __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; { @@ -705,19 +853,393 @@ namespace sgns return false; } + bool ValidatorRegistry::VerifyCertificateForUpdate( + const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry, + std::unordered_set &approved_out, + std::unordered_set &unregistered_out, + std::unordered_map ®istered_votes_out, + std::unordered_map &unregistered_votes_out ) const + { + logger_->trace( "{}: entry proposal_id={}", __func__, certificate.proposal_id() ); + if ( certificate.proposal_id().empty() ) + { + logger_->error( "{}: empty proposal_id", __func__ ); + return false; + } + + if ( certificate.registry_epoch() != current_registry.epoch() ) + { + logger_->error( "{}: registry epoch mismatch cert={} registry={}", + __func__, + certificate.registry_epoch(), + current_registry.epoch() ); + return false; + } + + const std::string current_id = GetRegistryCid(); + if ( !current_id.empty() && !certificate.registry_cid().empty() && certificate.registry_cid() != current_id ) + { + logger_->error( "{}: registry CID mismatch cert={} registry={}", + __func__, + certificate.registry_cid(), + current_id ); + return false; + } + + uint64_t total_weight = TotalWeight( current_registry ); + uint64_t approved_weight = 0; + std::unordered_set seen; + + approved_out.clear(); + unregistered_out.clear(); + registered_votes_out.clear(); + unregistered_votes_out.clear(); + + 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 ) + { + unregistered_out.insert( vote.voter_id() ); + unregistered_votes_out[vote.voter_id()] = vote.approve(); + continue; + } + + registered_votes_out[vote.voter_id()] = vote.approve(); + + if ( vote.approve() && validator->status() == Status::ACTIVE ) + { + approved_weight += validator->weight(); + approved_out.insert( vote.voter_id() ); + } + } + + if ( !IsQuorum( approved_weight, total_weight ) ) + { + logger_->error( "{}: quorum not reached approved={} total={}", __func__, approved_weight, total_weight ); + return false; + } + + logger_->debug( "{}: quorum verified approved={} total={}", __func__, approved_weight, total_weight ); + return true; + } + + 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 + { + 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; + } + + logger_->debug( "{}: built registry from certificate proposal_id={} epoch={}", + __func__, + certificate.proposal_id(), + next.epoch() ); + 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 ); + ++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_; + 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 ); + } + } + } + + 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 new_weight = ( entry.weight() > dec ) ? ( entry.weight() - dec ) : 0; + entry.set_weight( new_weight ); + if ( new_weight == 0 ) + { + entry.set_status( Status::SUSPENDED ); + } + } + } + } + } + + 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; + } + + 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 scaled = ( entries[i].weight() * weight_cap ) / total_active; + entries[i].set_weight( scaled ); + scaled_sum += scaled; + active_indices.push_back( i ); + } + + 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(); + } + } + +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 b950d7506..7971e8dfb 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" @@ -35,23 +38,34 @@ 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; + 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_ = 5; + uint32_t inactivity_decrement_ = 1; + uint64_t total_weight_cap_multiplier_ = 4; }; static std::shared_ptr New( std::shared_ptr db, @@ -75,6 +89,9 @@ namespace sgns 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 ); + void SetMaxNewValidatorsPerUpdate( size_t max_new ); outcome::result> SerializeRegistry( const Registry ®istry ) const; outcome::result DeserializeRegistry( const std::vector &buffer ) const; @@ -98,6 +115,8 @@ namespace sgns return "gnus-validator-registry-cid"; } + static const ValidatorEntry *FindValidator( const Registry ®istry, const std::string &validator_id ); + protected: friend class sgns::Migration3_5_1To3_6_0; @@ -116,12 +135,30 @@ namespace sgns 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 ) const; + bool VerifyCertificateForUpdate( const sgns::ConsensusCertificate &certificate, + const Registry ¤t_registry, + std::unordered_set &approved_out, + std::unordered_set &unregistered_out, + std::unordered_map ®istered_votes_out, + std::unordered_map &unregistered_votes_out ) const; + Registry BuildRegistryFromCertificate( const Registry ¤t_registry, + const sgns::ConsensusCertificate &certificate, + 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(); + 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_; @@ -133,7 +170,8 @@ namespace sgns 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; InitCallback init_callback_; std::function )> callback )> diff --git a/src/blockchain/impl/proto/ValidatorRegistry.proto b/src/blockchain/impl/proto/ValidatorRegistry.proto index f9647a40a..38522adce 100644 --- a/src/blockchain/impl/proto/ValidatorRegistry.proto +++ b/src/blockchain/impl/proto/ValidatorRegistry.proto @@ -7,6 +7,8 @@ message ValidatorEntry { uint64 weight = 2; Role role = 3; Status status = 4; + uint32 penalty_score = 5; + uint32 missed_epochs = 6; } message Registry { @@ -36,6 +38,7 @@ message RegistryUpdate { Registry registry = 1; string prev_registry_hash = 2; repeated SignatureEntry signatures = 3; + bytes certificate = 4; } message RegistrySigningPayload { From 9fbef719d34c7594bc224374633a1797f09e3fd8 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 9 Feb 2026 18:38:11 -0300 Subject: [PATCH 029/114] Feat: Adding verification of proposal when creating a validator registry --- src/blockchain/Consensus.cpp | 2 +- src/blockchain/ConsensusAuth.hpp | 101 +++++++++++++++++++++++++++ src/blockchain/ConsensusSigning.hpp | 52 -------------- src/blockchain/ValidatorRegistry.cpp | 38 ++++++++-- 4 files changed, 135 insertions(+), 58 deletions(-) create mode 100644 src/blockchain/ConsensusAuth.hpp delete mode 100644 src/blockchain/ConsensusSigning.hpp diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index cd7791cd0..2054ccd39 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -17,7 +17,7 @@ #include "base/sgns_version.hpp" #include "crypto/hasher/hasher_impl.hpp" #include "account/GeniusAccount.hpp" -#include "blockchain/ConsensusSigning.hpp" +#include "blockchain/ConsensusAuth.hpp" namespace sgns { 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/ConsensusSigning.hpp b/src/blockchain/ConsensusSigning.hpp deleted file mode 100644 index 5579e343d..000000000 --- a/src/blockchain/ConsensusSigning.hpp +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @file ConsensusSigning.hpp - * @brief Header-only helpers for consensus signing bytes. - * @date 2026-02-07 - * @author Henrique A. Klein (hklein@gnus.ai) - */ -#pragma once - -#include -#include - -#include "blockchain/impl/proto/Consensus.pb.h" -#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() ); - } -} diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 62fd2a2e8..ee944e355 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -17,7 +17,7 @@ #include #include "account/GeniusAccount.hpp" -#include "blockchain/ConsensusSigning.hpp" +#include "blockchain/ConsensusAuth.hpp" #include "blockchain/impl/proto/ValidatorRegistry.pb.h" #include "crdt/graphsync_dagsyncer.hpp" @@ -862,27 +862,55 @@ namespace sgns std::unordered_map &unregistered_votes_out ) 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 ( certificate.proposal_id().empty() ) { logger_->error( "{}: empty proposal_id", __func__ ); return false; } - if ( certificate.registry_epoch() != current_registry.epoch() ) + if ( proposal.registry_epoch() != current_registry.epoch() ) { logger_->error( "{}: registry epoch mismatch cert={} registry={}", __func__, - certificate.registry_epoch(), + proposal.registry_epoch(), current_registry.epoch() ); return false; } const std::string current_id = GetRegistryCid(); - if ( !current_id.empty() && !certificate.registry_cid().empty() && certificate.registry_cid() != current_id ) + if ( !current_id.empty() && !proposal.registry_cid().empty() && proposal.registry_cid() != current_id ) { logger_->error( "{}: registry CID mismatch cert={} registry={}", __func__, - certificate.registry_cid(), + proposal.registry_cid(), current_id ); return false; } From 80a1e2de68d9a1a99b234159bb8e0a30097351d4 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 9 Feb 2026 19:15:53 -0300 Subject: [PATCH 030/114] Feat: Creating protection against attacks and out of order certificates --- src/blockchain/ValidatorRegistry.cpp | 130 ++++++++++++++++----------- src/blockchain/ValidatorRegistry.hpp | 25 ++++-- 2 files changed, 97 insertions(+), 58 deletions(-) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index ee944e355..22fa91aa8 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -485,22 +486,20 @@ namespace sgns } auto current_registry = registry_result.value(); - std::unordered_set approved; - std::unordered_set unregistered; - std::unordered_map registered_votes; - std::unordered_map unregistered_votes; - if ( !VerifyCertificateForUpdate( certificate, current_registry, approved, unregistered, registered_votes, unregistered_votes ) ) + if ( !ValidateCertificateForUpdate( certificate, current_registry ) ) { logger_->error( "{}: invalid certificate", __func__ ); return outcome::failure( std::errc::invalid_argument ); } + auto votes = ExtractCertificateVotes( certificate, current_registry ); + RegistryUpdate update; update.set_prev_registry_hash( GetRegistryCid() ); *update.mutable_registry() = BuildRegistryFromCertificate( current_registry, certificate, - registered_votes, - unregistered_votes ); + votes.registered_votes, + votes.unregistered_votes ); std::string serialized_cert; if ( !certificate.SerializeToString( &serialized_cert ) ) @@ -644,7 +643,7 @@ namespace sgns } } - if ( !VerifyUpdate( update, current_ptr ) ) + if ( !VerifyUpdate( update, current_ptr, false ) ) { logger_->error( "{}: verification failed, rejecting: {}", __func__, element.key() ); return std::vector{}; @@ -698,7 +697,9 @@ namespace sgns return std::vector( serialized.begin(), serialized.end() ); } - bool ValidatorRegistry::VerifyUpdate( const RegistryUpdate &update, const Registry *current_registry ) const + 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() ) @@ -747,32 +748,35 @@ namespace sgns return false; } - std::unordered_set approved; - std::unordered_set unregistered; - std::unordered_map registered_votes; - std::unordered_map unregistered_votes; - if ( !VerifyCertificateForUpdate( certificate, - *current_registry, - approved, - unregistered, - registered_votes, - unregistered_votes ) ) + if ( enforce_time_window ) { - logger_->error( "{}: certificate verification failed", __func__ ); - return false; + 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; + } } + auto votes = ExtractCertificateVotes( certificate, *current_registry ); Registry expected = BuildRegistryFromCertificate( *current_registry, certificate, - registered_votes, - unregistered_votes ); + votes.registered_votes, + votes.unregistered_votes ); Registry provided = update.registry(); NormalizeRegistry( provided ); NormalizeRegistry( expected ); - if ( provided.epoch() <= current_registry->epoch() ) + if ( provided.epoch() != current_registry->epoch() + 1 ) { - logger_->error( "{}: epoch not increasing", __func__ ); + logger_->error( "{}: epoch not next expected", __func__ ); return false; } @@ -811,9 +815,9 @@ namespace sgns return false; } - if ( update.registry().epoch() <= current_registry->epoch() ) + if ( update.registry().epoch() != current_registry->epoch() + 1 ) { - logger_->error( "{}: epoch not increasing", __func__ ); + logger_->error( "{}: epoch not next expected", __func__ ); return false; } @@ -853,13 +857,9 @@ namespace sgns return false; } - bool ValidatorRegistry::VerifyCertificateForUpdate( - const sgns::ConsensusCertificate &certificate, - const Registry ¤t_registry, - std::unordered_set &approved_out, - std::unordered_set &unregistered_out, - std::unordered_map ®istered_votes_out, - std::unordered_map &unregistered_votes_out ) const + 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() ) @@ -890,12 +890,6 @@ namespace sgns proposal.proposal_id() ); return false; } - if ( certificate.proposal_id().empty() ) - { - logger_->error( "{}: empty proposal_id", __func__ ); - return false; - } - if ( proposal.registry_epoch() != current_registry.epoch() ) { logger_->error( "{}: registry epoch mismatch cert={} registry={}", @@ -915,15 +909,45 @@ namespace sgns 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; - approved_out.clear(); - unregistered_out.clear(); - registered_votes_out.clear(); - unregistered_votes_out.clear(); - for ( const auto &vote : certificate.votes() ) { if ( vote.proposal_id() != certificate.proposal_id() ) @@ -948,28 +972,32 @@ namespace sgns const auto *validator = FindValidator( current_registry, vote.voter_id() ); if ( !validator ) { - unregistered_out.insert( vote.voter_id() ); - unregistered_votes_out[vote.voter_id()] = vote.approve(); + result.unregistered.insert( vote.voter_id() ); + result.unregistered_votes[vote.voter_id()] = vote.approve(); continue; } - registered_votes_out[vote.voter_id()] = vote.approve(); + result.registered_votes[vote.voter_id()] = vote.approve(); if ( vote.approve() && validator->status() == Status::ACTIVE ) { approved_weight += validator->weight(); - approved_out.insert( vote.voter_id() ); + result.approved.insert( vote.voter_id() ); } } if ( !IsQuorum( approved_weight, total_weight ) ) { logger_->error( "{}: quorum not reached approved={} total={}", __func__, approved_weight, total_weight ); - return false; + 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 true; + return result; } ValidatorRegistry::Registry ValidatorRegistry::BuildRegistryFromCertificate( diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index 7971e8dfb..ade5f89e7 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -66,6 +66,7 @@ namespace sgns uint32_t missed_epoch_threshold_ = 5; 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, @@ -124,6 +125,14 @@ namespace sgns 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, @@ -135,13 +144,15 @@ namespace sgns 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; - bool VerifyCertificateForUpdate( const sgns::ConsensusCertificate &certificate, - const Registry ¤t_registry, - std::unordered_set &approved_out, - std::unordered_set &unregistered_out, - std::unordered_map ®istered_votes_out, - std::unordered_map &unregistered_votes_out ) const; + 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, From e6cf3eb921aefa80d3e1dd2bc618c255f695613f Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 10 Feb 2026 17:43:55 -0300 Subject: [PATCH 031/114] Feat: Certificate posting on CRDT --- src/blockchain/Consensus.cpp | 334 +++++++++++++++--- src/blockchain/Consensus.hpp | 31 +- src/blockchain/impl/Blockchain.cpp | 1 + .../blockchain/consensus_certificate_test.cpp | 2 + 4 files changed, 322 insertions(+), 46 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 2054ccd39..7e0e04d1d 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -30,6 +31,7 @@ namespace sgns } std::shared_ptr ConsensusManager::New( std::shared_ptr registry, + std::shared_ptr db, std::shared_ptr pubsub, Signer signer, std::string address, @@ -40,6 +42,11 @@ namespace sgns 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__ ); @@ -57,13 +64,14 @@ namespace sgns } 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_topic_, + instance->consensus_messages_topic_, [weakptr( std::weak_ptr( instance ) )]( boost::optional message ) { @@ -71,29 +79,80 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: Received Consensus Message on topic {}", __func__, - self->consensus_topic_ ); + self->consensus_messages_topic_ ); self->OnConsensusMessage( message ); } } ) ); - ConsensusManagerLogger()->debug( "{}: Subscribed to Consensus topic {}", __func__, instance->consensus_topic_ ); + ConsensusManagerLogger()->debug( "{}: Subscribed to Consensus topic {}", + __func__, + instance->consensus_messages_topic_ ); + instance->StartRoundTimer(); 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_topic_( std::string( CONSENSUS_CHANNEL_PREFIX ) + sgns::version::GetNetAndVersionAppendix() + - consensus_topic ) + 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(); + if ( round_timer_.joinable() ) + { + round_timer_.join(); + } + } + + void ConsensusManager::StartRoundTimer() + { + if ( round_timer_.joinable() ) + { + 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; + } + if ( self->timer_cv_.wait_for( lock, interval, [self]() { return self->stop_timer_.load(); } ) ) + { + return; + } + lock.unlock(); + self->ProcessCertificates(); + } + } ); + } + void ConsensusManager::SetCertificateCallback( CertificateCallback callback ) { certificate_callback_ = std::move( callback ); @@ -108,8 +167,8 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - ConsensusManagerLogger()->debug( "{}: Sending consensus packet to {}", __func__, consensus_topic_ ); - pubsub_->Publish( consensus_topic_, serialized_proto ); + 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() ); @@ -159,6 +218,28 @@ namespace sgns 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; + } + bool ConsensusManager::IsTimestampSane( uint64_t timestamp_ms ) const { if ( timestamp_ms == 0 ) @@ -173,6 +254,71 @@ namespace sgns 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() ); + return static_cast( ( elapsed - skew_ms ) / round_ms ); + } + + 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() ); + return validators; + } + + bool ConsensusManager::IsCurrentAggregator( const Proposal &proposal, + const ValidatorRegistry::Registry ®istry ) const + { + 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 ) const { if ( subject.type() == SubjectType::SUBJECT_NONCE ) @@ -459,9 +605,21 @@ namespace sgns cert.set_registry_epoch( proposal.registry_epoch() ); cert.set_total_weight( tally.total_weight ); cert.set_approved_weight( tally.approved_weight ); - cert.set_timestamp( - std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) - .count() ); + 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; @@ -663,6 +821,48 @@ namespace sgns ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); return result; } + + if ( !db_ ) + { + ConsensusManagerLogger()->error( "{}: failed: db is null", __func__ ); + return outcome::failure( std::errc::not_supported ); + } + if ( !certificate.has_proposal() ) + { + ConsensusManagerLogger()->error( "{}: failed: certificate missing proposal proposal_id={}", + __func__, + certificate.proposal_id() ); + return outcome::failure( std::errc::invalid_argument ); + } + + auto subject_hash_result = GetSubjectHash( certificate.proposal().subject() ); + if ( subject_hash_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: subject hash error proposal_id={}", + __func__, + certificate.proposal_id() ); + 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 = "/cert/" + subject_hash_result.value(); + crdt::HierarchicalKey cert_key( key ); + crdt::GlobalDB::Buffer cert_value; + cert_value.put( serialized ); + + auto put_result = db_->Put( cert_key, cert_value, { consensus_datastore_topic_ } ); + if ( put_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: crdt put error={}", __func__, put_result.error().message() ); + return outcome::failure( put_result.error() ); + } + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); return result; } @@ -845,6 +1045,79 @@ namespace sgns return outcome::success(); } + void ConsensusManager::ProcessCertificates() + { + auto registry_result = registry_->LoadRegistry(); + if ( registry_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: aborted: registry load error={}", + __func__, + registry_result.error().message() ); + return; + } + const auto ®istry = registry_result.value(); + + std::vector to_process; + { + std::lock_guard lock( proposals_mutex_ ); + for ( auto &kv : proposals_ ) + { + auto &state = kv.second; + if ( !state.quorum_reached || state.certificate.has_value() ) + { + continue; + } + to_process.push_back( state ); + } + } + + for ( auto &state : to_process ) + { + const auto round = GetCurrentRound( state.proposal.timestamp() ); + if ( round == state.last_attempt_round ) + { + continue; + } + if ( !IsCurrentAggregator( state.proposal, registry ) ) + { + 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; + } + } + + auto certificate_result = CreateCertificate( state.proposal, state.votes ); + if ( certificate_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: certificate creation error={}", + __func__, + certificate_result.error().message() ); + continue; + } + + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( state.proposal.proposal_id() ); + if ( it != proposals_.end() && !it->second.certificate.has_value() ) + { + it->second.certificate = certificate_result.value(); + } + } + + (void)SubmitCertificate( certificate_result.value() ); + NotifyCertificate( state.proposal, certificate_result.value() ); + ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", + __func__, + state.proposal.proposal_id() ); + } + } + void ConsensusManager::HandleVote( const Vote &vote ) { ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={}", @@ -946,44 +1219,17 @@ namespace sgns it->second.votes.push_back( vote ); it->second.seen_voters.insert( vote.voter_id() ); it->second.approved_weight += validator->weight(); - state = it->second; has_quorum = registry_->IsQuorum( it->second.approved_weight, it->second.total_weight ); - } - - if ( !has_quorum ) - { - return; - } - - if ( state.certificate.has_value() ) - { - ConsensusManagerLogger()->debug( "{}: skipped: certificate already present proposal_id={}", - __func__, - vote.proposal_id() ); - return; - } - - auto certificate_result = CreateCertificate( state.proposal, state.votes ); - if ( certificate_result.has_error() ) - { - ConsensusManagerLogger()->error( "{}: failed: certificate creation error={}", - __func__, - certificate_result.error().message() ); - return; - } - - { - std::lock_guard lock( proposals_mutex_ ); - auto it = proposals_.find( vote.proposal_id() ); - if ( it != proposals_.end() ) + if ( has_quorum ) { - it->second.certificate = certificate_result.value(); + it->second.quorum_reached = true; + ConsensusManagerLogger()->debug( + "{}: quorum reached; certificate will be created by timer proposal_id={}", + __func__, + vote.proposal_id() ); } + state = it->second; } - - (void)SubmitCertificate( certificate_result.value() ); - NotifyCertificate( state.proposal, certificate_result.value() ); - ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", __func__, vote.proposal_id() ); } void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 05f710228..6fa9e8fab 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -13,13 +13,17 @@ #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 "ipfs_pubsub/gossip_pubsub.hpp" #include "outcome/outcome.hpp" @@ -28,6 +32,7 @@ namespace sgns class ConsensusManager : public std::enable_shared_from_this { public: + ~ConsensusManager(); using Proposal = ConsensusProposal; using Vote = ConsensusVote; using VoteBundle = ConsensusVoteBundle; @@ -56,6 +61,7 @@ namespace sgns }; static std::shared_ptr New( std::shared_ptr registry, + std::shared_ptr db, std::shared_ptr pubsub, Signer signer, std::string address, @@ -109,19 +115,26 @@ namespace sgns outcome::result SubmitVote( const Vote &vote ); outcome::result SubmitCertificate( const Certificate &certificate ); outcome::result ResumeProposalHandling( const std::string &subject_hash ); + void ProcessCertificates(); 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::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 ); struct ProposalState { @@ -132,6 +145,8 @@ namespace sgns uint64_t total_weight = 0; uint64_t approved_weight = 0; std::unordered_set seen_voters; + bool quorum_reached = false; + uint64_t last_attempt_round = 0; }; struct SlotState @@ -149,6 +164,10 @@ namespace sgns 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 GetSubjectHash( const Subject &subject ) const; void ContinueProposalAfterSubject( const Proposal &proposal ); void AddPendingProposal( const Proposal &proposal, const std::string &subject_hash ); @@ -162,6 +181,7 @@ namespace sgns static bool CheckProposal( const Proposal &proposal ); static bool CheckVote( const Vote &vote ); std::shared_ptr registry_; + std::shared_ptr db_; VoteBundleHandler vote_bundle_handler_; CertificateHandler certificate_handler_; CertificateCallback certificate_callback_; @@ -176,8 +196,15 @@ namespace sgns mutable std::mutex proposals_mutex_; std::shared_ptr pubsub_; - std::string consensus_topic_; + 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 timestamp_window_{ DEFAULT_TIMESTAMP_WINDOW }; + std::chrono::milliseconds round_duration_{ DEFAULT_ROUND_DURATION }; + std::chrono::milliseconds round_skew_{ DEFAULT_ROUND_SKEW }; + std::atomic stop_timer_{ false }; + std::condition_variable timer_cv_; + std::mutex timer_mutex_; + std::thread round_timer_; }; } diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 93f93ee52..24bb8b2d3 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -135,6 +135,7 @@ namespace sgns instance->consensus_manager_ = ConsensusManager::New( instance->validator_registry_, + instance->db_, std::move( pubsub ), [weak_ptr( std::weak_ptr( instance ) )]( std::vector payload ) -> outcome::result> diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index 44460fe33..70ec54f7e 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -73,6 +73,7 @@ namespace sgns::test auto manager = ConsensusManager::New( registry, + db_, pubs_, [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, account->GetAddress() ); @@ -109,6 +110,7 @@ namespace sgns::test auto manager = ConsensusManager::New( registry, + db_, pubs_, [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, account->GetAddress() ); From 2f08f61c1ad61a4dc4ecef93ba7f9beffe271169 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 11 Feb 2026 18:44:53 -0300 Subject: [PATCH 032/114] WIP: Adding CRDT entry of certificates --- src/account/TransactionManager.cpp | 13 +- src/blockchain/Blockchain.hpp | 3 +- src/blockchain/Consensus.cpp | 505 ++++++++++++------ src/blockchain/Consensus.hpp | 84 +-- src/blockchain/impl/Blockchain.cpp | 9 +- .../blockchain/consensus_certificate_test.cpp | 16 +- 6 files changed, 418 insertions(+), 212 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 2c8ce5bb7..b3d18540a 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -48,13 +48,18 @@ namespace sgns timestamp_tolerance, mutability_window ) ); - instance->blockchain_->SetCertificateCallback( - [weak_ptr( std::weak_ptr( instance ) )]( const ConsensusProposal &proposal, - const ConsensusCertificate &certificate ) + instance->blockchain_->RegisterCertificateHandler( + SubjectType::SUBJECT_NONCE, + [weak_ptr( std::weak_ptr( instance ) )]( + const std::string &subject_hash, const ConsensusCertificate &certificate ) { + (void)subject_hash; if ( auto strong = weak_ptr.lock() ) { - strong->OnConsensusCertificate( proposal, certificate ); + if ( certificate.has_proposal() ) + { + strong->OnConsensusCertificate( certificate.proposal(), certificate ); + } } } ); instance->blockchain_->RegisterSubjectHandler( diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index 5eebdf7f7..9cad4c48e 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -108,8 +108,9 @@ namespace sgns bool RegisterSubjectHandler( SubjectType type, ConsensusManager::SubjectHandler handler ); void UnregisterSubjectHandler( SubjectType type ); + bool RegisterCertificateHandler( SubjectType type, ConsensusManager::CertificateSubjectHandler handler ); + void UnregisterCertificateHandler( SubjectType type ); - void SetCertificateCallback( ConsensusManager::CertificateCallback callback ); outcome::result CreateConsensusProposal( const std::string &account_id, uint64_t nonce, diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 7e0e04d1d..a7a00ba4e 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -87,6 +87,10 @@ namespace sgns __func__, instance->consensus_messages_topic_ ); instance->StartRoundTimer(); + if ( !instance->RegisterCertificateFilter() ) + { + ConsensusManagerLogger()->error( "{}: Failed to register certificate filter", __func__ ); + } return instance; } @@ -153,11 +157,6 @@ namespace sgns } ); } - void ConsensusManager::SetCertificateCallback( CertificateCallback callback ) - { - certificate_callback_ = std::move( callback ); - } - outcome::result ConsensusManager::Publish( const ConsensusMessage &message ) { std::vector serialized_proto( message.ByteSizeLong() ); @@ -181,11 +180,6 @@ namespace sgns vote_bundle_handler_ = std::move( handler ); } - void ConsensusManager::SetCertificateHandler( CertificateHandler handler ) - { - certificate_handler_ = std::move( handler ); - } - bool ConsensusManager::RegisterSubjectHandler( SubjectType type, SubjectHandler handler ) { if ( !handler ) @@ -207,6 +201,29 @@ namespace sgns subject_handlers_.erase( static_cast( type ) ); } + bool ConsensusManager::RegisterCertificateHandler( SubjectType type, CertificateSubjectHandler handler ) + { + if ( !handler ) + { + ConsensusManagerLogger()->warn( "{}: ignored empty certificate handler type={}", + __func__, + static_cast( type ) ); + return false; + } + 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 ) @@ -631,7 +648,7 @@ namespace sgns } outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, - const std::vector &votes ) + const std::vector &votes ) const { ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", __func__, @@ -879,22 +896,6 @@ namespace sgns return; } - auto signing_bytes = ProposalSigningBytes( proposal ); - if ( signing_bytes.has_error() ) - { - ConsensusManagerLogger()->error( "{}: rejected: signing bytes error={}", - __func__, - signing_bytes.error().message() ); - return; - } - if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) - { - ConsensusManagerLogger()->error( "{}: rejected: signature verification failed proposer_id={}", - __func__, - proposal.proposer_id() ); - return; - } - if ( !IsTimestampSane( proposal.timestamp() ) ) { ConsensusManagerLogger()->error( "{}: rejected: timestamp out of bounds proposal_id={}", @@ -1063,7 +1064,7 @@ namespace sgns for ( auto &kv : proposals_ ) { auto &state = kv.second; - if ( !state.quorum_reached || state.certificate.has_value() ) + if ( !state.quorum_reached ) { continue; } @@ -1101,21 +1102,159 @@ namespace sgns continue; } + (void)SubmitCertificate( certificate_result.value() ); + ClearProposalState( state.proposal ); + ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", + __func__, + state.proposal.proposal_id() ); + } + } + + 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> { - std::lock_guard lock( proposals_mutex_ ); - auto it = proposals_.find( state.proposal.proposal_id() ); - if ( it != proposals_.end() && !it->second.certificate.has_value() ) + if ( auto strong = weak_self.lock() ) { - it->second.certificate = certificate_result.value(); + 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 ( !ValidateCertificateForCrdt( 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; + } + + HandleCertificate( certificate ); + + if ( !certificate.has_proposal() ) + { + return; + } + auto subject_hash = GetSubjectHash( certificate.proposal().subject() ); + if ( subject_hash.has_error() ) + { + return; + } + + 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; + } - (void)SubmitCertificate( certificate_result.value() ); - NotifyCertificate( state.proposal, certificate_result.value() ); - ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", - __func__, - state.proposal.proposal_id() ); + handler( subject_hash.value(), certificate ); + } + + bool ConsensusManager::ValidateCertificateForCrdt( const Certificate &certificate ) const + { + if ( !registry_ ) + { + return false; + } + if ( !certificate.has_proposal() ) + { + return false; + } + + const auto &proposal = certificate.proposal(); + if ( proposal.proposal_id() != certificate.proposal_id() ) + { + return false; + } + if ( proposal.registry_cid() != certificate.registry_cid() || + proposal.registry_epoch() != certificate.registry_epoch() ) + { + return false; + } + if ( !ValidateSubject( proposal.subject() ) ) + { + 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 = CreateProposalId( proposal ); + if ( computed_id.empty() || computed_id != certificate.proposal_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 ); + if ( tally.has_error() || !tally.value().has_quorum ) + { + return false; + } + + return true; } void ConsensusManager::HandleVote( const Vote &vote ) @@ -1252,137 +1391,87 @@ namespace sgns void ConsensusManager::HandleCertificate( const Certificate &certificate ) { ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); - if ( certificate_handler_ ) + + if ( !CheckCertificate( certificate ) ) { - certificate_handler_( certificate ); + ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", + __func__, + certificate.proposal_id() ); + return; } - if ( !registry_ ) + auto fetch_proposal_state_ret = FetchProposalState( certificate ); + if ( fetch_proposal_state_ret.has_error() ) { - ConsensusManagerLogger()->error( "{}: aborted: registry is null", __func__ ); + ConsensusManagerLogger()->error( "{}: rejected: Proposal state already has a certificate, proposal_id={}", + __func__, + certificate.proposal_id() ); return; } + auto &proposal_state = fetch_proposal_state_ret.value(); + auto &proposal = certificate.proposal(); - ProposalState state; - Proposal proposal; - bool have_proposal = false; - if ( certificate.has_proposal() ) + if ( !ValidateCertificateBestProposal( proposal_state, certificate ) ) { - 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; - } + return; + } - 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; - } + auto votes = CollectCertificateVotes( certificate ); + if ( !HasQuorumForCertificate( proposal, votes ) ) + { + return; + } - if ( !ValidateSubject( proposal.subject() ) ) - { - ConsensusManagerLogger()->error( "{}: rejected: invalid subject proposal_id={}", - __func__, - proposal.proposal_id() ); - return; - } + ClearProposalState( proposal ); + ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); + } - auto signing_bytes = ProposalSigningBytes( proposal ); - if ( signing_bytes.has_error() ) - { - ConsensusManagerLogger()->error( "{}: rejected: signing bytes error={}", - __func__, - signing_bytes.error().message() ); - return; - } - if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), - proposal.signature(), - signing_bytes.value() ) ) - { - ConsensusManagerLogger()->error( "{}: rejected: signature verification failed proposer_id={}", - __func__, - proposal.proposer_id() ); - return; - } + 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 it->second; + } - const auto computed_id = CreateProposalId( proposal ); - if ( computed_id.empty() ) - { - ConsensusManagerLogger()->error( "{}: rejected: computed_id empty", __func__ ); - return; - } - if ( computed_id != certificate.proposal_id() ) - { - ConsensusManagerLogger()->error( "{}: rejected: computed_id mismatch cert={} computed={}", - __func__, - certificate.proposal_id(), - computed_id ); - return; - } + 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 ); - have_proposal = true; - } + auto &slot_state = slot_states_[new_state.slot_key]; + if ( slot_state.best_proposal_id.empty() ) { - std::lock_guard lock( proposals_mutex_ ); - auto it = proposals_.find( certificate.proposal_id() ); - if ( it != proposals_.end() ) + slot_state.best_proposal_id = new_state.proposal.proposal_id(); + if ( new_state.proposal.subject().has_nonce() ) { - if ( it->second.certificate.has_value() ) - { - ConsensusManagerLogger()->debug( "{}: skipped: already have certificate proposal_id={}", - __func__, - certificate.proposal_id() ); - return; - } - state = it->second; - proposal = state.proposal; - have_proposal = true; - } - else if ( have_proposal ) - { - ProposalState new_state; - new_state.proposal = proposal; - new_state.slot_key = GetSlotKey( proposal ); - proposals_.emplace( proposal.proposal_id(), new_state ); - state = std::move( new_state ); - - auto &slot_state = slot_states_[state.slot_key]; - if ( slot_state.best_proposal_id.empty() ) - { - slot_state.best_proposal_id = proposal.proposal_id(); - if ( proposal.subject().has_nonce() ) - { - slot_state.best_tx_hash = proposal.subject().nonce().tx_hash(); - } - } - } - else - { - ConsensusManagerLogger()->error( "{}: aborted: missing proposal proposal_id={}", - __func__, - certificate.proposal_id() ); - return; + slot_state.best_tx_hash = new_state.proposal.subject().nonce().tx_hash(); } + } - 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; - } + return new_state; + } + + bool ConsensusManager::ValidateCertificateBestProposal( const ProposalState &state, + const Certificate &certificate ) const + { + 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() ) @@ -1390,7 +1479,11 @@ namespace sgns ConsensusManagerLogger()->trace( "{}: processing vote voter_id={}", __func__, vote.voter_id() ); votes.push_back( vote ); } + return votes; + } + bool ConsensusManager::HasQuorumForCertificate( const Proposal &proposal, const std::vector &votes ) const + { auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() || !tally_result.value().has_quorum ) { @@ -1400,28 +1493,36 @@ namespace sgns __func__, tally_result.error().message() ); } - return; + return false; } + return true; + } + void ConsensusManager::ClearProposalState( const Proposal &proposal ) + { + std::lock_guard lock( proposals_mutex_ ); + auto it = proposals_.find( proposal.proposal_id() ); + if ( it != proposals_.end() ) { - std::lock_guard lock( proposals_mutex_ ); - auto it = proposals_.find( certificate.proposal_id() ); - if ( it != proposals_.end() ) + const auto slot_key = it->second.slot_key; + proposals_.erase( it ); + auto slot_it = slot_states_.find( slot_key ); + if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id == proposal.proposal_id() ) { - it->second.certificate = certificate; + slot_states_.erase( slot_it ); } } - NotifyCertificate( proposal, certificate ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); - } + auto pending_it = pending_proposals_.find( proposal.proposal_id() ); + if ( pending_it != pending_proposals_.end() ) + { + pending_proposals_.erase( pending_it ); + } - void ConsensusManager::NotifyCertificate( const Proposal &proposal, const Certificate &certificate ) - { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); - if ( certificate_callback_ ) + for ( auto &kv : pending_by_subject_hash_ ) { - certificate_callback_( proposal, certificate ); + auto &vec = kv.second; + vec.erase( std::remove( vec.begin(), vec.end(), proposal.proposal_id() ), vec.end() ); } } @@ -1721,6 +1822,21 @@ namespace sgns 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; } @@ -1738,4 +1854,69 @@ namespace sgns } return true; } + + bool ConsensusManager::CheckCertificate( const Certificate &certificate ) + { + 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; + } + + 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; + } + + 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; + } + return true; + } } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 6fa9e8fab..d33d7624a 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -24,6 +24,7 @@ #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" @@ -39,12 +40,10 @@ namespace sgns using Certificate = ConsensusCertificate; using Subject = ConsensusSubject; - using Signer = std::function>( std::vector payload )>; - using ProposalHandler = std::function; - using VoteHandler = std::function; - using VoteBundleHandler = std::function; - using CertificateHandler = std::function; - using CertificateCallback = std::function; + using Signer = std::function>( std::vector payload )>; + using ProposalHandler = std::function; + using VoteHandler = std::function; + using VoteBundleHandler = std::function; enum class SubjectCheck { Approve, @@ -52,6 +51,8 @@ namespace sgns Pending }; using SubjectHandler = std::function( const Subject &subject )>; + using CertificateSubjectHandler = + std::function; struct QuorumTally { @@ -69,12 +70,12 @@ namespace sgns bool RegisterSubjectHandler( SubjectType type, SubjectHandler handler ); void UnregisterSubjectHandler( SubjectType type ); - void SetCertificateCallback( CertificateCallback callback ); + bool RegisterCertificateHandler( SubjectType type, CertificateSubjectHandler handler ); + void UnregisterCertificateHandler( SubjectType type ); outcome::result Publish( const ConsensusMessage &message ); void SetVoteBundleHandler( VoteBundleHandler handler ); - void SetCertificateHandler( CertificateHandler handler ); outcome::result CreateProposal( const Subject &subject, const std::string &proposer_id, @@ -98,7 +99,7 @@ namespace sgns outcome::result CreateCertificate( const Proposal &proposal, const std::vector &votes ); - outcome::result TallyVotes( const Proposal &proposal, const std::vector &votes ); + 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 ); @@ -133,19 +134,18 @@ namespace sgns static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; 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 std::chrono::milliseconds DEFAULT_ROUND_DURATION = std::chrono::milliseconds( 500 ); + static constexpr std::chrono::milliseconds DEFAULT_ROUND_SKEW = std::chrono::milliseconds( 250 ); struct ProposalState { Proposal proposal; std::vector votes; - std::optional certificate; std::string slot_key; uint64_t total_weight = 0; uint64_t approved_weight = 0; std::unordered_set seen_voters; - bool quorum_reached = false; + bool quorum_reached = false; uint64_t last_attempt_round = 0; }; @@ -156,37 +156,49 @@ namespace sgns bool voted = false; }; - void HandleProposal( const Proposal &proposal ); - void HandleVote( const Vote &vote ); - void HandleVoteBundle( const VoteBundle &bundle ); - void HandleCertificate( const Certificate &certificate ); - void NotifyCertificate( const Proposal &proposal, 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; + 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; + bool LoadProposalStateForCertificate( const Certificate &certificate, + ProposalState &state, + Proposal &proposal ); + + outcome::result FetchProposalState( const Certificate &certificate ); + bool ValidateCertificateBestProposal( const ProposalState &state, const Certificate &certificate ) const; + std::vector CollectCertificateVotes( const Certificate &certificate ) const; + bool HasQuorumForCertificate( const Proposal &proposal, const std::vector &votes ) const; + void ClearProposalState( const Proposal &proposal ); outcome::result GetSubjectHash( const Subject &subject ) const; void ContinueProposalAfterSubject( const Proposal &proposal ); void AddPendingProposal( const Proposal &proposal, const std::string &subject_hash ); std::vector TakePendingProposals( const std::string &subject_hash ); + bool RegisterCertificateFilter(); + std::optional> FilterCertificate( const crdt::pb::Element &element ); + void CertificateReceived( crdt::CRDTCallbackManager::NewDataPair new_data, const std::string &cid ); + bool ValidateCertificateForCrdt( const Certificate &certificate ) const; - static std::string CreateProposalId( const Proposal &proposal ); - static bool ValidateSubject( const Subject &subject ); + static std::string CreateProposalId( const Proposal &proposal ); + static bool ValidateSubject( const Subject &subject ); void OnConsensusMessage( boost::optional message ); static bool CheckSubject( const Subject &subject ); static bool CheckProposal( const Proposal &proposal ); static bool CheckVote( const Vote &vote ); + static bool CheckCertificate( const Certificate &certificate ); std::shared_ptr registry_; std::shared_ptr db_; VoteBundleHandler vote_bundle_handler_; - CertificateHandler certificate_handler_; - CertificateCallback certificate_callback_; 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_; @@ -199,12 +211,12 @@ namespace sgns 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 round_duration_{ DEFAULT_ROUND_DURATION }; - std::chrono::milliseconds round_skew_{ DEFAULT_ROUND_SKEW }; - std::atomic stop_timer_{ false }; - std::condition_variable timer_cv_; - std::mutex timer_mutex_; - std::thread round_timer_; + std::chrono::milliseconds timestamp_window_{ DEFAULT_TIMESTAMP_WINDOW }; + std::chrono::milliseconds round_duration_{ DEFAULT_ROUND_DURATION }; + std::chrono::milliseconds round_skew_{ DEFAULT_ROUND_SKEW }; + std::atomic stop_timer_{ false }; + std::condition_variable timer_cv_; + std::mutex timer_mutex_; + std::thread round_timer_; }; } diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 24bb8b2d3..6ab03f22a 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1537,9 +1537,14 @@ namespace sgns consensus_manager_->UnregisterSubjectHandler( type ); } - void Blockchain::SetCertificateCallback( ConsensusManager::CertificateCallback callback ) + bool Blockchain::RegisterCertificateHandler( SubjectType type, ConsensusManager::CertificateSubjectHandler handler ) { - consensus_manager_->SetCertificateCallback( std::move( callback ) ); + return consensus_manager_->RegisterCertificateHandler( type, std::move( handler ) ); + } + + void Blockchain::UnregisterCertificateHandler( SubjectType type ) + { + consensus_manager_->UnregisterCertificateHandler( type ); } outcome::result Blockchain::CreateConsensusProposal( const std::string &account_id, diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index 70ec54f7e..f3ecef19a 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -139,18 +139,20 @@ namespace sgns::test auto cert = cert_result.value(); - bool notified = false; - manager->SetCertificateCallback( [¬ified]( const ConsensusProposal &, const ConsensusCertificate & ) - { notified = true; } ); - + 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( notified ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) == manager->proposals_.end() ); - notified = false; + 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_FALSE( notified ); + EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) != manager->proposals_.end() ); } } // namespace sgns::test From 2ef69d9a27428f2f0f4085021b2f4109a384a6fa Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Feb 2026 17:26:39 -0300 Subject: [PATCH 033/114] Fix: Unified method to validate the certificate --- src/blockchain/Consensus.cpp | 104 ++++++++++------------------------- src/blockchain/Consensus.hpp | 7 +-- 2 files changed, 30 insertions(+), 81 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index a7a00ba4e..b2cf243ae 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -1152,7 +1152,7 @@ namespace sgns return std::vector{}; } - if ( !ValidateCertificateForCrdt( certificate ) ) + if ( !ValidateCertificate( certificate ) ) { ConsensusManagerLogger()->error( "{}: validation failed, rejecting: {}", __func__, element.key() ); return std::vector{}; @@ -1200,45 +1200,63 @@ namespace sgns handler( subject_hash.value(), certificate ); } - bool ConsensusManager::ValidateCertificateForCrdt( const Certificate &certificate ) const + bool ConsensusManager::ValidateCertificate( const Certificate &certificate ) const { - if ( !registry_ ) + 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; } if ( !ValidateSubject( proposal.subject() ) ) { + ConsensusManagerLogger()->error( "{}: rejected: invalid subject proposal_id={}", + __func__, + proposal.proposal_id() ); return false; } - - auto signing_bytes = ProposalSigningBytes( proposal ); - if ( signing_bytes.has_error() ) + if ( !CheckProposal( proposal ) ) { + ConsensusManagerLogger()->error( "{}: rejected: invalid proposal proposal_id={}", + __func__, + proposal.proposal_id() ); return false; } - if ( !GeniusAccount::VerifySignature( proposal.proposer_id(), proposal.signature(), signing_bytes.value() ) ) + + const auto computed_id = CreateProposalId( proposal ); + if ( computed_id.empty() ) { + ConsensusManagerLogger()->error( "{}: rejected: computed_id empty", __func__ ); return false; } - - auto computed_id = CreateProposalId( proposal ); - if ( computed_id.empty() || computed_id != certificate.proposal_id() ) + if ( computed_id != certificate.proposal_id() ) { + ConsensusManagerLogger()->error( "{}: rejected: computed_id mismatch cert={} computed={}", + __func__, + certificate.proposal_id(), + computed_id ); return false; } @@ -1392,7 +1410,7 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); - if ( !CheckCertificate( certificate ) ) + if ( !ValidateCertificate( certificate ) ) { ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", __func__, @@ -1855,68 +1873,4 @@ namespace sgns return true; } - bool ConsensusManager::CheckCertificate( const Certificate &certificate ) - { - 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; - } - - 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; - } - - 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; - } - return true; - } } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index d33d7624a..5b27b5ea3 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -166,10 +166,6 @@ namespace sgns 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; - bool LoadProposalStateForCertificate( const Certificate &certificate, - ProposalState &state, - Proposal &proposal ); - outcome::result FetchProposalState( const Certificate &certificate ); bool ValidateCertificateBestProposal( const ProposalState &state, const Certificate &certificate ) const; std::vector CollectCertificateVotes( const Certificate &certificate ) const; @@ -182,7 +178,7 @@ namespace sgns bool RegisterCertificateFilter(); std::optional> FilterCertificate( const crdt::pb::Element &element ); void CertificateReceived( crdt::CRDTCallbackManager::NewDataPair new_data, const std::string &cid ); - bool ValidateCertificateForCrdt( const Certificate &certificate ) const; + bool ValidateCertificate( const Certificate &certificate ) const; static std::string CreateProposalId( const Proposal &proposal ); static bool ValidateSubject( const Subject &subject ); @@ -191,7 +187,6 @@ namespace sgns static bool CheckSubject( const Subject &subject ); static bool CheckProposal( const Proposal &proposal ); static bool CheckVote( const Vote &vote ); - static bool CheckCertificate( const Certificate &certificate ); std::shared_ptr registry_; std::shared_ptr db_; VoteBundleHandler vote_bundle_handler_; From a679167068943784c4653d7b0b58ba8e692c5e1f Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 16 Feb 2026 09:32:37 -0300 Subject: [PATCH 034/114] Chore: Trimming code on the certificate callback --- src/blockchain/Consensus.cpp | 75 +++++++++++++++++++++--------------- src/blockchain/Consensus.hpp | 6 ++- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index b2cf243ae..f5d7e58bd 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -839,19 +839,6 @@ namespace sgns return result; } - if ( !db_ ) - { - ConsensusManagerLogger()->error( "{}: failed: db is null", __func__ ); - return outcome::failure( std::errc::not_supported ); - } - if ( !certificate.has_proposal() ) - { - ConsensusManagerLogger()->error( "{}: failed: certificate missing proposal proposal_id={}", - __func__, - certificate.proposal_id() ); - return outcome::failure( std::errc::invalid_argument ); - } - auto subject_hash_result = GetSubjectHash( certificate.proposal().subject() ); if ( subject_hash_result.has_error() ) { @@ -1173,13 +1160,15 @@ namespace sgns ConsensusManagerLogger()->error( "{}: invalid certificate payload key={}", __func__, key ); return; } - - HandleCertificate( certificate ); - - if ( !certificate.has_proposal() ) + + if ( !CheckCertificate( certificate ) ) { + ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", + __func__, + certificate.proposal_id() ); return; } + auto subject_hash = GetSubjectHash( certificate.proposal().subject() ); if ( subject_hash.has_error() ) { @@ -1410,7 +1399,7 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); - if ( !ValidateCertificate( certificate ) ) + if ( !CheckCertificate( certificate ) ) { ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", __func__, @@ -1418,24 +1407,24 @@ namespace sgns return; } + ProposalState & proposal_state; auto fetch_proposal_state_ret = FetchProposalState( certificate ); - if ( fetch_proposal_state_ret.has_error() ) + if ( fetch_proposal_state_ret.has_value() ) { - ConsensusManagerLogger()->error( "{}: rejected: Proposal state already has a certificate, proposal_id={}", + proposal_state = fetch_proposal_state_ret.value(); + ConsensusManagerLogger()->debug( "{}: fetched proposal state, proposal_id={}", __func__, certificate.proposal_id() ); - return; } - auto &proposal_state = fetch_proposal_state_ret.value(); - auto &proposal = certificate.proposal(); - - if ( !ValidateCertificateBestProposal( proposal_state, certificate ) ) + else { - return; + ConsensusManagerLogger()->debug( "{}: proposal state not found, creating new one proposal_id={}", + __func__, + certificate.proposal_id() ); + proposal_state = CreateProposalState( certificate ); } - auto votes = CollectCertificateVotes( certificate ); - if ( !HasQuorumForCertificate( proposal, votes ) ) + if ( !ValidateCertificateBestProposal( proposal_state, certificate ) ) { return; } @@ -1449,11 +1438,15 @@ namespace sgns { std::lock_guard lock( proposals_mutex_ ); auto it = proposals_.find( certificate.proposal_id() ); - if ( it != proposals_.end() ) + if ( it == proposals_.end() ) { - return it->second; + return outcome::failure( std::errc::not_found ); } + 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 ); @@ -1873,4 +1866,24 @@ namespace sgns return true; } -} + bool ConsensusManager::CheckCertificate( const Certificate &certificate ) + { + if ( !ValidateCertificate( certificate ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: invalid certificate, proposal_id={}", + __func__, + certificate.proposal_id() ); + return false; + } + + auto votes = CollectCertificateVotes( certificate ); + if ( !HasQuorumForCertificate( certificate.proposal(), votes ) ) + { + ConsensusManagerLogger()->error( "{}: rejected: Certificate without quorum, proposal_id={}", + __func__, + certificate.proposal_id() ); + return false; + } + + return true; + } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 5b27b5ea3..c834de311 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -164,9 +164,10 @@ namespace sgns 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; + 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; bool HasQuorumForCertificate( const Proposal &proposal, const std::vector &votes ) const; @@ -187,6 +188,7 @@ namespace sgns static bool CheckSubject( const Subject &subject ); static bool CheckProposal( const Proposal &proposal ); static bool CheckVote( const Vote &vote ); + static bool CheckCertificate( const Certificate &certificate ); std::shared_ptr registry_; std::shared_ptr db_; VoteBundleHandler vote_bundle_handler_; From 06ced633cb99a0245a42fbe16b30ab371c05b01e Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 16 Feb 2026 09:38:37 -0300 Subject: [PATCH 035/114] Fix: Consensus callback on transaction manager definition --- src/account/TransactionManager.cpp | 17 ++++++----------- src/account/TransactionManager.hpp | 5 +++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index b3d18540a..70a5ebd02 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -50,17 +50,15 @@ namespace sgns instance->blockchain_->RegisterCertificateHandler( SubjectType::SUBJECT_NONCE, - [weak_ptr( std::weak_ptr( instance ) )]( - const std::string &subject_hash, const ConsensusCertificate &certificate ) + [weak_ptr( std::weak_ptr( instance ) )]( const std::string &subject_hash, + const ConsensusCertificate &certificate ) { - (void)subject_hash; + (void)certificate; if ( auto strong = weak_ptr.lock() ) { - if ( certificate.has_proposal() ) - { - strong->OnConsensusCertificate( certificate.proposal(), certificate ); - } + return strong->OnConsensusCertificate( subject_hash ); } + eturn outcome::failure( std::errc::owner_dead ); } ); instance->blockchain_->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, @@ -2997,10 +2995,7 @@ namespace sgns return outcome::failure( std::errc::no_such_file_or_directory ); } - void TransactionManager::OnConsensusCertificate( const ConsensusProposal &proposal, - const ConsensusCertificate &certificate ) - { - } + void TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) {} outcome::result TransactionManager::HandleNonceConsensusSubject( const ConsensusManager::Subject &subject ) diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 03d212367..582bc303a 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -258,7 +258,7 @@ namespace sgns bool SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ); - void OnConsensusCertificate( const ConsensusProposal &proposal, const ConsensusCertificate &certificate ); + void OnConsensusCertificate( const std::string &tx_hash ); std::shared_ptr globaldb_m; @@ -367,7 +367,8 @@ namespace sgns public: outcome::result GetTransactionCID( const std::string &tx_hash ) const; - outcome::result HandleNonceConsensusSubject( const ConsensusManager::Subject &subject ); + 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; From 99f67062288c177d63cb91f9d7e365979fc27795 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 16 Feb 2026 16:06:20 -0300 Subject: [PATCH 036/114] Feat: Creating method to return content of a CID --- src/crdt/crdt_datastore.hpp | 5 ++++- src/crdt/globaldb/globaldb.cpp | 11 +++++++++++ src/crdt/globaldb/globaldb.hpp | 3 +++ src/crdt/impl/crdt_datastore.cpp | 23 +++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/crdt/crdt_datastore.hpp b/src/crdt/crdt_datastore.hpp index 06859f0b9..a77c3cceb 100644 --- a/src/crdt/crdt_datastore.hpp +++ b/src/crdt/crdt_datastore.hpp @@ -242,11 +242,14 @@ 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::ValidatorRegistry; - + struct RootCIDJob { std::shared_ptr node_; ///< Current node to process diff --git a/src/crdt/globaldb/globaldb.cpp b/src/crdt/globaldb/globaldb.cpp index a8a673050..1a2cbe579 100644 --- a/src/crdt/globaldb/globaldb.cpp +++ b/src/crdt/globaldb/globaldb.cpp @@ -444,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 5ba455912..5cfad94c5 100644 --- a/src/crdt/globaldb/globaldb.hpp +++ b/src/crdt/globaldb/globaldb.hpp @@ -178,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 efdd167d3..79fe26322 100644 --- a/src/crdt/impl/crdt_datastore.cpp +++ b/src/crdt/impl/crdt_datastore.cpp @@ -1767,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; + } } From f9f3f6ed1240bfa759ddcd17b96ebc28858ca45d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 16 Feb 2026 16:08:05 -0300 Subject: [PATCH 037/114] Feat: Created method to load an old registry --- src/blockchain/ValidatorRegistry.cpp | 102 +++++++++++++++++---------- src/blockchain/ValidatorRegistry.hpp | 77 ++++++++++---------- 2 files changed, 104 insertions(+), 75 deletions(-) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 22fa91aa8..fcb4fb459 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -255,9 +255,9 @@ namespace sgns 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() ) { @@ -267,29 +267,32 @@ namespace sgns } 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; } @@ -458,6 +461,35 @@ namespace sgns 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() ); + for ( auto &[key, registry_content] : cid_content ) + { + ValidatorRegistryLogger()->trace( "{}: Processing CID content key={}", __func__, key ); + if ( key != std::string( RegistryKey() ) ) + { + ValidatorRegistryLogger()->debug( "{}: Skipping non-registry content key={}", __func__, key ); + 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__ ); @@ -485,7 +517,7 @@ namespace sgns return outcome::failure( registry_result.error() ); } - auto current_registry = registry_result.value(); + auto current_registry = registry_result.value(); if ( !ValidateCertificateForUpdate( certificate, current_registry ) ) { logger_->error( "{}: invalid certificate", __func__ ); @@ -698,8 +730,8 @@ namespace sgns } bool ValidatorRegistry::VerifyUpdate( const RegistryUpdate &update, - const Registry *current_registry, - bool enforce_time_window ) const + const Registry *current_registry, + bool enforce_time_window ) const { logger_->trace( "{}: entry validators={}", __func__, update.registry().validators().size() ); if ( update.registry().validators().empty() ) @@ -765,7 +797,7 @@ namespace sgns } } - auto votes = ExtractCertificateVotes( certificate, *current_registry ); + auto votes = ExtractCertificateVotes( certificate, *current_registry ); Registry expected = BuildRegistryFromCertificate( *current_registry, certificate, votes.registered_votes, @@ -857,9 +889,8 @@ namespace sgns return false; } - bool ValidatorRegistry::ValidateCertificate( - const sgns::ConsensusCertificate &certificate, - const Registry ¤t_registry ) const + 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() ) @@ -885,9 +916,7 @@ namespace sgns if ( proposal.registry_epoch() != certificate.registry_epoch() || proposal.registry_cid() != certificate.registry_cid() ) { - logger_->error( "{}: registry metadata mismatch proposal_id={}", - __func__, - proposal.proposal_id() ); + logger_->error( "{}: registry metadata mismatch proposal_id={}", __func__, proposal.proposal_id() ); return false; } if ( proposal.registry_epoch() != current_registry.epoch() ) @@ -918,9 +947,8 @@ namespace sgns return true; } - bool ValidatorRegistry::ValidateCertificateForUpdate( - const sgns::ConsensusCertificate &certificate, - const Registry ¤t_registry ) const + 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 ) @@ -943,7 +971,7 @@ namespace sgns const sgns::ConsensusCertificate &certificate, const Registry ¤t_registry ) const { - CertificateVotes result; + CertificateVotes result; uint64_t total_weight = TotalWeight( current_registry ); uint64_t approved_weight = 0; std::unordered_set seen; @@ -1050,7 +1078,7 @@ namespace sgns return next; } - void ValidatorRegistry::InsertNewValidators( Registry ®istry, + void ValidatorRegistry::InsertNewValidators( Registry ®istry, const std::unordered_map &unregistered_votes ) const { std::vector new_ids; @@ -1077,7 +1105,7 @@ namespace sgns entry->set_role( Role::REGULAR ); entry->set_status( Status::ACTIVE ); entry->set_weight( ComputeWeight( entry->role() ) ); - auto it = unregistered_votes.find( validator_id ); + 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 ); @@ -1085,9 +1113,8 @@ namespace sgns } } - void ValidatorRegistry::ApplyVoteEffects( - std::vector &entries, - const std::unordered_map ®istered_votes ) const + void ValidatorRegistry::ApplyVoteEffects( std::vector &entries, + const std::unordered_map ®istered_votes ) const { for ( auto &entry : entries ) { @@ -1145,8 +1172,9 @@ namespace sgns { if ( entry.status() == Status::BLACKLISTED ) { - const uint32_t bumped = - std::min( cap, static_cast( penalty + weight_config_.blacklist_bump_ ) ); + const uint32_t bumped = std::min( + cap, + static_cast( penalty + weight_config_.blacklist_bump_ ) ); penalty = bumped; } else @@ -1158,8 +1186,9 @@ namespace sgns 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_ ) ); + const uint32_t bumped = std::min( + cap, + static_cast( penalty + weight_config_.blacklist_bump_ ) ); penalty = bumped; } } @@ -1168,7 +1197,7 @@ namespace sgns } } - void ValidatorRegistry::ApplyInactivityDecay( std::vector &entries, + void ValidatorRegistry::ApplyInactivityDecay( std::vector &entries, const std::unordered_set &participants ) const { for ( auto &entry : entries ) @@ -1215,14 +1244,13 @@ namespace sgns } } - const uint64_t weight_cap = - weight_config_.genesis_weight_ * weight_config_.total_weight_cap_multiplier_; + const uint64_t weight_cap = weight_config_.genesis_weight_ * weight_config_.total_weight_cap_multiplier_; if ( weight_cap == 0 || total_active <= weight_cap ) { return; } - uint64_t scaled_sum = 0; + uint64_t scaled_sum = 0; std::vector active_indices; active_indices.reserve( entries.size() ); for ( size_t i = 0; i < entries.size(); ++i ) @@ -1258,11 +1286,11 @@ namespace sgns { entries[active_indices[idx]].set_weight( entries[active_indices[idx]].weight() + 1 ); remainder -= 1; - idx = ( idx + 1 ) % active_indices.size(); + idx = ( idx + 1 ) % active_indices.size(); } } -void ValidatorRegistry::NormalizeRegistry( Registry ®istry ) + void ValidatorRegistry::NormalizeRegistry( Registry ®istry ) { std::vector entries; entries.reserve( static_cast( registry.validators_size() ) ); diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index ade5f89e7..bc8b279e7 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -51,21 +51,21 @@ namespace sgns struct WeightConfig { - 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_ = 5; - uint32_t inactivity_decrement_ = 1; - uint64_t total_weight_cap_multiplier_ = 4; + 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_ = 5; + uint32_t inactivity_decrement_ = 1; + uint64_t total_weight_cap_multiplier_ = 4; uint64_t certificate_timestamp_window_ms_ = 300000; }; @@ -78,15 +78,16 @@ namespace sgns 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(); @@ -144,27 +145,27 @@ namespace sgns 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, - 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; + 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; - 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 ); + Registry BuildRegistryFromCertificate( const Registry ¤t_registry, + const sgns::ConsensusCertificate &certificate, + 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(); void NotifyInitialized( bool success ) const; From 00131536a6ba0380af0fb7fac1f5cb92e678181a Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 16 Feb 2026 16:09:10 -0300 Subject: [PATCH 038/114] Feat: Certificate callback implemented in transaction manager --- src/account/TransactionManager.cpp | 80 ++++++++++++++++++++++++++++-- src/account/TransactionManager.hpp | 1 + 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 70a5ebd02..1c1c0e284 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -56,9 +56,8 @@ namespace sgns (void)certificate; if ( auto strong = weak_ptr.lock() ) { - return strong->OnConsensusCertificate( subject_hash ); + strong->OnConsensusCertificate( subject_hash ); } - eturn outcome::failure( std::errc::owner_dead ); } ); instance->blockchain_->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, @@ -2171,6 +2170,21 @@ namespace sgns 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 ); @@ -2995,7 +3009,67 @@ namespace sgns return outcome::failure( std::errc::no_such_file_or_directory ); } - void TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) {} + void TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) + { + auto tracked_tx_ret = GetTrackedTxByHash( tx_hash ); + if ( !tracked_tx_ret ) + { + m_logger->error( "[{} - full: {}] {}: Tracked transaction not found for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return; + } + auto &tracked_tx = tracked_tx_ret.value(); + if ( tracked_tx.status != TransactionStatus::FAILED && tracked_tx.status != TransactionStatus::INVALID && + tracked_tx.status != TransactionStatus::CONFIRMED ) + { + m_logger->debug( "[{} - full: {}] {}: Transaction {} is {}, updating status to CONFIRMED", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + static_cast( tracked_tx.status ) ); + tracked_tx.status = TransactionStatus::CONFIRMED; + if ( tracked_tx.tx ) + { + m_logger->debug( "[{} - full: {}] {}: Parsing transaction {} after consensus certificate", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + auto parse_result = ParseTransaction( tracked_tx.tx ); + if ( parse_result.has_error() ) + { + m_logger->error( + "[{} - full: {}] {}: Failed to parse transaction {} after consensus certificate: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + parse_result.error().message() ); + } + } + else + { + m_logger->error( "[{} - full: {}] {}: Tracked transaction missing for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + } + } + else + { + m_logger->debug( "[{} - full: {}] {}: Transaction {} has status {}, not updating to APPROVED", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash, + static_cast( tracked_tx.status ) ); + } + } outcome::result TransactionManager::HandleNonceConsensusSubject( const ConsensusManager::Subject &subject ) diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 582bc303a..cf6830dcc 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -255,6 +255,7 @@ namespace sgns std::shared_ptr GetTransactionByNonceAndAddress( uint64_t nonce, const std::string &address ) const; std::optional GetTrackedTxByNonceAndAddress( uint64_t nonce, const std::string &address ) const; + std::optional GetTrackedTxByHash( const std::string &tx_hash ) const; bool SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ); From d5b0a93c7eac5843bd0ffc7c7f7713d36e6cec7d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 16 Feb 2026 16:11:18 -0300 Subject: [PATCH 039/114] Fix: Validation of a certificate now works with old registries --- src/blockchain/Consensus.cpp | 79 ++++++++++++++++++++++-------------- src/blockchain/Consensus.hpp | 6 ++- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index f5d7e58bd..fff4bbf1e 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -647,30 +647,16 @@ namespace sgns return cert; } - outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, - const std::vector &votes ) const + outcome::result ConsensusManager::TallyVotes( + const Proposal &proposal, + const std::vector &votes, + const ValidatorRegistry::Registry ®istry, + const std::string ®istry_cid ) const { - ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", + ConsensusManagerLogger()->error( "{}: failed: registry cid mismatch proposal={} registry={}", __func__, - proposal.proposal_id(), - votes.size() ); - if ( !registry_ ) - { - ConsensusManagerLogger()->error( "{}: failed: registry is null", __func__ ); - return outcome::failure( std::errc::not_supported ); - } - - auto registry_result = registry_->LoadRegistry(); - if ( registry_result.has_error() ) - { - ConsensusManagerLogger()->error( "{}: failed: registry load error={}", - __func__, - registry_result.error().message() ); - return outcome::failure( registry_result.error() ); - } - - const auto ®istry = registry_result.value(); - const auto registry_cid = registry_->GetRegistryCid(); + proposal.registry_cid(), + registry_cid ); if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { ConsensusManagerLogger()->error( "{}: failed: registry cid mismatch proposal={} registry={}", @@ -688,7 +674,7 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - uint64_t total_weight = registry_->TotalWeight( registry ); + uint64_t total_weight = ValidatorRegistry::TotalWeight( registry ); uint64_t approved_weight = 0; std::set seen; @@ -707,7 +693,7 @@ namespace sgns continue; } - const auto *validator = registry_->FindValidator( registry, vote.voter_id() ); + const auto *validator = ValidatorRegistry::FindValidator( registry, vote.voter_id() ); if ( !validator || validator->status() != ValidatorRegistry::Status::ACTIVE ) { continue; @@ -743,6 +729,25 @@ namespace sgns return tally; } + outcome::result ConsensusManager::TallyVotes( const Proposal &proposal, + const std::vector &votes ) const + { + ConsensusManagerLogger()->trace( "{}: Tallying with current registry, proposal_id={} votes={}", + __func__, + proposal.proposal_id(), + votes.size() ); + + auto registry_result = registry_->LoadRegistry(); + if ( registry_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: registry load error={}", + __func__, + registry_result.error().message() ); + return outcome::failure( registry_result.error() ); + } + return TallyVotes( proposal, votes, registry_result.value(), registry_->GetRegistryCid() ); + } + outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) { ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); @@ -1160,7 +1165,7 @@ namespace sgns ConsensusManagerLogger()->error( "{}: invalid certificate payload key={}", __func__, key ); return; } - + if ( !CheckCertificate( certificate ) ) { ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", @@ -1219,6 +1224,17 @@ namespace sgns 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={}", @@ -1255,7 +1271,7 @@ namespace sgns { votes.push_back( vote ); } - auto tally = TallyVotes( proposal, votes ); + auto tally = TallyVotes( proposal, votes, registry, certificate.registry_cid() ); if ( tally.has_error() || !tally.value().has_quorum ) { return false; @@ -1407,8 +1423,8 @@ namespace sgns return; } - ProposalState & proposal_state; - auto fetch_proposal_state_ret = FetchProposalState( certificate ); + ProposalState proposal_state; + auto fetch_proposal_state_ret = FetchProposalState( certificate ); if ( fetch_proposal_state_ret.has_value() ) { proposal_state = fetch_proposal_state_ret.value(); @@ -1429,7 +1445,7 @@ namespace sgns return; } - ClearProposalState( proposal ); + ClearProposalState( certificate.proposal() ); ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); } @@ -1440,7 +1456,7 @@ namespace sgns auto it = proposals_.find( certificate.proposal_id() ); if ( it == proposals_.end() ) { - return outcome::failure( std::errc::not_found ); + return outcome::failure( std::errc::no_such_device ); } return it->second; } @@ -1866,7 +1882,7 @@ namespace sgns return true; } - bool ConsensusManager::CheckCertificate( const Certificate &certificate ) + bool ConsensusManager::CheckCertificate( const Certificate &certificate ) const { if ( !ValidateCertificate( certificate ) ) { @@ -1887,3 +1903,4 @@ namespace sgns return true; } +} diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index c834de311..10a138bf1 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -99,6 +99,10 @@ namespace sgns 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 ); @@ -188,7 +192,7 @@ namespace sgns static bool CheckSubject( const Subject &subject ); static bool CheckProposal( const Proposal &proposal ); static bool CheckVote( const Vote &vote ); - static bool CheckCertificate( const Certificate &certificate ); + bool CheckCertificate( const Certificate &certificate ) const; std::shared_ptr registry_; std::shared_ptr db_; VoteBundleHandler vote_bundle_handler_; From 5e2a90554df489f952191077a397ca538dbb7839 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 16 Feb 2026 16:34:47 -0300 Subject: [PATCH 040/114] Chore: Cleanup of clutter --- src/blockchain/Consensus.cpp | 58 ++---------------------------------- src/blockchain/Consensus.hpp | 10 +------ 2 files changed, 3 insertions(+), 65 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index fff4bbf1e..61b5264a3 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -174,12 +174,6 @@ namespace sgns return outcome::success(); } - - void ConsensusManager::SetVoteBundleHandler( VoteBundleHandler handler ) - { - vote_bundle_handler_ = std::move( handler ); - } - bool ConsensusManager::RegisterSubjectHandler( SubjectType type, SubjectHandler handler ) { if ( !handler ) @@ -1166,14 +1160,6 @@ namespace sgns return; } - if ( !CheckCertificate( certificate ) ) - { - ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", - __func__, - certificate.proposal_id() ); - return; - } - auto subject_hash = GetSubjectHash( certificate.proposal().subject() ); if ( subject_hash.has_error() ) { @@ -1400,10 +1386,7 @@ namespace sgns __func__, bundle.proposal_id(), bundle.votes_size() ); - if ( vote_bundle_handler_ ) - { - vote_bundle_handler_( bundle ); - } + for ( const auto &vote : bundle.votes() ) { ConsensusManagerLogger()->trace( "{}: processing voter_id={}", __func__, vote.voter_id() ); @@ -1415,7 +1398,7 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); - if ( !CheckCertificate( certificate ) ) + if ( !ValidateCertificate( certificate ) ) { ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", __func__, @@ -1509,22 +1492,6 @@ namespace sgns return votes; } - bool ConsensusManager::HasQuorumForCertificate( const Proposal &proposal, const std::vector &votes ) const - { - auto tally_result = TallyVotes( proposal, votes ); - if ( tally_result.has_error() || !tally_result.value().has_quorum ) - { - if ( tally_result.has_error() ) - { - ConsensusManagerLogger()->error( "{}: aborted: tally error={}", - __func__, - tally_result.error().message() ); - } - return false; - } - return true; - } - void ConsensusManager::ClearProposalState( const Proposal &proposal ) { std::lock_guard lock( proposals_mutex_ ); @@ -1882,25 +1849,4 @@ namespace sgns return true; } - bool ConsensusManager::CheckCertificate( const Certificate &certificate ) const - { - if ( !ValidateCertificate( certificate ) ) - { - ConsensusManagerLogger()->error( "{}: rejected: invalid certificate, proposal_id={}", - __func__, - certificate.proposal_id() ); - return false; - } - - auto votes = CollectCertificateVotes( certificate ); - if ( !HasQuorumForCertificate( certificate.proposal(), votes ) ) - { - ConsensusManagerLogger()->error( "{}: rejected: Certificate without quorum, proposal_id={}", - __func__, - certificate.proposal_id() ); - return false; - } - - return true; - } } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 10a138bf1..fb9387039 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -41,9 +41,6 @@ namespace sgns using Subject = ConsensusSubject; using Signer = std::function>( std::vector payload )>; - using ProposalHandler = std::function; - using VoteHandler = std::function; - using VoteBundleHandler = std::function; enum class SubjectCheck { Approve, @@ -75,8 +72,6 @@ namespace sgns outcome::result Publish( const ConsensusMessage &message ); - void SetVoteBundleHandler( VoteBundleHandler handler ); - outcome::result CreateProposal( const Subject &subject, const std::string &proposer_id, const std::string ®istry_cid, @@ -174,7 +169,6 @@ namespace sgns ProposalState CreateProposalState( const Certificate &certificate ); bool ValidateCertificateBestProposal( const ProposalState &state, const Certificate &certificate ) const; std::vector CollectCertificateVotes( const Certificate &certificate ) const; - bool HasQuorumForCertificate( const Proposal &proposal, const std::vector &votes ) const; void ClearProposalState( const Proposal &proposal ); outcome::result GetSubjectHash( const Subject &subject ) const; void ContinueProposalAfterSubject( const Proposal &proposal ); @@ -192,10 +186,8 @@ namespace sgns static bool CheckSubject( const Subject &subject ); static bool CheckProposal( const Proposal &proposal ); static bool CheckVote( const Vote &vote ); - bool CheckCertificate( const Certificate &certificate ) const; std::shared_ptr registry_; std::shared_ptr db_; - VoteBundleHandler vote_bundle_handler_; std::unordered_map subject_handlers_; mutable std::shared_mutex subject_handlers_mutex_; std::unordered_map certificate_subject_handlers_; @@ -220,4 +212,4 @@ namespace sgns std::mutex timer_mutex_; std::thread round_timer_; }; -} +} \ No newline at end of file From 107d13d8b69e78a9e599ccf36da5c940290ac950 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 17 Feb 2026 15:17:50 -0300 Subject: [PATCH 041/114] Fix: Method to get the nonce --- src/account/IGeniusTransactions.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/account/IGeniusTransactions.hpp b/src/account/IGeniusTransactions.hpp index 8e593fed1..87662cfb1 100644 --- a/src/account/IGeniusTransactions.hpp +++ b/src/account/IGeniusTransactions.hpp @@ -91,6 +91,11 @@ namespace sgns return dag_st.timestamp(); } + uint64_t GetNonce() const + { + return dag_st.nonce(); + } + virtual std::unordered_set GetTopics() const; void FillHash(); From 5b0d2cd4eafaec19a9289e0a314d29c5dd921786 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 18 Feb 2026 18:08:50 -0300 Subject: [PATCH 042/114] Feat: Created method to check the stored certificate of a subject --- src/blockchain/Blockchain.hpp | 2 ++ src/blockchain/Consensus.cpp | 36 +++++++++++++++++++++++++++++- src/blockchain/Consensus.hpp | 21 +++++++++-------- src/blockchain/impl/Blockchain.cpp | 5 +++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index 9cad4c48e..b02a5a46e 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -120,6 +120,8 @@ namespace sgns outcome::result TryResumeProposal( const std::string &hash ); + bool CheckCertificate(const std::string &subject_hash) const; + protected: friend class Migration3_5_1To3_6_0; diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 61b5264a3..2bfe6374d 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -174,6 +174,7 @@ namespace sgns return outcome::success(); } + bool ConsensusManager::RegisterSubjectHandler( SubjectType type, SubjectHandler handler ) { if ( !handler ) @@ -854,7 +855,7 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - const auto key = "/cert/" + subject_hash_result.value(); + const auto key = CERTIFICATE_BASE_PATH_KEY + subject_hash_result.value(); crdt::HierarchicalKey cert_key( key ); crdt::GlobalDB::Buffer cert_value; cert_value.put( serialized ); @@ -1849,4 +1850,37 @@ namespace sgns return true; } + outcome::result ConsensusManager::GetCertificateBySubjectHash( const std::string &subject_hash ) const + { + const auto key = CERTIFICATE_BASE_PATH_KEY + subject_hash; + + OUTCOME_TRY( auto certificate_data, db_->Get( { key } ) ); + + Certificate certificate; + if ( !certificate.ParseFromArray( value.data(), value.size() ) ) + { + ConsensusManagerLogger()->error( "{}: invalid certificate payload key={}", __func__, key ); + return outcome::failure( std::errc::invalid_argument ); + } + + auto subject_hash = GetSubjectHash( certificate.proposal().subject() ); + if ( subject_hash.has_error() ) + { + return outcome::failure( subject_hash.error() ); + } + + 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; + } + } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index fb9387039..0efe9fc1d 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -40,7 +40,7 @@ namespace sgns using Certificate = ConsensusCertificate; using Subject = ConsensusSubject; - using Signer = std::function>( std::vector payload )>; + using Signer = std::function>( std::vector payload )>; enum class SubjectCheck { Approve, @@ -117,6 +117,8 @@ namespace sgns outcome::result ResumeProposalHandling( const std::string &subject_hash ); void ProcessCertificates(); + outcome::result GetCertificateBySubjectHash( const std::string &subject_hash ) const; + protected: void ConfigureTimestampWindow( std::chrono::milliseconds window ); void ConfigureRoundDuration( std::chrono::milliseconds duration ); @@ -131,10 +133,11 @@ namespace sgns std::string consensus_topic ); void StartRoundTimer(); - static constexpr std::string_view CONSENSUS_CHANNEL_PREFIX = "consensus-channel-"; - 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 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 ); struct ProposalState { @@ -168,8 +171,8 @@ namespace sgns 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 ClearProposalState( const Proposal &proposal ); + std::vector CollectCertificateVotes( const Certificate &certificate ) const; + void ClearProposalState( const Proposal &proposal ); outcome::result GetSubjectHash( const Subject &subject ) const; void ContinueProposalAfterSubject( const Proposal &proposal ); void AddPendingProposal( const Proposal &proposal, const std::string &subject_hash ); @@ -178,7 +181,7 @@ namespace sgns 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; - + bool CheckCertificateForSubject( const std::string &subject_hash ) const; static std::string CreateProposalId( const Proposal &proposal ); static bool ValidateSubject( const Subject &subject ); @@ -212,4 +215,4 @@ namespace sgns std::mutex timer_mutex_; std::thread round_timer_; }; -} \ No newline at end of file +} diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 6ab03f22a..cf6691e67 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1571,4 +1571,9 @@ namespace sgns return consensus_manager_->ResumeProposalHandling( hash ); } + bool Blockchain::CheckCertificate( const std::string &subject_hash ) const + { + return consensus_manager_->CheckCertificateForSubject( subject_hash ); + } + } From 9d75f0ab42ae39cb333b7dcc648524a2ba49e2ab Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 19 Feb 2026 16:54:57 -0300 Subject: [PATCH 043/114] Chore: removing unecessary method --- src/account/GeniusAccount.cpp | 121 ++++++++++++++++++++++++++++++++-- src/account/GeniusAccount.hpp | 6 -- 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/account/GeniusAccount.cpp b/src/account/GeniusAccount.cpp index 5dba5735a..348a2a059 100644 --- a/src/account/GeniusAccount.cpp +++ b/src/account/GeniusAccount.cpp @@ -560,11 +560,124 @@ namespace sgns return signed_vector; } - void GeniusAccount::SetLocalConfirmedNonce( uint64_t nonce ) + outcome::result< + std::pair, std::pair>> + GeniusAccount::GenerateGeniusAddress( const char *eth_private_key, boost::filesystem::path base_path ) { - genius_account_logger()->debug( "Setting local confirmed nonce to {}", nonce ); - SetPeerConfirmedNonce( nonce, eth_keypair_->GetEntirePubValue() ); - std::lock_guard lock( nonce_mutex_ ); + constexpr std::string_view PREFIX = "SGNS"; + constexpr std::string_view FILE_NAME = "secure_storage_id"; + + // Convert to absolute path to handle relative paths properly + base_path = boost::filesystem::absolute( base_path ); + + boost::filesystem::create_directories( base_path ); + + // Use canonical() after directory exists to get fully normalized path + base_path = boost::filesystem::canonical( base_path ); + base_path /= FILE_NAME; + + genius_account_logger()->info( "Secure storage ID path: {}", base_path.string() ); + + // Try to load existing storage + std::shared_ptr storage; + nil::crypto3::multiprecision::uint256_t key_seed; + bool key_seed_loaded = false; + + if ( std::ifstream file( base_path.string() ); file.is_open() ) + { + std::string public_key; + file >> public_key; + genius_account_logger()->info( "Loaded public key from file: {} (length: {})", + public_key.substr( 0, 16 ) + "...", + public_key.length() ); + + OUTCOME_TRY( std::vector vec, base::unhex( public_key ) ); + + genius_account_logger()->info( "Unhexed public key vector size: {}", vec.size() ); + + // Create storage using the public key from the file + storage = std::make_shared( std::string( PREFIX ) + + libp2p::multi::detail::encodeBase58( vec ) ); + + if ( auto load_res = storage->Load( "sgns_key" ) ) + { + genius_account_logger()->info( "Successfully loaded key_seed from storage" ); + key_seed = nil::crypto3::multiprecision::uint256_t( load_res.value() ); + + // Validate that the loaded key_seed produces the same public key + ethereum::EthereumKeyGenerator temp_eth_key( key_seed ); + auto regenerated_pub_key = temp_eth_key.GetEntirePubValue(); + + if ( regenerated_pub_key == public_key ) + { + genius_account_logger()->info( "Validation successful: key_seed matches stored public key" ); + key_seed_loaded = true; + } + else + { + genius_account_logger()->error( "Validation failed: key_seed does not match stored public key" ); + genius_account_logger()->error( "Expected: {}", public_key.substr( 0, 16 ) + "..." ); + genius_account_logger()->error( "Got: {}", regenerated_pub_key.substr( 0, 16 ) + "..." ); + // Don't set key_seed_loaded, will regenerate + } + } + else + { + genius_account_logger()->warn( "Could not load sgns_key from secure storage, will regenerate" ); + } + } + else + { + genius_account_logger()->debug( "Secure storage ID file does not exist, will create new one" ); + } + + // Generate key_seed from ethereum private key if not loaded + if ( !key_seed_loaded ) + { + genius_account_logger()->trace( "Key seed from ethereum private key" ); + if ( eth_private_key == nullptr ) + { + return outcome::failure( std::errc::invalid_argument ); + } + + OUTCOME_TRY( auto as_vec, base::unhex( eth_private_key ) ); + TW::PrivateKey private_key( as_vec ); + + auto signed_secret = private_key.sign( + TW::Data( ELGAMAL_PUBKEY_PREDEFINED.cbegin(), ELGAMAL_PUBKEY_PREDEFINED.cend() ), + TWCurveSECP256k1 ); + + if ( signed_secret.empty() ) + { + genius_account_logger()->error( "Cannot sign secret" ); + return outcome::failure( std::errc::invalid_argument ); + } + + key_seed = nil::crypto3::multiprecision::uint256_t( TW::Hash::sha256( signed_secret ) ); + + // Create storage with loaded key + ethereum::EthereumKeyGenerator temp_eth_key( key_seed ); + auto pub_key = temp_eth_key.GetEntirePubValue(); + OUTCOME_TRY( std::vector vec, base::unhex( pub_key ) ); + storage = std::make_shared( std::string( PREFIX ) + + libp2p::multi::detail::encodeBase58( vec ) ); + + BOOST_OUTCOME_TRYV2( auto &&, storage->Save( "sgns_key", key_seed.str() ) ); + + // Write public key to file + std::ofstream out_file( base_path.string() ); + if ( !out_file.is_open() ) + { + return outcome::failure( std::errc::bad_file_descriptor ); + } + out_file << pub_key << std::endl; + } + + KeyGenerator::ElGamal elgamal_key( key_seed ); + ethereum::EthereumKeyGenerator eth_key( key_seed ); + auto pub_key = eth_key.GetEntirePubValue(); + + return std::make_pair( storage, std::make_pair( elgamal_key, eth_key ) ); } void GeniusAccount::SetPeerConfirmedNonce( uint64_t nonce, const std::string &address ) diff --git a/src/account/GeniusAccount.hpp b/src/account/GeniusAccount.hpp index 0f7143c40..9fcc6c891 100644 --- a/src/account/GeniusAccount.hpp +++ b/src/account/GeniusAccount.hpp @@ -157,12 +157,6 @@ 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 - */ - void SetLocalConfirmedNonce( uint64_t nonce ); - /** * @brief Set the local confirmed nonce for a peer * @param[in] nonce The nonce value to be set From 50f44b165cda19e95d3081ba4ed781b7d42587b8 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 19 Feb 2026 16:56:49 -0300 Subject: [PATCH 044/114] Fix: Consensus interface methods --- src/blockchain/Consensus.cpp | 20 ++++++++++++-------- src/blockchain/Consensus.hpp | 3 ++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 2bfe6374d..a0c0dd858 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -855,7 +855,7 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - const auto key = CERTIFICATE_BASE_PATH_KEY + subject_hash_result.value(); + 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 ); @@ -1850,25 +1850,29 @@ namespace sgns return true; } - outcome::result ConsensusManager::GetCertificateBySubjectHash( const std::string &subject_hash ) const + outcome::result ConsensusManager::GetCertificateBySubjectHash( const std::string &subject_hash ) const { - const auto key = CERTIFICATE_BASE_PATH_KEY + subject_hash; + const auto key = std::string{CERTIFICATE_BASE_PATH_KEY} + subject_hash; OUTCOME_TRY( auto certificate_data, db_->Get( { key } ) ); Certificate certificate; - if ( !certificate.ParseFromArray( value.data(), value.size() ) ) + 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 subject_hash = GetSubjectHash( certificate.proposal().subject() ); - if ( subject_hash.has_error() ) + auto current_hash = GetSubjectHash( certificate.proposal().subject() ); + if ( current_hash.has_error() ) { - return outcome::failure( subject_hash.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; } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 0efe9fc1d..eda66f7f4 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -118,6 +118,8 @@ namespace sgns void ProcessCertificates(); outcome::result GetCertificateBySubjectHash( const std::string &subject_hash ) const; + bool CheckCertificateForSubject( const std::string &subject_hash ) const; + protected: void ConfigureTimestampWindow( std::chrono::milliseconds window ); @@ -181,7 +183,6 @@ namespace sgns 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; - bool CheckCertificateForSubject( const std::string &subject_hash ) const; static std::string CreateProposalId( const Proposal &proposal ); static bool ValidateSubject( const Subject &subject ); From 1e7ddb0c0d2611652b00e382f1ce7743cebead18 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 19 Feb 2026 16:57:37 -0300 Subject: [PATCH 045/114] Feat: Rewriting how transaction confirmation should work with consensus --- src/account/TransactionManager.cpp | 654 +++++++++++++++-------------- src/account/TransactionManager.hpp | 11 +- 2 files changed, 355 insertions(+), 310 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 1c1c0e284..b2c0155d4 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -321,7 +321,7 @@ namespace sgns break; case State::SYNCING: - this->SyncNonce(); + SyncNonce(); break; case State::READY: @@ -346,9 +346,10 @@ namespace sgns auto rollback_result = RollbackTransactions( tx_queue_m.front() ); if ( rollback_result.has_error() ) { - m_logger->error( "[{} - full: {}] RollbackTransactions error, couldn't fetch nonce", + m_logger->error( "[{} - full: {}] {} error, couldn't fetch nonce", account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + full_node_m, + __func__ ); break; } @@ -368,17 +369,7 @@ 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; } @@ -669,14 +660,14 @@ namespace sgns 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 ) + { + m_logger->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 ); @@ -705,9 +696,8 @@ namespace sgns return dag; } - outcome::result> TransactionManager::SendTransactionItem( TransactionItem &item ) + outcome::result TransactionManager::SendTransactionItem( TransactionItem &item ) { - std::unordered_set nonces_set; auto [transaction_batch, maybe_crdt_transaction] = item; std::shared_ptr crdt_transaction = nullptr; @@ -759,16 +749,23 @@ namespace sgns expected_next_nonce = static_cast( confirmed_nonce ) + 1; } } + 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 ( transaction->GetNonce() != expected_next_nonce ) { 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() ); + transaction->GetNonce() ); return outcome::failure( boost::system::errc::make_error_code( boost::system::errc::invalid_argument ) ); @@ -797,155 +794,43 @@ namespace sgns 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() ); - m_logger->debug( "[{} - full: {}] Creating Consensus Proposal for tx {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_path ); - OUTCOME_TRY( auto &&proposal, - blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), - transaction->dag_st.nonce(), - transaction->GetHash() ) ); - OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); + proof_transaction.put( proof ); + BOOST_OUTCOME_TRYV2( auto &&, + crdt_transaction->Put( std::move( proof_key ), std::move( proof_transaction ) ) ); } - else - { - 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() ); + m_logger->debug( "[{} - full: {}] Creating Consensus Proposal for tx {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_path ); + + topicSet.merge( transaction->GetTopics() ); + transactions_sent.insert( transaction ); - return outcome::failure( - boost::system::errc::make_error_code( boost::system::errc::invalid_argument ) ); - } expected_next_nonce++; } - std::unordered_set topicSet; - if ( !transaction_batch.empty() ) - { - topicSet.emplace( full_node_topic_m ); - topicSet.emplace( account_m->GetAddress() ); - } - 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() ) - { - if ( it->second.status != tx_state && tx_state == TransactionStatus::VERIFYING ) - { - verifying_count_.fetch_add( 1, std::memory_order_relaxed ); - } - it->second.status = tx_state; - } - else - { - tx_processed_m[key] = TrackedTx{ tx, tx_state, nonce }; - if ( tx_state == TransactionStatus::VERIFYING ) - { - verifying_count_.fetch_add( 1, std::memory_order_relaxed ); - } - } - } + OUTCOME_TRY( auto &&proposal, + blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), + transaction->GetNonce(), + transaction->GetHash() ) ); + OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); - BOOST_OUTCOME_TRYV2( auto &&, crdt_transaction->Commit( topicSet ) ); + OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); + } - 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(); } @@ -1225,24 +1110,23 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - auto maybe_parsed = ParseTransaction( transaction ); - if ( maybe_parsed.has_error() ) + m_logger->debug( "[{} - full: {}] Checking if the transaction has a valid certificate to be confirmed {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx_key ); + + auto next_tx_state = TransactionStatus::VERIFYING; + + if ( blockchain_->CheckCertificate( transaction->GetHash() ) ) { - m_logger->debug( "[{} - full: {}] Can't parse the transaction {}", + m_logger->debug( "[{} - full: {}] Transaction has a valid certificate, marking as CONFIRMED {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, tx_key ); - return outcome::failure( maybe_parsed.error() ); + next_tx_state = TransactionStatus::CONFIRMED; } + OUTCOME_TRY( ChangeTransactionState( transaction, next_tx_state ) ); - const auto nonce = transaction->dag_st.nonce(); - - account_m->SetPeerConfirmedNonce( nonce, transaction->dag_st.source_addr() ); - - { - 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() ); @@ -2213,7 +2097,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 ) @@ -2228,33 +2114,25 @@ namespace sgns { 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 ) + tx = tracked.tx; + break; + } + if ( tx ) + { + auto result = ChangeTransactionState( std::move( tx ), s ); + if ( !result.has_error() ) { - verifying_count_.fetch_add( 1, std::memory_order_relaxed ); + ret = true; } - - m_logger->debug( "[{} - full: {}] Set tx {} (nonce {}) to {}", + } + else + { + m_logger->debug( "[{} - full: {}] No outgoing tx found with nonce {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, - tracked.tx->GetHash(), - nonce, - static_cast( s ) ); - return true; + nonce ); } - m_logger->debug( "[{} - full: {}] No outgoing tx found with nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); - return false; + return ret; } outcome::result TransactionManager::ConfirmTransactions() @@ -2385,36 +2263,37 @@ namespace sgns std::shared_ptr conflicting_tx; auto conflicting_tx_res = GetConflictingTransaction( *new_tx ); - if ( !conflicting_tx_res.has_value() ) + + if ( conflicting_tx_res.has_value() ) { - //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() ) + conflicting_tx = std::move( conflicting_tx_res.value() ); + m_logger->debug( "[{} - full: {}] Found existing conflicting transaction with hash: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx->GetHash() ); + std::unique_lock tx_lock( tx_mutex_m ); + auto key = GetTransactionPath( conflicting_tx->GetHash() ); + auto it = tx_processed_m.find( key ); + if ( it == tx_processed_m.end() ) { - m_logger->trace( "[{} - full: {}] No existing transaction, accepting new transaction {}", + m_logger->error( "[{} - full: {}] Conflicting transaction not found in processed maps: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, - element.key() ); - + key ); break; } - m_logger->debug( - "[{} - full: {}] Found transaction with the same key {}, checking mutability window and timestamps", - 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() ) + if ( it->second.status == TransactionStatus::CONFIRMED ) { - m_logger->warn( "[{} - full: {}] Failed to deserialize existing transaction {}, accepting new one", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - element.key() ); + m_logger->debug( + "[{} - full: {}] Conflicting transaction is already CONFIRMED, checking the incoming transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); + + should_delete = true; 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 ), @@ -2733,9 +2612,8 @@ namespace sgns 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() ) + + if ( new_tx->GetHash().empty() ) { m_logger->error( "[{} - full: {}] Empty hash on {}", account_m->GetAddress().substr( 0, 8 ), @@ -2744,61 +2622,67 @@ namespace sgns 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 ); - - 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() ); + // TODO - Evaluate if we need this, because theoretically we already check this on the filter + m_logger->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() ) ); + + // No need to check if not found because we already found it on GetConflictingTransaction - const auto conflict_hash = conflicting_tx.value()->GetHash(); - tx_lock.unlock(); - OUTCOME_TRY( RemoveTransactionFromProcessedMaps( GetTransactionPath( conflict_hash ), true ) ); - tx_lock.lock(); + if ( it->second.status == TransactionStatus::CONFIRMED ) + { + m_logger->debug( + "[{} - full: {}] Conflicting transaction is already CONFIRMED, not adding incoming transaction{}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); + OUTCOME_TRY( ChangeTransactionState( new_tx, TransactionStatus::FAILED ) ); + return outcome::failure( boost::system::error_code{} ); + } + m_logger->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() ); + OUTCOME_TRY( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::VERIFYING ) ); } - m_logger->debug( "[{} - full: {}] Parsing new transaction {}", + m_logger->debug( "[{} - full: {}] Checking if the transaction has a valid certificate to be confirmed {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, key ); - OUTCOME_TRY( ParseTransaction( new_tx ) ); - - (void)blockchain_->TryResumeProposal( tx_hash ); - 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() ); + m_logger->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() ) + { + m_logger->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(); } @@ -3000,8 +2884,8 @@ 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; } @@ -3011,64 +2895,33 @@ namespace sgns void TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) { - auto tracked_tx_ret = GetTrackedTxByHash( tx_hash ); - if ( !tracked_tx_ret ) + auto tx = GetTransactionByHash( tx_hash ); + if ( !tx ) { - m_logger->error( "[{} - full: {}] {}: Tracked transaction not found for hash {}", + m_logger->error( "[{} - full: {}] {}: Transaction not found for hash {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, __func__, tx_hash ); return; } - auto &tracked_tx = tracked_tx_ret.value(); - if ( tracked_tx.status != TransactionStatus::FAILED && tracked_tx.status != TransactionStatus::INVALID && - tracked_tx.status != TransactionStatus::CONFIRMED ) - { - m_logger->debug( "[{} - full: {}] {}: Transaction {} is {}, updating status to CONFIRMED", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash, - static_cast( tracked_tx.status ) ); - tracked_tx.status = TransactionStatus::CONFIRMED; - if ( tracked_tx.tx ) - { - m_logger->debug( "[{} - full: {}] {}: Parsing transaction {} after consensus certificate", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); - auto parse_result = ParseTransaction( tracked_tx.tx ); - if ( parse_result.has_error() ) - { - m_logger->error( - "[{} - full: {}] {}: Failed to parse transaction {} after consensus certificate: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash, - parse_result.error().message() ); - } - } - else - { - m_logger->error( "[{} - full: {}] {}: Tracked transaction missing for hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); - } - } - else + + auto result = ChangeTransactionState( tx, TransactionStatus::CONFIRMED ); + if ( result.has_error() ) { - m_logger->debug( "[{} - full: {}] {}: Transaction {} has status {}, not updating to APPROVED", + m_logger->error( "[{} - full: {}] {}: Failed to change transaction state to CONFIRMED for hash {}: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, __func__, tx_hash, - static_cast( tracked_tx.status ) ); + result.error().message() ); + return; } + m_logger->debug( "[{} - full: {}] {}: Transaction {} confirmed by consensus", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); } outcome::result TransactionManager::HandleNonceConsensusSubject( @@ -3130,7 +2983,7 @@ namespace sgns return ConsensusManager::SubjectCheck::Reject; } - if ( tracked.status == TransactionStatus::FAILED || tracked.status == TransactionStatus::INVALID ) + if ( tracked.status == TransactionStatus::FAILED ) { m_logger->error( "[{} - full: {}] {}: Transaction status invalid for hash {}", account_m->GetAddress().substr( 0, 8 ), @@ -3395,7 +3248,7 @@ namespace sgns } const auto confirmed_nonce = nonce_result.value(); - const auto tx_nonce = tx.dag_st.nonce(); + const auto tx_nonce = tx.GetNonce(); if ( tx_nonce <= confirmed_nonce ) { @@ -3437,7 +3290,7 @@ namespace sgns tx.GetSrcAddress() ); return false; } - if ( tracked->status == TransactionStatus::FAILED || tracked->status == TransactionStatus::INVALID ) + if ( tracked->status == TransactionStatus::FAILED ) { m_logger->error( "[{} - full: {}] {}: Intermediate nonce {} invalid for address {}", account_m->GetAddress().substr( 0, 8 ), @@ -3562,6 +3415,195 @@ namespace sgns window ); nonce_window_m = window; } + + outcome::result TransactionManager::ChangeTransactionState( const std::shared_ptr &tx, + TransactionStatus new_status ) + { + 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() ) + { + m_logger->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 ); + } + m_logger->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() ) + { + m_logger->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 ) + { + m_logger->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; + m_logger->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 ) + { + m_logger->error( "[{} - full: {}] {}: Trying to VERIFY a transaction that is already in VERIFY {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return outcome::failure( std::errc::invalid_argument ); + } + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) + { + m_logger->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() }; + verifying_count_.fetch_add( 1, std::memory_order_relaxed ); + m_logger->debug( "[{} - full: {}] {}: Set status of VERIFYING to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + m_logger->debug( "[{} - full: {}] {}: Attempting to resume the proposal handling to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + OUTCOME_TRY( blockchain_->TryResumeProposal( 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 ) + { + m_logger->error( "[{} - full: {}] {}: Trying to CONFIRM a transaction that is already CONFIRMED {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return outcome::failure( std::errc::file_exists ); + } + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::VERIFYING ) + { + m_logger->debug( "[{} - full: {}] {}: Reming verification count on {} before confirming", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + verifying_count_.fetch_sub( 1, std::memory_order_relaxed ); + } + tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::CONFIRMED, tx->GetNonce() }; + + m_logger->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() ); + } + + 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 ) + { + m_logger->error( "[{} - full: {}] {}: Trying to FAIL a transaction that is already FAILED {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return outcome::failure( std::errc::file_exists ); + } + if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) + { + m_logger->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() ); + } + tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::FAILED, tx->GetNonce() }; + account_m->ReleaseNonce( tx->GetNonce() ); + + m_logger->debug( "[{} - full: {}] {}: Set status of FAILED to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + } + + break; + default: + m_logger->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 ); + } + return outcome::success(); + } } diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index cf6830dcc..c7020d4d9 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -220,10 +220,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::string transaction_hash = "" ) const; + outcome::result SendTransactionItem( TransactionItem &item ); + outcome::result ConfirmTransactions(); + outcome::result RollbackTransactions( TransactionItem &item_to_rollback ); static std::vector GetMonitoredNetworkIDs(); static std::string GetBlockChainBase(); @@ -378,6 +378,9 @@ namespace sgns bool CheckTransactionTypeRules( 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 ); }; } From e59acfd9a36dc3ada0b87d77e680c8014878e7a8 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Feb 2026 11:31:02 -0300 Subject: [PATCH 046/114] Fix: Some deadlocks --- src/account/GeniusNode.cpp | 7 +++++-- src/account/TransactionManager.cpp | 18 +++++++++++++++++- src/blockchain/ValidatorRegistry.cpp | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index a7f7b8e4e..f9500e14c 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -481,7 +481,7 @@ namespace sgns auto loggerDAGSyncer = ConfigureLogger( "GraphsyncDAGSyncer", logdir, spdlog::level::err ); auto loggerGraphsync = ConfigureLogger( "graphsync", logdir, spdlog::level::err ); auto loggerBroadcaster = ConfigureLogger( "PubSubBroadcasterExt", logdir, spdlog::level::err ); - auto loggerDataStore = ConfigureLogger( "CrdtDatastore", logdir, spdlog::level::debug ); + auto loggerDataStore = ConfigureLogger( "CrdtDatastore", logdir, spdlog::level::err ); auto loggerCRDTHeads = ConfigureLogger( "CrdtHeads", logdir, spdlog::level::trace ); auto loggerTransactions = ConfigureLogger( "TransactionManager", logdir, spdlog::level::debug ); auto loggerMigration = ConfigureLogger( "MigrationManager", logdir, spdlog::level::trace ); @@ -507,6 +507,7 @@ namespace sgns 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 ); @@ -531,7 +532,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 ); @@ -563,6 +564,8 @@ namespace sgns 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/TransactionManager.cpp b/src/account/TransactionManager.cpp index b2c0155d4..489ebca0d 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -657,7 +657,6 @@ namespace sgns { m_logger->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 ) { auto result = ChangeTransactionState( tx, TransactionStatus::CREATED ); @@ -2117,6 +2116,7 @@ namespace sgns tx = tracked.tx; break; } + tx_lock.unlock(); if ( tx ) { auto result = ChangeTransactionState( std::move( tx ), s ); @@ -2648,13 +2648,16 @@ namespace sgns 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{} ); } m_logger->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( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::VERIFYING ) ); } @@ -3419,6 +3422,12 @@ namespace sgns outcome::result TransactionManager::ChangeTransactionState( const std::shared_ptr &tx, TransactionStatus new_status ) { + m_logger->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: @@ -3602,6 +3611,13 @@ namespace sgns tx->GetHash() ); return outcome::failure( std::errc::invalid_argument ); } + + m_logger->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(); } } diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index fcb4fb459..358915008 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -446,7 +446,7 @@ namespace sgns std::shared_lock lock( cache_mutex_ ); if ( cached_registry_ ) { - logger_->debug( "{}: returning cached registry", __func__ ); + logger_->trace( "{}: returning cached registry", __func__ ); return cached_registry_.value(); } } From 2978821bc82e6ce0393fd7234e55e52f45dfb5ff Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Feb 2026 12:43:50 -0300 Subject: [PATCH 047/114] Fix: First round of voting was always not allowed --- src/blockchain/Consensus.cpp | 45 ++++++++++++++++++++++++------------ src/blockchain/Consensus.hpp | 4 +++- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index a0c0dd858..ffbc77536 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -855,7 +855,7 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - const auto key = std::string{CERTIFICATE_BASE_PATH_KEY} + subject_hash_result.value(); + 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 ); @@ -978,6 +978,7 @@ namespace sgns { return outcome::failure( std::errc::invalid_argument ); } + ConsensusManagerLogger()->trace( "{}: called subject_hash={}", __func__, subject_hash ); auto to_process = TakePendingProposals( subject_hash ); @@ -1038,11 +1039,9 @@ namespace sgns auto registry_result = registry_->LoadRegistry(); if ( registry_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: aborted: registry load error={}", - __func__, - registry_result.error().message() ); return; } + //ConsensusManagerLogger()->trace( "{}: Checking if need to process certificates", __func__ ); const auto ®istry = registry_result.value(); std::vector to_process; @@ -1053,6 +1052,10 @@ namespace sgns auto &state = kv.second; if ( !state.quorum_reached ) { + ConsensusManagerLogger()->debug( "{}: Found proposal without quorum reached proposal_id={}", + __func__, + state.proposal.proposal_id() ); + continue; } to_process.push_back( state ); @@ -1061,13 +1064,23 @@ namespace sgns for ( auto &state : to_process ) { + ConsensusManagerLogger()->debug( "{}: Processing proposal with quorum reached proposal_id={}", + __func__, + state.proposal.proposal_id() ); const auto round = GetCurrentRound( state.proposal.timestamp() ); - if ( round == state.last_attempt_round ) + if ( state.last_attempt_round != NO_ROUND && round == state.last_attempt_round ) { + ConsensusManagerLogger()->debug( "{}: proposal already attempted in round proposal_id={} round={}", + __func__, + state.proposal.proposal_id(), + round ); continue; } if ( !IsCurrentAggregator( state.proposal, registry ) ) { + ConsensusManagerLogger()->debug( "{}: not aggregator for proposal proposal_id={}", + __func__, + state.proposal.proposal_id() ); continue; } @@ -1079,7 +1092,10 @@ namespace sgns it->second.last_attempt_round = round; } } - + ConsensusManagerLogger()->debug( "{}: Attempting to create certificate proposal_id={} round={}", + __func__, + state.proposal.proposal_id(), + round ); auto certificate_result = CreateCertificate( state.proposal, state.votes ); if ( certificate_result.has_error() ) { @@ -1807,11 +1823,6 @@ namespace sgns ConsensusManagerLogger()->error( "{}: Registry CID missing ", __func__ ); return false; } - if ( proposal.registry_epoch() == 0 ) - { - ConsensusManagerLogger()->error( "{}: Registry EPOCH is zero ", __func__ ); - return false; - } if ( !proposal.has_subject() ) { ConsensusManagerLogger()->error( "{}: Proposal without subject ", __func__ ); @@ -1850,9 +1861,10 @@ namespace sgns return true; } - outcome::result ConsensusManager::GetCertificateBySubjectHash( const std::string &subject_hash ) const + outcome::result ConsensusManager::GetCertificateBySubjectHash( + const std::string &subject_hash ) const { - const auto key = std::string{CERTIFICATE_BASE_PATH_KEY} + subject_hash; + const auto key = std::string{ CERTIFICATE_BASE_PATH_KEY } + subject_hash; OUTCOME_TRY( auto certificate_data, db_->Get( { key } ) ); @@ -1870,7 +1882,10 @@ namespace sgns } if ( current_hash.value() != subject_hash ) { - ConsensusManagerLogger()->error( "{}: certificate subject hash mismatch expected={} actual={}", __func__, subject_hash, current_hash.value() ); + ConsensusManagerLogger()->error( "{}: certificate subject hash mismatch expected={} actual={}", + __func__, + subject_hash, + current_hash.value() ); return outcome::failure( std::errc::invalid_argument ); } return certificate; @@ -1884,7 +1899,7 @@ namespace sgns return false; } //TODO - Check if we need to call ValidateCertificate here. I don't think so because it was validated before. - return true; + return true; } } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index eda66f7f4..64cf46970 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "blockchain/ValidatorRegistry.hpp" #include "blockchain/impl/proto/Consensus.pb.h" @@ -140,6 +141,7 @@ namespace sgns 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 { @@ -150,7 +152,7 @@ namespace sgns uint64_t approved_weight = 0; std::unordered_set seen_voters; bool quorum_reached = false; - uint64_t last_attempt_round = 0; + uint64_t last_attempt_round = NO_ROUND; }; struct SlotState From b0c4ed1c4a30264825d10e62a4ccde520bcbd657 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Feb 2026 12:44:21 -0300 Subject: [PATCH 048/114] Fix: More deadlocks and filter now working --- src/account/TransactionManager.cpp | 76 +++++++++++++++++++----------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 489ebca0d..a97ecdad1 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -2264,35 +2264,36 @@ namespace sgns auto conflicting_tx_res = GetConflictingTransaction( *new_tx ); - if ( conflicting_tx_res.has_value() ) + if ( !conflicting_tx_res.has_value() ) { - conflicting_tx = std::move( conflicting_tx_res.value() ); - m_logger->debug( "[{} - full: {}] Found existing conflicting transaction with hash: {}", + break; + } + conflicting_tx = std::move( conflicting_tx_res.value() ); + m_logger->debug( "[{} - full: {}] Found existing conflicting transaction with hash: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + conflicting_tx->GetHash() ); + std::unique_lock tx_lock( tx_mutex_m ); + auto key = GetTransactionPath( conflicting_tx->GetHash() ); + auto it = tx_processed_m.find( key ); + if ( it == tx_processed_m.end() ) + { + m_logger->error( "[{} - full: {}] Conflicting transaction not found in processed maps: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, - conflicting_tx->GetHash() ); - std::unique_lock tx_lock( tx_mutex_m ); - auto key = GetTransactionPath( conflicting_tx->GetHash() ); - auto it = tx_processed_m.find( key ); - if ( it == tx_processed_m.end() ) - { - m_logger->error( "[{} - full: {}] Conflicting transaction not found in processed maps: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); - break; - } - if ( it->second.status == TransactionStatus::CONFIRMED ) - { - m_logger->debug( - "[{} - full: {}] Conflicting transaction is already CONFIRMED, checking the incoming transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + key ); + break; + } + if ( it->second.status == TransactionStatus::CONFIRMED ) + { + m_logger->debug( + "[{} - full: {}] Conflicting transaction is already CONFIRMED, checking the incoming transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); - should_delete = true; - break; - } + should_delete = true; + break; } m_logger->debug( "[{} - full: {}] Checking if new tx {} is the correct one", @@ -2380,6 +2381,9 @@ namespace sgns bool TransactionManager::ShouldReplaceTransaction( const IGeniusTransactions &existing_tx, const IGeniusTransactions &new_tx ) const { + m_logger->debug( "[{} - full: {}] ShouldReplaceTransaction?", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); // First check if the existing transaction is immutable if ( existing_tx.GetHash() == new_tx.GetHash() ) { @@ -2395,6 +2399,9 @@ namespace sgns full_node_m ); return false; } + m_logger->debug( "[{} - full: {}] ShouldReplaceTransaction?1111", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); // Get timestamps and elapsed times auto existing_timestamp = existing_tx.GetTimestamp(); @@ -2648,7 +2655,7 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m, key ); - tx_lock.unlock(); + tx_lock.unlock(); OUTCOME_TRY( ChangeTransactionState( new_tx, TransactionStatus::FAILED ) ); tx_lock.lock(); return outcome::failure( boost::system::error_code{} ); @@ -2657,7 +2664,7 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m, conflicting_tx.value()->GetHash() ); - tx_lock.unlock(); + tx_lock.unlock(); OUTCOME_TRY( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::VERIFYING ) ); } @@ -3242,11 +3249,20 @@ namespace sgns auto nonce_result = account_m->GetPeerNonce( tx.GetSrcAddress() ); if ( nonce_result.has_error() ) { + if ( tx.GetNonce() == 0 ) + { + m_logger->debug( "[{} - full: {}] {}: No peer nonce required for tx with nonce=0", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); + return true; + } m_logger->error( "[{} - full: {}] {}: Missing peer nonce for address {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, __func__, tx.GetSrcAddress() ); + return false; } @@ -3524,7 +3540,13 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); + tx_lock.unlock(); OUTCOME_TRY( blockchain_->TryResumeProposal( tx->GetHash() ) ); + m_logger->debug( "[{} - full: {}] {}: Resumed the proposal handling to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); } break; From 7e56dd37cdb7ab6b5f7dea03a8b54cc5c423eddf Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Feb 2026 13:40:49 -0300 Subject: [PATCH 049/114] Feat: Confirmation of transactions is now based on consensus certificates --- src/account/GeniusNode.cpp | 44 +++++++++++++++--------------- src/account/TransactionManager.cpp | 10 +------ 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index f9500e14c..592d81325 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -497,17 +497,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 loggerConsensusManager = ConfigureLogger( "ConsensusManager", logdir, spdlog::level::trace ); + 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::debug ); + 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 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 ); @@ -554,17 +554,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::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 ); + 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 ); diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index a97ecdad1..51b96b587 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -374,13 +374,6 @@ namespace sgns 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 @@ -2014,8 +2007,7 @@ namespace sgns 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() ) + if ( tracked.tx && tracked.tx->GetHash() == tx_hash ) { return tracked.tx; } From f756d671521b507eb180049fe2cf43b2c12f9294 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Feb 2026 15:23:52 -0300 Subject: [PATCH 050/114] Fix: Certificate callback with correct key checking --- src/blockchain/ValidatorRegistry.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 358915008..7e90bd496 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -467,16 +467,20 @@ namespace sgns 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 != std::string( RegistryKey() ) ) + if ( key != registry_key.GetKey() ) { - ValidatorRegistryLogger()->debug( "{}: Skipping non-registry content key={}", __func__, key ); + 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 ); + auto decoded = DeserializeRegistryUpdate( bytes ); if ( decoded.has_error() ) { ValidatorRegistryLogger()->error( "{}: failed to parse registry update ", __func__ ); From a815342cb85f452b7eb93e568b83c7a86fc43409 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Feb 2026 15:32:34 -0300 Subject: [PATCH 051/114] Fix: Handling a self vote --- src/blockchain/Consensus.cpp | 10 +++++----- src/blockchain/Consensus.hpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index ffbc77536..cb37fc014 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -648,10 +648,6 @@ namespace sgns const ValidatorRegistry::Registry ®istry, const std::string ®istry_cid ) const { - ConsensusManagerLogger()->error( "{}: failed: registry cid mismatch proposal={} registry={}", - __func__, - proposal.registry_cid(), - registry_cid ); if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { ConsensusManagerLogger()->error( "{}: failed: registry cid mismatch proposal={} registry={}", @@ -806,7 +802,7 @@ namespace sgns return outcome::success(); } - outcome::result ConsensusManager::SubmitVote( const Vote &vote ) + outcome::result ConsensusManager::SubmitVote( const Vote &vote, bool self_handle ) { ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={}", __func__, @@ -824,6 +820,10 @@ namespace sgns __func__, vote.proposal_id(), vote.voter_id() ); + if ( self_handle ) + { + HandleVote( vote ); + } return result; } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 64cf46970..76c8a7089 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -113,7 +113,7 @@ namespace sgns const std::string &task_result_hash, uint64_t result_epoch ); outcome::result SubmitProposal( const Proposal &proposal, bool self_vote = true ); - outcome::result SubmitVote( const Vote &vote ); + 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(); From f879e8a4bb699b72c6b45a73c581e438709026e6 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Feb 2026 15:42:21 -0300 Subject: [PATCH 052/114] Chore: Removing old nonce confirmation stuff --- src/account/TransactionManager.cpp | 100 ----------------------------- src/account/TransactionManager.hpp | 2 - 2 files changed, 102 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 51b96b587..187e6d673 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -2127,101 +2127,7 @@ namespace sgns return ret; } - 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(); - } - - // Collect nonces of VERIFYING transactions using index - std::vector verifying_nonces; - { - 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.status == TransactionStatus::VERIFYING ) - { - verifying_nonces.push_back( tracked.cached_nonce ); - } - } - } - // 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 - { - 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.cached_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 ); - } - } - } - } - } - - return outcome::success(); - } std::optional> TransactionManager::FilterTransaction( const crdt::pb::Element &element ) @@ -2575,10 +2481,6 @@ namespace sgns } 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; @@ -3521,7 +3423,6 @@ namespace sgns account_m->RollBackPeerConfirmedNonce( it->second.cached_nonce, tx->GetSrcAddress() ); } tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::VERIFYING, tx->GetNonce() }; - verifying_count_.fetch_add( 1, std::memory_order_relaxed ); m_logger->debug( "[{} - full: {}] {}: Set status of VERIFYING to transaction {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -3563,7 +3464,6 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); - verifying_count_.fetch_sub( 1, std::memory_order_relaxed ); } tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::CONFIRMED, tx->GetNonce() }; diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index c7020d4d9..971f8eff3 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -222,7 +222,6 @@ namespace sgns SGTransaction::DAGStruct FillDAGStruct( std::string transaction_hash = "" ) const; outcome::result SendTransactionItem( TransactionItem &item ); - outcome::result ConfirmTransactions(); outcome::result RollbackTransactions( TransactionItem &item_to_rollback ); static std::vector GetMonitoredNetworkIDs(); @@ -292,7 +291,6 @@ namespace sgns mutable std::shared_mutex tx_mutex_m; std::unordered_map tx_processed_m; - std::atomic verifying_count_{ 0 }; // Count of VERIFYING transactions std::unordered_map pending_proposals_; std::function task_m; std::atomic stopped_{ false }; From d8754830b21e9e83c20ae344dcd05901b42d6d75 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 23 Feb 2026 11:50:47 -0300 Subject: [PATCH 053/114] Fix: Destructor issue on consensus thread --- src/blockchain/Consensus.cpp | 68 +++++++++++++++++++++++++++++- src/blockchain/Consensus.hpp | 3 ++ src/blockchain/impl/Blockchain.cpp | 8 ++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index cb37fc014..db41e0331 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -113,6 +113,12 @@ namespace sgns } ConsensusManager::~ConsensusManager() + { + stop_timer_.store( true ); + timer_cv_.notify_all(); + } + + void ConsensusManager::Close() { stop_timer_.store( true ); timer_cv_.notify_all(); @@ -128,6 +134,10 @@ namespace sgns { return; } + if ( stop_timer_.load() ) + { + return; + } std::weak_ptr weak_self = shared_from_this(); round_timer_ = std::thread( @@ -147,12 +157,27 @@ namespace sgns { interval = DEFAULT_ROUND_DURATION / 2; } - if ( self->timer_cv_.wait_for( lock, interval, [self]() { return self->stop_timer_.load(); } ) ) + 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; + } } } ); } @@ -1113,6 +1138,27 @@ namespace sgns } } + 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/[^/]+"; @@ -1395,6 +1441,11 @@ namespace sgns } state = it->second; } + if ( has_quorum ) + { + certificates_pending_.store( true ); + timer_cv_.notify_all(); + } } void ConsensusManager::HandleVoteBundle( const VoteBundle &bundle ) @@ -1535,6 +1586,21 @@ namespace sgns auto &vec = kv.second; vec.erase( std::remove( vec.begin(), vec.end(), proposal.proposal_id() ), vec.end() ); } + + 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 diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 76c8a7089..283dc0c90 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -35,6 +35,7 @@ namespace sgns { public: ~ConsensusManager(); + void Close(); using Proposal = ConsensusProposal; using Vote = ConsensusVote; using VoteBundle = ConsensusVoteBundle; @@ -189,6 +190,7 @@ namespace sgns 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 ); @@ -214,6 +216,7 @@ namespace sgns 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/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index cf6691e67..b3d33a28a 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -330,6 +330,10 @@ 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 ); @@ -1404,6 +1408,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(); } From 5fc6d5c22de1f3167242d5e0466101c651ad4534 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Feb 2026 11:59:02 -0300 Subject: [PATCH 054/114] Fix: Deterministic transaction decision --- src/account/GeniusNode.hpp | 11 + src/account/TransactionManager.cpp | 264 ++++++++++-------- src/account/TransactionManager.hpp | 9 +- test/src/multiaccount/multi_account_sync.cpp | 12 +- .../processing_nodes_test.cpp | 6 +- 5 files changed, 180 insertions(+), 122 deletions(-) diff --git a/src/account/GeniusNode.hpp b/src/account/GeniusNode.hpp index 37edc6846..b5c719222 100644 --- a/src/account/GeniusNode.hpp +++ b/src/account/GeniusNode.hpp @@ -175,6 +175,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/TransactionManager.cpp b/src/account/TransactionManager.cpp index 187e6d673..3be67c99c 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -374,7 +374,6 @@ namespace sgns break; } - // 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; @@ -1374,6 +1373,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 @@ -2127,13 +2144,11 @@ namespace sgns 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 { @@ -2144,7 +2159,6 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m, element.key() ); - should_delete = true; break; } new_tx = maybe_new_tx.value(); @@ -2155,51 +2169,17 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m, element.key() ); - should_delete = true; break; } - std::shared_ptr conflicting_tx; - - auto conflicting_tx_res = GetConflictingTransaction( *new_tx ); - - if ( !conflicting_tx_res.has_value() ) + if ( IsGoingToOverwrite( GetTransactionPath( *new_tx ); ) ) { - break; - } - conflicting_tx = std::move( conflicting_tx_res.value() ); - m_logger->debug( "[{} - full: {}] Found existing conflicting transaction with hash: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - conflicting_tx->GetHash() ); - std::unique_lock tx_lock( tx_mutex_m ); - auto key = GetTransactionPath( conflicting_tx->GetHash() ); - auto it = tx_processed_m.find( key ); - if ( it == tx_processed_m.end() ) - { - m_logger->error( "[{} - full: {}] Conflicting transaction not found in processed maps: {}", + m_logger->debug( "[{} - full: {}] New transaction {} would overwrite an existing one. Preventing that", account_m->GetAddress().substr( 0, 8 ), full_node_m, - key ); + new_tx->GetHash() ); break; } - if ( it->second.status == TransactionStatus::CONFIRMED ) - { - m_logger->debug( - "[{} - full: {}] Conflicting transaction is already CONFIRMED, checking the incoming transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); - - should_delete = true; - break; - } - - 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 ); @@ -2279,10 +2259,13 @@ namespace sgns bool TransactionManager::ShouldReplaceTransaction( const IGeniusTransactions &existing_tx, const IGeniusTransactions &new_tx ) const { - m_logger->debug( "[{} - full: {}] ShouldReplaceTransaction?", + m_logger->debug( "[{} - full: {}] {}: Checking if new transaction {} should replace existing one {}", account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - // First check if the existing transaction is immutable + full_node_m, + __func__, + new_tx.GetHash(), + existing_tx.GetHash() ); + if ( existing_tx.GetHash() == new_tx.GetHash() ) { m_logger->info( "[{} - full: {}] Already have the same transaction, rejecting replacement attempt", @@ -2290,65 +2273,24 @@ namespace sgns full_node_m ); return false; } - if ( IsTransactionImmutable( existing_tx ) ) + const bool replace = new_tx.GetHash() < existing_tx.GetHash(); + if ( replace ) { - m_logger->info( "[{} - full: {}] Existing transaction is immutable, rejecting replacement attempt", + m_logger->info( "[{} - full: {}] Deterministic replacement by hash: new {} < existing {}", account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return false; + full_node_m, + new_tx.GetHash(), + existing_tx.GetHash() ); } - m_logger->debug( "[{} - full: {}] ShouldReplaceTransaction?1111", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - - // 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 ) + else { - 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->info( "[{} - full: {}] Deterministic replacement by hash: new {} >= existing {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_tx.GetHash(), + existing_tx.GetHash() ); } - - m_logger->warn( - "[{} - full: {}] New transaction not eligible for replacement. Existing: {} , New: {} , Diff: {} ms, Tolerance: {} ms", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - existing_timestamp, - new_timestamp, - time_diff, - timestamp_tolerance_m.count() ); - return false; + return replace; } uint64_t TransactionManager::GetCurrentTimestamp() @@ -2532,7 +2474,6 @@ namespace sgns if ( conflicting_tx.has_value() ) { - // TODO - Evaluate if we need this, because theoretically we already check this on the filter m_logger->warn( "[{} - full: {}] Found conflicting transaction that passed the FILTER with hash: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -2799,6 +2740,11 @@ namespace sgns void TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) { + m_logger->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 ) { @@ -2809,6 +2755,85 @@ namespace sgns tx_hash ); return; } + m_logger->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() ) + { + m_logger->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 ) + { + m_logger->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, *tx ) ) + { + auto result = ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ); + if ( result.has_error() ) + { + m_logger->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() ) + { + m_logger->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 + { + m_logger->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() ) + { + m_logger->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() ) @@ -3407,7 +3432,7 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); - return outcome::failure( std::errc::invalid_argument ); + break; } if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) { @@ -3455,15 +3480,7 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); - return outcome::failure( std::errc::file_exists ); - } - if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::VERIFYING ) - { - m_logger->debug( "[{} - full: {}] {}: Reming verification count on {} before confirming", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + break; } tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::CONFIRMED, tx->GetNonce() }; @@ -3490,7 +3507,7 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); - return outcome::failure( std::errc::file_exists ); + break; } if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) { @@ -3534,6 +3551,29 @@ namespace sgns 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() ) + { + m_logger->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() ) + { + m_logger->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; + } } diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 971f8eff3..e77d77316 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -112,6 +112,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, @@ -289,8 +292,8 @@ namespace sgns mutable std::mutex mutex_m; std::deque tx_queue_m; - mutable std::shared_mutex tx_mutex_m; - std::unordered_map tx_processed_m; + mutable std::shared_mutex tx_mutex_m; + std::unordered_map tx_processed_m; std::unordered_map pending_proposals_; std::function task_m; std::atomic stopped_{ false }; @@ -379,6 +382,8 @@ namespace sgns outcome::result ChangeTransactionState( const std::shared_ptr &tx, TransactionStatus new_status ); + + bool IsGoingToOverwrite( const std::string &key ) const; }; } diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 882208475..9c2717e2f 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -280,9 +280,9 @@ 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, @@ -351,6 +351,8 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) 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 @@ -364,8 +366,8 @@ 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 ); diff --git a/test/src/processing_nodes/processing_nodes_test.cpp b/test/src/processing_nodes/processing_nodes_test.cpp index fa7b77d7c..2f2290fb9 100644 --- a/test/src/processing_nodes/processing_nodes_test.cpp +++ b/test/src/processing_nodes/processing_nodes_test.cpp @@ -169,9 +169,9 @@ TEST_F( ProcessingNodesTest, DISABLED_ProcessNodesTransactionsCount ) node_main->MintTokens( 50000000000, "", "", sgns::TokenID::FromBytes( { 0x00 } ) ); node_main->MintTokens( 50000000000, "", "", 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; From e9a4bbb96384a574825fb851d3cc53c24635e62d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Feb 2026 12:06:10 -0300 Subject: [PATCH 055/114] Chore: static logger instead of member --- src/account/TransactionManager.cpp | 1808 ++++++++++++++-------------- src/account/TransactionManager.hpp | 4 +- 2 files changed, 932 insertions(+), 880 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 3be67c99c..d48a843cc 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -28,6 +28,13 @@ namespace sgns { + 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, @@ -180,9 +187,9 @@ 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(); @@ -218,23 +225,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 ); } @@ -285,25 +292,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() ) { @@ -311,9 +318,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; @@ -338,18 +346,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: {}] {} error, couldn't fetch nonce", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {} error, couldn't fetch nonce", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); break; } @@ -357,9 +365,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 @@ -393,33 +402,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 ); } } @@ -564,21 +573,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 ); @@ -599,11 +610,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(), @@ -611,10 +622,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; @@ -647,17 +658,19 @@ 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 ); { for ( auto &&[tx, _] : element.first ) { auto result = ChangeTransactionState( tx, TransactionStatus::CREATED ); if ( !result ) { - m_logger->error( "[{} - full: {}] Failed to change transaction state for {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx->GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] Failed to change transaction state for {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx->GetHash() ); } } } @@ -692,7 +705,7 @@ namespace sgns 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() ) { @@ -709,34 +722,34 @@ namespace sgns if ( 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 ); + TransactionManagerLogger()->debug( "[{} - full: {}] Set nonce to {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + confirmed_nonce ); expected_next_nonce = static_cast( confirmed_nonce ) + 1; } else if ( nonce_result.has_error() && nonce_result.error() == AccountMessenger::Error::NO_RESPONSE_RECEIVED ) { if ( !full_node_m ) { - m_logger->error( "[{} - full: {}] {}: Network unreachable when fetching nonce", - __func__, - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->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 ); + TransactionManagerLogger()->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() ); + TransactionManagerLogger()->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; } } @@ -752,11 +765,12 @@ namespace sgns { if ( transaction->GetNonce() != expected_next_nonce ) { - 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->GetNonce() ); + TransactionManagerLogger()->error( + "[{} - full: {}] Transaction with unexpected nonce - Expected: {}, Tried to send: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + expected_next_nonce, + transaction->GetNonce() ); return outcome::failure( boost::system::errc::make_error_code( boost::system::errc::invalid_argument ) ); @@ -766,10 +780,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 ) ) ); @@ -780,19 +794,19 @@ 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 ) ) ); } - m_logger->debug( "[{} - full: {}] Creating Consensus Proposal for tx {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_path ); + TransactionManagerLogger()->debug( "[{} - full: {}] Creating Consensus Proposal for tx {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_path ); topicSet.merge( transaction->GetTopics() ); transactions_sent.insert( transaction ); @@ -936,9 +950,9 @@ 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; } @@ -950,9 +964,9 @@ 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; } @@ -986,17 +1000,17 @@ namespace sgns outcome::result TransactionManager::CheckProof( const std::shared_ptr &tx ) { auto proof_path = GetTransactionProofPath( *tx ); - m_logger->debug( "[{} - full: {}] Checking the proof in {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - proof_path ); + TransactionManagerLogger()->debug( "[{} - full: {}] Checking the proof in {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + proof_path ); OUTCOME_TRY( ( auto &&, proof_data ), globaldb_m->Get( { proof_path } ) ); auto proof_data_vector = proof_data.toVector(); - m_logger->debug( "[{} - full: {}] Proof data acquired. Verifying...", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->debug( "[{} - full: {}] Proof data acquired. Verifying...", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); return IBasicProof::VerifyFullProof( proof_data_vector ); } @@ -1008,34 +1022,34 @@ namespace sgns { std::string blockchain_base = GetBlockChainBase( network_id ); std::string query_path = blockchain_base + "tx"; - m_logger->trace( "[{} - full: {}] Probing transactions on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - query_path ); + TransactionManagerLogger()->trace( "[{} - full: {}] Probing transactions on {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + query_path ); OUTCOME_TRY( auto transaction_list, globaldb_m->QueryKeyValues( query_path ) ); - m_logger->trace( "[{} - full: {}] Transaction list grabbed from CRDT with Size {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_list.size() ); + TransactionManagerLogger()->trace( "[{} - full: {}] Transaction list grabbed from CRDT with Size {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_list.size() ); for ( const auto &[key, value] : transaction_list ) { auto transaction_key = globaldb_m->KeyToString( key ); if ( !transaction_key.has_value() ) { - m_logger->error( "[{} - full: {}] Unable to convert a key to string", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); + TransactionManagerLogger()->error( "[{} - full: {}] Unable to convert a key to string", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); continue; } auto process_result = FetchAndProcessTransaction( transaction_key.value(), value ); if ( !transaction_key.has_value() ) { - m_logger->error( "[{} - full: {}] Unable to fetch and process transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - transaction_key.value() ); + TransactionManagerLogger()->error( "[{} - full: {}] Unable to fetch and process transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + transaction_key.value() ); } } } @@ -1056,10 +1070,10 @@ namespace sgns 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(); } } @@ -1068,52 +1082,54 @@ 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 ); } - m_logger->debug( "[{} - full: {}] Checking if the transaction has a valid certificate to be confirmed {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + 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 ); auto next_tx_state = TransactionStatus::VERIFYING; if ( blockchain_->CheckCertificate( transaction->GetHash() ) ) { - m_logger->debug( "[{} - full: {}] Transaction has a valid certificate, marking as CONFIRMED {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx_key ); + 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 ) ); @@ -1137,23 +1153,23 @@ namespace sgns GeniusUTXO new_utxo( hash, i, dest_infos[i].encrypted_amount, dest_infos[i].token_id ); 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_ ); } utxo_manager_.ConsumeUTXOs( transfer_tx->GetInputInfos(), transfer_tx->GetSrcAddress() ); return outcome::success(); @@ -1166,11 +1182,11 @@ namespace sgns auto hash = ( base::Hash256::fromReadableString( mint_tx->GetHash() ) ).value(); GeniusUTXO new_utxo( hash, 0, mint_tx->GetAmount(), mint_tx->GetTokenID() ); utxo_manager_.PutUTXO( new_utxo, 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(); } @@ -1206,17 +1222,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(); } @@ -1232,34 +1248,34 @@ namespace sgns auto hash = ( base::Hash256::fromReadableString( transfer_tx->GetHash() ) ).value(); utxo_manager_.DeleteUTXO( hash, 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 ) ); } } @@ -1274,12 +1290,12 @@ namespace sgns auto hash = ( base::Hash256::fromReadableString( mint_tx->GetHash() ) ).value(); 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() ) ); + 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(); } @@ -1303,10 +1319,10 @@ namespace sgns 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 ) ); } } @@ -1324,17 +1340,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(); } @@ -1413,9 +1429,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 ) ); @@ -1434,10 +1450,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 ) { @@ -1445,29 +1461,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 ) ); @@ -1499,10 +1515,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; @@ -1787,9 +1804,9 @@ namespace sgns 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; @@ -1816,27 +1833,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 ); @@ -1844,12 +1862,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 @@ -1871,10 +1890,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; } } @@ -1882,29 +1902,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 ); } } @@ -1914,10 +1934,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 ) { @@ -1928,19 +1948,19 @@ namespace sgns continue; } - m_logger->debug( "[{} - full: {}] {}: Seeing if transaction {} is valid {}", - __func__, - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tracked.cached_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.cached_nonce == nonce ) { bool valid_tx = true; if ( !CheckTransactionAuthorization( *tracked.tx ) ) { - m_logger->error( + TransactionManagerLogger()->error( "[{} - full: {}] Could not validate signature of transaction with nonce {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -1949,22 +1969,22 @@ namespace sgns } 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 { @@ -1987,24 +2007,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(); } @@ -2020,10 +2040,10 @@ 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 ); + 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; @@ -2136,10 +2156,10 @@ namespace sgns } else { - m_logger->debug( "[{} - full: {}] No outgoing tx found with nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - nonce ); + TransactionManagerLogger()->debug( "[{} - full: {}] No outgoing tx found with nonce {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + nonce ); } return ret; } @@ -2155,28 +2175,29 @@ namespace sgns 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() ); + 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 ( !CheckTransactionAuthorization( *new_tx ) ) { - m_logger->error( "[{} - full: {}] Could not validate signature of transaction {}", - 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; } if ( IsGoingToOverwrite( GetTransactionPath( *new_tx ); ) ) { - m_logger->debug( "[{} - full: {}] New transaction {} would overwrite an existing one. Preventing that", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_tx->GetHash() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] New transaction {} would overwrite an existing one. Preventing that", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_tx->GetHash() ); break; } should_delete = false; @@ -2211,10 +2232,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; } @@ -2225,16 +2246,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 ); @@ -2259,38 +2280,15 @@ namespace sgns bool TransactionManager::ShouldReplaceTransaction( const IGeniusTransactions &existing_tx, const IGeniusTransactions &new_tx ) const { - m_logger->debug( "[{} - full: {}] {}: Checking if new transaction {} should replace existing one {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - new_tx.GetHash(), - existing_tx.GetHash() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Checking if new transaction {} should replace existing one {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + new_tx.GetHash(), + existing_tx.GetHash() ); - 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; - } - const bool replace = new_tx.GetHash() < existing_tx.GetHash(); - if ( replace ) - { - m_logger->info( "[{} - full: {}] Deterministic replacement by hash: new {} < existing {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_tx.GetHash(), - existing_tx.GetHash() ); - } - else - { - m_logger->info( "[{} - full: {}] Deterministic replacement by hash: new {} >= existing {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_tx.GetHash(), - existing_tx.GetHash() ); - } - return replace; + return IsBetterTransaction( existing_tx.GetHash(), new_tx.GetHash() ); } uint64_t TransactionManager::GetCurrentTimestamp() @@ -2308,20 +2306,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; @@ -2346,10 +2345,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; } @@ -2357,19 +2357,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; @@ -2379,39 +2381,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 ) { @@ -2431,10 +2433,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(); } @@ -2444,40 +2446,41 @@ 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 ); + TransactionManagerLogger()->debug( "[{} - full: {}] Deserialized transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); if ( new_tx->GetHash().empty() ) { - m_logger->error( "[{} - full: {}] Empty hash on {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + 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: {}] Verifying if we have a conflicting 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 ); auto conflicting_tx = GetConflictingTransaction( *new_tx ); if ( conflicting_tx.has_value() ) { - m_logger->warn( "[{} - full: {}] Found conflicting transaction that passed the FILTER with hash: {}", - 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() ) ); @@ -2485,7 +2488,7 @@ namespace sgns if ( it->second.status == TransactionStatus::CONFIRMED ) { - m_logger->debug( + TransactionManagerLogger()->debug( "[{} - full: {}] Conflicting transaction is already CONFIRMED, not adding incoming transaction{}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -2495,31 +2498,34 @@ namespace sgns tx_lock.lock(); return outcome::failure( boost::system::error_code{} ); } - m_logger->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() ); + 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( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::VERIFYING ) ); } - m_logger->debug( "[{} - full: {}] Checking if the transaction has a valid certificate to be confirmed {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + TransactionManagerLogger()->debug( + "[{} - full: {}] Checking if the transaction has a valid certificate to be confirmed {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + key ); auto next_tx_state = TransactionStatus::VERIFYING; if ( blockchain_->CheckCertificate( new_tx->GetHash() ) ) { - m_logger->debug( "[{} - full: {}] Transaction has a valid certificate, marking as CONFIRMED {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + 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() ) { - m_logger->warn( + 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, @@ -2534,20 +2540,20 @@ namespace sgns 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() ); } } @@ -2585,20 +2591,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 { @@ -2607,7 +2613,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 ); @@ -2633,11 +2639,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(); @@ -2652,11 +2658,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(); @@ -2680,11 +2686,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_ ) @@ -2740,35 +2746,35 @@ namespace sgns void TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) { - m_logger->debug( "[{} - full: {}] {}: Consensus certificate arrived for transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - 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 ) { - m_logger->error( "[{} - full: {}] {}: Transaction not found for hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Transaction not found for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); return; } - m_logger->debug( "[{} - full: {}] {}: Checking for conflicting transaction with {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + 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() ) { - m_logger->warn( "[{} - full: {}] Found conflicting transaction: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - conflicting_tx.value()->GetHash() ); + 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() ) ); @@ -2776,7 +2782,7 @@ namespace sgns if ( it->second.status == TransactionStatus::CONFIRMED ) { - m_logger->error( + 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, @@ -2788,7 +2794,7 @@ namespace sgns auto result = ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ); if ( result.has_error() ) { - m_logger->error( + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to change conflicting transaction state to FAILED for current tx {}: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -2802,7 +2808,7 @@ namespace sgns auto result = ChangeTransactionState( tx, TransactionStatus::FAILED ); if ( result.has_error() ) { - m_logger->error( + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to change transaction state to FAILED for new tx {}: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -2815,7 +2821,7 @@ namespace sgns } else { - m_logger->warn( + TransactionManagerLogger()->warn( "[{} - full: {}] Setting conflicting transaction {} to FAILED since the new one {} is confirmed: ", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -2825,12 +2831,13 @@ namespace sgns auto result = ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ); if ( result.has_error() ) { - m_logger->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() ); + 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() ); } } } @@ -2838,19 +2845,20 @@ namespace sgns auto result = ChangeTransactionState( tx, TransactionStatus::CONFIRMED ); if ( result.has_error() ) { - m_logger->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() ); + 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; } - m_logger->debug( "[{} - full: {}] {}: Transaction {} confirmed by consensus", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Transaction {} confirmed by consensus", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); } outcome::result TransactionManager::HandleNonceConsensusSubject( @@ -2858,11 +2866,11 @@ namespace sgns { if ( subject.type() != SubjectType::SUBJECT_NONCE ) { - m_logger->error( "[{} - full: {}] {}: Received unexpected subject type: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - static_cast( subject.type() ) ); + 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 ); } @@ -2873,52 +2881,52 @@ namespace sgns auto it = tx_processed_m.find( key ); if ( it == tx_processed_m.end() ) { - m_logger->debug( "[{} - full: {}] {}: Transaction not found for hash {}, pending", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + 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; } auto &tracked = it->second; if ( !tracked.tx ) { - m_logger->error( "[{} - full: {}] {}: Tracked transaction missing for hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + 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 ); } if ( tracked.cached_nonce != subject.nonce().nonce() ) { - m_logger->error( "[{} - full: {}] {}: Nonce mismatch for hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Nonce mismatch for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); return ConsensusManager::SubjectCheck::Reject; } if ( !subject.account_id().empty() && tracked.tx->GetSrcAddress() != subject.account_id() ) { - m_logger->error( "[{} - full: {}] {}: Account mismatch for hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Account mismatch for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); return ConsensusManager::SubjectCheck::Reject; } if ( tracked.status == TransactionStatus::FAILED ) { - m_logger->error( "[{} - full: {}] {}: Transaction status invalid for hash {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Transaction status invalid for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); return ConsensusManager::SubjectCheck::Reject; } @@ -2930,211 +2938,211 @@ namespace sgns bool TransactionManager::ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, const std::string &address ) const { - m_logger->debug( "[{} - full: {}] {}: Validating UTXO params for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - address ); + 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() ) { - m_logger->error( "[{} - full: {}] {}: Empty inputs or outputs", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Empty inputs or outputs", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } if ( !full_node_m && address != account_m->GetAddress() ) { - m_logger->error( "[{} - full: {}] {}: Non-full node cannot verify address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - address ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Non-full node cannot verify address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + address ); return false; } if ( !utxo_manager_.VerifyParameters( params, address ) ) { - m_logger->error( "[{} - full: {}] {}: VerifyParameters failed for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - address ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: VerifyParameters failed for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + address ); return false; } - m_logger->debug( "[{} - full: {}] {}: UTXO params valid for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - address ); + 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 { - m_logger->debug( "[{} - full: {}] {}: Validating transaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Validating transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); if ( !tx ) { - m_logger->error( "[{} - full: {}] {}: Null transaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Null transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } if ( !CheckTransactionWellFormed( *tx ) ) { - m_logger->error( "[{} - full: {}] {}: Well-formed check failed tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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 ) ) { - m_logger->error( "[{} - full: {}] {}: Authorization check failed tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Authorization check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); return false; } if ( !CheckTransactionTimestamp( *tx ) ) { - m_logger->error( "[{} - full: {}] {}: Timestamp check failed tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Timestamp check failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); return false; } if ( !CheckTransactionReplayProtection( *tx ) ) { - m_logger->error( "[{} - full: {}] {}: Replay protection failed tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Replay protection failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); return false; } if ( !CheckTransactionTypeRules( tx ) ) { - m_logger->error( "[{} - full: {}] {}: Type rules failed tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Type rules failed tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); return false; } - m_logger->debug( "[{} - full: {}] {}: Transaction valid tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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 { - m_logger->debug( "[{} - full: {}] {}: Checking well-formed tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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() ) { - m_logger->error( "[{} - full: {}] {}: Hash invalid tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Hash invalid tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return false; } if ( tx.GetSrcAddress().empty() ) { - m_logger->error( "[{} - full: {}] {}: Empty source address tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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 ) { - m_logger->error( "[{} - full: {}] {}: Missing timestamp tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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() ) { - m_logger->error( "[{} - full: {}] {}: Unknown tx type {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetType() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Unknown tx type {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetType() ); return false; } - m_logger->debug( "[{} - full: {}] {}: Well-formed ok tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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 { - m_logger->debug( "[{} - full: {}] {}: Checking authorization tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking authorization tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); if ( tx.CheckSignature() || tx.CheckDAGSignatureLegacy() ) { - m_logger->debug( "[{} - full: {}] {}: Authorization ok tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Authorization ok tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return true; } - m_logger->error( "[{} - full: {}] {}: Authorization failed tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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 { - m_logger->debug( "[{} - full: {}] {}: Checking timestamp tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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 ) { - m_logger->error( "[{} - full: {}] {}: Missing timestamp tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing timestamp tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return false; } @@ -3142,45 +3150,45 @@ namespace sgns if ( elapsed < 0 && timestamp_tolerance_m.count() > 0 && ( -elapsed ) > static_cast( timestamp_tolerance_m.count() ) ) { - m_logger->error( "[{} - full: {}] {}: Timestamp out of tolerance tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Timestamp out of tolerance tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetHash() ); return false; } - m_logger->debug( "[{} - full: {}] {}: Timestamp ok tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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 { - m_logger->debug( "[{} - full: {}] {}: Checking replay protection tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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 ) { - m_logger->debug( "[{} - full: {}] {}: No peer nonce required for tx with nonce=0", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: No peer nonce required for tx with nonce=0", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return true; } - m_logger->error( "[{} - full: {}] {}: Missing peer nonce for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetSrcAddress() ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing peer nonce for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx.GetSrcAddress() ); return false; } @@ -3190,26 +3198,27 @@ namespace sgns if ( tx_nonce <= confirmed_nonce ) { - m_logger->error( "[{} - full: {}] {}: Nonce too low tx={} nonce={} confirmed={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash(), - 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 ) { - m_logger->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 ); + 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; } @@ -3220,47 +3229,49 @@ namespace sgns auto tracked = GetTrackedTxByNonceAndAddress( n, tx.GetSrcAddress() ); if ( !tracked.has_value() ) { - m_logger->error( "[{} - full: {}] {}: Missing intermediate nonce {} for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - n, - tx.GetSrcAddress() ); + 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 ) { - m_logger->error( "[{} - full: {}] {}: Intermediate nonce {} invalid for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - n, - tx.GetSrcAddress() ); + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Intermediate nonce {} invalid for address {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + n, + tx.GetSrcAddress() ); return false; } } } - m_logger->debug( "[{} - full: {}] {}: Replay protection ok tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetHash() ); + 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 { - m_logger->debug( "[{} - full: {}] {}: Checking type rules", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking type rules", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); if ( !tx ) { - m_logger->error( "[{} - full: {}] {}: Null transaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Null transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } @@ -3269,10 +3280,10 @@ namespace sgns auto transfer_tx = std::dynamic_pointer_cast( tx ); if ( !transfer_tx ) { - m_logger->error( "[{} - full: {}] {}: Failed to cast transfer tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast transfer tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return ValidateUTXOParametersForConsensus( @@ -3285,10 +3296,10 @@ namespace sgns auto escrow_tx = std::dynamic_pointer_cast( tx ); if ( !escrow_tx ) { - m_logger->error( "[{} - full: {}] {}: Failed to cast escrow-hold tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast escrow-hold tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return ValidateUTXOParametersForConsensus( escrow_tx->GetUTXOParameters(), escrow_tx->GetSrcAddress() ); @@ -3299,10 +3310,10 @@ namespace sgns auto escrow_release_tx = std::dynamic_pointer_cast( tx ); if ( !escrow_release_tx ) { - m_logger->error( "[{} - full: {}] {}: Failed to cast escrow-release tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast escrow-release tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return ValidateUTXOParametersForConsensus( escrow_release_tx->GetUTXOParameters(), @@ -3314,18 +3325,18 @@ namespace sgns auto mint_tx = std::dynamic_pointer_cast( tx ); if ( !mint_tx ) { - m_logger->error( "[{} - full: {}] {}: Failed to cast mint tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast mint tx", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } if ( mint_tx->GetAmount() == 0 ) { - m_logger->error( "[{} - full: {}] {}: Mint amount is zero", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + TransactionManagerLogger()->error( "[{} - full: {}] {}: Mint amount is zero", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__ ); return false; } return true; @@ -3338,31 +3349,31 @@ namespace sgns { if ( window == 0 ) { - m_logger->warn( "[{} - full: {}] {}: Nonce window 0, using default {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - DEFAULT_NONCE_WINDOW ); + 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; } - m_logger->info( "[{} - full: {}] {}: Setting nonce window to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - window ); + 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 ) { - m_logger->debug( "[{} - full: {}] {}: Changing transaction state to {} for transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - static_cast( new_status ), - tx->GetHash() ); + 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: @@ -3372,18 +3383,19 @@ namespace sgns auto it = tx_processed_m.find( key ); if ( it != tx_processed_m.end() ) { - m_logger->error( "[{} - full: {}] {}: Trying to CREATE a transaction that already exists {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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 ); } - m_logger->debug( "[{} - full: {}] {}: Set status of CREATE to transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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; @@ -3394,16 +3406,17 @@ namespace sgns auto it = tx_processed_m.find( key ); if ( it == tx_processed_m.end() ) { - m_logger->error( "[{} - full: {}] {}: Trying to SEND a transaction that doesn't exist {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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 ) { - m_logger->error( + TransactionManagerLogger()->error( "[{} - full: {}] {}: Trying to SEND a transaction that is not in CREATED status {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -3412,11 +3425,11 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } it->second.status = TransactionStatus::SENDING; - m_logger->debug( "[{} - full: {}] {}: Set status of SENDING to transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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: @@ -3427,20 +3440,22 @@ namespace sgns if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::VERIFYING ) { - m_logger->error( "[{} - full: {}] {}: Trying to VERIFY a transaction that is already in VERIFY {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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 ) { - m_logger->warn( "[{} - full: {}] {}: Unconfirming transaction {} and verifying it again", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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() ) ); @@ -3448,23 +3463,25 @@ namespace sgns account_m->RollBackPeerConfirmedNonce( it->second.cached_nonce, tx->GetSrcAddress() ); } tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::VERIFYING, tx->GetNonce() }; - m_logger->debug( "[{} - full: {}] {}: Set status of VERIFYING to transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); - m_logger->debug( "[{} - full: {}] {}: Attempting to resume the proposal handling to transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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() ) ); - m_logger->debug( "[{} - full: {}] {}: Resumed the proposal handling to transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Resumed the proposal handling to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); } break; @@ -3475,20 +3492,21 @@ namespace sgns auto it = tx_processed_m.find( key ); if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::CONFIRMED ) { - m_logger->error( "[{} - full: {}] {}: Trying to CONFIRM a transaction that is already CONFIRMED {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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() }; - m_logger->debug( "[{} - full: {}] {}: Set status of CONFIRMED to transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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() ); } @@ -3502,20 +3520,21 @@ namespace sgns auto it = tx_processed_m.find( key ); if ( it != tx_processed_m.end() && it->second.status == TransactionStatus::FAILED ) { - m_logger->error( "[{} - full: {}] {}: Trying to FAIL a transaction that is already FAILED {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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 ) { - m_logger->debug( "[{} - full: {}] {}: Unconfirming transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + 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() ) ); @@ -3525,30 +3544,31 @@ namespace sgns tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::FAILED, tx->GetNonce() }; account_m->ReleaseNonce( tx->GetNonce() ); - m_logger->debug( "[{} - full: {}] {}: Set status of FAILED to transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of FAILED to transaction {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); } break; default: - m_logger->error( "[{} - full: {}] {}: Invalid transaction status {} for transaction {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - static_cast( new_status ), - tx->GetHash() ); + 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 ); } - m_logger->debug( "[{} - full: {}] {}: Transaction {} state changed to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - static_cast( new_status ) ); + 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 @@ -3556,24 +3576,56 @@ namespace sgns auto existing_data_result = globaldb_m->Get( key ); if ( existing_data_result.has_value() ) { - m_logger->debug( "[{} - full: {}] {}: Key {} already exists in global DB, will overwrite", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - key ); + 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() ) { - m_logger->error( "[{} - full: {}] Failed to deserialize existing transaction, allow to replace it {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - key ); + 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; } + bool TransactionManager::IsBetterTransaction( const std::string &existing_hash, const std::string &new_hash ) + { + if ( existing_hash == new_hash ) + { + TransactionManagerLogger()->info( + "[{} - full: {}] Already have the same transaction, rejecting replacement attempt", + account_m->GetAddress().substr( 0, 8 ), + full_node_m ); + return false; + } + const bool replace = new_hash < existing_hash; + if ( replace ) + { + TransactionManagerLogger()->info( "[{} - full: {}] Deterministic replacement by hash: new {} < existing {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_hash, + existing_hash ); + } + else + { + TransactionManagerLogger()->info( + "[{} - full: {}] Deterministic replacement by hash: new {} >= existing {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + new_hash, + existing_hash ); + } + return replace; + } + } diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index e77d77316..62932e2db 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -340,8 +340,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 ); @@ -384,6 +382,8 @@ namespace sgns TransactionStatus new_status ); bool IsGoingToOverwrite( const std::string &key ) const; + + static bool IsBetterTransaction( const std::string &existing_hash, const std::string &new_hash ); }; } From 87771533a2bffd1906a595b257abf32bd2bd6651 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Feb 2026 12:38:28 -0300 Subject: [PATCH 056/114] Fix: Criteria for deciding best transaction and proposal now match --- src/account/TransactionManager.cpp | 36 +++--------------------------- src/account/TransactionManager.hpp | 2 -- src/blockchain/Blockchain.hpp | 3 ++- src/blockchain/Consensus.cpp | 10 +++++---- src/blockchain/Consensus.hpp | 1 + src/blockchain/impl/Blockchain.cpp | 5 +++++ 6 files changed, 17 insertions(+), 40 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index d48a843cc..35a207b76 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -2191,7 +2191,7 @@ namespace sgns element.key() ); break; } - if ( IsGoingToOverwrite( GetTransactionPath( *new_tx ); ) ) + if ( IsGoingToOverwrite( GetTransactionPath( *new_tx ) ) ) { TransactionManagerLogger()->debug( "[{} - full: {}] New transaction {} would overwrite an existing one. Preventing that", @@ -2288,7 +2288,7 @@ namespace sgns new_tx.GetHash(), existing_tx.GetHash() ); - return IsBetterTransaction( existing_tx.GetHash(), new_tx.GetHash() ); + return blockchain_->BestHash( existing_tx.GetHash(), new_tx.GetHash() ) == new_tx.GetHash(); } uint64_t TransactionManager::GetCurrentTimestamp() @@ -2789,7 +2789,7 @@ namespace sgns conflicting_tx.value()->GetHash(), tx_hash ); tx_lock.unlock(); - if ( ShouldReplaceTransaction( *conflicting_tx, *tx ) ) + if ( ShouldReplaceTransaction( *conflicting_tx.value(), *tx ) ) { auto result = ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ); if ( result.has_error() ) @@ -3595,36 +3595,6 @@ namespace sgns } return false; } - bool TransactionManager::IsBetterTransaction( const std::string &existing_hash, const std::string &new_hash ) - { - if ( existing_hash == new_hash ) - { - TransactionManagerLogger()->info( - "[{} - full: {}] Already have the same transaction, rejecting replacement attempt", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return false; - } - const bool replace = new_hash < existing_hash; - if ( replace ) - { - TransactionManagerLogger()->info( "[{} - full: {}] Deterministic replacement by hash: new {} < existing {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_hash, - existing_hash ); - } - else - { - TransactionManagerLogger()->info( - "[{} - full: {}] Deterministic replacement by hash: new {} >= existing {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - new_hash, - existing_hash ); - } - return replace; - } } diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 62932e2db..10ce11a5d 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -382,8 +382,6 @@ namespace sgns TransactionStatus new_status ); bool IsGoingToOverwrite( const std::string &key ) const; - - static bool IsBetterTransaction( const std::string &existing_hash, const std::string &new_hash ); }; } diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index b02a5a46e..4fdb18121 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -120,7 +120,8 @@ namespace sgns outcome::result TryResumeProposal( const std::string &hash ); - bool CheckCertificate(const std::string &subject_hash) const; + bool CheckCertificate( const std::string &subject_hash ) const; + const std::string &BestHash( const std::string &a, const std::string &b ) const; protected: friend class Migration3_5_1To3_6_0; diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index db41e0331..b1a8df3c1 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -1635,15 +1635,17 @@ namespace sgns { return candidate.proposal_id() < current.proposal_id(); } - return std::lexicographical_compare( cand_hash.begin(), - cand_hash.end(), - curr_hash.begin(), - curr_hash.end() ); + 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() ) ); diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 283dc0c90..2cf2554b1 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -113,6 +113,7 @@ namespace sgns const std::string &escrow_path, const std::string &task_result_hash, uint64_t result_epoch ); + 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 ); diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index b3d33a28a..f6f7c3cd7 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1584,4 +1584,9 @@ namespace sgns return consensus_manager_->CheckCertificateForSubject( subject_hash ); } + const std::string &Blockchain::BestHash( const std::string &a, const std::string &b ) const + { + return consensus_manager_->BestHash( a, b ); + } + } From 6890e8b5d13b542440952c40000a9a2e32b4f656 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Feb 2026 12:50:17 -0300 Subject: [PATCH 057/114] Fix: Multi account sync test --- test/src/multiaccount/multi_account_sync.cpp | 26 ++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 9c2717e2f..d899574c9 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -280,9 +280,11 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) balance_full_start ); // Get initial transaction counts - 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(); + 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, @@ -336,6 +338,7 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) ASSERT_TRUE( transfer2_res.has_value() ) << "Transfer 2 failed on node_same_addr_2"; + auto best_tx = ConsensusManager::BestHash( transfer1_res.value(), transfer2_res.value() ); // Add peers to each node node_same_addr_2->GetPubSub()->AddPeers( { node_same_addr_1->GetPubSub()->GetInterfaceAddress() } ); @@ -343,15 +346,22 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) transfer1_res.value(), std::chrono::milliseconds( INCOMING_TIMEOUT_MILLISECONDS ) ); + uint64_t correct_tokens_transferred = 10000000000; + if ( best_tx == transfer2_res.value() ) + { + correct_tokens_transferred = 13000000000; + } test::assertWaitForCondition( - [&]() { return node_same_addr_2->GetBalance() == ( balance_node1_after_mint - 10000000000 ); }, + [&]() { return node_same_addr_1->GetBalance() == node_same_addr_1->GetBalance() - correct_tokens_transferred; }, std::chrono::milliseconds( 50000 ), "node_same_addr_2 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() ); + 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 ) ); @@ -366,8 +376,10 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) balance_full_final ); // Get final transaction counts - 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(); + 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 ); From 10701cd2349e14f7c9ff72d92650713f82979140 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Feb 2026 14:26:10 -0300 Subject: [PATCH 058/114] Fix: Multi account test wrong condition --- test/src/multiaccount/multi_account_sync.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index d899574c9..752c76f21 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -352,7 +352,7 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) correct_tokens_transferred = 13000000000; } test::assertWaitForCondition( - [&]() { return node_same_addr_1->GetBalance() == node_same_addr_1->GetBalance() - correct_tokens_transferred; }, + [&]() { return node_same_addr_1->GetBalance() == ( balance_node1_after_mint - correct_tokens_transferred ); }, std::chrono::milliseconds( 50000 ), "node_same_addr_2 balance not synced" ); test::assertWaitForCondition( [&]() { return node_same_addr_2->GetBalance() == node_same_addr_1->GetBalance(); }, From 2d0e01319247cf5140ae6dd4009a52442cda36b1 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Feb 2026 17:01:38 -0300 Subject: [PATCH 059/114] WIP: Updating validator registry after consensus --- src/blockchain/Consensus.cpp | 39 +++++++++++++++++++++++++--- src/blockchain/ValidatorRegistry.cpp | 33 +++++++++++++++++++++++ src/blockchain/ValidatorRegistry.hpp | 4 ++- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index b1a8df3c1..e24361729 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -885,11 +885,42 @@ namespace sgns crdt::GlobalDB::Buffer cert_value; cert_value.put( serialized ); - auto put_result = db_->Put( cert_key, cert_value, { consensus_datastore_topic_ } ); - if ( put_result.has_error() ) + auto update_result = registry_->CreateUpdateFromCertificate( certificate ); + if ( update_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: crdt put error={}", __func__, put_result.error().message() ); - return outcome::failure( put_result.error() ); + ConsensusManagerLogger()->error( "{}: failed: registry update creation error={}", + __func__, + update_result.error().message() ); + return outcome::failure( update_result.error() ); + } + + auto tx_result = registry_->BeginRegistryUpdateTransaction( update_result.value() ); + if ( tx_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: begin registry update transaction error={}", + __func__, + tx_result.error().message() ); + return outcome::failure( tx_result.error() ); + } + + auto tx = tx_result.value(); + auto cert_put = tx->Put( cert_key, cert_value ); + if ( cert_put.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: stage certificate put error={}", + __func__, + cert_put.error().message() ); + return outcome::failure( cert_put.error() ); + } + + auto commit_result = tx->Commit( + { consensus_datastore_topic_, std::string( ValidatorRegistry::ValidatorTopic() ) } ); + if ( commit_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: failed: transaction commit error={}", + __func__, + commit_result.error().message() ); + return outcome::failure( commit_result.error() ); } ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 7e90bd496..31b44862a 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -584,6 +584,39 @@ namespace sgns return outcome::success(); } + outcome::result> ValidatorRegistry::BeginRegistryUpdateTransaction( + const RegistryUpdate &update ) + { + 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() ); + } + + base::Buffer update_buffer( + gsl::span( serialized_update.value().data(), serialized_update.value().size() ) ); + + auto tx = db_->BeginTransaction(); + if ( !tx ) + { + logger_->error( "{}: failed to begin atomic transaction", __func__ ); + return outcome::failure( std::errc::not_enough_memory ); + } + + crdt::HierarchicalKey registry_key{ std::string( RegistryKey() ) }; + auto registry_put = tx->Put( registry_key, update_buffer ); + if ( registry_put.has_error() ) + { + logger_->error( "{}: failed to stage registry update in transaction", __func__ ); + return outcome::failure( registry_put.error() ); + } + + logger_->debug( "{}: staged registry update in transaction", __func__ ); + return tx; + } + void ValidatorRegistry::SetMaxNewValidatorsPerUpdate( size_t max_new ) { logger_->trace( "{}: entry max_new={}", __func__, max_new ); diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index bc8b279e7..126a52bc4 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -93,7 +93,9 @@ namespace sgns bool RegisterFilter(); outcome::result CreateUpdateFromCertificate( const sgns::ConsensusCertificate &certificate ); outcome::result StoreRegistryUpdate( const RegistryUpdate &update ); - void SetMaxNewValidatorsPerUpdate( size_t max_new ); + 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; From cddb72b15973e4e67be054e80854344a08f366dd Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 5 Mar 2026 12:34:34 -0300 Subject: [PATCH 060/114] Chore: More consensus tests --- .../blockchain/consensus_certificate_test.cpp | 333 +++++++++++++++++- 1 file changed, 321 insertions(+), 12 deletions(-) diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index f3ecef19a..e26e92621 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -12,6 +12,7 @@ namespace { constexpr const char *kTestPrivateKey = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce8b1a6f0d4f3b9b7f0a1b2"; + constexpr const char *kTestPrivateKey2 = "0x6c3e7b1a8d3f2c9b0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2"; std::shared_ptr MakeAccount( const std::string &path ) { @@ -20,6 +21,13 @@ namespace 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 ) { @@ -51,6 +59,21 @@ namespace 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; + } } namespace sgns::test @@ -71,12 +94,7 @@ namespace sgns::test auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = ConsensusManager::New( - registry, - db_, - pubs_, - [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, - account->GetAddress() ); + auto manager = MakeManager( registry, db_, pubs_, account ); std::string tx_hash = "0x010203"; auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); @@ -108,12 +126,7 @@ namespace sgns::test auto account = MakeAccount( getPathString() ); auto registry = MakeRegistry( db_, account ); - auto manager = ConsensusManager::New( - registry, - db_, - pubs_, - [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, - account->GetAddress() ); + auto manager = MakeManager( registry, db_, pubs_, account ); std::string tx_hash = "0x010203"; auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); @@ -155,4 +168,300 @@ namespace sgns::test 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" ); + 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" ); + 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" ); + 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" ); + 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 ); + 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 ); + } } // namespace sgns::test From fb118168494cf8cf85fbebad6411592cdc254369 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Mar 2026 10:34:21 -0300 Subject: [PATCH 061/114] Fix: Merge errors --- src/account/GeniusAccount.cpp | 120 --------------- src/account/GeniusNode.cpp | 2 +- src/account/Migration3_4_0To3_5_0.cpp | 2 +- src/account/TransactionManager.cpp | 212 +++++++++++++------------- 4 files changed, 111 insertions(+), 225 deletions(-) diff --git a/src/account/GeniusAccount.cpp b/src/account/GeniusAccount.cpp index 348a2a059..51c1c3959 100644 --- a/src/account/GeniusAccount.cpp +++ b/src/account/GeniusAccount.cpp @@ -560,126 +560,6 @@ namespace sgns return signed_vector; } - outcome::result< - std::pair, std::pair>> - GeniusAccount::GenerateGeniusAddress( const char *eth_private_key, boost::filesystem::path base_path ) - { - constexpr std::string_view PREFIX = "SGNS"; - constexpr std::string_view FILE_NAME = "secure_storage_id"; - - // Convert to absolute path to handle relative paths properly - base_path = boost::filesystem::absolute( base_path ); - - boost::filesystem::create_directories( base_path ); - - // Use canonical() after directory exists to get fully normalized path - base_path = boost::filesystem::canonical( base_path ); - base_path /= FILE_NAME; - - genius_account_logger()->info( "Secure storage ID path: {}", base_path.string() ); - - // Try to load existing storage - std::shared_ptr storage; - nil::crypto3::multiprecision::uint256_t key_seed; - bool key_seed_loaded = false; - - if ( std::ifstream file( base_path.string() ); file.is_open() ) - { - std::string public_key; - file >> public_key; - genius_account_logger()->info( "Loaded public key from file: {} (length: {})", - public_key.substr( 0, 16 ) + "...", - public_key.length() ); - - OUTCOME_TRY( std::vector vec, base::unhex( public_key ) ); - - genius_account_logger()->info( "Unhexed public key vector size: {}", vec.size() ); - - // Create storage using the public key from the file - storage = std::make_shared( std::string( PREFIX ) + - libp2p::multi::detail::encodeBase58( vec ) ); - - if ( auto load_res = storage->Load( "sgns_key" ) ) - { - genius_account_logger()->info( "Successfully loaded key_seed from storage" ); - key_seed = nil::crypto3::multiprecision::uint256_t( load_res.value() ); - - // Validate that the loaded key_seed produces the same public key - ethereum::EthereumKeyGenerator temp_eth_key( key_seed ); - auto regenerated_pub_key = temp_eth_key.GetEntirePubValue(); - - if ( regenerated_pub_key == public_key ) - { - genius_account_logger()->info( "Validation successful: key_seed matches stored public key" ); - key_seed_loaded = true; - } - else - { - genius_account_logger()->error( "Validation failed: key_seed does not match stored public key" ); - genius_account_logger()->error( "Expected: {}", public_key.substr( 0, 16 ) + "..." ); - genius_account_logger()->error( "Got: {}", regenerated_pub_key.substr( 0, 16 ) + "..." ); - // Don't set key_seed_loaded, will regenerate - } - } - else - { - genius_account_logger()->warn( "Could not load sgns_key from secure storage, will regenerate" ); - } - } - else - { - genius_account_logger()->debug( "Secure storage ID file does not exist, will create new one" ); - } - - // Generate key_seed from ethereum private key if not loaded - if ( !key_seed_loaded ) - { - genius_account_logger()->trace( "Key seed from ethereum private key" ); - if ( eth_private_key == nullptr ) - { - return outcome::failure( std::errc::invalid_argument ); - } - - OUTCOME_TRY( auto as_vec, base::unhex( eth_private_key ) ); - TW::PrivateKey private_key( as_vec ); - - auto signed_secret = private_key.sign( - TW::Data( ELGAMAL_PUBKEY_PREDEFINED.cbegin(), ELGAMAL_PUBKEY_PREDEFINED.cend() ), - TWCurveSECP256k1 ); - - if ( signed_secret.empty() ) - { - genius_account_logger()->error( "Cannot sign secret" ); - return outcome::failure( std::errc::invalid_argument ); - } - - key_seed = nil::crypto3::multiprecision::uint256_t( TW::Hash::sha256( signed_secret ) ); - - // Create storage with loaded key - ethereum::EthereumKeyGenerator temp_eth_key( key_seed ); - auto pub_key = temp_eth_key.GetEntirePubValue(); - OUTCOME_TRY( std::vector vec, base::unhex( pub_key ) ); - storage = std::make_shared( std::string( PREFIX ) + - libp2p::multi::detail::encodeBase58( vec ) ); - - BOOST_OUTCOME_TRYV2( auto &&, storage->Save( "sgns_key", key_seed.str() ) ); - - // Write public key to file - std::ofstream out_file( base_path.string() ); - if ( !out_file.is_open() ) - { - return outcome::failure( std::errc::bad_file_descriptor ); - } - out_file << pub_key << std::endl; - } - - KeyGenerator::ElGamal elgamal_key( key_seed ); - ethereum::EthereumKeyGenerator eth_key( key_seed ); - auto pub_key = eth_key.GetEntirePubValue(); - - return std::make_pair( storage, std::make_pair( elgamal_key, eth_key ) ); - } - void GeniusAccount::SetPeerConfirmedNonce( uint64_t nonce, const std::string &address ) { std::unique_lock lock( nonce_mutex_ ); diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index 592d81325..883ff815e 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -365,7 +365,7 @@ namespace sgns strong->node_logger_->error( "Error starting blockchain: {}", result.error().message() ); strong->node_logger_->info( "Scheduling blockchain retry after failure" ); - strong->account_->RequestHeads({std::string(blockchain::ValidatorRegistry::ValidatorTopic())}); + strong->account_->RequestHeads({std::string(ValidatorRegistry::ValidatorTopic())}); strong->ScheduleBlockchainRetry(); return; } diff --git a/src/account/Migration3_4_0To3_5_0.cpp b/src/account/Migration3_4_0To3_5_0.cpp index eebb2ba41..d72dfe8e1 100644 --- a/src/account/Migration3_4_0To3_5_0.cpp +++ b/src/account/Migration3_4_0To3_5_0.cpp @@ -139,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/TransactionManager.cpp b/src/account/TransactionManager.cpp index 35a207b76..3729be4d5 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -1545,14 +1545,16 @@ 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(); @@ -1561,31 +1563,32 @@ namespace sgns 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; } @@ -1596,30 +1599,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; } @@ -1630,10 +1634,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; } @@ -1659,10 +1663,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; } @@ -1698,18 +1702,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; @@ -1717,45 +1721,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 ); @@ -1766,9 +1771,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; } @@ -1776,29 +1781,29 @@ 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; } @@ -2567,10 +2572,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 ); } @@ -2626,11 +2632,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; @@ -2712,11 +2718,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; @@ -3571,7 +3577,8 @@ namespace sgns static_cast( new_status ) ); return outcome::success(); } - bool TransactionManager::IsGoingToOverwrite( const std::string &key ) const + + bool TransactionManager::IsGoingToOverwrite( const std::string &key ) const { auto existing_data_result = globaldb_m->Get( key ); if ( existing_data_result.has_value() ) @@ -3598,7 +3605,6 @@ namespace sgns } - fmt::format_context::iterator fmt::formatter::format( sgns::TransactionManager::State s, format_context &ctx ) const From 99036ac316969b16b07e48ab19cf98e36af8cffe Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Mar 2026 10:51:55 -0300 Subject: [PATCH 062/114] Fix: When trying to send new transaction don't rely on cached nonce --- src/account/TransactionManager.cpp | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 3729be4d5..846aa7ef5 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -715,20 +715,10 @@ namespace sgns { crdt_transaction = globaldb_m->BeginTransaction(); } - auto nonce_result = account_m->GetConfirmedNonce( NONCE_REQUEST_TIMEOUT_MS ); + auto nonce_result = account_m->FetchNetworkNonce( NONCE_REQUEST_TIMEOUT_MS ); uint64_t expected_next_nonce = 0; - int64_t confirmed_nonce = -1; - if ( nonce_result.has_value() ) - { - confirmed_nonce = static_cast( nonce_result.value() ); - TransactionManagerLogger()->debug( "[{} - full: {}] Set nonce to {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - confirmed_nonce ); - expected_next_nonce = static_cast( confirmed_nonce ) + 1; - } - else if ( nonce_result.has_error() && nonce_result.error() == AccountMessenger::Error::NO_RESPONSE_RECEIVED ) + if ( nonce_result.has_error() ) { if ( !full_node_m ) { @@ -738,21 +728,26 @@ namespace sgns full_node_m ); return outcome::failure( boost::system::errc::make_error_code( boost::system::errc::timed_out ) ); } - TransactionManagerLogger()->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() ); - TransactionManagerLogger()->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; + expected_next_nonce = local_confirmed.value() + 1; } } + else if ( nonce_result.value().has_value() ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] Set nonce to {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + nonce_result.value().value() ); + expected_next_nonce = nonce_result.value().value() + 1; + } std::unordered_set topicSet; std::set> transactions_sent; if ( !transaction_batch.empty() ) From 0e0fb04adbd5ca0961ce5408e254468711000a4d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 6 Mar 2026 16:19:32 -0300 Subject: [PATCH 063/114] Fix: multi account sync test now works --- src/account/GeniusNode.cpp | 2 +- src/account/TransactionManager.cpp | 18 ++++----- src/blockchain/Consensus.cpp | 42 ++++++++++++++++---- test/src/multiaccount/multi_account_sync.cpp | 38 ++++++++++++++---- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index 883ff815e..d10574584 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -482,7 +482,7 @@ namespace sgns auto loggerGraphsync = ConfigureLogger( "graphsync", logdir, spdlog::level::err ); auto loggerBroadcaster = ConfigureLogger( "PubSubBroadcasterExt", logdir, spdlog::level::err ); auto loggerDataStore = ConfigureLogger( "CrdtDatastore", logdir, spdlog::level::err ); - auto loggerCRDTHeads = ConfigureLogger( "CrdtHeads", logdir, spdlog::level::trace ); + 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 ); diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 846aa7ef5..1ddd84bda 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -1060,11 +1060,6 @@ 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() ); - } TransactionManagerLogger()->trace( "[{} - full: {}] Transaction already processed: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, @@ -1129,11 +1124,6 @@ namespace sgns } OUTCOME_TRY( ChangeTransactionState( transaction, next_tx_state ) ); - { - std::lock_guard missing_lock( missing_tx_mutex_ ); - missing_tx_hashes_.erase( transaction->GetHash() ); - } - return outcome::success(); } @@ -3510,6 +3500,10 @@ namespace sgns 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; @@ -3550,6 +3544,10 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); + { + std::lock_guard missing_lock( missing_tx_mutex_ ); + missing_tx_hashes_.erase( tx->GetHash() ); + } } break; diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index e24361729..290fa3395 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -157,9 +157,9 @@ namespace sgns { interval = DEFAULT_ROUND_DURATION / 2; } - self->timer_cv_.wait( lock, [self]() { - return self->stop_timer_.load() || self->certificates_pending_.load(); - } ); + self->timer_cv_.wait( lock, + [self]() + { return self->stop_timer_.load() || self->certificates_pending_.load(); } ); if ( self->stop_timer_.load() ) { return; @@ -170,9 +170,10 @@ namespace sgns 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(); - } ); + self->timer_cv_.wait_for( + lock, + interval, + [self]() { return self->stop_timer_.load() || !self->certificates_pending_.load(); } ); } if ( self->stop_timer_.load() ) { @@ -379,12 +380,24 @@ namespace sgns void ConsensusManager::ContinueProposalAfterSubject( const Proposal &proposal ) { + ConsensusManagerLogger()->debug( "{}: Continuing proposal_id={}", + __func__, + proposal.proposal_id().substr( 0, 8 ) ); const auto slot_key = GetSlotKey( proposal ); bool should_vote = false; + + ConsensusManagerLogger()->debug( "{}: proposal_id={}, slot key {}", + __func__, + proposal.proposal_id().substr( 0, 8 ), + slot_key ); { std::lock_guard lock( proposals_mutex_ ); if ( proposals_.find( proposal.proposal_id() ) == proposals_.end() ) { + ConsensusManagerLogger()->debug( "{}: Creating proposal state proposal_id={}, slot key {}", + __func__, + proposal.proposal_id().substr( 0, 8 ), + slot_key ); ProposalState state; state.proposal = proposal; state.slot_key = slot_key; @@ -394,6 +407,10 @@ namespace sgns auto &slot_state = slot_states_[slot_key]; if ( slot_state.best_proposal_id.empty() ) { + ConsensusManagerLogger()->debug( "{}: Configuring best proposal_id={}, slot key {}", + __func__, + proposal.proposal_id().substr( 0, 8 ), + slot_key ); slot_state.best_proposal_id = proposal.proposal_id(); if ( proposal.subject().has_nonce() ) { @@ -403,8 +420,16 @@ namespace sgns else { const auto ¤t = proposals_.at( slot_state.best_proposal_id ).proposal; + ConsensusManagerLogger()->debug( + "{}: Already have a best proposal_id={}, slot key {}. Seeing if {} is better ", + __func__, + current.proposal_id().substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); if ( IsBetterProposal( proposal, current ) ) { + ConsensusManagerLogger()->debug( "{}: Better proposal_id={}Ã¥ ", + __func__, + proposal.proposal_id().substr( 0, 8 ) ); slot_state.best_proposal_id = proposal.proposal_id(); if ( proposal.subject().has_nonce() ) { @@ -415,6 +440,9 @@ namespace sgns if ( slot_state.best_proposal_id == proposal.proposal_id() && !slot_state.voted ) { + ConsensusManagerLogger()->debug( "{}: My proposal_id={}, is better so let's vote on it. ", + __func__, + proposal.proposal_id().substr( 0, 8 )); slot_state.voted = true; should_vote = true; } @@ -903,7 +931,7 @@ namespace sgns return outcome::failure( tx_result.error() ); } - auto tx = tx_result.value(); + auto tx = tx_result.value(); auto cert_put = tx->Put( cert_key, cert_value ); if ( cert_put.has_error() ) { diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 752c76f21..00c3a9df8 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -213,6 +213,10 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) [&] { return ( balance_original_start + 60000 + 2000 + 100 + 30 ) == node_duplicated->GetBalance(); }, std::chrono::milliseconds( 30000 ), "node_duplicated balance not synced" ); + test::assertWaitForCondition( + [&] { return ( balance_original_start + 60000 + 2000 + 100 + 30 ) == node_original->GetBalance(); }, + std::chrono::milliseconds( 30000 ), + "node_duplicated balance not synced" ); ASSERT_EQ( node_duplicated->GetBalance(), node_original->GetBalance() ); } @@ -338,7 +342,6 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) ASSERT_TRUE( transfer2_res.has_value() ) << "Transfer 2 failed on node_same_addr_2"; - auto best_tx = ConsensusManager::BestHash( transfer1_res.value(), transfer2_res.value() ); // Add peers to each node node_same_addr_2->GetPubSub()->AddPeers( { node_same_addr_1->GetPubSub()->GetInterfaceAddress() } ); @@ -346,19 +349,40 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) transfer1_res.value(), std::chrono::milliseconds( INCOMING_TIMEOUT_MILLISECONDS ) ); - uint64_t correct_tokens_transferred = 10000000000; - if ( best_tx == transfer2_res.value() ) - { - correct_tokens_transferred = 13000000000; - } + fmt::println( "Waiting for the conflict resolution" ); + + uint64_t correct_tokens_transferred = 0; + test::assertWaitForCondition( + [&]() + { + auto status1 = node_same_addr_1->GetTransactionStatus( transfer1_res.value() ); + if ( status1 == TransactionManager::TransactionStatus::CONFIRMED ) + { + correct_tokens_transferred = 10000000000; + return true; + } + + auto status2 = node_same_addr_2->GetTransactionStatus( transfer2_res.value() ); + if ( status2 == TransactionManager::TransactionStatus::CONFIRMED ) + { + correct_tokens_transferred = 13000000000; + return true; + } + + return false; + }, + std::chrono::milliseconds( 50000 ), + "Neither transfer was confirmed" ); + test::assertWaitForCondition( [&]() { return node_same_addr_1->GetBalance() == ( balance_node1_after_mint - correct_tokens_transferred ); }, std::chrono::milliseconds( 50000 ), - "node_same_addr_2 balance not synced" ); + "node_same_addr_1 balance not synced" ); test::assertWaitForCondition( [&]() { return node_same_addr_2->GetBalance() == node_same_addr_1->GetBalance(); }, std::chrono::milliseconds( 50000 ), "node_same_addr_2 balance not synced" ); + fmt::println( "Balances after bootstrap - Node1: {}, Node2: {}", node_same_addr_2->GetBalance(), node_same_addr_1->GetBalance() ); From 99676cbaa51420ef043a7e3d0a902e416f277740 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 9 Mar 2026 15:27:23 -0300 Subject: [PATCH 064/114] Chore: Improving logger on consensus --- src/blockchain/Consensus.cpp | 388 ++++++++++++++++++++++++----------- src/blockchain/Consensus.hpp | 32 +-- 2 files changed, 286 insertions(+), 134 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 290fa3395..3d0628ae1 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -205,9 +205,12 @@ namespace sgns { if ( !handler ) { - ConsensusManagerLogger()->warn( "{}: ignored empty handler type={}", __func__, static_cast( type ) ); + 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; @@ -226,11 +229,14 @@ namespace sgns { if ( !handler ) { - ConsensusManagerLogger()->warn( "{}: ignored empty certificate handler type={}", - __func__, - static_cast( type ) ); + 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; @@ -312,7 +318,9 @@ namespace sgns return 0; } const auto round_ms = static_cast( round_duration_.count() ); - return static_cast( ( elapsed - skew_ms ) / round_ms ); + auto round = static_cast( ( elapsed - skew_ms ) / round_ms ); + ConsensusManagerLogger()->debug( "{}: Returning round={}", __func__, round ); + return round; } std::vector ConsensusManager::GetOrderedActiveValidators( @@ -328,12 +336,14 @@ namespace sgns } } 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() ) { @@ -357,7 +367,7 @@ namespace sgns return ordered[index] == account_address_; } - outcome::result ConsensusManager::GetSubjectHash( const Subject &subject ) const + outcome::result ConsensusManager::GetSubjectHash( const Subject &subject ) { if ( subject.type() == SubjectType::SUBJECT_NONCE ) { @@ -380,24 +390,28 @@ namespace sgns void ConsensusManager::ContinueProposalAfterSubject( const Proposal &proposal ) { - ConsensusManagerLogger()->debug( "{}: Continuing proposal_id={}", + 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( "{}: proposal_id={}, slot key {}", + 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( "{}: Creating proposal state proposal_id={}, slot key {}", - __func__, - proposal.proposal_id().substr( 0, 8 ), - slot_key ); + 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; @@ -407,8 +421,9 @@ namespace sgns auto &slot_state = slot_states_[slot_key]; if ( slot_state.best_proposal_id.empty() ) { - ConsensusManagerLogger()->debug( "{}: Configuring best proposal_id={}, slot key {}", + 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(); @@ -421,15 +436,19 @@ namespace sgns { const auto ¤t = proposals_.at( slot_state.best_proposal_id ).proposal; ConsensusManagerLogger()->debug( - "{}: Already have a best proposal_id={}, slot key {}. Seeing if {} is better ", + "{}: 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_id={}Ã¥ ", + ConsensusManagerLogger()->debug( "{}: Better proposal for hash {}, id={}, slot key {}. ", __func__, - proposal.proposal_id().substr( 0, 8 ) ); + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); slot_state.best_proposal_id = proposal.proposal_id(); if ( proposal.subject().has_nonce() ) { @@ -440,9 +459,12 @@ namespace sgns if ( slot_state.best_proposal_id == proposal.proposal_id() && !slot_state.voted ) { - ConsensusManagerLogger()->debug( "{}: My proposal_id={}, is better so let's vote on it. ", - __func__, - proposal.proposal_id().substr( 0, 8 )); + 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; } @@ -454,15 +476,19 @@ namespace sgns if ( vote_result.has_value() ) { (void)SubmitVote( vote_result.value() ); - ConsensusManagerLogger()->debug( "{}: self-vote submitted proposal_id={}", + ConsensusManagerLogger()->debug( "{}: self-vote submitted for hash {}, id={}, slot key {}", __func__, - proposal.proposal_id() ); + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key ); } else { - ConsensusManagerLogger()->error( "{}: self-vote failed proposal_id={} error={}", + ConsensusManagerLogger()->error( "{}: self-vote failed for hash {}, id={}, slot key {} error={}", __func__, - proposal.proposal_id(), + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), + slot_key, vote_result.error().message() ); } } @@ -473,8 +499,17 @@ namespace sgns 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() ); } @@ -486,6 +521,7 @@ namespace sgns 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 ) @@ -497,6 +533,7 @@ namespace sgns 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; } @@ -515,16 +552,23 @@ namespace sgns uint64_t registry_epoch, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: called for proposer_id={}", __func__, proposer_id ); + ConsensusManagerLogger()->trace( "{}: called by {} with hash {}", + __func__, + proposer_id.substr( 0, 8 ), + GetPrintableSubjectHash( subject ) ); if ( !sign ) { - ConsensusManagerLogger()->error( "{}: failed: signer is empty", __func__ ); + ConsensusManagerLogger()->error( "{}: failed for hash {}: signer is empty", + __func__, + GetPrintableSubjectHash( subject ) ); return outcome::failure( std::errc::invalid_argument ); } if ( !ValidateSubject( subject ) ) { - ConsensusManagerLogger()->error( "{}: failed: subject validation failed", __func__ ); + ConsensusManagerLogger()->error( "{}: failed for hash {}: subject validation failed", + __func__, + GetPrintableSubjectHash( subject ) ); return outcome::failure( std::errc::invalid_argument ); } @@ -542,8 +586,9 @@ namespace sgns auto subject_id_result = ComputeSubjectId( proposal.subject() ); if ( subject_id_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: subject id computation 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() ); } @@ -559,10 +604,17 @@ namespace sgns 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 proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: success for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( subject ), + proposal.proposal_id().substr( 0, 8 ) ); return proposal; } @@ -571,10 +623,10 @@ namespace sgns bool approve, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={} approve={}", + ConsensusManagerLogger()->trace( "{}: called by {}: proposal_id={} approve={}", __func__, - proposal_id, - voter_id, + voter_id.substr( 0, 8 ), + proposal_id.substr( 0, 8 ), approve ); if ( !sign ) { @@ -602,7 +654,10 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); vote.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={} voter_id={}", __func__, proposal_id, voter_id ); + ConsensusManagerLogger()->debug( "{}: {} voted for proposal_id={}", + __func__, + voter_id.substr( 0, 8 ), + proposal_id.substr( 0, 8 ) ); return vote; } @@ -611,10 +666,10 @@ namespace sgns const std::vector &votes, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={} aggregator_id={} votes={}", + ConsensusManagerLogger()->trace( "{}: called by {}: proposal_id={} votes={}", __func__, - proposal_id, - aggregator_id, + aggregator_id.substr( 0, 8 ), + proposal_id.substr( 0, 8 ), votes.size() ); if ( !sign ) { @@ -645,16 +700,22 @@ namespace sgns OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); bundle.set_signature( signature.data(), signature.size() ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={} votes={}", __func__, proposal_id, votes.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( "{}: called proposal_id={} votes={}", + ConsensusManagerLogger()->trace( "{}: Creating certificate for hash {}: proposal_id={} votes={}", __func__, - proposal.proposal_id(), + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), votes.size() ); auto tally_result = TallyVotes( proposal, votes ); if ( tally_result.has_error() ) @@ -691,7 +752,10 @@ namespace sgns } *cert.mutable_proposal() = proposal; - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: Success creating certificate for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return cert; } @@ -703,18 +767,22 @@ namespace sgns { if ( !proposal.registry_cid().empty() && !registry_cid.empty() && proposal.registry_cid() != registry_cid ) { - ConsensusManagerLogger()->error( "{}: failed: registry cid mismatch proposal={} registry={}", - __func__, - 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 proposal={} registry={}", - __func__, - 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 ); } @@ -724,9 +792,10 @@ namespace sgns for ( const auto &vote : votes ) { - ConsensusManagerLogger()->trace( "{}: processing vote voter_id={} approve={}", + ConsensusManagerLogger()->trace( "{}: processing vote for hash {}: voter_id={} approve={}", __func__, - vote.voter_id(), + GetPrintableSubjectHash( proposal.subject() ), + vote.voter_id().substr( 0, 8 ), vote.approve() ); if ( vote.proposal_id() != proposal.proposal_id() ) { @@ -740,6 +809,11 @@ namespace sgns 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; } @@ -754,8 +828,18 @@ namespace sgns 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(); } } @@ -764,22 +848,26 @@ namespace sgns tally.total_weight = total_weight; tally.approved_weight = approved_weight; tally.has_quorum = registry_->IsQuorum( approved_weight, total_weight ); - ConsensusManagerLogger()->debug( "{}: success proposal_id={} approved_weight={} total_weight={} quorum={}", - __func__, - proposal.proposal_id(), - approved_weight, - total_weight, - tally.has_quorum ); + 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, proposal_id={} votes={}", - __func__, - proposal.proposal_id(), - votes.size() ); + 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() ); auto registry_result = registry_->LoadRegistry(); if ( registry_result.has_error() ) @@ -794,15 +882,18 @@ namespace sgns outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); + 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 voter_id={} proposal_id={}", + ConsensusManagerLogger()->trace( "{}: called with voter address {} proposal_id={}", __func__, - vote.voter_id(), + vote.voter_id().substr( 0, 8 ), vote.proposal_id() ); return sgns::VoteSigningBytes( vote ); } @@ -811,16 +902,17 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", __func__, - bundle.proposal_id(), + 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 proposal_id={} self_vote={}", + ConsensusManagerLogger()->trace( "{}: called for hash {} proposal_id={} self_vote={}", __func__, - proposal.proposal_id(), + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ), self_vote ); const auto slot_key = GetSlotKey( proposal ); { @@ -828,6 +920,10 @@ namespace sgns 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; @@ -845,7 +941,10 @@ namespace sgns publish_result.error().message() ); return publish_result; } - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, proposal.proposal_id() ); + ConsensusManagerLogger()->debug( "{}: success for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); if ( self_vote ) { @@ -857,10 +956,10 @@ namespace sgns outcome::result ConsensusManager::SubmitVote( const Vote &vote, bool self_handle ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={}", + ConsensusManagerLogger()->trace( "{}: called by {} proposal_id={}", __func__, - vote.proposal_id(), - vote.voter_id() ); + vote.voter_id().substr( 0, 8 ), + vote.proposal_id().substr( 0, 8 ) ); ConsensusMessage message; *message.mutable_vote() = vote; auto result = Publish( message ); @@ -869,10 +968,10 @@ namespace sgns ConsensusManagerLogger()->error( "{}: failed: publish error={}", __func__, result.error().message() ); return result; } - ConsensusManagerLogger()->debug( "{}: success proposal_id={} voter_id={}", + ConsensusManagerLogger()->debug( "{}: success voter_id={} proposal_id={} ", __func__, - vote.proposal_id(), - vote.voter_id() ); + vote.voter_id().substr( 0, 8 ), + vote.proposal_id().substr( 0, 8 ) ); if ( self_handle ) { HandleVote( vote ); @@ -882,7 +981,10 @@ namespace sgns outcome::result ConsensusManager::SubmitCertificate( const Certificate &certificate ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); + 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 ); @@ -895,9 +997,10 @@ namespace sgns auto subject_hash_result = GetSubjectHash( certificate.proposal().subject() ); if ( subject_hash_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: subject hash error proposal_id={}", + ConsensusManagerLogger()->error( "{}: failed: subject hash {} error proposal_id={}", __func__, - certificate.proposal_id() ); + GetPrintableSubjectHash( certificate.proposal().subject() ), + certificate.proposal_id().substr( 0, 8 ) ); return outcome::failure( subject_hash_result.error() ); } @@ -916,8 +1019,9 @@ namespace sgns auto update_result = registry_->CreateUpdateFromCertificate( certificate ); if ( update_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: registry update creation error={}", + ConsensusManagerLogger()->error( "{}: failed: registry update for hash {} error={}", __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ), update_result.error().message() ); return outcome::failure( update_result.error() ); } @@ -925,18 +1029,23 @@ namespace sgns auto tx_result = registry_->BeginRegistryUpdateTransaction( update_result.value() ); if ( tx_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: begin registry update transaction error={}", + ConsensusManagerLogger()->error( "{}: failed: begin registry update transaction for hash {} error={}", __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ), tx_result.error().message() ); return outcome::failure( tx_result.error() ); } + ConsensusManagerLogger()->debug( "{}: Creating CRDT transaction for registry update for hash {}", + __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ) ); auto tx = tx_result.value(); auto cert_put = tx->Put( cert_key, cert_value ); if ( cert_put.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: stage certificate put error={}", + ConsensusManagerLogger()->error( "{}: failed: stage certificate put for hash {} error={}", __func__, + GetPrintableSubjectHash( certificate.proposal().subject() ), cert_put.error().message() ); return outcome::failure( cert_put.error() ); } @@ -951,41 +1060,51 @@ namespace sgns return outcome::failure( commit_result.error() ); } - ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); + 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 proposal_id={}", __func__, proposal.proposal_id() ); + 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 proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: Invalid proposal for hash {} proposal_id={}", __func__, - proposal.proposal_id() ); + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return; } if ( !IsTimestampSane( proposal.timestamp() ) ) { - ConsensusManagerLogger()->error( "{}: rejected: timestamp out of bounds proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: timestamp out of bounds for hash {} proposal_id={}", __func__, - proposal.proposal_id() ); + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return; } const auto registry_cid = registry_->GetRegistryCid(); if ( registry_cid.empty() ) { - ConsensusManagerLogger()->error( "{}: rejected: Local registry doesn't have a CID. proposal_id={}", - __func__, - proposal.proposal_id() ); + ConsensusManagerLogger()->error( + "{}: rejected: Local registry doesn't have a CID for hash {}. proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return; } if ( proposal.registry_cid() != registry_cid ) { - ConsensusManagerLogger()->error( "{}: rejected: registry cid mismatch proposal={} registry={}", + ConsensusManagerLogger()->error( "{}: rejected: registry CID mismatch proposal={} registry={}", __func__, proposal.registry_cid(), registry_cid ); @@ -1002,9 +1121,10 @@ namespace sgns if ( !CheckSubject( proposal.subject() ) ) { - ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", __func__, - proposal.proposal_id() ); + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return; } @@ -1025,17 +1145,19 @@ namespace sgns auto subject_result = subject_handler( proposal.subject() ); if ( subject_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: rejected: subject handler error proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject handler error for hash {} proposal_id={}", __func__, - proposal.proposal_id() ); + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return; } if ( subject_result.value() == SubjectCheck::Reject ) { - ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", __func__, - proposal.proposal_id() ); + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return; } @@ -1046,9 +1168,13 @@ namespace sgns { ConsensusManagerLogger()->error( "{}: rejected: subject hash missing proposal_id={}", __func__, - proposal.proposal_id() ); + proposal.proposal_id().substr( 0, 8 ) ); return; } + 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; } @@ -1062,7 +1188,9 @@ namespace sgns { return outcome::failure( std::errc::invalid_argument ); } - ConsensusManagerLogger()->trace( "{}: called subject_hash={}", __func__, subject_hash ); + ConsensusManagerLogger()->trace( "{}: Attempting to resume proposals for hash={}", + __func__, + subject_hash.substr( 0, 8 ) ); auto to_process = TakePendingProposals( subject_hash ); @@ -1085,17 +1213,19 @@ namespace sgns auto subject_result = subject_handler( proposal.subject() ); if ( subject_result.has_error() ) { - ConsensusManagerLogger()->error( "{}: rejected: subject handler error proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject handler error for hash {} proposal_id={}", __func__, - proposal.proposal_id() ); + subject_hash.substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); continue; } if ( subject_result.value() == SubjectCheck::Reject ) { - ConsensusManagerLogger()->error( "{}: rejected: subject check failed proposal_id={}", + ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", __func__, - proposal.proposal_id() ); + subject_hash.substr( 0, 8 ), + proposal.proposal_id().substr( 0, 8 ) ); continue; } @@ -1109,6 +1239,10 @@ namespace sgns 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; } @@ -1136,9 +1270,11 @@ namespace sgns auto &state = kv.second; if ( !state.quorum_reached ) { - ConsensusManagerLogger()->debug( "{}: Found proposal without quorum reached proposal_id={}", - __func__, - state.proposal.proposal_id() ); + ConsensusManagerLogger()->debug( + "{}: Found proposal without quorum reached for hash {} proposal_id={}", + __func__, + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); continue; } @@ -1148,23 +1284,27 @@ namespace sgns for ( auto &state : to_process ) { - ConsensusManagerLogger()->debug( "{}: Processing proposal with quorum reached proposal_id={}", + ConsensusManagerLogger()->debug( "{}: Processing proposal with quorum reached for hash {} proposal_id={}", __func__, - state.proposal.proposal_id() ); + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); 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 proposal_id={} round={}", - __func__, - state.proposal.proposal_id(), - 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; } if ( !IsCurrentAggregator( state.proposal, registry ) ) { - ConsensusManagerLogger()->debug( "{}: not aggregator for proposal proposal_id={}", + ConsensusManagerLogger()->debug( "{}: not aggregator for proposal for hash {} proposal_id={}", __func__, - state.proposal.proposal_id() ); + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); continue; } @@ -1176,24 +1316,29 @@ namespace sgns it->second.last_attempt_round = round; } } - ConsensusManagerLogger()->debug( "{}: Attempting to create certificate proposal_id={} round={}", + ConsensusManagerLogger()->debug( "{}: Attempting to create certificate for hash {} proposal_id={} round={}", __func__, - state.proposal.proposal_id(), + 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={}", - __func__, - certificate_result.error().message() ); + 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() ); ClearProposalState( state.proposal ); - ConsensusManagerLogger()->debug( "{}: certificate submitted proposal_id={}", + ConsensusManagerLogger()->debug( "{}: certificate submitted for hash {} proposal_id={}", __func__, - state.proposal.proposal_id() ); + GetPrintableSubjectHash( state.proposal.subject() ), + state.proposal.proposal_id().substr( 0, 8 ) ); } } @@ -2029,4 +2174,11 @@ namespace sgns return true; } + std::string ConsensusManager::GetPrintableSubjectHash( const Subject &subject ) + { + auto subject_hash = GetSubjectHash( subject ); + const std::string short_hash = subject_hash.has_value() ? subject_hash.value().substr( 0, 8 ) : "Invalid"; + return short_hash; + } + } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 2cf2554b1..3d130dea3 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -113,7 +113,7 @@ namespace sgns const std::string &escrow_path, const std::string &task_result_hash, uint64_t result_epoch ); - static const std::string & BestHash( const std::string &a, const std::string &b ); + 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 ); @@ -121,8 +121,7 @@ namespace sgns void ProcessCertificates(); outcome::result GetCertificateBySubjectHash( const std::string &subject_hash ) const; - bool CheckCertificateForSubject( const std::string &subject_hash ) const; - + bool CheckCertificateForSubject( const std::string &subject_hash ) const; protected: void ConfigureTimestampWindow( std::chrono::milliseconds window ); @@ -143,7 +142,7 @@ namespace sgns static constexpr std::chrono::milliseconds DEFAULT_TIMESTAMP_WINDOW = std::chrono::minutes( 5 ); static constexpr std::chrono::milliseconds DEFAULT_ROUND_DURATION = std::chrono::milliseconds( 500 ); static constexpr std::chrono::milliseconds DEFAULT_ROUND_SKEW = std::chrono::milliseconds( 250 ); - static constexpr uint64_t NO_ROUND = std::numeric_limits::max(); + static constexpr uint64_t NO_ROUND = std::numeric_limits::max(); struct ProposalState { @@ -177,24 +176,25 @@ namespace sgns 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 ClearProposalState( const Proposal &proposal ); - outcome::result GetSubjectHash( const Subject &subject ) const; - void ContinueProposalAfterSubject( const Proposal &proposal ); - void AddPendingProposal( const Proposal &proposal, const std::string &subject_hash ); - std::vector TakePendingProposals( const std::string &subject_hash ); - bool RegisterCertificateFilter(); + std::vector CollectCertificateVotes( const Certificate &certificate ) const; + void ClearProposalState( 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 ); + 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 ); + 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_; From 47f4e8a5a5aa4d1c96c0277d6128198eac1c1b82 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 9 Mar 2026 18:24:28 -0300 Subject: [PATCH 065/114] Chore: Inserting delay to generate a certificate to test insertion of new voters --- src/blockchain/Consensus.cpp | 65 +++++++++++++++++++++++++++--------- src/blockchain/Consensus.hpp | 3 ++ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 3d0628ae1..5215bf74f 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -284,6 +284,17 @@ namespace sgns 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 ) @@ -1288,6 +1299,19 @@ namespace sgns __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 ) { @@ -1618,13 +1642,8 @@ namespace sgns } const auto *validator = registry_->FindValidator( registry, vote.voter_id() ); - if ( !validator || validator->status() != ValidatorRegistry::Status::ACTIVE ) - { - ConsensusManagerLogger()->error( "{}: rejected: validator not active voter_id={}", - __func__, - vote.voter_id() ); - return; - } + const bool is_active_validator = + validator && validator->status() == ValidatorRegistry::Status::ACTIVE; if ( it->second.total_weight == 0 ) { @@ -1633,15 +1652,31 @@ namespace sgns it->second.votes.push_back( vote ); it->second.seen_voters.insert( vote.voter_id() ); - it->second.approved_weight += validator->weight(); - has_quorum = registry_->IsQuorum( it->second.approved_weight, it->second.total_weight ); - if ( has_quorum ) + if ( is_active_validator ) { - it->second.quorum_reached = true; - ConsensusManagerLogger()->debug( - "{}: quorum reached; certificate will be created by timer proposal_id={}", - __func__, - vote.proposal_id() ); + 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() ); } state = it->second; } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 3d130dea3..2d97dcbf4 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -119,6 +119,7 @@ namespace sgns 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; @@ -153,6 +154,7 @@ namespace sgns 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; }; @@ -214,6 +216,7 @@ namespace sgns 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( 0 ) }; std::chrono::milliseconds round_duration_{ DEFAULT_ROUND_DURATION }; std::chrono::milliseconds round_skew_{ DEFAULT_ROUND_SKEW }; std::atomic stop_timer_{ false }; From 267ace835e7196cf4c991c0c0f9866a6809882c9 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Mar 2026 09:11:20 -0300 Subject: [PATCH 066/114] Feat: Adding previous hash and using uncle hash as the other chain hash --- src/account/TransactionManager.cpp | 79 ++++++++++++++++++++++++++---- src/account/TransactionManager.hpp | 9 +++- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 1ddd84bda..5cc11f0b7 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -497,6 +497,7 @@ namespace sgns TransferTransaction::New( inputs, outputs, FillDAGStruct() ) ); transfer_transaction->MakeSignature( *account_m ); + RecordOutgoingTxHash( transfer_transaction->GetNonce(), transfer_transaction->GetHash() ); utxo_manager_.ReserveUTXOs( inputs ); @@ -521,6 +522,7 @@ namespace sgns FillDAGStruct( std::move( transaction_hash ) ) ) ); mint_transaction->MakeSignature( *account_m ); + RecordOutgoingTxHash( mint_transaction->GetNonce(), mint_transaction->GetHash() ); // Store the transaction ID before moving the transaction auto txId = mint_transaction->GetHash(); @@ -552,6 +554,7 @@ namespace sgns EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct() ) ); escrow_transaction->MakeSignature( *account_m ); + RecordOutgoingTxHash( escrow_transaction->GetNonce(), escrow_transaction->GetHash() ); // Get the transaction ID for tracking auto txId = escrow_transaction->GetHash(); @@ -636,6 +639,9 @@ namespace sgns auto transfer_transaction = std::make_shared( TransferTransaction::New( std::vector{ escrow_utxo_input }, payout_peers, FillDAGStruct() ) ); + transfer_transaction->MakeSignature( *account_m ); + RecordOutgoingTxHash( transfer_transaction->GetNonce(), transfer_transaction->GetHash() ); + auto escrow_release_tx = std::make_shared( EscrowReleaseTransaction::New( escrow_tx->GetUTXOParameters(), escrow_tx->GetAmount(), @@ -644,10 +650,10 @@ namespace sgns escrow_tx->GetHash(), FillDAGStruct() ) ); - TransactionBatch tx_batch; - - transfer_transaction->MakeSignature( *account_m ); escrow_release_tx->MakeSignature( *account_m ); + RecordOutgoingTxHash( escrow_release_tx->GetNonce(), escrow_release_tx->GetHash() ); + + 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 ) ); @@ -684,22 +690,73 @@ 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; } + std::string TransactionManager::GetOutgoingPreviousHash( uint64_t nonce ) const + { + if ( nonce == 0 ) + { + return ""; + } + std::lock_guard lock( outgoing_tx_hash_mutex_ ); + auto it = outgoing_tx_hash_by_nonce_.find( nonce - 1 ); + if ( it == outgoing_tx_hash_by_nonce_.end() ) + { + return ""; + } + return it->second; + } + + void TransactionManager::RecordOutgoingTxHash( uint64_t nonce, const std::string &hash ) + { + std::lock_guard lock( outgoing_tx_hash_mutex_ ); + outgoing_tx_hash_by_nonce_[nonce] = hash; + + if ( nonce_window_m == 0 ) + { + return; + } + const uint64_t min_nonce = ( nonce > ( nonce_window_m + 1 ) ) ? ( nonce - ( nonce_window_m + 1 ) ) : 0; + for ( auto it = outgoing_tx_hash_by_nonce_.begin(); it != outgoing_tx_hash_by_nonce_.end(); ) + { + if ( it->first < min_nonce ) + { + it = outgoing_tx_hash_by_nonce_.erase( it ); + } + else + { + ++it; + } + } + } + + void TransactionManager::RemoveOutgoingTxHash( uint64_t nonce ) + { + std::lock_guard lock( outgoing_tx_hash_mutex_ ); + outgoing_tx_hash_by_nonce_.erase( nonce ); + } + outcome::result TransactionManager::SendTransactionItem( TransactionItem &item ) { auto [transaction_batch, maybe_crdt_transaction] = item; @@ -3538,6 +3595,10 @@ namespace sgns } tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::FAILED, tx->GetNonce() }; account_m->ReleaseNonce( tx->GetNonce() ); + if ( tx->GetSrcAddress() == account_m->GetAddress() ) + { + RemoveOutgoingTxHash( tx->GetNonce() ); + } TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of FAILED to transaction {}", account_m->GetAddress().substr( 0, 8 ), diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 10ce11a5d..0459f2785 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -223,7 +223,10 @@ namespace sgns using TransactionParserFn = outcome::result ( TransactionManager::* )( const std::shared_ptr & ); - SGTransaction::DAGStruct FillDAGStruct( std::string transaction_hash = "" ) const; + SGTransaction::DAGStruct FillDAGStruct( std::optional other_chain_hash = std::nullopt ); + std::string GetOutgoingPreviousHash( uint64_t nonce ) const; + void RecordOutgoingTxHash( uint64_t nonce, const std::string &hash ); + void RemoveOutgoingTxHash( uint64_t nonce ); outcome::result SendTransactionItem( TransactionItem &item ); outcome::result RollbackTransactions( TransactionItem &item_to_rollback ); @@ -300,6 +303,8 @@ namespace sgns std::chrono::milliseconds timestamp_tolerance_m; std::chrono::milliseconds mutability_window_m; uint64_t nonce_window_m = DEFAULT_NONCE_WINDOW; + mutable std::mutex outgoing_tx_hash_mutex_; + std::unordered_map outgoing_tx_hash_by_nonce_; static constexpr std::chrono::milliseconds TIMESTAMP_TOLERANCE = std::chrono::seconds( 10 ); static constexpr std::chrono::milliseconds MUTABILITY_WINDOW = std::chrono::minutes( 15 ); @@ -366,7 +371,7 @@ namespace sgns void ChangeState( State new_state ); public: - outcome::result GetTransactionCID( const std::string &tx_hash ) const; + outcome::result GetTransactionCID( const std::string &tx_hash ) const; outcome::result HandleNonceConsensusSubject( const ConsensusManager::Subject &subject ); bool ValidateTransactionForConsensus( const std::shared_ptr &tx ) const; From 318dfec0ada72b3f7b5e54320a345f6efdf63455 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Mar 2026 10:19:28 -0300 Subject: [PATCH 067/114] Feat: Creating methods to expose previous and uncle hashes --- src/account/IGeniusTransactions.cpp | 10 ++++++++++ src/account/IGeniusTransactions.hpp | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/account/IGeniusTransactions.cpp b/src/account/IGeniusTransactions.cpp index 5ab47b6ee..40fadb23e 100644 --- a/src/account/IGeniusTransactions.cpp +++ b/src/account/IGeniusTransactions.cpp @@ -99,6 +99,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 87662cfb1..22a86cbc5 100644 --- a/src/account/IGeniusTransactions.hpp +++ b/src/account/IGeniusTransactions.hpp @@ -85,6 +85,8 @@ namespace sgns } [[nodiscard]] std::string GetHash() const; + [[nodiscard]] std::string GetPreviousHash() const; + [[nodiscard]] std::string GetUncleHash() const; uint64_t GetTimestamp() const { From c261380e4496670fcfae31e420a6d994ab6c672a Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Mar 2026 12:08:24 -0300 Subject: [PATCH 068/114] Feat: Creating method to verify the entire subject. Fixed votes received before proposal --- src/blockchain/Blockchain.hpp | 18 ++-- src/blockchain/Consensus.cpp | 158 +++++++++++++++++++++++------ src/blockchain/Consensus.hpp | 8 +- src/blockchain/impl/Blockchain.cpp | 18 +++- 4 files changed, 158 insertions(+), 44 deletions(-) diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index 4fdb18121..59818824b 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -111,6 +111,9 @@ namespace sgns 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 ); outcome::result CreateConsensusProposal( const std::string &account_id, uint64_t nonce, @@ -119,9 +122,9 @@ namespace sgns outcome::result SubmitProposal( const ConsensusManager::Proposal &proposal ); outcome::result TryResumeProposal( const std::string &hash ); - - bool CheckCertificate( const std::string &subject_hash ) const; - const std::string &BestHash( const std::string &a, const std::string &b ) const; + bool CheckCertificate( const std::string &subject_hash ) const; + bool CheckCertificateStrict( const ConsensusManager::Subject &subject ) const; + const std::string &BestHash( const std::string &a, const std::string &b ) const; protected: friend class Migration3_5_1To3_6_0; @@ -158,7 +161,8 @@ namespace sgns const AccountCreationBlock &candidate ) const; void GenesisReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair &new_data, const std::string &cid ); - void AccountCreationReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair& new_data, const std::string &cid ); + void AccountCreationReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair &new_data, + const std::string &cid ); outcome::result InformBlockchainResult( outcome::result result ) const; void InformGenesisResult( outcome::result result ); void InformAccountCreationResponse( outcome::result creation_result ); @@ -180,9 +184,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 - GenesisBlock genesis_block_; ///< Cached genesis block for easy access - 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 { diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 5215bf74f..095e33889 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -481,6 +481,12 @@ namespace sgns } } + 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_ ); @@ -549,6 +555,26 @@ namespace sgns 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, @@ -563,10 +589,12 @@ namespace sgns uint64_t registry_epoch, Signer sign ) { - ConsensusManagerLogger()->trace( "{}: called by {} with hash {}", + ConsensusManagerLogger()->trace( "{}: called by {} with hash {}, registry CID {} and epoch {}", __func__, proposer_id.substr( 0, 8 ), - GetPrintableSubjectHash( subject ) ); + GetPrintableSubjectHash( subject ), + registry_cid, + registry_epoch ); if ( !sign ) { ConsensusManagerLogger()->error( "{}: failed for hash {}: signer is empty", @@ -723,11 +751,14 @@ namespace sgns outcome::result ConsensusManager::CreateCertificate( const Proposal &proposal, const std::vector &votes ) { - ConsensusManagerLogger()->trace( "{}: Creating certificate for hash {}: proposal_id={} votes={}", - __func__, - GetPrintableSubjectHash( proposal.subject() ), - proposal.proposal_id().substr( 0, 8 ), - votes.size() ); + 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() ) { @@ -1174,6 +1205,16 @@ namespace sgns 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 ) ); + } + } auto subject_hash = GetSubjectHash( proposal.subject() ); if ( subject_hash.has_error() ) { @@ -1559,10 +1600,10 @@ namespace sgns void ConsensusManager::HandleVote( const Vote &vote ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={} voter_id={}", + ConsensusManagerLogger()->trace( "{}: called. Vote by {} on proposal_id={} ", __func__, - vote.proposal_id(), - vote.voter_id() ); + vote.voter_id().substr( 0, 8 ), + vote.proposal_id().substr( 0, 8 ) ); if ( !CheckVote( vote ) ) { ConsensusManagerLogger()->error( "{}: rejected: Invalid vote proposal_id={} voter_id={}", @@ -1573,7 +1614,9 @@ namespace sgns } if ( !vote.approve() ) { - ConsensusManagerLogger()->debug( "{}: ignored: vote not approved voter_id={}", __func__, vote.voter_id() ); + ConsensusManagerLogger()->debug( "{}: ignored: vote not approved voter_id={}", + __func__, + vote.voter_id().substr( 0, 8 ) ); //TODO - maybe see reputation? return; } @@ -1590,7 +1633,7 @@ namespace sgns { ConsensusManagerLogger()->error( "{}: rejected: signature verification failed voter_id={}", __func__, - vote.voter_id() ); + vote.voter_id().substr( 0, 8 ) ); return; } @@ -1611,9 +1654,10 @@ namespace sgns auto it = proposals_.find( vote.proposal_id() ); if ( it == proposals_.end() ) { - ConsensusManagerLogger()->error( "{}: ignored: proposal not found proposal_id={}", + pending_votes_[vote.proposal_id()].push_back( vote ); + ConsensusManagerLogger()->debug( "{}: queued pending vote proposal_id={}", __func__, - vote.proposal_id() ); + vote.proposal_id().substr( 0, 8 ) ); return; } auto &proposal_state = it->second; @@ -1622,13 +1666,15 @@ namespace sgns { ConsensusManagerLogger()->error( "{}: ignored: not best proposal proposal_id={}", __func__, - vote.proposal_id() ); + 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() ); + ConsensusManagerLogger()->trace( "{}: ignored: duplicate vote voter_id={}", + __func__, + vote.voter_id().substr( 0, 8 ) ); return; } @@ -1637,13 +1683,12 @@ namespace sgns { ConsensusManagerLogger()->error( "{}: rejected: registry mismatch proposal_id={}", __func__, - vote.proposal_id() ); + vote.proposal_id().substr( 0, 8 ) ); return; } - const auto *validator = registry_->FindValidator( registry, vote.voter_id() ); - const bool is_active_validator = - validator && validator->status() == ValidatorRegistry::Status::ACTIVE; + const auto *validator = registry_->FindValidator( registry, vote.voter_id() ); + const bool is_active_validator = validator && validator->status() == ValidatorRegistry::Status::ACTIVE; if ( it->second.total_weight == 0 ) { @@ -1660,11 +1705,10 @@ namespace sgns { 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(); + 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={}", @@ -1676,7 +1720,7 @@ namespace sgns { ConsensusManagerLogger()->debug( "{}: accepted vote from non-validator voter_id={}", __func__, - vote.voter_id() ); + vote.voter_id().substr( 0, 8 ) ); } state = it->second; } @@ -1691,12 +1735,12 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: called proposal_id={} votes={}", __func__, - bundle.proposal_id(), + bundle.proposal_id().substr( 0, 8 ), bundle.votes_size() ); for ( const auto &vote : bundle.votes() ) { - ConsensusManagerLogger()->trace( "{}: processing voter_id={}", __func__, vote.voter_id() ); + ConsensusManagerLogger()->trace( "{}: processing voter_id={}", __func__, vote.voter_id().substr( 0, 8 ) ); HandleVote( vote ); } } @@ -1962,14 +2006,14 @@ namespace sgns std::string ConsensusManager::CreateProposalId( const Proposal &proposal ) { - ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, proposal.proposal_id() ); + 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: signing bytes error={}", + ConsensusManagerLogger()->error( "{}: failed, no proposal ID created: signing bytes error={}", __func__, signing_bytes.error().message() ); return {}; @@ -1978,8 +2022,9 @@ namespace sgns sgns::crypto::HasherImpl hasher; auto hash = hasher.sha2_256( gsl::span( signing_bytes.value().data(), signing_bytes.value().size() ) ); - ConsensusManagerLogger()->debug( "{}: success", __func__ ); - return base::hex_lower( gsl::span( hash.data(), hash.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 ) @@ -2209,6 +2254,55 @@ namespace sgns 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 ); diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 2d97dcbf4..37498f18a 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -123,6 +123,7 @@ namespace sgns 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 ); @@ -153,9 +154,9 @@ namespace sgns uint64_t total_weight = 0; uint64_t approved_weight = 0; std::unordered_set seen_voters; - bool quorum_reached = false; + bool quorum_reached = false; uint64_t quorum_reached_ts_ms = 0; - uint64_t last_attempt_round = NO_ROUND; + uint64_t last_attempt_round = NO_ROUND; }; struct SlotState @@ -184,6 +185,8 @@ namespace sgns 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 ); @@ -209,6 +212,7 @@ namespace sgns 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_; diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index f6f7c3cd7..70784987e 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -224,8 +224,8 @@ namespace sgns } ); instance->account_->SetGetValidatorWeightMethod( - [weak_ptr( std::weak_ptr( - instance ) )]( const std::string &address ) -> outcome::result> + [weak_ptr( std::weak_ptr( instance ) )]( + const std::string &address ) -> outcome::result> { if ( auto strong = weak_ptr.lock() ) { @@ -1555,11 +1555,18 @@ namespace sgns consensus_manager_->UnregisterCertificateHandler( type ); } + outcome::result Blockchain::CreateConsensusNonceSubject( const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash ) + { + return consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash ); + } + outcome::result Blockchain::CreateConsensusProposal( const std::string &account_id, uint64_t nonce, const std::string &tx_hash ) { - OUTCOME_TRY( auto &&nonce_subject, consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash ) ); + OUTCOME_TRY( auto &&nonce_subject, CreateConsensusNonceSubject( account_id, nonce, tx_hash ) ); OUTCOME_TRY( auto &&nonce_proposal, consensus_manager_->CreateProposal( nonce_subject, account_id, @@ -1584,6 +1591,11 @@ namespace sgns return consensus_manager_->CheckCertificateForSubject( subject_hash ); } + bool Blockchain::CheckCertificateStrict( const ConsensusManager::Subject &subject ) const + { + return consensus_manager_->CheckCertificateForSubject( subject ); + } + const std::string &Blockchain::BestHash( const std::string &a, const std::string &b ) const { return consensus_manager_->BestHash( a, b ); From 7c7fb61f5440d32ecd2f976c3819a29b0846e752 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Mar 2026 12:11:47 -0300 Subject: [PATCH 069/114] Fix: Insertind the checking of previous certificate on replay protection --- src/account/TransactionManager.cpp | 31 ++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 5cc11f0b7..45a3605e9 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -3226,19 +3226,30 @@ namespace sgns { if ( tx.GetNonce() == 0 ) { - TransactionManagerLogger()->debug( "[{} - full: {}] {}: No peer nonce required for tx with nonce=0", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + 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()->error( "[{} - full: {}] {}: Missing peer nonce for address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx.GetSrcAddress() ); + 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() ); + auto previous_hash_subject_result = blockchain_->CreateConsensusNonceSubject( tx.GetSrcAddress(), + tx.GetNonce() - 1, + tx.GetPreviousHash() ); + if ( previous_hash_subject_result.has_error() ) + { + return false; + } - return false; + return blockchain_->CheckCertificateStrict( previous_hash_subject_result.value() ); } const auto confirmed_nonce = nonce_result.value(); From f17cbc5744828d1e00b1e4e86261a10792959375 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Mar 2026 14:39:45 -0300 Subject: [PATCH 070/114] Fix: Non full node should be able to verify UTXOs --- src/account/TransactionManager.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 45a3605e9..ce43aed26 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -3000,16 +3000,6 @@ namespace sgns return false; } - if ( !full_node_m && address != account_m->GetAddress() ) - { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Non-full node cannot verify address {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - address ); - return false; - } - if ( !utxo_manager_.VerifyParameters( params, address ) ) { TransactionManagerLogger()->error( "[{} - full: {}] {}: VerifyParameters failed for address {}", From 92434b57e4c736705ab32ee9907b3d8e17883dc3 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Mar 2026 14:42:01 -0300 Subject: [PATCH 071/114] Chore: Inserting better debugging when adding new validators --- src/blockchain/ValidatorRegistry.cpp | 50 ++++- test/src/multiaccount/multi_account_sync.cpp | 211 ++++++++++++++++++- 2 files changed, 253 insertions(+), 8 deletions(-) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 31b44862a..f9aabc76c 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -441,12 +441,11 @@ namespace sgns outcome::result ValidatorRegistry::LoadRegistry() const { - logger_->trace( "{}: entry", __func__ ); + { std::shared_lock lock( cache_mutex_ ); if ( cached_registry_ ) { - logger_->trace( "{}: returning cached registry", __func__ ); return cached_registry_.value(); } } @@ -1071,10 +1070,50 @@ namespace sgns 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() ) ); @@ -1108,10 +1147,11 @@ namespace sgns *next.add_validators() = entry; } - logger_->debug( "{}: built registry from certificate proposal_id={} epoch={}", + logger_->debug( "{}: built registry from certificate proposal_id={} epoch={} validators={}", __func__, - certificate.proposal_id(), - next.epoch() ); + certificate.proposal_id().substr( 0, 8 ), + next.epoch(), + next.validators_size() ); return next; } diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 00c3a9df8..f397eb563 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -23,11 +23,16 @@ #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 { @@ -135,7 +140,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", @@ -221,7 +226,7 @@ TEST_F( MultiAccountTest, SyncThroughEachOther ) 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 @@ -382,7 +387,6 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) 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() ); @@ -415,3 +419,204 @@ TEST_F( MultiAccountTest, CRDTFilterDuplicateTx ) std::cout << "CRDT Filter test completed successfully!" << std::endl; } + +TEST_F( MultiAccountTest, NodeConsensusTest ) +{ + auto node_full = CreateNode( "node_consensus_full", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + true, // is full node + true, // is processor + true ); // is genesis authorized + + test::assertWaitForCondition( + [&]() { return node_full->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_full not synced" ); + + auto node_client = CreateNode( "node_consensus_client", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, // not full node + false // not processor + ); + + auto node_peer1 = CreateNode( "node_consensus_peer1", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + auto node_peer2 = CreateNode( "node_consensus_peer2", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + auto node_peer3 = CreateNode( "node_consensus_peer3", + "0xcafe", + "1.0", + TokenID::FromBytes( { 0x00 } ), + false, + false ); + + node_client->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer1->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer2->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + node_peer3->GetPubSub()->AddPeers( { node_full->GetPubSub()->GetInterfaceAddress() } ); + test::assertWaitForCondition( + [&]() { return node_client->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_client not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer1->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer1 not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer2->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer2 not synced" ); + test::assertWaitForCondition( + [&]() { return node_peer3->GetTransactionManagerState() == TransactionManager::State::READY; }, + std::chrono::milliseconds( 30000 ), + "node_peer3 not synced" ); + + 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 ); + + auto *full_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_full->GetAddress() ); + auto *peer1_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_peer1->GetAddress() ); + auto *peer2_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_peer2->GetAddress() ); + auto *peer3_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_peer3->GetAddress() ); + + ASSERT_TRUE( full_validator ); + ASSERT_TRUE( peer1_validator ); + ASSERT_TRUE( peer2_validator ); + ASSERT_TRUE( peer3_validator ); + EXPECT_GT( full_validator->weight(), 0 ); + EXPECT_GT( peer1_validator->weight(), 0 ); + EXPECT_GT( peer2_validator->weight(), 0 ); + EXPECT_GT( peer3_validator->weight(), 0 ); + }; + + auto mint1 = node_client->MintTokens( 100, + "", + "", + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; + fmt::println( "Mint 1 succeeded" ); + + auto mint2 = node_client->MintTokens( 250, + "", + "", + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + ASSERT_TRUE( mint2.has_value() ) << "Mint 2 failed on node_client"; + fmt::println( "Mint 2 succeeded" ); + + + 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" ); + + + 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" ); + + + 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" ); + + + auto wait_confirmed = [&]( const std::string &tx_id, const char *label ) + { + test::assertWaitForCondition( + [&]() + { return node_client->GetTransactionStatus( tx_id ) == TransactionManager::TransactionStatus::CONFIRMED; }, + std::chrono::milliseconds( INCOMING_TIMEOUT_MILLISECONDS ), + label ); + }; + + auto registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + auto epoch_before = registry_state.value().epoch(); + auto cid_before = registry->GetRegistryCid(); + wait_confirmed( mint1.value().first, "mint1 not confirmed" ); + assert_registry_updated( epoch_before, cid_before ); + + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + wait_confirmed( mint2.value().first, "mint2 not confirmed" ); + assert_registry_updated( epoch_before, cid_before ); + + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + wait_confirmed( transfer1.value().first, "transfer1 not confirmed" ); + assert_registry_updated( epoch_before, cid_before ); + + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + wait_confirmed( transfer2.value().first, "transfer2 not confirmed" ); + assert_registry_updated( epoch_before, cid_before ); + + registry_state = registry->LoadRegistry(); + ASSERT_TRUE( registry_state.has_value() ); + epoch_before = registry_state.value().epoch(); + cid_before = registry->GetRegistryCid(); + wait_confirmed( transfer3.value().first, "transfer3 not confirmed" ); + assert_registry_updated( epoch_before, cid_before ); +} From e815bd6c54858ed9edac0967a8f4e10dfe3b6241 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 12 Mar 2026 17:08:57 -0300 Subject: [PATCH 072/114] Feat: Creating merkle root of UTXOs --- src/account/UTXOManager.cpp | 141 +++++++++++++++++++++++++ src/account/UTXOManager.hpp | 10 ++ test/src/account/utxo_manager_test.cpp | 43 ++++++++ 3 files changed, 194 insertions(+) diff --git a/src/account/UTXOManager.cpp b/src/account/UTXOManager.cpp index 41fdd8725..1b2386ff8 100644 --- a/src/account/UTXOManager.cpp +++ b/src/account/UTXOManager.cpp @@ -1,14 +1,67 @@ #include "UTXOManager.hpp" +#include #include #include #include "account/proto/SGTransaction.pb.h" #include "base/blob.hpp" +#include "crypto/sha/sha256.hpp" #include "storage/database_error.hpp" namespace sgns { + namespace + { + constexpr uint8_t kLeafPrefix = 0x00; + constexpr uint8_t kNodePrefix = 0x01; + + 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 ) ); + } + + 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 ) ); + } + + base::Hash256 EmptyUTXOMerkleRoot() + { + static const base::Hash256 empty_root = crypto::sha256( std::string_view( "UTXO_EMPTY_V1" ) ); + return empty_root; + } + + base::Hash256 HashLeaf( const std::vector &payload ) + { + std::vector leaf_bytes; + leaf_bytes.reserve( payload.size() + 1 ); + leaf_bytes.push_back( kLeafPrefix ); + leaf_bytes.insert( leaf_bytes.end(), payload.begin(), payload.end() ); + return crypto::sha256( gsl::span( leaf_bytes.data(), leaf_bytes.size() ) ); + } + + base::Hash256 HashNode( const base::Hash256 &left, const base::Hash256 &right ) + { + std::vector node_bytes; + node_bytes.reserve( 1 + left.size() + right.size() ); + node_bytes.push_back( kNodePrefix ); + node_bytes.insert( node_bytes.end(), left.begin(), left.end() ); + node_bytes.insert( node_bytes.end(), right.begin(), right.end() ); + return crypto::sha256( gsl::span( node_bytes.data(), node_bytes.size() ) ); + } + } // namespace + uint64_t UTXOManager::GetBalance() const { return GetBalance( address_ ); @@ -370,6 +423,94 @@ namespace sgns return real_amount == expected_amount && input_amount == 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 EmptyUTXOMerkleRoot(); + } + + std::vector unspent; + { + std::shared_lock lock( utxos_mutex_ ); + auto it = utxos_.find( address ); + if ( it == utxos_.end() ) + { + return EmptyUTXOMerkleRoot(); + } + + unspent.reserve( it->second.size() ); + for ( const auto &[state, utxo] : it->second ) + { + if ( state != UTXOState::UTXO_READY ) + { + continue; + } + unspent.push_back( utxo ); + } + } + + if ( unspent.empty() ) + { + return EmptyUTXOMerkleRoot(); + } + + std::sort( unspent.begin(), + unspent.end(), + []( const GeniusUTXO &lhs, const GeniusUTXO &rhs ) + { + if ( lhs.GetTxID() != rhs.GetTxID() ) + { + return lhs.GetTxID() < rhs.GetTxID(); + } + return lhs.GetOutputIdx() < rhs.GetOutputIdx(); + } ); + + std::vector level_hashes; + level_hashes.reserve( unspent.size() ); + + for ( const auto &utxo : unspent ) + { + std::vector payload; + payload.reserve( 32 + 4 + 4 + address.size() + utxo.GetTokenID().bytes().size() + 8 ); + + payload.insert( payload.end(), utxo.GetTxID().begin(), utxo.GetTxID().end() ); + AppendUInt32BE( payload, utxo.GetOutputIdx() ); + AppendUInt32BE( payload, static_cast( address.size() ) ); + payload.insert( payload.end(), address.begin(), address.end() ); + + const auto &token_bytes = utxo.GetTokenID().bytes(); + payload.insert( payload.end(), token_bytes.begin(), token_bytes.end() ); + AppendUInt64BE( payload, utxo.GetAmount() ); + + level_hashes.push_back( HashLeaf( payload ) ); + } + + 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(); + } + outcome::result UTXOManager::LoadUTXOs( std::shared_ptr db ) { if ( db == nullptr ) diff --git a/src/account/UTXOManager.hpp b/src/account/UTXOManager.hpp index 39529d697..38f28a5e5 100644 --- a/src/account/UTXOManager.hpp +++ b/src/account/UTXOManager.hpp @@ -132,6 +132,16 @@ 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; + outcome::result LoadUTXOs( std::shared_ptr db ); /** diff --git a/test/src/account/utxo_manager_test.cpp b/test/src/account/utxo_manager_test.cpp index ab3d17100..07b8a3e4f 100644 --- a/test/src/account/utxo_manager_test.cpp +++ b/test/src/account/utxo_manager_test.cpp @@ -170,6 +170,49 @@ TEST_F( UTXOManagerTest, Storage ) EXPECT_EQ( utxos.size(), 2 ); } +TEST_F( UTXOManagerTest, MerkleRootDeterministicAcrossInsertionOrder ) +{ + const auto hash_a = HASHER.sha2_256( { 0xA1 } ); + const auto hash_b = HASHER.sha2_256( { 0xB2 } ); + + 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 auto hash_a = HASHER.sha2_256( { 0xC3 } ); + const auto hash_b = HASHER.sha2_256( { 0xD4 } ); + + 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( GeniusUTXO, PropertyAccessors ) { uint32_t idx = 5; From bdbd9cc514ac0c1528e3f9ff94f430921eb171bf Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 13 Mar 2026 10:25:14 -0300 Subject: [PATCH 073/114] WIP: Updating the UTXOs to make sure consensus doesn't have security flaws --- src/account/GeniusUTXO.hpp | 66 ++- src/account/TransactionManager.cpp | 9 +- src/account/UTXOManager.cpp | 550 ++++++++++++++++--------- src/account/UTXOManager.hpp | 34 +- src/account/proto/SGTransaction.proto | 20 +- test/src/account/utxo_manager_test.cpp | 16 +- 6 files changed, 458 insertions(+), 237 deletions(-) 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/TransactionManager.cpp b/src/account/TransactionManager.cpp index ce43aed26..422caec4a 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -1285,10 +1285,11 @@ 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 ) { + const auto &dest_info = dest_infos[i]; auto hash = ( base::Hash256::fromReadableString( transfer_tx->GetHash() ) ).value(); - utxo_manager_.DeleteUTXO( hash, dest_info.dest_address ); + utxo_manager_.DeleteUTXO( hash, i, dest_info.dest_address ); TransactionManagerLogger()->debug( "[{} - full: {}] Notify {} of deletion of {} to it", account_m->GetAddress().substr( 0, 8 ), @@ -1331,7 +1332,7 @@ namespace sgns auto mint_tx = std::dynamic_pointer_cast( tx ); auto hash = ( base::Hash256::fromReadableString( mint_tx->GetHash() ) ).value(); - utxo_manager_.DeleteUTXO( hash, mint_tx->GetSrcAddress() ); + 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, @@ -1354,7 +1355,7 @@ namespace sgns auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); if ( outputs.size() > 1 ) { - utxo_manager_.DeleteUTXO( hash, outputs[1].dest_address ); + utxo_manager_.DeleteUTXO( hash, 1, outputs[1].dest_address ); } for ( auto &input : inputs ) { diff --git a/src/account/UTXOManager.cpp b/src/account/UTXOManager.cpp index 1b2386ff8..67b330982 100644 --- a/src/account/UTXOManager.cpp +++ b/src/account/UTXOManager.cpp @@ -60,6 +60,46 @@ namespace sgns node_bytes.insert( node_bytes.end(), right.begin(), right.end() ); return crypto::sha256( gsl::span( node_bytes.data(), node_bytes.size() ) ); } + + 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::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; + } } // namespace uint64_t UTXOManager::GetBalance() const @@ -79,13 +119,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() ) + { + continue; + } + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + if ( reserved_outpoints_.find( outpoint ) == reserved_outpoints_.end() ) { - retval += curr.GetAmount(); + //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(); } } } @@ -110,19 +160,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 bool UTXOManager::PutUTXO( GeniusUTXO new_utxo, const std::string &address ) { // If not a full node and trying to store UTXOs for other addresses, reject @@ -132,42 +196,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 ) ); - StoreUTXOs( address ); + return false; } - return is_new; + + utxo_outpoints_[outpoint] = UTXOEntry{ UTXOState::UTXO_READY, new_utxo }; + address_outpoints_[address].push_back( outpoint ); + + StoreUTXOs( address ); + return true; } - void UTXOManager::DeleteUTXO( const base::Hash256 &utxo_id, const std::string &address ) + void 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_ ) @@ -176,23 +221,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 ); StoreUTXOs( address ); } } @@ -202,35 +244,31 @@ 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() ) - { - continue; - } - if ( input_info.output_idx_ != curr.GetOutputIdx() ) + auto &entry = canonical_it->second; + if ( entry.state == UTXOState::UTXO_READY && entry.utxo.GetOwnerAddress() == address ) { - continue; + utxo_found = true; + entry.state = UTXOState::UTXO_CONSUMED; + entry.utxo.SetOwnerAddress( address ); } - utxo_found = true; - utxo_it = it; - break; } - if ( utxo_found ) - { - utxo_list.erase( utxo_it ); - } - else + + reserved_outpoints_.erase( outpoint ); + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { - GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID() ); - utxo_list.emplace_back( UTXOState::UTXO_CONSUMED, consumed_utxo ); + RemoveOutPointFromVector( address_it->second, outpoint ); } + + 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; } @@ -242,17 +280,22 @@ namespace sgns std::vector UTXOManager::GetUTXOs( const std::string &address ) const { std::shared_lock lock( utxos_mutex_ ); - if ( auto it = utxos_.find( address ); it != utxos_.end() ) + if ( auto address_it = address_outpoints_.find( address ); address_it != address_outpoints_.end() ) { std::vector result; - result.reserve( it->second.size() ); - for ( const auto &[state, utxo] : it->second ) + result.reserve( address_it->second.size() ); + for ( const auto &outpoint : address_it->second ) { - if ( state == UTXOState::UTXO_CONSUMED ) + auto utxo_it = utxo_outpoints_.find( outpoint ); + if ( utxo_it == utxo_outpoints_.end() ) { continue; } - result.push_back( utxo ); + if ( utxo_it->second.state != UTXOState::UTXO_READY ) + { + continue; + } + result.push_back( utxo_it->second.utxo ); } return result; } @@ -262,7 +305,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 ) @@ -275,12 +325,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(); + 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() ) @@ -348,15 +413,9 @@ namespace sgns { std::unique_lock lock( utxos_mutex_ ); - for ( auto &[state, utxo] : utxos_[address_] ) + for ( const auto &input_utxo : inputs ) { - for ( auto &input_utxo : inputs ) - { - if ( input_utxo.txid_hash_ == utxo.GetTxID() ) - { - utxo.SetLocked( true ); - } - } + reserved_outpoints_.insert( OutPoint{ input_utxo.txid_hash_, input_utxo.output_idx_ } ); } } @@ -364,55 +423,63 @@ namespace sgns { std::unique_lock lock( utxos_mutex_ ); - for ( auto &[state, utxo] : utxos_[address_] ) + for ( const auto &input_utxo : inputs ) { - for ( auto &input_utxo : inputs ) - { - if ( input_utxo.txid_hash_ == utxo.GetTxID() ) - { - utxo.SetLocked( false ); - } - } + reserved_outpoints_.erase( OutPoint{ input_utxo.txid_hash_, input_utxo.output_idx_ } ); } } 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(), @@ -420,7 +487,7 @@ 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 @@ -439,20 +506,25 @@ namespace sgns std::vector unspent; { std::shared_lock lock( utxos_mutex_ ); - auto it = utxos_.find( address ); - if ( it == utxos_.end() ) + auto it = address_outpoints_.find( address ); + if ( it == address_outpoints_.end() ) { return EmptyUTXOMerkleRoot(); } unspent.reserve( it->second.size() ); - for ( const auto &[state, utxo] : it->second ) + for ( const auto &outpoint : it->second ) { - if ( 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; } - unspent.push_back( utxo ); + unspent.push_back( utxo_it->second.utxo ); } } @@ -478,12 +550,13 @@ namespace sgns for ( const auto &utxo : unspent ) { std::vector payload; - payload.reserve( 32 + 4 + 4 + address.size() + utxo.GetTokenID().bytes().size() + 8 ); + const auto &owner_address = utxo.GetOwnerAddress(); + payload.reserve( 32 + 4 + 4 + owner_address.size() + utxo.GetTokenID().bytes().size() + 8 ); payload.insert( payload.end(), utxo.GetTxID().begin(), utxo.GetTxID().end() ); AppendUInt32BE( payload, utxo.GetOutputIdx() ); - AppendUInt32BE( payload, static_cast( address.size() ) ); - payload.insert( payload.end(), address.begin(), address.end() ); + AppendUInt32BE( payload, static_cast( owner_address.size() ) ); + payload.insert( payload.end(), owner_address.begin(), owner_address.end() ); const auto &token_bytes = utxo.GetTokenID().bytes(); payload.insert( payload.end(), token_bytes.begin(), token_bytes.end() ); @@ -524,7 +597,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 ); @@ -549,35 +624,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 ) @@ -588,49 +690,76 @@ 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 & ) - { - logger_->error( "There are no UTXOs in cache for address {}", address ); - return std::errc::bad_address; - } - base::Buffer buf( std::vector( utxos.ByteSizeLong() ) ); - if ( !utxos.SerializeToArray( buf.data(), buf.size() ) ) + uint64_t stored = 0; + for ( const auto &[outpoint, entry] : utxo_outpoints_ ) { - logger_->error( "Failed to serialize to array" ); - return std::errc::bad_message; - } + if ( entry.utxo.GetOwnerAddress() != address ) + { + continue; + } - std::string key( DB_PREFIX ); - key.push_back( '/' ); - key.append( address ); - base::Buffer key_buf; - key_buf.put( key ); + SGTransaction::UTXOEntryRecord entry_record; + auto *utxo_proto = entry_record.mutable_utxo(); + utxo_proto->set_hash( entry.utxo.GetTxID().data(), entry.utxo.GetTxID().size() ); + utxo_proto->set_token( entry.utxo.GetTokenID().bytes().data(), entry.utxo.GetTokenID().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() ); + } - if ( auto result = db_->put( key_buf, buf ); result.has_error() ) - { - logger_->error( "Error when storing UTXOs" ); - return result.error(); + base::Buffer value_buf( std::vector( entry_record.ByteSizeLong() ) ); + if ( !entry_record.SerializeToArray( value_buf.data(), value_buf.size() ) ) + { + logger_->error( "Failed to serialize UTXO record for address {}", address ); + return std::errc::bad_message; + } + + base::Buffer key_buf; + key_buf.put( BuildUTXORecordKey( address, outpoint ) ); + + if ( auto put_res = db_->put( key_buf, value_buf ); put_res.has_error() ) + { + logger_->error( "Error when storing UTXO record for address {}", address ); + return put_res.error(); + } + ++stored; } - logger_->info( "Stored {} UTXOs for address {}", utxos.utxos_size(), address ); + logger_->info( "Stored {} UTXOs for address {}", stored, address ); return outcome::success(); } @@ -641,29 +770,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() ) + for ( const auto &outpoint : address_it->second ) { - continue; - } - if ( state == UTXOState::UTXO_CONSUMED || state == UTXOState::UTXO_RESERVED ) - { - continue; - } - if ( !token_id.Equals( utxo.GetTokenID() ) ) - { - 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 38f28a5e5..2d303814f 100644 --- a/src/account/UTXOManager.hpp +++ b/src/account/UTXOManager.hpp @@ -7,21 +7,44 @@ #include "crdt/globaldb/globaldb.hpp" #include "storage/rocksdb/rocksdb.hpp" +#include #include +#include namespace sgns { + struct OutPointHash + { + size_t operator()( const OutPoint &outpoint ) const + { + size_t seed = 0; + boost::hash_combine( seed, 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; + }; + + 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 +94,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 */ - void DeleteUTXO( const base::Hash256 &utxo_id, const std::string &address ); + void DeleteUTXO( const base::Hash256 &utxo_id, uint32_t output_idx, const std::string &address ); /** * @brief Consume UTXOs from the account @@ -165,8 +189,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_set reserved_outpoints_; }; } diff --git a/src/account/proto/SGTransaction.proto b/src/account/proto/SGTransaction.proto index 0a8b30c14..f1967dfdf 100644 --- a/src/account/proto/SGTransaction.proto +++ b/src/account/proto/SGTransaction.proto @@ -43,6 +43,25 @@ 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 UTXOList { repeated UTXO utxos = 1; @@ -88,4 +107,3 @@ message EscrowReleaseTx string original_escrow_hash = 6; } - diff --git a/test/src/account/utxo_manager_test.cpp b/test/src/account/utxo_manager_test.cpp index 07b8a3e4f..78dd71fbf 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" @@ -172,8 +174,10 @@ TEST_F( UTXOManagerTest, Storage ) TEST_F( UTXOManagerTest, MerkleRootDeterministicAcrossInsertionOrder ) { - const auto hash_a = HASHER.sha2_256( { 0xA1 } ); - const auto hash_b = HASHER.sha2_256( { 0xB2 } ); + 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 ), @@ -196,8 +200,10 @@ TEST_F( UTXOManagerTest, MerkleRootDeterministicAcrossInsertionOrder ) TEST_F( UTXOManagerTest, MerkleRootChangesWhenUTXOSetChanges ) { - const auto hash_a = HASHER.sha2_256( { 0xC3 } ); - const auto hash_b = HASHER.sha2_256( { 0xD4 } ); + 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 } ) ) ) ); @@ -223,7 +229,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 ) From 8d8388fb50f3a9462fd490b79f06b4da71cf0034 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 13 Mar 2026 16:10:55 -0300 Subject: [PATCH 074/114] Docs: Starting doxygen on Consensus --- src/blockchain/Consensus.hpp | 54 ++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 37498f18a..a7408d1a8 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -31,33 +31,63 @@ 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; - using Vote = ConsensusVote; - using VoteBundle = ConsensusVoteBundle; - using Certificate = ConsensusCertificate; - using Subject = ConsensusSubject; + 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, - Reject, - Pending + 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; - uint64_t approved_weight = 0; - bool has_quorum = false; + 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, @@ -220,7 +250,7 @@ namespace sgns 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( 0 ) }; + 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 }; From 9bb0abcf01ccf567ef7747dfee0e6821e87209ea Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 18 Mar 2026 08:59:29 -0300 Subject: [PATCH 075/114] Feat: Using merkle proofs to verify UTXOs --- src/account/GeniusNode.cpp | 14 +- src/account/TransactionManager.cpp | 836 +++++++++++++++++- src/account/TransactionManager.hpp | 10 +- src/account/UTXOManager.cpp | 370 +++++--- src/account/UTXOManager.hpp | 45 +- src/account/UTXOMerkle.hpp | 148 ++++ src/account/proto/SGTransaction.proto | 12 +- src/blockchain/Blockchain.hpp | 9 +- src/blockchain/Consensus.cpp | 41 +- src/blockchain/Consensus.hpp | 4 +- src/blockchain/ValidatorRegistry.cpp | 1 + src/blockchain/impl/Blockchain.cpp | 19 +- src/blockchain/impl/proto/Consensus.proto | 26 + test/src/account/utxo_manager_test.cpp | 23 + .../blockchain/consensus_certificate_test.cpp | 51 ++ test/src/multiaccount/multi_account_sync.cpp | 120 ++- 16 files changed, 1500 insertions(+), 229 deletions(-) create mode 100644 src/account/UTXOMerkle.hpp diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index d10574584..386a1b552 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -477,15 +477,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::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 ); @@ -497,11 +497,11 @@ 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 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::debug ); - auto loggerValidator = ConfigureLogger( "ValidatorRegistry", logdir, spdlog::level::debug ); + 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 ); diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 422caec4a..cc1317a64 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,7 @@ #include "MintTransaction.hpp" #include "EscrowTransaction.hpp" #include "EscrowReleaseTransaction.hpp" +#include "UTXOMerkle.hpp" #include "account/TokenAmount.hpp" #include "account/AccountMessenger.hpp" #include "account/proto/SGTransaction.pb.h" @@ -28,6 +30,16 @@ 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; + } // namespace + base::Logger TransactionManagerLogger() { // Always call base::createLogger to get the current logger @@ -499,7 +511,7 @@ namespace sgns transfer_transaction->MakeSignature( *account_m ); RecordOutgoingTxHash( transfer_transaction->GetNonce(), transfer_transaction->GetHash() ); - utxo_manager_.ReserveUTXOs( inputs ); + utxo_manager_.ReserveUTXOs( inputs, transfer_transaction->GetHash() ); EnqueueTransaction( std::make_pair( transfer_transaction, std::nullopt ) ); @@ -548,13 +560,12 @@ namespace sgns "0x" + hash_data.toReadableString(), TokenID::FromBytes( { 0x00 } ) ) ); auto [inputs, outputs] = params; - utxo_manager_.ReserveUTXOs( inputs ); - auto escrow_transaction = std::make_shared( EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct() ) ); escrow_transaction->MakeSignature( *account_m ); RecordOutgoingTxHash( escrow_transaction->GetNonce(), escrow_transaction->GetHash() ); + utxo_manager_.ReserveUTXOs( inputs, escrow_transaction->GetHash() ); // Get the transaction ID for tracking auto txId = escrow_transaction->GetHash(); @@ -870,10 +881,27 @@ namespace sgns for ( auto &transaction : transactions_sent ) { + std::optional utxo_commitment = BuildUTXOTransitionCommitment( transaction ); + std::optional utxo_witness = BuildUTXOWitness( transaction ); + const bool is_utxo_tx = transaction->GetType() == "transfer" || transaction->GetType() == "escrow-hold" || + transaction->GetType() == "escrow-release"; + if ( is_utxo_tx && ( !utxo_commitment.has_value() || !utxo_witness.has_value() ) ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Missing UTXO commitment/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 ); + } OUTCOME_TRY( auto &&proposal, blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), transaction->GetNonce(), - transaction->GetHash() ) ); + transaction->GetHash(), + utxo_commitment ? &utxo_commitment.value() : nullptr, + utxo_witness ? &utxo_witness.value() : nullptr ) ); OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); @@ -1322,7 +1350,7 @@ namespace sgns OUTCOME_TRY( ParseTransaction( tx ) ); } } - utxo_manager_.RollbackUTXOs( transfer_tx->GetInputInfos() ); + utxo_manager_.RollbackUTXOs( transfer_tx->GetInputInfos(), transfer_tx->GetHash() ); return outcome::success(); } @@ -1369,7 +1397,7 @@ namespace sgns OUTCOME_TRY( ParseTransaction( tx ) ); } } - utxo_manager_.RollbackUTXOs( inputs ); + utxo_manager_.RollbackUTXOs( inputs, escrow_tx->GetHash() ); } } @@ -1600,8 +1628,45 @@ namespace sgns 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; @@ -2908,6 +2973,46 @@ namespace sgns 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( @@ -2979,7 +3084,18 @@ namespace sgns return ConsensusManager::SubjectCheck::Reject; } - auto validate_result = ValidateTransactionForConsensus( tracked.tx ); + const bool witness_valid = ValidateWitnessForConsensus( subject, tracked.tx ); + if ( !witness_valid ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Witness validation failed for hash {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::SubjectCheck::Reject; + } + + auto validate_result = ValidateTransactionForConsensus( tracked.tx, witness_valid ); return validate_result ? ConsensusManager::SubjectCheck::Approve : ConsensusManager::SubjectCheck::Reject; } @@ -3019,7 +3135,8 @@ namespace sgns return true; } - bool TransactionManager::ValidateTransactionForConsensus( const std::shared_ptr &tx ) const + bool TransactionManager::ValidateTransactionForConsensus( const std::shared_ptr &tx, + bool skip_utxo_state_validation ) const { TransactionManagerLogger()->debug( "[{} - full: {}] {}: Validating transaction", account_m->GetAddress().substr( 0, 8 ), @@ -3070,7 +3187,11 @@ namespace sgns tx->GetHash() ); return false; } - if ( !CheckTransactionTypeRules( tx ) ) + //TODO - Deal with checking the Mint + //TODO - Make all transactions have UTXOs maybe. + const bool is_utxo_type = + tx->GetType() == "transfer" || tx->GetType() == "escrow-hold" || tx->GetType() == "escrow-release"; + if ( !( skip_utxo_state_validation && is_utxo_type ) && !CheckTransactionTypeRules( tx ) ) { TransactionManagerLogger()->error( "[{} - full: {}] {}: Type rules failed tx={}", account_m->GetAddress().substr( 0, 8 ), @@ -3232,15 +3353,26 @@ namespace sgns full_node_m, __func__, tx.GetSrcAddress() ); - auto previous_hash_subject_result = blockchain_->CreateConsensusNonceSubject( tx.GetSrcAddress(), - tx.GetNonce() - 1, - tx.GetPreviousHash() ); - if ( previous_hash_subject_result.has_error() ) + const auto previous_hash = tx.GetPreviousHash(); + if ( previous_hash.empty() ) { return false; } - - return blockchain_->CheckCertificateStrict( previous_hash_subject_result.value() ); + 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(); @@ -3395,6 +3527,676 @@ namespace sgns return true; } + bool TransactionManager::ValidateWitnessForConsensus( const ConsensusSubject &subject, + const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return false; + } + + if ( !subject.has_nonce() ) + { + return true; + } + + std::vector inputs; + if ( tx->GetType() == "transfer" ) + { + auto transfer_tx = std::dynamic_pointer_cast( tx ); + if ( !transfer_tx ) + { + return false; + } + inputs = transfer_tx->GetInputInfos(); + } + else if ( tx->GetType() == "escrow-hold" ) + { + auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return false; + } + inputs = escrow_tx->GetUTXOParameters().first; + } + else if ( tx->GetType() == "escrow-release" ) + { + auto escrow_release_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_release_tx ) + { + return false; + } + inputs = escrow_release_tx->GetUTXOParameters().first; + } + else + { + return true; + } + + if ( inputs.empty() ) + { + return false; + } + if ( !subject.nonce().has_utxo_commitment() || !subject.nonce().has_utxo_witness() ) + { + return false; + } + + const auto &commitment = subject.nonce().utxo_commitment(); + if ( commitment.pre_utxo_root().size() != base::Hash256::size() || commitment.post_utxo_root().size() != base::Hash256::size() ) + { + return false; + } + if ( commitment.account_state_version() != ( tx->GetNonce() > 0 ? tx->GetNonce() - 1 : 0 ) ) + { + return false; + } + + auto pre_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( commitment.pre_utxo_root().data() ) ), + commitment.pre_utxo_root().size() ) ); + if ( pre_root_result.has_error() ) + { + return false; + } + const auto pre_root = pre_root_result.value(); + + // Anchor pre-state root in the certified per-account chain. + if ( tx->GetNonce() == 0 ) + { + auto checkpoint_result = utxo_manager_.LoadLatestCheckpoint( tx->GetSrcAddress() ); + if ( checkpoint_result.has_error() ) + { + return false; + } + + if ( checkpoint_result.value().has_value() ) + { + if ( checkpoint_result.value()->utxo_merkle_root != pre_root ) + { + return false; + } + } + else if ( pre_root != utxo_merkle::EmptyUTXOMerkleRoot() ) + { + return false; + } + } + else + { + const auto prev_hash = tx->GetPreviousHash(); + if ( prev_hash.empty() ) + { + return false; + } + + auto prev_cert_result = blockchain_->GetCertificateBySubjectHash( prev_hash ); + if ( prev_cert_result.has_error() ) + { + return false; + } + const auto &prev_subject = prev_cert_result.value().proposal().subject(); + if ( !prev_subject.has_nonce() || !prev_subject.nonce().has_utxo_commitment() ) + { + return false; + } + if ( prev_subject.account_id() != tx->GetSrcAddress() ) + { + return false; + } + if ( prev_subject.nonce().nonce() + 1 != tx->GetNonce() ) + { + return false; + } + + const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); + auto prev_post_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); + if ( prev_post_root_result.has_error() ) + { + return false; + } + if ( prev_post_root_result.value() != pre_root ) + { + 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; + } + } + + std::vector outputs; + if ( tx->GetType() == "transfer" ) + { + auto transfer_tx = std::dynamic_pointer_cast( tx ); + if ( !transfer_tx ) + { + return false; + } + outputs = transfer_tx->GetDstInfos(); + } + else if ( tx->GetType() == "escrow-hold" ) + { + auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return false; + } + outputs = escrow_tx->GetUTXOParameters().second; + } + else if ( tx->GetType() == "escrow-release" ) + { + auto escrow_release_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_release_tx ) + { + return false; + } + outputs = escrow_release_tx->GetUTXOParameters().second; + } + + if ( outputs.empty() ) + { + 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 current_hash = HashLeaf( payload_vec ); + for ( const auto &step : proof.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() ) + { + current_hash = HashNode( sibling_hash_result.value(), current_hash ); + } + else + { + current_hash = HashNode( current_hash, sibling_hash_result.value() ); + } + } + + if ( current_hash != pre_root ) + { + 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; + } + + std::optional TransactionManager::BuildUTXOTransitionCommitment( + const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return std::nullopt; + } + std::vector before_snapshot = utxo_manager_.GetUTXOsForReservation( tx->GetSrcAddress(), tx->GetHash() ); + std::vector after_snapshot = before_snapshot; + if ( !ApplyTransactionToUTXOSnapshot( tx, after_snapshot ) ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Could not build transition snapshot for tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; + } + + UTXOTransitionCommitment commitment; + const auto pre_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( before_snapshot ); + const auto post_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( after_snapshot ); + + // Safety guard before proposing: local pre-state must be anchored to bootstrap/certified chain. + if ( tx->GetNonce() == 0 ) + { + auto checkpoint_result = utxo_manager_.LoadLatestCheckpoint( tx->GetSrcAddress() ); + if ( checkpoint_result.has_error() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Failed checkpoint lookup for tx={} err={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + checkpoint_result.error().message() ); + return std::nullopt; + } + if ( checkpoint_result.value().has_value() ) + { + if ( checkpoint_result.value()->utxo_merkle_root != pre_root ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Nonce-0 pre_root mismatches checkpoint root tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; + } + } + else if ( pre_root != utxo_merkle::EmptyUTXOMerkleRoot() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Nonce-0 pre_root is not empty root tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; + } + } + else + { + const auto previous_hash = tx->GetPreviousHash(); + if ( previous_hash.empty() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Missing previous hash for tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; + } + auto previous_cert = blockchain_->GetCertificateBySubjectHash( previous_hash ); + if ( previous_cert.has_error() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Missing previous certificate tx={} prev={} err={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + previous_hash, + previous_cert.error().message() ); + return std::nullopt; + } + const auto &previous_subject = previous_cert.value().proposal().subject(); + if ( !previous_subject.has_nonce() || !previous_subject.nonce().has_utxo_commitment() || + previous_subject.account_id() != tx->GetSrcAddress() || + ( previous_subject.nonce().nonce() + 1 ) != tx->GetNonce() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Previous subject continuity mismatch tx={} prev={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + previous_hash ); + return std::nullopt; + } + + const auto &prev_commitment = previous_subject.nonce().utxo_commitment(); + auto prev_post_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); + if ( prev_post_root_result.has_error() || prev_post_root_result.value() != pre_root ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Pre-root not anchored to prev certified post-root tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; + } + } + + commitment.set_pre_utxo_root( pre_root.data(), pre_root.size() ); + commitment.set_post_utxo_root( post_root.data(), post_root.size() ); + commitment.set_utxo_count_before( before_snapshot.size() ); + commitment.set_utxo_count_after( after_snapshot.size() ); + commitment.set_account_state_version( tx->GetNonce() > 0 ? tx->GetNonce() - 1 : 0 ); + return commitment; + } + + std::optional TransactionManager::BuildUTXOWitness( + const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return std::nullopt; + } + + std::vector inputs; + if ( tx->GetType() == "transfer" ) + { + auto transfer_tx = std::dynamic_pointer_cast( tx ); + if ( !transfer_tx ) + { + return std::nullopt; + } + inputs = transfer_tx->GetInputInfos(); + } + else if ( tx->GetType() == "escrow-hold" ) + { + auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return std::nullopt; + } + inputs = escrow_tx->GetUTXOParameters().first; + } + else if ( tx->GetType() == "escrow-release" ) + { + auto escrow_release_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_release_tx ) + { + return std::nullopt; + } + inputs = escrow_release_tx->GetUTXOParameters().first; + } + else + { + return std::nullopt; + } + + if ( inputs.empty() ) + { + return std::nullopt; + } + + 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 ); + } + } + + 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->GetType() == "transfer" ) + { + auto transfer_tx = std::dynamic_pointer_cast( tx ); + if ( !transfer_tx ) + { + return false; + } + remove_inputs( transfer_tx->GetInputInfos() ); + const auto &outputs = transfer_tx->GetDstInfos(); + 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; + } + + if ( tx->GetType() == "mint" ) + { + auto mint_tx = std::dynamic_pointer_cast( tx ); + if ( !mint_tx ) + { + return false; + } + snapshot.emplace_back( tx_hash.value(), + 0, + mint_tx->GetAmount(), + mint_tx->GetTokenID(), + tx->GetSrcAddress() ); + return true; + } + + if ( tx->GetType() == "escrow-hold" ) + { + auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return false; + } + auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); + 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; + } + + if ( tx->GetType() == "escrow-release" ) + { + auto escrow_release_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_release_tx ) + { + return false; + } + auto [inputs, outputs] = escrow_release_tx->GetUTXOParameters(); + 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; + } + + return false; + } + void TransactionManager::SetNonceWindow( uint64_t window ) { if ( window == 0 ) diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 0459f2785..163ade24f 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -374,12 +374,20 @@ namespace sgns 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 ValidateTransactionForConsensus( const std::shared_ptr &tx, + bool skip_utxo_state_validation = false ) 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; + bool 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 ); diff --git a/src/account/UTXOManager.cpp b/src/account/UTXOManager.cpp index 67b330982..3cd9c24d6 100644 --- a/src/account/UTXOManager.cpp +++ b/src/account/UTXOManager.cpp @@ -1,74 +1,37 @@ #include "UTXOManager.hpp" +#include "UTXOMerkle.hpp" #include +#include #include #include #include "account/proto/SGTransaction.pb.h" #include "base/blob.hpp" -#include "crypto/sha/sha256.hpp" #include "storage/database_error.hpp" namespace sgns { namespace { - constexpr uint8_t kLeafPrefix = 0x00; - constexpr uint8_t kNodePrefix = 0x01; - - 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 ) ); - } - - 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 ) ); - } - - base::Hash256 EmptyUTXOMerkleRoot() - { - static const base::Hash256 empty_root = crypto::sha256( std::string_view( "UTXO_EMPTY_V1" ) ); - return empty_root; - } - - base::Hash256 HashLeaf( const std::vector &payload ) + void RemoveOutPointFromVector( std::vector &outpoints, const OutPoint &target ) { - std::vector leaf_bytes; - leaf_bytes.reserve( payload.size() + 1 ); - leaf_bytes.push_back( kLeafPrefix ); - leaf_bytes.insert( leaf_bytes.end(), payload.begin(), payload.end() ); - return crypto::sha256( gsl::span( leaf_bytes.data(), leaf_bytes.size() ) ); + outpoints.erase( std::remove( outpoints.begin(), outpoints.end(), target ), outpoints.end() ); } - base::Hash256 HashNode( const base::Hash256 &left, const base::Hash256 &right ) + std::string BuildUTXORecordKey( const std::string &owner_address, const OutPoint &outpoint ) { - std::vector node_bytes; - node_bytes.reserve( 1 + left.size() + right.size() ); - node_bytes.push_back( kNodePrefix ); - node_bytes.insert( node_bytes.end(), left.begin(), left.end() ); - node_bytes.insert( node_bytes.end(), right.begin(), right.end() ); - return crypto::sha256( gsl::span( node_bytes.data(), node_bytes.size() ) ); + return fmt::format( "/utxo/{}/{}:{}", owner_address, outpoint.txid_hash_.toReadableString(), outpoint.output_idx_ ); } - void RemoveOutPointFromVector( std::vector &outpoints, const OutPoint &target ) + std::string BuildCheckpointRecordKey( const std::string &owner_address, uint64_t epoch ) { - outpoints.erase( std::remove( outpoints.begin(), outpoints.end(), target ), outpoints.end() ); + return fmt::format( "/utxo-checkpoint/{}/{}", owner_address, epoch ); } - std::string BuildUTXORecordKey( const std::string &owner_address, const OutPoint &outpoint ) + std::string BuildLatestCheckpointPointerKey( const std::string &owner_address ) { - return fmt::format( "/utxo/{}/{}:{}", owner_address, outpoint.txid_hash_.toReadableString(), outpoint.output_idx_ ); + return fmt::format( "/utxo-checkpoint/{}/latest", owner_address ); } std::optional ParseOwnerAddrFromUTXORecordKey( std::string_view key ) @@ -100,6 +63,11 @@ namespace sgns 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 @@ -254,9 +222,8 @@ namespace sgns auto &entry = canonical_it->second; if ( entry.state == UTXOState::UTXO_READY && entry.utxo.GetOwnerAddress() == address ) { - utxo_found = true; - entry.state = UTXOState::UTXO_CONSUMED; - entry.utxo.SetOwnerAddress( address ); + utxo_found = true; + entry.state = UTXOState::UTXO_CONSUMED; } } @@ -266,8 +233,11 @@ namespace sgns RemoveOutPointFromVector( address_it->second, outpoint ); } - GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID(), address ); - utxo_outpoints_[outpoint] = UTXOEntry{ UTXOState::UTXO_CONSUMED, consumed_utxo }; + if ( !utxo_found ) + { + GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID(), address ); + utxo_outpoints_[outpoint] = UTXOEntry{ UTXOState::UTXO_CONSUMED, consumed_utxo }; + } consumed = consumed && utxo_found; } @@ -302,6 +272,39 @@ namespace sgns 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_it->second.utxo ); + } + return result; + } + return {}; + } + std::unordered_map> UTXOManager::GetAllUTXOs() const { std::shared_lock lock( utxos_mutex_ ); @@ -337,7 +340,7 @@ namespace sgns } auto &outpoints = address_outpoints_[address]; - outpoints.clear(); + 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 ) { @@ -409,23 +412,44 @@ 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 ( const auto &input_utxo : inputs ) { - reserved_outpoints_.insert( OutPoint{ input_utxo.txid_hash_, input_utxo.output_idx_ } ); + const OutPoint outpoint{ input_utxo.txid_hash_, input_utxo.output_idx_ }; + auto it = reserved_outpoints_.find( outpoint ); + if ( it == reserved_outpoints_.end() ) + { + 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 ( const auto &input_utxo : inputs ) { - reserved_outpoints_.erase( OutPoint{ input_utxo.txid_hash_, input_utxo.output_idx_ } ); + const OutPoint outpoint{ input_utxo.txid_hash_, input_utxo.output_idx_ }; + auto it = reserved_outpoints_.find( outpoint ); + if ( it == reserved_outpoints_.end() ) + { + continue; + } + if ( reservation_id.empty() || it->second == reservation_id ) + { + reserved_outpoints_.erase( it ); + } } } @@ -500,7 +524,7 @@ namespace sgns if ( !is_full_node_ && address != address_ ) { logger_->warn( "Non-full node cannot compute UTXO Merkle root for other addresses" ); - return EmptyUTXOMerkleRoot(); + return utxo_merkle::EmptyUTXOMerkleRoot(); } std::vector unspent; @@ -509,7 +533,7 @@ namespace sgns auto it = address_outpoints_.find( address ); if ( it == address_outpoints_.end() ) { - return EmptyUTXOMerkleRoot(); + return utxo_merkle::EmptyUTXOMerkleRoot(); } unspent.reserve( it->second.size() ); @@ -528,60 +552,12 @@ namespace sgns } } - if ( unspent.empty() ) - { - return EmptyUTXOMerkleRoot(); - } - - std::sort( unspent.begin(), - unspent.end(), - []( const GeniusUTXO &lhs, const GeniusUTXO &rhs ) - { - if ( lhs.GetTxID() != rhs.GetTxID() ) - { - return lhs.GetTxID() < rhs.GetTxID(); - } - return lhs.GetOutputIdx() < rhs.GetOutputIdx(); - } ); - - std::vector level_hashes; - level_hashes.reserve( unspent.size() ); - - for ( const auto &utxo : unspent ) - { - std::vector payload; - const auto &owner_address = utxo.GetOwnerAddress(); - payload.reserve( 32 + 4 + 4 + owner_address.size() + utxo.GetTokenID().bytes().size() + 8 ); - - payload.insert( payload.end(), utxo.GetTxID().begin(), utxo.GetTxID().end() ); - AppendUInt32BE( payload, utxo.GetOutputIdx() ); - AppendUInt32BE( payload, static_cast( owner_address.size() ) ); - payload.insert( payload.end(), owner_address.begin(), owner_address.end() ); - - const auto &token_bytes = utxo.GetTokenID().bytes(); - payload.insert( payload.end(), token_bytes.begin(), token_bytes.end() ); - AppendUInt64BE( payload, utxo.GetAmount() ); - - level_hashes.push_back( HashLeaf( payload ) ); - } - - 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 ComputeMerkleRootFromUTXOList( std::move( unspent ) ); + } - return level_hashes.front(); + base::Hash256 UTXOManager::ComputeUTXOMerkleRootFromSnapshot( const std::vector &utxos ) const + { + return ComputeMerkleRootFromUTXOList( utxos ); } outcome::result UTXOManager::LoadUTXOs( std::shared_ptr db ) @@ -723,8 +699,10 @@ namespace sgns SGTransaction::UTXOEntryRecord entry_record; auto *utxo_proto = entry_record.mutable_utxo(); - utxo_proto->set_hash( entry.utxo.GetTxID().data(), entry.utxo.GetTxID().size() ); - utxo_proto->set_token( entry.utxo.GetTokenID().bytes().data(), entry.utxo.GetTokenID().size() ); + 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 ); @@ -763,6 +741,170 @@ namespace sgns 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( "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; + } + + 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(); + } + + 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( "Failed to store checkpoint latest pointer for address {}", address ); + return put_latest_res.error(); + } + + 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 ) { diff --git a/src/account/UTXOManager.hpp b/src/account/UTXOManager.hpp index 2d303814f..379d4fdb9 100644 --- a/src/account/UTXOManager.hpp +++ b/src/account/UTXOManager.hpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace sgns @@ -17,8 +18,7 @@ namespace sgns { size_t operator()( const OutPoint &outpoint ) const { - size_t seed = 0; - boost::hash_combine( seed, outpoint.txid_hash_ ); + size_t seed = std::hash{}( outpoint.txid_hash_ ); boost::hash_combine( seed, outpoint.output_idx_ ); return seed; } @@ -42,6 +42,16 @@ namespace sgns 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>; @@ -124,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; /** @@ -145,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 { @@ -166,6 +179,11 @@ namespace sgns */ [[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 ); /** @@ -173,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 ); @@ -192,7 +227,7 @@ namespace sgns mutable std::shared_mutex utxos_mutex_; ///< Mutex for UTXO state structures UTXOOutPointMap utxo_outpoints_; AddressOutPointList address_outpoints_; - std::unordered_set reserved_outpoints_; + std::unordered_map reserved_outpoints_; }; } diff --git a/src/account/UTXOMerkle.hpp b/src/account/UTXOMerkle.hpp new file mode 100644 index 000000000..b0c4b2041 --- /dev/null +++ b/src/account/UTXOMerkle.hpp @@ -0,0 +1,148 @@ +#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 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 ) ); + } + + 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 ) ); + } +} diff --git a/src/account/proto/SGTransaction.proto b/src/account/proto/SGTransaction.proto index f1967dfdf..1a57c2d8a 100644 --- a/src/account/proto/SGTransaction.proto +++ b/src/account/proto/SGTransaction.proto @@ -62,6 +62,17 @@ message UTXOEntryRecord 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; @@ -106,4 +117,3 @@ message EscrowReleaseTx string escrow_source = 5; string original_escrow_hash = 6; } - diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index 59818824b..6f6d16c6c 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -113,17 +113,22 @@ namespace sgns outcome::result CreateConsensusNonceSubject( const std::string &account_id, uint64_t nonce, - const std::string &tx_hash ); + const std::string &tx_hash, + const UTXOTransitionCommitment *utxo_commitment = nullptr, + const UTXOWitness *utxo_witness = nullptr ); outcome::result CreateConsensusProposal( const std::string &account_id, uint64_t nonce, - const std::string &tx_hash ); + const std::string &tx_hash, + const UTXOTransitionCommitment *utxo_commitment = nullptr, + const UTXOWitness *utxo_witness = nullptr ); 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: diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 095e33889..c6616628e 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -1950,7 +1950,9 @@ namespace sgns outcome::result ConsensusManager::CreateNonceSubject( const std::string &account_id, uint64_t nonce, - const std::string &tx_hash ) + const std::string &tx_hash, + const UTXOTransitionCommitment *utxo_commitment, + const UTXOWitness *utxo_witness ) { ConsensusManagerLogger()->trace( "{}: called account_id={} nonce={}", __func__, account_id, nonce ); Subject subject; @@ -1959,6 +1961,14 @@ namespace sgns auto *payload = subject.mutable_nonce(); payload->set_nonce( nonce ); payload->set_tx_hash( tx_hash.data(), tx_hash.size() ); + if ( utxo_commitment != nullptr ) + { + *payload->mutable_utxo_commitment() = *utxo_commitment; + } + if ( utxo_witness != nullptr ) + { + *payload->mutable_utxo_witness() = *utxo_witness; + } auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) @@ -2034,6 +2044,16 @@ namespace sgns { 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() ) { @@ -2043,19 +2063,8 @@ namespace sgns return subject.has_task_result() && !subject.task_result().task_result_hash().empty(); case SubjectType::SUBJECT_UNSPECIFIED: default: - break; - } - - if ( subject.has_nonce() ) - { - return !subject.nonce().tx_hash().empty(); - } - if ( subject.has_task_result() ) - { - return !subject.task_result().task_result_hash().empty(); + return false; } - - return false; } void ConsensusManager::OnConsensusMessage( boost::optional message ) @@ -2114,6 +2123,12 @@ namespace sgns 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 ) { diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index a7408d1a8..888de70b0 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -138,7 +138,9 @@ namespace sgns 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::string &tx_hash, + const UTXOTransitionCommitment *utxo_commitment = nullptr, + const UTXOWitness *utxo_witness = nullptr ); static outcome::result CreateTaskResultSubject( const std::string &account_id, const std::string &escrow_path, const std::string &task_result_hash, diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index f9aabc76c..766d919c6 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -446,6 +446,7 @@ namespace sgns std::shared_lock lock( cache_mutex_ ); if ( cached_registry_ ) { + logger_->debug( "{}: returning cached registry", __func__ ); return cached_registry_.value(); } } diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 70784987e..3fb53f536 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1557,16 +1557,21 @@ namespace sgns outcome::result Blockchain::CreateConsensusNonceSubject( const std::string &account_id, uint64_t nonce, - const std::string &tx_hash ) + const std::string &tx_hash, + const UTXOTransitionCommitment *utxo_commitment, + const UTXOWitness *utxo_witness ) { - return consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash ); + 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::string &tx_hash, + const UTXOTransitionCommitment *utxo_commitment, + const UTXOWitness *utxo_witness ) { - OUTCOME_TRY( auto &&nonce_subject, CreateConsensusNonceSubject( account_id, nonce, tx_hash ) ); + 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, @@ -1596,6 +1601,12 @@ namespace sgns 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/proto/Consensus.proto b/src/blockchain/impl/proto/Consensus.proto index e3b1e93ca..38becd793 100644 --- a/src/blockchain/impl/proto/Consensus.proto +++ b/src/blockchain/impl/proto/Consensus.proto @@ -19,9 +19,35 @@ enum SubjectType { SUBJECT_TASK_RESULT = 2; } +message UTXOTransitionCommitment { + bytes pre_utxo_root = 1; + bytes post_utxo_root = 2; + uint64 utxo_count_before = 3; + uint64 utxo_count_after = 4; + uint64 account_state_version = 5; +} + +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; +} + +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 { diff --git a/test/src/account/utxo_manager_test.cpp b/test/src/account/utxo_manager_test.cpp index 78dd71fbf..601dbbd51 100644 --- a/test/src/account/utxo_manager_test.cpp +++ b/test/src/account/utxo_manager_test.cpp @@ -219,6 +219,29 @@ TEST_F( UTXOManagerTest, MerkleRootChangesWhenUTXOSetChanges ) 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; diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index e26e92621..6a694a0d9 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -464,4 +464,55 @@ namespace sgns::test "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" ); + 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; + commitment.set_pre_utxo_root( std::string( 32, '\x01' ) ); + commitment.set_post_utxo_root( std::string( 32, '\x02' ) ); + commitment.set_utxo_count_before( 1 ); + commitment.set_utxo_count_after( 1 ); + commitment.set_account_state_version( 0 ); + + 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 f397eb563..47d540a0f 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -515,25 +515,42 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) EXPECT_GT( registry_after.value().epoch(), epoch_before ); EXPECT_NE( registry->GetRegistryCid(), cid_before ); - auto *full_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), - node_full->GetAddress() ); - auto *peer1_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), - node_peer1->GetAddress() ); - auto *peer2_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), - node_peer2->GetAddress() ); - auto *peer3_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), - node_peer3->GetAddress() ); - - ASSERT_TRUE( full_validator ); - ASSERT_TRUE( peer1_validator ); - ASSERT_TRUE( peer2_validator ); - ASSERT_TRUE( peer3_validator ); - EXPECT_GT( full_validator->weight(), 0 ); - EXPECT_GT( peer1_validator->weight(), 0 ); - EXPECT_GT( peer2_validator->weight(), 0 ); - EXPECT_GT( peer3_validator->weight(), 0 ); + 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 *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() > 2 ) + { + 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() > 3 ) + { + auto *peer3_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), + node_peer3->GetAddress() ); + ASSERT_TRUE( peer3_validator ); + + EXPECT_GT( peer3_validator->weight(), 0 ); + } }; + 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, "", "", @@ -542,6 +559,13 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; fmt::println( "Mint 1 succeeded" ); + assert_registry_updated( epoch_before, cid_before ); + + 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, "", "", @@ -549,7 +573,11 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); ASSERT_TRUE( mint2.has_value() ) << "Mint 2 failed on node_client"; fmt::println( "Mint 2 succeeded" ); - + assert_registry_updated( epoch_before, cid_before ); + 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(), @@ -557,7 +585,11 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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 ); + 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(), @@ -565,7 +597,11 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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 ); + 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(), @@ -574,49 +610,5 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) ASSERT_TRUE( transfer3.has_value() ) << "Transfer 3 failed on node_client"; fmt::println( "Transfer 3 succeeded" ); - - - auto wait_confirmed = [&]( const std::string &tx_id, const char *label ) - { - test::assertWaitForCondition( - [&]() - { return node_client->GetTransactionStatus( tx_id ) == TransactionManager::TransactionStatus::CONFIRMED; }, - std::chrono::milliseconds( INCOMING_TIMEOUT_MILLISECONDS ), - label ); - }; - - auto registry_state = registry->LoadRegistry(); - ASSERT_TRUE( registry_state.has_value() ); - auto epoch_before = registry_state.value().epoch(); - auto cid_before = registry->GetRegistryCid(); - wait_confirmed( mint1.value().first, "mint1 not confirmed" ); - assert_registry_updated( epoch_before, cid_before ); - - registry_state = registry->LoadRegistry(); - ASSERT_TRUE( registry_state.has_value() ); - epoch_before = registry_state.value().epoch(); - cid_before = registry->GetRegistryCid(); - wait_confirmed( mint2.value().first, "mint2 not confirmed" ); - assert_registry_updated( epoch_before, cid_before ); - - registry_state = registry->LoadRegistry(); - ASSERT_TRUE( registry_state.has_value() ); - epoch_before = registry_state.value().epoch(); - cid_before = registry->GetRegistryCid(); - wait_confirmed( transfer1.value().first, "transfer1 not confirmed" ); - assert_registry_updated( epoch_before, cid_before ); - - registry_state = registry->LoadRegistry(); - ASSERT_TRUE( registry_state.has_value() ); - epoch_before = registry_state.value().epoch(); - cid_before = registry->GetRegistryCid(); - wait_confirmed( transfer2.value().first, "transfer2 not confirmed" ); - assert_registry_updated( epoch_before, cid_before ); - - registry_state = registry->LoadRegistry(); - ASSERT_TRUE( registry_state.has_value() ); - epoch_before = registry_state.value().epoch(); - cid_before = registry->GetRegistryCid(); - wait_confirmed( transfer3.value().first, "transfer3 not confirmed" ); assert_registry_updated( epoch_before, cid_before ); } From 029a5abc9ec6d0e83c5ef13cc410299bb23a18b8 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 18 Mar 2026 09:00:03 -0300 Subject: [PATCH 076/114] WIP: New transaction sync tests --- .../transaction_sync_test.cpp | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/test/src/transaction_sync/transaction_sync_test.cpp b/test/src/transaction_sync/transaction_sync_test.cpp index a32c309b6..bf7e905ab 100644 --- a/test/src/transaction_sync/transaction_sync_test.cpp +++ b/test/src/transaction_sync/transaction_sync_test.cpp @@ -143,7 +143,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 ); } @@ -553,3 +553,71 @@ 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, + "", + "", + TokenID::FromBytes( { 0x00 } ), + std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + 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() ); + 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 ); +} From b4a66d76dba1ad3f2dc707207514f804864afe5c Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 18 Mar 2026 12:49:42 -0300 Subject: [PATCH 077/114] Fix: Nonce 0 bootstrap. The first transaction checks if a previous certificate has as outputs the inputs used in the mint --- src/account/TransactionManager.cpp | 288 +++++++++++++++++----- src/blockchain/impl/proto/Consensus.proto | 2 + 2 files changed, 230 insertions(+), 60 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index cc1317a64..bc10d4b8d 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -38,6 +38,97 @@ namespace sgns 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->GetType() == "transfer" ) + { + auto transfer_tx = std::dynamic_pointer_cast( tx ); + if ( !transfer_tx ) + { + return false; + } + const auto &dst_infos = transfer_tx->GetDstInfos(); + 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; + } + + if ( tx->GetType() == "mint" ) + { + auto mint_tx = std::dynamic_pointer_cast( tx ); + if ( !mint_tx ) + { + return false; + } + outputs.emplace_back( tx_hash.value(), + 0, + mint_tx->GetAmount(), + mint_tx->GetTokenID(), + mint_tx->GetSrcAddress() ); + return true; + } + + if ( tx->GetType() == "escrow-hold" ) + { + auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return false; + } + const auto &dst_infos = escrow_tx->GetUTXOParameters().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; + } + + if ( tx->GetType() == "escrow-release" ) + { + auto escrow_release_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_release_tx ) + { + return false; + } + const auto &dst_infos = escrow_release_tx->GetUTXOParameters().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; + } + + return false; + } } // namespace base::Logger TransactionManagerLogger() @@ -3601,28 +3692,8 @@ namespace sgns } const auto pre_root = pre_root_result.value(); - // Anchor pre-state root in the certified per-account chain. - if ( tx->GetNonce() == 0 ) - { - auto checkpoint_result = utxo_manager_.LoadLatestCheckpoint( tx->GetSrcAddress() ); - if ( checkpoint_result.has_error() ) - { - return false; - } - - if ( checkpoint_result.value().has_value() ) - { - if ( checkpoint_result.value()->utxo_merkle_root != pre_root ) - { - return false; - } - } - else if ( pre_root != utxo_merkle::EmptyUTXOMerkleRoot() ) - { - return false; - } - } - else + // For nonce > 0, anchor pre-state root in the certified per-account chain. + if ( tx->GetNonce() > 0 ) { const auto prev_hash = tx->GetPreviousHash(); if ( prev_hash.empty() ) @@ -3821,6 +3892,57 @@ namespace sgns { return false; } + + 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 ) @@ -3864,44 +3986,8 @@ namespace sgns const auto pre_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( before_snapshot ); const auto post_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( after_snapshot ); - // Safety guard before proposing: local pre-state must be anchored to bootstrap/certified chain. - if ( tx->GetNonce() == 0 ) - { - auto checkpoint_result = utxo_manager_.LoadLatestCheckpoint( tx->GetSrcAddress() ); - if ( checkpoint_result.has_error() ) - { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Failed checkpoint lookup for tx={} err={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - checkpoint_result.error().message() ); - return std::nullopt; - } - if ( checkpoint_result.value().has_value() ) - { - if ( checkpoint_result.value()->utxo_merkle_root != pre_root ) - { - TransactionManagerLogger()->warn( - "[{} - full: {}] {}: Nonce-0 pre_root mismatches checkpoint root tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); - return std::nullopt; - } - } - else if ( pre_root != utxo_merkle::EmptyUTXOMerkleRoot() ) - { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Nonce-0 pre_root is not empty root tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); - return std::nullopt; - } - } - else + // Safety guard before proposing: for nonce > 0 pre-state must be anchored to certified chain. + if ( tx->GetNonce() > 0 ) { const auto previous_hash = tx->GetPreviousHash(); if ( previous_hash.empty() ) @@ -3954,11 +4040,24 @@ namespace sgns } } + 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; + } + const auto produced_outputs_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( produced_outputs ); + commitment.set_pre_utxo_root( pre_root.data(), pre_root.size() ); commitment.set_post_utxo_root( post_root.data(), post_root.size() ); commitment.set_utxo_count_before( before_snapshot.size() ); commitment.set_utxo_count_after( after_snapshot.size() ); commitment.set_account_state_version( tx->GetNonce() > 0 ? tx->GetNonce() - 1 : 0 ); + commitment.set_produced_outputs_root( produced_outputs_root.data(), produced_outputs_root.size() ); return commitment; } @@ -4076,6 +4175,75 @@ namespace sgns 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; diff --git a/src/blockchain/impl/proto/Consensus.proto b/src/blockchain/impl/proto/Consensus.proto index 38becd793..a2ae273fb 100644 --- a/src/blockchain/impl/proto/Consensus.proto +++ b/src/blockchain/impl/proto/Consensus.proto @@ -25,6 +25,7 @@ message UTXOTransitionCommitment { uint64 utxo_count_before = 3; uint64 utxo_count_after = 4; uint64 account_state_version = 5; + bytes produced_outputs_root = 6; } message MerkleProofStep { @@ -37,6 +38,7 @@ message ConsumedInputProof { uint32 output_index = 2; bytes leaf_payload = 3; repeated MerkleProofStep branch = 4; + repeated MerkleProofStep produced_branch = 5; } message UTXOWitness { From e80374492a10c30b0b645ed50acd0cf7d1915552 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 20 Mar 2026 14:54:59 -0300 Subject: [PATCH 078/114] Fix: Mint not triggering error on certificate due to not having UTXOs --- src/account/TransactionManager.cpp | 54 +++++++++++--------- src/blockchain/ValidatorRegistry.cpp | 22 ++++---- test/src/multiaccount/multi_account_sync.cpp | 11 +++- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index bc10d4b8d..9e95b8fa1 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -3707,7 +3707,7 @@ namespace sgns return false; } const auto &prev_subject = prev_cert_result.value().proposal().subject(); - if ( !prev_subject.has_nonce() || !prev_subject.nonce().has_utxo_commitment() ) + if ( !prev_subject.has_nonce() ) { return false; } @@ -3720,17 +3720,20 @@ namespace sgns return false; } - const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); - if ( prev_post_root_result.has_error() ) + if ( prev_subject.nonce().has_utxo_commitment() ) { - return false; - } - if ( prev_post_root_result.value() != pre_root ) - { - return false; + const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); + auto prev_post_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); + if ( prev_post_root_result.has_error() ) + { + return false; + } + if ( prev_post_root_result.value() != pre_root ) + { + return false; + } } } @@ -4012,8 +4015,7 @@ namespace sgns return std::nullopt; } const auto &previous_subject = previous_cert.value().proposal().subject(); - if ( !previous_subject.has_nonce() || !previous_subject.nonce().has_utxo_commitment() || - previous_subject.account_id() != tx->GetSrcAddress() || + if ( !previous_subject.has_nonce() || previous_subject.account_id() != tx->GetSrcAddress() || ( previous_subject.nonce().nonce() + 1 ) != tx->GetNonce() ) { TransactionManagerLogger()->warn( "[{} - full: {}] {}: Previous subject continuity mismatch tx={} prev={}", @@ -4025,18 +4027,22 @@ namespace sgns return std::nullopt; } - const auto &prev_commitment = previous_subject.nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); - if ( prev_post_root_result.has_error() || prev_post_root_result.value() != pre_root ) + if ( previous_subject.nonce().has_utxo_commitment() ) { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Pre-root not anchored to prev certified post-root tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); - return std::nullopt; + const auto &prev_commitment = previous_subject.nonce().utxo_commitment(); + auto prev_post_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); + if ( prev_post_root_result.has_error() || prev_post_root_result.value() != pre_root ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Pre-root not anchored to prev certified post-root tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; + } } } diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 766d919c6..2779adf4a 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -441,12 +441,11 @@ namespace sgns outcome::result ValidatorRegistry::LoadRegistry() const { - { std::shared_lock lock( cache_mutex_ ); if ( cached_registry_ ) { - logger_->debug( "{}: returning cached registry", __func__ ); + logger_->trace( "{}: returning cached registry", __func__ ); return cached_registry_.value(); } } @@ -1071,13 +1070,14 @@ namespace sgns 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() ); + 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; @@ -1087,9 +1087,7 @@ namespace sgns 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, "," ) ); + logger_->debug( "{}: unregistered voter ids (prefixes)={}", __func__, fmt::join( unregistered_ids, "," ) ); } Registry next = current_registry; diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 47d540a0f..8ee02df28 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -523,20 +523,27 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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() > 2 ) + 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() > 3 ) + if ( registry_after.value().validators().size() > 4 ) { auto *peer3_validator = sgns::ValidatorRegistry::FindValidator( registry_after.value(), node_peer3->GetAddress() ); From 0974df7a7e6c943ff14ddea792cc7246d2a323a0 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 23 Mar 2026 13:46:05 -0300 Subject: [PATCH 079/114] Fix: Issues after merge --- src/account/GeniusNode.cpp | 2 +- src/account/MintTransactionV2.cpp | 4 +- src/account/MintTransactionV2.hpp | 3 +- src/account/TransactionManager.cpp | 198 +++++++++--------- test/src/multiaccount/multi_account_sync.cpp | 27 +-- .../transaction_sync_test.cpp | 8 +- 6 files changed, 112 insertions(+), 130 deletions(-) diff --git a/src/account/GeniusNode.cpp b/src/account/GeniusNode.cpp index efab4b604..f042439fd 100644 --- a/src/account/GeniusNode.cpp +++ b/src/account/GeniusNode.cpp @@ -380,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; } diff --git a/src/account/MintTransactionV2.cpp b/src/account/MintTransactionV2.cpp index 9acdd55d4..eebc12b3d 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(); diff --git a/src/account/MintTransactionV2.hpp b/src/account/MintTransactionV2.hpp index 6aa9de0e9..538afa1c6 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 */ @@ -52,7 +53,7 @@ namespace sgns * @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 diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 095ef4c13..b65ca1adf 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -658,7 +658,7 @@ namespace sgns utxo_manager_.CreateTxParameter( amount, "0x" + hash_data.toReadableString(), TokenID::FromBytes( { 0x00 } ) ) ); - auto [inputs, outputs] = params; + auto [inputs, outputs] = params; auto escrow_transaction = std::make_shared( EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct() ) ); @@ -1361,11 +1361,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(); } @@ -1443,7 +1443,7 @@ namespace sgns for ( std::uint32_t i = 0; i < dest_infos.size(); ++i ) { const auto &dest_info = dest_infos[i]; - auto hash = ( base::Hash256::fromReadableString( transfer_tx->GetHash() ) ).value(); + auto hash = ( base::Hash256::fromReadableString( transfer_tx->GetHash() ) ).value(); BOOST_OUTCOME_TRY( utxo_manager_.DeleteUTXO( hash, i, dest_info.dest_address ) ); TransactionManagerLogger()->debug( "[{} - full: {}] Notify {} of deletion of {} to it", @@ -1489,21 +1489,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(); } @@ -1782,18 +1784,19 @@ namespace sgns full_node_m ); } - 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() ); + 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() ) { @@ -3151,21 +3154,20 @@ namespace sgns 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() ) ); + 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 ); + 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() ); + 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() ); } } @@ -3343,8 +3345,8 @@ namespace sgns } //TODO - Deal with checking the Mint //TODO - Make all transactions have UTXOs maybe. - const bool is_utxo_type = - tx->GetType() == "transfer" || tx->GetType() == "escrow-hold" || tx->GetType() == "escrow-release"; + const bool is_utxo_type = tx->GetType() == "transfer" || tx->GetType() == "escrow-hold" || + tx->GetType() == "escrow-release"; if ( !( skip_utxo_state_validation && is_utxo_type ) && !CheckTransactionTypeRules( tx ) ) { TransactionManagerLogger()->error( "[{} - full: {}] {}: Type rules failed tx={}", @@ -3681,7 +3683,7 @@ namespace sgns return true; } - bool TransactionManager::ValidateWitnessForConsensus( const ConsensusSubject &subject, + bool TransactionManager::ValidateWitnessForConsensus( const ConsensusSubject &subject, const std::shared_ptr &tx ) const { if ( !tx ) @@ -3737,7 +3739,8 @@ namespace sgns } const auto &commitment = subject.nonce().utxo_commitment(); - if ( commitment.pre_utxo_root().size() != base::Hash256::size() || commitment.post_utxo_root().size() != base::Hash256::size() ) + if ( commitment.pre_utxo_root().size() != base::Hash256::size() || + commitment.post_utxo_root().size() != base::Hash256::size() ) { return false; } @@ -3785,10 +3788,10 @@ namespace sgns if ( prev_subject.nonce().has_utxo_commitment() ) { - const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); + const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); + auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( + reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); if ( prev_post_root_result.has_error() ) { return false; @@ -3852,8 +3855,8 @@ namespace sgns } const auto add_amount = []( std::unordered_map &bucket, - const std::string &token_key, - uint64_t amount ) -> bool + const std::string &token_key, + uint64_t amount ) -> bool { auto &total = bucket[token_key]; if ( amount > std::numeric_limits::max() - total ) @@ -3864,7 +3867,7 @@ namespace sgns return true; }; - std::unordered_set seen_inputs; + std::unordered_set seen_inputs; std::unordered_map input_amounts_by_token; std::unordered_map output_amounts_by_token; seen_inputs.reserve( inputs.size() ); @@ -3873,10 +3876,11 @@ namespace sgns for ( const auto &input : inputs ) { - if ( !GeniusAccount::VerifySignature( tx->GetSrcAddress(), - std::string_view( reinterpret_cast( input.signature_.data() ), - input.signature_.size() ), - input.SerializeForSigning() ) ) + if ( !GeniusAccount::VerifySignature( + tx->GetSrcAddress(), + std::string_view( reinterpret_cast( input.signature_.data() ), + input.signature_.size() ), + input.SerializeForSigning() ) ) { return false; } @@ -3906,8 +3910,7 @@ namespace sgns { return false; } - const auto payload_output_idx = - ReadUInt32BE( reinterpret_cast( payload.data() ) + 32 ); + const auto payload_output_idx = ReadUInt32BE( reinterpret_cast( payload.data() ) + 32 ); if ( payload_output_idx != input.output_idx_ ) { return false; @@ -3922,11 +3925,11 @@ namespace sgns { return false; } - const size_t token_offset = 40 + owner_len; - const size_t amount_offset = token_offset + 32; + 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 ); + 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; @@ -3959,8 +3962,7 @@ namespace sgns return false; } - auto producer_cert_result = - blockchain_->GetCertificateBySubjectHash( input.txid_hash_.toReadableString() ); + auto producer_cert_result = blockchain_->GetCertificateBySubjectHash( input.txid_hash_.toReadableString() ); if ( producer_cert_result.has_error() ) { return false; @@ -3975,10 +3977,9 @@ namespace sgns { 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() ) ); + 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; @@ -4013,7 +4014,7 @@ namespace sgns for ( const auto &output : outputs ) { - const auto &token_bytes = output.token_id.bytes(); + 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 ) ) { @@ -4036,7 +4037,8 @@ namespace sgns { return std::nullopt; } - std::vector before_snapshot = utxo_manager_.GetUTXOsForReservation( tx->GetSrcAddress(), tx->GetHash() ); + std::vector before_snapshot = utxo_manager_.GetUTXOsForReservation( tx->GetSrcAddress(), + tx->GetHash() ); std::vector after_snapshot = before_snapshot; if ( !ApplyTransactionToUTXOSnapshot( tx, after_snapshot ) ) { @@ -4049,8 +4051,8 @@ namespace sgns } UTXOTransitionCommitment commitment; - const auto pre_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( before_snapshot ); - const auto post_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( after_snapshot ); + const auto pre_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( before_snapshot ); + const auto post_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( after_snapshot ); // Safety guard before proposing: for nonce > 0 pre-state must be anchored to certified chain. if ( tx->GetNonce() > 0 ) @@ -4068,34 +4070,36 @@ namespace sgns auto previous_cert = blockchain_->GetCertificateBySubjectHash( previous_hash ); if ( previous_cert.has_error() ) { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Missing previous certificate tx={} prev={} err={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - previous_hash, - previous_cert.error().message() ); + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Missing previous certificate tx={} prev={} err={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + previous_hash, + previous_cert.error().message() ); return std::nullopt; } const auto &previous_subject = previous_cert.value().proposal().subject(); if ( !previous_subject.has_nonce() || previous_subject.account_id() != tx->GetSrcAddress() || ( previous_subject.nonce().nonce() + 1 ) != tx->GetNonce() ) { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Previous subject continuity mismatch tx={} prev={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - previous_hash ); + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Previous subject continuity mismatch tx={} prev={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + previous_hash ); return std::nullopt; } if ( previous_subject.nonce().has_utxo_commitment() ) { - const auto &prev_commitment = previous_subject.nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); + const auto &prev_commitment = previous_subject.nonce().utxo_commitment(); + auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( + reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); if ( prev_post_root_result.has_error() || prev_post_root_result.value() != pre_root ) { TransactionManagerLogger()->warn( @@ -4178,7 +4182,7 @@ namespace sgns struct SnapshotLeaf { - std::string outpoint_key; + std::string outpoint_key; std::vector payload; }; @@ -4187,7 +4191,8 @@ namespace sgns leaves.reserve( utxos.size() ); for ( const auto &utxo : utxos ) { - leaves.push_back( { OutPointKey( utxo.GetTxID(), utxo.GetOutputIdx() ), SerializeUTXOLeafPayload( utxo ) } ); + leaves.push_back( + { OutPointKey( utxo.GetTxID(), utxo.GetOutputIdx() ), SerializeUTXOLeafPayload( utxo ) } ); } std::sort( leaves.begin(), @@ -4220,7 +4225,7 @@ namespace sgns 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; + size_t current_index = leaf_index; std::vector current_level = level_hashes; while ( current_level.size() > 1 ) { @@ -4260,9 +4265,8 @@ namespace sgns 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 ) } ); + produced_leaves.push_back( { OutPointKey( output_utxo.GetTxID(), output_utxo.GetOutputIdx() ), + SerializeUTXOLeafPayload( output_utxo ) } ); } std::sort( produced_leaves.begin(), produced_leaves.end(), @@ -4288,7 +4292,7 @@ namespace sgns return std::nullopt; } - size_t produced_index = produced_it->second; + size_t produced_index = produced_it->second; std::vector produced_level = produced_level_hashes; while ( produced_level.size() > 1 ) { @@ -4299,8 +4303,7 @@ namespace sgns 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_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; @@ -4319,7 +4322,7 @@ namespace sgns } bool TransactionManager::ApplyTransactionToUTXOSnapshot( const std::shared_ptr &tx, - std::vector &snapshot ) const + std::vector &snapshot ) const { if ( !tx ) { @@ -4329,12 +4332,11 @@ namespace sgns { 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_; - } ); + 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 ); diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index f230a5afe..a9ffe0848 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -541,11 +541,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) auto epoch_before = registry_state.value().epoch(); auto cid_before = registry->GetRegistryCid(); - auto mint1 = node_client->MintTokens( 100, - "", - "", - TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + auto mint1 = node_client->MintTokens( 100, "", "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; fmt::println( "Mint 1 succeeded" ); @@ -556,11 +552,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) epoch_before = registry_state.value().epoch(); cid_before = registry->GetRegistryCid(); - auto mint2 = node_client->MintTokens( 250, - "", - "", - TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + auto mint2 = node_client->MintTokens( 250, "", "", 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 ); @@ -569,10 +561,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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 ) ); + auto transfer1 = node_client->TransferFunds( 75, node_peer1->GetAddress(), TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( transfer1.has_value() ) << "Transfer 1 failed on node_client"; fmt::println( "Transfer 1 succeeded" ); assert_registry_updated( epoch_before, cid_before ); @@ -581,10 +570,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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 ) ); + auto transfer2 = node_client->TransferFunds( 40, node_peer2->GetAddress(), TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( transfer2.has_value() ) << "Transfer 2 failed on node_client"; fmt::println( "Transfer 2 succeeded" ); assert_registry_updated( epoch_before, cid_before ); @@ -593,10 +579,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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 ) ); + auto transfer3 = node_client->TransferFunds( 10, node_peer3->GetAddress(), TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( transfer3.has_value() ) << "Transfer 3 failed on node_client"; fmt::println( "Transfer 3 succeeded" ); diff --git a/test/src/transaction_sync/transaction_sync_test.cpp b/test/src/transaction_sync/transaction_sync_test.cpp index 082214042..708292ac4 100644 --- a/test/src/transaction_sync/transaction_sync_test.cpp +++ b/test/src/transaction_sync/transaction_sync_test.cpp @@ -547,11 +547,7 @@ TEST_F( TransactionSyncTest, InvalidPreviousHashTest ) "node_proc2 not synched" ); // Mint tokens to ensure sufficient balance - auto mint_result = node_proc1->MintTokens( 20000000000, - "", - "", - TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + auto mint_result = node_proc1->MintTokens( 20000000000, "", "", 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 @@ -575,7 +571,7 @@ TEST_F( TransactionSyncTest, InvalidPreviousHashTest ) node_proc2->GetAddress() ); ASSERT_TRUE( tx_pair2.has_value() ); - auto [tx2, proof2] = tx_pair2.value(); + auto [tx2, proof2] = tx_pair2.value(); std::string bad_prev = tx1_id; if ( !bad_prev.empty() ) { From 7cffc548aecdf588153f3c1d11b1f6742403875d Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 23 Mar 2026 16:05:07 -0300 Subject: [PATCH 080/114] WIP: Fixing UTXO issue on Mint --- src/account/TransactionManager.cpp | 412 ++++-------------- src/blockchain/Blockchain.hpp | 8 +- src/blockchain/Consensus.cpp | 17 +- src/blockchain/Consensus.hpp | 4 +- src/blockchain/impl/Blockchain.cpp | 8 +- .../blockchain/consensus_certificate_test.cpp | 46 +- 6 files changed, 141 insertions(+), 354 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index b65ca1adf..75d5f14e9 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -54,82 +54,28 @@ namespace sgns } outputs.clear(); - if ( tx->GetType() == "transfer" ) + if ( !tx->HasUTXOParameters() ) { - auto transfer_tx = std::dynamic_pointer_cast( tx ); - if ( !transfer_tx ) - { - return false; - } - const auto &dst_infos = transfer_tx->GetDstInfos(); - 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; - } - - if ( tx->GetType() == "mint" ) - { - auto mint_tx = std::dynamic_pointer_cast( tx ); - if ( !mint_tx ) - { - return false; - } - outputs.emplace_back( tx_hash.value(), - 0, - mint_tx->GetAmount(), - mint_tx->GetTokenID(), - mint_tx->GetSrcAddress() ); - return true; + return false; } - if ( tx->GetType() == "escrow-hold" ) + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) { - auto escrow_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_tx ) - { - return false; - } - const auto &dst_infos = escrow_tx->GetUTXOParameters().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; + return false; } - if ( tx->GetType() == "escrow-release" ) + const auto &dst_infos = params_opt->second; + outputs.reserve( dst_infos.size() ); + for ( std::uint32_t i = 0; i < dst_infos.size(); ++i ) { - auto escrow_release_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_release_tx ) - { - return false; - } - const auto &dst_infos = escrow_release_tx->GetUTXOParameters().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; + outputs.emplace_back( tx_hash.value(), + i, + dst_infos[i].encrypted_amount, + dst_infos[i].token_id, + dst_infos[i].dest_address ); } - - return false; + return true; } } // namespace @@ -982,12 +928,10 @@ namespace sgns { std::optional utxo_commitment = BuildUTXOTransitionCommitment( transaction ); std::optional utxo_witness = BuildUTXOWitness( transaction ); - const bool is_utxo_tx = transaction->GetType() == "transfer" || transaction->GetType() == "escrow-hold" || - transaction->GetType() == "escrow-release"; - if ( is_utxo_tx && ( !utxo_commitment.has_value() || !utxo_witness.has_value() ) ) + if ( !utxo_commitment.has_value() || !utxo_witness.has_value() ) { - TransactionManagerLogger()->error( - "[{} - full: {}] {}: Missing UTXO commitment/witness for tx={} type={}", + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing required UTXO data for tx={} type={} " + "(commitment_required=true, witness_required=true)", account_m->GetAddress().substr( 0, 8 ), full_node_m, __func__, @@ -999,8 +943,8 @@ namespace sgns blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), transaction->GetNonce(), transaction->GetHash(), - utxo_commitment ? &utxo_commitment.value() : nullptr, - utxo_witness ? &utxo_witness.value() : nullptr ) ); + utxo_commitment.value(), + utxo_witness.value() ) ); OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); @@ -3343,10 +3287,7 @@ namespace sgns tx->GetHash() ); return false; } - //TODO - Deal with checking the Mint - //TODO - Make all transactions have UTXOs maybe. - const bool is_utxo_type = tx->GetType() == "transfer" || tx->GetType() == "escrow-hold" || - tx->GetType() == "escrow-release"; + const bool is_utxo_type = tx->HasUTXOParameters(); if ( !( skip_utxo_state_validation && is_utxo_type ) && !CheckTransactionTypeRules( tx ) ) { TransactionManagerLogger()->error( "[{} - full: {}] {}: Type rules failed tx={}", @@ -3613,71 +3554,19 @@ namespace sgns return false; } - if ( tx->GetType() == "transfer" ) - { - auto transfer_tx = std::dynamic_pointer_cast( tx ); - if ( !transfer_tx ) - { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast transfer tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); - return false; - } - return ValidateUTXOParametersForConsensus( - UTXOTxParameters{ transfer_tx->GetInputInfos(), transfer_tx->GetDstInfos() }, - transfer_tx->GetSrcAddress() ); - } - - if ( tx->GetType() == "escrow-hold" ) - { - auto escrow_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_tx ) - { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast escrow-hold tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); - return false; - } - return ValidateUTXOParametersForConsensus( escrow_tx->GetUTXOParameters(), escrow_tx->GetSrcAddress() ); - } - - if ( tx->GetType() == "escrow-release" ) + if ( tx->HasUTXOParameters() ) { - auto escrow_release_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_release_tx ) + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast escrow-release tx", + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing UTXO parameters for tx={}", account_m->GetAddress().substr( 0, 8 ), full_node_m, - __func__ ); - return false; - } - return ValidateUTXOParametersForConsensus( escrow_release_tx->GetUTXOParameters(), - escrow_release_tx->GetSrcAddress() ); - } - - if ( tx->GetType() == "mint" ) - { - auto mint_tx = std::dynamic_pointer_cast( tx ); - if ( !mint_tx ) - { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to cast mint tx", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); - return false; - } - if ( mint_tx->GetAmount() == 0 ) - { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Mint amount is zero", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__ ); + __func__, + tx->GetHash() ); return false; } - return true; + return ValidateUTXOParametersForConsensus( params_opt.value(), tx->GetSrcAddress() ); } return true; @@ -3696,44 +3585,11 @@ namespace sgns return true; } - std::vector inputs; - if ( tx->GetType() == "transfer" ) - { - auto transfer_tx = std::dynamic_pointer_cast( tx ); - if ( !transfer_tx ) - { - return false; - } - inputs = transfer_tx->GetInputInfos(); - } - else if ( tx->GetType() == "escrow-hold" ) - { - auto escrow_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_tx ) - { - return false; - } - inputs = escrow_tx->GetUTXOParameters().first; - } - else if ( tx->GetType() == "escrow-release" ) - { - auto escrow_release_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_release_tx ) - { - return false; - } - inputs = escrow_release_tx->GetUTXOParameters().first; - } - else - { - return true; - } - - if ( inputs.empty() ) + if ( !subject.nonce().has_utxo_commitment() ) { return false; } - if ( !subject.nonce().has_utxo_commitment() || !subject.nonce().has_utxo_witness() ) + if ( !subject.nonce().has_utxo_witness() ) { return false; } @@ -3773,7 +3629,7 @@ namespace sgns return false; } const auto &prev_subject = prev_cert_result.value().proposal().subject(); - if ( !prev_subject.has_nonce() ) + if ( !prev_subject.has_nonce() || !prev_subject.nonce().has_utxo_commitment() ) { return false; } @@ -3786,21 +3642,35 @@ namespace sgns return false; } - if ( prev_subject.nonce().has_utxo_commitment() ) + const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); + auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( + reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); + if ( prev_post_root_result.has_error() ) { - const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( - reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); - if ( prev_post_root_result.has_error() ) - { - return false; - } - if ( prev_post_root_result.value() != pre_root ) - { - return false; - } + return false; } + if ( prev_post_root_result.value() != pre_root ) + { + return false; + } + } + + if ( !tx->HasUTXOParameters() ) + { + return false; + } + + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + return false; + } + + const auto &inputs = params_opt->first; + if ( inputs.empty() ) + { + return false; } std::unordered_map proofs; @@ -3820,35 +3690,7 @@ namespace sgns } } - std::vector outputs; - if ( tx->GetType() == "transfer" ) - { - auto transfer_tx = std::dynamic_pointer_cast( tx ); - if ( !transfer_tx ) - { - return false; - } - outputs = transfer_tx->GetDstInfos(); - } - else if ( tx->GetType() == "escrow-hold" ) - { - auto escrow_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_tx ) - { - return false; - } - outputs = escrow_tx->GetUTXOParameters().second; - } - else if ( tx->GetType() == "escrow-release" ) - { - auto escrow_release_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_release_tx ) - { - return false; - } - outputs = escrow_release_tx->GetUTXOParameters().second; - } - + const auto &outputs = params_opt->second; if ( outputs.empty() ) { return false; @@ -4094,7 +3936,18 @@ namespace sgns return std::nullopt; } - if ( previous_subject.nonce().has_utxo_commitment() ) + if ( !previous_subject.nonce().has_utxo_commitment() ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Previous subject missing mandatory UTXO commitment tx={} prev={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + previous_hash ); + return std::nullopt; + } + { const auto &prev_commitment = previous_subject.nonce().utxo_commitment(); auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( @@ -4142,43 +3995,17 @@ namespace sgns return std::nullopt; } - std::vector inputs; - if ( tx->GetType() == "transfer" ) - { - auto transfer_tx = std::dynamic_pointer_cast( tx ); - if ( !transfer_tx ) - { - return std::nullopt; - } - inputs = transfer_tx->GetInputInfos(); - } - else if ( tx->GetType() == "escrow-hold" ) - { - auto escrow_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_tx ) - { - return std::nullopt; - } - inputs = escrow_tx->GetUTXOParameters().first; - } - else if ( tx->GetType() == "escrow-release" ) - { - auto escrow_release_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_release_tx ) - { - return std::nullopt; - } - inputs = escrow_release_tx->GetUTXOParameters().first; - } - else + if ( !tx->HasUTXOParameters() ) { return std::nullopt; } - if ( inputs.empty() ) + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) { return std::nullopt; } + const auto &inputs = params_opt->first; struct SnapshotLeaf { @@ -4349,91 +4176,30 @@ namespace sgns return false; } - if ( tx->GetType() == "transfer" ) - { - auto transfer_tx = std::dynamic_pointer_cast( tx ); - if ( !transfer_tx ) - { - return false; - } - remove_inputs( transfer_tx->GetInputInfos() ); - const auto &outputs = transfer_tx->GetDstInfos(); - 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; - } - - if ( tx->GetType() == "mint" ) + if ( !tx->HasUTXOParameters() ) { - auto mint_tx = std::dynamic_pointer_cast( tx ); - if ( !mint_tx ) - { - return false; - } - snapshot.emplace_back( tx_hash.value(), - 0, - mint_tx->GetAmount(), - mint_tx->GetTokenID(), - tx->GetSrcAddress() ); - return true; + return false; } - if ( tx->GetType() == "escrow-hold" ) + auto params_opt = tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) { - auto escrow_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_tx ) - { - return false; - } - auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); - 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; + return false; } - - if ( tx->GetType() == "escrow-release" ) + const auto &[inputs, outputs] = params_opt.value(); + remove_inputs( inputs ); + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) { - auto escrow_release_tx = std::dynamic_pointer_cast( tx ); - if ( !escrow_release_tx ) - { - return false; - } - auto [inputs, outputs] = escrow_release_tx->GetUTXOParameters(); - remove_inputs( inputs ); - for ( std::uint32_t i = 0; i < outputs.size(); ++i ) + if ( outputs[i].dest_address == tx->GetSrcAddress() ) { - if ( outputs[i].dest_address == tx->GetSrcAddress() ) - { - snapshot.emplace_back( tx_hash.value(), - i, - outputs[i].encrypted_amount, - outputs[i].token_id, - tx->GetSrcAddress() ); - } + snapshot.emplace_back( tx_hash.value(), + i, + outputs[i].encrypted_amount, + outputs[i].token_id, + tx->GetSrcAddress() ); } - return true; } - - return false; + return true; } void TransactionManager::SetNonceWindow( uint64_t window ) diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index a4c1190c8..5f5751551 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -114,14 +114,14 @@ namespace sgns outcome::result CreateConsensusNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment *utxo_commitment = nullptr, - const UTXOWitness *utxo_witness = nullptr ); + const UTXOTransitionCommitment &utxo_commitment, + const UTXOWitness &utxo_witness ); outcome::result CreateConsensusProposal( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment *utxo_commitment = nullptr, - const UTXOWitness *utxo_witness = nullptr ); + const UTXOTransitionCommitment &utxo_commitment, + const UTXOWitness &utxo_witness ); outcome::result SubmitProposal( const ConsensusManager::Proposal &proposal ); diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index c6616628e..91d8dbc36 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -1951,8 +1951,8 @@ namespace sgns outcome::result ConsensusManager::CreateNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment *utxo_commitment, - const UTXOWitness *utxo_witness ) + const UTXOTransitionCommitment &utxo_commitment, + const UTXOWitness &utxo_witness ) { ConsensusManagerLogger()->trace( "{}: called account_id={} nonce={}", __func__, account_id, nonce ); Subject subject; @@ -1961,14 +1961,8 @@ namespace sgns auto *payload = subject.mutable_nonce(); payload->set_nonce( nonce ); payload->set_tx_hash( tx_hash.data(), tx_hash.size() ); - if ( utxo_commitment != nullptr ) - { - *payload->mutable_utxo_commitment() = *utxo_commitment; - } - if ( utxo_witness != nullptr ) - { - *payload->mutable_utxo_witness() = *utxo_witness; - } + *payload->mutable_utxo_commitment() = utxo_commitment; + *payload->mutable_utxo_witness() = utxo_witness; auto subject_id = ComputeSubjectId( subject ); if ( subject_id.has_error() ) @@ -2058,7 +2052,8 @@ namespace sgns switch ( subject.type() ) { case SubjectType::SUBJECT_NONCE: - return subject.has_nonce() && !subject.nonce().tx_hash().empty(); + return subject.has_nonce() && !subject.nonce().tx_hash().empty() && + subject.nonce().has_utxo_commitment() && subject.nonce().has_utxo_witness(); case SubjectType::SUBJECT_TASK_RESULT: return subject.has_task_result() && !subject.task_result().task_result_hash().empty(); case SubjectType::SUBJECT_UNSPECIFIED: diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 888de70b0..e7e134053 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -139,8 +139,8 @@ namespace sgns static outcome::result CreateNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment *utxo_commitment = nullptr, - const UTXOWitness *utxo_witness = nullptr ); + const UTXOTransitionCommitment &utxo_commitment, + const UTXOWitness &utxo_witness ); static outcome::result CreateTaskResultSubject( const std::string &account_id, const std::string &escrow_path, const std::string &task_result_hash, diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 23afe20e9..b3d667bf9 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1557,8 +1557,8 @@ namespace sgns outcome::result Blockchain::CreateConsensusNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment *utxo_commitment, - const UTXOWitness *utxo_witness ) + const UTXOTransitionCommitment &utxo_commitment, + const UTXOWitness &utxo_witness ) { return consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ); } @@ -1566,8 +1566,8 @@ namespace sgns outcome::result Blockchain::CreateConsensusProposal( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment *utxo_commitment, - const UTXOWitness *utxo_witness ) + const UTXOTransitionCommitment &utxo_commitment, + const UTXOWitness &utxo_witness ) { OUTCOME_TRY( auto &&nonce_subject, CreateConsensusNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ) ); diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index 6a694a0d9..67a0da839 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -74,6 +74,24 @@ namespace EXPECT_TRUE( manager ); return manager; } + + sgns::UTXOTransitionCommitment MakeTestCommitment() + { + sgns::UTXOTransitionCommitment commitment; + commitment.set_pre_utxo_root( std::string( 32, '\x01' ) ); + commitment.set_post_utxo_root( std::string( 32, '\x02' ) ); + commitment.set_utxo_count_before( 1 ); + commitment.set_utxo_count_after( 1 ); + commitment.set_account_state_version( 0 ); + commitment.set_produced_outputs_root( std::string( 32, '\x04' ) ); + return commitment; + } + + sgns::UTXOWitness MakeTestWitness() + { + sgns::UTXOWitness witness; + return witness; + } } namespace sgns::test @@ -97,7 +115,8 @@ namespace sgns::test auto manager = MakeManager( registry, db_, pubs_, account ); std::string tx_hash = "0x010203"; - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash ); + 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(), @@ -129,7 +148,8 @@ namespace sgns::test auto manager = MakeManager( registry, db_, pubs_, account ); std::string tx_hash = "0x010203"; - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash ); + 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(), @@ -237,7 +257,8 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 2, "0x0a0b0c" ); + 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(), @@ -296,7 +317,8 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 3, "0x111213" ); + 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(), @@ -346,7 +368,8 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 4, "0x222324" ); + 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(), @@ -387,7 +410,8 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 5, "0x333435" ); + 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(), @@ -421,7 +445,8 @@ namespace sgns::test auto manager = MakeManager( registry, db_, pubs_, account ); std::string tx_hash = "0x444546"; - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 6, tx_hash ); + 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(), @@ -471,7 +496,8 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), 11, "0xabc123" ); + auto subject_result = ConsensusManager::CreateNonceSubject( + account->GetAddress(), 11, "0xabc123", MakeTestCommitment(), MakeTestWitness() ); ASSERT_TRUE( subject_result.has_value() ); auto subject = subject_result.value(); @@ -504,8 +530,8 @@ namespace sgns::test account->GetAddress(), 1, "0xdeadbeef", - &commitment, - &witness ); + commitment, + witness ); ASSERT_TRUE( subject_result.has_value() ); auto subject = subject_result.value(); From 11c43f485841a520f047e8295c29a631e2dc6066 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Mar 2026 12:06:30 -0300 Subject: [PATCH 081/114] Feat: Creating supergenius chain id --- src/account/IGeniusTransactions.hpp | 11 +++++++++++ src/account/MintTransaction.cpp | 5 +++++ src/account/MintTransaction.hpp | 2 ++ src/account/MintTransactionV2.cpp | 5 +++++ src/account/MintTransactionV2.hpp | 6 ++++++ 5 files changed, 29 insertions(+) diff --git a/src/account/IGeniusTransactions.hpp b/src/account/IGeniusTransactions.hpp index 4be96b59a..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 */ @@ -82,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 ) diff --git a/src/account/MintTransaction.cpp b/src/account/MintTransaction.cpp index bffcf31e8..5d53f6a1b 100644 --- a/src/account/MintTransaction.cpp +++ b/src/account/MintTransaction.cpp @@ -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 348d19024..8192b9813 100644 --- a/src/account/MintTransaction.hpp +++ b/src/account/MintTransaction.hpp @@ -34,6 +34,8 @@ namespace sgns 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 eebc12b3d..e27ac646a 100644 --- a/src/account/MintTransactionV2.cpp +++ b/src/account/MintTransactionV2.cpp @@ -135,6 +135,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_; diff --git a/src/account/MintTransactionV2.hpp b/src/account/MintTransactionV2.hpp index 538afa1c6..f684ea9b0 100644 --- a/src/account/MintTransactionV2.hpp +++ b/src/account/MintTransactionV2.hpp @@ -67,6 +67,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 From 101482f004b89fd8a1e1ae3d58470535f8c3d531 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Mar 2026 12:09:49 -0300 Subject: [PATCH 082/114] Feat: Decoupling of input validation --- src/account/CMakeLists.txt | 1 + src/account/InputValidators.cpp | 279 +++++++++++++++++++++++++++++ src/account/InputValidators.hpp | 63 +++++++ src/account/TransactionManager.cpp | 251 ++++---------------------- src/account/TransactionManager.hpp | 10 +- 5 files changed, 391 insertions(+), 213 deletions(-) create mode 100644 src/account/InputValidators.cpp create mode 100644 src/account/InputValidators.hpp 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/InputValidators.cpp b/src/account/InputValidators.cpp new file mode 100644 index 000000000..e9dff1e5e --- /dev/null +++ b/src/account/InputValidators.cpp @@ -0,0 +1,279 @@ +/** + * @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::ReadUInt32BE; + using utxo_merkle::ReadUInt64BE; + } // 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 base::Hash256 &pre_root, + const std::shared_ptr &blockchain ) const + { + if ( !tx || !blockchain ) + { + return false; + } + if ( !subject.has_nonce() || !subject.nonce().has_utxo_witness() ) + { + return false; + } + + const auto &inputs = params.first; + const auto &outputs = params.second; + if ( inputs.empty() || outputs.empty() ) + { + 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 current_hash = HashLeaf( payload_vec ); + for ( const auto &step : proof.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() ) + { + current_hash = HashNode( sibling_hash_result.value(), current_hash ); + } + else + { + current_hash = HashNode( current_hash, sibling_hash_result.value() ); + } + } + + if ( current_hash != pre_root ) + { + return false; + } + + 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 base::Hash256 &pre_root, + const std::shared_ptr &blockchain ) const + { + (void)subject; + (void)tx; + (void)pre_root; + (void)blockchain; + // Placeholder: external proof verification (burn receipt/header finality/replay protection) + // will live here. For now we only assert explicit input/output presence. + return !params.first.empty() && !params.second.empty(); + } +} // namespace sgns + diff --git a/src/account/InputValidators.hpp b/src/account/InputValidators.hpp new file mode 100644 index 000000000..5c73be4f7 --- /dev/null +++ b/src/account/InputValidators.hpp @@ -0,0 +1,63 @@ +/** + * @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 base::Hash256 &pre_root, + const std::shared_ptr &blockchain ) 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 base::Hash256 &pre_root, + const std::shared_ptr &blockchain ) const override; + }; + + 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 base::Hash256 &pre_root, + const std::shared_ptr &blockchain ) const override; + }; +} // namespace sgns + diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 75d5f14e9..431910ed8 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -784,6 +784,30 @@ namespace sgns return it->second; } + 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() ) + { + 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_; + } + void TransactionManager::RecordOutgoingTxHash( uint64_t nonce, const std::string &hash ) { std::lock_guard lock( outgoing_tx_hash_mutex_ ); @@ -932,11 +956,11 @@ namespace sgns { TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing required UTXO data for tx={} type={} " "(commitment_required=true, witness_required=true)", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - transaction->GetHash(), - transaction->GetType() ); + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + transaction->GetHash(), + transaction->GetType() ); return outcome::failure( std::errc::invalid_argument ); } OUTCOME_TRY( auto &&proposal, @@ -3287,6 +3311,7 @@ namespace sgns tx->GetHash() ); return false; } + //TODO - Deal with checking the Mint const bool is_utxo_type = tx->HasUTXOParameters(); if ( !( skip_utxo_state_validation && is_utxo_type ) && !CheckTransactionTypeRules( tx ) ) { @@ -3566,7 +3591,9 @@ namespace sgns tx->GetHash() ); return false; } - return ValidateUTXOParametersForConsensus( params_opt.value(), tx->GetSrcAddress() ); + const auto chain_id = GetValidationChainId( tx ); + const auto &validator = GetInputValidator( chain_id ); + return validator.ValidateUTXOParameters( params_opt.value(), tx->GetSrcAddress(), utxo_manager_ ); } return true; @@ -3643,9 +3670,9 @@ namespace sgns } const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( - reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); + auto prev_post_root_result = base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), + prev_commitment.post_utxo_root().size() ) ); if ( prev_post_root_result.has_error() ) { return false; @@ -3667,209 +3694,9 @@ namespace sgns return false; } - const auto &inputs = params_opt->first; - if ( inputs.empty() ) - { - 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 &outputs = params_opt->second; - if ( outputs.empty() ) - { - 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 current_hash = HashLeaf( payload_vec ); - for ( const auto &step : proof.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() ) - { - current_hash = HashNode( sibling_hash_result.value(), current_hash ); - } - else - { - current_hash = HashNode( current_hash, sibling_hash_result.value() ); - } - } - - if ( current_hash != pre_root ) - { - return false; - } - - 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; + const auto chain_id = GetValidationChainId( tx ); + const auto &validator = GetInputValidator( chain_id ); + return validator.ValidateWitness( subject, tx, params_opt.value(), pre_root, blockchain_ ); } std::optional TransactionManager::BuildUTXOTransitionCommitment( diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 15332c8cc..ac703f4a0 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -24,6 +24,7 @@ #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" @@ -393,11 +394,18 @@ namespace sgns 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 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_; }; } From 38f562c390325f2e6abbeca9457311725acc3104 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Mar 2026 12:54:33 -0300 Subject: [PATCH 083/114] Fix: Added signature on MintV2 UTXO input --- src/account/GeniusAccount.cpp | 17 +++++++++++++++++ src/account/GeniusAccount.hpp | 9 +++++++++ src/account/MintTransactionV2.cpp | 20 +------------------- src/account/MintTransactionV2.hpp | 2 ++ src/account/TransactionManager.cpp | 19 ++++++++++++++++++- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/account/GeniusAccount.cpp b/src/account/GeniusAccount.cpp index f56099d7f..a7cc0942e 100644 --- a/src/account/GeniusAccount.cpp +++ b/src/account/GeniusAccount.cpp @@ -598,6 +598,23 @@ namespace sgns return signed_vector; } + std::vector GeniusAccount::CreateInputsFromUTXOs( const std::vector &utxos ) const + { + 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 ) { std::unique_lock lock( nonce_mutex_ ); diff --git a/src/account/GeniusAccount.hpp b/src/account/GeniusAccount.hpp index 1e42e3ff1..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" @@ -153,6 +155,13 @@ namespace sgns */ std::vector Sign( const std::vector &data ) const; + /** + * @brief Build signed transaction inputs from UTXOs + * @param[in] utxos UTXOs to turn into transaction inputs + * @return Signed input descriptors + */ + std::vector CreateInputsFromUTXOs( const std::vector &utxos ) const; + /** * @brief Set the local confirmed nonce for a peer * @param[in] nonce The nonce value to be set diff --git a/src/account/MintTransactionV2.cpp b/src/account/MintTransactionV2.cpp index e27ac646a..34056f0e9 100644 --- a/src/account/MintTransactionV2.cpp +++ b/src/account/MintTransactionV2.cpp @@ -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, @@ -169,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 f684ea9b0..ec53f126c 100644 --- a/src/account/MintTransactionV2.hpp +++ b/src/account/MintTransactionV2.hpp @@ -40,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 */ @@ -47,6 +48,7 @@ namespace sgns std::string chain_id, TokenID token_id, SGTransaction::DAGStruct dag, + std::vector mint_inputs, std::string mint_destination ); /** diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 431910ed8..0cb9ad164 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -571,11 +571,28 @@ namespace sgns { destination = account_m->GetAddress(); } + + auto source_hash = base::Hash256::fromReadableString( transaction_hash ); + if ( source_hash.has_error() ) + { + TransactionManagerLogger()->error( "[{} - full: {}] {}: Invalid source transaction hash for mint: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + transaction_hash ); + return outcome::failure( std::errc::invalid_argument ); + } + + std::vector source_utxos; + source_utxos.emplace_back( source_hash.value(), 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 ) ), + FillDAGStruct(), + std::move( mint_inputs ), destination ) ); mint_transaction->MakeSignature( *account_m ); From 66275c98c196bc2adfe924318a36567213fb7df3 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Mar 2026 14:24:31 -0300 Subject: [PATCH 084/114] Fix: Setting UTXO commitment and witness as optional for mints --- src/account/TransactionManager.cpp | 2 +- src/blockchain/Blockchain.hpp | 8 ++++---- src/blockchain/Consensus.cpp | 25 +++++++++++++++++++------ src/blockchain/Consensus.hpp | 4 ++-- src/blockchain/impl/Blockchain.cpp | 8 ++++---- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 0cb9ad164..13e9eace7 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -591,7 +591,7 @@ namespace sgns MintTransactionV2::New( amount, std::move( chainid ), std::move( tokenid ), - FillDAGStruct(), + FillDAGStruct( std::move( transaction_hash ) ), std::move( mint_inputs ), destination ) ); diff --git a/src/blockchain/Blockchain.hpp b/src/blockchain/Blockchain.hpp index 5f5751551..147b8bd4a 100644 --- a/src/blockchain/Blockchain.hpp +++ b/src/blockchain/Blockchain.hpp @@ -114,14 +114,14 @@ namespace sgns outcome::result CreateConsensusNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment &utxo_commitment, - const UTXOWitness &utxo_witness ); + 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 UTXOTransitionCommitment &utxo_commitment, - const UTXOWitness &utxo_witness ); + const std::optional &utxo_commitment, + const std::optional &utxo_witness ); outcome::result SubmitProposal( const ConsensusManager::Proposal &proposal ); diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 91d8dbc36..c2b0e7dd4 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -1951,8 +1951,8 @@ namespace sgns outcome::result ConsensusManager::CreateNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment &utxo_commitment, - const UTXOWitness &utxo_witness ) + const std::optional &utxo_commitment, + const std::optional &utxo_witness ) { ConsensusManagerLogger()->trace( "{}: called account_id={} nonce={}", __func__, account_id, nonce ); Subject subject; @@ -1961,8 +1961,14 @@ namespace sgns auto *payload = subject.mutable_nonce(); payload->set_nonce( nonce ); payload->set_tx_hash( tx_hash.data(), tx_hash.size() ); - *payload->mutable_utxo_commitment() = utxo_commitment; - *payload->mutable_utxo_witness() = utxo_witness; + 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() ) @@ -2052,8 +2058,15 @@ namespace sgns switch ( subject.type() ) { case SubjectType::SUBJECT_NONCE: - return subject.has_nonce() && !subject.nonce().tx_hash().empty() && - subject.nonce().has_utxo_commitment() && subject.nonce().has_utxo_witness(); + if ( !subject.has_nonce() || subject.nonce().tx_hash().empty() ) + { + return false; + } + if ( subject.nonce().has_utxo_commitment() != subject.nonce().has_utxo_witness() ) + { + return false; + } + return true; case SubjectType::SUBJECT_TASK_RESULT: return subject.has_task_result() && !subject.task_result().task_result_hash().empty(); case SubjectType::SUBJECT_UNSPECIFIED: diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index e7e134053..eb3dbd01e 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -139,8 +139,8 @@ namespace sgns static outcome::result CreateNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment &utxo_commitment, - const UTXOWitness &utxo_witness ); + 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, diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index b3d667bf9..ae38c3e5e 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1557,8 +1557,8 @@ namespace sgns outcome::result Blockchain::CreateConsensusNonceSubject( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment &utxo_commitment, - const UTXOWitness &utxo_witness ) + const std::optional &utxo_commitment, + const std::optional &utxo_witness ) { return consensus_manager_->CreateNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ); } @@ -1566,8 +1566,8 @@ namespace sgns outcome::result Blockchain::CreateConsensusProposal( const std::string &account_id, uint64_t nonce, const std::string &tx_hash, - const UTXOTransitionCommitment &utxo_commitment, - const UTXOWitness &utxo_witness ) + 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 ) ); From 8f1799d3e73a300c8b81829e8e34aa691419ffd5 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Mar 2026 14:53:34 -0300 Subject: [PATCH 085/114] Fix: Checking the previous certificate for mints too --- src/account/InputValidators.cpp | 38 +++++++-- src/account/InputValidators.hpp | 17 +++- src/account/TransactionManager.cpp | 129 ++++++++++++++++++----------- 3 files changed, 129 insertions(+), 55 deletions(-) diff --git a/src/account/InputValidators.cpp b/src/account/InputValidators.cpp index e9dff1e5e..057b70a7d 100644 --- a/src/account/InputValidators.cpp +++ b/src/account/InputValidators.cpp @@ -268,12 +268,40 @@ namespace sgns const std::shared_ptr &blockchain ) const { (void)subject; - (void)tx; (void)pre_root; (void)blockchain; - // Placeholder: external proof verification (burn receipt/header finality/replay protection) - // will live here. For now we only assert explicit input/output presence. - return !params.first.empty() && !params.second.empty(); + 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(); + } + + if ( source_reference.empty() ) + { + return false; + } + + return VerifyPublicChainSmartContract( tx, source_reference ); } -} // namespace sgns + 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. + return true; + } +} // namespace sgns diff --git a/src/account/InputValidators.hpp b/src/account/InputValidators.hpp index 5c73be4f7..4f28a38e9 100644 --- a/src/account/InputValidators.hpp +++ b/src/account/InputValidators.hpp @@ -30,6 +30,8 @@ namespace sgns const UTXOTxParameters ¶ms, const base::Hash256 &pre_root, const std::shared_ptr &blockchain ) const = 0; + + virtual bool RequiresConsensusUTXOData() const = 0; }; class GeniusInputValidator final : public IInputValidator @@ -44,6 +46,11 @@ namespace sgns const UTXOTxParameters ¶ms, const base::Hash256 &pre_root, const std::shared_ptr &blockchain ) const override; + + bool RequiresConsensusUTXOData() const override + { + return true; + } }; class PublicChainInputValidator final : public IInputValidator @@ -58,6 +65,14 @@ namespace sgns const UTXOTxParameters ¶ms, const base::Hash256 &pre_root, 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/TransactionManager.cpp b/src/account/TransactionManager.cpp index 13e9eace7..d7377ac36 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -573,18 +573,23 @@ namespace sgns } auto source_hash = base::Hash256::fromReadableString( transaction_hash ); + base::Hash256 source_input_hash; if ( source_hash.has_error() ) { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Invalid source transaction hash for mint: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - transaction_hash ); - return outcome::failure( std::errc::invalid_argument ); + 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_hash.value(), 0, amount, tokenid, account_m->GetAddress() ); + 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( @@ -967,25 +972,37 @@ namespace sgns for ( auto &transaction : transactions_sent ) { - std::optional utxo_commitment = BuildUTXOTransitionCommitment( transaction ); - std::optional utxo_witness = BuildUTXOWitness( transaction ); - if ( !utxo_commitment.has_value() || !utxo_witness.has_value() ) + 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 ( utxo_data_required ) { - TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing required UTXO data for tx={} type={} " - "(commitment_required=true, witness_required=true)", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - transaction->GetHash(), - transaction->GetType() ); - return outcome::failure( std::errc::invalid_argument ); + utxo_commitment = BuildUTXOTransitionCommitment( transaction ); + utxo_witness = BuildUTXOWitness( transaction ); + if ( !utxo_commitment.has_value() || !utxo_witness.has_value() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] {}: Missing required UTXO data for tx={} type={} " + "(commitment_required=true, witness_required=true)", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + transaction->GetHash(), + transaction->GetType() ); + return outcome::failure( std::errc::invalid_argument ); + } } + OUTCOME_TRY( auto &&proposal, blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), transaction->GetNonce(), transaction->GetHash(), - utxo_commitment.value(), - utxo_witness.value() ) ); + utxo_commitment, + utxo_witness ) ); OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); @@ -3608,8 +3625,8 @@ namespace sgns tx->GetHash() ); return false; } - const auto chain_id = GetValidationChainId( tx ); - const auto &validator = GetInputValidator( chain_id ); + const auto chain_id = GetValidationChainId( tx ); + const auto &validator = GetInputValidator( chain_id ); return validator.ValidateUTXOParameters( params_opt.value(), tx->GetSrcAddress(), utxo_manager_ ); } @@ -3629,6 +3646,45 @@ namespace sgns return true; } + const auto chain_id = GetValidationChainId( tx ); + const auto &validator = GetInputValidator( chain_id ); + + // For nonce > 0, anchor pre-state root in the certified per-account chain. + const ConsensusSubject *prev_subject_ptr = nullptr; + if ( tx->GetNonce() > 0 ) + { + const auto prev_hash = tx->GetPreviousHash(); + if ( prev_hash.empty() ) + { + return false; + } + + auto prev_cert_result = blockchain_->GetCertificateBySubjectHash( prev_hash ); + if ( prev_cert_result.has_error() ) + { + return false; + } + const auto &prev_subject = prev_cert_result.value().proposal().subject(); + if ( !prev_subject.has_nonce() ) + { + return false; + } + if ( prev_subject.account_id() != tx->GetSrcAddress() ) + { + return false; + } + if ( prev_subject.nonce().nonce() + 1 != tx->GetNonce() ) + { + return false; + } + prev_subject_ptr = &prev_subject; + } + + if ( !validator.RequiresConsensusUTXOData() ) + { + return true; + } + if ( !subject.nonce().has_utxo_commitment() ) { return false; @@ -3658,35 +3714,13 @@ namespace sgns } const auto pre_root = pre_root_result.value(); - // For nonce > 0, anchor pre-state root in the certified per-account chain. if ( tx->GetNonce() > 0 ) { - const auto prev_hash = tx->GetPreviousHash(); - if ( prev_hash.empty() ) + if ( !prev_subject_ptr || !prev_subject_ptr->nonce().has_utxo_commitment() ) { return false; } - - auto prev_cert_result = blockchain_->GetCertificateBySubjectHash( prev_hash ); - if ( prev_cert_result.has_error() ) - { - return false; - } - const auto &prev_subject = prev_cert_result.value().proposal().subject(); - if ( !prev_subject.has_nonce() || !prev_subject.nonce().has_utxo_commitment() ) - { - return false; - } - if ( prev_subject.account_id() != tx->GetSrcAddress() ) - { - return false; - } - if ( prev_subject.nonce().nonce() + 1 != tx->GetNonce() ) - { - return false; - } - - const auto &prev_commitment = prev_subject.nonce().utxo_commitment(); + const auto &prev_commitment = prev_subject_ptr->nonce().utxo_commitment(); auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), prev_commitment.post_utxo_root().size() ) ); @@ -3710,9 +3744,6 @@ namespace sgns { return false; } - - const auto chain_id = GetValidationChainId( tx ); - const auto &validator = GetInputValidator( chain_id ); return validator.ValidateWitness( subject, tx, params_opt.value(), pre_root, blockchain_ ); } From 22aa892c61967183691891f2b6240f596f6fb35b Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Mar 2026 15:44:09 -0300 Subject: [PATCH 086/114] Fix: MintV2 working with current consensus implementation --- src/account/InputValidators.cpp | 6 +---- src/account/TransactionManager.cpp | 41 ++++++++++++++++++++++-------- src/blockchain/Consensus.cpp | 4 ++- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/account/InputValidators.cpp b/src/account/InputValidators.cpp index 057b70a7d..85520c368 100644 --- a/src/account/InputValidators.cpp +++ b/src/account/InputValidators.cpp @@ -288,11 +288,6 @@ namespace sgns source_reference = tx->GetUncleHash(); } - if ( source_reference.empty() ) - { - return false; - } - return VerifyPublicChainSmartContract( tx, source_reference ); } @@ -302,6 +297,7 @@ namespace sgns (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/TransactionManager.cpp b/src/account/TransactionManager.cpp index d7377ac36..360981a2d 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -571,6 +571,11 @@ 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; @@ -815,6 +820,10 @@ namespace sgns 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; @@ -979,15 +988,13 @@ namespace sgns std::optional utxo_commitment; std::optional utxo_witness; - if ( utxo_data_required ) + if ( transaction->HasUTXOParameters() ) { utxo_commitment = BuildUTXOTransitionCommitment( transaction ); - utxo_witness = BuildUTXOWitness( transaction ); - if ( !utxo_commitment.has_value() || !utxo_witness.has_value() ) + if ( !utxo_commitment.has_value() ) { TransactionManagerLogger()->error( - "[{} - full: {}] {}: Missing required UTXO data for tx={} type={} " - "(commitment_required=true, witness_required=true)", + "[{} - full: {}] {}: Missing required UTXO commitment for tx={} type={}", account_m->GetAddress().substr( 0, 8 ), full_node_m, __func__, @@ -995,6 +1002,22 @@ namespace sgns transaction->GetType() ); return outcome::failure( std::errc::invalid_argument ); } + + if ( utxo_data_required ) + { + 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 ); + } + } } OUTCOME_TRY( auto &&proposal, @@ -3680,7 +3703,7 @@ namespace sgns prev_subject_ptr = &prev_subject; } - if ( !validator.RequiresConsensusUTXOData() ) + if ( !tx->HasUTXOParameters() ) { return true; } @@ -3689,10 +3712,6 @@ namespace sgns { return false; } - if ( !subject.nonce().has_utxo_witness() ) - { - return false; - } const auto &commitment = subject.nonce().utxo_commitment(); if ( commitment.pre_utxo_root().size() != base::Hash256::size() || @@ -3734,7 +3753,7 @@ namespace sgns } } - if ( !tx->HasUTXOParameters() ) + if ( validator.RequiresConsensusUTXOData() && !subject.nonce().has_utxo_witness() ) { return false; } diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index c2b0e7dd4..59dd84a0f 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -2062,7 +2062,9 @@ namespace sgns { return false; } - if ( subject.nonce().has_utxo_commitment() != subject.nonce().has_utxo_witness() ) + // 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; } From cb02364d5c5f2c8e22322158ce9352fb28e86674 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 24 Mar 2026 17:27:23 -0300 Subject: [PATCH 087/114] Fix: Dangling raw pointer replaced with copy --- src/account/TransactionManager.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 360981a2d..c46f402d0 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -3673,7 +3673,7 @@ namespace sgns const auto &validator = GetInputValidator( chain_id ); // For nonce > 0, anchor pre-state root in the certified per-account chain. - const ConsensusSubject *prev_subject_ptr = nullptr; + std::optional prev_subject; if ( tx->GetNonce() > 0 ) { const auto prev_hash = tx->GetPreviousHash(); @@ -3687,20 +3687,20 @@ namespace sgns { return false; } - const auto &prev_subject = prev_cert_result.value().proposal().subject(); - if ( !prev_subject.has_nonce() ) + const auto &prev_subject_ref = prev_cert_result.value().proposal().subject(); + if ( !prev_subject_ref.has_nonce() ) { return false; } - if ( prev_subject.account_id() != tx->GetSrcAddress() ) + if ( prev_subject_ref.account_id() != tx->GetSrcAddress() ) { return false; } - if ( prev_subject.nonce().nonce() + 1 != tx->GetNonce() ) + if ( prev_subject_ref.nonce().nonce() + 1 != tx->GetNonce() ) { return false; } - prev_subject_ptr = &prev_subject; + prev_subject = prev_subject_ref; } if ( !tx->HasUTXOParameters() ) @@ -3735,11 +3735,11 @@ namespace sgns if ( tx->GetNonce() > 0 ) { - if ( !prev_subject_ptr || !prev_subject_ptr->nonce().has_utxo_commitment() ) + if ( !prev_subject.has_value() || !prev_subject->nonce().has_utxo_commitment() ) { return false; } - const auto &prev_commitment = prev_subject_ptr->nonce().utxo_commitment(); + const auto &prev_commitment = prev_subject->nonce().utxo_commitment(); auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), prev_commitment.post_utxo_root().size() ) ); From 2848466cca033bdb623cceffd45f0da7f44f27a7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 25 Mar 2026 14:16:46 -0300 Subject: [PATCH 088/114] Fix: Decay of the validator increased to 500 transactions --- src/blockchain/ValidatorRegistry.cpp | 46 +++++++++++++++++++++++++++- src/blockchain/ValidatorRegistry.hpp | 2 +- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 2779adf4a..5e40dcd3e 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -445,7 +445,6 @@ namespace sgns std::shared_lock lock( cache_mutex_ ); if ( cached_registry_ ) { - logger_->trace( "{}: returning cached registry", __func__ ); return cached_registry_.value(); } } @@ -1185,6 +1184,13 @@ namespace sgns 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; } } @@ -1203,6 +1209,9 @@ namespace sgns 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 ) @@ -1270,6 +1279,17 @@ namespace sgns } 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() ) ); } } @@ -1298,12 +1318,20 @@ namespace sgns 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() ) ); } } } @@ -1326,6 +1354,8 @@ namespace sgns 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() ); @@ -1335,10 +1365,16 @@ namespace sgns { 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; @@ -1364,6 +1400,14 @@ namespace sgns 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 ) diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index d3641da1e..0f9730e77 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -63,7 +63,7 @@ namespace sgns uint32_t penalty_threshold_ = 10; uint32_t penalty_cap_ = 100; uint32_t blacklist_bump_ = 10; - uint32_t missed_epoch_threshold_ = 5; + 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; From 4f0319701fc70955afc8d50bd33bf3ce0177bbe2 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 25 Mar 2026 14:17:19 -0300 Subject: [PATCH 089/114] Fix: Previous hash now works on boot --- src/account/TransactionManager.cpp | 68 +++++++++++------------------- src/account/TransactionManager.hpp | 4 -- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index c46f402d0..5c421a0bb 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -548,7 +548,6 @@ namespace sgns TransferTransaction::New( inputs, outputs, FillDAGStruct() ) ); transfer_transaction->MakeSignature( *account_m ); - RecordOutgoingTxHash( transfer_transaction->GetNonce(), transfer_transaction->GetHash() ); utxo_manager_.ReserveUTXOs( inputs, transfer_transaction->GetHash() ); @@ -606,7 +605,6 @@ namespace sgns destination ) ); mint_transaction->MakeSignature( *account_m ); - RecordOutgoingTxHash( mint_transaction->GetNonce(), mint_transaction->GetHash() ); // Store the transaction ID before moving the transaction auto txId = mint_transaction->GetHash(); @@ -636,7 +634,6 @@ namespace sgns EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct() ) ); escrow_transaction->MakeSignature( *account_m ); - RecordOutgoingTxHash( escrow_transaction->GetNonce(), escrow_transaction->GetHash() ); utxo_manager_.ReserveUTXOs( inputs, escrow_transaction->GetHash() ); // Get the transaction ID for tracking @@ -723,7 +720,8 @@ namespace sgns TransferTransaction::New( std::vector{ escrow_utxo_input }, payout_peers, FillDAGStruct() ) ); transfer_transaction->MakeSignature( *account_m ); - RecordOutgoingTxHash( transfer_transaction->GetNonce(), transfer_transaction->GetHash() ); + 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(), @@ -731,10 +729,9 @@ namespace sgns escrow_tx->GetDevAddress(), escrow_tx->dag_st.source_addr(), escrow_tx->GetHash(), - FillDAGStruct() ) ); + escrow_release_dag ) ); escrow_release_tx->MakeSignature( *account_m ); - RecordOutgoingTxHash( escrow_release_tx->GetNonce(), escrow_release_tx->GetHash() ); TransactionBatch tx_batch; @@ -802,13 +799,29 @@ namespace sgns { return ""; } - std::lock_guard lock( outgoing_tx_hash_mutex_ ); - auto it = outgoing_tx_hash_by_nonce_.find( nonce - 1 ); - if ( it == outgoing_tx_hash_by_nonce_.end() ) + + std::shared_lock tx_lock( tx_mutex_m ); + for ( const auto &[_, tracked] : tx_processed_m ) { - return ""; + 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 it->second; + return ""; } std::string TransactionManager::GetValidationChainId( const std::shared_ptr &tx ) const @@ -839,35 +852,6 @@ namespace sgns return public_chain_input_validator_; } - void TransactionManager::RecordOutgoingTxHash( uint64_t nonce, const std::string &hash ) - { - std::lock_guard lock( outgoing_tx_hash_mutex_ ); - outgoing_tx_hash_by_nonce_[nonce] = hash; - - if ( nonce_window_m == 0 ) - { - return; - } - const uint64_t min_nonce = ( nonce > ( nonce_window_m + 1 ) ) ? ( nonce - ( nonce_window_m + 1 ) ) : 0; - for ( auto it = outgoing_tx_hash_by_nonce_.begin(); it != outgoing_tx_hash_by_nonce_.end(); ) - { - if ( it->first < min_nonce ) - { - it = outgoing_tx_hash_by_nonce_.erase( it ); - } - else - { - ++it; - } - } - } - - void TransactionManager::RemoveOutgoingTxHash( uint64_t nonce ) - { - std::lock_guard lock( outgoing_tx_hash_mutex_ ); - outgoing_tx_hash_by_nonce_.erase( nonce ); - } - outcome::result TransactionManager::SendTransactionItem( TransactionItem &item ) { auto [transaction_batch, maybe_crdt_transaction] = item; @@ -4298,10 +4282,6 @@ namespace sgns } tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::FAILED, tx->GetNonce() }; account_m->ReleaseNonce( tx->GetNonce() ); - if ( tx->GetSrcAddress() == account_m->GetAddress() ) - { - RemoveOutgoingTxHash( tx->GetNonce() ); - } TransactionManagerLogger()->debug( "[{} - full: {}] {}: Set status of FAILED to transaction {}", account_m->GetAddress().substr( 0, 8 ), diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index ac703f4a0..88c4e7523 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -227,8 +227,6 @@ namespace sgns SGTransaction::DAGStruct FillDAGStruct( std::optional other_chain_hash = std::nullopt ); std::string GetOutgoingPreviousHash( uint64_t nonce ) const; - void RecordOutgoingTxHash( uint64_t nonce, const std::string &hash ); - void RemoveOutgoingTxHash( uint64_t nonce ); outcome::result SendTransactionItem( TransactionItem &item ); outcome::result RollbackTransactions( TransactionItem &item_to_rollback ); @@ -305,8 +303,6 @@ namespace sgns std::chrono::milliseconds timestamp_tolerance_m; std::chrono::milliseconds mutability_window_m; uint64_t nonce_window_m = DEFAULT_NONCE_WINDOW; - mutable std::mutex outgoing_tx_hash_mutex_; - std::unordered_map outgoing_tx_hash_by_nonce_; static constexpr std::chrono::milliseconds TIMESTAMP_TOLERANCE = std::chrono::seconds( 10 ); static constexpr std::chrono::milliseconds MUTABILITY_WINDOW = std::chrono::minutes( 15 ); From 978ffb576da99c199377428ca818ed35da2f29e7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 25 Mar 2026 14:18:13 -0300 Subject: [PATCH 090/114] Fix: Multi account sync test now works with consensus --- test/src/multiaccount/multi_account_sync.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index a9ffe0848..0656408ff 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -130,6 +130,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 @@ -561,7 +563,10 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) epoch_before = registry_state.value().epoch(); cid_before = registry->GetRegistryCid(); - auto transfer1 = node_client->TransferFunds( 75, node_peer1->GetAddress(), TokenID::FromBytes( { 0x00 } ) ); + 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 ); @@ -570,7 +575,10 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) epoch_before = registry_state.value().epoch(); cid_before = registry->GetRegistryCid(); - auto transfer2 = node_client->TransferFunds( 40, node_peer2->GetAddress(), TokenID::FromBytes( { 0x00 } ) ); + 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 ); @@ -579,7 +587,10 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) epoch_before = registry_state.value().epoch(); cid_before = registry->GetRegistryCid(); - auto transfer3 = node_client->TransferFunds( 10, node_peer3->GetAddress(), TokenID::FromBytes( { 0x00 } ) ); + 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" ); From 9a6a8483bee44bd22b62f6476cd3a78811cc872f Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 26 Mar 2026 14:44:18 -0300 Subject: [PATCH 091/114] Feat: Creating registry update subject --- src/blockchain/Consensus.cpp | 121 +++++++++++++++------- src/blockchain/Consensus.hpp | 6 ++ src/blockchain/impl/proto/Consensus.proto | 10 ++ 3 files changed, 100 insertions(+), 37 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 59dd84a0f..fbd5f03e3 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -396,6 +396,14 @@ namespace sgns } 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 ); } @@ -1058,50 +1066,16 @@ namespace sgns crdt::GlobalDB::Buffer cert_value; cert_value.put( serialized ); - auto update_result = registry_->CreateUpdateFromCertificate( certificate ); - if ( update_result.has_error() ) - { - ConsensusManagerLogger()->error( "{}: failed: registry update for hash {} error={}", - __func__, - GetPrintableSubjectHash( certificate.proposal().subject() ), - update_result.error().message() ); - return outcome::failure( update_result.error() ); - } - - auto tx_result = registry_->BeginRegistryUpdateTransaction( update_result.value() ); - if ( tx_result.has_error() ) - { - ConsensusManagerLogger()->error( "{}: failed: begin registry update transaction for hash {} error={}", - __func__, - GetPrintableSubjectHash( certificate.proposal().subject() ), - tx_result.error().message() ); - return outcome::failure( tx_result.error() ); - } - - ConsensusManagerLogger()->debug( "{}: Creating CRDT transaction for registry update for hash {}", - __func__, - GetPrintableSubjectHash( certificate.proposal().subject() ) ); - auto tx = tx_result.value(); - auto cert_put = tx->Put( cert_key, cert_value ); + auto cert_put = db_->Put( cert_key, cert_value, { consensus_datastore_topic_ } ); if ( cert_put.has_error() ) { - ConsensusManagerLogger()->error( "{}: failed: stage certificate put for hash {} error={}", + ConsensusManagerLogger()->error( "{}: failed: cert put for hash {} error={}", __func__, GetPrintableSubjectHash( certificate.proposal().subject() ), cert_put.error().message() ); return outcome::failure( cert_put.error() ); } - auto commit_result = tx->Commit( - { consensus_datastore_topic_, std::string( ValidatorRegistry::ValidatorTopic() ) } ); - if ( commit_result.has_error() ) - { - ConsensusManagerLogger()->error( "{}: failed: transaction commit error={}", - __func__, - commit_result.error().message() ); - return outcome::failure( commit_result.error() ); - } - ConsensusManagerLogger()->debug( "{}: success submitting certificate for {} and proposal_id={}", __func__, GetPrintableSubjectHash( certificate.proposal().subject() ), @@ -1498,6 +1472,8 @@ namespace sgns return; } + registry_->OnFinalizedCertificate( certificate ); + CertificateSubjectHandler handler; { std::shared_lock lock( certificate_handlers_mutex_ ); @@ -2014,6 +1990,43 @@ namespace sgns 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__ ); @@ -2071,6 +2084,10 @@ namespace sgns 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; @@ -2140,7 +2157,8 @@ namespace sgns return false; } - if ( subject.type() != SubjectType::SUBJECT_NONCE && subject.type() != SubjectType::SUBJECT_TASK_RESULT ) + if ( subject.type() != SubjectType::SUBJECT_NONCE && subject.type() != SubjectType::SUBJECT_TASK_RESULT && + subject.type() != SubjectType::SUBJECT_REGISTRY_BATCH ) { ConsensusManagerLogger()->error( "{}: Invalid Subject type {}", __func__, @@ -2180,6 +2198,35 @@ namespace sgns } } + 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; } diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index eb3dbd01e..9878b6dce 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -145,6 +145,12 @@ namespace sgns 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 ); diff --git a/src/blockchain/impl/proto/Consensus.proto b/src/blockchain/impl/proto/Consensus.proto index a2ae273fb..44a5b7c03 100644 --- a/src/blockchain/impl/proto/Consensus.proto +++ b/src/blockchain/impl/proto/Consensus.proto @@ -10,6 +10,7 @@ message ConsensusSubject { oneof payload { NonceSubject nonce = 10; TaskResultSubject task_result = 11; + RegistryBatchSubject registry_batch = 12; } } @@ -17,6 +18,7 @@ enum SubjectType { SUBJECT_UNSPECIFIED = 0; SUBJECT_NONCE = 1; SUBJECT_TASK_RESULT = 2; + SUBJECT_REGISTRY_BATCH = 3; } message UTXOTransitionCommitment { @@ -58,6 +60,14 @@ message TaskResultSubject { 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; From a385bdcc66c226a90ce5d37fd2b06a9bef49376b Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 27 Mar 2026 10:59:57 -0300 Subject: [PATCH 092/114] Feat: Creating consensus on validator registry update --- src/blockchain/Consensus.cpp | 63 ++- src/blockchain/Consensus.hpp | 2 +- src/blockchain/ValidatorRegistry.cpp | 535 +++++++++++++++++- src/blockchain/ValidatorRegistry.hpp | 33 ++ src/blockchain/impl/Blockchain.cpp | 65 +++ .../impl/proto/ValidatorRegistry.proto | 1 + 6 files changed, 678 insertions(+), 21 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index fbd5f03e3..606260f49 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -1373,7 +1373,7 @@ namespace sgns } (void)SubmitCertificate( certificate_result.value() ); - ClearProposalState( state.proposal ); + ClearProposalSlot( state.proposal ); ConsensusManagerLogger()->debug( "{}: certificate submitted for hash {} proposal_id={}", __func__, GetPrintableSubjectHash( state.proposal.subject() ), @@ -1755,7 +1755,7 @@ namespace sgns return; } - ClearProposalState( certificate.proposal() ); + ClearProposalSlot( certificate.proposal() ); ConsensusManagerLogger()->debug( "{}: success proposal_id={}", __func__, certificate.proposal_id() ); } @@ -1794,6 +1794,13 @@ namespace sgns 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() ) @@ -1819,33 +1826,59 @@ namespace sgns return votes; } - void ConsensusManager::ClearProposalState( const Proposal &proposal ) + void ConsensusManager::ClearProposalSlot( const Proposal &proposal ) { std::lock_guard lock( proposals_mutex_ ); - auto it = proposals_.find( proposal.proposal_id() ); + + std::string slot_key; + auto it = proposals_.find( proposal.proposal_id() ); if ( it != proposals_.end() ) { - const auto slot_key = it->second.slot_key; - proposals_.erase( it ); - auto slot_it = slot_states_.find( slot_key ); - if ( slot_it != slot_states_.end() && slot_it->second.best_proposal_id == proposal.proposal_id() ) + 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 ) { - slot_states_.erase( slot_it ); + ids_to_remove.insert( kv.first ); } } - auto pending_it = pending_proposals_.find( proposal.proposal_id() ); - if ( pending_it != pending_proposals_.end() ) + for ( const auto &proposal_id : ids_to_remove ) { - pending_proposals_.erase( pending_it ); + proposals_.erase( proposal_id ); + pending_proposals_.erase( proposal_id ); + pending_votes_.erase( proposal_id ); } - for ( auto &kv : pending_by_subject_hash_ ) + for ( auto it_hash = pending_by_subject_hash_.begin(); it_hash != pending_by_subject_hash_.end(); ) { - auto &vec = kv.second; - vec.erase( std::remove( vec.begin(), vec.end(), proposal.proposal_id() ), vec.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_ ) { diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 9878b6dce..6f3fa43be 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -218,7 +218,7 @@ namespace sgns ProposalState CreateProposalState( const Certificate &certificate ); bool ValidateCertificateBestProposal( const ProposalState &state, const Certificate &certificate ) const; std::vector CollectCertificateVotes( const Certificate &certificate ) const; - void ClearProposalState( const Proposal &proposal ); + 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 ); diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 5e40dcd3e..a9012e3b8 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -12,14 +12,18 @@ #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 @@ -59,6 +63,35 @@ namespace sgns 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, @@ -637,6 +670,374 @@ namespace sgns return 0; } + void ValidatorRegistry::SetCertificatesPerBatch( size_t batch_size ) + { + if ( batch_size == 0 ) + { + logger_->warn( "{}: ignored zero batch size", __func__ ); + return; + } + std::lock_guard lock( batch_mutex_ ); + certificates_per_batch_ = batch_size; + } + + void ValidatorRegistry::SetBatchSubjectSubmitter( + std::function( const ConsensusSubject &subject )> submitter ) + { + std::lock_guard lock( batch_mutex_ ); + submit_batch_subject_ = std::move( submitter ); + } + + std::string ValidatorRegistry::BuildBatchKey( const std::string &base_registry_cid, uint64_t base_registry_epoch ) + { + return base_registry_cid + ":" + std::to_string( base_registry_epoch ); + } + + outcome::result ValidatorRegistry::ComputeBatchRoot( const std::vector &subject_hashes ) const + { + if ( subject_hashes.empty() ) + { + return outcome::failure( std::errc::invalid_argument ); + } + 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() ) ); + } + + outcome::result> ValidatorRegistry::SelectBatchSubjects( + const std::string &base_registry_cid, + uint64_t base_registry_epoch, + uint32_t certificate_count, + std::optional expected_root ) const + { + if ( certificate_count == 0 ) + { + 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; + } + + 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() ) + { + return outcome::failure( cert_get.error() ); + } + + sgns::ConsensusCertificate certificate; + std::string serialized = std::string(cert_get.value().toString()); + if ( !certificate.ParseFromString( serialized ) ) + { + return outcome::failure( std::errc::invalid_argument ); + } + return certificate; + } + + void ValidatorRegistry::OnFinalizedCertificate( const sgns::ConsensusCertificate &certificate ) + { + if ( !certificate.has_proposal() ) + { + return; + } + if ( certificate.proposal().subject().type() == SubjectType::SUBJECT_REGISTRY_BATCH ) + { + return; + } + + auto subject_hash_result = ExtractConsensusSubjectHash( certificate.proposal().subject() ); + if ( subject_hash_result.has_error() ) + { + return; + } + + const auto key = BuildBatchKey( certificate.registry_cid(), certificate.registry_epoch() ); + { + std::lock_guard lock( batch_mutex_ ); + pending_certificate_subjects_by_base_[key].insert( subject_hash_result.value() ); + } + + (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 { @@ -832,11 +1233,91 @@ namespace sgns } } - auto votes = ExtractCertificateVotes( certificate, *current_registry ); - Registry expected = BuildRegistryFromCertificate( *current_registry, - certificate, - votes.registered_votes, - votes.unregistered_votes ); + 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 ); @@ -1153,6 +1634,50 @@ namespace sgns 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 { diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index 0f9730e77..ade047717 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -39,6 +39,7 @@ namespace sgns { public: static constexpr size_t DefaultMaxNewValidatorsPerUpdate = 10; + static constexpr size_t DefaultCertificatesPerBatch = 5; using ValidatorEntry = validator::ValidatorEntry; using Registry = validator::Registry; using SignatureEntry = validator::SignatureEntry; @@ -103,6 +104,20 @@ namespace sgns 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() { @@ -160,6 +175,9 @@ namespace sgns 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, @@ -170,6 +188,14 @@ namespace sgns 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 ); @@ -186,6 +212,13 @@ namespace sgns std::string cached_registry_id_; 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 ae38c3e5e..da7b34d48 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -149,6 +149,71 @@ namespace sgns }, 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() ) { diff --git a/src/blockchain/impl/proto/ValidatorRegistry.proto b/src/blockchain/impl/proto/ValidatorRegistry.proto index 38522adce..e35c59436 100644 --- a/src/blockchain/impl/proto/ValidatorRegistry.proto +++ b/src/blockchain/impl/proto/ValidatorRegistry.proto @@ -39,6 +39,7 @@ message RegistryUpdate { string prev_registry_hash = 2; repeated SignatureEntry signatures = 3; bytes certificate = 4; + repeated string batch_certificate_subject_hashes = 5; } message RegistrySigningPayload { From 841e7f3f723770f6aa4678c12f115d53ec83c4a7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 30 Mar 2026 12:45:05 -0300 Subject: [PATCH 093/114] Fix: Dom't resume proposal if has certificate --- src/blockchain/impl/Blockchain.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index da7b34d48..517f7485f 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -1652,6 +1652,10 @@ namespace sgns outcome::result Blockchain::TryResumeProposal( const std::string &hash ) { + if ( consensus_manager_->CheckCertificateForSubject( hash ) ) + { + return outcome::success(); + } return consensus_manager_->ResumeProposalHandling( hash ); } From 6f80ecf8f5ca547522175e27b89ae4931d0c0e53 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 31 Mar 2026 09:18:00 -0300 Subject: [PATCH 094/114] Fix: Protections against voting and handling proposal or certified objects --- src/blockchain/Consensus.cpp | 52 ++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 606260f49..ee1b75a18 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -1144,6 +1144,26 @@ namespace sgns 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; + } + 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_ ); @@ -1189,14 +1209,6 @@ namespace sgns proposals_.emplace( proposal.proposal_id(), std::move( state ) ); } } - 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; - } ConsensusManagerLogger()->debug( "{}: Adding pending proposal for hash {} proposal_id={}", __func__, GetPrintableSubjectHash( proposal.subject() ), @@ -1310,6 +1322,16 @@ namespace sgns 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() ), @@ -1623,8 +1645,7 @@ namespace sgns } const auto ®istry = registry_result.value(); - bool has_quorum = false; - ProposalState state; + bool has_quorum = false; { std::lock_guard lock( proposals_mutex_ ); auto it = proposals_.find( vote.proposal_id() ); @@ -1637,6 +1658,16 @@ namespace sgns 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() ) { @@ -1698,7 +1729,6 @@ namespace sgns __func__, vote.voter_id().substr( 0, 8 ) ); } - state = it->second; } if ( has_quorum ) { From 8e06bcde1d6393eff28c82e7f9e766b26581d915 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 31 Mar 2026 17:32:04 -0300 Subject: [PATCH 095/114] Feat: Creating validator batch test --- test/src/multiaccount/multi_account_sync.cpp | 212 ++++++++++++++++++- 1 file changed, 203 insertions(+), 9 deletions(-) diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 0656408ff..0180faf53 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -407,6 +407,22 @@ TEST_F( MultiAccountTest, DISABLED_CRDTFilterDuplicateTx ) 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", @@ -447,6 +463,12 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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() } ); @@ -564,9 +586,9 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) cid_before = registry->GetRegistryCid(); auto transfer1 = node_client->TransferFunds( 75, - node_peer1->GetAddress(), - TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + 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 ); @@ -576,9 +598,9 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) cid_before = registry->GetRegistryCid(); auto transfer2 = node_client->TransferFunds( 40, - node_peer2->GetAddress(), - TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + 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 ); @@ -588,11 +610,183 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) cid_before = registry->GetRegistryCid(); auto transfer3 = node_client->TransferFunds( 10, - node_peer3->GetAddress(), - TokenID::FromBytes( { 0x00 } ), - std::chrono::milliseconds( OUTGOING_TIMEOUT_MILLISECONDS ) ); + 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, "", "", TokenID::FromBytes( { 0x00 } ) ); + ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; + assert_registry_immutable( "tx1" ); + + auto mint2 = node_client->MintTokens( 250, "", "", 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; + } +} From 380f28b18e25f55d0a35190e1d4b1d7c1547a126 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 3 Apr 2026 17:47:30 -0300 Subject: [PATCH 096/114] Fix: Removing the rule that the regitry must match the latest --- src/blockchain/Consensus.cpp | 123 ++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 44 deletions(-) diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index ee1b75a18..d5d8905c5 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -919,15 +919,22 @@ namespace sgns proposal.proposal_id().substr( 0, 8 ), votes.size() ); - auto registry_result = registry_->LoadRegistry(); + 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={}", + ConsensusManagerLogger()->error( "{}: failed: registry load error={} cid={}", __func__, - registry_result.error().message() ); + registry_result.error().message(), + proposal.registry_cid() ); return outcome::failure( registry_result.error() ); } - return TallyVotes( proposal, votes, registry_result.value(), registry_->GetRegistryCid() ); + return TallyVotes( proposal, votes, registry_result.value(), proposal.registry_cid() ); } outcome::result> ConsensusManager::ProposalSigningBytes( const Proposal &proposal ) @@ -1108,30 +1115,56 @@ namespace sgns return; } - const auto registry_cid = registry_->GetRegistryCid(); - if ( registry_cid.empty() ) + if ( proposal.registry_cid().empty() ) { ConsensusManagerLogger()->error( - "{}: rejected: Local registry doesn't have a CID for hash {}. proposal_id={}", + "{}: rejected: proposal registry CID missing for hash {}. proposal_id={}", __func__, GetPrintableSubjectHash( proposal.subject() ), proposal.proposal_id().substr( 0, 8 ) ); return; } - if ( proposal.registry_cid() != registry_cid ) + + auto subject_hash = GetSubjectHash( proposal.subject() ); + if ( subject_hash.has_error() ) { - ConsensusManagerLogger()->error( "{}: rejected: registry CID mismatch proposal={} registry={}", + ConsensusManagerLogger()->error( "{}: rejected: subject hash missing proposal_id={}", __func__, - proposal.registry_cid(), - registry_cid ); + 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() != registry_->GetRegistryEpoch() ) + if ( proposal.registry_epoch() != proposal_registry_result.value().epoch() ) { ConsensusManagerLogger()->error( "{}: rejected: registry epoch mismatch proposal={} registry={}", __func__, proposal.registry_epoch(), - registry_->GetRegistryEpoch() ); + proposal_registry_result.value().epoch() ); return; } @@ -1144,14 +1177,6 @@ namespace sgns 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; - } if ( CheckCertificateForSubject( subject_hash.value() ) ) { ConsensusManagerLogger()->debug( "{}: ignored: subject already certified hash={} proposal_id={}", @@ -1292,14 +1317,6 @@ namespace sgns void ConsensusManager::ProcessCertificates() { - auto registry_result = registry_->LoadRegistry(); - if ( registry_result.has_error() ) - { - return; - } - //ConsensusManagerLogger()->trace( "{}: Checking if need to process certificates", __func__ ); - const auto ®istry = registry_result.value(); - std::vector to_process; { std::lock_guard lock( proposals_mutex_ ); @@ -1360,7 +1377,25 @@ namespace sgns round ); continue; } - if ( !IsCurrentAggregator( state.proposal, registry ) ) + 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__, @@ -1635,16 +1670,6 @@ namespace sgns return; } - auto registry_result = registry_->LoadRegistry(); - if ( registry_result.has_error() ) - { - ConsensusManagerLogger()->error( "{}: rejected: registry load error={}", - __func__, - registry_result.error().message() ); - return; - } - const auto ®istry = registry_result.value(); - bool has_quorum = false; { std::lock_guard lock( proposals_mutex_ ); @@ -1685,8 +1710,18 @@ namespace sgns return; } - if ( proposal_state.proposal.registry_cid() != registry_->GetRegistryCid() || - proposal_state.proposal.registry_epoch() != registry.epoch() ) + 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__, @@ -1694,12 +1729,12 @@ namespace sgns return; } - const auto *validator = registry_->FindValidator( registry, vote.voter_id() ); + 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( registry ); + it->second.total_weight = registry_->TotalWeight( proposal_registry ); } it->second.votes.push_back( vote ); From 1282523072eb2816e615bfad258715fa2d2ebcf2 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 6 Apr 2026 17:19:45 -0300 Subject: [PATCH 097/114] Fix: UTXO chain proofs work with incoming TXs. Nonce fetching removed --- src/account/TransactionManager.cpp | 554 +++++++++++++++++++---------- src/account/TransactionManager.hpp | 25 +- 2 files changed, 393 insertions(+), 186 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 5c421a0bb..fcebca10e 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -867,38 +867,24 @@ namespace sgns { crdt_transaction = globaldb_m->BeginTransaction(); } - auto nonce_result = account_m->FetchNetworkNonce( NONCE_REQUEST_TIMEOUT_MS ); - uint64_t expected_next_nonce = 0; - - if ( nonce_result.has_error() ) + std::optional expected_next_nonce; + if ( auto local_confirmed = account_m->GetLocalConfirmedNonce(); local_confirmed.has_value() ) { - if ( !full_node_m ) - { - TransactionManagerLogger()->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 ) ); - } - TransactionManagerLogger()->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() ) - { - TransactionManagerLogger()->debug( "[{} - full: {}] Using local confirmed nonce {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - local_confirmed.value() ); - expected_next_nonce = local_confirmed.value() + 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.value().has_value() ) + else if ( !transaction_batch.empty() ) { - TransactionManagerLogger()->debug( "[{} - full: {}] Set nonce to {}", + // 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, - nonce_result.value().value() ); - expected_next_nonce = nonce_result.value().value() + 1; + expected_next_nonce.value() ); } std::unordered_set topicSet; std::set> transactions_sent; @@ -910,13 +896,18 @@ namespace sgns for ( auto &[transaction, maybe_proof] : transaction_batch ) { - if ( transaction->GetNonce() != expected_next_nonce ) + if ( !expected_next_nonce.has_value() ) + { + expected_next_nonce = transaction->GetNonce(); + } + + if ( transaction->GetNonce() != expected_next_nonce.value() ) { TransactionManagerLogger()->error( "[{} - full: {}] Transaction with unexpected nonce - Expected: {}, Tried to send: {}", account_m->GetAddress().substr( 0, 8 ), full_node_m, - expected_next_nonce, + expected_next_nonce.value(), transaction->GetNonce() ); return outcome::failure( @@ -958,7 +949,7 @@ namespace sgns topicSet.merge( transaction->GetTopics() ); transactions_sent.insert( transaction ); - expected_next_nonce++; + expected_next_nonce = expected_next_nonce.value() + 1; } OUTCOME_TRY( crdt_transaction->Commit( topicSet ) ); @@ -1010,9 +1001,8 @@ namespace sgns transaction->GetHash(), utxo_commitment, utxo_witness ) ); - OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); - OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); + OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); } return outcome::success(); @@ -1144,7 +1134,12 @@ namespace sgns 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 ) @@ -1158,7 +1153,127 @@ namespace sgns return std::errc::invalid_argument; } - return ( this->*( it->second.second ) )( tx ); + utxo_state_tracking_suppression_.fetch_add( 1 ); + auto revert_result = ( this->*( it->second.second ) )( tx ); + utxo_state_tracking_suppression_.fetch_sub( 1 ); + OUTCOME_TRY( revert_result ); + if ( DoesTransactionMutateUTXOState( tx ) && utxo_state_tracking_suppression_.load() == 0 ) + { + UpdateAccountUTXOState( CollectTouchedAccounts( tx ), false ); + } + return outcome::success(); + } + + bool TransactionManager::DoesTransactionMutateUTXOState( const std::shared_ptr &tx ) const + { + if ( !tx ) + { + return false; + } + + if ( tx->HasUTXOParameters() ) + { + return true; + } + + // Legacy mint transactions still create UTXOs for the source account. + return tx->GetType() == "mint"; + } + + std::unordered_set TransactionManager::CollectTouchedAccounts( + const std::shared_ptr &tx ) const + { + std::unordered_set addresses; + if ( !tx ) + { + return addresses; + } + + if ( tx->HasUTXOParameters() ) + { + auto params_opt = tx->GetUTXOParametersOpt(); + if ( params_opt.has_value() ) + { + const auto &[inputs, outputs] = params_opt.value(); + if ( !inputs.empty() ) + { + if ( full_node_m || tx->GetSrcAddress() == account_m->GetAddress() ) + { + addresses.insert( tx->GetSrcAddress() ); + } + } + for ( const auto &output : outputs ) + { + if ( !output.dest_address.empty() && + ( full_node_m || output.dest_address == account_m->GetAddress() ) ) + { + addresses.insert( output.dest_address ); + } + } + } + } + else if ( tx->GetType() == "mint" && !tx->GetSrcAddress().empty() && + ( full_node_m || tx->GetSrcAddress() == account_m->GetAddress() ) ) + { + addresses.insert( tx->GetSrcAddress() ); + } + + return addresses; + } + + TransactionManager::AccountUTXOState TransactionManager::GetOrInitAccountUTXOState( const std::string &address ) const + { + const auto current_root = 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( @@ -3196,20 +3311,28 @@ namespace sgns const std::string tx_hash = subject.nonce().tx_hash(); const auto key = GetTransactionPath( tx_hash ); - std::shared_lock tx_lock( tx_mutex_m ); - auto it = tx_processed_m.find( key ); - if ( it == tx_processed_m.end() ) + std::shared_ptr tracked_tx; + uint64_t tracked_nonce = 0; + TransactionStatus tracked_status = TransactionStatus::INVALID; { - 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; + 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; } - auto &tracked = it->second; - if ( !tracked.tx ) + if ( !tracked_tx ) { TransactionManagerLogger()->error( "[{} - full: {}] {}: Tracked transaction missing for hash {}", account_m->GetAddress().substr( 0, 8 ), @@ -3219,50 +3342,100 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - if ( tracked.cached_nonce != subject.nonce().nonce() ) + 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 ConsensusManager::SubjectCheck::Reject; + return reject_and_maybe_fail_local( "nonce mismatch" ); } - if ( !subject.account_id().empty() && tracked.tx->GetSrcAddress() != subject.account_id() ) + 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 ConsensusManager::SubjectCheck::Reject; + return reject_and_maybe_fail_local( "account mismatch" ); } - if ( tracked.status == TransactionStatus::FAILED ) + 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 ConsensusManager::SubjectCheck::Reject; + return reject_and_maybe_fail_local( "transaction already failed" ); } - const bool witness_valid = ValidateWitnessForConsensus( subject, tracked.tx ); - if ( !witness_valid ) + const auto witness_validation = ValidateWitnessForConsensus( subject, tracked_tx ); + if ( witness_validation == WitnessValidationResult::DRIFT ) + { + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Witness validation drift for hash {}, deferring as pending and requesting heads", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + RequestRelevantHeads(); + return ConsensusManager::SubjectCheck::Pending; + } + 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 ConsensusManager::SubjectCheck::Reject; + return reject_and_maybe_fail_local( "witness validation failed" ); } - auto validate_result = ValidateTransactionForConsensus( tracked.tx, witness_valid ); + auto validate_result = ValidateTransactionForConsensus( tracked_tx, true ); + + if ( !validate_result ) + { + return reject_and_maybe_fail_local( "transaction validation failed" ); + } - return validate_result ? ConsensusManager::SubjectCheck::Approve : ConsensusManager::SubjectCheck::Reject; + return ConsensusManager::SubjectCheck::Approve; } bool TransactionManager::ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, @@ -3640,114 +3813,176 @@ namespace sgns return true; } - bool TransactionManager::ValidateWitnessForConsensus( const ConsensusSubject &subject, - const std::shared_ptr &tx ) const + TransactionManager::WitnessValidationResult TransactionManager::ValidateWitnessForConsensus( + const ConsensusSubject &subject, + const std::shared_ptr &tx ) const { if ( !tx ) { - return false; + 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() ) { - return true; + 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 ); - // For nonce > 0, anchor pre-state root in the certified per-account chain. - std::optional prev_subject; - if ( tx->GetNonce() > 0 ) - { - const auto prev_hash = tx->GetPreviousHash(); - if ( prev_hash.empty() ) - { - return false; - } - - auto prev_cert_result = blockchain_->GetCertificateBySubjectHash( prev_hash ); - if ( prev_cert_result.has_error() ) - { - return false; - } - const auto &prev_subject_ref = prev_cert_result.value().proposal().subject(); - if ( !prev_subject_ref.has_nonce() ) - { - return false; - } - if ( prev_subject_ref.account_id() != tx->GetSrcAddress() ) - { - return false; - } - if ( prev_subject_ref.nonce().nonce() + 1 != tx->GetNonce() ) - { - return false; - } - prev_subject = prev_subject_ref; - } - if ( !tx->HasUTXOParameters() ) { - return true; + 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() ) { - return false; + 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.pre_utxo_root().size() != base::Hash256::size() || commitment.post_utxo_root().size() != base::Hash256::size() ) { - return false; - } - if ( commitment.account_state_version() != ( tx->GetNonce() > 0 ? tx->GetNonce() - 1 : 0 ) ) - { - return false; + TransactionManagerLogger()->error( "[{} - full: {}] {}: Invalid commitment root sizes tx={} pre_size={} " + "post_size={} expected={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + commitment.pre_utxo_root().size(), + commitment.post_utxo_root().size(), + base::Hash256::size() ); + return WitnessValidationResult::INVALID; } - auto pre_root_result = base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( commitment.pre_utxo_root().data() ) ), commitment.pre_utxo_root().size() ) ); if ( pre_root_result.has_error() ) { - return false; + TransactionManagerLogger()->error( "[{} - full: {}] {}: Failed to parse commitment pre-root tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return WitnessValidationResult::INVALID; } const auto pre_root = pre_root_result.value(); - if ( tx->GetNonce() > 0 ) + // Canonical pre-state is only guaranteed locally for our own account. + // Full nodes may validate proposals for remote accounts before their local + // snapshot converges, so this check must not reject those proposals. + const bool can_validate_canonical_pre_state = tx->GetSrcAddress() == account_m->GetAddress(); + if ( can_validate_canonical_pre_state ) { - if ( !prev_subject.has_value() || !prev_subject->nonce().has_utxo_commitment() ) + const auto canonical_state = GetOrInitAccountUTXOState( tx->GetSrcAddress() ); + if ( canonical_state.version != commitment.account_state_version() ) { - return false; - } - const auto &prev_commitment = prev_subject->nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); - if ( prev_post_root_result.has_error() ) - { - return false; + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Account state version drift tx={} local={} " + "commitment={}. Continuing with witness validation.", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + canonical_state.version, + commitment.account_state_version() ); + return WitnessValidationResult::DRIFT; } - if ( prev_post_root_result.value() != pre_root ) + if ( canonical_state.root != pre_root ) { - return false; + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Pre-root drift tx={} local_root={} " + "commitment_pre_root={}. Continuing with witness validation.", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + canonical_state.root.toReadableString(), + pre_root.toReadableString() ); + return WitnessValidationResult::DRIFT; } + TransactionManagerLogger()->debug( "[{} - full: {}] {}: Canonical pre-state matched tx={} version={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + canonical_state.version ); + } + else + { + TransactionManagerLogger()->debug( + "[{} - full: {}] {}: Skipping canonical pre-state check for foreign source account tx={} src={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash(), + tx->GetSrcAddress() ); } if ( validator.RequiresConsensusUTXOData() && !subject.nonce().has_utxo_witness() ) { - return false; + 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() ) { - return false; + TransactionManagerLogger()->error( "[{} - full: {}] {}: Missing UTXO params payload tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return WitnessValidationResult::INVALID; } - return validator.ValidateWitness( subject, tx, params_opt.value(), pre_root, blockchain_ ); + const bool witness_ok = validator.ValidateWitness( subject, tx, params_opt.value(), pre_root, 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( @@ -3774,74 +4009,16 @@ namespace sgns const auto pre_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( before_snapshot ); const auto post_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( after_snapshot ); - // Safety guard before proposing: for nonce > 0 pre-state must be anchored to certified chain. - if ( tx->GetNonce() > 0 ) + // Anchor to current canonical state for the source account. + const auto canonical_state = GetOrInitAccountUTXOState( tx->GetSrcAddress() ); + if ( canonical_state.root != pre_root ) { - const auto previous_hash = tx->GetPreviousHash(); - if ( previous_hash.empty() ) - { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Missing previous hash for tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); - return std::nullopt; - } - auto previous_cert = blockchain_->GetCertificateBySubjectHash( previous_hash ); - if ( previous_cert.has_error() ) - { - TransactionManagerLogger()->warn( - "[{} - full: {}] {}: Missing previous certificate tx={} prev={} err={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - previous_hash, - previous_cert.error().message() ); - return std::nullopt; - } - const auto &previous_subject = previous_cert.value().proposal().subject(); - if ( !previous_subject.has_nonce() || previous_subject.account_id() != tx->GetSrcAddress() || - ( previous_subject.nonce().nonce() + 1 ) != tx->GetNonce() ) - { - TransactionManagerLogger()->warn( - "[{} - full: {}] {}: Previous subject continuity mismatch tx={} prev={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - previous_hash ); - return std::nullopt; - } - - if ( !previous_subject.nonce().has_utxo_commitment() ) - { - TransactionManagerLogger()->warn( - "[{} - full: {}] {}: Previous subject missing mandatory UTXO commitment tx={} prev={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - previous_hash ); - return std::nullopt; - } - - { - const auto &prev_commitment = previous_subject.nonce().utxo_commitment(); - auto prev_post_root_result = base::Hash256::fromSpan( gsl::span( - reinterpret_cast( const_cast( prev_commitment.post_utxo_root().data() ) ), - prev_commitment.post_utxo_root().size() ) ); - if ( prev_post_root_result.has_error() || prev_post_root_result.value() != pre_root ) - { - TransactionManagerLogger()->warn( - "[{} - full: {}] {}: Pre-root not anchored to prev certified post-root tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); - return std::nullopt; - } - } + TransactionManagerLogger()->warn( "[{} - full: {}] {}: Stale pre-root while creating commitment tx={}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx->GetHash() ); + return std::nullopt; } std::vector produced_outputs; @@ -3860,7 +4037,7 @@ namespace sgns commitment.set_post_utxo_root( post_root.data(), post_root.size() ); commitment.set_utxo_count_before( before_snapshot.size() ); commitment.set_utxo_count_after( after_snapshot.size() ); - commitment.set_account_state_version( tx->GetNonce() > 0 ? tx->GetNonce() - 1 : 0 ); + commitment.set_account_state_version( canonical_state.version ); commitment.set_produced_outputs_root( produced_outputs_root.data(), produced_outputs_root.size() ); return commitment; } @@ -4280,6 +4457,15 @@ namespace sgns 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() ); diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 88c4e7523..78a8ec1dc 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -210,6 +210,12 @@ namespace sgns 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, @@ -241,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(); @@ -297,6 +308,9 @@ namespace sgns 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 }; @@ -371,6 +385,13 @@ namespace sgns void ChangeState( State new_state ); public: + enum class WitnessValidationResult : uint8_t + { + VALID, + DRIFT, + INVALID + }; + outcome::result GetTransactionCID( const std::string &tx_hash ) const; outcome::result HandleNonceConsensusSubject( const ConsensusManager::Subject &subject ); @@ -386,8 +407,8 @@ namespace sgns std::optional BuildUTXOWitness( const std::shared_ptr &tx ) const; bool ApplyTransactionToUTXOSnapshot( const std::shared_ptr &tx, std::vector &snapshot ) const; - bool ValidateWitnessForConsensus( const ConsensusSubject &subject, - const std::shared_ptr &tx ) 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, From e03a35df7bd2078e68e07c0d26d60e5997d4f502 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Mon, 6 Apr 2026 17:20:06 -0300 Subject: [PATCH 098/114] Fix: Transaction sync and multi account tests working --- test/src/multiaccount/multi_account_sync.cpp | 23 +++++++ .../transaction_sync_test.cpp | 63 +++++-------------- 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 0180faf53..087a0a310 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -560,6 +560,25 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) } }; + 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(); @@ -570,6 +589,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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() ); @@ -580,6 +600,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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(); @@ -592,6 +613,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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(); @@ -604,6 +626,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) 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(); diff --git a/test/src/transaction_sync/transaction_sync_test.cpp b/test/src/transaction_sync/transaction_sync_test.cpp index 708292ac4..658f84faa 100644 --- a/test/src/transaction_sync/transaction_sync_test.cpp +++ b/test/src/transaction_sync/transaction_sync_test.cpp @@ -119,7 +119,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 +128,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 ) ); @@ -439,7 +442,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 +461,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 +477,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(); @@ -568,7 +538,8 @@ TEST_F( TransactionSyncTest, InvalidPreviousHashTest ) auto tx_pair2 = CreateTransfer( GetAccountFromNode( *node_proc1 ), *GetUTXOManagerFromNode( *node_proc1 ), 10000000000, - node_proc2->GetAddress() ); + node_proc2->GetAddress(), + tx1_id ); ASSERT_TRUE( tx_pair2.has_value() ); auto [tx2, proof2] = tx_pair2.value(); From 4be98e8b918ae117e83f3b02000165968dac435a Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 8 Apr 2026 17:24:54 -0300 Subject: [PATCH 099/114] Feat: Compute Merkle root for generic vector --- src/account/UTXOMerkle.hpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/account/UTXOMerkle.hpp b/src/account/UTXOMerkle.hpp index b0c4b2041..83e01fcb5 100644 --- a/src/account/UTXOMerkle.hpp +++ b/src/account/UTXOMerkle.hpp @@ -120,20 +120,13 @@ namespace sgns::utxo_merkle return level_hashes.front(); } - inline base::Hash256 ComputeMerkleRootFromUTXOs( const std::vector &utxos ) + inline base::Hash256 ComputeMerkleRootFromPayloads( std::vector> payloads ) { - if ( utxos.empty() ) + if ( payloads.empty() ) { return EmptyUTXOMerkleRoot(); } - std::vector> payloads; - payloads.reserve( utxos.size() ); - for ( const auto &utxo : utxos ) - { - payloads.push_back( SerializeUTXOLeafPayload( utxo ) ); - } - std::sort( payloads.begin(), payloads.end() ); std::vector leaf_hashes; @@ -145,4 +138,19 @@ namespace sgns::utxo_merkle 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 ) ); + } } From 8235a484b1e63a22bbbacde9c88c21a780b5b62f Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 9 Apr 2026 09:38:07 -0300 Subject: [PATCH 100/114] Fix: UTXO validation working by appending to the certificate the inputs and outputs --- src/account/InputValidators.cpp | 192 +++++++++++++++--- src/account/InputValidators.hpp | 3 - src/account/TransactionManager.cpp | 227 ++++++++++++---------- src/account/TransactionManager.hpp | 4 +- src/blockchain/impl/proto/Consensus.proto | 23 ++- 5 files changed, 306 insertions(+), 143 deletions(-) diff --git a/src/account/InputValidators.cpp b/src/account/InputValidators.cpp index 85520c368..1fbbf150c 100644 --- a/src/account/InputValidators.cpp +++ b/src/account/InputValidators.cpp @@ -19,8 +19,53 @@ namespace sgns 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, @@ -38,14 +83,13 @@ namespace sgns bool GeniusInputValidator::ValidateWitness( const ConsensusSubject &subject, const std::shared_ptr &tx, const UTXOTxParameters ¶ms, - const base::Hash256 &pre_root, const std::shared_ptr &blockchain ) const { if ( !tx || !blockchain ) { return false; } - if ( !subject.has_nonce() || !subject.nonce().has_utxo_witness() ) + if ( !subject.has_nonce() || !subject.nonce().has_utxo_witness() || !subject.nonce().has_utxo_commitment() ) { return false; } @@ -56,6 +100,123 @@ namespace sgns { 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() ); @@ -156,31 +317,6 @@ namespace sgns } std::vector payload_vec( payload.begin(), payload.end() ); - auto current_hash = HashLeaf( payload_vec ); - for ( const auto &step : proof.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() ) - { - current_hash = HashNode( sibling_hash_result.value(), current_hash ); - } - else - { - current_hash = HashNode( current_hash, sibling_hash_result.value() ); - } - } - - if ( current_hash != pre_root ) - { - return false; - } auto producer_cert_result = blockchain->GetCertificateBySubjectHash( input.txid_hash_.toReadableString() ); if ( producer_cert_result.has_error() ) @@ -264,11 +400,9 @@ namespace sgns bool PublicChainInputValidator::ValidateWitness( const ConsensusSubject &subject, const std::shared_ptr &tx, const UTXOTxParameters ¶ms, - const base::Hash256 &pre_root, const std::shared_ptr &blockchain ) const { (void)subject; - (void)pre_root; (void)blockchain; if ( !tx || params.first.empty() || params.second.empty() ) { diff --git a/src/account/InputValidators.hpp b/src/account/InputValidators.hpp index 4f28a38e9..45059dfa0 100644 --- a/src/account/InputValidators.hpp +++ b/src/account/InputValidators.hpp @@ -28,7 +28,6 @@ namespace sgns virtual bool ValidateWitness( const ConsensusSubject &subject, const std::shared_ptr &tx, const UTXOTxParameters ¶ms, - const base::Hash256 &pre_root, const std::shared_ptr &blockchain ) const = 0; virtual bool RequiresConsensusUTXOData() const = 0; @@ -44,7 +43,6 @@ namespace sgns bool ValidateWitness( const ConsensusSubject &subject, const std::shared_ptr &tx, const UTXOTxParameters ¶ms, - const base::Hash256 &pre_root, const std::shared_ptr &blockchain ) const override; bool RequiresConsensusUTXOData() const override @@ -63,7 +61,6 @@ namespace sgns bool ValidateWitness( const ConsensusSubject &subject, const std::shared_ptr &tx, const UTXOTxParameters ¶ms, - const base::Hash256 &pre_root, const std::shared_ptr &blockchain ) const override; bool RequiresConsensusUTXOData() const override diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index fcebca10e..8133af8ea 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -3139,6 +3139,53 @@ namespace sgns 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 {}", @@ -3406,18 +3453,18 @@ namespace sgns return reject_and_maybe_fail_local( "transaction already failed" ); } - const auto witness_validation = ValidateWitnessForConsensus( subject, tracked_tx ); - if ( witness_validation == WitnessValidationResult::DRIFT ) + if ( HasConfirmedInputConflict( tracked_tx ) ) { - TransactionManagerLogger()->warn( - "[{} - full: {}] {}: Witness validation drift for hash {}, deferring as pending and requesting heads", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash ); - RequestRelevantHeads(); - return ConsensusManager::SubjectCheck::Pending; + 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 {}", @@ -3428,7 +3475,7 @@ namespace sgns return reject_and_maybe_fail_local( "witness validation failed" ); } - auto validate_result = ValidateTransactionForConsensus( tracked_tx, true ); + auto validate_result = ValidateTransactionForConsensus( tracked_tx ); if ( !validate_result ) { @@ -3473,8 +3520,7 @@ namespace sgns return true; } - bool TransactionManager::ValidateTransactionForConsensus( const std::shared_ptr &tx, - bool skip_utxo_state_validation ) const + bool TransactionManager::ValidateTransactionForConsensus( const std::shared_ptr &tx ) const { TransactionManagerLogger()->debug( "[{} - full: {}] {}: Validating transaction", account_m->GetAddress().substr( 0, 8 ), @@ -3526,8 +3572,7 @@ namespace sgns return false; } //TODO - Deal with checking the Mint - const bool is_utxo_type = tx->HasUTXOParameters(); - if ( !( skip_utxo_state_validation && is_utxo_type ) && !CheckTransactionTypeRules( tx ) ) + if ( !CheckTransactionTypeRules( tx ) ) { TransactionManagerLogger()->error( "[{} - full: {}] {}: Type rules failed tx={}", account_m->GetAddress().substr( 0, 8 ), @@ -3874,82 +3919,32 @@ namespace sgns } const auto &commitment = subject.nonce().utxo_commitment(); - if ( commitment.pre_utxo_root().size() != base::Hash256::size() || - commitment.post_utxo_root().size() != base::Hash256::size() ) + 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={} pre_size={} " - "post_size={} expected={}", + 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.pre_utxo_root().size(), - commitment.post_utxo_root().size(), + commitment.consumed_outpoints_root().size(), + commitment.produced_outputs_root().size(), base::Hash256::size() ); return WitnessValidationResult::INVALID; } - auto pre_root_result = base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( commitment.pre_utxo_root().data() ) ), - commitment.pre_utxo_root().size() ) ); - if ( pre_root_result.has_error() ) + 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 pre-root tx={}", + 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; } - const auto pre_root = pre_root_result.value(); - - // Canonical pre-state is only guaranteed locally for our own account. - // Full nodes may validate proposals for remote accounts before their local - // snapshot converges, so this check must not reject those proposals. - const bool can_validate_canonical_pre_state = tx->GetSrcAddress() == account_m->GetAddress(); - if ( can_validate_canonical_pre_state ) - { - const auto canonical_state = GetOrInitAccountUTXOState( tx->GetSrcAddress() ); - if ( canonical_state.version != commitment.account_state_version() ) - { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Account state version drift tx={} local={} " - "commitment={}. Continuing with witness validation.", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - canonical_state.version, - commitment.account_state_version() ); - return WitnessValidationResult::DRIFT; - } - if ( canonical_state.root != pre_root ) - { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Pre-root drift tx={} local_root={} " - "commitment_pre_root={}. Continuing with witness validation.", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - canonical_state.root.toReadableString(), - pre_root.toReadableString() ); - return WitnessValidationResult::DRIFT; - } - TransactionManagerLogger()->debug( "[{} - full: {}] {}: Canonical pre-state matched tx={} version={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - canonical_state.version ); - } - else - { - TransactionManagerLogger()->debug( - "[{} - full: {}] {}: Skipping canonical pre-state check for foreign source account tx={} src={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash(), - tx->GetSrcAddress() ); - } if ( validator.RequiresConsensusUTXOData() && !subject.nonce().has_utxo_witness() ) { @@ -3974,7 +3969,8 @@ namespace sgns tx->GetHash() ); return WitnessValidationResult::INVALID; } - const bool witness_ok = validator.ValidateWitness( subject, tx, params_opt.value(), pre_root, blockchain_ ); + (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, @@ -3992,34 +3988,42 @@ namespace sgns { return std::nullopt; } - std::vector before_snapshot = utxo_manager_.GetUTXOsForReservation( tx->GetSrcAddress(), - tx->GetHash() ); - std::vector after_snapshot = before_snapshot; - if ( !ApplyTransactionToUTXOSnapshot( tx, after_snapshot ) ) + 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() ) { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Could not build transition snapshot for tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); return std::nullopt; } UTXOTransitionCommitment commitment; - const auto pre_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( before_snapshot ); - const auto post_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( after_snapshot ); - - // Anchor to current canonical state for the source account. - const auto canonical_state = GetOrInitAccountUTXOState( tx->GetSrcAddress() ); - if ( canonical_state.root != pre_root ) + std::vector> consumed_payloads; + consumed_payloads.reserve( inputs.size() ); + for ( const auto &input : inputs ) { - TransactionManagerLogger()->warn( "[{} - full: {}] {}: Stale pre-root while creating commitment tx={}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx->GetHash() ); - return std::nullopt; + 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 ) ) @@ -4031,13 +4035,30 @@ namespace sgns 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_pre_utxo_root( pre_root.data(), pre_root.size() ); - commitment.set_post_utxo_root( post_root.data(), post_root.size() ); - commitment.set_utxo_count_before( before_snapshot.size() ); - commitment.set_utxo_count_after( after_snapshot.size() ); - commitment.set_account_state_version( canonical_state.version ); + 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; } diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index 78a8ec1dc..6e0e8afb5 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -395,8 +395,7 @@ namespace sgns outcome::result GetTransactionCID( const std::string &tx_hash ) const; outcome::result HandleNonceConsensusSubject( const ConsensusManager::Subject &subject ); - bool ValidateTransactionForConsensus( const std::shared_ptr &tx, - bool skip_utxo_state_validation = false ) const; + 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; @@ -413,6 +412,7 @@ namespace sgns 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; diff --git a/src/blockchain/impl/proto/Consensus.proto b/src/blockchain/impl/proto/Consensus.proto index 44a5b7c03..e44ffd314 100644 --- a/src/blockchain/impl/proto/Consensus.proto +++ b/src/blockchain/impl/proto/Consensus.proto @@ -22,12 +22,23 @@ enum SubjectType { } message UTXOTransitionCommitment { - bytes pre_utxo_root = 1; - bytes post_utxo_root = 2; - uint64 utxo_count_before = 3; - uint64 utxo_count_after = 4; - uint64 account_state_version = 5; - bytes produced_outputs_root = 6; + 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 { From 94e438b9cf90ab9433b2e81211c183c6bdad8e62 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Thu, 9 Apr 2026 09:38:45 -0300 Subject: [PATCH 101/114] Fix: MintTokens on tests were fixed to avoid using the same previous hash --- .../blockchain/blockchain_genesis_test.cpp | 15 ++++++++- .../blockchain/consensus_certificate_test.cpp | 25 +++++++++------ test/src/multiaccount/multi_account_sync.cpp | 30 ++++++++++++------ .../processing_multi_test.cpp | 16 ++++++++-- .../processing_nodes/child_tokens_test.cpp | 25 +++++++++++---- test/src/processing_nodes/full_node_test.cpp | 15 ++++++++- .../processing_nodes_test.cpp | 19 ++++++++++-- .../transaction_crash_test.cpp | 13 +++++++- .../transaction_sync_test.cpp | 31 ++++++++++++++----- 9 files changed, 149 insertions(+), 40 deletions(-) diff --git a/test/src/blockchain/blockchain_genesis_test.cpp b/test/src/blockchain/blockchain_genesis_test.cpp index dc3b95ca4..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 @@ -29,6 +30,18 @@ #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 index 67a0da839..94ffc4447 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -78,11 +78,16 @@ namespace sgns::UTXOTransitionCommitment MakeTestCommitment() { sgns::UTXOTransitionCommitment commitment; - commitment.set_pre_utxo_root( std::string( 32, '\x01' ) ); - commitment.set_post_utxo_root( std::string( 32, '\x02' ) ); - commitment.set_utxo_count_before( 1 ); - commitment.set_utxo_count_after( 1 ); - commitment.set_account_state_version( 0 ); + 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; } @@ -514,11 +519,11 @@ namespace sgns::test auto manager = MakeManager( registry, db_, pubs_, account ); UTXOTransitionCommitment commitment; - commitment.set_pre_utxo_root( std::string( 32, '\x01' ) ); - commitment.set_post_utxo_root( std::string( 32, '\x02' ) ); - commitment.set_utxo_count_before( 1 ); - commitment.set_utxo_count_after( 1 ); - commitment.set_account_state_version( 0 ); + 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(); diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index 087a0a310..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 @@ -37,6 +38,17 @@ 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, @@ -172,12 +184,12 @@ TEST_F( MultiAccountTest, DISABLED_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"; @@ -197,7 +209,7 @@ TEST_F( MultiAccountTest, DISABLED_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( @@ -290,7 +302,7 @@ TEST_F( MultiAccountTest, DISABLED_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"; @@ -584,7 +596,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) auto epoch_before = registry_state.value().epoch(); auto cid_before = registry->GetRegistryCid(); - auto mint1 = node_client->MintTokens( 100, "", "", TokenID::FromBytes( { 0x00 } ) ); + 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" ); @@ -596,7 +608,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) epoch_before = registry_state.value().epoch(); cid_before = registry->GetRegistryCid(); - auto mint2 = node_client->MintTokens( 250, "", "", TokenID::FromBytes( { 0x00 } ) ); + 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 ); @@ -759,11 +771,11 @@ TEST_F( MultiAccountTest, NodeConsensusBatch5Test ) } }; - auto mint1 = node_client->MintTokens( 100, "", "", TokenID::FromBytes( { 0x00 } ) ); + 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, "", "", TokenID::FromBytes( { 0x00 } ) ); + 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" ); 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 b97283b4b..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,8 +178,8 @@ 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->GetTransactions(TransactionManager::TransactionStatus::CONFIRMED).size(); int transcount_node1 = node_proc1->GetTransactions(TransactionManager::TransactionStatus::CONFIRMED).size(); @@ -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 658f84faa..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 @@ -173,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(); @@ -246,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(); @@ -254,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 @@ -426,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(); @@ -517,7 +533,8 @@ TEST_F( TransactionSyncTest, InvalidPreviousHashTest ) "node_proc2 not synched" ); // Mint tokens to ensure sufficient balance - auto mint_result = node_proc1->MintTokens( 20000000000, "", "", TokenID::FromBytes( { 0x00 } ) ); + 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 From b356fbae24a2582878d79b92f65ef64fffaab060 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 15 Apr 2026 17:34:20 -0300 Subject: [PATCH 102/114] Fix: Merge errors. Now it builds --- src/account/TransactionManager.cpp | 48 +++++------ src/account/UTXOManager.cpp | 120 +++++++++++++++------------ src/blockchain/Consensus.cpp | 8 +- src/blockchain/ValidatorRegistry.cpp | 2 +- src/blockchain/impl/Blockchain.cpp | 6 +- src/crdt/impl/crdt_datastore.cpp | 6 +- 6 files changed, 102 insertions(+), 88 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 99b5bfe68..410d2a9f1 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -1083,7 +1083,7 @@ namespace sgns return std::errc::invalid_argument; } - OUTCOME_TRY( ( this->*it->second.first )( tx ) ); + BOOST_OUTCOME_TRY( ( this->*it->second.first )( tx ) ); if ( DoesTransactionMutateUTXOState( tx ) && utxo_state_tracking_suppression_.load() == 0 ) { UpdateAccountUTXOState( CollectTouchedAccounts( tx ), true ); @@ -1105,7 +1105,7 @@ namespace sgns 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 ); + BOOST_OUTCOME_TRY( revert_result ); if ( DoesTransactionMutateUTXOState( tx ) && utxo_state_tracking_suppression_.load() == 0 ) { UpdateAccountUTXOState( CollectTouchedAccounts( tx ), false ); @@ -1172,7 +1172,7 @@ namespace sgns TransactionManager::AccountUTXOState TransactionManager::GetOrInitAccountUTXOState( const std::string &address ) const { - const auto current_root = utxo_manager_.ComputeUTXOMerkleRoot( address ); + const auto current_root = account_m->GetUTXOManager().ComputeUTXOMerkleRoot( address ); std::unique_lock state_lock( account_utxo_state_mutex_ ); auto &state = account_utxo_state_[address]; @@ -1201,7 +1201,7 @@ namespace sgns { continue; } - roots.emplace( address, utxo_manager_.ComputeUTXOMerkleRoot( address ) ); + roots.emplace( address, account_m->GetUTXOManager().ComputeUTXOMerkleRoot( address ) ); } std::unique_lock state_lock( account_utxo_state_mutex_ ); @@ -1379,7 +1379,7 @@ namespace sgns tx_key ); next_tx_state = TransactionStatus::CONFIRMED; } - OUTCOME_TRY( ChangeTransactionState( transaction, next_tx_state ) ); + BOOST_OUTCOME_TRY( ChangeTransactionState( transaction, next_tx_state ) ); return outcome::success(); } @@ -1567,7 +1567,7 @@ namespace sgns for ( std::uint32_t i = 0; i < outputs.size(); ++i ) { const auto &dest_info = outputs[i]; - OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, i, dest_info.dest_address ) ) + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, i, dest_info.dest_address ) ) } if ( !inputs.empty() ) { @@ -1869,7 +1869,7 @@ namespace sgns if ( has_local_utxos ) { - auto checkpoint_result = utxo_manager_.LoadLatestCheckpoint( account_m->GetAddress() ); + auto checkpoint_result = account_m->GetUTXOManager().LoadLatestCheckpoint( account_m->GetAddress() ); if ( checkpoint_result.has_error() ) { TransactionManagerLogger()->warn( @@ -1880,7 +1880,7 @@ namespace sgns } else if ( checkpoint_result.value().has_value() ) { - const auto local_root = utxo_manager_.ComputeUTXOMerkleRoot( account_m->GetAddress() ); + const auto local_root = account_m->GetUTXOManager().ComputeUTXOMerkleRoot( account_m->GetAddress() ); if ( local_root != checkpoint_result.value()->utxo_merkle_root ) { TransactionManagerLogger()->warn( @@ -1888,7 +1888,7 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m ); - auto clear_result = utxo_manager_.SetUTXOs( std::vector{}, account_m->GetAddress() ); + auto clear_result = account_m->GetUTXOManager().SetUTXOs( std::vector{}, account_m->GetAddress() ); if ( clear_result.has_error() ) { TransactionManagerLogger()->error( @@ -2844,7 +2844,7 @@ namespace sgns full_node_m, key ); tx_lock.unlock(); - OUTCOME_TRY( ChangeTransactionState( new_tx, TransactionStatus::FAILED ) ); + BOOST_OUTCOME_TRY( ChangeTransactionState( new_tx, TransactionStatus::FAILED ) ); tx_lock.lock(); return outcome::failure( boost::system::error_code{} ); } @@ -2880,10 +2880,10 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m, conflicting_tx.value()->GetHash() ); - OUTCOME_TRY( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ) ); + BOOST_OUTCOME_TRY( ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED ) ); } } - OUTCOME_TRY( ChangeTransactionState( new_tx, next_tx_state ) ); + BOOST_OUTCOME_TRY( ChangeTransactionState( new_tx, next_tx_state ) ); return outcome::success(); } @@ -3284,7 +3284,7 @@ namespace sgns 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 ); + if ( auto checkpoint_res = account_m->GetUTXOManager().CreateCheckpoint( registry_epoch, tx_hash_bin.value(), registry_hash ); checkpoint_res.has_error() ) { TransactionManagerLogger()->error( @@ -3458,7 +3458,7 @@ namespace sgns return false; } - if ( !utxo_manager_.VerifyParameters( params, address ) ) + if ( !account_m->GetUTXOManager().VerifyParameters( params, address ) ) { TransactionManagerLogger()->error( "[{} - full: {}] {}: VerifyParameters failed for address {}", account_m->GetAddress().substr( 0, 8 ), @@ -3808,7 +3808,7 @@ namespace sgns } const auto chain_id = GetValidationChainId( tx ); const auto &validator = GetInputValidator( chain_id ); - return validator.ValidateUTXOParameters( params_opt.value(), tx->GetSrcAddress(), utxo_manager_ ); + return validator.ValidateUTXOParameters( params_opt.value(), tx->GetSrcAddress(), account_m->GetUTXOManager() ); } return true; @@ -4006,7 +4006,7 @@ namespace sgns produced_payloads.push_back( SerializeUTXOLeafPayload( produced_output ) ); } - const auto produced_outputs_root = utxo_manager_.ComputeUTXOMerkleRootFromSnapshot( produced_outputs ); + const auto produced_outputs_root = account_m->GetUTXOManager().ComputeUTXOMerkleRootFromSnapshot( produced_outputs ); const auto produced_outputs_root_from_payloads = utxo_merkle::ComputeMerkleRootFromPayloads( std::move( produced_payloads ) ); if ( produced_outputs_root != produced_outputs_root_from_payloads ) @@ -4046,7 +4046,7 @@ namespace sgns }; std::vector leaves; - auto utxos = utxo_manager_.GetUTXOsForReservation( tx->GetSrcAddress(), tx->GetHash() ); + auto utxos = account_m->GetUTXOManager().GetUTXOsForReservation( tx->GetSrcAddress(), tx->GetHash() ); leaves.reserve( utxos.size() ); for ( const auto &utxo : utxos ) { @@ -4345,9 +4345,9 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); - OUTCOME_TRY( RevertTransaction( tx ) ); + BOOST_OUTCOME_TRY( RevertTransaction( tx ) ); - OUTCOME_TRY( DeleteTransaction( key, tx->GetTopics() ) ); + BOOST_OUTCOME_TRY( DeleteTransaction( key, tx->GetTopics() ) ); account_m->RollBackPeerConfirmedNonce( it->second.cached_nonce, tx->GetSrcAddress() ); } @@ -4364,7 +4364,7 @@ namespace sgns __func__, tx->GetHash() ); tx_lock.unlock(); - OUTCOME_TRY( blockchain_->TryResumeProposal( tx->GetHash() ) ); + BOOST_OUTCOME_TRY( blockchain_->TryResumeProposal( tx->GetHash() ) ); TransactionManagerLogger()->debug( "[{} - full: {}] {}: Resumed the proposal handling to transaction {}", account_m->GetAddress().substr( 0, 8 ), @@ -4396,7 +4396,7 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); - OUTCOME_TRY( ParseTransaction( tx ) ); + BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); account_m->SetPeerConfirmedNonce( tx->GetNonce(), tx->GetSrcAddress() ); { std::lock_guard missing_lock( missing_tx_mutex_ ); @@ -4428,9 +4428,9 @@ namespace sgns full_node_m, __func__, tx->GetHash() ); - OUTCOME_TRY( RevertTransaction( tx ) ); + BOOST_OUTCOME_TRY( RevertTransaction( tx ) ); - OUTCOME_TRY( DeleteTransaction( key, tx->GetTopics() ) ); + BOOST_OUTCOME_TRY( DeleteTransaction( key, tx->GetTopics() ) ); account_m->RollBackPeerConfirmedNonce( it->second.cached_nonce, tx->GetSrcAddress() ); } @@ -4440,7 +4440,7 @@ namespace sgns auto params_opt = tx->GetUTXOParametersOpt(); if ( params_opt.has_value() ) { - utxo_manager_.RollbackUTXOs( params_opt->first, tx->GetHash() ); + account_m->GetUTXOManager().RollbackUTXOs( params_opt->first, tx->GetHash() ); } } tx_processed_m[key] = TrackedTx{ tx, TransactionStatus::FAILED, tx->GetNonce() }; diff --git a/src/account/UTXOManager.cpp b/src/account/UTXOManager.cpp index 5d25566ff..f1276193e 100644 --- a/src/account/UTXOManager.cpp +++ b/src/account/UTXOManager.cpp @@ -21,7 +21,10 @@ namespace sgns std::string BuildUTXORecordKey( const std::string &owner_address, const OutPoint &outpoint ) { - return fmt::format( "/utxo/{}/{}:{}", owner_address, outpoint.txid_hash_.toReadableString(), outpoint.output_idx_ ); + return fmt::format( "/utxo/{}/{}:{}", + owner_address, + outpoint.txid_hash_.toReadableString(), + outpoint.output_idx_ ); } std::string BuildCheckpointRecordKey( const std::string &owner_address, uint64_t epoch ) @@ -61,7 +64,7 @@ namespace sgns UTXOManager::UTXOState FromProtoState( SGTransaction::UTXOEntryState state ) { return state == SGTransaction::UTXO_ENTRY_CONSUMED ? UTXOManager::UTXOState::UTXO_CONSUMED - : UTXOManager::UTXOState::UTXO_READY; + : UTXOManager::UTXOState::UTXO_READY; } base::Hash256 ComputeMerkleRootFromUTXOList( std::vector unspent ) @@ -173,14 +176,17 @@ namespace sgns return false; } - utxo_outpoints_[outpoint] = UTXOEntry{ UTXOState::UTXO_READY, new_utxo }; + utxo_outpoints_[outpoint] = + UTXOEntry{ UTXOState::UTXO_READY, new_utxo, 0, std::nullopt, std::nullopt }; address_outpoints_[address].push_back( outpoint ); BOOST_OUTCOME_TRY( StoreUTXOs( address ) ); return true; } - outcome::result UTXOManager::DeleteUTXO( const base::Hash256 &utxo_id, uint32_t output_idx, 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_ ) @@ -191,12 +197,12 @@ namespace sgns std::unique_lock lock( utxos_mutex_ ); 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; } ); + 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; @@ -239,7 +245,8 @@ namespace sgns if ( !utxo_found ) { GeniusUTXO consumed_utxo( input_info.txid_hash_, input_info.output_idx_, 0, TokenID(), address ); - utxo_outpoints_[outpoint] = UTXOEntry{ UTXOState::UTXO_CONSUMED, consumed_utxo }; + utxo_outpoints_[outpoint] = + UTXOEntry{ UTXOState::UTXO_CONSUMED, consumed_utxo, 0, std::nullopt, std::nullopt }; } consumed = consumed && utxo_found; @@ -310,7 +317,7 @@ namespace sgns std::unordered_map> UTXOManager::GetAllUTXOs() const { - std::shared_lock lock( utxos_mutex_ ); + std::shared_lock lock( utxos_mutex_ ); std::unordered_map> result; for ( const auto &[outpoint, entry] : utxo_outpoints_ ) { @@ -350,7 +357,8 @@ namespace sgns 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 }; + utxo_outpoints_[outpoint] = + UTXOEntry{ UTXOState::UTXO_READY, owned_utxo, 0, std::nullopt, std::nullopt }; outpoints.push_back( outpoint ); } @@ -495,13 +503,18 @@ namespace sgns if ( utxo_it->second.state != UTXOState::UTXO_READY ) { - logger_->warn( "Outpoint {}:{} is not spendable", input.txid_hash_.toReadableString(), input.output_idx_ ); + 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 ); + logger_->warn( "Outpoint {}:{} does not belong to {}", + input.txid_hash_.toReadableString(), + input.output_idx_, + address ); return false; } @@ -625,19 +638,21 @@ namespace sgns const auto state = FromProtoState( entry_record.state() ); - BOOST_OUTCOME_TRY( auto hash, - base::Hash256::fromSpan( - gsl::span( reinterpret_cast( const_cast( entry_record.utxo().hash().data() ) ), - entry_record.utxo().hash().size() ) ) ); + BOOST_OUTCOME_TRY( + auto hash, + base::Hash256::fromSpan( + gsl::span( reinterpret_cast( const_cast( entry_record.utxo().hash().data() ) ), + entry_record.utxo().hash().size() ) ) ); - auto token_id = TokenID::FromBytes( entry_record.utxo().token().data(), entry_record.utxo().token().size() ); + 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; + UTXOEntry loaded_entry; loaded_entry.state = state; loaded_entry.utxo = loaded_utxo; loaded_entry.created_epoch = entry_record.created_epoch(); @@ -647,10 +662,10 @@ namespace sgns } 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() ) ) ); + BOOST_OUTCOME_TRY( auto spent_by_hash, + base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( + entry_record.spent_by_txid().data() ) ), + entry_record.spent_by_txid().size() ) ) ); loaded_entry.spent_by_txid = spent_by_hash; } @@ -702,8 +717,8 @@ namespace sgns SGTransaction::UTXOEntryRecord entry_record; auto *utxo_proto = entry_record.mutable_utxo(); - const auto txid = entry.utxo.GetTxID(); - const auto token_id = entry.utxo.GetTokenID(); + 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() ); @@ -719,7 +734,8 @@ namespace sgns 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() ); + 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() ) ); @@ -744,7 +760,7 @@ namespace sgns return outcome::success(); } - outcome::result UTXOManager::CreateCheckpoint( uint64_t epoch, + outcome::result UTXOManager::CreateCheckpoint( uint64_t epoch, const base::Hash256 &last_finalized_tx, const base::Hash256 ®istry_hash ) { @@ -809,7 +825,7 @@ namespace sgns return std::errc::bad_message; } - const auto checkpoint_key = BuildCheckpointRecordKey( address, epoch ); + 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() ) @@ -822,16 +838,14 @@ namespace sgns 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() ) + if ( auto put_latest_res = db_->put( latest_pointer_key_buf, latest_pointer_value_buf ); + put_latest_res.has_error() ) { logger_->error( "Failed to store checkpoint latest pointer for address {}", address ); return put_latest_res.error(); } - logger_->info( "Created checkpoint owner={} epoch={} utxo_count={}", - address, - epoch, - unspent_snapshot.size() ); + logger_->info( "Created checkpoint owner={} epoch={} utxo_count={}", address, epoch, unspent_snapshot.size() ); return outcome::success(); } @@ -883,27 +897,27 @@ namespace sgns 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() ) ) ); + BOOST_OUTCOME_TRY( auto last_finalized_tx_hash, + base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( + checkpoint_record.last_finalized_tx().data() ) ), + checkpoint_record.last_finalized_tx().size() ) ) ); + BOOST_OUTCOME_TRY( auto registry_hash, + base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( + checkpoint_record.registry_hash().data() ) ), + checkpoint_record.registry_hash().size() ) ) ); + BOOST_OUTCOME_TRY( auto utxo_root_hash, + base::Hash256::fromSpan( gsl::span( reinterpret_cast( const_cast( + checkpoint_record.utxo_merkle_root().data() ) ), + checkpoint_record.utxo_merkle_root().size() ) ) ); UTXOCheckpoint checkpoint; - checkpoint.owner_address = checkpoint_record.owner_address(); - checkpoint.epoch = checkpoint_record.epoch(); + checkpoint.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(); + 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 }; } diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index d5d8905c5..32f6336f1 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -655,7 +655,7 @@ namespace sgns __func__, proposal.proposal_id().substr( 0, 8 ), GetPrintableSubjectHash( subject ) ); - OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); + BOOST_OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); proposal.set_signature( signature.data(), signature.size() ); ConsensusManagerLogger()->debug( "{}: success for hash {} proposal_id={}", @@ -698,7 +698,7 @@ namespace sgns return outcome::failure( signing_bytes.error() ); } - OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); + BOOST_OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); vote.set_signature( signature.data(), signature.size() ); ConsensusManagerLogger()->debug( "{}: {} voted for proposal_id={}", @@ -744,7 +744,7 @@ namespace sgns return outcome::failure( signing_bytes.error() ); } - OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); + BOOST_OUTCOME_TRY( auto &&signature, sign( signing_bytes.value() ) ); bundle.set_signature( signature.data(), signature.size() ); ConsensusManagerLogger()->debug( @@ -2388,7 +2388,7 @@ namespace sgns { const auto key = std::string{ CERTIFICATE_BASE_PATH_KEY } + subject_hash; - OUTCOME_TRY( auto certificate_data, db_->Get( { key } ) ); + BOOST_OUTCOME_TRY( auto certificate_data, db_->Get( { key } ) ); Certificate certificate; if ( !certificate.ParseFromArray( certificate_data.data(), certificate_data.size() ) ) diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 045b30d77..0116f7b9f 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -496,7 +496,7 @@ namespace sgns { ValidatorRegistryLogger()->trace( "{}: entry cid={}", __func__, cid ); - OUTCOME_TRY( auto cid_content, db_->GetCIDContent( cid ) ); + BOOST_OUTCOME_TRY( auto cid_content, db_->GetCIDContent( cid ) ); ValidatorRegistryLogger()->trace( "{}: Got CID content with {} entries ", __func__, cid_content.size() ); crdt::HierarchicalKey registry_key{ std::string( RegistryKey() ) }; for ( auto &[key, registry_content] : cid_content ) diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 5473681a5..2135c2e95 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -318,7 +318,7 @@ namespace sgns case 2: { sgns::crdt::GlobalDB::Buffer registry_cid_key; - registry_cid_key.put( std::string( blockchain::ValidatorRegistry::RegistryCidKey() ) ); + registry_cid_key.put( std::string( ValidatorRegistry::RegistryCidKey() ) ); auto registry_cid = strong->db_->GetDataStore()->get( registry_cid_key ); if ( registry_cid.has_value() ) { @@ -1679,9 +1679,9 @@ namespace sgns const std::optional &utxo_commitment, const std::optional &utxo_witness ) { - OUTCOME_TRY( auto &&nonce_subject, + BOOST_OUTCOME_TRY( auto &&nonce_subject, CreateConsensusNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ) ); - OUTCOME_TRY( auto &&nonce_proposal, + BOOST_OUTCOME_TRY( auto &&nonce_proposal, consensus_manager_->CreateProposal( nonce_subject, account_id, validator_registry_->GetRegistryCid(), diff --git a/src/crdt/impl/crdt_datastore.cpp b/src/crdt/impl/crdt_datastore.cpp index 607cb8480..0aa1b70f7 100644 --- a/src/crdt/impl/crdt_datastore.cpp +++ b/src/crdt/impl/crdt_datastore.cpp @@ -1808,12 +1808,12 @@ namespace sgns::crdt outcome::result>> CrdtDatastore::GetILPDNodeContent( const std::string &cid_string ) { - OUTCOME_TRY( auto cid, CID::fromString( cid_string ) ); + BOOST_OUTCOME_TRY( auto cid, CID::fromString( cid_string ) ); - OUTCOME_TRY( auto node, dagSyncer_->GetNodeWithoutRequest( cid ) ); + BOOST_OUTCOME_TRY( auto node, dagSyncer_->GetNodeWithoutRequest( cid ) ); //TODO - Check if filtering is needed here. Currently not filtering. - OUTCOME_TRY( auto delta, GetDeltaFromNode( *node, true ) ); + BOOST_OUTCOME_TRY( auto delta, GetDeltaFromNode( *node, true ) ); //TODO - Maybe check tombstones, right now just grabbing elements. std::vector elements( delta.elements().begin(), delta.elements().end() ); From 4697a4208516b5d56b88d149cac946906183d7a7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 17 Apr 2026 17:06:19 -0300 Subject: [PATCH 103/114] Fix: Unique mint utxo hash --- .../blockchain/blockchain_genesis_test.cpp | 15 +----- test/src/multiaccount/multi_account_sync.cpp | 30 ++++------- .../processing_multi_test.cpp | 16 ++---- .../processing_nodes/child_tokens_test.cpp | 22 +++----- test/src/processing_nodes/full_node_test.cpp | 15 +----- .../processing_nodes_test.cpp | 20 ++------ .../transaction_crash_test.cpp | 14 +---- .../transaction_sync_test.cpp | 26 +++------- test/testutil/mint_source_hash.hpp | 51 +++++++++++++++++++ 9 files changed, 90 insertions(+), 119 deletions(-) create mode 100644 test/testutil/mint_source_hash.hpp diff --git a/test/src/blockchain/blockchain_genesis_test.cpp b/test/src/blockchain/blockchain_genesis_test.cpp index 09da08969..07bb06e15 100644 --- a/test/src/blockchain/blockchain_genesis_test.cpp +++ b/test/src/blockchain/blockchain_genesis_test.cpp @@ -28,20 +28,9 @@ #include "FileManager.hpp" #include #include +#include "testutil/mint_source_hash.hpp" #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: @@ -314,7 +303,7 @@ TEST_F( BlockchainGenesisTest, WithAuthorizationCanSyncAndProcessTransactions ) // Mint tokens on the first regular node after sync is confirmed auto mint_result = node_regular_1->MintTokens( mint_amount, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", token_id, "", diff --git a/test/src/multiaccount/multi_account_sync.cpp b/test/src/multiaccount/multi_account_sync.cpp index c341c8e9f..c96233f7c 100644 --- a/test/src/multiaccount/multi_account_sync.cpp +++ b/test/src/multiaccount/multi_account_sync.cpp @@ -32,6 +32,7 @@ #include "FileManager.hpp" #include #include +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" #include "blockchain/ValidatorRegistry.hpp" @@ -40,17 +41,6 @@ class MultiAccountTest : public ::testing::Test protected: static constexpr std::string_view FILE_PREFIX = "node_multi_account_"; - 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, @@ -184,7 +174,7 @@ TEST_F( MultiAccountTest, DISABLED_SyncThroughEachOther ) auto balance_original_start = node_original->GetBalance(); // Mint some tokens auto mint_result = node_original->MintTokens( 100, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", @@ -192,14 +182,14 @@ TEST_F( MultiAccountTest, DISABLED_SyncThroughEachOther ) ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_original"; mint_result = node_original->MintTokens( 2000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out on node_original"; mint_result = node_original->MintTokens( 30, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", @@ -224,7 +214,7 @@ TEST_F( MultiAccountTest, DISABLED_SyncThroughEachOther ) "node_duplicated not synced" ); mint_result = node_duplicated->MintTokens( 60000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", @@ -321,7 +311,7 @@ TEST_F( MultiAccountTest, DISABLED_CRDTFilterDuplicateTx ) std::cout << "Minting tokens on isolated nodes..." << std::endl; auto mint_result_1 = node_same_addr_1->MintTokens( 50000000000, // 50 GNUS - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -617,7 +607,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) auto epoch_before = registry_state.value().epoch(); auto cid_before = registry->GetRegistryCid(); - auto mint1 = node_client->MintTokens( 100, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + auto mint1 = node_client->MintTokens( 100, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; fmt::println( "Mint 1 succeeded" ); @@ -629,7 +619,7 @@ TEST_F( MultiAccountTest, NodeConsensusTest ) epoch_before = registry_state.value().epoch(); cid_before = registry->GetRegistryCid(); - auto mint2 = node_client->MintTokens( 250, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + auto mint2 = node_client->MintTokens( 250, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint2.has_value() ) << "Mint 2 failed on node_client"; fmt::println( "Mint 2 succeeded" ); assert_registry_updated( epoch_before, cid_before ); @@ -792,11 +782,11 @@ TEST_F( MultiAccountTest, NodeConsensusBatch5Test ) } }; - auto mint1 = node_client->MintTokens( 100, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + auto mint1 = node_client->MintTokens( 100, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint1.has_value() ) << "Mint 1 failed on node_client"; assert_registry_immutable( "tx1" ); - auto mint2 = node_client->MintTokens( 250, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + auto mint2 = node_client->MintTokens( 250, sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); ASSERT_TRUE( mint2.has_value() ) << "Mint 2 failed on node_client"; assert_registry_immutable( "tx2" ); diff --git a/test/src/processing_multi/processing_multi_test.cpp b/test/src/processing_multi/processing_multi_test.cpp index d270508c5..0261c117d 100644 --- a/test/src/processing_multi/processing_multi_test.cpp +++ b/test/src/processing_multi/processing_multi_test.cpp @@ -25,17 +25,7 @@ #include "FileManager.hpp" #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 +#include "testutil/mint_source_hash.hpp" class ProcessingMultiTest : public ::testing::Test { @@ -148,13 +138,13 @@ std::string ProcessingMultiTest::binary_path = ""; TEST_F( ProcessingMultiTest, MintTokens ) { node_main->MintTokens( 50000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); node_main->MintTokens( 50000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/processing_nodes/child_tokens_test.cpp b/test/src/processing_nodes/child_tokens_test.cpp index 33aa9a39c..f534475b2 100644 --- a/test/src/processing_nodes/child_tokens_test.cpp +++ b/test/src/processing_nodes/child_tokens_test.cpp @@ -13,6 +13,7 @@ #include "account/GeniusNode.hpp" #include "account/GeniusAccount.hpp" #include "account/TokenID.hpp" +#include "testutil/mint_source_hash.hpp" #include "blockchain/Blockchain.hpp" #include "testutil/wait_condition.hpp" #include @@ -23,15 +24,6 @@ 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. @@ -189,7 +181,7 @@ TEST( TransferTokenValue, ThreeNodeTransferTest ) // Ensure enough balance with +1 change auto mintRes51 = node51->MintTokens( totalMint51 + 1, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x51 } ), "", @@ -199,7 +191,7 @@ TEST( TransferTokenValue, ThreeNodeTransferTest ) auto mintRes52 = node52->MintTokens( totalMint52 + 1, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x52 } ), "", @@ -294,7 +286,7 @@ TEST_P( GeniusNodeMintMainTest, MintMainBalance ) ASSERT_TRUE( parsedInitialChild.has_value() ); auto res = node->MintTokens( p.mintMain, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", p.TokenID, "", @@ -373,7 +365,7 @@ TEST_P( GeniusNodeMintChildTest, MintChildBalance ) ASSERT_TRUE( parsedMint.has_value() ); auto res = node->MintTokens( parsedMint.value(), - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", p.TokenID, "", @@ -454,7 +446,7 @@ TEST( GeniusNodeMultiTokenMintTest, MintMultipleTokenIds ) for ( const auto &tm : mints ) { - auto res = node->MintTokens( tm.amount, NextMintSourceHash(), "", tm.tokenId, + auto res = node->MintTokens( tm.amount, sgns::test::NextMintSourceHash(), "", tm.tokenId, "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( res.has_value() ); // << "MintTokens failed for token=" << tm.tokenId << " amount=" << tm.amount; @@ -513,7 +505,7 @@ TEST_F( ProcessingNodesModuleTest, SinglePostProcessing ) auto mintResMain = node_main->MintTokens( 1000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/processing_nodes/full_node_test.cpp b/test/src/processing_nodes/full_node_test.cpp index f9bc7c704..6f07afe18 100644 --- a/test/src/processing_nodes/full_node_test.cpp +++ b/test/src/processing_nodes/full_node_test.cpp @@ -8,23 +8,12 @@ #include #include "account/GeniusNode.hpp" #include "account/TokenID.hpp" +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" #include "local_secure_storage/impl/json/JSONSecureStorage.hpp" 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 @@ -111,7 +100,7 @@ TEST( NodeBalancePersistenceTest, BalancePersistsAfterRecreation ) for ( size_t i = 0; i < mintAmount; ++i ) { auto mintRes = originalNode->MintTokens( 500000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/processing_nodes/processing_nodes_test.cpp b/test/src/processing_nodes/processing_nodes_test.cpp index d020c6617..2700f8759 100644 --- a/test/src/processing_nodes/processing_nodes_test.cpp +++ b/test/src/processing_nodes/processing_nodes_test.cpp @@ -10,21 +10,11 @@ #include "account/GeniusNode.hpp" #include #include +#include "testutil/mint_source_hash.hpp" #include "testutil/wait_condition.hpp" 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,13 +156,13 @@ TEST_F( ProcessingNodesTest, DISABLED_ProcessNodesTransactionsCount ) std::chrono::milliseconds( 20000 ), "Node proc 2 not synched" ); node_main->MintTokens( 50000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); node_main->MintTokens( 50000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -483,7 +473,7 @@ TEST_F( ProcessingNodesTest, PostProcessing ) auto mint_result = node_main->MintTokens( 50000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -523,7 +513,7 @@ TEST_F( ProcessingNodesTest, PostProcessing ) { auto result = node_proc1->GetBalance() + node_proc2->GetBalance(); auto expected_peer_gain = ( ( cost * 65 ) / 100 ) / 2; - return balance_node1 + balance_node2 + 2 * expected_peer_gain; + return result == balance_node1 + balance_node2 + 2 * expected_peer_gain; }, std::chrono::milliseconds( 40000 ), "Balances not updated in time" ); diff --git a/test/src/transaction_sync/transaction_crash_test.cpp b/test/src/transaction_sync/transaction_crash_test.cpp index c7692ee2b..79d2cc0bd 100644 --- a/test/src/transaction_sync/transaction_crash_test.cpp +++ b/test/src/transaction_sync/transaction_crash_test.cpp @@ -10,20 +10,10 @@ #include #include #include "account/GeniusNode.hpp" +#include "testutil/mint_source_hash.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 * @brief Verifies transaction synchronization after a node crash and recovery, @@ -122,7 +112,7 @@ namespace sgns std::cout << "Minting the required tokens" << std::endl; auto mint_result = node1->MintTokens( total_amount, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ), "", diff --git a/test/src/transaction_sync/transaction_sync_test.cpp b/test/src/transaction_sync/transaction_sync_test.cpp index dec9241ce..3aef1209a 100644 --- a/test/src/transaction_sync/transaction_sync_test.cpp +++ b/test/src/transaction_sync/transaction_sync_test.cpp @@ -20,19 +20,9 @@ #include #include "account/TransferTransaction.hpp" #include "proof/TransferProof.hpp" +#include "testutil/mint_source_hash.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 @@ -177,7 +167,7 @@ TEST_F( TransactionSyncTest, TransactionSimpleTransfer ) // Mint tokens with timeout auto mint_result = node_proc1->MintTokens( 10000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -255,7 +245,7 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) for ( auto amount : mint_amounts ) { auto mint_result = node_proc1->MintTokens( amount, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -269,7 +259,7 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) // Mint tokens on node_proc2 auto mint_result1 = node_proc2->MintTokens( 10000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -278,7 +268,7 @@ TEST_F( TransactionSyncTest, TransactionMintSync ) auto mint_result2 = node_proc2->MintTokens( 20000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -453,14 +443,14 @@ TEST_F( TransactionSyncTest, InvalidTransactionTest ) // Mint tokens with timeout auto mint_result = node_proc1->MintTokens( 10000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", std::chrono::milliseconds( GeniusNode::TIMEOUT_MINT ) ); ASSERT_TRUE( mint_result.has_value() ) << "Mint transaction failed or timed out"; mint_result = node_proc1->MintTokens( 10000000000, - NextMintSourceHash(), + sgns::test::NextMintSourceHash(), "", sgns::TokenID::FromBytes( { 0x00 } ), "", @@ -554,7 +544,7 @@ TEST_F( TransactionSyncTest, InvalidPreviousHashTest ) // Mint tokens to ensure sufficient balance auto mint_result = - node_proc1->MintTokens( 20000000000, NextMintSourceHash(), "", TokenID::FromBytes( { 0x00 } ) ); + node_proc1->MintTokens( 20000000000, sgns::test::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 diff --git a/test/testutil/mint_source_hash.hpp b/test/testutil/mint_source_hash.hpp new file mode 100644 index 000000000..c5b0eb8ad --- /dev/null +++ b/test/testutil/mint_source_hash.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace sgns::test +{ + inline std::string NextMintSourceHash() + { + static std::atomic counter{ 0 }; + static std::mutex rng_mutex; + static std::mt19937_64 rng( [] + { + const auto now = static_cast( + std::chrono::high_resolution_clock::now().time_since_epoch().count() ); + std::random_device rd; + std::seed_seq seed{ static_cast( now ), + static_cast( now >> 32 ), + rd(), + rd(), + rd(), + rd() }; + return std::mt19937_64( seed ); + }() ); + + const uint64_t sequence = counter.fetch_add( 1, std::memory_order_relaxed ); + uint64_t random_hi; + uint64_t random_lo; + + { + std::lock_guard lock( rng_mutex ); + random_hi = rng(); + random_lo = rng(); + } + + char buffer[65] = {}; + std::snprintf( buffer, + sizeof( buffer ), + "%016llx%016llx%016llx%016llx", + static_cast( random_hi ), + static_cast( random_lo ), + static_cast( sequence ), + static_cast( sequence ^ random_hi ^ random_lo ) ); + return std::string( buffer ); + } +} // namespace sgns::test From a28e7f6c15172932a7c9c7c4dc38316666785049 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 17 Apr 2026 17:21:00 -0300 Subject: [PATCH 104/114] Fix: Topics can be inserted in the atomic transaction --- src/crdt/atomic_transaction.hpp | 17 +++++++++++ src/crdt/impl/atomic_transaction.cpp | 43 ++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/crdt/atomic_transaction.hpp b/src/crdt/atomic_transaction.hpp index ef4ddb0d7..7437999ec 100644 --- a/src/crdt/atomic_transaction.hpp +++ b/src/crdt/atomic_transaction.hpp @@ -7,6 +7,7 @@ #include "outcome/outcome.hpp" #include "primitives/cid/cid.hpp" #include +#include #include #include #include @@ -75,6 +76,20 @@ namespace sgns::crdt */ bool HasKey( const HierarchicalKey &key ) const; + /** + * @brief Add a single topic to this transaction's internal topic set. + * @param topic topic name to add + * @return outcome::success or failure if already committed + */ + outcome::result AddTopic( const std::string &topic ); + + /** + * @brief Add multiple topics to this transaction's internal topic set. + * @param topics topic names to add + * @return outcome::success or failure if already committed + */ + outcome::result AddTopics( const std::unordered_set &topics ); + /** * @brief Commits all pending operations atomically. * Combines all pending operations into a single Delta and publishes it. @@ -112,7 +127,9 @@ namespace sgns::crdt std::shared_ptr datastore_; std::vector operations_; std::unordered_set modified_keys_; // Track which keys have been modified + std::unordered_set stored_topics_; // Topics accumulated before commit bool is_committed_; + mutable std::mutex mutex_; }; } // namespace sgns::crdt diff --git a/src/crdt/impl/atomic_transaction.cpp b/src/crdt/impl/atomic_transaction.cpp index 454cf48d3..7488d737c 100644 --- a/src/crdt/impl/atomic_transaction.cpp +++ b/src/crdt/impl/atomic_transaction.cpp @@ -12,14 +12,12 @@ namespace sgns::crdt AtomicTransaction::~AtomicTransaction() { - if ( !is_committed_ ) - { - Rollback(); - } + Rollback(); } outcome::result AtomicTransaction::Put( HierarchicalKey key, Buffer value ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -31,6 +29,7 @@ namespace sgns::crdt outcome::result AtomicTransaction::Remove( const HierarchicalKey &key ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -41,6 +40,7 @@ namespace sgns::crdt outcome::result AtomicTransaction::Get( const HierarchicalKey &key ) const { + std::lock_guard lock( mutex_ ); // First, check pending operations in reverse order (most recent first) auto latest_op = FindLatestOperation( key ); if ( latest_op.has_value() ) @@ -63,6 +63,7 @@ namespace sgns::crdt outcome::result AtomicTransaction::Erase( const HierarchicalKey &key ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -86,11 +87,38 @@ namespace sgns::crdt bool AtomicTransaction::HasKey( const HierarchicalKey &key ) const { + std::lock_guard lock( mutex_ ); return modified_keys_.find( key.GetKey() ) != modified_keys_.end(); } + outcome::result AtomicTransaction::AddTopic( const std::string &topic ) + { + std::lock_guard lock( mutex_ ); + if ( is_committed_ ) + { + return outcome::failure( boost::system::error_code{} ); + } + if ( !topic.empty() ) + { + stored_topics_.insert( topic ); + } + return outcome::success(); + } + + outcome::result AtomicTransaction::AddTopics( const std::unordered_set &topics ) + { + std::lock_guard lock( mutex_ ); + if ( is_committed_ ) + { + return outcome::failure( boost::system::error_code{} ); + } + stored_topics_.insert( topics.begin(), topics.end() ); + return outcome::success(); + } + outcome::result AtomicTransaction::Commit( const std::unordered_set &topics ) { + std::lock_guard lock( mutex_ ); if ( is_committed_ ) { return outcome::failure( boost::system::error_code{} ); @@ -129,7 +157,10 @@ namespace sgns::crdt } combined_delta->set_priority( max_priority ); - auto result = datastore_->Publish( combined_delta, topics ); + auto merged_topics = stored_topics_; + merged_topics.insert( topics.begin(), topics.end() ); + + auto result = datastore_->Publish( combined_delta, merged_topics ); if ( !result.has_failure() ) { is_committed_ = true; @@ -140,8 +171,10 @@ namespace sgns::crdt void AtomicTransaction::Rollback() { + std::lock_guard lock( mutex_ ); operations_.clear(); modified_keys_.clear(); + stored_topics_.clear(); } std::optional AtomicTransaction::FindLatestOperation( From e7e517f949fe9ba17650079419905364f3206f84 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 17 Apr 2026 17:21:37 -0300 Subject: [PATCH 105/114] Fix: Inserting processing topic on the finalization --- src/processing/impl/processing_task_queue_impl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/processing/impl/processing_task_queue_impl.cpp b/src/processing/impl/processing_task_queue_impl.cpp index 90dc2c9a8..e7ca4e593 100644 --- a/src/processing/impl/processing_task_queue_impl.cpp +++ b/src/processing/impl/processing_task_queue_impl.cpp @@ -198,6 +198,7 @@ namespace sgns::processing auto job_completion_transaction = m_db->BeginTransaction(); data.put( taskResult.SerializeAsString() ); BOOST_OUTCOME_TRY( job_completion_transaction->Put( std::move( result_key ), std::move( data ) ) ); + BOOST_OUTCOME_TRY( job_completion_transaction->AddTopic( m_processing_topic ) ); m_logger->debug( "TASK_COMPLETED: {}, results stored", taskKey ); return job_completion_transaction; From 9f3c5ed686964a849585dbf136d0288f9056b132 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 17 Apr 2026 17:23:14 -0300 Subject: [PATCH 106/114] Feat: Creating methods to check if owner is address or hashed job ID --- src/account/UTXOStructs.cpp | 23 +++++++++++++++++++++++ src/account/UTXOStructs.hpp | 9 +++++++++ 2 files changed, 32 insertions(+) diff --git a/src/account/UTXOStructs.cpp b/src/account/UTXOStructs.cpp index f57d8f206..73ce94ee2 100644 --- a/src/account/UTXOStructs.cpp +++ b/src/account/UTXOStructs.cpp @@ -3,6 +3,8 @@ #include "account/proto/SGTransaction.pb.h" #include "base/endian.h" +#include +#include std::vector sgns::InputUTXOInfo::SerializeForSigning() const { @@ -15,3 +17,24 @@ std::vector sgns::InputUTXOInfo::SerializeForSigning() const return vec; } + +bool sgns::utxo_address::IsEscrowLockAddress( std::string_view address ) +{ + if ( address.size() != 66 || address.substr( 0, 2 ) != "0x" ) + { + return false; + } + + return std::all_of( + address.begin() + 2, address.end(), []( unsigned char c ) { return std::isxdigit( c ) != 0; } ); +} + +bool sgns::utxo_address::IsAccountPublicKeyAddress( std::string_view address ) +{ + if ( address.size() != 128 ) + { + return false; + } + + return std::all_of( address.begin(), address.end(), []( unsigned char c ) { return std::isxdigit( c ) != 0; } ); +} diff --git a/src/account/UTXOStructs.hpp b/src/account/UTXOStructs.hpp index 153fb6218..7929b6fcc 100644 --- a/src/account/UTXOStructs.hpp +++ b/src/account/UTXOStructs.hpp @@ -5,6 +5,15 @@ namespace sgns { + namespace utxo_address + { + // Escrow locks are encoded as 0x + 32-byte hash (66 chars total). + bool IsEscrowLockAddress( std::string_view address ); + + // Genius account addresses are full pubkeys encoded as 64 bytes hex (128 chars). + bool IsAccountPublicKeyAddress( std::string_view address ); + } // namespace utxo_address + /** * @brief Raw UTXO input data for a transaction */ From 2fea1ca5c101b8894e93bf56749f7b9a2968cf6a Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 17 Apr 2026 17:32:03 -0300 Subject: [PATCH 107/114] Fix: UTXO validation now ignores the owner if it's a job ID --- src/account/InputValidators.cpp | 6 ++++- src/account/UTXOManager.cpp | 42 +++++++++++++++++++++++++++------ src/account/UTXOManager.hpp | 4 ++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/account/InputValidators.cpp b/src/account/InputValidators.cpp index 1fbbf150c..812ba0fc3 100644 --- a/src/account/InputValidators.cpp +++ b/src/account/InputValidators.cpp @@ -66,6 +66,7 @@ namespace sgns } return utxo_merkle::ComputeMerkleRootFromLeafHashes( std::move( leaf_hashes ) ); } + } // namespace bool GeniusInputValidator::ValidateUTXOParameters( const UTXOTxParameters ¶ms, @@ -302,7 +303,10 @@ namespace sgns return false; } const std::string payload_owner( payload.data() + 40, payload.data() + 40 + owner_len ); - if ( payload_owner != tx->GetSrcAddress() ) + const bool delegated_escrow_spend = + payload_owner != tx->GetSrcAddress() && tx->GetType() == "transfer" && input.output_idx_ == 0 && + utxo_address::IsEscrowLockAddress( payload_owner ) && tx->GetUncleHash() == payload_owner; + if ( payload_owner != tx->GetSrcAddress() && !delegated_escrow_spend ) { return false; } diff --git a/src/account/UTXOManager.cpp b/src/account/UTXOManager.cpp index f1276193e..4576b46c0 100644 --- a/src/account/UTXOManager.cpp +++ b/src/account/UTXOManager.cpp @@ -71,6 +71,7 @@ namespace sgns { return utxo_merkle::ComputeMerkleRootFromUTXOs( unspent ); } + } // namespace uint64_t UTXOManager::GetBalance() const @@ -470,12 +471,6 @@ namespace sgns std::shared_lock lock( utxos_mutex_ ); - if ( address_outpoints_.find( address ) == address_outpoints_.end() ) - { - logger_->warn( "Could not find UTXOs from address {}", address ); - return false; - } - std::unordered_set seen_inputs; seen_inputs.reserve( params.first.size() ); @@ -509,7 +504,12 @@ namespace sgns return false; } - if ( utxo_it->second.utxo.GetOwnerAddress() != address ) + const auto &owner_address = utxo_it->second.utxo.GetOwnerAddress(); + const bool delegated_escrow_spend = owner_address != address && + input.output_idx_ == 0 && + utxo_address::IsEscrowLockAddress( owner_address ); + + if ( owner_address != address && !delegated_escrow_spend ) { logger_->warn( "Outpoint {}:{} does not belong to {}", input.txid_hash_.toReadableString(), @@ -518,6 +518,15 @@ namespace sgns return false; } + if ( delegated_escrow_spend ) + { + logger_->debug( "Allowing delegated escrow spend for outpoint {}:{} by {} (lock owner: {})", + input.txid_hash_.toReadableString(), + input.output_idx_, + address.substr( 0, 8 ), + owner_address ); + } + expected_amount += utxo_it->second.utxo.GetAmount(); } @@ -530,6 +539,25 @@ namespace sgns return real_amount == expected_amount && seen_inputs.size() == params.first.size(); } + std::optional UTXOManager::GetOutPointState( const base::Hash256 &utxo_id, + uint32_t output_idx ) const + { + std::shared_lock lock( utxos_mutex_ ); + const OutPoint outpoint{ utxo_id, output_idx }; + auto it = utxo_outpoints_.find( outpoint ); + if ( it == utxo_outpoints_.end() ) + { + return std::nullopt; + } + return it->second.state; + } + + bool UTXOManager::IsOutPointConsumed( const base::Hash256 &utxo_id, uint32_t output_idx ) const + { + auto state = GetOutPointState( utxo_id, output_idx ); + return state.has_value() && state.value() == UTXOState::UTXO_CONSUMED; + } + base::Hash256 UTXOManager::ComputeUTXOMerkleRoot() const { return ComputeUTXOMerkleRoot( address_ ); diff --git a/src/account/UTXOManager.hpp b/src/account/UTXOManager.hpp index 62b593ea5..73371890e 100644 --- a/src/account/UTXOManager.hpp +++ b/src/account/UTXOManager.hpp @@ -169,6 +169,10 @@ namespace sgns bool VerifyParameters( const UTXOTxParameters ¶ms, const std::string &address ) const; + std::optional GetOutPointState( const base::Hash256 &utxo_id, uint32_t output_idx ) const; + + bool IsOutPointConsumed( const base::Hash256 &utxo_id, uint32_t output_idx ) const; + /** * @brief Compute a deterministic Merkle root for unspent UTXOs owned by this node address */ From 18cc3e0b98ba0d7341d37cf1a30c8dda27b7cc80 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 17 Apr 2026 17:43:12 -0300 Subject: [PATCH 108/114] Fix: Removing escrow release and fixing payout with consensus --- src/account/CMakeLists.txt | 1 - src/account/EscrowReleaseTransaction.cpp | 170 ----------------- src/account/EscrowReleaseTransaction.hpp | 157 ---------------- src/account/Migration0_2_0To1_0_0.cpp | 6 - src/account/Migration1_0_0To3_4_0.cpp | 6 - src/account/Migration3_4_0To3_5_0.cpp | 6 - src/account/Migration3_5_0To3_6_0.cpp | 6 - src/account/TransactionManager.cpp | 229 +++++++++++------------ src/account/TransactionManager.hpp | 8 +- 9 files changed, 107 insertions(+), 482 deletions(-) delete mode 100644 src/account/EscrowReleaseTransaction.cpp delete mode 100644 src/account/EscrowReleaseTransaction.hpp diff --git a/src/account/CMakeLists.txt b/src/account/CMakeLists.txt index 9622915d5..513a3c78f 100644 --- a/src/account/CMakeLists.txt +++ b/src/account/CMakeLists.txt @@ -59,7 +59,6 @@ add_library(genius_node MintTransactionV2.cpp ProcessingTransaction.cpp EscrowTransaction.cpp - EscrowReleaseTransaction.cpp InputValidators.cpp TransactionManager.cpp TokenAmount.cpp diff --git a/src/account/EscrowReleaseTransaction.cpp b/src/account/EscrowReleaseTransaction.cpp deleted file mode 100644 index 69b6efa7f..000000000 --- a/src/account/EscrowReleaseTransaction.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @file EscrowReleaseTransaction.cpp - * @brief Implementation of the EscrowReleaseTransaction class. - */ - -#include "account/EscrowReleaseTransaction.hpp" -#include "account/proto/SGTransaction.pb.h" -#include "crypto/hasher/hasher_impl.hpp" -#include "base/blob.hpp" -#include - -namespace sgns -{ - - EscrowReleaseTransaction::EscrowReleaseTransaction( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ) : - IGeniusTransactions( "escrow-release", SetDAGWithType( std::move( dag ), "escrow-release" ) ), - utxo_params_( std::move( params ) ), - release_amount_( release_amount ), - release_address_( std::move( release_address ) ), - escrow_source_( std::move( escrow_source ) ), - original_escrow_hash_( std::move( original_escrow_hash ) ) - { - } - - EscrowReleaseTransaction EscrowReleaseTransaction::New( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ) - { - EscrowReleaseTransaction instance( std::move( params ), - release_amount, - std::move( release_address ), - std::move( escrow_source ), - std::move( original_escrow_hash ), - std::move( dag ) ); - instance.FillHash(); - return instance; - } - - std::vector EscrowReleaseTransaction::SerializeByteVector( const SGTransaction::DAGStruct &dag ) const - { - SGTransaction::EscrowReleaseTx tx_struct; - tx_struct.mutable_dag_struct()->CopyFrom( dag ); - auto *utxo_proto_params = tx_struct.mutable_utxo_params(); - for ( const auto &[txid_hash_, output_idx_, signature_] : utxo_params_.first ) - { - auto *input_proto = utxo_proto_params->add_inputs(); - input_proto->set_tx_id_hash( txid_hash_.toReadableString() ); - input_proto->set_output_index( output_idx_ ); - input_proto->set_signature( signature_.data(), signature_.size() ); - } - for ( const auto &[encrypted_amount, dest_address, token_id] : utxo_params_.second ) - { - auto *out_proto = utxo_proto_params->add_outputs(); - out_proto->set_encrypted_amount( encrypted_amount ); - out_proto->set_dest_addr( dest_address ); - out_proto->set_token_id( token_id.bytes().data(), token_id.size() ); - } - tx_struct.set_release_amount( release_amount_ ); - tx_struct.set_release_address( release_address_ ); - tx_struct.set_escrow_source( escrow_source_ ); - tx_struct.set_original_escrow_hash( original_escrow_hash_ ); - size_t size = tx_struct.ByteSizeLong(); - std::vector serialized_proto( size ); - if ( !tx_struct.SerializeToArray( serialized_proto.data(), static_cast( size ) ) ) - { - std::cerr << "Failed to serialize transaction\n"; - } - return serialized_proto; - } - - std::shared_ptr EscrowReleaseTransaction::DeSerializeByteVector( - const std::vector &data ) - { - SGTransaction::EscrowReleaseTx tx_struct; - if ( !tx_struct.ParseFromArray( data.data(), static_cast( data.size() ) ) ) - { - std::cerr << "Failed to parse EscrowReleaseTx from array.\n"; - return nullptr; - } - std::vector inputs; - auto *utxo_proto_params = tx_struct.mutable_utxo_params(); - for ( int i = 0; i < utxo_proto_params->inputs_size(); ++i ) - { - const auto &input_proto = utxo_proto_params->inputs( i ); - InputUTXOInfo curr; - auto maybe_hash = base::Hash256::fromReadableString( input_proto.tx_id_hash() ); - if ( !maybe_hash ) - { - std::cerr << "Invalid hash in input\n"; - return nullptr; - } - curr.txid_hash_ = maybe_hash.value(); - curr.output_idx_ = input_proto.output_index(); - curr.signature_ = std::vector( input_proto.signature().cbegin(), input_proto.signature().cend() ); - inputs.push_back( curr ); - } - std::vector outputs; - for ( int i = 0; i < utxo_proto_params->outputs_size(); ++i ) - { - const auto &out_proto = utxo_proto_params->outputs( i ); - outputs.push_back( { out_proto.encrypted_amount(), - out_proto.dest_addr(), - TokenID::FromBytes( out_proto.token_id().data(), out_proto.token_id().size() ) } ); - } - UTXOTxParameters utxo_params( inputs, outputs ); - auto releaseTx = std::make_shared( - EscrowReleaseTransaction( std::move( utxo_params ), - tx_struct.release_amount(), - tx_struct.release_address(), - tx_struct.escrow_source(), - tx_struct.original_escrow_hash(), - tx_struct.dag_struct() ) ); - return releaseTx; - } - - UTXOTxParameters EscrowReleaseTransaction::GetUTXOParameters() const - { - return utxo_params_; - } - - bool EscrowReleaseTransaction::HasUTXOParameters() const - { - return true; - } - - std::optional EscrowReleaseTransaction::GetUTXOParametersOpt() const - { - return utxo_params_; - } - - uint64_t EscrowReleaseTransaction::GetReleaseAmount() const - { - return release_amount_; - } - - std::string EscrowReleaseTransaction::GetReleaseAddress() const - { - return release_address_; - } - - std::string EscrowReleaseTransaction::GetEscrowSource() const - { - return escrow_source_; - } - - std::string EscrowReleaseTransaction::GetOriginalEscrowHash() const - { - return original_escrow_hash_; - } - - std::string EscrowReleaseTransaction::GetTransactionSpecificPath() const - { - return GetType(); - } - - std::unordered_set EscrowReleaseTransaction::GetTopics() const - { - auto topics = IGeniusTransactions::GetTopics(); - topics.emplace( GetEscrowSource() ); - return topics; - } -} diff --git a/src/account/EscrowReleaseTransaction.hpp b/src/account/EscrowReleaseTransaction.hpp deleted file mode 100644 index 6c5a61684..000000000 --- a/src/account/EscrowReleaseTransaction.hpp +++ /dev/null @@ -1,157 +0,0 @@ -/** - * @file EscrowReleaseTransaction.hpp - * @brief Declaration of the EscrowReleaseTransaction class. - */ - -#ifndef _ESCROW_RELEASE_TRANSACTION_HPP_ -#define _ESCROW_RELEASE_TRANSACTION_HPP_ - -#include "account/IGeniusTransactions.hpp" -#include "account/UTXOStructs.hpp" - -namespace sgns -{ - /** - * @brief Represents a transaction used to release funds from an escrow. - * - * This transaction holds the UTXO parameters, the amount to be released, the release address, - * the escrow source, and the original escrow hash. - */ - class EscrowReleaseTransaction final : public IGeniusTransactions - { - public: - /** - * @brief Creates a new EscrowReleaseTransaction. - * - * @param params UTXO transaction parameters. - * @param release_amount Amount to be released. - * @param release_address Address where funds will be sent. - * @param escrow_source Source of the escrow. - * @param original_escrow_hash Original hash of the escrow transaction. - * @param dag DAG structure containing transaction metadata. - * @return An instance of EscrowReleaseTransaction. - */ - static EscrowReleaseTransaction New( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ); - - /** - * @brief Deserializes a byte vector into an EscrowReleaseTransaction. - * - * @param data Serialized transaction data. - * @return A shared pointer to an EscrowReleaseTransaction instance. - */ - static std::shared_ptr DeSerializeByteVector( const std::vector &data ); - - /** - * @brief Destructor. - */ - ~EscrowReleaseTransaction() override = default; - - /** - * @brief Serializes the transaction to a byte vector. - * - * @return A vector of bytes representing the serialized transaction. - */ - using IGeniusTransactions::SerializeByteVector; - std::vector SerializeByteVector( const SGTransaction::DAGStruct &dag ) const override; - - /** - * @brief Gets the UTXO parameters. - * - * @return The UTXO parameters of the transaction. - */ - UTXOTxParameters GetUTXOParameters() const; - - /** - * @brief Returns if transaction supports UTXOs - * @return True if supported, false otherwise - */ - bool HasUTXOParameters() const override; - - /** - * @brief Returns the UTXOs - * @return If exists, returns the UTXOs of the transaction - */ - std::optional GetUTXOParametersOpt() const override; - - /** - * @brief Gets the release amount. - * - * @return The amount to be released. - */ - uint64_t GetReleaseAmount() const; - - /** - * @brief Gets the release address. - * - * @return The address where funds will be released. - */ - std::string GetReleaseAddress() const; - - /** - * @brief Gets the escrow source. - * - * @return The source address of the escrow. - */ - std::string GetEscrowSource() const; - - /** - * @brief Gets the original escrow hash. - * - * @return The original hash of the escrow transaction. - */ - std::string GetOriginalEscrowHash() const; - - /** - * @brief Gets the transaction-specific path. - * - * @return A string representing the transaction path. - */ - std::string GetTransactionSpecificPath() const override; - - std::unordered_set GetTopics() const override; - - private: - /** - * @brief Private constructor. - * - * @param params UTXO transaction parameters. - * @param release_amount Amount to be released. - * @param release_address Address where funds will be sent. - * @param original_escrow_hash Original hash of the escrow transaction. - * @param escrow_source Source address of the escrow. - * @param dag DAG structure containing transaction metadata. - */ - EscrowReleaseTransaction( UTXOTxParameters params, - uint64_t release_amount, - std::string release_address, - std::string escrow_source, - std::string original_escrow_hash, - SGTransaction::DAGStruct dag ); - - UTXOTxParameters utxo_params_; ///< UTXO parameters for the transaction. - uint64_t release_amount_; ///< Amount to be released. - std::string release_address_; ///< Address to which funds will be released. - std::string escrow_source_; ///< Source address of the escrow. - std::string original_escrow_hash_; ///< Original hash of the escrow transaction. - - /** - * @brief Registers the deserializer for the escrow release transaction. - * - * @return True if registration is successful. - */ - static bool Register() - { - RegisterDeserializer( "escrow-release", &EscrowReleaseTransaction::DeSerializeByteVector ); - return true; - } - - static inline bool registered_ = Register(); ///< Ensures deserializer registration. - }; -} - -#endif // _ESCROW_RELEASE_TRANSACTION_HPP_ diff --git a/src/account/Migration0_2_0To1_0_0.cpp b/src/account/Migration0_2_0To1_0_0.cpp index f68d331ca..df017a9d5 100644 --- a/src/account/Migration0_2_0To1_0_0.cpp +++ b/src/account/Migration0_2_0To1_0_0.cpp @@ -13,7 +13,6 @@ #include #include "account/TransactionManager.hpp" #include "account/TransferTransaction.hpp" -#include "account/EscrowReleaseTransaction.hpp" #include "proof/IBasicProof.hpp" #include "MigrationManager.hpp" #include "base/sgns_version.hpp" @@ -321,11 +320,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } sgns::crdt::GlobalDB::Buffer data_transaction; data_transaction.put( tx->SerializeByteVector() ); diff --git a/src/account/Migration1_0_0To3_4_0.cpp b/src/account/Migration1_0_0To3_4_0.cpp index 9892d6b7d..dd61e2127 100644 --- a/src/account/Migration1_0_0To3_4_0.cpp +++ b/src/account/Migration1_0_0To3_4_0.cpp @@ -9,7 +9,6 @@ #include #include "MigrationManager.hpp" -#include "EscrowReleaseTransaction.hpp" #include "TransactionManager.hpp" #include "TransferTransaction.hpp" #include "base/sgns_version.hpp" @@ -161,11 +160,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } sgns::crdt::GlobalDB::Buffer data_transaction; data_transaction.put( tx->SerializeByteVector() ); diff --git a/src/account/Migration3_4_0To3_5_0.cpp b/src/account/Migration3_4_0To3_5_0.cpp index 0e0cc0acd..cf8a7b6e5 100644 --- a/src/account/Migration3_4_0To3_5_0.cpp +++ b/src/account/Migration3_4_0To3_5_0.cpp @@ -7,7 +7,6 @@ #include "Migration3_4_0To3_5_0.hpp" -#include "account/EscrowReleaseTransaction.hpp" #include "account/GeniusAccount.hpp" #include "account/MintTransaction.hpp" #include "account/TransactionManager.hpp" @@ -275,11 +274,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( record.tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } ++migrated_count; if ( migrated_count >= BATCH_SIZE ) diff --git a/src/account/Migration3_5_0To3_6_0.cpp b/src/account/Migration3_5_0To3_6_0.cpp index a1e5e9d31..badc68a6b 100644 --- a/src/account/Migration3_5_0To3_6_0.cpp +++ b/src/account/Migration3_5_0To3_6_0.cpp @@ -3,7 +3,6 @@ #include "account/MigrationManager.hpp" #include "account/TransactionManager.hpp" #include "account/TransferTransaction.hpp" -#include "account/EscrowReleaseTransaction.hpp" #include "blockchain/Blockchain.hpp" #include "blockchain/ValidatorRegistry.hpp" #include "base/sgns_version.hpp" @@ -163,11 +162,6 @@ namespace sgns topics_.emplace( dest_info.dest_address ); } } - if ( auto escrow_tx = std::dynamic_pointer_cast( tx ) ) - { - topics_.emplace( escrow_tx->GetSrcAddress() ); - topics_.emplace( escrow_tx->GetEscrowSource() ); - } ++migrated_count; if ( migrated_count >= BATCH_SIZE ) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 410d2a9f1..ffbeb62ba 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -19,7 +19,6 @@ #include "MintTransaction.hpp" #include "MintTransactionV2.hpp" #include "EscrowTransaction.hpp" -#include "EscrowReleaseTransaction.hpp" #include "UTXOMerkle.hpp" #include "account/TokenAmount.hpp" #include "account/AccountMessenger.hpp" @@ -572,15 +571,16 @@ namespace sgns { return outcome::failure( boost::system::error_code{} ); } - auto hash_data = hasher_m->blake2b_256( std::vector{ job_id.begin(), job_id.end() } ); + auto hash_data = hasher_m->blake2b_256( std::vector{ job_id.begin(), job_id.end() } ); + const std::string lock_id = "0x" + hash_data.toReadableString(); BOOST_OUTCOME_TRY( auto params, account_m->GetUTXOManager().CreateTxParameter( amount, - "0x" + hash_data.toReadableString(), + lock_id, TokenID::FromBytes( { 0x00 } ) ) ); auto [inputs, outputs] = params; auto escrow_transaction = std::make_shared( - EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct() ) ); + EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct( lock_id ) ) ); escrow_transaction->MakeSignature( *account_m ); account_m->GetUTXOManager().ReserveUTXOs( inputs, escrow_transaction->GetHash() ); @@ -595,7 +595,7 @@ namespace sgns // Return both the transaction ID and the original EscrowDataPair return std::make_pair( txId, - std::make_pair( "0x" + hash_data.toReadableString(), std::move( data_transaction ) ) ); + std::make_pair( lock_id, std::move( data_transaction ) ) ); } outcome::result TransactionManager::PayEscrow( @@ -625,6 +625,10 @@ namespace sgns BOOST_OUTCOME_TRY( auto transaction, FetchTransaction( globaldb_m, escrow_path ) ); std::shared_ptr escrow_tx = std::dynamic_pointer_cast( transaction ); + if ( crdt_transaction && escrow_tx && !escrow_tx->GetSrcAddress().empty() ) + { + BOOST_OUTCOME_TRY( crdt_transaction->AddTopic( escrow_tx->GetSrcAddress() ) ); + } std::vector subtask_ids; std::vector payout_peers; @@ -665,28 +669,24 @@ namespace sgns escrow_utxo_input.output_idx_ = 0; escrow_utxo_input.signature_ = account_m->Sign( escrow_utxo_input.SerializeForSigning() ); + std::string lock_id = escrow_tx->GetUncleHash(); + if ( lock_id.empty() && !escrow_tx->GetUTXOParameters().second.empty() ) + { + lock_id = escrow_tx->GetUTXOParameters().second[0].dest_address; + TransactionManagerLogger()->warn( "[{} - full: {}] Escrow transaction {} has empty lock_id but has UTXO parameters - using dest_address as fallback lock_id: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + escrow_tx->GetHash(), + lock_id ); + } + auto transfer_transaction = std::make_shared( - TransferTransaction::New( std::vector{ escrow_utxo_input }, payout_peers, FillDAGStruct() ) ); + TransferTransaction::New( std::vector{ escrow_utxo_input }, payout_peers, FillDAGStruct( lock_id ) ) ); 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(), - escrow_release_dag ) ); - - 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 ) ); - EnqueueTransaction( std::make_pair( tx_batch, std::move( crdt_transaction ) ) ); return transfer_transaction->GetHash(); } @@ -1465,46 +1465,25 @@ namespace sgns outcome::result TransactionManager::ParseEscrowTransaction( const std::shared_ptr &tx ) { auto escrow_tx = std::dynamic_pointer_cast( tx ); - - if ( escrow_tx->GetSrcAddress() == account_m->GetAddress() ) + if ( !escrow_tx ) { - auto [_, outputs] = escrow_tx->GetUTXOParameters(); - - if ( !outputs.empty() ) - { - //The first is the escrow, second is the change (might not happen) - auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); - if ( outputs.size() > 1 ) - { - GeniusUTXO new_utxo( hash, 1, outputs[1].encrypted_amount, outputs[1].token_id ); - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().PutUTXO( new_utxo, outputs[1].dest_address ) ); - } - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().ConsumeUTXOs( escrow_tx->GetUTXOParameters().first, - escrow_tx->GetSrcAddress() ) ); - } + return std::errc::invalid_argument; } - return outcome::success(); - } - - outcome::result TransactionManager::ParseEscrowReleaseTransaction( - const std::shared_ptr &tx ) - { - auto escrowReleaseTx = std::dynamic_pointer_cast( tx ); + auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); + auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); - if ( !escrowReleaseTx ) + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) { - TransactionManagerLogger()->error( "[{} - full: {}] Failed to cast transaction to EscrowReleaseTransaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m ); - return std::errc::invalid_argument; + // output[0] is escrow hold, optional output[1] is change. + GeniusUTXO new_utxo( hash, i, outputs[i].encrypted_amount, outputs[i].token_id ); + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().PutUTXO( new_utxo, outputs[i].dest_address ) ); } - std::string originalEscrowHash = escrowReleaseTx->GetOriginalEscrowHash(); - TransactionManagerLogger()->debug( "[{} - full: {}] Successfully fetched release for escrow: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - originalEscrowHash ); + if ( !inputs.empty() ) + { + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().ConsumeUTXOs( inputs, escrow_tx->GetSrcAddress() ) ); + } return outcome::success(); } @@ -1605,58 +1584,36 @@ namespace sgns outcome::result TransactionManager::RevertEscrowTransaction( const std::shared_ptr &tx ) { auto escrow_tx = std::dynamic_pointer_cast( tx ); + if ( !escrow_tx ) + { + return std::errc::invalid_argument; + } - if ( escrow_tx->GetSrcAddress() == account_m->GetAddress() ) + if ( auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); !outputs.empty() ) { - if ( auto [inputs, outputs] = escrow_tx->GetUTXOParameters(); !outputs.empty() ) + auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); + for ( std::uint32_t i = 0; i < outputs.size(); ++i ) { - //The first is the escrow, second is the change (might not happen) - auto hash = ( base::Hash256::fromReadableString( escrow_tx->GetHash() ) ).value(); - if ( outputs.size() > 1 ) - { - BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, 1, outputs[1].dest_address ) ); - } - for ( auto &input : inputs ) + BOOST_OUTCOME_TRY( account_m->GetUTXOManager().DeleteUTXO( hash, i, outputs[i].dest_address ) ); + } + for ( auto &input : inputs ) + { + auto tx = GetTransactionByHashNoLock( input.txid_hash_.toReadableString() ); + if ( tx ) { - auto tx = GetTransactionByHashNoLock( input.txid_hash_.toReadableString() ); - if ( tx ) - { - TransactionManagerLogger()->debug( "[{} - full: {}] Re-parsing {} transaction", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tx->GetType() ); - BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); - } + TransactionManagerLogger()->debug( "[{} - full: {}] Re-parsing {} transaction", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + tx->GetType() ); + BOOST_OUTCOME_TRY( ParseTransaction( tx ) ); } - account_m->GetUTXOManager().RollbackUTXOs( inputs, escrow_tx->GetHash() ); } + account_m->GetUTXOManager().RollbackUTXOs( inputs, escrow_tx->GetHash() ); } return outcome::success(); } - outcome::result TransactionManager::RevertEscrowReleaseTransaction( - const std::shared_ptr &tx ) - { - auto escrowReleaseTx = std::dynamic_pointer_cast( tx ); - - if ( !escrowReleaseTx ) - { - 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(); - TransactionManagerLogger()->debug( "[{} - full: {}] Successfully fetched release for escrow: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - originalEscrowHash ); - - return outcome::success(); - } - std::vector> TransactionManager::GetOutTransactions() const { std::vector> result; @@ -1803,47 +1760,73 @@ namespace sgns std::chrono::milliseconds timeout ) const { auto start = std::chrono::steady_clock::now(); - auto retval = TransactionStatus::INVALID; + auto escrow_hash_result = base::Hash256::fromReadableString( originalEscrowId ); + if ( escrow_hash_result.has_error() ) + { + TransactionManagerLogger()->warn( "[{} - full: {}] Invalid original escrow tx id while waiting release: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowId ); + return TransactionStatus::INVALID; + } + const auto escrow_hash = escrow_hash_result.value(); - while ( std::chrono::steady_clock::now() - start < timeout ) + auto is_escrow_spent_by_confirmed_transfer = [this, &escrow_hash]() -> bool { + std::shared_lock tx_lock( tx_mutex_m ); + for ( const auto &[_, tracked] : tx_processed_m ) { - std::shared_lock tx_lock( tx_mutex_m ); - for ( const auto &[_, tracked] : tx_processed_m ) + if ( tracked.status != TransactionStatus::CONFIRMED || !tracked.tx || !tracked.tx->HasUTXOParameters() ) { - if ( !tracked.tx ) - { - continue; - } + continue; + } - if ( tracked.tx->GetType() == "escrow-release" ) - { - auto escrowReleaseTx = std::dynamic_pointer_cast( tracked.tx ); - if ( escrowReleaseTx && escrowReleaseTx->GetOriginalEscrowHash() == originalEscrowId ) - { - TransactionManagerLogger()->debug( - "[{} - full: {}] Found matching escrow release transaction with tx id: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - tracked.tx->GetHash() ); + const auto params_opt = tracked.tx->GetUTXOParametersOpt(); + if ( !params_opt.has_value() ) + { + continue; + } - retval = tracked.status; + const auto &inputs = params_opt->first; + const bool spends_original_escrow = std::any_of( + inputs.begin(), + inputs.end(), + [&escrow_hash]( const InputUTXOInfo &input ) + { return input.txid_hash_ == escrow_hash && input.output_idx_ == 0; } ); - // If finalized, return immediately; otherwise keep waiting. - if ( retval == TransactionStatus::CONFIRMED || retval == TransactionStatus::FAILED || - retval == TransactionStatus::INVALID ) - { - return retval; - } - } - } + if ( spends_original_escrow ) + { + return true; } } + return false; + }; + + while ( std::chrono::steady_clock::now() - start < timeout ) + { + if ( account_m->GetUTXOManager().IsOutPointConsumed( escrow_hash, 0 ) ) + { + TransactionManagerLogger()->debug( "[{} - full: {}] Escrow hold ({},0) is consumed", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowId ); + return TransactionStatus::CONFIRMED; + } + + if ( is_escrow_spent_by_confirmed_transfer() ) + { + TransactionManagerLogger()->debug( + "[{} - full: {}] Escrow release confirmed via tracked transfer spend for {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + originalEscrowId ); + return TransactionStatus::CONFIRMED; + } std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); } - return retval; // Will be INVALID if not seen within timeout + return TransactionStatus::INVALID; } void TransactionManager::InitializeUTXOs() diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index a8f0e3989..bca80839f 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -330,12 +330,9 @@ namespace sgns outcome::result ParseTransferTransaction( const std::shared_ptr &tx ); outcome::result ParseMintTransaction( const std::shared_ptr &tx ); outcome::result ParseEscrowTransaction( const std::shared_ptr &tx ); - outcome::result ParseEscrowReleaseTransaction( const std::shared_ptr &tx ); - outcome::result RevertTransferTransaction( const std::shared_ptr &tx ); outcome::result RevertMintTransaction( const std::shared_ptr &tx ); outcome::result RevertEscrowTransaction( const std::shared_ptr &tx ); - outcome::result RevertEscrowReleaseTransaction( const std::shared_ptr &tx ); static inline const std::unordered_map> transaction_parsers = { @@ -345,10 +342,7 @@ namespace sgns { "mint-v2", { &TransactionManager::ParseMintTransaction, &TransactionManager::RevertMintTransaction } }, { "escrow-hold", - { &TransactionManager::ParseEscrowTransaction, &TransactionManager::RevertEscrowTransaction } }, - { "escrow-release", - { &TransactionManager::ParseEscrowReleaseTransaction, - &TransactionManager::RevertEscrowReleaseTransaction } } }; + { &TransactionManager::ParseEscrowTransaction, &TransactionManager::RevertEscrowTransaction } } }; std::optional> FilterTransaction( const crdt::pb::Element &element ); std::optional> FilterProof( const crdt::pb::Element &element ); From a432c4e0f46d33af5b2505113539135882c869ea Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Fri, 17 Apr 2026 17:43:43 -0300 Subject: [PATCH 109/114] Fix: Lingering node fixed (possibly) --- .../processing_subtask_queue_accessor_impl.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/processing/processing_subtask_queue_accessor_impl.cpp b/src/processing/processing_subtask_queue_accessor_impl.cpp index c44487548..5b409dcd1 100644 --- a/src/processing/processing_subtask_queue_accessor_impl.cpp +++ b/src/processing/processing_subtask_queue_accessor_impl.cpp @@ -152,6 +152,7 @@ namespace sgns::processing { std::lock_guard guard( m_mutexResults ); auto queue = m_subTaskQueueManager->GetQueueSnapshot(); + auto finalization_ret = FinalizationRetVal::NOT_FINALIZED; std::set subTaskIds; for ( size_t itemIdx = 0; itemIdx < static_cast( queue->subtasks().items_size() ); ++itemIdx ) @@ -172,8 +173,8 @@ namespace sgns::processing if ( isFullyProcessed ) { std::set invalidSubTaskIds; - auto finalized_ret = FinalizeQueueProcessing( queue->subtasks(), invalidSubTaskIds ); - if ( finalized_ret == FinalizationRetVal::NOT_FINALIZED ) + finalization_ret = FinalizeQueueProcessing( queue->subtasks(), invalidSubTaskIds ); + if ( finalization_ret == FinalizationRetVal::NOT_FINALIZED ) { m_subTaskQueueManager->ChangeSubTaskProcessingStates( processedSubTaskIds, false ); isFullyProcessed = false; @@ -184,6 +185,13 @@ namespace sgns::processing if ( !isFullyProcessed ) { m_subTaskQueueManager->GrabSubTask( onSubTaskGrabbedCallback ); + return; + } + + if ( finalization_ret == FinalizationRetVal::FINALIZED_BUT_NOT_OWNER ) + { + // The owner finalized using the received results; signal completion so this worker can shut down cleanly. + onSubTaskGrabbedCallback( boost::none ); } } From 577900789dd742540f381361cb618f229d53090f Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 21 Apr 2026 09:56:15 -0300 Subject: [PATCH 110/114] Feat: Creating CRDT work journal --- src/crdt/globaldb/crdt_work_journal.hpp | 64 ++++++ src/crdt/impl/crdt_work_journal.cpp | 260 ++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 src/crdt/globaldb/crdt_work_journal.hpp create mode 100644 src/crdt/impl/crdt_work_journal.cpp diff --git a/src/crdt/globaldb/crdt_work_journal.hpp b/src/crdt/globaldb/crdt_work_journal.hpp new file mode 100644 index 000000000..459b69680 --- /dev/null +++ b/src/crdt/globaldb/crdt_work_journal.hpp @@ -0,0 +1,64 @@ +#ifndef SUPERGENIUS_CRDT_WORK_JOURNAL_HPP +#define SUPERGENIUS_CRDT_WORK_JOURNAL_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sgns::storage +{ + class rocksdb; +} + +namespace sgns::crdt +{ + class CRDTWorkJournal + { + public: + enum class State : uint8_t + { + Seen = 0, + Processing = 1, + }; + + struct Entry + { + std::string key; + State state = State::Seen; + uint64_t attempt_count = 0; + uint64_t updated_at_ms = 0; + uint64_t lease_until_ms = 0; + }; + + explicit CRDTWorkJournal( std::shared_ptr datastore ); + + void MarkSeen( const std::string &key ); + void MarkProcessing( const std::string &key, std::chrono::milliseconds lease = std::chrono::minutes( 5 ) ); + bool MarkDone( const std::string &key ); + + std::optional GetEntry( const std::string &key ) const; + std::vector ListUnfinished() const; + size_t RecoverStaleProcessing( std::chrono::milliseconds stale = std::chrono::milliseconds( 0 ) ); + + private: + static constexpr std::string_view NAMESPACE_PREFIX = "/crdt/work/"; + static uint64_t NowMs(); + std::string BuildStorageKey( const std::string &key ) const; + static std::optional DeserializeEntry( std::string_view storage_key, std::string_view value ); + static std::string SerializeEntry( const Entry &entry ); + static std::vector Split( const std::string &value, char separator ); + + std::optional GetEntryUnlocked( const std::string &key ) const; + bool PutEntryUnlocked( const Entry &entry ) const; + + std::shared_ptr datastore_; + mutable std::mutex mutex_; + }; +} + +#endif // SUPERGENIUS_CRDT_WORK_JOURNAL_HPP diff --git a/src/crdt/impl/crdt_work_journal.cpp b/src/crdt/impl/crdt_work_journal.cpp new file mode 100644 index 000000000..ad60dd4ce --- /dev/null +++ b/src/crdt/impl/crdt_work_journal.cpp @@ -0,0 +1,260 @@ +#include "crdt/globaldb/crdt_work_journal.hpp" + +#include +#include + +#include "base/buffer.hpp" +#include "storage/rocksdb/rocksdb.hpp" + +namespace sgns::crdt +{ + CRDTWorkJournal::CRDTWorkJournal( std::shared_ptr datastore ) : + datastore_( std::move( datastore ) ) + { + } + + void CRDTWorkJournal::MarkSeen( const std::string &key ) + { + if ( key.empty() ) + { + return; + } + std::lock_guard lock( mutex_ ); + auto maybe_entry = GetEntryUnlocked( key ); + + Entry entry; + if ( maybe_entry.has_value() ) + { + entry = maybe_entry.value(); + if ( entry.state == State::Processing ) + { + return; + } + } + entry.key = key; + entry.state = State::Seen; + entry.updated_at_ms = NowMs(); + entry.lease_until_ms = 0; + PutEntryUnlocked( entry ); + } + + void CRDTWorkJournal::MarkProcessing( const std::string &key, std::chrono::milliseconds lease ) + { + if ( key.empty() ) + { + return; + } + std::lock_guard lock( mutex_ ); + auto maybe_entry = GetEntryUnlocked( key ); + if ( !maybe_entry.has_value() ) + { + return; + } + + auto entry = maybe_entry.value(); + entry.state = State::Processing; + entry.updated_at_ms = NowMs(); + entry.lease_until_ms = entry.updated_at_ms + static_cast( std::max( 0, lease.count() ) ); + entry.attempt_count += 1; + PutEntryUnlocked( entry ); + } + + bool CRDTWorkJournal::MarkDone( const std::string &key ) + { + if ( key.empty() ) + { + return false; + } + std::lock_guard lock( mutex_ ); + if ( !datastore_ ) + { + return false; + } + base::Buffer key_buf; + key_buf.put( BuildStorageKey( key ) ); + return datastore_->remove( key_buf ).has_value(); + } + + std::optional CRDTWorkJournal::GetEntry( const std::string &key ) const + { + if ( key.empty() ) + { + return std::nullopt; + } + std::lock_guard lock( mutex_ ); + return GetEntryUnlocked( key ); + } + + std::vector CRDTWorkJournal::ListUnfinished() const + { + std::lock_guard lock( mutex_ ); + std::vector out; + if ( !datastore_ ) + { + return out; + } + + base::Buffer prefix_buf; + prefix_buf.put( NAMESPACE_PREFIX ); + auto result = datastore_->query( prefix_buf ); + if ( result.has_error() ) + { + return out; + } + out.reserve( result.value().size() ); + for ( const auto &[raw_key, raw_value] : result.value() ) + { + auto parsed = DeserializeEntry( raw_key.toString(), raw_value.toString() ); + if ( parsed.has_value() ) + { + out.push_back( std::move( parsed.value() ) ); + } + } + return out; + } + + size_t CRDTWorkJournal::RecoverStaleProcessing( std::chrono::milliseconds stale ) + { + std::lock_guard lock( mutex_ ); + if ( !datastore_ ) + { + return 0; + } + + const uint64_t now_ms = NowMs(); + const uint64_t grace_ms = static_cast( std::max( 0, stale.count() ) ); + size_t recovered = 0; + + base::Buffer prefix_buf; + prefix_buf.put( NAMESPACE_PREFIX ); + auto result = datastore_->query( prefix_buf ); + if ( result.has_error() ) + { + return recovered; + } + for ( const auto &[raw_key, raw_value] : result.value() ) + { + auto parsed = DeserializeEntry( raw_key.toString(), raw_value.toString() ); + if ( !parsed.has_value() ) + { + continue; + } + auto &entry = parsed.value(); + if ( entry.state != State::Processing ) + { + continue; + } + if ( entry.lease_until_ms != 0 && entry.lease_until_ms + grace_ms > now_ms ) + { + continue; + } + entry.state = State::Seen; + entry.updated_at_ms = now_ms; + entry.lease_until_ms = 0; + PutEntryUnlocked( entry ); + recovered += 1; + } + return recovered; + } + + uint64_t CRDTWorkJournal::NowMs() + { + return static_cast( + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count() ); + } + + std::string CRDTWorkJournal::BuildStorageKey( const std::string &key ) const + { + return NAMESPACE_PREFIX + key; + } + + std::optional CRDTWorkJournal::DeserializeEntry( std::string_view storage_key, + std::string_view value ) + { + auto fields = Split( std::string( value ), '|' ); + if ( fields.size() != 5 || fields[0] != "v1" ) + { + return std::nullopt; + } + Entry entry; + try + { + const auto state = std::stoi( fields[1] ); + if ( state != static_cast( State::Seen ) && state != static_cast( State::Processing ) ) + { + return std::nullopt; + } + entry.state = static_cast( state ); + entry.attempt_count = static_cast( std::stoull( fields[2] ) ); + entry.updated_at_ms = static_cast( std::stoull( fields[3] ) ); + entry.lease_until_ms = static_cast( std::stoull( fields[4] ) ); + } + catch ( ... ) + { + return std::nullopt; + } + + const auto key_str = std::string( storage_key ); + const auto pos = key_str.find( "/crdt/work/" ); + if ( pos == std::string::npos ) + { + return std::nullopt; + } + entry.key = key_str.substr( pos + std::string( "/crdt/work/" ).size() ); + return entry; + } + + std::string CRDTWorkJournal::SerializeEntry( const Entry &entry ) + { + return "v1|" + std::to_string( static_cast( entry.state ) ) + "|" + std::to_string( entry.attempt_count ) + + "|" + std::to_string( entry.updated_at_ms ) + "|" + std::to_string( entry.lease_until_ms ); + } + + std::vector CRDTWorkJournal::Split( const std::string &value, char separator ) + { + std::vector out; + size_t start = 0; + while ( true ) + { + const auto pos = value.find( separator, start ); + if ( pos == std::string::npos ) + { + out.push_back( value.substr( start ) ); + break; + } + out.push_back( value.substr( start, pos - start ) ); + start = pos + 1; + } + return out; + } + + std::optional CRDTWorkJournal::GetEntryUnlocked( const std::string &key ) const + { + if ( !datastore_ ) + { + return std::nullopt; + } + base::Buffer key_buf; + key_buf.put( BuildStorageKey( key ) ); + auto maybe_value = datastore_->get( key_buf ); + if ( maybe_value.has_error() ) + { + return std::nullopt; + } + return DeserializeEntry( BuildStorageKey( key ), maybe_value.value().toString() ); + } + + bool CRDTWorkJournal::PutEntryUnlocked( const Entry &entry ) const + { + if ( !datastore_ ) + { + return false; + } + base::Buffer key_buf; + key_buf.put( BuildStorageKey( entry.key ) ); + base::Buffer value_buf; + value_buf.put( SerializeEntry( entry ) ); + return datastore_->put( key_buf, value_buf ).has_value(); + } +} From dff3117fea24e3c65ca3af5dfe1328c279a9f8b4 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 21 Apr 2026 09:57:22 -0300 Subject: [PATCH 111/114] Feat: Adding CRDT work journal to filter and callback manager --- src/crdt/CMakeLists.txt | 1 + src/crdt/crdt_callback_manager.hpp | 5 +- src/crdt/crdt_data_filter.hpp | 15 +- src/crdt/crdt_datastore.hpp | 3 + src/crdt/globaldb/globaldb.cpp | 5 + src/crdt/globaldb/globaldb.hpp | 1 + src/crdt/impl/crdt_callback_manager.cpp | 173 +++++++++++++----------- src/crdt/impl/crdt_data_filter.cpp | 11 +- src/crdt/impl/crdt_datastore.cpp | 10 +- 9 files changed, 134 insertions(+), 90 deletions(-) diff --git a/src/crdt/CMakeLists.txt b/src/crdt/CMakeLists.txt index 3e1359534..78c8805bd 100644 --- a/src/crdt/CMakeLists.txt +++ b/src/crdt/CMakeLists.txt @@ -70,6 +70,7 @@ supergenius_install(crdt_data_filter) add_library(crdt_datastore impl/crdt_datastore.cpp + impl/crdt_work_journal.cpp impl/atomic_transaction.cpp ) target_link_libraries(crdt_datastore diff --git a/src/crdt/crdt_callback_manager.hpp b/src/crdt/crdt_callback_manager.hpp index a4ef77c96..f199f4800 100644 --- a/src/crdt/crdt_callback_manager.hpp +++ b/src/crdt/crdt_callback_manager.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -16,6 +17,7 @@ namespace sgns::crdt { + class CRDTWorkJournal; class CRDTCallbackManager { @@ -30,7 +32,7 @@ namespace sgns::crdt /** * @brief Construct a new CRDTCallbackManager object */ - explicit CRDTCallbackManager(); + explicit CRDTCallbackManager( std::shared_ptr work_journal ); /** * @brief Destroy the CRDTCallbackManager object */ @@ -76,6 +78,7 @@ namespace sgns::crdt void DeleteDataCallback( const std::string &deleted_key, const std::string &cid ); private: + std::shared_ptr work_journal_; std::shared_mutex new_data_callback_registry_mutex_; ///< Mutex to manipulate @ref new_data_callback_registry_ NewDataCallbackRegistry new_data_callback_registry_; ///< New data callback registry std::shared_mutex diff --git a/src/crdt/crdt_data_filter.hpp b/src/crdt/crdt_data_filter.hpp index ae244c6d8..5f0b8ff38 100644 --- a/src/crdt/crdt_data_filter.hpp +++ b/src/crdt/crdt_data_filter.hpp @@ -19,6 +19,8 @@ namespace sgns::crdt { + class CRDTWorkJournal; + class CRDTDataFilter { public: @@ -33,7 +35,7 @@ namespace sgns::crdt * @param[in] accept_by_default: if true, every delta that doesn't have a filter gets accepted. * if false, rejects by default. */ - explicit CRDTDataFilter( bool accept_by_default = true ); + explicit CRDTDataFilter( std::shared_ptr work_journal, bool accept_by_default = true ); /** * @brief Destroy the CRDTDataFilter object @@ -81,11 +83,12 @@ namespace sgns::crdt void FilterTombstonesOnDelta( pb::Delta &delta ); private: - const bool accept_by_default_; ///< The default behavior for values not matching any filter - mutable std::shared_mutex element_registry_mutex_; ///< Mutex for the element registry - std::shared_mutex tombstone_registry_mutex_; ///< Mutex for the tombstone registry - FilterCallbackRegistry element_registry_; ///< Element filter callback registry - FilterCallbackRegistry tombstone_registry_; ///< Tombstone filter callback registry + std::shared_ptr work_journal_; + const bool accept_by_default_; ///< The default behavior for values not matching any filter + mutable std::shared_mutex element_registry_mutex_; ///< Mutex for the element registry + std::shared_mutex tombstone_registry_mutex_; ///< Mutex for the tombstone registry + FilterCallbackRegistry element_registry_; ///< Element filter callback registry + FilterCallbackRegistry tombstone_registry_; ///< Tombstone filter callback registry }; } diff --git a/src/crdt/crdt_datastore.hpp b/src/crdt/crdt_datastore.hpp index e19f30d44..c0e5ac01f 100644 --- a/src/crdt/crdt_datastore.hpp +++ b/src/crdt/crdt_datastore.hpp @@ -30,6 +30,7 @@ #include "crdt/crdt_options.hpp" #include "crdt/crdt_data_filter.hpp" #include "crdt/crdt_callback_manager.hpp" +#include "crdt/globaldb/crdt_work_journal.hpp" #include "storage/rocksdb/rocksdb.hpp" namespace sgns @@ -216,6 +217,7 @@ namespace sgns::crdt void UnregisterElementFilter( const std::string &pattern ); void UnregisterNewElementCallback( const std::string &pattern ); void UnregisterDeletedElementCallback( const std::string &pattern ); + std::shared_ptr GetWorkJournal() const; /** * @brief Configure which topic this datastore should filter on. @@ -454,6 +456,7 @@ namespace sgns::crdt std::queue pendingRootQueue_; std::optional activeRootCID_; + std::shared_ptr work_journal_; CRDTDataFilter crdt_filter_; bool started_ = false; bool broadcast_enabled_ = false; diff --git a/src/crdt/globaldb/globaldb.cpp b/src/crdt/globaldb/globaldb.cpp index 77b2b411c..0e7c654b4 100644 --- a/src/crdt/globaldb/globaldb.cpp +++ b/src/crdt/globaldb/globaldb.cpp @@ -386,6 +386,11 @@ namespace sgns::crdt return m_datastore; } + std::shared_ptr GlobalDB::GetWorkJournal() const + { + return m_crdtDatastore ? m_crdtDatastore->GetWorkJournal() : nullptr; + } + outcome::result GlobalDB::GetCRDTHeadList() { return m_crdtDatastore->GetHeadList(); diff --git a/src/crdt/globaldb/globaldb.hpp b/src/crdt/globaldb/globaldb.hpp index 29d80073a..463fe765d 100644 --- a/src/crdt/globaldb/globaldb.hpp +++ b/src/crdt/globaldb/globaldb.hpp @@ -146,6 +146,7 @@ namespace sgns::crdt std::shared_ptr GetDataStore(); std::shared_ptr GetBroadcaster(); + std::shared_ptr GetWorkJournal() const; bool RegisterElementFilter( const std::string &pattern, GlobalDBFilterCallback filter ); bool RegisterNewElementCallback( const std::string &pattern, GlobalDBNewElementCallback callback ); diff --git a/src/crdt/impl/crdt_callback_manager.cpp b/src/crdt/impl/crdt_callback_manager.cpp index fb9275686..0e5e6d678 100644 --- a/src/crdt/impl/crdt_callback_manager.cpp +++ b/src/crdt/impl/crdt_callback_manager.cpp @@ -6,39 +6,40 @@ */ #include #include "crdt/crdt_callback_manager.hpp" +#include "crdt/globaldb/crdt_work_journal.hpp" namespace sgns::crdt { - CRDTCallbackManager::CRDTCallbackManager() + CRDTCallbackManager::CRDTCallbackManager( std::shared_ptr work_journal ) : + work_journal_( std::move( work_journal ) ) { - - logger_->debug("CRDTCallbackManager constructed"); + logger_->debug( "CRDTCallbackManager constructed" ); } - CRDTCallbackManager::~CRDTCallbackManager() + CRDTCallbackManager::~CRDTCallbackManager() { - logger_->debug("CRDTCallbackManager destroyed"); + logger_->debug( "CRDTCallbackManager destroyed" ); } bool CRDTCallbackManager::RegisterNewDataCallback( const std::string &pattern, NewDataCallback callback ) { bool ret = false; std::lock_guard lock( new_data_callback_registry_mutex_ ); - - logger_->debug("Attempting to register new data callback for pattern: '{}'", pattern); - + + logger_->debug( "Attempting to register new data callback for pattern: '{}'", pattern ); + if ( new_data_callback_registry_.find( pattern ) == new_data_callback_registry_.end() ) { new_data_callback_registry_[pattern] = std::move( callback ); - ret = true; - logger_->info("Successfully registered new data callback for pattern: '{}'", pattern); + ret = true; + logger_->info( "Successfully registered new data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Pattern '{}' already exists in new data callback registry", pattern); + logger_->warn( "Pattern '{}' already exists in new data callback registry", pattern ); } - - logger_->debug("Total registered new data callbacks: {}", new_data_callback_registry_.size()); + + logger_->debug( "Total registered new data callbacks: {}", new_data_callback_registry_.size() ); return ret; } @@ -46,166 +47,178 @@ namespace sgns::crdt { bool ret = false; std::lock_guard lock( deleted_data_callback_registry_mutex_ ); - - logger_->debug("Attempting to register deleted data callback for pattern: '{}'", pattern); - + + logger_->debug( "Attempting to register deleted data callback for pattern: '{}'", pattern ); + if ( deleted_data_callback_registry_.find( pattern ) == deleted_data_callback_registry_.end() ) { deleted_data_callback_registry_[pattern] = std::move( callback ); - ret = true; - logger_->info("Successfully registered deleted data callback for pattern: '{}'", pattern); + ret = true; + logger_->info( "Successfully registered deleted data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Pattern '{}' already exists in deleted data callback registry", pattern); + logger_->warn( "Pattern '{}' already exists in deleted data callback registry", pattern ); } - - logger_->debug("Total registered deleted data callbacks: {}", deleted_data_callback_registry_.size()); + + logger_->debug( "Total registered deleted data callbacks: {}", deleted_data_callback_registry_.size() ); return ret; } void CRDTCallbackManager::UnregisterNewDataCallback( const std::string &pattern ) { std::lock_guard lock( new_data_callback_registry_mutex_ ); - - auto it = new_data_callback_registry_.find(pattern); - if (it != new_data_callback_registry_.end()) + + auto it = new_data_callback_registry_.find( pattern ); + if ( it != new_data_callback_registry_.end() ) { new_data_callback_registry_.erase( pattern ); - logger_->info("Successfully unregistered new data callback for pattern: '{}'", pattern); + logger_->info( "Successfully unregistered new data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Attempted to unregister non-existent pattern: '{}'", pattern); + logger_->warn( "Attempted to unregister non-existent pattern: '{}'", pattern ); } - - logger_->debug("Total registered new data callbacks after unregister: {}", new_data_callback_registry_.size()); + + logger_->debug( "Total registered new data callbacks after unregister: {}", + new_data_callback_registry_.size() ); } void CRDTCallbackManager::UnregisterDeletedDataCallback( const std::string &pattern ) { std::lock_guard lock( deleted_data_callback_registry_mutex_ ); - - auto it = deleted_data_callback_registry_.find(pattern); - if (it != deleted_data_callback_registry_.end()) + + auto it = deleted_data_callback_registry_.find( pattern ); + if ( it != deleted_data_callback_registry_.end() ) { deleted_data_callback_registry_.erase( pattern ); - logger_->info("Successfully unregistered deleted data callback for pattern: '{}'", pattern); + logger_->info( "Successfully unregistered deleted data callback for pattern: '{}'", pattern ); } else { - logger_->warn("Attempted to unregister non-existent pattern: '{}'", pattern); + logger_->warn( "Attempted to unregister non-existent pattern: '{}'", pattern ); } - - logger_->debug("Total registered deleted data callbacks after unregister: {}", deleted_data_callback_registry_.size()); + + logger_->debug( "Total registered deleted data callbacks after unregister: {}", + deleted_data_callback_registry_.size() ); } void CRDTCallbackManager::PutDataCallback( const std::string &key, const base::Buffer &value, const std::string &cid ) { - logger_->debug("PutDataCallback triggered for key: '{}', cid: '{}', value size: {} bytes", - key, cid, value.size()); - + logger_->debug( "PutDataCallback triggered for key: '{}', cid: '{}', value size: {} bytes", + key, + cid, + value.size() ); + NewDataCallbackRegistry registry_copy; { std::shared_lock lock( new_data_callback_registry_mutex_ ); registry_copy = new_data_callback_registry_; - logger_->debug("Copied {} registered patterns for matching", registry_copy.size()); + logger_->debug( "Copied {} registered patterns for matching", registry_copy.size() ); } - - if (registry_copy.empty()) + work_journal_->MarkProcessing( key ); + + if ( registry_copy.empty() ) { - logger_->warn("No new data callbacks registered - key '{}' will not trigger any callbacks", key); + logger_->warn( "No new data callbacks registered - key '{}' will not trigger any callbacks", key ); return; } - + bool callback_triggered = false; for ( const auto &[pattern, callback] : registry_copy ) { - logger_->debug("Testing key '{}' against pattern '{}'", key, pattern); - - try + logger_->debug( "Testing key '{}' against pattern '{}'", key, pattern ); + + try { std::regex regex( pattern ); - bool matches = std::regex_match( key, regex ); - - logger_->debug("Regex match result for key '{}' vs pattern '{}': {}", - key, pattern, matches ? "MATCH" : "NO MATCH"); - + bool matches = std::regex_match( key, regex ); + + logger_->debug( "Regex match result for key '{}' vs pattern '{}': {}", + key, + pattern, + matches ? "MATCH" : "NO MATCH" ); + if ( matches ) { - logger_->info("Executing callback for key '{}' matching pattern '{}'", key, pattern); + logger_->info( "Executing callback for key '{}' matching pattern '{}'", key, pattern ); callback( std::make_pair( key, value ), cid ); callback_triggered = true; } } - catch (const std::regex_error& e) + catch ( const std::regex_error &e ) { - logger_->error("Regex error for pattern '{}': {}", pattern, e.what()); + logger_->error( "Regex error for pattern '{}': {}", pattern, e.what() ); } } - - if (!callback_triggered) + + if ( !callback_triggered ) { - logger_->warn("No callbacks were triggered for key '{}' - no pattern matches found", key); + logger_->warn( "No callbacks were triggered for key '{}' - no pattern matches found", key ); } else { - logger_->debug("Successfully triggered callbacks for key '{}'", key); + logger_->debug( "Successfully triggered callbacks for key '{}'", key ); } } void CRDTCallbackManager::DeleteDataCallback( const std::string &deleted_key, const std::string &cid ) { - logger_->debug("DeleteDataCallback triggered for key: '{}', cid: '{}'", deleted_key, cid); - + logger_->debug( "DeleteDataCallback triggered for key: '{}', cid: '{}'", deleted_key, cid ); + DeletedDataCallbackRegistry registry_copy; { std::shared_lock lock( deleted_data_callback_registry_mutex_ ); registry_copy = deleted_data_callback_registry_; - logger_->debug("Copied {} registered delete patterns for matching", registry_copy.size()); + logger_->debug( "Copied {} registered delete patterns for matching", registry_copy.size() ); } - - if (registry_copy.empty()) + + if ( registry_copy.empty() ) { - logger_->warn("No deleted data callbacks registered - key '{}' will not trigger any callbacks", deleted_key); + logger_->warn( "No deleted data callbacks registered - key '{}' will not trigger any callbacks", + deleted_key ); return; } - + bool callback_triggered = false; for ( const auto &[pattern, callback] : registry_copy ) { - logger_->debug("Testing deleted key '{}' against pattern '{}'", deleted_key, pattern); - - try + logger_->debug( "Testing deleted key '{}' against pattern '{}'", deleted_key, pattern ); + + try { std::regex regex( pattern ); - bool matches = std::regex_match( deleted_key, regex ); - - logger_->debug("Regex match result for deleted key '{}' vs pattern '{}': {}", - deleted_key, pattern, matches ? "MATCH" : "NO MATCH"); - + bool matches = std::regex_match( deleted_key, regex ); + + logger_->debug( "Regex match result for deleted key '{}' vs pattern '{}': {}", + deleted_key, + pattern, + matches ? "MATCH" : "NO MATCH" ); + if ( matches ) { - logger_->info("Executing delete callback for key '{}' matching pattern '{}'", deleted_key, pattern); + logger_->info( "Executing delete callback for key '{}' matching pattern '{}'", + deleted_key, + pattern ); callback( deleted_key, cid ); callback_triggered = true; } } - catch (const std::regex_error& e) + catch ( const std::regex_error &e ) { - logger_->error("Regex error for delete pattern '{}': {}", pattern, e.what()); + logger_->error( "Regex error for delete pattern '{}': {}", pattern, e.what() ); } } - - if (!callback_triggered) + + if ( !callback_triggered ) { - logger_->warn("No delete callbacks were triggered for key '{}' - no pattern matches found", deleted_key); + logger_->warn( "No delete callbacks were triggered for key '{}' - no pattern matches found", deleted_key ); } else { - logger_->debug("Successfully triggered delete callbacks for key '{}'", deleted_key); + logger_->debug( "Successfully triggered delete callbacks for key '{}'", deleted_key ); } } diff --git a/src/crdt/impl/crdt_data_filter.cpp b/src/crdt/impl/crdt_data_filter.cpp index d3a2c2c58..e7dcb0957 100644 --- a/src/crdt/impl/crdt_data_filter.cpp +++ b/src/crdt/impl/crdt_data_filter.cpp @@ -5,11 +5,16 @@ * @author Henrique A. Klein (hklein@gnus.ai) */ #include "crdt/crdt_data_filter.hpp" +#include "crdt/globaldb/crdt_work_journal.hpp" #include namespace sgns::crdt { - CRDTDataFilter::CRDTDataFilter( bool accept_by_default ) : accept_by_default_( std::move( accept_by_default ) ) {} + CRDTDataFilter::CRDTDataFilter( std::shared_ptr work_journal, bool accept_by_default ) : + work_journal_( std::move( work_journal ) ), // + accept_by_default_( std::move( accept_by_default ) ) // + { + } bool CRDTDataFilter::RegisterElementFilter( const std::string &pattern, ElementFilterCallback filter ) { @@ -73,6 +78,10 @@ namespace sgns::crdt } } } + else + { + work_journal_->MarkSeen( element.key() ); + } filter_matched = true; break; } diff --git a/src/crdt/impl/crdt_datastore.cpp b/src/crdt/impl/crdt_datastore.cpp index 0aa1b70f7..9ea5587c5 100644 --- a/src/crdt/impl/crdt_datastore.cpp +++ b/src/crdt/impl/crdt_datastore.cpp @@ -371,8 +371,9 @@ namespace sgns::crdt namespaceKey_( aKey ), broadcaster_( std::move( aBroadcaster ) ), dagSyncer_( std::move( aDagSyncer ) ), - crdt_filter_( true ), - crdt_cb_manager_() + work_journal_( CRDTWorkJournal::New( dataStore_ ) ), + crdt_filter_( work_journal_, true ), + crdt_cb_manager_( work_journal_ ) { logger_ = options_->logger; numberOfDagWorkers = options_->numWorkers; @@ -1636,6 +1637,11 @@ namespace sgns::crdt crdt_cb_manager_.DeleteDataCallback( key, cid ); } + std::shared_ptr CrdtDatastore::GetWorkJournal() const + { + return work_journal_; + } + void CrdtDatastore::UpdateCRDTHeads( const CID &rootCID, uint64_t rootPriority, bool add_topics_to_broadcast ) { std::lock_guard lock( pendingHeadsMutex_ ); From cefc2ce79367e0e541c0018eba8b6d97620c71c8 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 21 Apr 2026 16:26:01 -0300 Subject: [PATCH 112/114] WIP: CRDT work journal fixes --- src/account/TransactionManager.cpp | 185 ++++++++++-------- src/account/TransactionManager.hpp | 28 +-- src/blockchain/Consensus.cpp | 159 +++++++++++---- src/blockchain/Consensus.hpp | 70 ++++--- src/blockchain/ValidatorRegistry.cpp | 67 ++++--- src/blockchain/ValidatorRegistry.hpp | 85 ++++---- src/blockchain/impl/Blockchain.cpp | 81 +++++--- src/crdt/crdt_datastore.hpp | 2 +- src/crdt/globaldb/crdt_work_journal.hpp | 21 +- src/crdt/impl/crdt_callback_manager.cpp | 8 + src/crdt/impl/crdt_work_journal.cpp | 74 ++++++- .../blockchain/consensus_certificate_test.cpp | 159 ++++++++------- 12 files changed, 592 insertions(+), 347 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index ffbeb62ba..9c3a874df 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -105,19 +105,29 @@ namespace sgns instance->blockchain_->RegisterCertificateHandler( SubjectType::SUBJECT_NONCE, - [weak_ptr( std::weak_ptr( instance ) )]( const std::string &subject_hash, - const ConsensusCertificate &certificate ) + [weak_ptr( std::weak_ptr( instance ) )]( + const std::string &subject_hash, + const ConsensusCertificate &certificate ) -> outcome::result { - (void)certificate; if ( auto strong = weak_ptr.lock() ) { - strong->OnConsensusCertificate( subject_hash ); + auto process_result = strong->OnConsensusCertificate( subject_hash ); + if ( process_result.has_error() ) + { + TransactionManagerLogger()->error( + "[{} - full: {}] Failed to process certificate proposal_id={} error={}", + strong->account_m->GetAddress().substr( 0, 8 ), + strong->full_node_m, + certificate.proposal_id(), + process_result.error().message() ); + } + return process_result; } } ); instance->blockchain_->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, [weak_ptr( std::weak_ptr( instance ) )]( - const ConsensusManager::Subject &subject ) -> outcome::result + const ConsensusManager::Subject &subject ) -> outcome::result { if ( auto strong = weak_ptr.lock() ) { @@ -296,7 +306,7 @@ namespace sgns auto now = std::chrono::steady_clock::now(); auto time_since_last_loop = std::chrono::duration_cast( now - last_loop_time_ ) .count(); - last_loop_time_ = now; + last_loop_time_ = now; std::vector elements_to_delete; std::vector elements_to_process; @@ -489,7 +499,8 @@ namespace sgns { return outcome::failure( boost::system::error_code{} ); } - BOOST_OUTCOME_TRY( auto params, account_m->GetUTXOManager().CreateTxParameter( amount, destination, token_id ) ); + BOOST_OUTCOME_TRY( auto params, + account_m->GetUTXOManager().CreateTxParameter( amount, destination, token_id ) ); auto [inputs, outputs] = params; auto transfer_transaction = std::make_shared( @@ -524,7 +535,7 @@ namespace sgns chainid = "public"; } - auto source_hash = base::Hash256::fromReadableString( transaction_hash ); + auto source_hash = base::Hash256::fromReadableString( transaction_hash ); base::Hash256 source_input_hash; if ( source_hash.has_error() ) { @@ -574,10 +585,9 @@ namespace sgns auto hash_data = hasher_m->blake2b_256( std::vector{ job_id.begin(), job_id.end() } ); const std::string lock_id = "0x" + hash_data.toReadableString(); - BOOST_OUTCOME_TRY( auto params, - account_m->GetUTXOManager().CreateTxParameter( amount, - lock_id, - TokenID::FromBytes( { 0x00 } ) ) ); + BOOST_OUTCOME_TRY( + auto params, + account_m->GetUTXOManager().CreateTxParameter( amount, lock_id, TokenID::FromBytes( { 0x00 } ) ) ); auto [inputs, outputs] = params; auto escrow_transaction = std::make_shared( EscrowTransaction::New( params, amount, dev_addr, peers_cut, FillDAGStruct( lock_id ) ) ); @@ -594,8 +604,7 @@ namespace sgns data_transaction.put( escrow_transaction->SerializeByteVector() ); // Return both the transaction ID and the original EscrowDataPair - return std::make_pair( txId, - std::make_pair( lock_id, std::move( data_transaction ) ) ); + return std::make_pair( txId, std::make_pair( lock_id, std::move( data_transaction ) ) ); } outcome::result TransactionManager::PayEscrow( @@ -629,8 +638,8 @@ namespace sgns { BOOST_OUTCOME_TRY( crdt_transaction->AddTopic( escrow_tx->GetSrcAddress() ) ); } - std::vector subtask_ids; - std::vector payout_peers; + std::vector subtask_ids; + std::vector payout_peers; BOOST_OUTCOME_TRY( auto escrow_amount_ptr, TokenAmount::New( escrow_tx->GetAmount() ) ); @@ -673,11 +682,12 @@ namespace sgns if ( lock_id.empty() && !escrow_tx->GetUTXOParameters().second.empty() ) { lock_id = escrow_tx->GetUTXOParameters().second[0].dest_address; - TransactionManagerLogger()->warn( "[{} - full: {}] Escrow transaction {} has empty lock_id but has UTXO parameters - using dest_address as fallback lock_id: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - escrow_tx->GetHash(), - lock_id ); + TransactionManagerLogger()->warn( + "[{} - full: {}] Escrow transaction {} has empty lock_id but has UTXO parameters - using dest_address as fallback lock_id: {}", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + escrow_tx->GetHash(), + lock_id ); } auto transfer_transaction = std::make_shared( @@ -887,8 +897,7 @@ namespace sgns proof_key.GetKey() ); proof_transaction.put( proof ); - BOOST_OUTCOME_TRY( - crdt_transaction->Put( std::move( proof_key ), std::move( proof_transaction ) ) ); + BOOST_OUTCOME_TRY( crdt_transaction->Put( std::move( proof_key ), std::move( proof_transaction ) ) ); } TransactionManagerLogger()->debug( "[{} - full: {}] Creating Consensus Proposal for tx {}", account_m->GetAddress().substr( 0, 8 ), @@ -945,11 +954,11 @@ namespace sgns } BOOST_OUTCOME_TRY( auto &&proposal, - blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), - transaction->GetNonce(), - transaction->GetHash(), - utxo_commitment, - utxo_witness ) ); + blockchain_->CreateConsensusProposal( transaction->GetSrcAddress(), + transaction->GetNonce(), + transaction->GetHash(), + utxo_commitment, + utxo_witness ) ); BOOST_OUTCOME_TRY( ChangeTransactionState( transaction, TransactionStatus::SENDING ) ); BOOST_OUTCOME_TRY( blockchain_->SubmitProposal( proposal ) ); } @@ -1170,7 +1179,8 @@ namespace sgns return addresses; } - TransactionManager::AccountUTXOState TransactionManager::GetOrInitAccountUTXOState( const std::string &address ) const + TransactionManager::AccountUTXOState TransactionManager::GetOrInitAccountUTXOState( + const std::string &address ) const { const auto current_root = account_m->GetUTXOManager().ComputeUTXOMerkleRoot( address ); @@ -1680,7 +1690,7 @@ namespace sgns for ( const auto &[_, tracked] : tx_processed_m ) { if ( tracked.tx && tracked.tx->GetHash() == txId && - tracked.tx->GetSrcAddress() != account_m->GetAddress() ) + tracked.tx->GetSrcAddress() != account_m->GetAddress() ) { retval = tracked.status; break; @@ -1720,7 +1730,7 @@ namespace sgns for ( const auto &[_, tracked] : tx_processed_m ) { if ( tracked.tx && tracked.tx->GetHash() == txId && - tracked.tx->GetSrcAddress() == account_m->GetAddress() ) + tracked.tx->GetSrcAddress() == account_m->GetAddress() ) { retval = tracked.status; TransactionManagerLogger()->trace( "[{} - full: {}] Transaction status is {}", @@ -1759,7 +1769,7 @@ namespace sgns const std::string &originalEscrowId, std::chrono::milliseconds timeout ) const { - auto start = std::chrono::steady_clock::now(); + auto start = std::chrono::steady_clock::now(); auto escrow_hash_result = base::Hash256::fromReadableString( originalEscrowId ); if ( escrow_hash_result.has_error() ) { @@ -1787,7 +1797,7 @@ namespace sgns continue; } - const auto &inputs = params_opt->first; + const auto &inputs = params_opt->first; const bool spends_original_escrow = std::any_of( inputs.begin(), inputs.end(), @@ -1871,7 +1881,8 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m ); - auto clear_result = account_m->GetUTXOManager().SetUTXOs( std::vector{}, account_m->GetAddress() ); + auto clear_result = account_m->GetUTXOManager().SetUTXOs( std::vector{}, + account_m->GetAddress() ); if ( clear_result.has_error() ) { TransactionManagerLogger()->error( @@ -3125,7 +3136,7 @@ namespace sgns return false; } - void TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) + outcome::result TransactionManager::OnConsensusCertificate( const std::string &tx_hash ) { TransactionManagerLogger()->debug( "[{} - full: {}] {}: Consensus certificate arrived for transaction {}", account_m->GetAddress().substr( 0, 8 ), @@ -3140,7 +3151,7 @@ namespace sgns full_node_m, __func__, tx_hash ); - return; + return ConsensusManager::Check::Stalled; } TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking for conflicting transaction with {}", account_m->GetAddress().substr( 0, 8 ), @@ -3197,7 +3208,7 @@ namespace sgns tx_hash, result.error().message() ); } - return; + return outcome::failure( result.error() ); } } else @@ -3233,7 +3244,7 @@ namespace sgns __func__, tx_hash, result.error().message() ); - return; + return outcome::failure( result.error() ); } TransactionManagerLogger()->debug( "[{} - full: {}] {}: Transaction {} confirmed by consensus", account_m->GetAddress().substr( 0, 8 ), @@ -3249,7 +3260,7 @@ namespace sgns full_node_m, __func__, tx_hash ); - return; + return outcome::failure( tx_hash_bin.error() ); } auto validator_registry = blockchain_->GetValidatorRegistry(); @@ -3259,7 +3270,7 @@ namespace sgns account_m->GetAddress().substr( 0, 8 ), full_node_m, __func__ ); - return; + return outcome::failure( std::errc::no_such_device ); } const uint64_t registry_epoch = validator_registry->GetRegistryEpoch(); @@ -3267,7 +3278,9 @@ namespace sgns auto registry_hash = hasher_m->sha2_256( gsl::span( reinterpret_cast( registry_cid.data() ), registry_cid.size() ) ); - if ( auto checkpoint_res = account_m->GetUTXOManager().CreateCheckpoint( registry_epoch, tx_hash_bin.value(), registry_hash ); + if ( auto checkpoint_res = account_m->GetUTXOManager().CreateCheckpoint( registry_epoch, + tx_hash_bin.value(), + registry_hash ); checkpoint_res.has_error() ) { TransactionManagerLogger()->error( @@ -3279,9 +3292,10 @@ namespace sgns registry_epoch, checkpoint_res.error().message() ); } + return ConsensusManager::Check::Approve; } - outcome::result TransactionManager::HandleNonceConsensusSubject( + outcome::result TransactionManager::HandleNonceConsensusSubject( const ConsensusManager::Subject &subject ) { if ( subject.type() != SubjectType::SUBJECT_NONCE ) @@ -3310,7 +3324,7 @@ namespace sgns full_node_m, __func__, tx_hash ); - return ConsensusManager::SubjectCheck::Pending; + return ConsensusManager::Check::Pending; } tracked_tx = it->second.tx; @@ -3328,15 +3342,14 @@ namespace sgns return outcome::failure( std::errc::invalid_argument ); } - auto reject_and_maybe_fail_local = [&]( const char *reason ) -> ConsensusManager::SubjectCheck + auto reject_and_maybe_fail_local = [&]( const char *reason ) -> ConsensusManager::Check { - TransactionManagerLogger()->error( - "[{} - full: {}] {}: Rejecting nonce subject for hash {}: {}", - account_m->GetAddress().substr( 0, 8 ), - full_node_m, - __func__, - tx_hash, - reason ); + 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() ) @@ -3359,7 +3372,7 @@ namespace sgns } } - return ConsensusManager::SubjectCheck::Reject; + return ConsensusManager::Check::Reject; }; if ( tracked_nonce != subject.nonce().nonce() ) @@ -3421,7 +3434,7 @@ namespace sgns return reject_and_maybe_fail_local( "transaction validation failed" ); } - return ConsensusManager::SubjectCheck::Approve; + return ConsensusManager::Check::Approve; } bool TransactionManager::ValidateUTXOParametersForConsensus( const UTXOTxParameters ¶ms, @@ -3791,7 +3804,9 @@ namespace sgns } const auto chain_id = GetValidationChainId( tx ); const auto &validator = GetInputValidator( chain_id ); - return validator.ValidateUTXOParameters( params_opt.value(), tx->GetSrcAddress(), account_m->GetUTXOManager() ); + return validator.ValidateUTXOParameters( params_opt.value(), + tx->GetSrcAddress(), + account_m->GetUTXOManager() ); } return true; @@ -3810,19 +3825,20 @@ namespace sgns 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() ); + 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() ) { @@ -3861,15 +3877,16 @@ namespace sgns 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() ); + 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( @@ -3947,7 +3964,7 @@ namespace sgns return std::nullopt; } - UTXOTransitionCommitment commitment; + UTXOTransitionCommitment commitment; std::vector> consumed_payloads; consumed_payloads.reserve( inputs.size() ); for ( const auto &input : inputs ) @@ -3962,7 +3979,8 @@ namespace sgns 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 ) ); + const auto consumed_outpoints_root = utxo_merkle::ComputeMerkleRootFromPayloads( + std::move( consumed_payloads ) ); std::vector produced_outputs; if ( !ExtractProducedUTXOs( tx, produced_outputs ) ) @@ -3978,7 +3996,7 @@ namespace sgns produced_payloads.reserve( produced_outputs.size() ); for ( size_t i = 0; i < produced_outputs.size(); ++i ) { - const auto &produced_output = produced_outputs[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 ) ); @@ -3989,9 +4007,10 @@ namespace sgns produced_payloads.push_back( SerializeUTXOLeafPayload( produced_output ) ); } - const auto produced_outputs_root = account_m->GetUTXOManager().ComputeUTXOMerkleRootFromSnapshot( produced_outputs ); - const auto produced_outputs_root_from_payloads = - utxo_merkle::ComputeMerkleRootFromPayloads( std::move( produced_payloads ) ); + const auto produced_outputs_root = account_m->GetUTXOManager().ComputeUTXOMerkleRootFromSnapshot( + produced_outputs ); + const auto produced_outputs_root_from_payloads = utxo_merkle::ComputeMerkleRootFromPayloads( + std::move( produced_payloads ) ); if ( produced_outputs_root != produced_outputs_root_from_payloads ) { return std::nullopt; @@ -4029,7 +4048,7 @@ namespace sgns }; std::vector leaves; - auto utxos = account_m->GetUTXOManager().GetUTXOsForReservation( tx->GetSrcAddress(), tx->GetHash() ); + auto utxos = account_m->GetUTXOManager().GetUTXOsForReservation( tx->GetSrcAddress(), tx->GetHash() ); leaves.reserve( utxos.size() ); for ( const auto &utxo : utxos ) { diff --git a/src/account/TransactionManager.hpp b/src/account/TransactionManager.hpp index bca80839f..5f2698ba3 100644 --- a/src/account/TransactionManager.hpp +++ b/src/account/TransactionManager.hpp @@ -122,9 +122,9 @@ namespace sgns const std::string &dev_addr, uint64_t peers_cut, const std::string &job_id ); - outcome::result PayEscrow( const std::string &escrow_path, - const SGProcessing::TaskResult &task_result, - std::shared_ptr crdt_transaction ); + outcome::result PayEscrow( const std::string &escrow_path, + const SGProcessing::TaskResult &task_result, + std::shared_ptr crdt_transaction ); // Wait for an incoming transaction to be processed with a timeout TransactionStatus WaitForTransactionIncoming( const std::string &txId, @@ -205,11 +205,12 @@ namespace sgns TransactionStatus status; uint64_t cached_nonce; // Cache nonce to avoid dereferencing tx }; + struct AccountUTXOState { - uint64_t version{ 0 }; + uint64_t version{ 0 }; base::Hash256 root{}; - bool initialized{ false }; + bool initialized{ false }; }; TransactionManager( std::shared_ptr processing_db, @@ -244,8 +245,7 @@ namespace sgns 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 UpdateAccountUTXOState( const std::unordered_set &addresses, bool increment_version ); void InitializeUTXOs(); void InitTransactions(); @@ -269,7 +269,7 @@ namespace sgns bool SetOutgoingStatusByNonce( uint64_t nonce, TransactionStatus s ); - void OnConsensusCertificate( const std::string &tx_hash ); + outcome::result OnConsensusCertificate( const std::string &tx_hash ); std::shared_ptr globaldb_m; @@ -377,8 +377,8 @@ namespace sgns INVALID }; - outcome::result GetTransactionCID( const std::string &tx_hash ) const; - outcome::result HandleNonceConsensusSubject( + 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; @@ -389,10 +389,10 @@ namespace sgns 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 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, diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 32f6336f1..57ad8e9c5 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -69,6 +69,14 @@ namespace sgns std::move( signer ), address, consensus_topic ) ); + instance->certificate_work_journal_ = instance->db_->GetWorkJournal(); + + if ( !instance->certificate_work_journal_ ) + { + ConsensusManagerLogger()->error( "{}: Failed to create ConsensusManager: crdt work journal is empty", + __func__ ); + return nullptr; + } instance->consensus_subs_future_ = std::move( instance->pubsub_->Subscribe( instance->consensus_messages_topic_, @@ -91,6 +99,7 @@ namespace sgns { ConsensusManagerLogger()->error( "{}: Failed to register certificate filter", __func__ ); } + instance->RecoverPendingCertificateWork(); return instance; } @@ -1117,11 +1126,10 @@ namespace sgns 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 ) ); + ConsensusManagerLogger()->error( "{}: rejected: proposal registry CID missing for hash {}. proposal_id={}", + __func__, + GetPrintableSubjectHash( proposal.subject() ), + proposal.proposal_id().substr( 0, 8 ) ); return; } @@ -1213,7 +1221,7 @@ namespace sgns return; } - if ( subject_result.value() == SubjectCheck::Reject ) + if ( subject_result.value() == Check::Reject ) { ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", __func__, @@ -1222,7 +1230,7 @@ namespace sgns return; } - if ( subject_result.value() == SubjectCheck::Pending ) + if ( subject_result.value() == Check::Pending ) { { std::lock_guard lock( proposals_mutex_ ); @@ -1283,7 +1291,7 @@ namespace sgns continue; } - if ( subject_result.value() == SubjectCheck::Reject ) + if ( subject_result.value() == Check::Reject ) { ConsensusManagerLogger()->error( "{}: rejected: subject check failed for hash {} proposal_id={}", __func__, @@ -1292,7 +1300,7 @@ namespace sgns continue; } - if ( subject_result.value() == SubjectCheck::Pending ) + if ( subject_result.value() == Check::Pending ) { auto subject_hash_result = GetSubjectHash( proposal.subject() ); if ( subject_hash_result.has_error() ) @@ -1461,7 +1469,7 @@ namespace sgns bool ConsensusManager::RegisterCertificateFilter() { - const std::string pattern = "^/?cert/[^/]+"; + const std::string pattern = std::string( CERT_KEY_PATTERN ); auto weak_self = weak_from_this(); const bool filter_registered = db_->RegisterElementFilter( @@ -1501,7 +1509,13 @@ namespace sgns return std::vector{}; } - if ( !ValidateCertificate( certificate ) ) + if ( certificate.proposal_id().empty() ) + { + ConsensusManagerLogger()->error( "{}: missing proposal_id, rejecting: {}", __func__, element.key() ); + return std::vector{}; + } + + if ( ValidateCertificate( certificate ) == Check::Reject ) { ConsensusManagerLogger()->error( "{}: validation failed, rejecting: {}", __func__, element.key() ); return std::vector{}; @@ -1526,6 +1540,22 @@ namespace sgns auto subject_hash = GetSubjectHash( certificate.proposal().subject() ); if ( subject_hash.has_error() ) { + ConsensusManagerLogger()->error( "{}: failed getting subject hash proposal_id={} error={}", + __func__, + certificate.proposal_id().substr( 0, 8 ), + subject_hash.error().message() ); + return; + } + + auto certificate_check = ValidateCertificate( certificate ); + + if ( certificate_check == Check::Stalled ) + { + ConsensusManagerLogger()->error( + "{}: Validation of the certificate pending for key {}, certificate handler not called ", + __func__, + key ); + certificate_work_journal_->MarkStalled( key ); return; } @@ -1542,20 +1572,40 @@ namespace sgns handler = it->second; } - handler( subject_hash.value(), certificate ); + auto certificate_handler_result = handler( subject_hash.value(), certificate ); + + if ( certificate_handler_result.has_error() ) + { + ConsensusManagerLogger()->error( "{}: certificate handler error proposal_id={} error={}", + __func__, + certificate.proposal_id().substr( 0, 8 ), + certificate_handler_result.error().message() ); + return; + } + auto certificate_result = certificate_handler_result.value(); + + if ( certificate_result == Check::Stalled ) + { + ConsensusManagerLogger()->error( "{}: certificate rejected by handler proposal_id={}", + __func__, + certificate.proposal_id().substr( 0, 8 ) ); + ConsensusManagerLogger()->debug( "{}: Key {} is not Done yet", __func__, key ); + certificate_work_journal_->MarkStalled( key ); + return; + } } - bool ConsensusManager::ValidateCertificate( const Certificate &certificate ) const + ConsensusManager::Check ConsensusManager::ValidateCertificate( const Certificate &certificate ) const { if ( certificate.proposal_id().empty() ) { ConsensusManagerLogger()->error( "{}: Certificate proposal ID missing ", __func__ ); - return false; + return Check::Reject; } if ( !certificate.has_proposal() ) { ConsensusManagerLogger()->error( "{}: Certificate missing proposal ", __func__ ); - return false; + return Check::Reject; } const auto &proposal = certificate.proposal(); @@ -1565,7 +1615,7 @@ namespace sgns __func__, certificate.proposal_id(), proposal.proposal_id() ); - return false; + return Check::Reject; } if ( proposal.registry_cid() != certificate.registry_cid() || proposal.registry_epoch() != certificate.registry_epoch() ) @@ -1573,7 +1623,7 @@ namespace sgns ConsensusManagerLogger()->error( "{}: rejected: registry mismatch proposal_id={}", __func__, certificate.proposal_id() ); - return false; + return Check::Reject; } auto registry_ret = registry_->LoadRegistry( certificate.registry_cid() ); if ( registry_ret.has_error() ) @@ -1583,7 +1633,7 @@ namespace sgns registry_ret.error().message(), certificate.registry_cid(), certificate.proposal_id() ); - return false; + return Check::Stalled; } auto ®istry = registry_ret.value(); if ( !ValidateSubject( proposal.subject() ) ) @@ -1591,21 +1641,21 @@ namespace sgns ConsensusManagerLogger()->error( "{}: rejected: invalid subject proposal_id={}", __func__, proposal.proposal_id() ); - return false; + return Check::Reject; } if ( !CheckProposal( proposal ) ) { ConsensusManagerLogger()->error( "{}: rejected: invalid proposal proposal_id={}", __func__, proposal.proposal_id() ); - return false; + return Check::Reject; } const auto computed_id = CreateProposalId( proposal ); if ( computed_id.empty() ) { ConsensusManagerLogger()->error( "{}: rejected: computed_id empty", __func__ ); - return false; + return Check::Reject; } if ( computed_id != certificate.proposal_id() ) { @@ -1613,7 +1663,7 @@ namespace sgns __func__, certificate.proposal_id(), computed_id ); - return false; + return Check::Reject; } std::vector votes; @@ -1625,10 +1675,10 @@ namespace sgns auto tally = TallyVotes( proposal, votes, registry, certificate.registry_cid() ); if ( tally.has_error() || !tally.value().has_quorum ) { - return false; + return Check::Reject; } - return true; + return Check::Approve; } void ConsensusManager::HandleVote( const Vote &vote ) @@ -1693,7 +1743,7 @@ namespace sgns pending_votes_.erase( vote.proposal_id() ); return; } - auto slot_it = slot_states_.find( proposal_state.slot_key ); + 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={}", @@ -1790,7 +1840,7 @@ namespace sgns { ConsensusManagerLogger()->trace( "{}: called proposal_id={}", __func__, certificate.proposal_id() ); - if ( !ValidateCertificate( certificate ) ) + if ( ValidateCertificate( certificate ) == Check::Reject ) { ConsensusManagerLogger()->error( "{}: rejected: invalid certificate proposal_id={}", __func__, @@ -1926,12 +1976,11 @@ namespace sgns 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() ); + 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 ); @@ -2022,11 +2071,12 @@ namespace sgns 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 ) + 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; @@ -2184,8 +2234,10 @@ namespace sgns 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(); + 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; @@ -2308,7 +2360,8 @@ namespace sgns 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 ) + 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; @@ -2383,6 +2436,32 @@ namespace sgns return true; } + void ConsensusManager::RecoverPendingCertificateWork() + { + //auto recovered = certificate_work_journal_->RecoverStaleProcessing( CERT_KEY_PATTERN, + // std::chrono::seconds( 15 ) ); + //if ( recovered > 0 ) + //{ + // ConsensusManagerLogger()->info( "{}: recovered {} stale certificate work items", __func__, recovered ); + //} + + auto unfinished = certificate_work_journal_->ListUnfinished( CERT_KEY_PATTERN ); + + for ( const auto &entry : unfinished ) + { + if ( entry.key.empty() ) + { + continue; + } + auto value = db_->Get( { entry.key } ); + if ( value.has_error() ) + { + continue; + } + CertificateReceived( { entry.key, value.value() }, std::string{} ); + } + } + outcome::result ConsensusManager::GetCertificateBySubjectHash( const std::string &subject_hash ) const { diff --git a/src/blockchain/Consensus.hpp b/src/blockchain/Consensus.hpp index 6f3fa43be..fba2787a5 100644 --- a/src/blockchain/Consensus.hpp +++ b/src/blockchain/Consensus.hpp @@ -24,6 +24,7 @@ #include "blockchain/ValidatorRegistry.hpp" #include "blockchain/impl/proto/Consensus.pb.h" +#include "crdt/globaldb/crdt_work_journal.hpp" #include "crdt/globaldb/globaldb.hpp" #include "crdt/proto/delta.pb.h" #include "ipfs_pubsub/gossip_pubsub.hpp" @@ -65,20 +66,21 @@ namespace sgns using Signer = std::function>( std::vector payload )>; /** - * @brief Subject checking values + * @brief Object checking values */ - enum class SubjectCheck + enum class Check { - Approve, ///< Subject is approved - Reject, ///< Subject is rejected - Pending ///< Subject evaluation is pending + Approve, ///< Object is approved + Reject, ///< Object is rejected + Pending, ///< Object evaluation is pending + Stalled ///< Object evaluation is stalled }; /// @brief Alias for a subject handler method type - using SubjectHandler = std::function( const Subject &subject )>; + using SubjectHandler = std::function( const Subject &subject )>; /// @brief Alias for a certificate handler method type using CertificateSubjectHandler = - std::function; + std::function( const std::string &subject_hash, const Certificate &certificate )>; /** * @brief Quorum tally structure @@ -136,28 +138,29 @@ namespace sgns 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 ); + 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; @@ -183,6 +186,7 @@ namespace sgns static constexpr std::chrono::milliseconds DEFAULT_ROUND_DURATION = std::chrono::milliseconds( 500 ); static constexpr std::chrono::milliseconds DEFAULT_ROUND_SKEW = std::chrono::milliseconds( 250 ); static constexpr uint64_t NO_ROUND = std::numeric_limits::max(); + static constexpr std::string_view CERT_KEY_PATTERN = "^/?cert/[^/]+"; struct ProposalState { @@ -228,9 +232,10 @@ namespace sgns 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 RecoverPendingCertificateWork(); + ConsensusManager::Check ValidateCertificate( const Certificate &certificate ) const; + static std::string CreateProposalId( const Proposal &proposal ); + static bool ValidateSubject( const Subject &subject ); void OnConsensusMessage( boost::optional message ); void UpdateCertificatesPending(); @@ -240,6 +245,7 @@ namespace sgns static std::string GetPrintableSubjectHash( const Subject &subject ); std::shared_ptr registry_; std::shared_ptr db_; + std::shared_ptr certificate_work_journal_; std::unordered_map subject_handlers_; mutable std::shared_mutex subject_handlers_mutex_; std::unordered_map certificate_subject_handlers_; diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 0116f7b9f..160379629 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -693,7 +693,8 @@ namespace sgns return base_registry_cid + ":" + std::to_string( base_registry_epoch ); } - outcome::result ValidatorRegistry::ComputeBatchRoot( const std::vector &subject_hashes ) const + outcome::result ValidatorRegistry::ComputeBatchRoot( + const std::vector &subject_hashes ) const { if ( subject_hashes.empty() ) { @@ -709,7 +710,7 @@ namespace sgns payload += subject_hashes[i]; } sgns::crypto::HasherImpl hasher; - auto hash = hasher.sha2_256( + auto hash = hasher.sha2_256( gsl::span( reinterpret_cast( payload.data() ), payload.size() ) ); return base::hex_lower( gsl::span( hash.data(), hash.size() ) ); } @@ -763,7 +764,7 @@ namespace sgns } sgns::ConsensusCertificate certificate; - std::string serialized = std::string(cert_get.value().toString()); + std::string serialized = std::string( cert_get.value().toString() ); if ( !certificate.ParseFromString( serialized ) ) { return outcome::failure( std::errc::invalid_argument ); @@ -798,13 +799,13 @@ namespace sgns } outcome::result ValidatorRegistry::TryCreateAndSubmitBatchProposal( const std::string &base_registry_cid, - uint64_t base_registry_epoch ) + uint64_t base_registry_epoch ) { std::function( const ConsensusSubject &subject )> submitter; - size_t threshold = 0; + size_t threshold = 0; { std::lock_guard lock( batch_mutex_ ); - submitter = submit_batch_subject_; + submitter = submit_batch_subject_; threshold = certificates_per_batch_; } if ( !submitter || threshold == 0 ) @@ -845,7 +846,7 @@ namespace sgns { std::lock_guard lock( batch_mutex_ ); - auto batch_hash_result = ExtractConsensusSubjectHash( subject_result.value() ); + auto batch_hash_result = ExtractConsensusSubjectHash( subject_result.value() ); if ( batch_hash_result.has_error() ) { return outcome::failure( batch_hash_result.error() ); @@ -868,8 +869,8 @@ namespace sgns return outcome::success( BatchSubjectDecision::Reject ); } - const auto &payload = subject.registry_batch(); - auto selected_result = SelectBatchSubjects( payload.base_registry_cid(), + 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() ) ); @@ -897,28 +898,29 @@ namespace sgns return outcome::success( BatchSubjectDecision::Approve ); } - void ValidatorRegistry::HandleBatchCertificate( const std::string &subject_hash, - const sgns::ConsensusCertificate &certificate ) + outcome::result ValidatorRegistry::HandleBatchCertificate( + const std::string &subject_hash, + const sgns::ConsensusCertificate &certificate ) { { std::lock_guard lock( batch_mutex_ ); if ( finalized_batch_subject_ids_.find( subject_hash ) != finalized_batch_subject_ids_.end() ) { - return; + return BatchCertificateDecision::Approve; } if ( applying_batch_subject_ids_.find( subject_hash ) != applying_batch_subject_ids_.end() ) { - return; + return BatchCertificateDecision::Approve; } applying_batch_subject_ids_.insert( subject_hash ); } if ( !certificate.has_proposal() || !certificate.proposal().has_subject() || certificate.proposal().subject().type() != SubjectType::SUBJECT_REGISTRY_BATCH || - !certificate.proposal().subject().has_registry_batch() ) + !certificate.proposal().subject().has_registry_batch() ) { std::lock_guard lock( batch_mutex_ ); applying_batch_subject_ids_.erase( subject_hash ); - return; + return BatchCertificateDecision::Reject; } auto current_registry_result = LoadRegistry(); @@ -926,17 +928,17 @@ namespace sgns { std::lock_guard lock( batch_mutex_ ); applying_batch_subject_ids_.erase( subject_hash ); - return; + return BatchCertificateDecision::Reject; } if ( !ValidateCertificate( certificate, current_registry_result.value() ) ) { std::lock_guard lock( batch_mutex_ ); applying_batch_subject_ids_.erase( subject_hash ); - return; + return BatchCertificateDecision::Reject; } - const auto &payload = certificate.proposal().subject().registry_batch(); - auto selected_result = SelectBatchSubjects( payload.base_registry_cid(), + 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() ) ); @@ -944,7 +946,7 @@ namespace sgns { std::lock_guard lock( batch_mutex_ ); applying_batch_subject_ids_.erase( subject_hash ); - return; + return BatchCertificateDecision::Reject; } auto base_registry_result = LoadRegistry( payload.base_registry_cid() ); @@ -952,7 +954,7 @@ namespace sgns { std::lock_guard lock( batch_mutex_ ); applying_batch_subject_ids_.erase( subject_hash ); - return; + return BatchCertificateDecision::Stalled; } std::vector certificates; @@ -964,7 +966,7 @@ namespace sgns { std::lock_guard lock( batch_mutex_ ); applying_batch_subject_ids_.erase( subject_hash ); - return; + return BatchCertificateDecision::Reject; } certificates.push_back( cert_result.value() ); } @@ -1005,13 +1007,14 @@ namespace sgns { std::lock_guard lock( batch_mutex_ ); applying_batch_subject_ids_.erase( subject_hash ); - return; + return outcome::failure( std::errc::invalid_argument ); } update.set_certificate( serialized_cert ); for ( const auto &tx_subject_hash : selected_result.value() ) { update.add_batch_certificate_subject_hashes( tx_subject_hash ); } + return BatchCertificateDecision::Approve; std::thread( [weak_self = weak_from_this(), subject_hash, update = std::move( update )]() mutable @@ -1021,7 +1024,7 @@ namespace sgns { return; } - auto store_result = self->StoreRegistryUpdate( update ); + 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() ) @@ -1239,8 +1242,8 @@ namespace sgns 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() || + 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__ ); @@ -1277,7 +1280,9 @@ namespace sgns auto certificate_result = LoadCertificateBySubjectHash( subject_hash ); if ( certificate_result.has_error() ) { - logger_->error( "{}: missing certificate for batch hash={}", __func__, subject_hash.substr( 0, 8 ) ); + logger_->error( "{}: missing certificate for batch hash={}", + __func__, + subject_hash.substr( 0, 8 ) ); return false; } const auto &tx_cert = certificate_result.value(); @@ -1731,9 +1736,9 @@ namespace sgns continue; } - const bool approve = vote_it->second; - uint32_t penalty = static_cast( entry.penalty_score() ); - const uint32_t cap = weight_config_.penalty_cap_; + 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(); @@ -1891,7 +1896,7 @@ namespace sgns continue; } const uint64_t old_weight = entries[i].weight(); - const uint64_t scaled = ( entries[i].weight() * weight_cap ) / total_active; + const uint64_t scaled = ( entries[i].weight() * weight_cap ) / total_active; entries[i].set_weight( scaled ); scaled_sum += scaled; active_indices.push_back( i ); diff --git a/src/blockchain/ValidatorRegistry.hpp b/src/blockchain/ValidatorRegistry.hpp index ade047717..cb48f5b66 100644 --- a/src/blockchain/ValidatorRegistry.hpp +++ b/src/blockchain/ValidatorRegistry.hpp @@ -84,14 +84,14 @@ namespace sgns 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; + Registry CreateGenesisRegistry( const std::string &genesis_validator_id ) const; + outcome::result StoreGenesisRegistry( const std::string &genesis_validator_id, + std::function( std::vector )> sign ); + outcome::result LoadRegistry() const; + outcome::result LoadRegistry( const std::string &cid ) const; + outcome::result LoadRegistryUpdate() const; outcome::result> GetValidatorWeight( const std::string &validator_id ) const; - bool RegisterFilter(); + bool RegisterFilter(); outcome::result CreateUpdateFromCertificate( const sgns::ConsensusCertificate &certificate ); outcome::result StoreRegistryUpdate( const RegistryUpdate &update ); outcome::result> BeginRegistryUpdateTransaction( @@ -107,7 +107,7 @@ namespace sgns void SetCertificatesPerBatch( size_t batch_size ); void SetBatchSubjectSubmitter( std::function( const ConsensusSubject &subject )> submitter ); - void OnFinalizedCertificate( const sgns::ConsensusCertificate &certificate ); + void OnFinalizedCertificate( const sgns::ConsensusCertificate &certificate ); enum class BatchSubjectDecision { @@ -115,9 +115,17 @@ namespace sgns Reject, Pending }; - outcome::result EvaluateBatchSubject( const ConsensusSubject &subject ); - void HandleBatchCertificate( const std::string &subject_hash, - const sgns::ConsensusCertificate &certificate ); + enum class BatchCertificateDecision + { + Approve, + Reject, + Pending, + Stalled + }; + outcome::result EvaluateBatchSubject( const ConsensusSubject &subject ); + outcome::result HandleBatchCertificate( + const std::string &subject_hash, + const sgns::ConsensusCertificate &certificate ); static constexpr std::string_view RegistryKey() { @@ -175,30 +183,33 @@ namespace sgns 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(); + 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 ); + 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_; @@ -214,10 +225,10 @@ namespace sgns 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::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_; diff --git a/src/blockchain/impl/Blockchain.cpp b/src/blockchain/impl/Blockchain.cpp index 2135c2e95..be77d6a5a 100644 --- a/src/blockchain/impl/Blockchain.cpp +++ b/src/blockchain/impl/Blockchain.cpp @@ -185,7 +185,8 @@ namespace sgns { if ( auto strong = weak_ptr.lock() ) { - auto weight_result = strong->validator_registry_->GetValidatorWeight( strong->account_->GetAddress() ); + auto weight_result = strong->validator_registry_->GetValidatorWeight( + strong->account_->GetAddress() ); if ( weight_result.has_error() ) { return outcome::failure( weight_result.error() ); @@ -194,10 +195,11 @@ namespace sgns { return outcome::success(); } - auto proposal_result = strong->consensus_manager_->CreateProposal( subject, - strong->account_->GetAddress(), - strong->validator_registry_->GetRegistryCid(), - strong->validator_registry_->GetRegistryEpoch() ); + 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() ); @@ -210,7 +212,7 @@ namespace sgns instance->consensus_manager_->RegisterSubjectHandler( SubjectType::SUBJECT_REGISTRY_BATCH, [weak_ptr( std::weak_ptr( instance ) )]( - const ConsensusManager::Subject &subject ) -> outcome::result + const ConsensusManager::Subject &subject ) -> outcome::result { if ( auto strong = weak_ptr.lock() ) { @@ -222,12 +224,12 @@ namespace sgns switch ( decision_result.value() ) { case ValidatorRegistry::BatchSubjectDecision::Approve: - return ConsensusManager::SubjectCheck::Approve; + return ConsensusManager::Check::Approve; case ValidatorRegistry::BatchSubjectDecision::Pending: - return ConsensusManager::SubjectCheck::Pending; + return ConsensusManager::Check::Pending; case ValidatorRegistry::BatchSubjectDecision::Reject: default: - return ConsensusManager::SubjectCheck::Reject; + return ConsensusManager::Check::Reject; } } return outcome::failure( std::errc::owner_dead ); @@ -235,13 +237,31 @@ namespace sgns instance->consensus_manager_->RegisterCertificateHandler( SubjectType::SUBJECT_REGISTRY_BATCH, - [weak_ptr( std::weak_ptr( instance ) )]( const std::string &subject_hash, - const ConsensusCertificate &certificate ) + [weak_ptr( std::weak_ptr( instance ) )]( + const std::string &subject_hash, + const ConsensusCertificate &certificate ) -> outcome::result { if ( auto strong = weak_ptr.lock() ) { - strong->validator_registry_->HandleBatchCertificate( subject_hash, certificate ); + auto decision = strong->validator_registry_->HandleBatchCertificate( subject_hash, certificate ); + if ( decision.has_error() ) + { + return outcome::failure( decision.error() ); + } + switch ( decision.value() ) + { + case ValidatorRegistry::BatchCertificateDecision::Approve: + return ConsensusManager::Check::Approve; + case ValidatorRegistry::BatchCertificateDecision::Pending: + return ConsensusManager::Check::Pending; + case ValidatorRegistry::BatchCertificateDecision::Stalled: + return ConsensusManager::Check::Stalled; + case ValidatorRegistry::BatchCertificateDecision::Reject: + default: + return ConsensusManager::Check::Reject; + } } + return outcome::failure( std::errc::owner_dead ); } ); auto ensure_registry_result = instance->EnsureValidatorRegistry(); @@ -1523,8 +1543,9 @@ namespace sgns return outcome::success(); } - outcome::result Blockchain::AccountCreationReceivedCallback( const crdt::CRDTCallbackManager::NewDataPair &new_data, - const std::string &cid ) + outcome::result Blockchain::AccountCreationReceivedCallback( + const crdt::CRDTCallbackManager::NewDataPair &new_data, + const std::string &cid ) { logger_->debug( "[{}] Account creation received callback triggered with CID: {}", account_->GetAddress().substr( 0, 8 ), @@ -1664,28 +1685,30 @@ namespace sgns 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 ) + 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::result Blockchain::CreateConsensusProposal( + const std::string &account_id, + uint64_t nonce, + const std::string &tx_hash, + const std::optional &utxo_commitment, + const std::optional &utxo_witness ) { BOOST_OUTCOME_TRY( auto &&nonce_subject, - CreateConsensusNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ) ); + CreateConsensusNonceSubject( account_id, nonce, tx_hash, utxo_commitment, utxo_witness ) ); BOOST_OUTCOME_TRY( auto &&nonce_proposal, - consensus_manager_->CreateProposal( nonce_subject, - account_id, - validator_registry_->GetRegistryCid(), - validator_registry_->GetRegistryEpoch() ) ); + consensus_manager_->CreateProposal( nonce_subject, + account_id, + validator_registry_->GetRegistryCid(), + validator_registry_->GetRegistryEpoch() ) ); return nonce_proposal; } diff --git a/src/crdt/crdt_datastore.hpp b/src/crdt/crdt_datastore.hpp index c0e5ac01f..c2b8b814d 100644 --- a/src/crdt/crdt_datastore.hpp +++ b/src/crdt/crdt_datastore.hpp @@ -403,7 +403,7 @@ namespace sgns::crdt outcome::result WaitForJob( const CID &cid ); private: - CrdtDatastore() = default; + CrdtDatastore() = delete; CrdtDatastore( std::shared_ptr aDatastore, const HierarchicalKey &aKey, diff --git a/src/crdt/globaldb/crdt_work_journal.hpp b/src/crdt/globaldb/crdt_work_journal.hpp index 459b69680..fbc3f972a 100644 --- a/src/crdt/globaldb/crdt_work_journal.hpp +++ b/src/crdt/globaldb/crdt_work_journal.hpp @@ -24,6 +24,7 @@ namespace sgns::crdt { Seen = 0, Processing = 1, + Stalled = 2, }; struct Entry @@ -35,23 +36,27 @@ namespace sgns::crdt uint64_t lease_until_ms = 0; }; - explicit CRDTWorkJournal( std::shared_ptr datastore ); + static std::shared_ptr New( std::shared_ptr datastore ); void MarkSeen( const std::string &key ); void MarkProcessing( const std::string &key, std::chrono::milliseconds lease = std::chrono::minutes( 5 ) ); + void MarkStalled( const std::string &key, std::chrono::milliseconds lease = std::chrono::minutes( 5 ) ); bool MarkDone( const std::string &key ); std::optional GetEntry( const std::string &key ) const; - std::vector ListUnfinished() const; - size_t RecoverStaleProcessing( std::chrono::milliseconds stale = std::chrono::milliseconds( 0 ) ); + std::vector ListUnfinished( std::string_view key_pattern = {} ) const; + size_t RecoverStaleProcessing( std::string_view key_pattern, + std::chrono::milliseconds stale = std::chrono::milliseconds( 0 ) ); private: static constexpr std::string_view NAMESPACE_PREFIX = "/crdt/work/"; - static uint64_t NowMs(); - std::string BuildStorageKey( const std::string &key ) const; - static std::optional DeserializeEntry( std::string_view storage_key, std::string_view value ); - static std::string SerializeEntry( const Entry &entry ); - static std::vector Split( const std::string &value, char separator ); + + explicit CRDTWorkJournal( std::shared_ptr datastore ); + static uint64_t NowMs(); + std::string BuildStorageKey( const std::string &key ) const; + static std::optional DeserializeEntry( std::string_view storage_key, std::string_view value ); + static std::string SerializeEntry( const Entry &entry ); + static std::vector Split( const std::string &value, char separator ); std::optional GetEntryUnlocked( const std::string &key ) const; bool PutEntryUnlocked( const Entry &entry ) const; diff --git a/src/crdt/impl/crdt_callback_manager.cpp b/src/crdt/impl/crdt_callback_manager.cpp index 0e5e6d678..bc4e2ea59 100644 --- a/src/crdt/impl/crdt_callback_manager.cpp +++ b/src/crdt/impl/crdt_callback_manager.cpp @@ -145,6 +145,14 @@ namespace sgns::crdt { logger_->info( "Executing callback for key '{}' matching pattern '{}'", key, pattern ); callback( std::make_pair( key, value ), cid ); + if ( auto entry = work_journal_->GetEntry( key ); + entry.has_value() && entry->state == CRDTWorkJournal::State::Processing ) + { + if ( !work_journal_->MarkDone( key ) ) + { + logger_->error( "Failed to auto-complete CRDT work for key '{}'", key ); + } + } callback_triggered = true; } } diff --git a/src/crdt/impl/crdt_work_journal.cpp b/src/crdt/impl/crdt_work_journal.cpp index ad60dd4ce..788f9342c 100644 --- a/src/crdt/impl/crdt_work_journal.cpp +++ b/src/crdt/impl/crdt_work_journal.cpp @@ -8,6 +8,15 @@ namespace sgns::crdt { + std::shared_ptr CRDTWorkJournal::New( std::shared_ptr datastore ) + { + if ( !datastore ) + { + return nullptr; + } + return std::shared_ptr( new CRDTWorkJournal( std::move( datastore ) ) ); + } + CRDTWorkJournal::CRDTWorkJournal( std::shared_ptr datastore ) : datastore_( std::move( datastore ) ) { @@ -59,6 +68,27 @@ namespace sgns::crdt PutEntryUnlocked( entry ); } + void CRDTWorkJournal::MarkStalled( const std::string &key, std::chrono::milliseconds lease ) + { + if ( key.empty() ) + { + return; + } + std::lock_guard lock( mutex_ ); + auto maybe_entry = GetEntryUnlocked( key ); + if ( !maybe_entry.has_value() ) + { + return; + } + + auto entry = maybe_entry.value(); + entry.state = State::Stalled; + entry.updated_at_ms = NowMs(); + entry.lease_until_ms = entry.updated_at_ms + static_cast( std::max( 0, lease.count() ) ); + entry.attempt_count += 1; + PutEntryUnlocked( entry ); + } + bool CRDTWorkJournal::MarkDone( const std::string &key ) { if ( key.empty() ) @@ -85,7 +115,7 @@ namespace sgns::crdt return GetEntryUnlocked( key ); } - std::vector CRDTWorkJournal::ListUnfinished() const + std::vector CRDTWorkJournal::ListUnfinished( std::string_view key_pattern ) const { std::lock_guard lock( mutex_ ); std::vector out; @@ -101,19 +131,37 @@ namespace sgns::crdt { return out; } + + std::optional pattern_regex; + if ( !key_pattern.empty() ) + { + try + { + pattern_regex.emplace( std::string( key_pattern ) ); + } + catch ( const std::regex_error & ) + { + return out; + } + } + out.reserve( result.value().size() ); for ( const auto &[raw_key, raw_value] : result.value() ) { auto parsed = DeserializeEntry( raw_key.toString(), raw_value.toString() ); if ( parsed.has_value() ) { + if ( pattern_regex.has_value() && !std::regex_match( parsed->key, pattern_regex.value() ) ) + { + continue; + } out.push_back( std::move( parsed.value() ) ); } } return out; } - size_t CRDTWorkJournal::RecoverStaleProcessing( std::chrono::milliseconds stale ) + size_t CRDTWorkJournal::RecoverStaleProcessing( std::string_view key_pattern, std::chrono::milliseconds stale ) { std::lock_guard lock( mutex_ ); if ( !datastore_ ) @@ -132,6 +180,19 @@ namespace sgns::crdt { return recovered; } + std::optional pattern_regex; + if ( !key_pattern.empty() ) + { + try + { + pattern_regex.emplace( std::string( key_pattern ) ); + } + catch ( const std::regex_error & ) + { + return recovered; + } + } + for ( const auto &[raw_key, raw_value] : result.value() ) { auto parsed = DeserializeEntry( raw_key.toString(), raw_value.toString() ); @@ -140,6 +201,10 @@ namespace sgns::crdt continue; } auto &entry = parsed.value(); + if ( pattern_regex.has_value() && !std::regex_match( entry.key, pattern_regex.value() ) ) + { + continue; + } if ( entry.state != State::Processing ) { continue; @@ -166,7 +231,7 @@ namespace sgns::crdt std::string CRDTWorkJournal::BuildStorageKey( const std::string &key ) const { - return NAMESPACE_PREFIX + key; + return std::string( NAMESPACE_PREFIX ) + key; } std::optional CRDTWorkJournal::DeserializeEntry( std::string_view storage_key, @@ -181,7 +246,8 @@ namespace sgns::crdt try { const auto state = std::stoi( fields[1] ); - if ( state != static_cast( State::Seen ) && state != static_cast( State::Processing ) ) + if ( state != static_cast( State::Seen ) && state != static_cast( State::Processing ) && + state != static_cast( State::Stalled ) ) { return std::nullopt; } diff --git a/test/src/blockchain/consensus_certificate_test.cpp b/test/src/blockchain/consensus_certificate_test.cpp index 94ffc4447..6840d2922 100644 --- a/test/src/blockchain/consensus_certificate_test.cpp +++ b/test/src/blockchain/consensus_certificate_test.cpp @@ -11,7 +11,7 @@ namespace { - constexpr const char *kTestPrivateKey = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce8b1a6f0d4f3b9b7f0a1b2"; + constexpr const char *kTestPrivateKey = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce8b1a6f0d4f3b9b7f0a1b2"; constexpr const char *kTestPrivateKey2 = "0x6c3e7b1a8d3f2c9b0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2"; std::shared_ptr MakeAccount( const std::string &path ) @@ -61,9 +61,9 @@ namespace } std::shared_ptr MakeManager( const std::shared_ptr ®istry, - const std::shared_ptr &db, + const std::shared_ptr &db, const std::shared_ptr &pubs, - const std::shared_ptr &account ) + const std::shared_ptr &account ) { auto manager = sgns::ConsensusManager::New( registry, @@ -78,7 +78,7 @@ namespace sgns::UTXOTransitionCommitment MakeTestCommitment() { sgns::UTXOTransitionCommitment commitment; - auto *consumed = commitment.add_consumed_outpoints(); + 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(); @@ -120,8 +120,11 @@ namespace sgns::test auto manager = MakeManager( registry, db_, pubs_, account ); std::string tx_hash = "0x010203"; - auto subject_result = - ConsensusManager::CreateNonceSubject( account->GetAddress(), 1, tx_hash, MakeTestCommitment(), MakeTestWitness() ); + 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(), @@ -153,8 +156,11 @@ namespace sgns::test auto manager = MakeManager( registry, db_, pubs_, account ); std::string tx_hash = "0x010203"; - auto subject_result = - ConsensusManager::CreateNonceSubject( account->GetAddress(), 7, tx_hash, MakeTestCommitment(), MakeTestWitness() ); + 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(), @@ -179,7 +185,7 @@ namespace sgns::test manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, []( const ConsensusManager::Subject & ) - { return ConsensusManager::SubjectCheck::Approve; } ); + { return ConsensusManager::Check::Approve; } ); manager->HandleProposal( proposal_result.value() ); EXPECT_TRUE( manager->proposals_.find( proposal_result.value().proposal_id() ) != manager->proposals_.end() ); manager->HandleCertificate( cert ); @@ -199,34 +205,34 @@ namespace sgns::test 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() ), + 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() ), + 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() ), + 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 ) ); }, - "" ), + EXPECT_EQ( ConsensusManager::New( + registry, + db_, + pubs_, + [account]( std::vector payload ) { return account->Sign( std::move( payload ) ); }, + "" ), nullptr ); } @@ -238,11 +244,11 @@ namespace sgns::test 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 & ) - { } ) ); + { return ConsensusManager::Check::Approve; } ) ); + EXPECT_TRUE( + manager->RegisterCertificateHandler( SubjectType::SUBJECT_NONCE, + []( const std::string &, const ConsensusManager::Certificate & ) + { return outcome::success( ConsensusManager::Check::Approve ); } ) ); EXPECT_TRUE( manager->subject_handlers_.find( static_cast( SubjectType::SUBJECT_NONCE ) ) != manager->subject_handlers_.end() ); EXPECT_TRUE( manager->certificate_subject_handlers_.find( static_cast( SubjectType::SUBJECT_NONCE ) ) != @@ -262,8 +268,11 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( - account->GetAddress(), 2, "0x0a0b0c", MakeTestCommitment(), MakeTestWitness() ); + 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(), @@ -302,7 +311,7 @@ namespace sgns::test TEST_F( ConsensusCertificateTest, CreateTaskResultSubjectAndComputeSubjectId ) { - auto account = MakeAccount( getPathString() ); + auto account = MakeAccount( getPathString() ); auto subject_result = ConsensusManager::CreateTaskResultSubject( account->GetAddress(), "escrow/path", "0xdeadbeef", @@ -322,8 +331,11 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( - account->GetAddress(), 3, "0x111213", MakeTestCommitment(), MakeTestWitness() ); + 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(), @@ -373,8 +385,11 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( - account->GetAddress(), 4, "0x222324", MakeTestCommitment(), MakeTestWitness() ); + 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(), @@ -385,7 +400,7 @@ namespace sgns::test manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, []( const ConsensusManager::Subject & ) - { return ConsensusManager::SubjectCheck::Approve; } ); + { return ConsensusManager::Check::Approve; } ); auto submit_prop = manager->SubmitProposal( proposal_result.value(), false ); EXPECT_FALSE( submit_prop.has_error() ); @@ -415,8 +430,11 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( - account->GetAddress(), 5, "0x333435", MakeTestCommitment(), MakeTestWitness() ); + 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(), @@ -427,14 +445,14 @@ namespace sgns::test manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, []( const ConsensusManager::Subject & ) - { return ConsensusManager::SubjectCheck::Pending; } ); + { return ConsensusManager::Check::Pending; } ); manager->HandleProposal( proposal_result.value() ); EXPECT_TRUE( manager->pending_proposals_.find( proposal_result.value().proposal_id() ) != manager->pending_proposals_.end() ); manager->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, []( const ConsensusManager::Subject & ) - { return ConsensusManager::SubjectCheck::Approve; } ); + { return ConsensusManager::Check::Approve; } ); auto resume = manager->ResumeProposalHandling( subject_result.value().nonce().tx_hash() ); EXPECT_FALSE( resume.has_error() ); @@ -449,9 +467,12 @@ namespace sgns::test 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() ); + 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(), @@ -479,20 +500,20 @@ namespace sgns::test { handler_called.store( true ); } + return outcome::success( ConsensusManager::Check::Approve ); } ); auto submit_result = manager->SubmitCertificate( cert_result.value() ); EXPECT_FALSE( submit_result.has_error() ); crdt::HierarchicalKey cert_key( "/cert/" + tx_hash ); - auto cert_get = db_->Get( cert_key ); + 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 ); + ASSERT_WAIT_FOR_CONDITION( [&handler_called]() { return handler_called.load(); }, + std::chrono::milliseconds( 2000 ), + "certificate handler", + nullptr ); } TEST_F( ConsensusCertificateTest, ValidateSubjectRejectsTamperedSubjectIdBinding ) @@ -501,8 +522,11 @@ namespace sgns::test auto registry = MakeRegistry( db_, account ); auto manager = MakeManager( registry, db_, pubs_, account ); - auto subject_result = ConsensusManager::CreateNonceSubject( - account->GetAddress(), 11, "0xabc123", MakeTestCommitment(), MakeTestWitness() ); + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 11, + "0xabc123", + MakeTestCommitment(), + MakeTestWitness() ); ASSERT_TRUE( subject_result.has_value() ); auto subject = subject_result.value(); @@ -519,7 +543,7 @@ namespace sgns::test auto manager = MakeManager( registry, db_, pubs_, account ); UTXOTransitionCommitment commitment; - auto *consumed = commitment.add_consumed_outpoints(); + 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' ) ); @@ -531,12 +555,11 @@ namespace sgns::test proof->set_output_index( 0 ); proof->set_leaf_payload( "leaf" ); - auto subject_result = ConsensusManager::CreateNonceSubject( - account->GetAddress(), - 1, - "0xdeadbeef", - commitment, - witness ); + auto subject_result = ConsensusManager::CreateNonceSubject( account->GetAddress(), + 1, + "0xdeadbeef", + commitment, + witness ); ASSERT_TRUE( subject_result.has_value() ); auto subject = subject_result.value(); From 79fed9924405e675c2e09f697efa1c97c74337c7 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Tue, 21 Apr 2026 17:12:24 -0300 Subject: [PATCH 113/114] Fix: Retrying of stalled objects --- src/account/TransactionManager.cpp | 13 ++--- src/blockchain/Consensus.cpp | 75 ++++++++++++++++++++--------- src/crdt/impl/crdt_work_journal.cpp | 2 +- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 9c3a874df..747a7516f 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -3146,12 +3146,13 @@ namespace sgns 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 ConsensusManager::Check::Stalled; + TransactionManagerLogger()->warn( + "[{} - full: {}] {}: Transaction not found for hash {}. Accepting certificate callback and waiting for tx ingestion path", + account_m->GetAddress().substr( 0, 8 ), + full_node_m, + __func__, + tx_hash ); + return ConsensusManager::Check::Approve; } TransactionManagerLogger()->debug( "[{} - full: {}] {}: Checking for conflicting transaction with {}", account_m->GetAddress().substr( 0, 8 ), diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index 57ad8e9c5..cbfc88ee9 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -152,6 +152,7 @@ namespace sgns round_timer_ = std::thread( [weak_self]() { + constexpr auto min_interval = std::chrono::milliseconds( 500 ); while ( true ) { auto self = weak_self.lock(); @@ -166,28 +167,26 @@ namespace sgns { interval = DEFAULT_ROUND_DURATION / 2; } - self->timer_cv_.wait( lock, - [self]() - { return self->stop_timer_.load() || self->certificates_pending_.load(); } ); + if ( interval < min_interval ) + { + interval = min_interval; + } + self->timer_cv_.wait_for( lock, + interval, + [self]() + { return self->stop_timer_.load() || self->certificates_pending_.load(); } ); if ( self->stop_timer_.load() ) { return; } lock.unlock(); - self->ProcessCertificates(); - self->UpdateCertificatesPending(); - lock.lock(); - if ( self->certificates_pending_.load() && !self->stop_timer_.load() ) + if ( self->certificates_pending_.load() ) { - self->timer_cv_.wait_for( - lock, - interval, - [self]() { return self->stop_timer_.load() || !self->certificates_pending_.load(); } ); - } - if ( self->stop_timer_.load() ) - { - return; + self->ProcessCertificates(); + self->UpdateCertificatesPending(); } + // Keep replaying unfinished certificate work while the node is running. + self->RecoverPendingCertificateWork(); } } ); } @@ -1567,6 +1566,7 @@ namespace sgns auto it = certificate_subject_handlers_.find( static_cast( certificate.proposal().subject().type() ) ); if ( it == certificate_subject_handlers_.end() ) { + (void)certificate_work_journal_->MarkDone( key ); return; } handler = it->second; @@ -1593,6 +1593,7 @@ namespace sgns certificate_work_journal_->MarkStalled( key ); return; } + (void)certificate_work_journal_->MarkDone( key ); } ConsensusManager::Check ConsensusManager::ValidateCertificate( const Certificate &certificate ) const @@ -2438,14 +2439,17 @@ namespace sgns void ConsensusManager::RecoverPendingCertificateWork() { - //auto recovered = certificate_work_journal_->RecoverStaleProcessing( CERT_KEY_PATTERN, - // std::chrono::seconds( 15 ) ); - //if ( recovered > 0 ) - //{ - // ConsensusManagerLogger()->info( "{}: recovered {} stale certificate work items", __func__, recovered ); - //} + auto recovered = certificate_work_journal_->RecoverStaleProcessing( CERT_KEY_PATTERN, std::chrono::seconds( 15 ) ); + if ( recovered > 0 ) + { + ConsensusManagerLogger()->info( "{}: recovered {} stale certificate work items", __func__, recovered ); + } auto unfinished = certificate_work_journal_->ListUnfinished( CERT_KEY_PATTERN ); + const auto now_ms = + static_cast( std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() ) + .count() ); for ( const auto &entry : unfinished ) { @@ -2453,6 +2457,14 @@ namespace sgns { continue; } + if ( entry.state != crdt::CRDTWorkJournal::State::Stalled ) + { + continue; + } + if ( entry.lease_until_ms != 0 && entry.lease_until_ms > now_ms ) + { + continue; + } auto value = db_->Get( { entry.key } ); if ( value.has_error() ) { @@ -2499,8 +2511,8 @@ namespace sgns { return false; } - //TODO - Check if we need to call ValidateCertificate here. I don't think so because it was validated before. - return true; + auto certificate_check = ValidateCertificate( certificate_result.value() ); + return certificate_check == Check::Approve; } bool ConsensusManager::CheckCertificateForSubject( const ConsensusManager::Subject &subject ) const @@ -2545,11 +2557,26 @@ namespace sgns } auto proposed_subject_id = subject_id_result.value(); bool equal = proposed_subject_id == certificate_subject_id; + if ( !equal ) + { + ConsensusManagerLogger()->debug( "{}: Match for subject and certificate (hash {}): MISMATCH", + __func__, + GetPrintableSubjectHash( subject ) ); + return false; + } + auto certificate_check = ValidateCertificate( certificate ); + if ( certificate_check != Check::Approve ) + { + ConsensusManagerLogger()->error( "{}: certificate failed validation for hash {}", + __func__, + GetPrintableSubjectHash( subject ) ); + return false; + } ConsensusManagerLogger()->debug( "{}: Match for subject and certificate (hash {}): {}", __func__, GetPrintableSubjectHash( subject ), equal ? "Match" : "MISMATCH" ); - return equal; + return true; } std::string ConsensusManager::GetPrintableSubjectHash( const Subject &subject ) diff --git a/src/crdt/impl/crdt_work_journal.cpp b/src/crdt/impl/crdt_work_journal.cpp index 788f9342c..8ba84bdd2 100644 --- a/src/crdt/impl/crdt_work_journal.cpp +++ b/src/crdt/impl/crdt_work_journal.cpp @@ -213,7 +213,7 @@ namespace sgns::crdt { continue; } - entry.state = State::Seen; + entry.state = State::Stalled; entry.updated_at_ms = now_ms; entry.lease_until_ms = 0; PutEntryUnlocked( entry ); From 3a60d04df4867725b7cd80305de2569fa5e7eb58 Mon Sep 17 00:00:00 2001 From: Henrique A Klein Date: Wed, 22 Apr 2026 13:02:50 -0300 Subject: [PATCH 114/114] Fix: Issues on registry update and timing of the consensus thread --- src/account/TransactionManager.cpp | 1 + src/blockchain/Consensus.cpp | 17 +++++++++++++---- src/blockchain/ValidatorRegistry.cpp | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/account/TransactionManager.cpp b/src/account/TransactionManager.cpp index 747a7516f..e293369c2 100644 --- a/src/account/TransactionManager.cpp +++ b/src/account/TransactionManager.cpp @@ -123,6 +123,7 @@ namespace sgns } return process_result; } + return outcome::failure( std::errc::owner_dead ); } ); instance->blockchain_->RegisterSubjectHandler( SubjectType::SUBJECT_NONCE, diff --git a/src/blockchain/Consensus.cpp b/src/blockchain/Consensus.cpp index cbfc88ee9..0c32811e3 100644 --- a/src/blockchain/Consensus.cpp +++ b/src/blockchain/Consensus.cpp @@ -171,10 +171,19 @@ namespace sgns { interval = min_interval; } - self->timer_cv_.wait_for( lock, - interval, - [self]() - { return self->stop_timer_.load() || self->certificates_pending_.load(); } ); + if ( self->certificates_pending_.load() ) + { + // Work is pending: run on cadence, only interrupt for shutdown. + self->timer_cv_.wait_for( lock, interval, [self]() { return self->stop_timer_.load(); } ); + } + else + { + // No pending work: wait up to interval, but wake immediately when new work appears. + self->timer_cv_.wait_for( + lock, + interval, + [self]() { return self->stop_timer_.load() || self->certificates_pending_.load(); } ); + } if ( self->stop_timer_.load() ) { return; diff --git a/src/blockchain/ValidatorRegistry.cpp b/src/blockchain/ValidatorRegistry.cpp index 160379629..f9604438a 100644 --- a/src/blockchain/ValidatorRegistry.cpp +++ b/src/blockchain/ValidatorRegistry.cpp @@ -1014,7 +1014,6 @@ namespace sgns { update.add_batch_certificate_subject_hashes( tx_subject_hash ); } - return BatchCertificateDecision::Approve; std::thread( [weak_self = weak_from_this(), subject_hash, update = std::move( update )]() mutable @@ -1039,6 +1038,7 @@ namespace sgns self->finalized_batch_subject_ids_.insert( subject_hash ); } ) .detach(); + return BatchCertificateDecision::Approve; } outcome::result> ValidatorRegistry::GetValidatorWeight(