securecrdt/SecureCrdtCandidate.cpp¶
Namespaces¶
| Name |
|---|
| sgns |
| sgns::securecrdt |
Source code¶
#include "securecrdt/SecureCrdtCandidate.hpp"
#include <algorithm>
#include <charconv>
#include <limits>
#include <gsl/span>
#include "base/hexutil.hpp"
#include "trustedpeer/CanonicalTrustCodec.hpp"
namespace sgns::securecrdt
{
namespace
{
constexpr size_t HASH_BYTES = 32;
constexpr size_t MAX_DOMAIN_BYTES = 128;
constexpr size_t MAX_SIGNATURE_BYTES = CandidateLimits::MAX_CANDIDATE_BYTES;
constexpr size_t MAX_APPROVAL_WIRE_BYTES = CandidateLimits::MAX_CANDIDATE_BYTES * 2 + 512;
bool IsLowerHex( const std::string &value, size_t length )
{
return value.size() == length &&
std::all_of( value.begin(),
value.end(),
[]( char byte )
{ return ( byte >= '0' && byte <= '9' ) || ( byte >= 'a' && byte <= 'f' ); } );
}
bool IsCanonicalDomain( const std::string &domain )
{
return !domain.empty() && domain.size() <= MAX_DOMAIN_BYTES &&
std::all_of( domain.begin(),
domain.end(),
[]( char byte )
{
return ( byte >= 'a' && byte <= 'z' ) || ( byte >= '0' && byte <= '9' ) ||
byte == '-' || byte == '_' || byte == '.';
} );
}
bool IsKnownKind( CandidateKind kind )
{
switch ( kind )
{
case CandidateKind::TrustPolicy:
case CandidateKind::BurnConfig:
case CandidateKind::TrustedPeerGenesis:
return true;
}
return false;
}
std::optional<std::vector<uint8_t>> DecodeHex( const std::string &hex, size_t expected_bytes )
{
if ( !IsLowerHex( hex, expected_bytes * 2 ) )
{
return std::nullopt;
}
auto decoded = sgns::base::unhex( hex );
if ( decoded.has_error() || decoded.value().size() != expected_bytes )
{
return std::nullopt;
}
return decoded.value();
}
std::string EncodeBytes( const std::vector<uint8_t> &bytes )
{
return sgns::base::hex_lower( gsl::span<const uint8_t>( bytes.data(), bytes.size() ) );
}
} // namespace
bool CandidateLimits::CandidateCountAllowed( size_t count )
{
return count <= MAX_ACTIVE_CANDIDATES_PER_PREDECESSOR;
}
bool CandidateLimits::ApprovalCountAllowed( size_t count )
{
return count <= MAX_APPROVALS_PER_CANDIDATE;
}
bool CandidateLimits::ApprovalBytesAllowed( size_t current_bytes, size_t additional_bytes )
{
return current_bytes <= MAX_ACTIVE_APPROVAL_BYTES_PER_PREDECESSOR &&
additional_bytes <= MAX_ACTIVE_APPROVAL_BYTES_PER_PREDECESSOR - current_bytes;
}
std::optional<std::vector<uint8_t>> CandidateCore::CanonicalBytes() const
{
if ( encoding_version != ENCODING_VERSION || !IsCanonicalDomain( domain ) || !IsKnownKind( kind ) ||
version == 0 || !IsLowerHex( expected_previous_hash, HASH_BYTES * 2 ) ||
!IsLowerHex( authorizing_policy_hash, HASH_BYTES * 2 ) )
{
return std::nullopt;
}
const auto previous_hash = DecodeHex( expected_previous_hash, HASH_BYTES );
const auto policy_hash = DecodeHex( authorizing_policy_hash, HASH_BYTES );
if ( !previous_hash || !policy_hash )
{
return std::nullopt;
}
sgns::trustedpeer::CanonicalTrustCodec::Writer writer;
writer.WriteBytes( RECORD_DOMAIN );
writer.WriteU8( encoding_version );
if ( !writer.WriteLengthPrefixedBytes(
gsl::span<const uint8_t>( reinterpret_cast<const uint8_t *>( domain.data() ), // NOLINT
domain.size() ) ) )
{
return std::nullopt;
}
writer.WriteU16( network_id );
writer.WriteU8( static_cast<uint8_t>( kind ) );
writer.WriteU64( version );
if ( !writer.WriteLengthPrefixedBytes( *previous_hash ) || !writer.WriteLengthPrefixedBytes( *policy_hash ) ||
!writer.WriteLengthPrefixedBytes( payload ) )
{
return std::nullopt;
}
auto bytes = writer.Take();
if ( bytes.size() > CandidateLimits::MAX_CANDIDATE_BYTES )
{
return std::nullopt;
}
return bytes;
}
std::optional<std::string> CandidateCore::Hash() const
{
const auto bytes = CanonicalBytes();
if ( !bytes )
{
return std::nullopt;
}
return sgns::trustedpeer::CanonicalTrustCodec::Sha256Hex(
gsl::span<const uint8_t>( bytes->data(), bytes->size() ) );
}
std::optional<CandidateCore> CandidateCore::DecodeCanonical( const std::vector<uint8_t> &bytes )
{
if ( bytes.size() > CandidateLimits::MAX_CANDIDATE_BYTES )
{
return std::nullopt;
}
sgns::trustedpeer::CanonicalTrustCodec::Reader reader( gsl::span<const uint8_t>( bytes.data(), bytes.size() ) );
const auto record_domain = reader.ReadBytes( RECORD_DOMAIN.size() );
const auto encoding = reader.ReadU8();
const auto domain_bytes = reader.ReadLengthPrefixedBytes( MAX_DOMAIN_BYTES );
const auto network = reader.ReadU16();
const auto kind_value = reader.ReadU8();
const auto version_value = reader.ReadU64();
const auto previous_hash = reader.ReadLengthPrefixedBytes( HASH_BYTES );
const auto policy_hash = reader.ReadLengthPrefixedBytes( HASH_BYTES );
const auto payload_bytes = reader.ReadLengthPrefixedBytes( CandidateLimits::MAX_CANDIDATE_BYTES );
if ( !record_domain || !std::equal( record_domain->begin(), record_domain->end(), RECORD_DOMAIN.begin() ) ||
!encoding || *encoding != ENCODING_VERSION || !domain_bytes || !network || !kind_value || !version_value ||
*version_value == 0 || !previous_hash || previous_hash->size() != HASH_BYTES || !policy_hash ||
policy_hash->size() != HASH_BYTES || !payload_bytes || !reader.Exhausted() )
{
return std::nullopt;
}
CandidateCore core;
core.encoding_version = *encoding;
core.domain.assign( domain_bytes->begin(), domain_bytes->end() );
core.network_id = *network;
core.kind = static_cast<CandidateKind>( *kind_value );
core.version = *version_value;
core.expected_previous_hash = EncodeBytes( *previous_hash );
core.authorizing_policy_hash = EncodeBytes( *policy_hash );
core.payload = std::move( *payload_bytes );
const auto canonical = core.CanonicalBytes();
if ( !canonical || *canonical != bytes )
{
return std::nullopt;
}
return core;
}
bool CandidateCore::operator==( const CandidateCore &other ) const
{
return encoding_version == other.encoding_version && domain == other.domain && network_id == other.network_id &&
kind == other.kind && version == other.version &&
expected_previous_hash == other.expected_previous_hash &&
authorizing_policy_hash == other.authorizing_policy_hash && payload == other.payload;
}
std::optional<CandidateId> CandidateId::FromCore( const CandidateCore &core )
{
const auto hash = core.Hash();
if ( !hash )
{
return std::nullopt;
}
return CandidateId{ core.domain, core.version, *hash };
}
bool CandidateId::operator==( const CandidateId &other ) const
{
return domain == other.domain && version == other.version && content_hash == other.content_hash;
}
sgns::crdt::HierarchicalKey CandidateKey::ToHierarchicalKey() const
{
if ( !IsCanonicalDomain( id.domain ) || id.version == 0 || !IsLowerHex( id.content_hash, HASH_BYTES * 2 ) ||
!IsLowerHex( signer, sgns::trustedpeer::CanonicalTrustCodec::PUBLIC_KEY_BYTES * 2 ) )
{
return {};
}
return sgns::crdt::HierarchicalKey( id.domain )
.ChildString( "candidate" )
.ChildString( "v" + std::to_string( id.version ) )
.ChildString( id.content_hash )
.ChildString( "approval" )
.ChildString( signer );
}
std::optional<CandidateKey> CandidateKey::Parse( const sgns::crdt::HierarchicalKey &key )
{
const auto segments = key.GetList();
if ( segments.size() != 6 || !IsCanonicalDomain( segments[0] ) || segments[1] != "candidate" ||
segments[2].size() < 2 || segments[2][0] != 'v' || segments[4] != "approval" ||
!IsLowerHex( segments[3], HASH_BYTES * 2 ) ||
!IsLowerHex( segments[5], sgns::trustedpeer::CanonicalTrustCodec::PUBLIC_KEY_BYTES * 2 ) )
{
return std::nullopt;
}
uint64_t version_value = 0;
const auto version_text = std::string_view( segments[2] ).substr( 1 );
const auto parsed = std::from_chars( version_text.data(),
version_text.data() + version_text.size(),
version_value );
if ( parsed.ec != std::errc() || parsed.ptr != version_text.data() + version_text.size() ||
version_value == 0 || std::to_string( version_value ) != version_text )
{
return std::nullopt;
}
return CandidateKey{ CandidateId{ segments[0], version_value, segments[3] }, segments[5] };
}
std::optional<std::vector<uint8_t>> CandidateApprovalRecord::CanonicalBytes() const
{
if ( encoding_version != ENCODING_VERSION ||
!IsLowerHex( signer, sgns::trustedpeer::CanonicalTrustCodec::PUBLIC_KEY_BYTES * 2 ) || signature.empty() ||
signature.size() > MAX_SIGNATURE_BYTES )
{
return std::nullopt;
}
const auto core_bytes = core.CanonicalBytes();
const auto signer_bytes = DecodeHex( signer, sgns::trustedpeer::CanonicalTrustCodec::PUBLIC_KEY_BYTES );
if ( !core_bytes || !signer_bytes )
{
return std::nullopt;
}
sgns::trustedpeer::CanonicalTrustCodec::Writer writer;
writer.WriteBytes( RECORD_DOMAIN );
writer.WriteU8( encoding_version );
if ( !writer.WriteLengthPrefixedBytes( *core_bytes ) || !writer.WriteLengthPrefixedBytes( *signer_bytes ) ||
!writer.WriteLengthPrefixedBytes( signature ) )
{
return std::nullopt;
}
return writer.Take();
}
std::optional<CandidateApprovalRecord> CandidateApprovalRecord::DecodeCanonical( const std::vector<uint8_t> &bytes,
const CandidateKey &key )
{
if ( bytes.size() > MAX_APPROVAL_WIRE_BYTES )
{
return std::nullopt;
}
sgns::trustedpeer::CanonicalTrustCodec::Reader reader( gsl::span<const uint8_t>( bytes.data(), bytes.size() ) );
const auto record_domain = reader.ReadBytes( RECORD_DOMAIN.size() );
const auto encoding = reader.ReadU8();
const auto core_bytes = reader.ReadLengthPrefixedBytes( CandidateLimits::MAX_CANDIDATE_BYTES );
const auto signer_bytes = reader.ReadLengthPrefixedBytes(
sgns::trustedpeer::CanonicalTrustCodec::PUBLIC_KEY_BYTES );
const auto signature_bytes = reader.ReadLengthPrefixedBytes( MAX_SIGNATURE_BYTES );
if ( !record_domain || !std::equal( record_domain->begin(), record_domain->end(), RECORD_DOMAIN.begin() ) ||
!encoding || *encoding != ENCODING_VERSION || !core_bytes || !signer_bytes ||
signer_bytes->size() != sgns::trustedpeer::CanonicalTrustCodec::PUBLIC_KEY_BYTES || !signature_bytes ||
signature_bytes->empty() || !reader.Exhausted() )
{
return std::nullopt;
}
const auto core = CandidateCore::DecodeCanonical( *core_bytes );
if ( !core )
{
return std::nullopt;
}
CandidateApprovalRecord record;
record.encoding_version = *encoding;
record.core = *core;
record.signer = EncodeBytes( *signer_bytes );
record.signature = std::move( *signature_bytes );
const auto id = CandidateId::FromCore( record.core );
const auto canonical = record.CanonicalBytes();
if ( !id || !( *id == key.id ) || record.signer != key.signer || !canonical || *canonical != bytes )
{
return std::nullopt;
}
return record;
}
} // namespace sgns::securecrdt
Updated on 2026-09-25 at 15:46:12 +0000