trustedpeer/TrustStateStore.cpp¶
Namespaces¶
| Name |
|---|
| sgns |
| sgns::trustedpeer |
Functions¶
| Name | |
|---|---|
| OUTCOME_CPP_DEFINE_CATEGORY_3(sgns::trustedpeer , TrustStateStore::Error , e ) | |
| std::string | BurnGenesisAnchorHash(const std::string & genesis_fingerprint) |
Functions Documentation¶
function OUTCOME_CPP_DEFINE_CATEGORY_3¶
function BurnGenesisAnchorHash¶
Domain-separated predecessor used by the deterministic burn v1 candidate.
Source code¶
#include "trustedpeer/TrustStateStore.hpp"
#include "securecrdt/SecureCrdtCandidate.hpp"
#include <algorithm>
#include <limits>
#include <map>
#include <gsl/span>
#include "base/hexutil.hpp"
#include "multisig/MultiSig.hpp"
#include "storage/rocksdb/rocksdb.hpp"
#include "storage/rocksdb/rocksdb_batch.hpp"
#include "trustedpeer/CanonicalTrustCodec.hpp"
OUTCOME_CPP_DEFINE_CATEGORY_3( sgns::trustedpeer, TrustStateStore::Error, e )
{
using Error = sgns::trustedpeer::TrustStateStore::Error;
switch ( e )
{
case Error::NOT_FOUND:
return "confirmed trust state not found";
case Error::ALREADY_INITIALIZED:
return "trust state is already initialized";
case Error::NETWORK_MISMATCH:
return "persisted trust state belongs to another network";
case Error::CORRUPT_GENESIS:
return "persisted genesis record is corrupt";
case Error::CORRUPT_FINGERPRINT:
return "persisted genesis fingerprint is corrupt";
case Error::INVALID_GENESIS_PROOF:
return "persisted genesis proof is invalid";
case Error::MISSING_POLICY_RECORD:
return "policy head references a missing record";
case Error::MISSING_BURN_RECORD:
return "burn head references a missing record";
case Error::CORRUPT_POLICY_RECORD:
return "persisted policy record is corrupt";
case Error::CORRUPT_BURN_RECORD:
return "persisted burn record is corrupt";
case Error::INVALID_POLICY_PROOF:
return "persisted policy proof is invalid";
case Error::INVALID_BURN_PROOF:
return "persisted burn proof is invalid";
case Error::VERSION_DECREASE:
return "candidate version does not advance the durable version";
case Error::VERSION_SKIP:
return "candidate version skips the next durable version";
case Error::WRONG_PREDECESSOR:
return "candidate does not descend from the durable head";
case Error::WRONG_AUTHORIZER:
return "candidate is not authorized by the durable policy head";
case Error::INITIAL_BURN_NOT_CONFIRMED:
return "initial burn state does not yet have peer-quorum authorization";
case Error::STALE_HEAD:
return "candidate lost the durable-head transition race";
case Error::COMMIT_FAILED:
return "synchronous trust-state batch commit failed";
}
return "unknown TrustStateStore::Error";
}
namespace sgns::trustedpeer
{
namespace
{
constexpr std::string_view BURN_DOMAIN = "SGNS_BURN_STATE_V1";
constexpr std::string_view BURN_GENESIS_ANCHOR_DOMAIN = "SGNS_BURN_GENESIS_ANCHOR_V1";
constexpr std::string_view GENESIS_RECORD = "SGNS_TRUST_GENESIS_RECORD_V1";
constexpr std::string_view POLICY_RECORD = "SGNS_TRUST_POLICY_RECORD_V1";
constexpr std::string_view BURN_RECORD = "SGNS_TRUST_BURN_RECORD_V1";
constexpr size_t HASH_HEX_LENGTH = 64;
constexpr size_t MAX_SIGNATURE_LEN = 256;
bool IsHash( std::string_view value )
{
return value.size() == HASH_HEX_LENGTH && sgns::base::IsLowerHex( value );
}
base::Buffer Buffer( std::string_view value )
{
return base::Buffer{}.put( value );
}
base::Buffer Buffer( const std::vector<uint8_t> &value )
{
return base::Buffer( value );
}
std::string Prefix( uint16_t network_id )
{
return "trust/version-1/network/" + std::to_string( network_id ) + "/";
}
std::string GenesisKey( uint16_t network_id )
{
return Prefix( network_id ) + "genesis";
}
std::string PolicyHeadKey( uint16_t network_id )
{
return Prefix( network_id ) + "policy/head";
}
std::string BurnHeadKey( uint16_t network_id )
{
return Prefix( network_id ) + "burn/head";
}
std::string PolicyRecordKey( uint16_t network_id, uint64_t version, const std::string &hash )
{
return Prefix( network_id ) + "policy/version-" + std::to_string( version ) + "/" + hash;
}
std::string BurnRecordKey( uint16_t network_id, uint64_t version, const std::string &hash )
{
return Prefix( network_id ) + "burn/version-" + std::to_string( version ) + "/" + hash;
}
std::vector<uint8_t> EncodeHead( uint64_t version, std::string_view hash )
{
CanonicalTrustCodec::Writer writer;
writer.WriteU64( version );
writer.WriteBytes( hash );
return writer.Take();
}
std::optional<std::pair<uint64_t, std::string>> DecodeHead( const base::Buffer &bytes )
{
CanonicalTrustCodec::Reader reader( bytes );
auto version = reader.ReadU64();
auto hash = reader.ReadBytes( HASH_HEX_LENGTH );
if ( !version || *version == 0 || !hash || !reader.Exhausted() )
{
return std::nullopt;
}
std::string hash_string( hash->begin(), hash->end() );
if ( !IsHash( hash_string ) )
{
return std::nullopt;
}
return std::make_pair( *version, std::move( hash_string ) );
}
void EncodeProof( CanonicalTrustCodec::Writer &writer, const multisig::CollectedSignatures &proof )
{
writer.WriteU32( static_cast<uint32_t>( proof.size() ) );
for ( const auto &[signer, signature] : proof )
{
(void) writer.WriteLengthPrefixedBytes(
gsl::span<const uint8_t>( reinterpret_cast<const uint8_t *>( signer.data() ), signer.size() ) );
(void) writer.WriteLengthPrefixedBytes( signature );
}
}
std::optional<multisig::CollectedSignatures> DecodeProof( CanonicalTrustCodec::Reader &reader )
{
auto count = reader.ReadU32();
if ( !count || *count == 0 || *count > CanonicalTrustCodec::MAX_TRUSTED_PEERS )
{
return std::nullopt;
}
multisig::CollectedSignatures proof;
proof.reserve( *count );
for ( uint32_t index = 0; index < *count; ++index )
{
auto signer = reader.ReadLengthPrefixedBytes( CanonicalTrustCodec::PUBLIC_KEY_BYTES * 2 );
auto signature = reader.ReadLengthPrefixedBytes( MAX_SIGNATURE_LEN );
if ( !signer || signer->size() != CanonicalTrustCodec::PUBLIC_KEY_BYTES * 2 || !signature ||
signature->empty() )
{
return std::nullopt;
}
proof.emplace_back( std::string( signer->begin(), signer->end() ), std::move( *signature ) );
}
return proof;
}
std::vector<uint8_t> EncodeGenesisRecord( const std::vector<uint8_t> &bytes,
std::string_view fingerprint,
const std::vector<uint8_t> &signature )
{
CanonicalTrustCodec::Writer writer;
writer.WriteBytes( GENESIS_RECORD );
(void) writer.WriteLengthPrefixedBytes( bytes );
(void) writer.WriteLengthPrefixedBytes(
gsl::span<const uint8_t>( reinterpret_cast<const uint8_t *>( fingerprint.data() ),
fingerprint.size() ) );
(void) writer.WriteLengthPrefixedBytes( signature );
return writer.Take();
}
struct GenesisRecord
{
std::vector<uint8_t> bytes;
std::string fingerprint;
std::vector<uint8_t> signature;
};
std::optional<GenesisRecord> DecodeGenesisRecord( const base::Buffer &value )
{
CanonicalTrustCodec::Reader reader( value );
auto domain = reader.ReadBytes( GENESIS_RECORD.size() );
auto bytes = reader.ReadLengthPrefixedBytes( 64 * 1024 );
auto fingerprint = reader.ReadLengthPrefixedBytes( HASH_HEX_LENGTH );
auto signature = reader.ReadLengthPrefixedBytes( MAX_SIGNATURE_LEN );
if ( !domain || !std::equal( domain->begin(), domain->end(), GENESIS_RECORD.begin() ) || !bytes ||
!fingerprint || fingerprint->size() != HASH_HEX_LENGTH || !signature || signature->empty() ||
!reader.Exhausted() )
{
return std::nullopt;
}
GenesisRecord record{ std::move( *bytes ),
std::string( fingerprint->begin(), fingerprint->end() ),
std::move( *signature ) };
if ( !IsHash( record.fingerprint ) )
{
return std::nullopt;
}
return record;
}
std::vector<uint8_t> EncodeSignedRecord( std::string_view domain,
const std::vector<uint8_t> &bytes,
const std::vector<uint8_t> &authorization_bytes,
const multisig::CollectedSignatures &proof )
{
CanonicalTrustCodec::Writer writer;
writer.WriteBytes( domain );
(void) writer.WriteLengthPrefixedBytes( bytes );
(void) writer.WriteLengthPrefixedBytes( authorization_bytes );
EncodeProof( writer, proof );
return writer.Take();
}
struct SignedRecord
{
std::vector<uint8_t> bytes;
std::vector<uint8_t> authorization_bytes;
multisig::CollectedSignatures proof;
};
std::optional<SignedRecord> DecodeSignedRecord( const base::Buffer &value, std::string_view domain )
{
CanonicalTrustCodec::Reader current_reader( value );
auto current_domain = current_reader.ReadBytes( domain.size() );
auto current_bytes = current_reader.ReadLengthPrefixedBytes( 64 * 1024 );
auto authorization_bytes = current_reader.ReadLengthPrefixedBytes( 128 * 1024 );
if ( current_domain && std::equal( current_domain->begin(), current_domain->end(), domain.begin() ) &&
current_bytes && authorization_bytes && !authorization_bytes->empty() )
{
auto proof = DecodeProof( current_reader );
if ( proof && current_reader.Exhausted() )
{
return SignedRecord{ std::move( *current_bytes ),
std::move( *authorization_bytes ),
std::move( *proof ) };
}
}
// Compatibility with the original Phase 13-02 record layout,
// where signatures directly followed the canonical state bytes.
CanonicalTrustCodec::Reader legacy_reader( value );
auto legacy_domain = legacy_reader.ReadBytes( domain.size() );
auto legacy_bytes = legacy_reader.ReadLengthPrefixedBytes( 64 * 1024 );
if ( !legacy_domain || !std::equal( legacy_domain->begin(), legacy_domain->end(), domain.begin() ) ||
!legacy_bytes )
{
return std::nullopt;
}
auto legacy_proof = DecodeProof( legacy_reader );
if ( !legacy_proof || !legacy_reader.Exhausted() )
{
return std::nullopt;
}
return SignedRecord{ *legacy_bytes, std::move( *legacy_bytes ), std::move( *legacy_proof ) };
}
bool VerifyProof( const QuorumPolicyState &policy,
uint64_t threshold,
const multisig::CollectedSignatures &proof,
const std::vector<uint8_t> &bytes )
{
multisig::MultiSig verifier( policy.peers, threshold );
return verifier.IsValid() && verifier.EvaluateQuorum( proof, bytes ).has_quorum;
}
bool AuthorizationBindsPolicy( const std::vector<uint8_t> &authorization_bytes,
const std::vector<uint8_t> &policy_bytes,
const QuorumPolicyState &policy )
{
if ( authorization_bytes == policy_bytes )
{
return true;
}
auto core = securecrdt::CandidateCore::DecodeCanonical( authorization_bytes );
return core && core->kind == securecrdt::CandidateKind::TrustPolicy &&
core->network_id == policy.network_id && core->version == policy.version &&
core->expected_previous_hash == policy.expected_previous_hash &&
core->authorizing_policy_hash == policy.authorizing_policy_hash && core->payload == policy_bytes;
}
bool AuthorizationBindsBurn( const std::vector<uint8_t> &authorization_bytes,
const std::vector<uint8_t> &burn_bytes,
const ConfirmedBurnState &burn )
{
if ( authorization_bytes == burn_bytes )
{
return true;
}
auto core = securecrdt::CandidateCore::DecodeCanonical( authorization_bytes );
return core && core->kind == securecrdt::CandidateKind::BurnConfig && core->network_id == burn.network_id &&
core->version == burn.version && core->expected_previous_hash == burn.expected_previous_hash &&
core->authorizing_policy_hash == burn.authorizing_policy_hash && core->payload == burn_bytes;
}
bool IsCanonicalBurnCandidateAuthorization( const std::vector<uint8_t> &authorization_bytes,
const std::vector<uint8_t> &burn_bytes,
const ConfirmedBurnState &burn )
{
// The canonical authorization is the candidate core, never the raw state bytes.
return authorization_bytes != burn_bytes && AuthorizationBindsBurn( authorization_bytes, burn_bytes, burn );
}
} // namespace
std::string BurnGenesisAnchorHash( const std::string &genesis_fingerprint )
{
CanonicalTrustCodec::Writer writer;
writer.WriteBytes( BURN_GENESIS_ANCHOR_DOMAIN );
writer.WriteBytes( genesis_fingerprint );
return CanonicalTrustCodec::Sha256Hex( writer.Take() );
}
std::optional<std::vector<uint8_t>> ConfirmedBurnState::CanonicalBytes() const
{
if ( encoding_version != ENCODING_VERSION || version == 0 || !IsHash( expected_previous_hash ) ||
!IsHash( authorizing_policy_hash ) || basis_points > 10000 )
{
return std::nullopt;
}
CanonicalTrustCodec::Writer writer;
writer.WriteBytes( BURN_DOMAIN );
writer.WriteU8( encoding_version );
writer.WriteU16( network_id );
writer.WriteU64( version );
writer.WriteBytes( expected_previous_hash );
writer.WriteBytes( authorizing_policy_hash );
writer.WriteU64( basis_points );
return writer.Take();
}
std::optional<std::string> ConfirmedBurnState::Hash() const
{
auto bytes = CanonicalBytes();
if ( !bytes )
{
return std::nullopt;
}
return CanonicalTrustCodec::Sha256Hex( *bytes );
}
std::optional<ConfirmedBurnState> ConfirmedBurnState::DecodeCanonical( const std::vector<uint8_t> &bytes )
{
CanonicalTrustCodec::Reader reader( bytes );
auto domain = reader.ReadBytes( BURN_DOMAIN.size() );
auto encoding = reader.ReadU8();
auto network = reader.ReadU16();
auto version_value = reader.ReadU64();
auto previous = reader.ReadBytes( HASH_HEX_LENGTH );
auto authorizer = reader.ReadBytes( HASH_HEX_LENGTH );
auto value = reader.ReadU64();
if ( !domain || !std::equal( domain->begin(), domain->end(), BURN_DOMAIN.begin() ) || !encoding || !network ||
!version_value || !previous || !authorizer || !value || !reader.Exhausted() )
{
return std::nullopt;
}
ConfirmedBurnState burn;
burn.encoding_version = *encoding;
burn.network_id = *network;
burn.version = *version_value;
burn.expected_previous_hash.assign( previous->begin(), previous->end() );
burn.authorizing_policy_hash.assign( authorizer->begin(), authorizer->end() );
burn.basis_points = *value;
auto canonical = burn.CanonicalBytes();
if ( !canonical || *canonical != bytes )
{
return std::nullopt;
}
return burn;
}
bool ConfirmedBurnState::operator==( const ConfirmedBurnState &other ) const
{
return encoding_version == other.encoding_version && network_id == other.network_id &&
version == other.version && expected_previous_hash == other.expected_previous_hash &&
authorizing_policy_hash == other.authorizing_policy_hash && basis_points == other.basis_points;
}
bool ConfirmedTrustSnapshot::operator==( const ConfirmedTrustSnapshot &other ) const
{
return genesis == other.genesis && genesis_fingerprint == other.genesis_fingerprint &&
bootstrap_signature == other.bootstrap_signature && policy == other.policy &&
policy_proof == other.policy_proof && burn == other.burn && burn_proof == other.burn_proof &&
burn_authorization == other.burn_authorization;
}
TrustStateStore::TrustStateStore( std::shared_ptr<storage::rocksdb> database,
uint16_t network_id,
BatchCommitter committer,
LoadObserver load_observer ) :
database_( std::move( database ) ),
network_id_( network_id ),
committer_( std::move( committer ) ),
load_observer_( std::move( load_observer ) )
{
}
outcome::result<std::shared_ptr<TrustStateStore>> TrustStateStore::Open( const std::string &path,
uint16_t network_id,
BatchCommitter committer,
LoadObserver load_observer )
{
storage::rocksdb::Options options;
options.create_if_missing = true;
auto database = storage::rocksdb::create( path, options );
if ( database.has_error() )
{
return outcome::failure( Error::COMMIT_FAILED );
}
return std::shared_ptr<TrustStateStore>(
new TrustStateStore(
database.value(), network_id, std::move( committer ), std::move( load_observer ) ) );
}
outcome::result<ConfirmedTrustSnapshot> TrustStateStore::LoadAndVerify() const
{
std::lock_guard<std::mutex> lock( transition_mutex_ );
return LoadAndVerifyUnlocked();
}
outcome::result<ConfirmedTrustSnapshot> TrustStateStore::LoadAndVerifyUnlocked() const
{
auto genesis_value = database_->get( Buffer( GenesisKey( network_id_ ) ) );
if ( genesis_value.has_error() )
{
auto any_network = database_->query( Buffer( "trust/version-1/network/" ) );
if ( any_network.has_value() && !any_network.value().empty() )
{
return outcome::failure( Error::NETWORK_MISMATCH );
}
return outcome::failure( Error::NOT_FOUND );
}
auto genesis_record = DecodeGenesisRecord( genesis_value.value() );
if ( !genesis_record )
{
return outcome::failure( Error::CORRUPT_GENESIS );
}
auto genesis = GenesisManifest::DecodeAndVerify( genesis_record->bytes, genesis_record->fingerprint );
if ( !genesis )
{
return outcome::failure( Error::CORRUPT_FINGERPRINT );
}
if ( genesis->network_id != network_id_ )
{
return outcome::failure( Error::NETWORK_MISMATCH );
}
const auto genesis_core = GenesisCandidateCore( *genesis, genesis_record->bytes, genesis_record->fingerprint );
const auto genesis_authorization = genesis_core.CanonicalBytes();
if ( !multisig::VerifyPayloadSignature( genesis->bootstrapper_public_key,
genesis_record->signature,
genesis_record->bytes ) &&
( !genesis_authorization || !multisig::VerifyPayloadSignature( genesis->bootstrapper_public_key,
genesis_record->signature,
*genesis_authorization ) ) )
{
return outcome::failure( Error::INVALID_GENESIS_PROOF );
}
auto policy_head_value = database_->get( Buffer( PolicyHeadKey( network_id_ ) ) );
if ( policy_head_value.has_error() )
{
return outcome::failure( Error::MISSING_POLICY_RECORD );
}
auto policy_head = DecodeHead( policy_head_value.value() );
if ( !policy_head )
{
return outcome::failure( Error::CORRUPT_POLICY_RECORD );
}
std::map<std::string, QuorumPolicyState> policies;
std::map<std::string, multisig::CollectedSignatures> policy_proofs;
std::map<std::string, std::vector<uint8_t>> policy_authorizations;
std::string policy_hash = policy_head->second;
for ( uint64_t version = policy_head->first; version > 0; --version )
{
auto value = database_->get( Buffer( PolicyRecordKey( network_id_, version, policy_hash ) ) );
if ( value.has_error() )
{
return outcome::failure( Error::MISSING_POLICY_RECORD );
}
auto record = DecodeSignedRecord( value.value(), POLICY_RECORD );
if ( !record )
{
return outcome::failure( Error::CORRUPT_POLICY_RECORD );
}
auto policy = QuorumPolicyState::DecodeCanonical( record->bytes );
if ( !policy || policy->network_id != network_id_ || policy->version != version ||
policy->Hash() != std::optional<std::string>( policy_hash ) )
{
return outcome::failure( Error::CORRUPT_POLICY_RECORD );
}
policies.emplace( policy_hash, *policy );
policy_proofs.emplace( policy_hash, record->proof );
policy_authorizations.emplace( policy_hash, record->authorization_bytes );
policy_hash = policy->expected_previous_hash;
}
const auto current_policy = policies.find( policy_head->second );
if ( current_policy == policies.end() )
{
return outcome::failure( Error::CORRUPT_POLICY_RECORD );
}
for ( const auto &[hash, policy] : policies )
{
const auto &proof = policy_proofs.at( hash );
const auto &authorization_bytes = policy_authorizations.at( hash );
auto bytes = policy.CanonicalBytes().value();
if ( policy.version == 1 )
{
if ( policy.expected_previous_hash != genesis_record->fingerprint ||
policy.authorizing_policy_hash != genesis_record->fingerprint || policy.peers != genesis->peers ||
policy.membership_threshold != genesis->membership_threshold ||
policy.burn_threshold != genesis->burn_threshold || proof.size() != 1 ||
proof.front().first != genesis->bootstrapper_public_key ||
proof.front().second != genesis_record->signature )
{
return outcome::failure( Error::INVALID_POLICY_PROOF );
}
}
else
{
auto predecessor = policies.find( policy.expected_previous_hash );
if ( predecessor == policies.end() || policy.authorizing_policy_hash != predecessor->first ||
!ValidatePolicySuccessor( predecessor->second, policy ) ||
!AuthorizationBindsPolicy( authorization_bytes, bytes, policy ) ||
!VerifyProof( predecessor->second,
predecessor->second.membership_threshold,
proof,
authorization_bytes ) )
{
return outcome::failure( Error::INVALID_POLICY_PROOF );
}
}
}
if ( load_observer_ )
{
load_observer_();
}
auto burn_head_value = database_->get( Buffer( BurnHeadKey( network_id_ ) ) );
if ( burn_head_value.has_error() )
{
return outcome::failure( Error::MISSING_BURN_RECORD );
}
auto burn_head = DecodeHead( burn_head_value.value() );
if ( !burn_head )
{
return outcome::failure( Error::CORRUPT_BURN_RECORD );
}
std::map<std::string, ConfirmedBurnState> burns;
std::map<std::string, multisig::CollectedSignatures> burn_proofs;
std::map<std::string, std::vector<uint8_t>> burn_authorizations;
std::map<std::string, BurnAuthorizationKind> burn_authorization_kinds;
std::string burn_hash = burn_head->second;
for ( uint64_t version = burn_head->first; version > 0; --version )
{
auto value = database_->get( Buffer( BurnRecordKey( network_id_, version, burn_hash ) ) );
if ( value.has_error() )
{
return outcome::failure( Error::MISSING_BURN_RECORD );
}
auto record = DecodeSignedRecord( value.value(), BURN_RECORD );
if ( !record )
{
return outcome::failure( Error::CORRUPT_BURN_RECORD );
}
auto burn = ConfirmedBurnState::DecodeCanonical( record->bytes );
if ( !burn || burn->network_id != network_id_ || burn->version != version ||
burn->Hash() != std::optional<std::string>( burn_hash ) )
{
return outcome::failure( Error::CORRUPT_BURN_RECORD );
}
burns.emplace( burn_hash, *burn );
burn_proofs.emplace( burn_hash, record->proof );
burn_authorizations.emplace( burn_hash, record->authorization_bytes );
burn_hash = burn->expected_previous_hash;
}
const auto current_burn = burns.find( burn_head->second );
if ( current_burn == burns.end() )
{
return outcome::failure( Error::CORRUPT_BURN_RECORD );
}
for ( const auto &[hash, burn] : burns )
{
const auto &proof = burn_proofs.at( hash );
const auto &authorization_bytes = burn_authorizations.at( hash );
if ( burn.version == 1 )
{
auto policy = policies.find( burn.authorizing_policy_hash );
auto bytes = burn.CanonicalBytes().value();
const bool legacy_bootstrap = burn.expected_previous_hash == genesis_record->fingerprint &&
authorization_bytes == bytes;
const bool bootstrap_proof = proof.size() == 1 &&
proof.front().first == genesis->bootstrapper_public_key &&
proof.front().second == genesis_record->signature &&
( authorization_bytes == genesis_record->bytes ||
( genesis_authorization &&
authorization_bytes == *genesis_authorization ) ||
legacy_bootstrap );
const bool peer_proof = policy != policies.end() &&
AuthorizationBindsBurn( authorization_bytes, bytes, burn ) &&
VerifyProof( policy->second,
policy->second.burn_threshold,
proof,
authorization_bytes );
if ( ( burn.expected_previous_hash != BurnGenesisAnchorHash( genesis_record->fingerprint ) &&
!legacy_bootstrap ) ||
policy == policies.end() || policy->second.version != 1 ||
burn.basis_points != genesis->initial_burn_basis_points || ( !bootstrap_proof && !peer_proof ) )
{
return outcome::failure( Error::INVALID_BURN_PROOF );
}
burn_authorization_kinds.emplace(
hash, peer_proof ? BurnAuthorizationKind::PeerQuorum : BurnAuthorizationKind::BootstrapOnly );
}
else
{
auto predecessor = burns.find( burn.expected_previous_hash );
auto policy = policies.find( burn.authorizing_policy_hash );
auto bytes = burn.CanonicalBytes().value();
if ( predecessor == burns.end() || predecessor->second.version + 1 != burn.version ||
policy == policies.end() || !AuthorizationBindsBurn( authorization_bytes, bytes, burn ) ||
!VerifyProof( policy->second, policy->second.burn_threshold, proof, authorization_bytes ) )
{
return outcome::failure( Error::INVALID_BURN_PROOF );
}
burn_authorization_kinds.emplace( hash, BurnAuthorizationKind::PeerQuorum );
}
}
return ConfirmedTrustSnapshot{ *genesis,
genesis_record->fingerprint,
genesis_record->signature,
current_policy->second,
policy_proofs.at( current_policy->first ),
current_burn->second,
burn_proofs.at( current_burn->first ),
burn_authorization_kinds.at( current_burn->first ) };
}
outcome::result<ConfirmedTrustSnapshot> TrustStateStore::CommitGenesis(
const GenesisManifest &manifest,
const std::vector<uint8_t> &bootstrap_signature,
const std::vector<uint8_t> &authorization_bytes )
{
std::lock_guard<std::mutex> lock( transition_mutex_ );
if ( database_->contains( Buffer( GenesisKey( network_id_ ) ) ) )
{
return outcome::failure( Error::ALREADY_INITIALIZED );
}
auto canonical = manifest.Canonicalized();
auto bytes = manifest.CanonicalBytes();
auto fingerprint = manifest.Fingerprint();
if ( !canonical || !bytes || !fingerprint )
{
return outcome::failure( Error::CORRUPT_GENESIS );
}
if ( canonical->network_id != network_id_ )
{
return outcome::failure( Error::NETWORK_MISMATCH );
}
const auto genesis_core = GenesisCandidateCore( *canonical, *bytes, *fingerprint );
const auto expected_candidate_authorization = genesis_core.CanonicalBytes();
const auto &proof_bytes = authorization_bytes.empty() ? *bytes : authorization_bytes;
if ( !multisig::VerifyPayloadSignature( canonical->bootstrapper_public_key,
bootstrap_signature,
proof_bytes ) ||
( proof_bytes != *bytes &&
( !expected_candidate_authorization || proof_bytes != *expected_candidate_authorization ) ) )
{
return outcome::failure( Error::INVALID_GENESIS_PROOF );
}
QuorumPolicyState policy;
policy.network_id = network_id_;
policy.version = 1;
policy.expected_previous_hash = *fingerprint;
policy.authorizing_policy_hash = *fingerprint;
policy.peers = canonical->peers;
policy.membership_threshold = canonical->membership_threshold;
policy.burn_threshold = canonical->burn_threshold;
auto policy_bytes = policy.CanonicalBytes();
auto policy_hash = policy.Hash();
if ( !policy_bytes || !policy_hash )
{
return outcome::failure( Error::CORRUPT_POLICY_RECORD );
}
ConfirmedBurnState burn;
burn.network_id = network_id_;
burn.version = 1;
burn.expected_previous_hash = BurnGenesisAnchorHash( *fingerprint );
burn.authorizing_policy_hash = *policy_hash;
burn.basis_points = canonical->initial_burn_basis_points;
auto burn_bytes = burn.CanonicalBytes();
auto burn_hash = burn.Hash();
if ( !burn_bytes || !burn_hash )
{
return outcome::failure( Error::CORRUPT_BURN_RECORD );
}
multisig::CollectedSignatures proof{ { canonical->bootstrapper_public_key, bootstrap_signature } };
std::vector<Write> writes{
{ Buffer( GenesisKey( network_id_ ) ),
Buffer( EncodeGenesisRecord( *bytes, *fingerprint, bootstrap_signature ) ) },
{ Buffer( PolicyRecordKey( network_id_, 1, *policy_hash ) ),
Buffer( EncodeSignedRecord( POLICY_RECORD, *policy_bytes, proof_bytes, proof ) ) },
{ Buffer( PolicyHeadKey( network_id_ ) ), Buffer( EncodeHead( 1, *policy_hash ) ) },
{ Buffer( BurnRecordKey( network_id_, 1, *burn_hash ) ),
Buffer( EncodeSignedRecord( BURN_RECORD, *burn_bytes, proof_bytes, proof ) ) },
{ Buffer( BurnHeadKey( network_id_ ) ), Buffer( EncodeHead( 1, *burn_hash ) ) },
};
auto committed = CommitWrites( writes );
if ( committed.has_error() )
{
return outcome::failure( Error::COMMIT_FAILED );
}
return LoadAndVerifyUnlocked();
}
outcome::result<void> TrustStateStore::CheckVersionSkip( uint64_t current_version,
uint64_t candidate_version ) const
{
if ( current_version == std::numeric_limits<uint64_t>::max() || candidate_version != current_version + 1 )
{
return outcome::failure( Error::VERSION_SKIP );
}
return outcome::success();
}
outcome::result<void> TrustStateStore::CheckPredecessorAndAuthorizer( const std::string &expected_previous_hash,
const std::string &authorizing_policy_hash,
const std::string ¤t_domain_hash,
const std::string ¤t_policy_hash ) const
{
if ( expected_previous_hash != current_domain_hash )
{
return outcome::failure( Error::WRONG_PREDECESSOR );
}
if ( authorizing_policy_hash != current_policy_hash )
{
return outcome::failure( Error::WRONG_AUTHORIZER );
}
return outcome::success();
}
outcome::result<ConfirmedTrustSnapshot> TrustStateStore::CommitPolicySuccessor(
const QuorumPolicyState &candidate,
const multisig::CollectedSignatures &proof,
const std::vector<uint8_t> &authorization_bytes )
{
std::lock_guard<std::mutex> lock( transition_mutex_ );
auto current_result = LoadAndVerifyUnlocked();
if ( current_result.has_error() )
{
return current_result.error();
}
const auto ¤t = current_result.value();
if ( current.burn_authorization != BurnAuthorizationKind::PeerQuorum )
{
return outcome::failure( Error::INITIAL_BURN_NOT_CONFIRMED );
}
if ( candidate.version < current.policy.version )
{
return outcome::failure( Error::VERSION_DECREASE );
}
if ( candidate.version == current.policy.version )
{
const auto candidate_hash = candidate.Hash();
const auto current_hash = current.policy.Hash();
if ( candidate_hash && current_hash && *candidate_hash != *current_hash )
{
return outcome::failure( Error::STALE_HEAD );
}
// Re-committing the exact durable candidate (the refresh-driven vs
// admin-driven activation race) is benign — but only with a valid quorum
// proof; an unproven duplicate stays a downgrade attempt.
auto bytes = candidate.CanonicalBytes();
if ( bytes )
{
const auto &signed_bytes = authorization_bytes.empty() ? *bytes : authorization_bytes;
if ( AuthorizationBindsPolicy( signed_bytes, *bytes, candidate ) &&
VerifyProof( current.policy, current.policy.membership_threshold, proof, signed_bytes ) )
{
return current;
}
}
return outcome::failure( Error::VERSION_DECREASE );
}
BOOST_OUTCOME_TRY( CheckVersionSkip( current.policy.version, candidate.version ) );
const auto current_hash = current.policy.Hash().value();
BOOST_OUTCOME_TRY( CheckPredecessorAndAuthorizer( candidate.expected_previous_hash,
candidate.authorizing_policy_hash,
current_hash,
current_hash ) );
auto canonical = candidate.Canonicalized();
auto bytes = candidate.CanonicalBytes();
auto hash = candidate.Hash();
if ( !canonical || !bytes || !hash || !ValidatePolicySuccessor( current.policy, candidate ) )
{
return outcome::failure( Error::CORRUPT_POLICY_RECORD );
}
const auto &signed_bytes = authorization_bytes.empty() ? *bytes : authorization_bytes;
if ( !AuthorizationBindsPolicy( signed_bytes, *bytes, candidate ) ||
!VerifyProof( current.policy, current.policy.membership_threshold, proof, signed_bytes ) )
{
return outcome::failure( Error::INVALID_POLICY_PROOF );
}
return CommitRecordAndHead(
{ Buffer( PolicyRecordKey( network_id_, canonical->version, *hash ) ),
Buffer( EncodeSignedRecord( POLICY_RECORD, *bytes, signed_bytes, proof ) ) },
{ Buffer( PolicyHeadKey( network_id_ ) ), Buffer( EncodeHead( canonical->version, *hash ) ) } );
}
outcome::result<ConfirmedTrustSnapshot> TrustStateStore::CommitBurnSuccessor(
const ConfirmedBurnState &candidate,
const multisig::CollectedSignatures &proof,
const std::vector<uint8_t> &authorization_bytes )
{
std::lock_guard<std::mutex> lock( transition_mutex_ );
auto current_result = LoadAndVerifyUnlocked();
if ( current_result.has_error() )
{
return current_result.error();
}
const auto ¤t = current_result.value();
if ( current.burn_authorization == BurnAuthorizationKind::BootstrapOnly )
{
const auto current_bytes = current.burn.CanonicalBytes();
const auto candidate_bytes = candidate.CanonicalBytes();
const auto current_hash = current.burn.Hash();
const auto candidate_hash = candidate.Hash();
const auto current_policy_hash = current.policy.Hash();
if ( current.burn.version != 1 || current.burn.basis_points != GenesisManifest::INITIAL_BURN_BASIS_POINTS ||
!( candidate == current.burn ) || !current_bytes || !candidate_bytes ||
*candidate_bytes != *current_bytes ||
!current_hash || !candidate_hash || *candidate_hash != *current_hash || !current_policy_hash ||
candidate.authorizing_policy_hash != *current_policy_hash || authorization_bytes.empty() ||
!IsCanonicalBurnCandidateAuthorization( authorization_bytes, *candidate_bytes, candidate ) ||
!VerifyProof( current.policy, current.policy.burn_threshold, proof, authorization_bytes ) )
{
return outcome::failure( Error::INITIAL_BURN_NOT_CONFIRMED );
}
}
if ( candidate.version < current.burn.version )
{
return outcome::failure( Error::VERSION_DECREASE );
}
if ( candidate.version == current.burn.version )
{
const auto candidate_hash = candidate.Hash();
const auto current_hash = current.burn.Hash();
if ( candidate_hash && current_hash && *candidate_hash != *current_hash )
{
return outcome::failure( Error::STALE_HEAD );
}
if ( !candidate_hash || !current_hash )
{
return outcome::failure( Error::VERSION_DECREASE );
}
auto bytes = candidate.CanonicalBytes();
if ( !bytes )
{
return outcome::failure( Error::CORRUPT_BURN_RECORD );
}
const auto &signed_bytes = authorization_bytes.empty() ? *bytes : authorization_bytes;
if ( !AuthorizationBindsBurn( signed_bytes, *bytes, candidate ) ||
!VerifyProof( current.policy, current.policy.burn_threshold, proof, signed_bytes ) )
{
return outcome::failure( Error::INVALID_BURN_PROOF );
}
// Re-committing the exact durable candidate (the refresh-driven vs
// admin/peer-driven activation race) is benign — return the durable
// state instead of failing as a downgrade attempt.
if ( candidate.version != 1 )
{
return current;
}
return CommitRecordAndHead(
{ Buffer( BurnRecordKey( network_id_, candidate.version, *candidate_hash ) ),
Buffer( EncodeSignedRecord( BURN_RECORD, *bytes, signed_bytes, proof ) ) },
{ Buffer( BurnHeadKey( network_id_ ) ), Buffer( EncodeHead( candidate.version, *candidate_hash ) ) } );
}
BOOST_OUTCOME_TRY( CheckVersionSkip( current.burn.version, candidate.version ) );
BOOST_OUTCOME_TRY( CheckPredecessorAndAuthorizer( candidate.expected_previous_hash,
candidate.authorizing_policy_hash,
current.burn.Hash().value(),
current.policy.Hash().value() ) );
auto bytes = candidate.CanonicalBytes();
auto hash = candidate.Hash();
if ( !bytes || !hash || candidate.network_id != network_id_ )
{
return outcome::failure( Error::CORRUPT_BURN_RECORD );
}
const auto &signed_bytes = authorization_bytes.empty() ? *bytes : authorization_bytes;
if ( !AuthorizationBindsBurn( signed_bytes, *bytes, candidate ) ||
!VerifyProof( current.policy, current.policy.burn_threshold, proof, signed_bytes ) )
{
return outcome::failure( Error::INVALID_BURN_PROOF );
}
return CommitRecordAndHead(
{ Buffer( BurnRecordKey( network_id_, candidate.version, *hash ) ),
Buffer( EncodeSignedRecord( BURN_RECORD, *bytes, signed_bytes, proof ) ) },
{ Buffer( BurnHeadKey( network_id_ ) ), Buffer( EncodeHead( candidate.version, *hash ) ) } );
}
outcome::result<void> TrustStateStore::CommitWrites( const std::vector<Write> &writes )
{
if ( committer_ )
{
return committer_( *database_, writes );
}
auto batch = database_->batch();
if ( !batch )
{
return outcome::failure( Error::COMMIT_FAILED );
}
for ( const auto &[key, value] : writes )
{
auto put = batch->put( key, value );
if ( put.has_error() )
{
return outcome::failure( Error::COMMIT_FAILED );
}
}
auto commit = batch->commit();
if ( commit.has_error() )
{
return outcome::failure( Error::COMMIT_FAILED );
}
return outcome::success();
}
outcome::result<ConfirmedTrustSnapshot> TrustStateStore::CommitRecordAndHead( Write record_write, Write head_write )
{
auto committed = CommitWrites( { std::move( record_write ), std::move( head_write ) } );
if ( committed.has_error() )
{
return outcome::failure( Error::COMMIT_FAILED );
}
return LoadAndVerifyUnlocked();
}
} // namespace sgns::trustedpeer
Updated on 2026-09-25 at 15:46:12 +0000