Skip to content

genesis_tool/main.cpp

Functions

Name
int main(int argc, char ** argv)

Functions Documentation

function main

int main(
    int argc,
    char ** argv
)

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