Skip to content

transaction/TransactionConsensusHandler.cpp

Consensus-facing validation and finalization split out of TransactionManager. More...

Namespaces

Name
sgns

Detailed Description

Consensus-facing validation and finalization split out of TransactionManager.

Date: 2026-09-08 Eduardo Menges Mattje ([email protected])

Source code

#include "transaction/TransactionConsensusHandler.hpp"

#include <algorithm>
#include <unordered_map>
#include <utility>
#include <vector>

#include "transaction/TransactionManager.hpp"
#include "account/InputValidators.hpp"
#include "account/MigrationAllowList.hpp"
#include "account/MigrationTransaction.hpp"
#include "account/UTXOMerkle.hpp"
#include "crypto/hasher.hpp"

namespace sgns
{
    namespace
    {
        using TransactionStatus = TransactionManager::TransactionStatus;

        using input_validator_constants::HASH256_BYTES;
        using input_validator_constants::SERIALIZED_UINT32_BYTES;
        using utxo_merkle::HashLeaf;
        using utxo_merkle::OutPointKey;
        using utxo_merkle::SerializeUTXOLeafPayload;
    }

    void TransactionConsensusHandler::OnProposalTimeoutCleanup( const std::string &tx_hash )
    {
        auto tx = owner_.GetTransactionByHash( tx_hash );
        if ( !tx )
        {
            // D-10: Entry not found — silently return, nothing to clean up.
            return;
        }

        const auto tracked = owner_.GetTrackedTxByHash( tx_hash );
        if ( !tracked || tracked->status != TransactionStatus::VERIFYING )
        {
            // D-10: Entry not in map OR entry status is not VERIFYING → silently skip.
            return;
        }

        if ( tx->GetSrcAddress() == owner_.account_m->GetAddress() )
        {
            logger_->info( "{}: Proposal timeout — transitioning local tx to UNCONFIRMED tx={}", __func__, tx_hash );
            (void) owner_.ChangeTransactionState( tx, TransactionStatus::UNCONFIRMED );
            return;
        }

        logger_->info( "{}: Proposal timeout — removing remote temp entry tx={}", __func__, tx_hash );
        (void) owner_.RemoveTrackedIfVerifying( tx_hash );
    }

    outcome::result<ConsensusManager::Check> TransactionConsensusHandler::OnConsensusCertificate(
        const std::string          &tx_hash,
        const ConsensusCertificate &certificate )
    {
        logger_->debug( "{}: Consensus certificate arrived for transaction {}", __func__, tx_hash );
        auto tx = owner_.GetTransactionByHash( tx_hash );
        if ( !tx )
        {
            BOOST_OUTCOME_TRY( auto crdt_transaction, owner_.FetchExactTransactionFromCRDT( tx_hash ) );
            if ( crdt_transaction.has_value() )
            {
                tx = crdt_transaction.value();
            }
        }

        if ( !tx )
        {
            // CONFLICT-01 / NONCE-01: Standalone validator without local transaction state.
            // Fall back only to the certificate's exact embedded proposal.
            auto nonce_subject_result = ConsensusManager::DecodeNonceSubject( certificate.proposal().subject() );
            if ( nonce_subject_result.has_error() )
            {
                logger_->warn( "{}: Certificate for hash {} has no decodable NonceSubject, accepting",
                               __func__,
                               tx_hash );
                // METRICS-01: Certificate fallback deserialization failure
                metrics_cert_fallback_failure_.fetch_add( 1, std::memory_order_relaxed );
                return ConsensusManager::Check::Approve;
            }
            const auto &nonce_subject = nonce_subject_result.value();

            if ( nonce_subject.transaction().transaction_case() == EmbeddedTransaction::TRANSACTION_NOT_SET )
            {
                logger_->warn( "{}: Certificate for hash {} has no embedded transaction "
                               "(pre-Phase-1 certificate), accepting",
                               __func__,
                               tx_hash );
                return ConsensusManager::Check::Approve;
            }

            auto tx_result = TransactionManager::DeSerializeEmbeddedTransaction( nonce_subject.transaction() );
            if ( tx_result.has_error() )
            {
                logger_->warn( "{}: Failed to deserialize tx from certificate for hash {}, accepting certificate",
                               __func__,
                               tx_hash );
                metrics_cert_fallback_failure_.fetch_add( 1, std::memory_order_relaxed );
                return ConsensusManager::Check::Approve;
            }
            tx = tx_result.value();

            // Verify hash binding — deserialized tx must match certificate's tx_hash
            if ( tx->GetHash() != tx_hash || !tx->CheckHash() )
            {
                logger_->warn( "{}: Certificate-embedded tx hash mismatch for {}, "
                               "accepting certificate without processing embedded data",
                               __func__,
                               tx_hash );
                metrics_cert_fallback_failure_.fetch_add( 1, std::memory_order_relaxed );
                return ConsensusManager::Check::Approve;
            }

            if ( !TransactionManager::CertificateMatchesTransaction( certificate, *tx ) )
            {
                logger_->warn( "{}: Certificate does not bind to embedded transaction {}, accepting without "
                               "processing",
                               __func__,
                               tx_hash );
                metrics_cert_fallback_failure_.fetch_add( 1, std::memory_order_relaxed );
                return ConsensusManager::Check::Approve;
            }

            auto result = owner_.ChangeTransactionState( tx, TransactionStatus::CONFIRMED );
            if ( result.has_error() )
            {
                logger_->error( "{}: Failed to confirm certificate-deserialized tx for hash {}: {}",
                                __func__,
                                tx_hash,
                                result.error().message() );
                metrics_cert_fallback_failure_.fetch_add( 1, std::memory_order_relaxed );
                return outcome::failure( result.error() );
            }

            // METRICS-01: Certificate fallback deserialization and confirmation succeeded
            metrics_cert_fallback_success_.fetch_add( 1, std::memory_order_relaxed );

            logger_->info( "{}: Standalone validator confirmed tx {} from certificate proposal_id={}",
                           __func__,
                           tx_hash,
                           certificate.proposal_id() );
        }
        else
        {
            if ( !TransactionManager::CertificateMatchesTransaction( certificate, *tx ) )
            {
                logger_->warn( "{}: Certificate does not bind to transaction {}, accepting without confirmation",
                               __func__,
                               tx_hash );
                return ConsensusManager::Check::Approve;
            }

            // TRACK-01: Confirm via ChangeTransactionState lifecycle (promote temp embedded-tx entry)
            {
                auto result = owner_.ChangeTransactionState( tx, TransactionStatus::CONFIRMED );
                if ( result.has_error() )
                {
                    logger_->error( "{}: Failed to change transaction state to CONFIRMED for hash {}: {}",
                                    __func__,
                                    tx_hash,
                                    result.error().message() );
                    return outcome::failure( result.error() );
                }
            }
            logger_->debug( "{}: Transaction {} confirmed by consensus", __func__, tx_hash );

            logger_->debug( "{}: Checking for conflicting transaction with {}", __func__, tx_hash );

            auto conflicting_tx = owner_.GetConflictingTransaction( *tx );

            if ( conflicting_tx.has_value() )
            {
                logger_->warn( "{}: Found conflicting transaction: {}", __func__, conflicting_tx.value()->GetHash() );
                std::unique_lock tx_lock( owner_.tx_mutex_m );
                auto             it = owner_.tx_processed_m.find(
                    TransactionManager::GetTransactionPath( conflicting_tx.value()->GetHash() ) );
                if ( it == owner_.tx_processed_m.end() )
                {
                    // The conflicting entry may live under a different network's key
                    // namespace; resolve by value scan before dereferencing.
                    it = std::find_if( owner_.tx_processed_m.begin(),
                                       owner_.tx_processed_m.end(),
                                       [&conflicting_tx]( const auto &kv ) {
                                           return kv.second.tx &&
                                                  kv.second.tx->GetHash() == conflicting_tx.value()->GetHash();
                                       } );
                }
                if ( it == owner_.tx_processed_m.end() )
                {
                    // Nothing locally tracked to arbitrate against; the incoming
                    // transaction was already confirmed above.
                    tx_lock.unlock();
                    return ConsensusManager::Check::Approve;
                }

                if ( it->second.status == TransactionStatus::CONFIRMED )
                {
                    logger_->error( "{}: Conflicting transaction {} is CONFIRMED as well as incoming {}, not sure "
                                    "what to do",
                                    __func__,
                                    conflicting_tx.value()->GetHash(),
                                    tx_hash );
                    tx_lock.unlock();
                    // The incoming transaction carries a validated quorum certificate
                    // (this handler only runs after ValidateCertificate and
                    // CertificateMatchesTransaction); the conflict outranks it only if
                    // it is final by the same standard. CheckTransactionValidity
                    // promotes locally tracked transactions to CONFIRMED on signature
                    // validity alone — letting that promotion win the BestHash
                    // tie-break reverted a certified winner's already-applied effects
                    // and DeleteTransaction'd it from the CRDT while every peer
                    // confirmed it: permanent divergence on exactly this node.
                    auto conflict_certificate = owner_.GetTransactionCertificate( *conflicting_tx.value() );
                    const bool conflict_is_certified =
                        conflict_certificate.has_value() &&
                        TransactionManager::CertificateMatchesTransaction( conflict_certificate.value(),
                                                                           *conflicting_tx.value() );
                    if ( !conflict_is_certified || owner_.ShouldReplaceTransaction( *conflicting_tx.value(), *tx ) )
                    {
                        auto result = owner_.ChangeTransactionState( conflicting_tx.value(),
                                                                     TransactionStatus::FAILED );
                        if ( result.has_error() )
                        {
                            logger_->error( "{}: Failed to change conflicting transaction state to FAILED for "
                                            "current tx {}: {}",
                                            __func__,
                                            conflicting_tx.value()->GetHash(),
                                            result.error().message() );
                        }
                    }
                    else
                    {
                        auto result = owner_.ChangeTransactionState( tx, TransactionStatus::FAILED );
                        if ( result.has_error() )
                        {
                            logger_->error( "{}: Failed to change transaction state to FAILED for new tx {}: {}",
                                            __func__,
                                            tx_hash,
                                            result.error().message() );
                            return outcome::failure( result.error() );
                        }
                        return outcome::failure( result.error() );
                    }
                }
                else
                {
                    logger_->warn( "{}: Setting conflicting transaction {} to FAILED since the new one {} is "
                                   "confirmed:",
                                   __func__,
                                   conflicting_tx.value()->GetHash(),
                                   tx_hash );
                    tx_lock.unlock();
                    auto result = owner_.ChangeTransactionState( conflicting_tx.value(), TransactionStatus::FAILED );
                    if ( result.has_error() )
                    {
                        logger_->error( "{}: Failed to change transaction state to FAILED for hash {}: {}",
                                        __func__,
                                        tx_hash,
                                        result.error().message() );
                    }
                }
            }
        }

        auto tx_hash_bin = base::Hash256::fromReadableString( tx_hash );
        if ( tx_hash_bin.has_error() )
        {
            logger_->error( "{}: Could not parse tx hash for checkpoint tx={}", __func__, tx_hash );
            return outcome::failure( tx_hash_bin.error() );
        }

        auto validator_registry = owner_.blockchain_->GetValidatorRegistry();
        if ( !validator_registry )
        {
            logger_->error( "{}: No validator registry, skipping checkpoint", __func__ );
            return outcome::failure( std::errc::no_such_device );
        }

        const uint64_t registry_epoch = validator_registry->GetRegistryEpoch();
        const auto     registry_cid   = validator_registry->GetRegistryCid();
        auto           registry_hash  = crypto::sha2_256( registry_cid.data(), registry_cid.size() );

        if ( auto checkpoint_res = owner_.account_m->GetUTXOManager().CreateCheckpoint( registry_epoch,
                                                                                        tx_hash_bin.value(),
                                                                                        registry_hash );
             checkpoint_res.has_error() )
        {
            logger_->error( "{}: Failed to create UTXO checkpoint tx={} epoch={} err={}",
                             __func__,
                             tx_hash,
                             registry_epoch,
                             checkpoint_res.error().message() );
        }
        logger_->debug( "{}: Transaction approved: {:.8}", __func__, tx_hash );
        return ConsensusManager::Check::Approve;
    }

    outcome::result<ConsensusManager::ValidationResult> TransactionConsensusHandler::HandleNonceConsensusSubject(
        const ConsensusManager::Subject &subject )
    {
        constexpr std::string_view FUNC{__func__};
        auto nonce_subject = ConsensusManager::DecodeNonceSubject( subject );
        if ( nonce_subject.has_error() )
        {
            logger_->error( "{}: Received unexpected subject payload", __func__ );
            return nonce_subject.error();
        }

        const std::string tx_hash = nonce_subject.value().tx_hash();

        // DESER-01: Deserialize from EmbeddedTransaction oneof field
        if ( nonce_subject.value().transaction().transaction_case() == EmbeddedTransaction::TRANSACTION_NOT_SET )
        {
            logger_->error( "{}: No embedded transaction set, rejecting", __func__ );
            return ConsensusManager::ValidationResult::Reject();
        }

        auto tx_result = TransactionManager::DeSerializeEmbeddedTransaction( nonce_subject.value().transaction() );
        if ( tx_result.has_error() )
        {
            logger_->error( "{}: Failed to deserialize embedded tx for hash {}", __func__, tx_hash );
            return ConsensusManager::ValidationResult::Reject();
        }
        auto tx = tx_result.value();

        // Hash binding verification — cryptographic integrity gate (defense-in-depth)
        if ( tx->GetHash() != tx_hash )
        {
            logger_->error( "{}: Hash binding mismatch, tx->GetHash() != subject.tx_hash for {}", __func__, tx_hash );
            return ConsensusManager::ValidationResult::Reject();
        }

        // BIND-01: Commitment-tx binding cross-check
        if ( nonce_subject.value().has_utxo_commitment() )
        {
            if ( !tx->HasUTXOParameters() )
            {
                logger_->error( "{}: Subject has UTXO commitment but deserialized tx lacks "
                                 "UTXO parameters — possible malicious embedding, rejecting tx={}",
                                 __func__,
                                 tx_hash );
                return ConsensusManager::ValidationResult::Reject();
            }

            auto reconstructed = BuildUTXOTransitionCommitment( *tx );
            if ( !reconstructed.has_value() ||
                 reconstructed->consumed_outpoints_root() !=
                     nonce_subject.value().utxo_commitment().consumed_outpoints_root() ||
                 reconstructed->produced_outputs_root() !=
                     nonce_subject.value().utxo_commitment().produced_outputs_root() )
            {
                logger_->error( "{}: Commitment-tx binding mismatch — "
                                 "reconstructed commitment differs from subject claim for tx={}",
                                 __func__,
                                 tx_hash );
                return ConsensusManager::ValidationResult::Reject();
            }
        }

        // TRACK-01: Insert temporary tracking entry via ChangeTransactionState lifecycle
        uint64_t tracked_nonce  = tx->GetNonce();
        auto     tracked_entry  = owner_.GetTrackedTxByHash( tx_hash );
        if ( !tracked_entry )
        {
            // Go through the state machine so the manager owns every tracking-map write.
            auto create_result = owner_.ChangeTransactionState( tx, TransactionStatus::CREATED );
            if ( create_result.has_error() )
            {
                logger_->warn( "{}: CREATE failed for embedded tx {}, entry may exist via race: {}",
                                __func__,
                                tx_hash,
                                create_result.error().message() );
                // Re-read in case another thread inserted it
                auto raced_entry = owner_.GetTrackedTxByHash( tx_hash );
                if ( raced_entry )
                {
                    if ( raced_entry->status == TransactionStatus::FAILED )
                    {
                        return ConsensusManager::ValidationResult::Reject();
                    }
                    tracked_nonce  = raced_entry->cached_nonce;
                }
            }
            else
            {
                owner_.ChangeTransactionState( tx, TransactionStatus::VERIFYING );
            }
        }
        else if ( tracked_entry->status == TransactionStatus::FAILED )
        {
            logger_->debug( "{}: Transaction {} previously FAILED, rejecting", __func__, tx_hash );
            return ConsensusManager::ValidationResult::Reject();
        }
        else
        {
            // Entry already exists with higher-status — use its values for downstream checks
            tracked_nonce  = tracked_entry->cached_nonce;
        }

        auto reject_and_maybe_fail_local = [&]( const char *reason ) -> ConsensusManager::ValidationResult
        {
            // METRICS-01: Validation reject counter with reason logged at info level
            metrics_validation_reject_.fetch_add( 1, std::memory_order_relaxed );
            logger_->info( "{}: Proposal rejected for hash {}: {}", FUNC, tx_hash, reason );

            logger_->error( "{}: Rejecting nonce subject for hash {}: {}", FUNC, tx_hash, reason );

            // Ensure local outgoing invalid transactions don't stay in VERIFYING forever.
            if ( tx->GetSrcAddress() == owner_.account_m->GetAddress() )
            {
                auto current_out_status = owner_.GetOutgoingStatusByTxId( tx->GetHash() );
                if ( current_out_status != TransactionStatus::FAILED &&
                     current_out_status != TransactionStatus::CONFIRMED )
                {
                    if ( auto fail_result = owner_.ChangeTransactionState( tx, TransactionStatus::FAILED );
                         fail_result.has_error() )
                    {
                        logger_->error( "{}: Failed to mark rejected local tx as FAILED for hash {}: {}",
                                         FUNC,
                                         tx_hash,
                                         fail_result.error().message() );
                    }
                }
            }
            else
            {
                // TRACK-01 per D-02: Mark remote embedded temp entry as FAILED via ChangeTransactionState
                {
                    const auto remote_entry = owner_.GetTrackedTxByHash( tx_hash );
                    if ( remote_entry && remote_entry->status == TransactionStatus::VERIFYING )
                    {
                        owner_.ChangeTransactionState( tx, TransactionStatus::FAILED );
                        logger_->debug( "{}: Marked rejected embedded tx as FAILED for {}", FUNC, tx_hash );
                    }
                }
            }

            return ConsensusManager::ValidationResult::Reject();
        };

        if ( tracked_nonce != nonce_subject.value().nonce() )
        {
            logger_->error( "{}: Nonce mismatch for hash {}", __func__, tx_hash );
            return reject_and_maybe_fail_local( "nonce mismatch" );
        }

        if ( !subject.account_id().empty() && tx->GetSrcAddress() != subject.account_id() )
        {
            logger_->error( "{}: Account mismatch for hash {}", __func__, tx_hash );
            return reject_and_maybe_fail_local( "account mismatch" );
        }

        if ( owner_.HasConfirmedInputConflict( *tx ) )
        {
            logger_->error( "{}: Outpoint conflict against finalized transaction for hash {}", __func__, tx_hash );
            return reject_and_maybe_fail_local( "input outpoint already finalized by another transaction" );
        }

        const auto witness_validation = ValidateWitnessForConsensus( subject, *tx );
        if ( witness_validation == WitnessValidationResult::INVALID )
        {
            logger_->error( "{}: Witness validation failed for hash {}", __func__, tx_hash );
            return reject_and_maybe_fail_local( "witness validation failed" );
        }

        if ( auto migration_tx = std::dynamic_pointer_cast<MigrationTransaction>( tx ) )
        {
            MigrationAllowList allow_list( owner_.globaldb_m->GetDataStore(), migration_tx->GetFromVersion() );
            auto eligibility_result = allow_list.IsEligible( migration_tx->GetSrcAddress(), migration_tx->GetAmount() );
            if ( eligibility_result.has_error() )
            {
                logger_->warn( "{}: Failed to evaluate local migration allowlist tx={} src={} err={}, pending",
                                __func__,
                                tx_hash,
                                migration_tx->GetSrcAddress(),
                                eligibility_result.error().message() );
                return ConsensusManager::ValidationResult::Pending();
            }
            if ( !eligibility_result.value() )
            {
                return reject_and_maybe_fail_local( "migration source address not locally eligible" );
            }
        }

        auto validate_result = ValidateTransactionForConsensus( *tx );

        if ( validate_result.check == ConsensusManager::Check::Pending )
        {
            return validate_result;
        }
        if ( validate_result.check != ConsensusManager::Check::Approve )
        {
            return reject_and_maybe_fail_local( "transaction validation failed" );
        }

        // METRICS-01: Validation approve counter
        metrics_validation_approve_.fetch_add( 1, std::memory_order_relaxed );
        return ConsensusManager::ValidationResult::Approve();
    }

    ConsensusManager::ValidationResult TransactionConsensusHandler::ValidateTransactionForConsensus(
        const GeniusTransaction &tx ) const
    {
        logger_->debug( "{}: Validating transaction", __func__ );
        if ( !CheckTransactionWellFormed( tx ) )
        {
            logger_->error( "{}: Well-formed check failed tx={}", __func__, tx.GetHash() );
            return ConsensusManager::ValidationResult::Reject();
        }
        if ( !owner_.CheckTransactionAuthorization( tx ) )
        {
            logger_->error( "{}: Authorization check failed tx={}", __func__, tx.GetHash() );
            return ConsensusManager::ValidationResult::Reject();
        }
        if ( !CheckTransactionTimestamp( tx ) )
        {
            logger_->error( "{}: Timestamp check failed tx={}", __func__, tx.GetHash() );
            return ConsensusManager::ValidationResult::Reject();
        }
        if ( !owner_.CheckParentChildAuthority( tx ) )
        {
            logger_->error( "{}: Parent-child authority check failed tx={}", __func__, tx.GetHash() );
            return ConsensusManager::ValidationResult::Reject();
        }
        auto replay_result = EvaluateTransactionReplayProtection( tx );
        if ( replay_result.validation.check != ConsensusManager::Check::Approve )
        {
            logger_->error( "{}: Replay protection failed tx={}", __func__, tx.GetHash() );
            return replay_result.validation;
        }
        //TODO - Deal with checking the Mint
        if ( !CheckTransactionTypeRules( tx ) )
        {
            logger_->error( "{}: Type rules failed tx={}", __func__, tx.GetHash() );
            return ConsensusManager::ValidationResult::Reject();
        }

        logger_->debug( "{}: Transaction valid tx={}", __func__, tx.GetHash() );
        return ConsensusManager::ValidationResult::Approve();
    }

    bool TransactionConsensusHandler::CheckTransactionWellFormed( const GeniusTransaction &tx ) const
    {
        logger_->debug( "{}: Checking well-formed tx={}", __func__, tx.GetHash() );
        if ( tx.GetHash().empty() || !tx.CheckHash() )
        {
            logger_->error( "{}: Hash invalid tx={}", __func__, tx.GetHash() );
            return false;
        }

        if ( tx.GetSrcAddress().empty() )
        {
            logger_->error( "{}: Empty source address tx={}", __func__, tx.GetHash() );
            return false;
        }

        if ( tx.GetTimestamp() == 0 )
        {
            logger_->error( "{}: Missing timestamp tx={}", __func__, tx.GetHash() );
            return false;
        }

        if ( TransactionManager::transaction_parsers.find( tx.GetType() ) ==
             TransactionManager::transaction_parsers.end() )
        {
            logger_->error( "{}: Unknown tx type {}", __func__, tx.GetType() );
            return false;
        }

        logger_->debug( "{}: Well-formed ok tx={}", __func__, tx.GetHash() );
        return true;
    }

    bool TransactionConsensusHandler::CheckTransactionTimestamp( const GeniusTransaction &tx ) const
    {
        logger_->debug( "{}: Checking timestamp tx={}", __func__, tx.GetHash() );
        const auto ts = tx.GetTimestamp();
        if ( ts == 0 )
        {
            logger_->error( "{}: Missing timestamp tx={}", __func__, tx.GetHash() );
            return false;
        }

        const auto elapsed      = owner_.GetElapsedTime( ts );
        const auto tolerance_ms = static_cast<int64_t>( owner_.timestamp_tolerance_m.count() );
        const auto drift_ms     = elapsed >= 0 ? elapsed : -elapsed;

        if ( tolerance_ms > 0 && drift_ms > tolerance_ms )
        {
            logger_->error( "{}: Timestamp out of tolerance tx={} (elapsed: {} ms, tolerance: {} ms)",
                             __func__,
                             tx.GetHash(),
                             elapsed,
                             tolerance_ms );
            return false;
        }

        logger_->debug( "{}: Timestamp ok tx={}", __func__, tx.GetHash() );
        return true;
    }

    TransactionConsensusHandler::ReplayProtectionResult TransactionConsensusHandler::
        EvaluateTransactionReplayProtection( const GeniusTransaction &tx ) const
    {
        logger_->debug( "{}: Checking replay protection tx={}", __func__, tx.GetHash() );

        if ( tx.GetNonce() > 0 )
        {
            const auto previous_hash = tx.GetPreviousHash();
            if ( previous_hash.empty() )
            {
                logger_->error( "{}: Missing previous hash tx={}", __func__, tx.GetHash() );
                return { ConsensusManager::ValidationResult::Reject() };
            }
            if ( tx.GetSrcAddress() == owner_.account_m->GetAddress() )
            {
                const auto expected_previous_hash = owner_.GetOutgoingPreviousHash( tx.GetNonce() );
                if ( !expected_previous_hash.empty() && previous_hash != expected_previous_hash )
                {
                    logger_->error( "{}: Previous hash does not match local account head tx={}",
                                     __func__,
                                     tx.GetHash() );
                    return { ConsensusManager::ValidationResult::Reject() };
                }
            }
            auto previous_transaction_result = TransactionManager::FetchTransaction(
                *owner_.globaldb_m, TransactionManager::GetTransactionPath( previous_hash ) );
            if ( previous_transaction_result.has_error() || !previous_transaction_result.value() ||
                 previous_transaction_result.value()->GetHash() != previous_hash )
            {
                // Registration transactions persist at reg/{src_addr}, not tx/{hash}
                // (SendTransactionItem routes them there), so any nonce chain that
                // passes through a registration (a re-registration, or an ordinary
                // transfer/mint after RegisterChild) cannot resolve its predecessor
                // through the tx/ namespace. The stored reg/ record is the child's
                // current chain head — but a revoke rewrites it locally (detach_flag)
                // under a fresh hash, so the stored record may no longer match the
                // ORIGINAL registration the sender chains to. The immutable
                // certificate at the predecessor's slot still embeds the original
                // transaction: resolve through it.
                const std::string reg_key = TransactionManager::GetBlockChainBase() + "reg/" + tx.GetSrcAddress();
                auto              reg_data = owner_.globaldb_m->Get( reg_key );
                if ( reg_data.has_value() )
                {
                    auto stored_reg = TransactionManager::DeSerializeTransaction( reg_data.value() );
                    if ( !stored_reg.has_error() && stored_reg.value() &&
                         stored_reg.value()->GetHash() == previous_hash )
                    {
                        previous_transaction_result = stored_reg;
                    }
                }
                if ( previous_transaction_result.has_error() || !previous_transaction_result.value() ||
                     previous_transaction_result.value()->GetHash() != previous_hash )
                {
                    const std::string prev_slot = tx.GetSrcAddress() + ":" + std::to_string( tx.GetNonce() - 1 );
                    auto              prev_cert = owner_.blockchain_->GetCertificateBySlot( prev_slot );
                    if ( prev_cert.has_value() )
                    {
                        auto prev_subject = ConsensusManager::DecodeNonceSubject(
                            prev_cert.value().proposal().subject() );
                        if ( !prev_subject.has_error() &&
                             prev_subject.value().tx_hash() == previous_hash &&
                             prev_cert.value().proposal().subject().account_id() == tx.GetSrcAddress() )
                        {
                            auto embedded = TransactionManager::DeSerializeEmbeddedTransaction(
                                prev_subject.value().transaction() );
                            if ( embedded.has_value() && embedded.value() &&
                                 embedded.value()->GetHash() == previous_hash )
                            {
                                previous_transaction_result = embedded;
                            }
                        }
                    }
                }
            }
            if ( previous_transaction_result.has_error() || !previous_transaction_result.value() ||
                 previous_transaction_result.value()->GetHash() != previous_hash )
            {
                // Invalid is not missing: when this node already holds the finalized
                // predecessor of the sender's chain for this nonce and it is not the
                // claimed previous hash, the reference can never resolve — reject
                // instead of parking the proposal behind a dependency that will never
                // fire (develop's local-account-head check). A node that does not know
                // the sender's head yet still pends so a late CRDT sync can satisfy it.
                if ( tx.GetSrcAddress() == owner_.account_m->GetAddress() )
                {
                    auto expected_previous = owner_.account_m->GetLocalConfirmedTxHash( tx.GetNonce() - 1 );
                    if ( expected_previous.has_value() && expected_previous.value() != previous_hash )
                    {
                        logger_->error( "{}: Previous hash mismatch tx={} claimed={} expected={}",
                                        __func__,
                                        tx.GetHash(),
                                        previous_hash.substr( 0, 8 ),
                                        expected_previous.value().substr( 0, 8 ) );
                        return { ConsensusManager::ValidationResult::Reject() };
                    }
                }
                logger_->error( "{}: Missing previous transaction for hash {}", __func__, previous_hash );
                return { ConsensusManager::ValidationResult::Pending(
                    { ConsensusManager::PendingDependencyKey::Certificate( previous_hash ) } ) };
            }

            auto previous_cert_result = owner_.blockchain_->GetCertificateBySlot(
                previous_transaction_result.value()->GetSlotID() );
            if ( previous_cert_result.has_error() ||
                 !TransactionManager::CertificateMatchesTransaction( previous_cert_result.value(),
                                                                     *previous_transaction_result.value() ) )
            {
                logger_->error( "{}: Missing previous certificate for hash {}", __func__, previous_hash );
                return { ConsensusManager::ValidationResult::Pending(
                    { ConsensusManager::PendingDependencyKey::Certificate( previous_hash ) } ) };
            }
            const auto &previous_subject = previous_cert_result.value().proposal().subject();
            auto        previous_nonce   = ConsensusManager::DecodeNonceSubject( previous_subject );
            if ( previous_nonce.has_error() )
            {
                return { ConsensusManager::ValidationResult::Reject() };
            }
            if ( previous_subject.account_id() != tx.GetSrcAddress() )
            {
                return { ConsensusManager::ValidationResult::Reject() };
            }
            if ( ( previous_nonce.value().nonce() + 1 ) != tx.GetNonce() )
            {
                return { ConsensusManager::ValidationResult::Reject() };
            }
        }

        auto nonce_result = owner_.account_m->GetPeerNonce( tx.GetSrcAddress() );
        if ( nonce_result.has_error() )
        {
            logger_->debug( "{}: No confirmed nonce for address {}", __func__, tx.GetSrcAddress() );
            return { ConsensusManager::ValidationResult::Approve() };
        }

        const auto confirmed_nonce = nonce_result.value();
        const auto tx_nonce        = tx.GetNonce();

        if ( tx_nonce <= confirmed_nonce )
        {
            logger_->error( "{}: Nonce too low tx={} nonce={} confirmed={}",
                             __func__,
                             tx.GetHash(),
                             tx_nonce,
                             confirmed_nonce );
            return { ConsensusManager::ValidationResult::Reject() };
        }

        if ( tx_nonce > confirmed_nonce + NONCE_WINDOW )
        {
            logger_->error( "{}: Nonce too high tx={} nonce={} confirmed={} window={}",
                             __func__,
                             tx.GetHash(),
                             tx_nonce,
                             confirmed_nonce,
                             NONCE_WINDOW );
            return { ConsensusManager::ValidationResult::Reject() };
        }

        if ( tx_nonce > confirmed_nonce + 1 )
        {
            for ( uint64_t n = confirmed_nonce + 1; n < tx_nonce; ++n )
            {
                auto tracked = owner_.GetTrackedTxByNonceAndAddress( n, tx.GetSrcAddress() );
                if ( !tracked.has_value() )
                {
                    logger_->error( "{}: Missing intermediate nonce {} for address {}",
                                     __func__,
                                     n,
                                     tx.GetSrcAddress() );
                    return { ConsensusManager::ValidationResult::Reject() };
                }
                if ( tracked->status == TransactionStatus::FAILED )
                {
                    logger_->error( "{}: Intermediate nonce {} invalid for address {}",
                                     __func__,
                                     n,
                                     tx.GetSrcAddress() );
                    return { ConsensusManager::ValidationResult::Reject() };
                }
            }
        }
        logger_->debug( "{}: Replay protection ok tx={}", __func__, tx.GetHash() );
        return { ConsensusManager::ValidationResult::Approve() };
    }

    bool TransactionConsensusHandler::CheckTransactionTypeRules( const GeniusTransaction &tx ) const
    {
        logger_->debug( "{}: Checking type rules", __func__ );
        if ( tx.HasUTXOParameters() )
        {
            auto params_opt = tx.GetUTXOParametersOpt();
            if ( !params_opt.has_value() )
            {
                logger_->error( "{}: Missing UTXO parameters for tx={}", __func__, tx.GetHash() );
                return false;
            }
            const auto &[_, validator] = owner_.SelectInputValidator( tx );
            return validator.ValidateUTXOParameters( params_opt.value(),
                                                     tx.GetSrcAddress(),
                                                     owner_.account_m->GetUTXOManager() );
        }

        return true;
    }

    TransactionConsensusHandler::WitnessValidationResult TransactionConsensusHandler::ValidateWitnessForConsensus(
        const ConsensusSubject  &subject,
        const GeniusTransaction &tx ) const
    {
        auto nonce_subject = ConsensusManager::DecodeNonceSubject( subject );
        logger_->debug( "{}: Start tx={} src={} nonce={} subject_nonce={} has_nonce={} "
                         "has_utxo_params={} has_commitment={} has_witness={}",
                         __func__,
                         tx.GetHash(),
                         tx.GetSrcAddress(),
                         tx.GetNonce(),
                         nonce_subject.has_value() ? nonce_subject.value().nonce() : 0,
                         nonce_subject.has_value(),
                         tx.HasUTXOParameters(),
                         nonce_subject.has_value() && nonce_subject.value().has_utxo_commitment(),
                         nonce_subject.has_value() && nonce_subject.value().has_utxo_witness() );

        if ( nonce_subject.has_error() )
        {
            logger_->debug( "{}: Subject has no nonce payload, accepting tx={}", __func__, tx.GetHash() );
            return WitnessValidationResult::VALID;
        }

        const auto [chain_id, validator] = owner_.SelectInputValidator( tx );

        if ( !tx.HasUTXOParameters() )
        {
            // BIND-01: Hardened early-return — if subject claims UTXO commitment
            // but tx lacks UTXO params, this is Pitfall 5 bypass → reject as INVALID
            if ( nonce_subject.has_value() && nonce_subject.value().has_utxo_commitment() )
            {
                logger_->error( "{}: Subject has UTXO commitment "
                                 "but tx has no UTXO params — rejecting tx={}",
                                 __func__,
                                 tx.GetHash() );
                return WitnessValidationResult::INVALID;
            }
            logger_->debug( "{}: Tx has no UTXO params, accepting tx={}", __func__, tx.GetHash() );
            return WitnessValidationResult::VALID;
        }

        if ( !nonce_subject.value().has_utxo_commitment() )
        {
            logger_->error( "{}: Missing UTXO commitment tx={}", __func__, tx.GetHash() );
            return WitnessValidationResult::INVALID;
        }

        const auto &commitment = nonce_subject.value().utxo_commitment();
        if ( commitment.consumed_outpoints_root().size() != base::Hash256::size() ||
             commitment.produced_outputs_root().size() != base::Hash256::size() )
        {
            logger_->error( "{}: Invalid commitment root sizes tx={} consumed_size={} "
                             "produced_size={} expected={}",
                             __func__,
                             tx.GetHash(),
                             commitment.consumed_outpoints_root().size(),
                             commitment.produced_outputs_root().size(),
                             base::Hash256::size() );
            return WitnessValidationResult::INVALID;
        }
        if ( validator.RequiresConsensusUTXOData() && !nonce_subject.value().has_utxo_witness() )
        {
            logger_->error( "{}: Missing required UTXO witness tx={} chain_id={} validator_requires_witness={}",
                             __func__,
                             tx.GetHash(),
                             chain_id,
                             validator.RequiresConsensusUTXOData() );
            return WitnessValidationResult::INVALID;
        }

        auto params_opt = tx.GetUTXOParametersOpt();
        if ( !params_opt.has_value() )
        {
            logger_->error( "{}: Missing UTXO params payload tx={}", __func__, tx.GetHash() );
            return WitnessValidationResult::INVALID;
        }
        const auto witness_verdict = validator.ValidateWitness( subject, tx, params_opt.value(), *owner_.blockchain_ );
        logger_->debug( "{}: Validator witness result tx={} chain_id={} result={}",
                         __func__,
                         tx.GetHash(),
                         chain_id,
                         static_cast<int>( witness_verdict ) );
        switch ( witness_verdict )
        {
        case IInputValidator::WitnessVerdict::kValid:
            return WitnessValidationResult::VALID;
        case IInputValidator::WitnessVerdict::kNotSynced:
            // Cross-delta arrival order is unordered: a producer's transaction or
            // certificate legitimately arrives after the spending subject. Retry,
            // do not reject — rejecting here turned a transient gap into a
            // validation failure for certificate-first delivery.
            return WitnessValidationResult::PENDING;
        case IInputValidator::WitnessVerdict::kInvalid:
        default:
            return WitnessValidationResult::INVALID;
        }
    }

    std::optional<UTXOTransitionCommitment> TransactionConsensusHandler::BuildUTXOTransitionCommitment(
        const GeniusTransaction &tx ) const
    {
        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;
        }
        UTXOTransitionCommitment          commitment;
        std::vector<std::vector<uint8_t>> consumed_payloads;
        consumed_payloads.reserve( inputs.size() );
        for ( const auto &input : inputs )
        {
            auto *committed_input = commitment.add_consumed_outpoints();
            committed_input->set_tx_id_hash( input.txid_hash_.data(), input.txid_hash_.size() );
            committed_input->set_output_index( input.output_idx_ );

            std::vector<uint8_t> leaf_payload;
            leaf_payload.reserve( HASH256_BYTES + SERIALIZED_UINT32_BYTES );
            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 ) );

        const auto produced_outputs = tx.GetProducedUTXOs();
        if ( !produced_outputs )
        {
            logger_->warn( "{}: Could not extract produced outputs for tx={}", __func__, tx.GetHash() );
            return std::nullopt;
        }
        for ( const auto &produced_output : *produced_outputs )
        {
            const auto produced_tx_hash = produced_output.GetTxID();
            auto      *committed_output = commitment.add_produced_outputs();
            committed_output->set_tx_id_hash( produced_tx_hash.data(), produced_tx_hash.size() );
            committed_output->set_output_index( produced_output.GetOutputIdx() );
            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() );
        }

        const auto produced_outputs_root = utxo_merkle::ComputeMerkleRootFromUTXOs( *produced_outputs );
        commitment.set_consumed_outpoints_root( consumed_outpoints_root.data(), consumed_outpoints_root.size() );
        commitment.set_produced_outputs_root( produced_outputs_root.data(), produced_outputs_root.size() );
        return commitment;
    }

    std::optional<UTXOWitness> TransactionConsensusHandler::BuildUTXOWitness( const GeniusTransaction &tx ) const
    {
        if ( !tx.HasUTXOParameters() )
        {
            logger_->error( "{}: No UTXO parameters for transaction {}", __func__, tx.GetHash() );
            return std::nullopt;
        }

        auto params_opt = tx.GetUTXOParametersOpt();
        if ( !params_opt.has_value() )
        {
            logger_->error( "{}: Unexpected missing UTXO parameters for transaction {}", __func__, tx.GetHash() );
            return std::nullopt;
        }
        const auto &inputs = params_opt->first;

        struct SnapshotLeaf
        {
            std::string          outpoint_key;
            std::vector<uint8_t> payload;
        };

        std::vector<SnapshotLeaf> leaves;
        leaves.reserve( inputs.size() );
        for ( const auto &input : inputs )
        {
            auto utxo = owner_.account_m->GetUTXOManager().GetUnconsumedUTXO( input.txid_hash_, input.output_idx_ );
            if ( !utxo.has_value() )
            {
                logger_->error( "{}: Missing input UTXO for transaction {} and key {}",
                                 __func__,
                                 tx.GetHash(),
                                 OutPointKey( input.txid_hash_, input.output_idx_ ) );
                return std::nullopt;
            }
            leaves.push_back(
                { OutPointKey( utxo->GetTxID(), utxo->GetOutputIdx() ), SerializeUTXOLeafPayload( utxo.value() ) } );
        }

        std::sort( leaves.begin(),
                   leaves.end(),
                   []( const SnapshotLeaf &a, const SnapshotLeaf &b ) { return a.payload < b.payload; } );

        std::unordered_map<std::string, size_t> outpoint_to_index;
        outpoint_to_index.reserve( leaves.size() );
        std::vector<base::Hash256> 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() )
            {
                logger_->error( "{}: Missing outpoint for transaction {} and key {}", __func__, tx.GetHash(), key );
                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() );

            utxo_merkle::AppendMerkleBranch( level_hashes,
                                             leaf_index,
                                             [proof]( const base::Hash256 &sibling, bool is_left_sibling )
                                             {
                                                 auto *step = proof->add_branch();
                                                 step->set_sibling_hash( sibling.data(), sibling.size() );
                                                 step->set_is_left_sibling( is_left_sibling );
                                             } );

            auto producer_tx = owner_.GetTransactionByHash( input.txid_hash_.toReadableString() );
            if ( !producer_tx )
            {
                logger_->error( "{}: Missing producer transaction for input {}",
                                 __func__,
                                 input.txid_hash_.toReadableString() );
                return std::nullopt;
            }
            const auto produced_outputs = producer_tx->GetProducedUTXOs();
            if ( !produced_outputs )
            {
                logger_->error( "{}: Could not extract produced outputs for producer transaction {}",
                                 __func__,
                                 producer_tx->GetHash() );
                return std::nullopt;
            }

            std::vector<SnapshotLeaf> 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<std::string, size_t> produced_outpoint_to_index;
            produced_outpoint_to_index.reserve( produced_leaves.size() );
            std::vector<base::Hash256> 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() )
            {
                logger_->error( "{}: Missing produced UTXO for transaction {} and key {}",
                                 __func__,
                                 tx.GetHash(),
                                 key );
                return std::nullopt;
            }
            if ( produced_leaves[produced_it->second].payload != leaves[leaf_index].payload )
            {
                logger_->error( "{}: Payload mismatch for produced UTXO for transaction {} and key {}",
                                 __func__,
                                 tx.GetHash(),
                                 key );
                return std::nullopt;
            }

            utxo_merkle::AppendMerkleBranch( produced_level_hashes,
                                             produced_it->second,
                                             [proof]( const base::Hash256 &sibling, bool is_left_sibling )
                                             {
                                                 auto *step = proof->add_produced_branch();
                                                 step->set_sibling_hash( sibling.data(), sibling.size() );
                                                 step->set_is_left_sibling( is_left_sibling );
                                             } );
        }

        return witness;
    }

}

Updated on 2026-09-29 at 16:45:11 +0000