/* * Copyright (c) 2015, Cryptonomex, Inc. * All rights reserved. * * This source code is provided for evaluation in private test networks only, until September 8, 2015. After this date, this license expires and * the code may not be used, modified or distributed for any purpose. Redistribution and use in source and binary forms, with or without modification, * are permitted until September 8, 2015, provided that the following conditions are met: * * 1. The code and/or derivative works are used only for private test networks consisting of no more than 10 P2P nodes. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace graphene { namespace app { using net::item_hash_t; using net::item_id; using net::message; using net::block_message; using net::trx_message; using chain::block_header; using chain::signed_block_header; using chain::signed_block; using chain::block_id_type; using std::vector; namespace bpo = boost::program_options; namespace detail { genesis_state_type create_example_genesis() { auto nathan_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan"))); dlog("Allocating all stake to ${key}", ("key", utilities::key_to_wif(nathan_key))); genesis_state_type initial_state; initial_state.initial_parameters.current_fees = fee_schedule::get_default();//->set_all_fees(GRAPHENE_BLOCKCHAIN_PRECISION); initial_state.initial_active_witnesses = 10; initial_state.initial_timestamp = time_point_sec(time_point::now().sec_since_epoch() / initial_state.initial_parameters.block_interval * initial_state.initial_parameters.block_interval); for( int i = 0; i < initial_state.initial_active_witnesses; ++i ) { auto name = "init"+fc::to_string(i); initial_state.initial_accounts.emplace_back(name, nathan_key.get_public_key(), nathan_key.get_public_key(), true); initial_state.initial_committee_candidates.push_back({name}); initial_state.initial_witness_candidates.push_back({name, nathan_key.get_public_key()}); } initial_state.initial_accounts.emplace_back("nathan", nathan_key.get_public_key()); initial_state.initial_balances.push_back({nathan_key.get_public_key(), GRAPHENE_SYMBOL, GRAPHENE_MAX_SHARE_SUPPLY}); return initial_state; } class application_impl : public net::node_delegate { public: fc::optional _lock_file; bool _is_block_producer = false; void reset_p2p_node(const fc::path& data_dir) { try { _p2p_network = std::make_shared("Graphene Reference Implementation"); _p2p_network->load_configuration(data_dir / "p2p"); _p2p_network->set_node_delegate(this); if( _options->count("seed-node") ) { auto seeds = _options->at("seed-node").as>(); for( const string& endpoint_string : seeds ) { std::vector endpoints = resolve_string_to_ip_endpoints(endpoint_string); for (const fc::ip::endpoint& endpoint : endpoints) { ilog("Adding seed node ${endpoint}", ("endpoint", endpoint)); _p2p_network->add_node(endpoint); _p2p_network->connect_to_endpoint(endpoint); } } } if( _options->count("p2p-endpoint") ) _p2p_network->listen_on_endpoint(fc::ip::endpoint::from_string(_options->at("p2p-endpoint").as()), true); else _p2p_network->listen_on_port(0, false); _p2p_network->listen_to_p2p_network(); ilog("Configured p2p node to listen on ${ip}", ("ip", _p2p_network->get_actual_listening_endpoint())); _p2p_network->connect_to_p2p_network(); _p2p_network->sync_from(net::item_id(net::core_message_type_enum::block_message_type, _chain_db->head_block_id()), std::vector()); } FC_CAPTURE_AND_RETHROW() } std::vector resolve_string_to_ip_endpoints(const std::string& endpoint_string) { try { string::size_type colon_pos = endpoint_string.find(':'); if (colon_pos == std::string::npos) FC_THROW("Missing required port number in endpoint string \"${endpoint_string}\"", ("endpoint_string", endpoint_string)); std::string port_string = endpoint_string.substr(colon_pos + 1); try { uint16_t port = boost::lexical_cast(port_string); std::string hostname = endpoint_string.substr(0, colon_pos); std::vector endpoints = fc::resolve(hostname, port); if (endpoints.empty()) FC_THROW_EXCEPTION(fc::unknown_host_exception, "The host name can not be resolved: ${hostname}", ("hostname", hostname)); return endpoints; } catch (const boost::bad_lexical_cast&) { FC_THROW("Bad port: ${port}", ("port", port_string)); } } FC_CAPTURE_AND_RETHROW((endpoint_string)) } void reset_websocket_server() { try { if( !_options->count("rpc-endpoint") ) return; _websocket_server = std::make_shared(); _websocket_server->on_connection([&]( const fc::http::websocket_connection_ptr& c ){ auto wsc = std::make_shared(*c); auto login = std::make_shared( std::ref(*_self) ); auto db_api = std::make_shared( std::ref(*_self->chain_database()) ); wsc->register_api(fc::api(db_api)); wsc->register_api(fc::api(login)); c->set_session_data( wsc ); }); ilog("Configured websocket rpc to listen on ${ip}", ("ip",_options->at("rpc-endpoint").as())); _websocket_server->listen( fc::ip::endpoint::from_string(_options->at("rpc-endpoint").as()) ); _websocket_server->start_accept(); } FC_CAPTURE_AND_RETHROW() } void reset_websocket_tls_server() { try { if( !_options->count("rpc-tls-endpoint") ) return; if( !_options->count("server-pem") ) { wlog( "Please specify a server-pem to use rpc-tls-endpoint" ); return; } string password = _options->count("server-pem-password") ? _options->at("server-pem-password").as() : ""; _websocket_tls_server = std::make_shared( _options->at("server-pem").as(), password ); _websocket_tls_server->on_connection([&]( const fc::http::websocket_connection_ptr& c ){ auto wsc = std::make_shared(*c); auto login = std::make_shared( std::ref(*_self) ); auto db_api = std::make_shared( std::ref(*_self->chain_database()) ); wsc->register_api(fc::api(db_api)); wsc->register_api(fc::api(login)); c->set_session_data( wsc ); }); ilog("Configured websocket TLS rpc to listen on ${ip}", ("ip",_options->at("rpc-tls-endpoint").as())); _websocket_tls_server->listen( fc::ip::endpoint::from_string(_options->at("rpc-tls-endpoint").as()) ); _websocket_tls_server->start_accept(); } FC_CAPTURE_AND_RETHROW() } application_impl(application* self) : _self(self), _chain_db(std::make_shared()) { } ~application_impl() { fc::remove_all(_data_dir / "blockchain/dblock"); } void startup() { try { bool clean = !fc::exists(_data_dir / "blockchain/dblock"); fc::create_directories(_data_dir / "blockchain/dblock"); auto initial_state = [&] { ilog("Initializing database..."); if( _options->count("genesis-json") ) return fc::json::from_file(_options->at("genesis-json").as()) .as(); else { std::string egenesis_json; graphene::egenesis::compute_egenesis_json( egenesis_json ); FC_ASSERT( egenesis_json != "" ); FC_ASSERT( graphene::egenesis::get_egenesis_json_hash() == fc::sha256::hash( egenesis_json ) ); return fc::json::from_string( egenesis_json ).as(); } }; if( _options->count("resync-blockchain") ) _chain_db->wipe(_data_dir / "blockchain", true); flat_map loaded_checkpoints; if( _options->count("checkpoint") ) { auto cps = _options->at("checkpoint").as>(); loaded_checkpoints.reserve( cps.size() ); for( auto cp : cps ) { auto item = fc::json::from_string(cp).as >(); loaded_checkpoints[item.first] = item.second; } } _chain_db->add_checkpoints( loaded_checkpoints ); if( _options->count("replay-blockchain") ) { ilog("Replaying blockchain on user request."); _chain_db->reindex(_data_dir/"blockchain", initial_state()); } else if( clean ) _chain_db->open(_data_dir / "blockchain", initial_state); else { wlog("Detected unclean shutdown. Replaying blockchain..."); _chain_db->reindex(_data_dir / "blockchain", initial_state()); } if (!_options->count("genesis-json") && _chain_db->get_chain_id() != graphene::egenesis::get_egenesis_chain_id()) { elog("Detected old database. Nuking and starting over."); _chain_db->wipe(_data_dir / "blockchain", true); _chain_db.reset(); _chain_db = std::make_shared(); _chain_db->add_checkpoints(loaded_checkpoints); _chain_db->open(_data_dir / "blockchain", initial_state); } graphene::time::now(); if( _options->count("apiaccess") ) _apiaccess = fc::json::from_file( _options->at("apiaccess").as() ) .as(); else { // TODO: Remove this generous default access policy // when the UI logs in properly _apiaccess = api_access(); api_access_info wild_access; wild_access.password_hash_b64 = "*"; wild_access.password_salt_b64 = "*"; wild_access.allowed_apis.push_back( "database_api" ); wild_access.allowed_apis.push_back( "network_broadcast_api" ); wild_access.allowed_apis.push_back( "history_api" ); _apiaccess.permission_map["*"] = wild_access; } reset_p2p_node(_data_dir); reset_websocket_server(); reset_websocket_tls_server(); } FC_CAPTURE_AND_RETHROW() } optional< api_access_info > get_api_access_info(const string& username)const { optional< api_access_info > result; auto it = _apiaccess.permission_map.find(username); if( it == _apiaccess.permission_map.end() ) { it = _apiaccess.permission_map.find("*"); if( it == _apiaccess.permission_map.end() ) return result; } return it->second; } /** * If delegate has the item, the network has no need to fetch it. */ virtual bool has_item(const net::item_id& id) override { try { if( id.item_type == graphene::net::block_message_type ) return _chain_db->is_known_block(id.item_hash); else return _chain_db->is_known_transaction(id.item_hash); } FC_CAPTURE_AND_RETHROW( (id) ) } /** * @brief allows the application to validate an item prior to broadcasting to peers. * * @param sync_mode true if the message was fetched through the sync process, false during normal operation * @returns true if this message caused the blockchain to switch forks, false if it did not * * @throws exception if error validating the item, otherwise the item is safe to broadcast on. */ virtual bool handle_block(const graphene::net::block_message& blk_msg, bool sync_mode, std::vector& contained_transaction_message_ids) override { try { if (!sync_mode || blk_msg.block.block_num() % 10000 == 0) ilog("Got block #${n} from network", ("n", blk_msg.block.block_num())); try { bool result = _chain_db->push_block(blk_msg.block, _is_block_producer ? database::skip_nothing : database::skip_transaction_signatures); // the block was accepted, so we now know all of the transactions contained in the block if (!sync_mode) { // if we're not in sync mode, there's a chance we will be seeing some transactions // included in blocks before we see the free-floating transaction itself. If that // happens, there's no reason to fetch the transactions, so construct a list of the // transaction message ids we no longer need. // during sync, it is unlikely that we'll see any old for (const processed_transaction& transaction : blk_msg.block.transactions) { graphene::net::trx_message transaction_message(transaction); contained_transaction_message_ids.push_back(graphene::net::message(transaction_message).id()); } } return result; } catch( const fc::exception& e ) { elog("Error when pushing block:\n${e}", ("e", e.to_detail_string())); throw; } if( !_is_finished_syncing && !sync_mode ) { _is_finished_syncing = true; _self->syncing_finished(); } } FC_CAPTURE_AND_RETHROW( (blk_msg)(sync_mode) ) } virtual void handle_transaction(const graphene::net::trx_message& transaction_message) override { try { ilog("Got transaction from network"); _chain_db->push_transaction( transaction_message.trx ); } FC_CAPTURE_AND_RETHROW( (transaction_message) ) } virtual void handle_message(const message& message_to_process) override { // not a transaction, not a block FC_THROW( "Invalid Message Type" ); } /** * Assuming all data elements are ordered in some way, this method should * return up to limit ids that occur *after* the last ID in synopsis that * we recognize. * * On return, remaining_item_count will be set to the number of items * in our blockchain after the last item returned in the result, * or 0 if the result contains the last item in the blockchain */ virtual std::vector get_item_ids(uint32_t item_type, const std::vector& blockchain_synopsis, uint32_t& remaining_item_count, uint32_t limit) override { try { FC_ASSERT( item_type == graphene::net::block_message_type ); vector result; remaining_item_count = 0; if( _chain_db->head_block_num() == 0 ) return result; result.reserve(limit); block_id_type last_known_block_id; auto itr = blockchain_synopsis.rbegin(); while( itr != blockchain_synopsis.rend() ) { if( _chain_db->is_known_block(*itr) || *itr == block_id_type() ) { last_known_block_id = *itr; break; } ++itr; } for( auto num = block_header::num_from_id(last_known_block_id); num <= _chain_db->head_block_num() && result.size() < limit; ++num ) if( num > 0 ) result.push_back(_chain_db->get_block_id_for_num(num)); if( block_header::num_from_id(result.back()) < _chain_db->head_block_num() ) remaining_item_count = _chain_db->head_block_num() - block_header::num_from_id(result.back()); return result; } FC_CAPTURE_AND_RETHROW( (blockchain_synopsis)(remaining_item_count)(limit) ) } /** * Given the hash of the requested data, fetch the body. */ virtual message get_item(const item_id& id) override { try { ilog("Request for item ${id}", ("id", id)); if( id.item_type == graphene::net::block_message_type ) { auto opt_block = _chain_db->fetch_block_by_id(id.item_hash); if( !opt_block ) elog("Couldn't find block ${id} -- corresponding ID in our chain is ${id2}", ("id", id.item_hash)("id2", _chain_db->get_block_id_for_num(block_header::num_from_id(id.item_hash)))); FC_ASSERT( opt_block.valid() ); ilog("Serving up block #${num}", ("num", opt_block->block_num())); return block_message(std::move(*opt_block)); } return trx_message( _chain_db->get_recent_transaction( id.item_hash ) ); } FC_CAPTURE_AND_RETHROW( (id) ) } virtual chain_id_type get_chain_id()const override { return _chain_db->get_chain_id(); } /** * Returns a synopsis of the blockchain used for syncing. * This consists of a list of selected item hashes from our current preferred * blockchain, exponentially falling off into the past. Horrible explanation. * * If the blockchain is empty, it will return the empty list. * If the blockchain has one block, it will return a list containing just that block. * If it contains more than one block: * the first element in the list will be the hash of the genesis block * the second element will be the hash of an item at the half way point in the blockchain * the third will be ~3/4 of the way through the block chain * the fourth will be at ~7/8... * &c. * the last item in the list will be the hash of the most recent block on our preferred chain */ virtual std::vector get_blockchain_synopsis(uint32_t item_type, const graphene::net::item_hash_t& reference_point, uint32_t number_of_blocks_after_reference_point) override { try { std::vector result; result.reserve(30); uint32_t head_block_num = _chain_db->head_block_num(); result.push_back(_chain_db->head_block_id()); uint32_t current = 1; while( current < head_block_num ) { result.push_back(_chain_db->get_block_id_for_num(head_block_num - current)); current = current*2; } std::reverse( result.begin(), result.end() ); idump((reference_point)(number_of_blocks_after_reference_point)(result)); return result; } FC_CAPTURE_AND_RETHROW( (reference_point)(number_of_blocks_after_reference_point) ) } /** * Call this after the call to handle_message succeeds. * * @param item_type the type of the item we're synchronizing, will be the same as item passed to the sync_from() call * @param item_count the number of items known to the node that haven't been sent to handle_item() yet. * After `item_count` more calls to handle_item(), the node will be in sync */ virtual void sync_status(uint32_t item_type, uint32_t item_count) override { // any status reports to GUI go here } /** * Call any time the number of connected peers changes. */ virtual void connection_count_changed(uint32_t c) override { // any status reports to GUI go here } virtual uint32_t get_block_number(const item_hash_t& block_id) override { try { return block_header::num_from_id(block_id); } FC_CAPTURE_AND_RETHROW( (block_id) ) } /** * Returns the time a block was produced (if block_id = 0, returns genesis time). * If we don't know about the block, returns time_point_sec::min() */ virtual fc::time_point_sec get_block_time(const item_hash_t& block_id) override { try { auto opt_block = _chain_db->fetch_block_by_id( block_id ); if( opt_block.valid() ) return opt_block->timestamp; return fc::time_point_sec::min(); } FC_CAPTURE_AND_RETHROW( (block_id) ) } /** returns graphene::time::now() */ virtual fc::time_point_sec get_blockchain_now() override { return graphene::time::now(); } virtual item_hash_t get_head_block_id() const override { return _chain_db->head_block_id(); } virtual uint32_t estimate_last_known_fork_from_git_revision_timestamp(uint32_t unix_timestamp) const override { return 0; // there are no forks in graphene } virtual void error_encountered(const std::string& message, const fc::oexception& error) override { // notify GUI or something cool } application* _self; fc::path _data_dir; const bpo::variables_map* _options = nullptr; api_access _apiaccess; std::shared_ptr _chain_db; std::shared_ptr _p2p_network; std::shared_ptr _websocket_server; std::shared_ptr _websocket_tls_server; std::map> _plugins; bool _is_finished_syncing = false; }; } application::application() : my(new detail::application_impl(this)) {} application::~application() { if( my->_p2p_network ) { my->_p2p_network->close(); my->_p2p_network.reset(); } if( my->_chain_db ) { my->_chain_db->close(); } } void application::set_program_options(boost::program_options::options_description& command_line_options, boost::program_options::options_description& configuration_file_options) const { configuration_file_options.add_options() ("p2p-endpoint", bpo::value(), "Endpoint for P2P node to listen on") ("seed-node,s", bpo::value>()->composing(), "P2P nodes to connect to on startup (may specify multiple times)") ("checkpoint,c", bpo::value>()->composing(), "Pairs of [BLOCK_NUM,BLOCK_ID] that should be enforced as checkpoints.") ("rpc-endpoint", bpo::value()->implicit_value("127.0.0.1:8090"), "Endpoint for websocket RPC to listen on") ("rpc-tls-endpoint", bpo::value()->implicit_value("127.0.0.1:8089"), "Endpoint for TLS websocket RPC to listen on") ("server-pem,p", bpo::value()->implicit_value("server.pem"), "The TLS certificate file for this server") ("server-pem-password,P", bpo::value()->implicit_value(""), "Password for this certificate") ("genesis-json", bpo::value(), "File to read Genesis State from") ("api-access", bpo::value(), "JSON file specifying API permissions") ; command_line_options.add(configuration_file_options); command_line_options.add_options() ("create-genesis-json", bpo::value(), "Path to create a Genesis State at. If a well-formed JSON file exists at the path, it will be parsed and any " "missing fields in a Genesis State will be added, and any unknown fields will be removed. If no file or an " "invalid file is found, it will be replaced with an example Genesis State.") ("replay-blockchain", "Rebuild object graph by replaying all blocks") ("resync-blockchain", "Delete all blocks and re-sync with network from scratch") ; command_line_options.add(_cli_options); configuration_file_options.add(_cfg_options); } void application::initialize(const fc::path& data_dir, const boost::program_options::variables_map& options) { my->_data_dir = data_dir; my->_options = &options; if( options.count("create-genesis-json") ) { fc::path genesis_out = options.at("create-genesis-json").as(); genesis_state_type genesis_state = detail::create_example_genesis(); if( fc::exists(genesis_out) ) { try { genesis_state = fc::json::from_file(genesis_out).as(); } catch(const fc::exception& e) { std::cerr << "Unable to parse existing genesis file:\n" << e.to_string() << "\nWould you like to replace it? [y/N] "; char response = std::cin.get(); if( toupper(response) != 'Y' ) return; } std::cerr << "Updating genesis state in file " << genesis_out.generic_string() << "\n"; } else { std::cerr << "Creating example genesis state in file " << genesis_out.generic_string() << "\n"; } fc::json::save_to_file(genesis_state, genesis_out); std::exit(EXIT_SUCCESS); } } void application::startup() { my->startup(); } std::shared_ptr application::get_plugin(const string& name) const { return my->_plugins[name]; } net::node_ptr application::p2p_node() { return my->_p2p_network; } std::shared_ptr application::chain_database() const { return my->_chain_db; } void application::set_block_production(bool producing_blocks) { my->_is_block_producer = producing_blocks; } optional< api_access_info > application::get_api_access_info( const string& username )const { return my->get_api_access_info( username ); } bool application::is_finished_syncing() const { return my->_is_finished_syncing; } void graphene::app::application::add_plugin(const string& name, std::shared_ptr p) { my->_plugins[name] = p; } void application::shutdown_plugins() { for( auto& entry : my->_plugins ) entry.second->plugin_shutdown(); return; } void application::initialize_plugins( const boost::program_options::variables_map& options ) { for( auto& entry : my->_plugins ) entry.second->plugin_initialize( options ); return; } void application::startup_plugins() { for( auto& entry : my->_plugins ) entry.second->plugin_startup(); return; } // namespace detail } }