account/GeniusInputValidator.cpp¶
Input validation strategy for native Genius-chain transactions. More...
Namespaces¶
| Name |
|---|
| sgns |
Functions¶
| Name | |
|---|---|
| base::Logger | InputValidatorLogger() |
| base::Hash256 | HashLeaf(const std::vector< uint8_t > & payload) Hashes a serialized UTXO leaf payload with the leaf domain separator. |
| base::Hash256 | HashNode(const base::Hash256 & left, const base::Hash256 & right) Hashes two child nodes with the internal-node domain separator. |
| std::string | OutPointKey(const base::Hash256 & txid, uint32_t idx) Generates a canonical key for a UTXO outpoint, used for deterministic ordering in Merkle tree construction. |
| void | AppendUInt32BE(std::vector< uint8_t > & out, uint32_t value) Appends a 32-bit unsigned integer in big-endian order. |
| void | AppendUInt64BE(std::vector< uint8_t > & out, uint64_t value) Appends a 64-bit unsigned integer in big-endian order. |
| uint32_t | ReadUInt32BE(const uint8_t * data) Reads a 32-bit unsigned integer from big-endian bytes. |
| uint64_t | ReadUInt64BE(const uint8_t * data) Reads a 64-bit unsigned integer from big-endian bytes. |
Detailed Description¶
Input validation strategy for native Genius-chain transactions.
Date: 2026-06-02
Functions Documentation¶
function InputValidatorLogger¶
function HashLeaf¶
Hashes a serialized UTXO leaf payload with the leaf domain separator.
Parameters:
- payload The payload to hash
Return: The hash of the payload as a leaf node in the Merkle tree
function HashNode¶
Hashes two child nodes with the internal-node domain separator.
Parameters:
- left The hash of the left child node
- right The hash of the right child node
Return: The hash of the parent node
function OutPointKey¶
Generates a canonical key for a UTXO outpoint, used for deterministic ordering in Merkle tree construction.
Parameters:
- txid The transaction hash that created the UTXO
- idx The output index of the UTXO within the transaction
Return: Canonical string key in the format "txid:idx" where txid is the readable hex representation of the transaction hash
function AppendUInt32BE¶
Appends a 32-bit unsigned integer in big-endian order.
Parameters:
- out The vector to append to
- value the value to append
function AppendUInt64BE¶
Appends a 64-bit unsigned integer in big-endian order.
Parameters:
- out The vector to append to
- value the value to append
function ReadUInt32BE¶
Reads a 32-bit unsigned integer from big-endian bytes.
Parameters:
- data A pointer to the byte array
Return: the 32 bit unsigned integer represented by the bytes
function ReadUInt64BE¶
Reads a 64-bit unsigned integer from big-endian bytes.
Parameters:
- data A pointer to the byte array
Return: the 64 bit unsigned integer represented by the bytes
Source code¶
#include "account/GeniusInputValidator.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "account/GeniusAccount.hpp"
#include "account/GeniusTransaction.hpp"
#include "account/UTXOManager.hpp"
#include "account/UTXOMerkle.hpp"
#include "blockchain/Blockchain.hpp"
#include "blockchain/Consensus.hpp"
#include "blockchain/impl/proto/Consensus.pb.h"
#include "base/logger.hpp"
namespace sgns
{
namespace
{
using utxo_merkle::HashLeaf;
using utxo_merkle::HashNode;
using utxo_merkle::OutPointKey;
using utxo_merkle::AppendUInt32BE;
using utxo_merkle::AppendUInt64BE;
using utxo_merkle::ReadUInt32BE;
using utxo_merkle::ReadUInt64BE;
using namespace input_validator_constants;
std::vector<uint8_t> SerializeOutpointLeafPayload( const base::Hash256 &txid_hash, uint32_t output_index )
{
std::vector<uint8_t> payload;
payload.reserve( HASH256_BYTES + SERIALIZED_UINT32_BYTES );
payload.insert( payload.end(), txid_hash.begin(), txid_hash.end() );
AppendUInt32BE( payload, output_index );
return payload;
}
std::vector<uint8_t> SerializeOutputLeafPayload( const base::Hash256 &txid_hash,
uint32_t output_index,
const std::string &owner_address,
gsl::span<const uint8_t> token_bytes,
uint64_t amount )
{
std::vector<uint8_t> payload;
payload.reserve( HASH256_BYTES + SERIALIZED_UINT32_BYTES + SERIALIZED_UINT32_BYTES + owner_address.size() +
token_bytes.size() + SERIALIZED_UINT64_BYTES );
payload.insert( payload.end(), txid_hash.begin(), txid_hash.end() );
AppendUInt32BE( payload, output_index );
AppendUInt32BE( payload, static_cast<uint32_t>( 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<std::vector<uint8_t>> payloads )
{
if ( payloads.empty() )
{
return utxo_merkle::EmptyUTXOMerkleRoot();
}
std::sort( payloads.begin(), payloads.end() );
std::vector<base::Hash256> 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 ) );
}
std::string PreviewValue( const std::string &value, size_t max_length = 12 )
{
return value.substr( 0, std::min( value.size(), max_length ) );
}
} // namespace
base::Logger InputValidatorLogger()
{
static const auto logger = base::createLogger( "InputValidator" );
return logger;
}
bool GeniusInputValidator::ValidateUTXOParameters( const UTXOTxParameters ¶ms,
const std::string &address,
const UTXOManager &utxo_manager ) const
{
auto logger = InputValidatorLogger();
logger->trace( "ValidateUTXOParameters: address={} inputs={} outputs={}",
PreviewValue( address ), params.first.size(), params.second.size() );
if ( params.first.empty() || params.second.empty() )
{
logger->debug( "ValidateUTXOParameters rejected empty UTXO set: address={} inputs={} outputs={}",
PreviewValue( address ), params.first.size(), params.second.size() );
return false;
}
const bool valid = utxo_manager.VerifyParameters( params, address );
if ( valid )
{
logger->info( "ValidateUTXOParameters accepted address={} inputs={} outputs={}",
PreviewValue( address ), params.first.size(), params.second.size() );
}
else
{
logger->debug( "ValidateUTXOParameters failed UTXOManager verification for address={}",
PreviewValue( address ) );
}
return valid;
}
bool GeniusInputValidator::ValidateWitness( const ConsensusSubject &subject,
const std::shared_ptr<GeniusTransaction> &tx,
const UTXOTxParameters ¶ms,
const std::shared_ptr<Blockchain> &blockchain ) const
{
auto logger = InputValidatorLogger();
logger->trace( "ValidateWitness(Genius): tx={} inputs={} outputs={}",
tx ? PreviewValue( tx->GetHash() ) : "<null>", params.first.size(), params.second.size() );
if ( !tx || !blockchain )
{
logger->error( "ValidateWitness(Genius) missing dependency: tx_present={} blockchain_present={}",
tx != nullptr, blockchain != nullptr );
return false;
}
auto nonce_subject = ConsensusManager::DecodeNonceSubject( subject );
if ( nonce_subject.has_error() ||
!nonce_subject.value().has_utxo_witness() ||
!nonce_subject.value().has_utxo_commitment() )
{
logger->error( "ValidateWitness(Genius) invalid nonce subject for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
const auto &inputs = params.first;
const auto &outputs = params.second;
if ( inputs.empty() || outputs.empty() )
{
logger->debug( "ValidateWitness(Genius) rejected empty params for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
const auto tx_hash_result = base::Hash256::fromReadableString( tx->GetHash() );
if ( tx_hash_result.has_error() )
{
logger->error( "ValidateWitness(Genius) invalid tx hash encoding: tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
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( "ValidateWitness(Genius) invalid commitment root sizes for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
auto consumed_root_result = base::Hash256::fromSpan(
gsl::span( reinterpret_cast<uint8_t *>( const_cast<char *>( commitment.consumed_outpoints_root().data() ) ),
commitment.consumed_outpoints_root().size() ) );
if ( consumed_root_result.has_error() )
{
logger->error( "ValidateWitness(Genius) failed to decode consumed root for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
auto produced_root_result = base::Hash256::fromSpan(
gsl::span( reinterpret_cast<uint8_t *>( const_cast<char *>( commitment.produced_outputs_root().data() ) ),
commitment.produced_outputs_root().size() ) );
if ( produced_root_result.has_error() )
{
logger->error( "ValidateWitness(Genius) failed to decode produced root for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
if ( commitment.consumed_outpoints_size() != static_cast<int>( inputs.size() ) ||
commitment.produced_outputs_size() != static_cast<int>( outputs.size() ) )
{
logger->debug( "ValidateWitness(Genius) commitment size mismatch for tx={}: committed_inputs={} tx_inputs={} committed_outputs={} tx_outputs={}",
PreviewValue( tx->GetHash() ),
commitment.consumed_outpoints_size(),
inputs.size(),
commitment.produced_outputs_size(),
outputs.size() );
return false;
}
std::unordered_set<std::string> commitment_outpoints;
commitment_outpoints.reserve( commitment.consumed_outpoints_size() );
std::vector<std::vector<uint8_t>> 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<uint8_t *>( const_cast<char *>( committed_outpoint.tx_id_hash().data() ) ),
committed_outpoint.tx_id_hash().size() ) );
if ( out_hash_result.has_error() )
{
logger->error( "ValidateWitness(Genius) failed to decode committed consumed outpoint hash for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
if ( !commitment_outpoints.emplace( OutPointKey( out_hash_result.value(), committed_outpoint.output_index() ) ).second )
{
logger->debug( "ValidateWitness(Genius) duplicate committed consumed outpoint for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
committed_consumed_payloads.push_back(
SerializeOutpointLeafPayload( out_hash_result.value(), committed_outpoint.output_index() ) );
}
std::vector<std::vector<uint8_t>> 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() )
{
logger->debug( "ValidateWitness(Genius) consumed root mismatch for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
std::unordered_set<std::string> commitment_outputs;
commitment_outputs.reserve( commitment.produced_outputs_size() );
std::vector<std::vector<uint8_t>> 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<uint8_t *>( const_cast<char *>( committed_output.tx_id_hash().data() ) ),
committed_output.tx_id_hash().size() ) );
if ( out_hash_result.has_error() )
{
logger->error( "ValidateWitness(Genius) failed to decode committed produced output hash for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
auto payload = SerializeOutputLeafPayload(
out_hash_result.value(),
committed_output.output_index(),
committed_output.owner_address(),
gsl::span<const uint8_t>( reinterpret_cast<const uint8_t *>( committed_output.token_id().data() ),
committed_output.token_id().size() ),
committed_output.amount() );
const std::string payload_key( reinterpret_cast<const char *>( payload.data() ), payload.size() );
if ( !commitment_outputs.emplace( payload_key ).second )
{
logger->debug( "ValidateWitness(Genius) duplicate committed produced output for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
committed_produced_payloads.push_back( std::move( payload ) );
}
std::unordered_set<std::string> tx_outputs;
tx_outputs.reserve( outputs.size() );
std::vector<std::vector<uint8_t>> 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<uint32_t>( i ),
output.dest_address,
gsl::span<const uint8_t>( token_bytes.data(), token_bytes.size() ),
output.encrypted_amount );
tx_outputs.emplace( reinterpret_cast<const char *>( 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() )
{
logger->debug( "ValidateWitness(Genius) produced output mismatch for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
std::unordered_map<std::string, const ConsumedInputProof *> proofs;
proofs.reserve( nonce_subject.value().utxo_witness().consumed_inputs_size() );
for ( const auto &proof : nonce_subject.value().utxo_witness().consumed_inputs() )
{
auto hash_result = base::Hash256::fromSpan(
gsl::span( reinterpret_cast<uint8_t *>( const_cast<char *>( proof.tx_id_hash().data() ) ),
proof.tx_id_hash().size() ) );
if ( hash_result.has_error() )
{
logger->error( "ValidateWitness(Genius) failed to decode consumed proof hash for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
if ( !proofs.emplace( OutPointKey( hash_result.value(), proof.output_index() ), &proof ).second )
{
logger->debug( "ValidateWitness(Genius) duplicate consumed proof entry for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
}
const auto add_amount = []( std::unordered_map<std::string, uint64_t> &bucket,
const std::string &token_key,
uint64_t amount ) -> bool
{
auto &total = bucket[token_key];
if ( amount > std::numeric_limits<uint64_t>::max() - total )
{
return false;
}
total += amount;
return true;
};
std::unordered_set<std::string> seen_inputs;
std::unordered_map<std::string, uint64_t> input_amounts_by_token;
std::unordered_map<std::string, uint64_t> 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<const char *>( input.signature_.data() ),
input.signature_.size() ),
input.SerializeForSigning() ) )
{
logger->debug( "ValidateWitness(Genius) signature verification failed for tx={} input_index={}",
PreviewValue( tx->GetHash() ), input.output_idx_ );
return false;
}
auto proof_it = proofs.find( OutPointKey( input.txid_hash_, input.output_idx_ ) );
if ( proof_it == proofs.end() )
{
logger->debug( "ValidateWitness(Genius) missing consumed proof for tx={} input_index={}",
PreviewValue( tx->GetHash() ), input.output_idx_ );
return false;
}
const auto outpoint_key = OutPointKey( input.txid_hash_, input.output_idx_ );
if ( !seen_inputs.insert( outpoint_key ).second )
{
logger->debug( "ValidateWitness(Genius) duplicate input detected for tx={} input_index={}",
PreviewValue( tx->GetHash() ), input.output_idx_ );
return false;
}
const auto &proof = *proof_it->second;
const auto &payload = proof.leaf_payload();
if ( payload.size() <
OWNER_ADDRESS_OFFSET + TOKEN_ID_BYTES_IN_PAYLOAD + AMOUNT_BYTES_IN_PAYLOAD )
{
logger->debug( "ValidateWitness(Genius) proof payload too short for tx={} input_index={}",
PreviewValue( tx->GetHash() ), input.output_idx_ );
return false;
}
auto payload_hash_result = base::Hash256::fromSpan(
gsl::span( reinterpret_cast<uint8_t *>( const_cast<char *>( payload.data() ) ), HASH256_BYTES ) );
if ( payload_hash_result.has_error() || payload_hash_result.value() != input.txid_hash_ )
{
logger->debug( "ValidateWitness(Genius) proof payload tx hash mismatch for tx={} input_index={}",
PreviewValue( tx->GetHash() ), input.output_idx_ );
return false;
}
const auto payload_output_idx =
ReadUInt32BE( reinterpret_cast<const uint8_t *>( payload.data() ) + OUTPUT_INDEX_OFFSET );
if ( payload_output_idx != input.output_idx_ )
{
logger->debug( "ValidateWitness(Genius) proof payload output index mismatch for tx={} input_index={}",
PreviewValue( tx->GetHash() ), input.output_idx_ );
return false;
}
const auto owner_len =
ReadUInt32BE( reinterpret_cast<const uint8_t *>( payload.data() ) + OWNER_ADDRESS_LENGTH_OFFSET );
if ( payload.size() <
OWNER_ADDRESS_OFFSET + owner_len + TOKEN_ID_BYTES_IN_PAYLOAD + AMOUNT_BYTES_IN_PAYLOAD )
{
logger->debug( "ValidateWitness(Genius) proof payload owner length overflow for tx={} input_index={}",
PreviewValue( tx->GetHash() ), input.output_idx_ );
return false;
}
const std::string payload_owner( payload.data() + OWNER_ADDRESS_OFFSET,
payload.data() + OWNER_ADDRESS_OFFSET + owner_len );
const bool delegated_escrow_spend =
payload_owner != tx->GetSrcAddress() && tx->GetType() == TRANSFER_TX_TYPE &&
input.output_idx_ == ESCROW_LOCK_OUTPUT_INDEX &&
utxo_address::IsEscrowLockAddress( payload_owner ) && tx->GetUncleHash() == payload_owner;
if ( payload_owner != tx->GetSrcAddress() && !delegated_escrow_spend )
{
logger->debug( "ValidateWitness(Genius) owner mismatch for tx={} owner={} src={}",
PreviewValue( tx->GetHash() ),
PreviewValue( payload_owner ),
PreviewValue( tx->GetSrcAddress() ) );
return false;
}
const size_t token_offset = OWNER_ADDRESS_OFFSET + owner_len;
const size_t amount_offset = token_offset + TOKEN_ID_BYTES_IN_PAYLOAD;
const std::string token_key( payload.data() + token_offset, payload.data() + amount_offset );
const uint64_t input_amount = ReadUInt64BE( reinterpret_cast<const uint8_t *>( payload.data() ) +
amount_offset );
if ( !add_amount( input_amounts_by_token, token_key, input_amount ) )
{
logger->error( "ValidateWitness(Genius) input amount overflow for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
std::vector<uint8_t> payload_vec( payload.begin(), payload.end() );
auto producer_cert_result = blockchain->GetCertificateBySubjectHash( input.txid_hash_.toReadableString() );
if ( producer_cert_result.has_error() )
{
logger->error( "ValidateWitness(Genius) missing producer certificate for input tx={}",
PreviewValue( input.txid_hash_.toReadableString() ) );
return false;
}
const auto &producer_subject = producer_cert_result.value().proposal().subject();
auto producer_nonce = ConsensusManager::DecodeNonceSubject( producer_subject );
if ( producer_nonce.has_error() || !producer_nonce.value().has_utxo_commitment() )
{
logger->error( "ValidateWitness(Genius) invalid producer nonce subject for input tx={}",
PreviewValue( input.txid_hash_.toReadableString() ) );
return false;
}
const auto &producer_commitment = producer_nonce.value().utxo_commitment();
if ( producer_commitment.produced_outputs_root().size() != base::Hash256::size() )
{
logger->error( "ValidateWitness(Genius) invalid producer output root size for input tx={}",
PreviewValue( input.txid_hash_.toReadableString() ) );
return false;
}
auto produced_root_result = base::Hash256::fromSpan( gsl::span(
reinterpret_cast<uint8_t *>( const_cast<char *>( producer_commitment.produced_outputs_root().data() ) ),
producer_commitment.produced_outputs_root().size() ) );
if ( produced_root_result.has_error() )
{
logger->error( "ValidateWitness(Genius) failed to decode producer output root for input tx={}",
PreviewValue( input.txid_hash_.toReadableString() ) );
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<uint8_t *>( const_cast<char *>( step.sibling_hash().data() ) ),
step.sibling_hash().size() ) );
if ( sibling_hash_result.has_error() )
{
logger->error( "ValidateWitness(Genius) failed to decode proof branch sibling for input tx={}",
PreviewValue( input.txid_hash_.toReadableString() ) );
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() )
{
logger->debug( "ValidateWitness(Genius) produced branch root mismatch for input tx={}",
PreviewValue( input.txid_hash_.toReadableString() ) );
return false;
}
}
for ( const auto &output : outputs )
{
const auto &token_bytes = output.token_id.bytes();
const std::string token_key( reinterpret_cast<const char *>( token_bytes.data() ), token_bytes.size() );
if ( !add_amount( output_amounts_by_token, token_key, output.encrypted_amount ) )
{
logger->error( "ValidateWitness(Genius) output amount overflow for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
}
if ( input_amounts_by_token != output_amounts_by_token )
{
logger->debug( "ValidateWitness(Genius) token balance mismatch for tx={}",
PreviewValue( tx->GetHash() ) );
return false;
}
logger->info( "ValidateWitness(Genius) succeeded for tx={}", PreviewValue( tx->GetHash() ) );
return true;
}
} // namespace sgns
Updated on 2026-06-05 at 17:22:19 -0700