genesis_tool/main.cpp¶
Functions¶
| Name | |
|---|---|
| int | main(int argc, char ** argv) |
Functions Documentation¶
function main¶
Source code¶
#include <charconv>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
#include "account/BurnConfig.hpp"
#include "base/hexutil.hpp"
#include "base/logger.hpp"
#include "crdt/globaldb/GlobalDbNetworkComposition.hpp"
#include "securecrdt/QuorumThresholdValidation.hpp"
#include "securecrdt/SecureCrdt.hpp"
#include "trustedpeer/GenesisManifest.hpp"
#include "trustedpeer/QuorumPolicy.hpp"
#include "trustedpeer/TrustStateStore.hpp"
#include "trustedpeer/TrustedPeerRegistry.hpp"
#include "trustedpeer/genesis_tool/GenesisCeremony.hpp"
#include "trustedpeer/genesis_tool/GenesisCeremonyPlatform.hpp"
#include "trustedpeer/genesis_tool/LocalTrustAdmin.hpp"
namespace
{
using namespace sgns;
using namespace sgns::trustedpeer;
void PrintHelp( std::ostream &out )
{
out << "Usage: sgns-trust <operation> [options]\n"
"\nOffline operations:\n"
" make-manifest build canonical genesis manifest bytes from plain values\n"
"\nLocal operations:\n"
" genesis review and submit one trusted-peer genesis\n"
" list list authenticated current-head candidates\n"
" propose-policy explicitly propose one policy successor\n"
" propose-burn explicitly propose one burn successor\n"
" approve explicitly approve one exact candidate ID\n"
"\nmake-manifest options:\n"
" --network-id N --bootstrapper ADDRESS --peers ADDR[,ADDR...] --out PATH\n"
" [--membership-threshold N] [--burn-threshold N] (default: majority/burn floors)\n"
"\ngenesis options:\n"
" [--timeout-seconds N] confirmation poll deadline (default 30)\n"
" [--serve-seconds N] keep serving the genesis DAG to peers after durable\n"
" confirmation (default 600, 0 exits immediately)\n"
"\nadmin options:\n"
" [--timeout-seconds N] list/approve catch-up window while candidates sync\n"
" in from peers (default 30, 0 reads immediately)\n"
" [--serve-seconds N] keep serving after approve/propose-* so peers fetch\n"
" the update (default 600, 0 exits immediately)\n";
}
struct Arguments
{
std::string operation;
std::map<std::string, std::string> values;
std::set<std::string> flags;
};
std::optional<Arguments> ParseArguments( int argc, char **argv, std::ostream &errors )
{
if ( argc < 2 )
return std::nullopt;
Arguments parsed;
parsed.operation = argv[1];
const std::set<std::string> flag_options{ "--key-stdin" };
for ( int i = 2; i < argc; ++i )
{
const std::string option = argv[i];
if ( option.rfind( "--", 0 ) != 0 )
{
errors << "unexpected positional argument\n";
return std::nullopt;
}
if ( flag_options.count( option ) != 0 )
{
if ( !parsed.flags.insert( option ).second )
{
errors << "duplicate option: " << option << '\n';
return std::nullopt;
}
continue;
}
if ( i + 1 >= argc || std::string( argv[i + 1] ).rfind( "--", 0 ) == 0 )
{
errors << "missing value for option: " << option << '\n';
return std::nullopt;
}
if ( !parsed.values.emplace( option, argv[++i] ).second )
{
errors << "duplicate option: " << option << '\n';
return std::nullopt;
}
}
return parsed;
}
bool ValidateOptions( const Arguments &arguments, std::ostream &errors )
{
static const std::set<std::string> operations{ "genesis", "list", "propose-policy",
"propose-burn", "approve", "make-manifest" };
if ( operations.count( arguments.operation ) == 0 )
{
errors << "unknown local operation: " << arguments.operation << '\n';
return false;
}
if ( arguments.operation == "make-manifest" )
{
static const std::set<std::string> allowed{ "--network-id", "--bootstrapper", "--peers",
"--membership-threshold", "--burn-threshold", "--out" };
for ( const auto &[option, unused] : arguments.values )
{
(void)unused;
if ( allowed.count( option ) == 0 )
{
errors << "option is not valid for make-manifest: " << option << '\n';
return false;
}
}
if ( !arguments.flags.empty() )
{
errors << "flags are not valid for make-manifest\n";
return false;
}
for ( const auto *required : { "--network-id", "--bootstrapper", "--peers", "--out" } )
{
if ( arguments.values.count( required ) == 0 )
{
errors << "required option missing: " << required << '\n';
return false;
}
}
return true;
}
std::set<std::string> allowed{ "--manifest", "--network-config", "--database", "--topic" };
if ( arguments.operation != "list" )
{
allowed.insert( "--key-file" );
allowed.insert( "--key-stdin" );
}
if ( arguments.operation == "genesis" )
{
allowed.insert( "--timeout-seconds" );
allowed.insert( "--serve-seconds" );
}
else if ( arguments.operation == "list" )
{
allowed.insert( "--timeout-seconds" );
}
else if ( arguments.operation == "approve" )
{
allowed.insert( "--timeout-seconds" );
allowed.insert( "--serve-seconds" );
}
else if ( arguments.operation == "propose-policy" || arguments.operation == "propose-burn" )
{
allowed.insert( "--serve-seconds" );
}
else if ( arguments.operation == "propose-policy" )
allowed.insert( "--candidate" );
else if ( arguments.operation == "propose-burn" )
allowed.insert( "--basis-points" );
else if ( arguments.operation == "approve" )
allowed.insert( "--candidate-id" );
for ( const auto &[option, unused] : arguments.values )
{
(void)unused;
if ( allowed.count( option ) == 0 )
{
errors << "option is not valid for " << arguments.operation << ": " << option << '\n';
return false;
}
}
for ( const auto &option : arguments.flags )
{
if ( allowed.count( option ) == 0 )
{
errors << "option is not valid for " << arguments.operation << ": " << option << '\n';
return false;
}
}
for ( const auto *required : { "--manifest", "--network-config", "--database", "--topic" } )
{
if ( arguments.values.count( required ) == 0 )
{
errors << "required option missing: " << required << '\n';
return false;
}
}
if ( arguments.operation != "list" )
{
const bool file = arguments.values.count( "--key-file" ) != 0;
const bool input = arguments.flags.count( "--key-stdin" ) != 0;
if ( file == input )
{
errors << "select exactly one of --key-file or --key-stdin\n";
return false;
}
}
if ( arguments.operation == "propose-policy" && arguments.values.count( "--candidate" ) == 0 )
return errors << "required option missing: --candidate\n", false;
if ( arguments.operation == "propose-burn" && arguments.values.count( "--basis-points" ) == 0 )
return errors << "required option missing: --basis-points\n", false;
if ( arguments.operation == "approve" && arguments.values.count( "--candidate-id" ) == 0 )
return errors << "required option missing: --candidate-id\n", false;
return true;
}
std::optional<std::vector<uint8_t>> ReadBoundedFile( const std::string &path, size_t maximum )
{
std::ifstream input( path, std::ios::binary );
if ( !input.good() )
return std::nullopt;
std::vector<uint8_t> bytes;
char value = 0;
while ( input.get( value ) )
{
if ( bytes.size() == maximum )
return std::nullopt;
bytes.push_back( static_cast<uint8_t>( value ) );
}
return input.eof() ? std::optional<std::vector<uint8_t>>( std::move( bytes ) ) : std::nullopt;
}
std::optional<uint64_t> ParseUint64( const std::string &value )
{
uint64_t result = 0;
const auto parsed = std::from_chars( value.data(), value.data() + value.size(), result );
if ( parsed.ec != std::errc() || parsed.ptr != value.data() + value.size() )
return std::nullopt;
return result;
}
// Post-write propagation window shared by genesis and the mutating admin
// operations. Local writes are not remote propagation: head announcements and
// the peers' GraphSync fetches are asynchronous, and exiting destroys
// GlobalDbNetworkComposition - the only transport serving the fresh DAG.
constexpr uint64_t kDefaultServeSeconds = 600;
std::optional<uint64_t> ParseServeSeconds( const Arguments &arguments, std::ostream &errors )
{
uint64_t serve_seconds = kDefaultServeSeconds;
if ( const auto serve = arguments.values.find( "--serve-seconds" ); serve != arguments.values.end() )
{
const auto seconds = ParseUint64( serve->second );
if ( !seconds || *seconds > 86400 )
{
errors << "invalid --serve-seconds\n";
return std::nullopt;
}
serve_seconds = *seconds;
}
return serve_seconds;
}
void ServeBeforeExit( uint64_t serve_seconds )
{
if ( serve_seconds == 0 )
{
return;
}
std::cout << "Serving updated trust state to peers for " << serve_seconds
<< "s before exit (0 peers fetched = update confined to this database).\n";
std::this_thread::sleep_for( std::chrono::seconds( serve_seconds ) );
std::cout << "Serving window complete.\n";
}
int MakeManifest( const Arguments &arguments )
{
GenesisManifest manifest;
const auto network_id = ParseUint64( arguments.values.at( "--network-id" ) );
if ( !network_id || *network_id == 0 || *network_id > 65535 )
{
std::cerr << "invalid --network-id (expected 1..65535)\n";
return EXIT_FAILURE;
}
manifest.network_id = static_cast<uint16_t>( *network_id );
manifest.bootstrapper_public_key = arguments.values.at( "--bootstrapper" );
std::istringstream peers_input( arguments.values.at( "--peers" ) );
std::string peer;
while ( std::getline( peers_input, peer, ',' ) )
{
if ( !peer.empty() )
{
manifest.peers.push_back( peer );
}
}
if ( manifest.peers.empty() )
{
std::cerr << "--peers must list at least one address\n";
return EXIT_FAILURE;
}
const auto peer_count = manifest.peers.size();
manifest.membership_threshold = sgns::securecrdt::MembershipQuorumFloor( peer_count );
manifest.burn_threshold = sgns::securecrdt::BurnQuorumFloor( peer_count );
if ( const auto value = arguments.values.find( "--membership-threshold" );
value != arguments.values.end() )
{
const auto threshold = ParseUint64( value->second );
if ( !threshold )
{
std::cerr << "invalid --membership-threshold\n";
return EXIT_FAILURE;
}
manifest.membership_threshold = *threshold;
}
if ( const auto value = arguments.values.find( "--burn-threshold" ); value != arguments.values.end() )
{
const auto threshold = ParseUint64( value->second );
if ( !threshold )
{
std::cerr << "invalid --burn-threshold\n";
return EXIT_FAILURE;
}
manifest.burn_threshold = *threshold;
}
// Canonicalized() validates addresses, enforces the quorum floors, and sorts
// the peer list; CanonicalBytes()/Fingerprint() pin policy_version=1 and the
// default initial burn (100 basis points), matching what nodes derive locally.
const auto canonical = manifest.Canonicalized();
const auto bytes = canonical ? canonical->CanonicalBytes() : std::nullopt;
const auto fingerprint = canonical ? canonical->Fingerprint() : std::nullopt;
if ( !canonical || !bytes || !fingerprint )
{
std::cerr << "invalid manifest: check addresses (128-hex), thresholds "
"(membership >= peers/2+1, burn >= peers-peers/3, both <= peer count), "
"and peer count (1..256)\n";
return EXIT_FAILURE;
}
const std::string &out_path = arguments.values.at( "--out" );
std::ofstream out( out_path, std::ios::binary | std::ios::trunc );
if ( !out.good() )
{
std::cerr << "cannot write manifest to " << out_path << '\n';
return EXIT_FAILURE;
}
out.write( reinterpret_cast<const char *>( bytes->data() ), static_cast<std::streamsize>( bytes->size() ) );
out.close();
if ( !out.good() )
{
std::cerr << "failed writing manifest to " << out_path << '\n';
return EXIT_FAILURE;
}
std::cout << "manifest written to " << out_path << '\n'
<< "network: " << canonical->network_id << '\n'
<< "bootstrapper: " << canonical->bootstrapper_public_key << '\n'
<< "policy version: " << canonical->policy_version << '\n'
<< "membership threshold: " << canonical->membership_threshold << '\n'
<< "burn threshold: " << canonical->burn_threshold << '\n'
<< "initial burn basis points: " << canonical->initial_burn_basis_points << '\n'
<< "ordered peers:\n";
for ( const auto &ordered_peer : canonical->peers )
{
std::cout << " " << ordered_peer << '\n';
}
std::cout << "fingerprint: " << *fingerprint << '\n';
return EXIT_SUCCESS;
}
std::optional<sgns::securecrdt::CandidateId> ParseCandidateId( const std::string &value )
{
const auto first = value.find( ':' );
const auto second = first == std::string::npos ? std::string::npos : value.find( ':', first + 1 );
if ( first == 0 || second == std::string::npos || second + 1 >= value.size() )
return std::nullopt;
const auto version = ParseUint64( value.substr( first + 1, second - first - 1 ) );
const auto hash = value.substr( second + 1 );
if ( !version || hash.size() != 64 || !sgns::base::IsLowerHex( hash ) )
return std::nullopt;
return sgns::securecrdt::CandidateId{ value.substr( 0, first ), *version, hash };
}
std::string FormatCandidateId( const sgns::securecrdt::CandidateId &id )
{
return id.domain + ":" + std::to_string( id.version ) + ":" + id.content_hash;
}
outcome::result<GenesisCeremony::Signer> LoadLocalSigner( const Arguments &arguments,
std::istream &input,
std::ostream &output )
{
auto hooks = GenesisCeremony::DefaultHooks();
std::string key;
const auto file = arguments.values.find( "--key-file" );
if ( file != arguments.values.end() )
{
BOOST_OUTCOME_TRY( auto status, hooks.inspect_key_file( file->second ) );
if ( auto problem = GenesisCeremony::KeyFileStatusProblem( status ) )
return outcome::failure( *problem );
BOOST_OUTCOME_TRY( key, hooks.read_key_file( file->second ) );
}
else
{
output << "local signing key (protected stdin): " << std::flush;
const auto read = sgns::trustedpeer::genesis_ceremony_platform::ReadProtectedLine( input, output, key );
if ( read == sgns::trustedpeer::genesis_ceremony_platform::ProtectedInputResult::NOT_A_TERMINAL )
return outcome::failure( GenesisCeremony::Error::INVALID_KEY_SOURCE );
if ( read != sgns::trustedpeer::genesis_ceremony_platform::ProtectedInputResult::SUCCESS )
return outcome::failure( GenesisCeremony::Error::KEY_FILE_IO );
}
outcome::result<GenesisCeremony::Signer> local_signer = hooks.create_signer( key );
if ( !key.empty() )
hooks.cleanse( key.data(), key.size() );
return local_signer;
}
class TrustRuntime
{
public:
TrustRuntime( const Arguments &arguments, GenesisManifest manifest ) :
arguments_( arguments ), manifest_( std::move( manifest ) )
{
sgns::crdt::GlobalDbNetworkComposition::Config config;
config.network_config_path = arguments_.values.at( "--network-config" );
config.database_path = arguments_.values.at( "--database" );
config.listen_topic = arguments_.values.at( "--topic" );
config.broadcast_topic = arguments_.values.at( "--topic" );
config.logger = sgns::base::createLogger( "sgns-trust" );
auto created = sgns::crdt::GlobalDbNetworkComposition::Create( std::move( config ) );
if ( created.has_value() )
composition_ = created.value();
else
composition_error_ = created.error();
}
outcome::result<void> Start()
{
if ( !composition_ )
return outcome::failure( composition_error_ ? composition_error_ : std::make_error_code( std::errc::invalid_argument ) );
return composition_->Start();
}
outcome::result<sgns::securecrdt::CandidateId> SubmitGenesis(
const GenesisManifest &manifest,
const std::vector<uint8_t> &signature,
const std::string &address,
TrustedPeerRegistry::SignCallback sign )
{
BOOST_OUTCOME_TRY( Prepare( address, std::move( sign ), signature ) );
return registry_->SubmitReviewedGenesisApproval();
}
outcome::result<void> PrepareAdmin( const GenesisCeremony::Signer &signer )
{
return Prepare( signer.address, signer.sign, {} );
}
outcome::result<std::optional<ConfirmedTrustSnapshot>> Confirmed() const
{
if ( !store_ )
return std::optional<ConfirmedTrustSnapshot>{};
auto loaded = store_->LoadAndVerify();
if ( loaded.has_error() )
{
if ( loaded.error() == TrustStateStore::Error::NOT_FOUND )
return std::optional<ConfirmedTrustSnapshot>{};
return loaded.error();
}
return std::optional<ConfirmedTrustSnapshot>( loaded.value() );
}
std::shared_ptr<TrustedPeerRegistry> registry() const { return registry_; }
std::shared_ptr<sgns::account::BurnConfig> burn_config() const { return burn_config_; }
private:
outcome::result<void> Prepare( const std::string &address,
TrustedPeerRegistry::SignCallback sign,
const std::vector<uint8_t> &bootstrap_signature )
{
if ( !composition_ || !composition_->db() )
return outcome::failure( std::errc::not_connected );
secure_crdt_ = std::make_shared<sgns::securecrdt::SecureCrdt>(
composition_->db(), arguments_.values.at( "--topic" ) );
BOOST_OUTCOME_TRY( store_, TrustStateStore::Open(
arguments_.values.at( "--database" ) + "/trust-state", manifest_.network_id ) );
BOOST_OUTCOME_TRY( registry_, TrustedPeerRegistry::NewProduction(
secure_crdt_, store_, manifest_, bootstrap_signature, address, sign ) );
BOOST_OUTCOME_TRY( burn_config_, sgns::account::BurnConfig::NewProduction(
secure_crdt_, registry_, store_, address, std::move( sign ) ) );
if ( !secure_crdt_->RegisterFilters() )
return outcome::failure( std::errc::operation_not_permitted );
return outcome::success();
}
const Arguments &arguments_;
GenesisManifest manifest_;
std::error_code composition_error_;
std::shared_ptr<sgns::crdt::GlobalDbNetworkComposition> composition_;
std::shared_ptr<sgns::securecrdt::SecureCrdt> secure_crdt_;
std::shared_ptr<TrustStateStore> store_;
std::shared_ptr<TrustedPeerRegistry> registry_;
std::shared_ptr<sgns::account::BurnConfig> burn_config_;
};
} // namespace
int main( int argc, char **argv )
{
if ( argc == 2 && std::string( argv[1] ) == "--help" )
{
PrintHelp( std::cout );
return EXIT_SUCCESS;
}
auto arguments = ParseArguments( argc, argv, std::cerr );
if ( !arguments || !ValidateOptions( *arguments, std::cerr ) )
{
PrintHelp( std::cerr );
return EXIT_FAILURE;
}
if ( arguments->operation == "make-manifest" )
{
return MakeManifest( *arguments );
}
auto manifest_bytes = ReadBoundedFile( arguments->values.at( "--manifest" ), 65536 );
auto manifest = manifest_bytes ? GenesisManifest::DecodeCanonical( *manifest_bytes ) : std::nullopt;
if ( !manifest )
{
std::cerr << "manifest must contain canonical GenesisManifest bytes\n";
return EXIT_FAILURE;
}
TrustRuntime runtime( *arguments, *manifest );
if ( arguments->operation == "genesis" )
{
GenesisCeremony::Request request;
request.manifest = *manifest;
if ( const auto key = arguments->values.find( "--key-file" ); key != arguments->values.end() )
request.key_file = key->second;
request.key_stdin = arguments->flags.count( "--key-stdin" ) != 0;
if ( const auto timeout = arguments->values.find( "--timeout-seconds" ); timeout != arguments->values.end() )
{
const auto seconds = ParseUint64( timeout->second );
if ( !seconds || *seconds > 86400 )
{
std::cerr << "invalid --timeout-seconds\n";
return EXIT_FAILURE;
}
request.confirmation_timeout = std::chrono::seconds( *seconds );
}
// After local durable confirmation the tool is still the only peer serving
// the freshly written genesis DAG; CRDT head delivery and the peers'
// GraphSync fetches are asynchronous. --serve-seconds 0 restores the
// immediate-exit behavior for scripting.
const auto serve_seconds = ParseServeSeconds( *arguments, std::cerr );
if ( !serve_seconds )
{
return EXIT_FAILURE;
}
request.serve_duration = std::chrono::seconds( *serve_seconds );
GenesisCeremony::Network network;
network.start = [&] { return runtime.Start(); };
network.submit = [&]( const GenesisManifest &value,
const std::vector<uint8_t> &signature,
const std::string &address,
TrustedPeerRegistry::SignCallback sign )
{ return runtime.SubmitGenesis( value, signature, address, std::move( sign ) ); };
network.confirmed = [&] { return runtime.Confirmed(); };
// Holding TrustRuntime (and its GlobalDbNetworkComposition) alive for the
// requested duration keeps pubsub broadcasting heads and GraphSync serving
// fetches while the process waits.
network.serve = []( std::chrono::milliseconds duration ) { std::this_thread::sleep_for( duration ); };
GenesisCeremony ceremony;
auto result = ceremony.Run( request, network, std::cin, std::cout, std::cerr );
return result.has_value() ? EXIT_SUCCESS : ( std::cerr << result.error().message() << '\n', EXIT_FAILURE );
}
GenesisCeremony::Signer signer;
if ( arguments->operation != "list" )
{
auto loaded = LoadLocalSigner( *arguments, std::cin, std::cout );
if ( loaded.has_error() )
{
std::cerr << loaded.error().message() << '\n';
return EXIT_FAILURE;
}
signer = std::move( loaded.value() );
}
if ( runtime.Start().has_error() || runtime.PrepareAdmin( signer ).has_error() )
{
std::cerr << "unable to start local trust administration\n";
return EXIT_FAILURE;
}
LocalTrustAdmin admin( runtime.registry(), runtime.burn_config() );
// Bounded catch-up window for reads: GlobalDbNetworkComposition::Start()
// only launches the asynchronous PubSub/GlobalDB stack - there is no
// synchronization barrier, so a list/approve issued against a database that
// has not received the latest candidate yet reports an empty list or fails
// while the candidate head/DAG is still arriving. Poll for up to the window
// (default 30s; --timeout-seconds 0 restores the immediate read).
uint64_t read_catchup_seconds = 30;
if ( const auto timeout = arguments->values.find( "--timeout-seconds" ); timeout != arguments->values.end() )
{
const auto seconds = ParseUint64( timeout->second );
if ( !seconds || *seconds > 86400 )
{
std::cerr << "invalid --timeout-seconds\n";
return EXIT_FAILURE;
}
read_catchup_seconds = *seconds;
}
const auto read_deadline = std::chrono::steady_clock::now() + std::chrono::seconds( read_catchup_seconds );
if ( arguments->operation == "list" )
{
auto listed = admin.ListCandidates();
while ( listed.has_value() && listed.value().empty() && std::chrono::steady_clock::now() < read_deadline )
{
std::cout << "No candidates visible yet - waiting for CRDT catch-up...\n";
std::this_thread::sleep_for( std::chrono::seconds( 1 ) );
listed = admin.ListCandidates();
}
if ( listed.has_error() )
return std::cerr << listed.error().message() << '\n', EXIT_FAILURE;
for ( const auto &candidate : listed.value() )
std::cout << ( candidate.type == LocalTrustAdmin::CandidateType::Policy ? "policy " : "burn " )
<< FormatCandidateId( candidate.id ) << '\n';
return EXIT_SUCCESS;
}
if ( arguments->operation == "propose-policy" )
{
auto bytes = ReadBoundedFile( arguments->values.at( "--candidate" ), 65536 );
auto candidate = bytes ? QuorumPolicyState::DecodeCanonical( *bytes ) : std::nullopt;
if ( !candidate )
return std::cerr << "invalid canonical policy candidate\n", EXIT_FAILURE;
auto proposed = admin.ProposePolicy( *candidate );
if ( proposed.has_error() )
return std::cerr << proposed.error().message() << '\n', EXIT_FAILURE;
std::cout << FormatCandidateId( proposed.value() ) << '\n';
// Serve the fresh proposal: exiting immediately would destroy the only
// transport serving its DAG before peers can fetch it.
const auto serve_seconds = ParseServeSeconds( *arguments, std::cerr );
if ( !serve_seconds )
{
return EXIT_FAILURE;
}
ServeBeforeExit( *serve_seconds );
return EXIT_SUCCESS;
}
if ( arguments->operation == "propose-burn" )
{
const auto basis_points = ParseUint64( arguments->values.at( "--basis-points" ) );
if ( !basis_points )
return std::cerr << "invalid basis points\n", EXIT_FAILURE;
auto proposed = admin.ProposeBurn( *basis_points );
if ( proposed.has_error() )
return std::cerr << proposed.error().message() << '\n', EXIT_FAILURE;
std::cout << FormatCandidateId( proposed.value() ) << '\n';
const auto serve_seconds = ParseServeSeconds( *arguments, std::cerr );
if ( !serve_seconds )
{
return EXIT_FAILURE;
}
ServeBeforeExit( *serve_seconds );
return EXIT_SUCCESS;
}
const auto candidate = ParseCandidateId( arguments->values.at( "--candidate-id" ) );
if ( !candidate )
return std::cerr << "invalid candidate ID\n", EXIT_FAILURE;
// The target ID is exact: retry while the referenced record has not arrived
// (ReadCandidateApprovals runs before the record is available otherwise).
auto approved = admin.Approve( *candidate );
while ( approved.has_error() && std::chrono::steady_clock::now() < read_deadline )
{
std::cout << "Candidate " << FormatCandidateId( *candidate )
<< " not visible yet - waiting for CRDT catch-up...\n";
std::this_thread::sleep_for( std::chrono::seconds( 1 ) );
approved = admin.Approve( *candidate );
}
if ( approved.has_error() )
return std::cerr << approved.error().message() << '\n', EXIT_FAILURE;
std::cout << FormatCandidateId( approved.value() ) << '\n';
// An approval can complete a quorum or activate a successor: serve the
// update so other nodes fetch it instead of staying on the old policy.
const auto serve_seconds = ParseServeSeconds( *arguments, std::cerr );
if ( !serve_seconds )
{
return EXIT_FAILURE;
}
ServeBeforeExit( *serve_seconds );
return EXIT_SUCCESS;
}
Updated on 2026-09-25 at 15:46:12 +0000