From 9856d5e8fd769ba1e048975088822433c497b65e Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Tue, 30 Jun 2015 17:28:07 -0400 Subject: [PATCH] Fix CLI commands for creating witnesses, add commands for listing witnesses, registering witness url. Derive memo, witness, etc. keys from the active key. Make witness_create_operation accept relative key identifiers. Prevent wif_to_key from throwing on invalid base58 input. Make witness_node accept witness keys in WIF format. --- libraries/app/api.cpp | 43 ++- libraries/app/include/graphene/app/api.hpp | 27 +- .../include/graphene/chain/operations.hpp | 2 +- libraries/chain/witness_evaluator.cpp | 8 +- libraries/plugins/witness/witness.cpp | 37 ++- libraries/utilities/key_conversion.cpp | 10 +- .../wallet/include/graphene/wallet/wallet.hpp | 33 ++- libraries/wallet/wallet.cpp | 268 ++++++++++++++---- programs/cli_wallet/cookbook.md | 195 +++++++++++++ 9 files changed, 561 insertions(+), 62 deletions(-) create mode 100755 programs/cli_wallet/cookbook.md diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index adde1683..1360303e 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -274,10 +274,51 @@ namespace graphene { namespace app { return {}; } + uint64_t database_api::get_witness_count()const + { + return _db.get_index_type().indices().size(); + } + + map database_api::lookup_witness_accounts(const string& lower_bound_name, uint32_t limit)const + { + FC_ASSERT( limit <= 1000 ); + const auto& witnesses_by_id = _db.get_index_type().indices().get(); + + // we want to order witnesses by account name, but that name is in the account object + // so the witness_index doesn't have a quick way to access it. + // get all the names and look them all up, sort them, then figure out what + // records to return. This could be optimized, but we expect the + // number of witnesses to be few and the frequency of calls to be rare + std::map witnesses_by_account_name; + for (const witness_object& witness : witnesses_by_id) + if (auto account_iter = _db.find(witness.witness_account)) + if (account_iter->name >= lower_bound_name) // we can ignore anything below lower_bound_name + witnesses_by_account_name.insert(std::make_pair(account_iter->name, witness.id)); + + auto end_iter = witnesses_by_account_name.begin(); + while (end_iter != witnesses_by_account_name.end() && limit--) + ++end_iter; + witnesses_by_account_name.erase(end_iter, witnesses_by_account_name.end()); + return witnesses_by_account_name; + } + + vector> database_api::get_witnesses(const vector& witness_ids)const + { + vector> result; result.reserve(witness_ids.size()); + std::transform(witness_ids.begin(), witness_ids.end(), std::back_inserter(result), + [this](witness_id_type id) -> optional { + if(auto o = _db.find(id)) + return *o; + return {}; + }); + return result; + } + login_api::login_api(application& a) :_app(a) { } + login_api::~login_api() { } @@ -466,7 +507,7 @@ namespace graphene { namespace app { return fc::to_hex(fc::raw::pack(trx)); } - vector history_api::get_account_history(account_id_type account, operation_history_id_type stop, int limit, operation_history_id_type start) const + vector history_api::get_account_history(account_id_type account, operation_history_id_type stop, unsigned limit, operation_history_id_type start) const { FC_ASSERT(_app.chain_database()); const auto& db = *_app.chain_database(); diff --git a/libraries/app/include/graphene/app/api.hpp b/libraries/app/include/graphene/app/api.hpp index ff488809..20dec941 100644 --- a/libraries/app/include/graphene/app/api.hpp +++ b/libraries/app/include/graphene/app/api.hpp @@ -193,6 +193,28 @@ namespace graphene { namespace app { */ fc::optional get_witness_by_account(account_id_type account)const; + /** + * @brief Get the total number of witnesses registered with the blockchain + */ + uint64_t get_witness_count()const; + + /** + * @brief Get names and IDs for registered witnesses + * @param lower_bound_name Lower bound of the first name to return + * @param limit Maximum number of results to return -- must not exceed 1000 + * @return Map of witness names to corresponding IDs + */ + map lookup_witness_accounts(const string& lower_bound_name, uint32_t limit)const; + + /** + * @brief Get a list of witnesses by ID + * @param witness_ids IDs of the witnesses to retrieve + * @return The witnesses corresponding to the provided IDs + * + * This function has semantics identical to @ref get_objects + */ + vector> get_witnesses(const vector& witness_ids)const; + /** * @group Push Notification Methods * These methods may be used to get push notifications whenever an object or market is changed @@ -295,7 +317,7 @@ namespace graphene { namespace app { */ vector get_account_history(account_id_type account, operation_history_id_type stop = operation_history_id_type(), - int limit = 100, + unsigned limit = 100, operation_history_id_type start = operation_history_id_type())const; vector get_market_history( asset_id_type a, asset_id_type b, uint32_t bucket_seconds, @@ -418,7 +440,10 @@ FC_API(graphene::app::database_api, (get_settle_orders) (list_assets) (get_delegate_by_account) + (get_witnesses) (get_witness_by_account) + (get_witness_count) + (lookup_witness_accounts) (subscribe_to_objects) (unsubscribe_from_objects) (subscribe_to_market) diff --git a/libraries/chain/include/graphene/chain/operations.hpp b/libraries/chain/include/graphene/chain/operations.hpp index 4f94757d..9b2e4e2d 100644 --- a/libraries/chain/include/graphene/chain/operations.hpp +++ b/libraries/chain/include/graphene/chain/operations.hpp @@ -367,7 +367,7 @@ namespace graphene { namespace chain { /// The account which owns the delegate. This account pays the fee for this operation. account_id_type witness_account; string url; - key_id_type block_signing_key; + object_id_type block_signing_key; // key_id_type or relative_key_id_type secret_hash_type initial_secret; account_id_type fee_payer()const { return witness_account; } diff --git a/libraries/chain/witness_evaluator.cpp b/libraries/chain/witness_evaluator.cpp index 0a0849fa..1ebe8780 100644 --- a/libraries/chain/witness_evaluator.cpp +++ b/libraries/chain/witness_evaluator.cpp @@ -27,6 +27,8 @@ namespace graphene { namespace chain { void_result witness_create_evaluator::do_evaluate( const witness_create_operation& op ) { try { FC_ASSERT(db().get(op.witness_account).is_lifetime_member()); + object_id_type block_signing_key_id = get_relative_id(op.block_signing_key); + FC_ASSERT(block_signing_key_id.type() == key_object_type); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -37,10 +39,14 @@ object_id_type witness_create_evaluator::do_apply( const witness_create_operatio vote_id = p.get_next_vote_id(vote_id_type::witness); }); + object_id_type block_signing_key_object_id = get_relative_id(op.block_signing_key); + FC_ASSERT(block_signing_key_object_id.type() == key_object_type); + key_id_type block_signing_key_id(block_signing_key_object_id.instance()); + const auto& new_witness_object = db().create( [&]( witness_object& obj ){ obj.witness_account = op.witness_account; obj.vote_id = vote_id; - obj.signing_key = op.block_signing_key; + obj.signing_key = block_signing_key_id; obj.next_secret = op.initial_secret; obj.url = op.url; }); diff --git a/libraries/plugins/witness/witness.cpp b/libraries/plugins/witness/witness.cpp index 2d5b1c91..e561da77 100644 --- a/libraries/plugins/witness/witness.cpp +++ b/libraries/plugins/witness/witness.cpp @@ -22,6 +22,8 @@ #include #include +#include + #include using namespace graphene::witness_plugin; @@ -39,8 +41,8 @@ void witness_plugin::plugin_set_program_options( ("witness-id,w", bpo::value>()->composing()->multitoken(), "ID of witness controlled by this node (e.g. \"1.7.0\", quotes are required, may specify multiple times)") ("private-key", bpo::value>()->composing()->multitoken()-> - DEFAULT_VALUE_VECTOR(std::make_pair(chain::key_id_type(), fc::ecc::private_key::regenerate(fc::sha256::hash(std::string("nathan"))))), - "Tuple of [key ID, private key] (may specify multiple times)") + DEFAULT_VALUE_VECTOR(std::make_pair(chain::key_id_type(), graphene::utilities::key_to_wif(fc::ecc::private_key::regenerate(fc::sha256::hash(std::string("nathan")))))), + "Tuple of [key ID, WIF private key] (may specify multiple times)") ; config_file_options.add(command_line_options); } @@ -51,12 +53,35 @@ std::string witness_plugin::plugin_name()const } void witness_plugin::plugin_initialize(const boost::program_options::variables_map& options) -{ +{ try { _options = &options; LOAD_VALUE_SET(options, "witness-id", _witnesses, chain::witness_id_type) - //Define a type T which doesn't have a comma, as I can't put a comma in a macro argument - using T = std::pair; - LOAD_VALUE_SET(options, "private-key", _private_keys, T) + + if( options.count("private-key") ) + { + const std::vector key_id_to_wif_pair_strings = options["private-key"].as>(); + for (const std::string& key_id_to_wif_pair_string : key_id_to_wif_pair_strings) + { + auto key_id_to_wif_pair = graphene::app::dejsonify >(key_id_to_wif_pair_string); + fc::optional private_key = graphene::utilities::wif_to_key(key_id_to_wif_pair.second); + if (!private_key) + { + // the key isn't in WIF format; see if they are still passing the old native private key format. This is + // just here to ease the transition, can be removed soon + try + { + private_key = fc::variant(key_id_to_wif_pair.second).as(); + } + catch (const fc::exception&) + { + FC_THROW("Invalid WIF-format private key ${key_string}", ("key_string", key_id_to_wif_pair.second)); + } + } + _private_keys[key_id_to_wif_pair.first] = *private_key; + } + } + } + catch (const fc::exception& e){ edump((e)); throw; } } void witness_plugin::plugin_startup() diff --git a/libraries/utilities/key_conversion.cpp b/libraries/utilities/key_conversion.cpp index 1fff6776..6c8d8a5f 100644 --- a/libraries/utilities/key_conversion.cpp +++ b/libraries/utilities/key_conversion.cpp @@ -37,7 +37,15 @@ std::string key_to_wif(const fc::ecc::private_key& key) fc::optional wif_to_key( const std::string& wif_key ) { - std::vector wif_bytes = fc::from_base58(wif_key); + std::vector wif_bytes; + try + { + wif_bytes = fc::from_base58(wif_key); + } + catch (const fc::parse_error_exception&) + { + return fc::optional(); + } if (wif_bytes.size() < 5) return fc::optional(); std::vector key_bytes(wif_bytes.begin() + 1, wif_bytes.end() - 4); diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 67b4130d..dd424801 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -86,7 +86,8 @@ struct wallet_data // map of account_name -> base58_private_key for // incomplete account regs - map pending_account_registrations; + map > pending_account_registrations; + map pending_witness_registrations; string ws_server = "ws://localhost:8090"; string ws_user; @@ -258,7 +259,7 @@ class wallet_api * @ingroup Transaction Builder API */ void replace_operation_in_builder_transaction(transaction_handle_type handle, - int operation_index, + unsigned operation_index, const operation& new_op); /** * @ingroup Transaction Builder API @@ -812,15 +813,39 @@ class wallet_api signed_transaction create_delegate(string owner_account, bool broadcast = false); + /** Lists all witnesses registered in the blockchain. + * This returns a list of all account names that own witnesses, and the associated witness id, + * sorted by name. This lists witnesses whether they are currently voted in or not. + * + * Use the \c lowerbound and limit parameters to page through the list. To retrieve all witnesss, + * start by setting \c lowerbound to the empty string \c "", and then each iteration, pass + * the last witness name returned as the \c lowerbound for the next \c list_witnesss() call. + * + * @param lowerbound the name of the first witness to return. If the named witness does not exist, + * the list will start at the witness that comes after \c lowerbound + * @param limit the maximum number of witnesss to return (max: 1000) + * @returns a list of witnesss mapping witness names to witness ids + */ + map list_witnesses(const string& lowerbound, uint32_t limit); + + /** Returns information about the given witness. + * @param witness_name_or_id the name or id of the witness account owner, or the id of the witness + * @returns the information about the witness stored in the block chain + */ + witness_object get_witness(string owner_account); + /** Creates a witness object owned by the given account. * * An account can have at most one witness object. * * @param owner_account the name or id of the account which is creating the witness + * @param url a URL to include in the witness record in the blockchain. Clients may + * display this when showing a list of witnesses. May be blank. * @param broadcast true to broadcast the transaction on the network * @returns the signed transaction registering a witness */ signed_transaction create_witness(string owner_account, + string url, bool broadcast = false); /** Vote for a given delegate. @@ -919,7 +944,7 @@ FC_REFLECT( graphene::wallet::wallet_data, (my_accounts) (cipher_keys) (extra_keys) - (pending_account_registrations) + (pending_account_registrations)(pending_witness_registrations) (ws_server) (ws_user) (ws_password) @@ -968,6 +993,8 @@ FC_API( graphene::wallet::wallet_api, (settle_asset) (whitelist_account) (create_delegate) + (get_witness) + (list_witnesses) (create_witness) (vote_for_delegate) (vote_for_witness) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index ac63eb4b..0d649576 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -24,6 +24,8 @@ #include #include +#include + #include #include #include @@ -33,6 +35,7 @@ #include #include #include +#include #include #include @@ -86,11 +89,19 @@ public: void operator()(const asset_create_operation& op)const; }; -template< class T > -optional< T > maybe_id( const string& name_or_id ) +template +optional maybe_id( const string& name_or_id ) { if( std::isdigit( name_or_id.front() ) ) - return fc::variant(name_or_id).as(); + { + try + { + return fc::variant(name_or_id).as(); + } + catch (const fc::exception&) + { + } + } return optional(); } @@ -112,10 +123,8 @@ string address_to_shorthash( const address& addr ) return result; } -fc::ecc::private_key derive_private_key( - const std::string& prefix_string, - int sequence_number - ) +fc::ecc::private_key derive_private_key( const std::string& prefix_string, + int sequence_number ) { std::string sequence_string = std::to_string(sequence_number); fc::sha512 h = fc::sha512::hash(prefix_string + " " + sequence_string); @@ -188,26 +197,47 @@ private: { auto it = _wallet.pending_account_registrations.find( account.name ); FC_ASSERT( it != _wallet.pending_account_registrations.end() ); - if( !import_key( account.name, it->second ) ) - { - // somebody else beat our pending registration, there is - // nothing we can do except log it and move on - elog( "account ${name} registered by someone else first!", - ("name", account.name) ); - // might as well remove it from pending regs, - // because there is now no way this registration - // can become valid (even in the extremely rare - // possibility of migrating to a fork where the - // name is available, the user can always - // manually re-register) - } + for (const std::string& wif_key : it->second) + if( !import_key( account.name, wif_key ) ) + { + // somebody else beat our pending registration, there is + // nothing we can do except log it and move on + elog( "account ${name} registered by someone else first!", + ("name", account.name) ); + // might as well remove it from pending regs, + // because there is now no way this registration + // can become valid (even in the extremely rare + // possibility of migrating to a fork where the + // name is available, the user can always + // manually re-register) + } _wallet.pending_account_registrations.erase( it ); } + // after a witness registration succeeds, this saves the private key in the wallet permanently + // + void claim_registered_witness(const std::string& witness_name) + { + auto iter = _wallet.pending_witness_registrations.find(witness_name); + FC_ASSERT(iter != _wallet.pending_witness_registrations.end()); + std::string wif_key = iter->second; + + // get the list key id this key is registered with in the chain + fc::optional witness_private_key = wif_to_key(wif_key); + FC_ASSERT(witness_private_key); + + graphene::chain::address witness_key_address = graphene::chain::address(witness_private_key->get_public_key()); + std::vector registered_key_ids = _remote_db->get_keys_for_address(witness_key_address); + + for (const key_id_type& id : registered_key_ids) + _keys[id] = wif_key; + _wallet.pending_witness_registrations.erase(iter); + } + fc::mutex _resync_mutex; void resync() { - _resync_mutex.lock(); + fc::scoped_lock lock(_resync_mutex); // this method is used to update wallet_data annotations // e.g. wallet has been restarted and was not notified // of events while it was down @@ -216,27 +246,38 @@ private: // notification is received, should also be done here // "batch style" by querying the blockchain - if( _wallet.pending_account_registrations.size() > 0 ) + if( !_wallet.pending_account_registrations.empty() ) { - std::vector v_names; - v_names.reserve( _wallet.pending_account_registrations.size() ); - std::transform(_wallet.pending_account_registrations.begin(), _wallet.pending_account_registrations.end(), - std::back_inserter(v_names), [](const std::pair& p) { - return p.first; - }); + // make a vector of the account names pending registration + std::vector pending_account_names = boost::copy_range >(boost::adaptors::keys(_wallet.pending_account_registrations)); - std::vector< fc::optional< graphene::chain::account_object >> - v_accounts = _remote_db->lookup_account_names( v_names ); + // look those up on the blockchain + std::vector> + pending_account_objects = _remote_db->lookup_account_names( pending_account_names ); - for( fc::optional< graphene::chain::account_object > opt_account : v_accounts ) - { - if( ! opt_account.valid() ) - continue; - claim_registered_account(*opt_account); - } + // if any of them exist, claim them + for( const fc::optional& optional_account : pending_account_objects ) + if( optional_account ) + claim_registered_account(*optional_account); } + + if (!_wallet.pending_witness_registrations.empty()) + { + // make a vector of the owner accounts for witnesses pending registration + std::vector pending_witness_names = boost::copy_range >(boost::adaptors::keys(_wallet.pending_witness_registrations)); + + // look up the owners on the blockchain + std::vector> owner_account_objects = _remote_db->lookup_account_names(pending_witness_names); - _resync_mutex.unlock(); + // if any of them have registered witnesses, claim them + for( const fc::optional& optional_account : owner_account_objects ) + if (optional_account) + { + fc::optional witness_obj = _remote_db->get_witness_by_account(optional_account->id); + if (witness_obj) + claim_registered_witness(optional_account->name); + } + } } void enable_umask_protection() { @@ -430,6 +471,7 @@ public: FC_ASSERT(opt); return *opt; } + asset_id_type get_asset_id(string asset_symbol_or_id) const { FC_ASSERT( asset_symbol_or_id.size() > 0 ); @@ -440,10 +482,12 @@ public: FC_ASSERT( (opt_asset.size() > 0) && (opt_asset[0].valid()) ); return opt_asset[0]->id; } + string get_wallet_filename() const { return _wallet_filename; } + fc::ecc::private_key get_private_key(key_id_type id)const { auto it = _keys.find(id); @@ -453,6 +497,15 @@ public: FC_ASSERT( privkey ); return *privkey; } + + fc::ecc::private_key get_private_key_for_account(const account_object& account)const + { + vector active_keys = account.active.get_keys(); + if (active_keys.size() != 1) + FC_THROW("Expecting a simple authority with one active key"); + return get_private_key(active_keys.front()); + } + fc::ecc::public_key get_public_key(key_id_type id)const { vector> keys = _remote_db->get_keys( {id} ); @@ -481,6 +534,8 @@ public: if( item.first.type() == key_object_type ) keys.insert( item.first ); } + keys.insert(acnt.options.get_memo_key()); + auto opt_keys = _remote_db->get_keys( vector(keys.begin(),keys.end()) ); for( const fc::optional& opt_key : opt_keys ) { @@ -574,7 +629,7 @@ public: _builder_transactions[transaction_handle].operations.emplace_back(op); } void replace_operation_in_builder_transaction(transaction_handle_type handle, - int operation_index, + unsigned operation_index, const operation& new_op) { FC_ASSERT(_builder_transactions.count(handle)); @@ -738,6 +793,44 @@ public: return sign_transaction( tx, broadcast ); } FC_CAPTURE_AND_RETHROW( (name) ) } + + // This function generates derived keys starting with index 0 and keeps incrementing + // the index until it finds a key that isn't registered in the block chain. To be + // safer, it continues checking for a few more keys to make sure there wasn't a short gap + // caused by a failed registration or the like. + int find_first_unused_derived_key_index(const fc::ecc::private_key& parent_key) + { + int first_unused_index = 0; + int number_of_consecutive_unused_keys = 0; + for (int key_index = 0; ; ++key_index) + { + fc::ecc::private_key derived_private_key = derive_private_key(key_to_wif(parent_key), key_index); + graphene::chain::public_key_type derived_public_key = derived_private_key.get_public_key(); + graphene::chain::address derived_address(derived_public_key); + std::vector registered_keys = _remote_db->get_keys_for_address(derived_address); + if (registered_keys.empty()) + { + if (number_of_consecutive_unused_keys) + { + ++number_of_consecutive_unused_keys; + if (number_of_consecutive_unused_keys > 5) + return first_unused_index; + } + else + { + first_unused_index = key_index; + number_of_consecutive_unused_keys = 1; + } + } + else + { + // key_index is used + first_unused_index = 0; + number_of_consecutive_unused_keys = 0; + } + } + } + signed_transaction create_account_with_private_key(fc::ecc::private_key owner_privkey, string account_name, string registrar_account, @@ -745,22 +838,25 @@ public: bool broadcast = false, bool save_wallet = true) { try { - fc::ecc::private_key active_privkey = derive_private_key( key_to_wif(owner_privkey), 0); + int active_key_index = find_first_unused_derived_key_index(owner_privkey); + fc::ecc::private_key active_privkey = derive_private_key( key_to_wif(owner_privkey), active_key_index); + + int memo_key_index = find_first_unused_derived_key_index(active_privkey); + fc::ecc::private_key memo_privkey = derive_private_key( key_to_wif(active_privkey), memo_key_index); graphene::chain::public_key_type owner_pubkey = owner_privkey.get_public_key(); graphene::chain::public_key_type active_pubkey = active_privkey.get_public_key(); + graphene::chain::public_key_type memo_pubkey = memo_privkey.get_public_key(); account_create_operation account_create_op; // TODO: process when pay_from_account is ID - account_object registrar_account_object = - this->get_account( registrar_account ); + account_object registrar_account_object = get_account( registrar_account ); account_id_type registrar_account_id = registrar_account_object.id; - account_object referrer_account_object = - this->get_account( referrer_account ); + account_object referrer_account_object = get_account( referrer_account ); account_create_op.referrer = referrer_account_object.id; account_create_op.referrer_percent = referrer_account_object.referrer_rewards_percentage; @@ -773,18 +869,23 @@ public: active_key_create_op.fee_paying_account = registrar_account_id; active_key_create_op.key_data = active_pubkey; + key_create_operation memo_key_create_op; + memo_key_create_op.fee_paying_account = registrar_account_id; + memo_key_create_op.key_data = memo_pubkey; + // key_create_op.calculate_fee(db.current_fee_schedule()); // TODO: Check if keys already exist!!! relative_key_id_type owner_rkid(0); relative_key_id_type active_rkid(1); + relative_key_id_type memo_rkid(2); account_create_op.registrar = registrar_account_id; account_create_op.name = account_name; account_create_op.owner = authority(1, owner_rkid, 1); account_create_op.active = authority(1, active_rkid, 1); - account_create_op.options.memo_key = active_rkid; + account_create_op.options.memo_key = memo_rkid; // current_fee_schedule() // find_account(pay_from_account) @@ -795,6 +896,7 @@ public: tx.operations.push_back( owner_key_create_op ); tx.operations.push_back( active_key_create_op ); + tx.operations.push_back( memo_key_create_op ); tx.operations.push_back( account_create_op ); tx.visit( operation_set_fee( _remote_db->get_global_properties().parameters.current_fees ) ); @@ -817,7 +919,8 @@ public: // we do not insert owner_privkey here because // it is intended to only be used for key recovery - _wallet.pending_account_registrations[ account_name ] = key_to_wif( active_privkey ); + _wallet.pending_account_registrations[account_name].push_back(key_to_wif( active_privkey )); + _wallet.pending_account_registrations[account_name].push_back(key_to_wif( memo_privkey )); if( save_wallet ) save_wallet_file(); if( broadcast ) @@ -1083,19 +1186,77 @@ public: return sign_transaction( tx, broadcast ); } FC_CAPTURE_AND_RETHROW( (owner_account)(broadcast) ) } + witness_object get_witness(string owner_account) + { + try + { + fc::optional witness_id = maybe_id(owner_account); + if (witness_id) + { + std::vector ids_to_get; + ids_to_get.push_back(*witness_id); + std::vector> witness_objects = _remote_db->get_witnesses(ids_to_get); + if (witness_objects.front()) + return *witness_objects.front(); + FC_THROW("No witness is registered for id ${id}", ("id", owner_account)); + } + else + { + // then maybe it's the owner account + try + { + account_id_type owner_account_id = get_account_id(owner_account); + fc::optional witness = _remote_db->get_witness_by_account(owner_account_id); + if (witness) + return *witness; + else + FC_THROW("No witness is registered for account ${account}", ("account", owner_account)); + } + catch (const fc::exception&) + { + FC_THROW("No account or witness named ${account}", ("account", owner_account)); + } + } + } + FC_CAPTURE_AND_RETHROW( (owner_account) ) + } + signed_transaction create_witness(string owner_account, + string url, bool broadcast /* = false */) { try { + account_object witness_account = get_account(owner_account); + fc::ecc::private_key active_private_key = get_private_key_for_account(witness_account); + int witness_key_index = find_first_unused_derived_key_index(active_private_key); + fc::ecc::private_key witness_private_key = derive_private_key(key_to_wif(active_private_key), witness_key_index); + graphene::chain::public_key_type witness_public_key = witness_private_key.get_public_key(); + + key_create_operation witness_key_create_op; + witness_key_create_op.fee_paying_account = witness_account.id; + witness_key_create_op.key_data = witness_public_key; + + relative_key_id_type witness_relative_key_id(0); witness_create_operation witness_create_op; - witness_create_op.witness_account = get_account_id(owner_account); + witness_create_op.witness_account = witness_account.id; + witness_create_op.block_signing_key = witness_relative_key_id; + witness_create_op.url = url; + + secret_hash_type::encoder enc; + fc::raw::pack(enc, witness_private_key); + fc::raw::pack(enc, secret_hash_type()); + witness_create_op.initial_secret = secret_hash_type::hash(enc.result()); + if (_remote_db->get_witness_by_account(witness_create_op.witness_account)) FC_THROW("Account ${owner_account} is already a witness", ("owner_account", owner_account)); signed_transaction tx; + tx.operations.push_back( witness_key_create_op ); tx.operations.push_back( witness_create_op ); tx.visit( operation_set_fee( _remote_db->get_global_properties().parameters.current_fees ) ); tx.validate(); + + _wallet.pending_witness_registrations[owner_account] = key_to_wif(witness_private_key); return sign_transaction( tx, broadcast ); } FC_CAPTURE_AND_RETHROW( (owner_account)(broadcast) ) } @@ -1711,7 +1872,7 @@ void wallet_api::add_operation_to_builder_transaction(transaction_handle_type tr my->add_operation_to_builder_transaction(transaction_handle, op); } -void wallet_api::replace_operation_in_builder_transaction(transaction_handle_type handle, int operation_index, const operation& new_op) +void wallet_api::replace_operation_in_builder_transaction(transaction_handle_type handle, unsigned operation_index, const operation& new_op) { my->replace_operation_in_builder_transaction(handle, operation_index, new_op); } @@ -1917,10 +2078,21 @@ signed_transaction wallet_api::create_delegate(string owner_account, return my->create_delegate(owner_account, broadcast); } +map wallet_api::list_witnesses(const string& lowerbound, uint32_t limit) +{ + return my->_remote_db->lookup_witness_accounts(lowerbound, limit); +} + +witness_object wallet_api::get_witness(string owner_account) +{ + return my->get_witness(owner_account); +} + signed_transaction wallet_api::create_witness(string owner_account, + string url, bool broadcast /* = false */) { - return my->create_witness(owner_account, broadcast); + return my->create_witness(owner_account, url, broadcast); } signed_transaction wallet_api::vote_for_delegate(string voting_account, diff --git a/programs/cli_wallet/cookbook.md b/programs/cli_wallet/cookbook.md new file mode 100755 index 00000000..971d8dbc --- /dev/null +++ b/programs/cli_wallet/cookbook.md @@ -0,0 +1,195 @@ +# Graphene CLI Wallet Cookbook +### Running a Local Test Network + +Right now, there is no public testnet, so the only way to test is to run your +own private network. To do this, launch a witness node to generate blocks. In +the directory where you built your graphene distribution: + +```` +cd programs/witness_node +# if you have previously run a witness node, you may need to remove the old blockchain. +# at this early stage, new commits often make it impossible to reuse an old database +# rm -r witness_node_data_dir +./witness_node --rpc-endpoint --enable-stale-production --witness-id \""1.7.0"\" \""1.7.1"\" \""1.7.2"\" \""1.7.3"\" \""1.7.4"\" \""1.7.5"\" \""1.7.6"\" \""1.7.7"\" \""1.7.8"\" \""1.7.9"\" --private-key "[\"1.2.0\",\"5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3\"]" +```` +The initial genesis state has ten pre-configured delegates (1.7.0-9) that all +use the same private key to sign their blocks. Launching `witness_node` this +way allows you to act as all ten delegates. + +Now, in a second window, launch a `cli_wallet` process to interact with the +network. +``` +cd programs/cli_wallet +# similarly, if you have previously run a wallet, you may need to wipe out your +# old wallet +# rm wallet.json +./cli_wallet +``` +Before doing anything with the new wallet, set a password and unlock the +wallet. + +*Warning*: your passwords will be displayed on the screen. +``` +new >>> set_password my_password +locked >>> unlock my_password +unlocked >>> +``` + +### Account Management +To create a new account, you will need to start with an existing account with +some of the CORE asset that will pay the transaction fee registering your new +account. The account paying this fee will be the *Registrar*. + +In the initial genesis state, the account `nathan` has all of the money, so we +will use it as the registrar. +``` +# first, import the private key to take ownership of the 'nathan' account +unlocked >>> import_key "nathan" 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +unlocked >>> list_my_accounts +[{ + "id": "1.3.15", + ... + "name": "nathan", + ... +] +# before nathan can create other accounts, we need to upgrade it to a prime member. +unlocked >>> upgrade_account nathan true +unlocked >>> create_account_with_brain_key "this is the brain key for my account" my-account nathan nathan true +``` +Like most methods in the wallet, `create_account_with_brain_key`'s last +parameter is the boolean `broadcast`. This parameter tells the wallet whether +you want to publish the transaction on the network immediately, which is +usually what you want to do. If you pass false, it will just create the +transaction and sign it, and display it on the console, but it wouldn't be sent +out onto the network. This could be used to build up a multi-sig transaction +and collect the other signatures offline, or it could be used to construct a +transaction in a offline cold wallet that you could put on a flash drive and +broadcast from a machine connected to the network. Here, we'll always pass +`true` for the `broadcast` parameter. + +### Transferring Currency +Your newly-created account doesn't have any funds in it yet, the `nathan` +account still has all the money. To send some CORE from `nathan` to your +account, use the `transfer` command: +``` +unlocked >>> transfer nathan my-account 10000 CORE "have some CORE" true +``` +### Becoming a Witness +To become a witness and be able to produce blocks, you first need to create a +witness object that can be voted in. + +Note: If you want to experiment with things that require voting, be aware that +votes are only tallied once per day at the maintenance interval. For testing, +it's helpful to change the `GRAPHENE_DEFAULT_MAINTENANCE_INTERVAL` in +`libraries/chain/include/graphene/chain/config.hpp` to, say, 10 minutes. + +Before we get started, we can see the current list of witnesses voted in, which +will simply be the ten default witnesses: +``` +unlocked >>> get_global_properties +... + "active_witnesses": [ + "1.7.0", + "1.7.1", + "1.7.2", + "1.7.3", + "1.7.4", + "1.7.5", + "1.7.6", + "1.7.7", + "1.7.8", + "1.7.9" + ], +... +``` +Only lifetime members can become witnesses, so you must first upgrade to a +lifetime member. Upgrade and create our witness object. +``` +unlocked >>> upgrade_account my-account true +unlocked >>> create_witness my-account "http://foo.bar.com/" true +{ + "ref_block_num": 139, + "ref_block_prefix": 3692461913, + "relative_expiration": 3, + "operations": [[ + 21,{ + "fee": { + "amount": 0, + "asset_id": "1.4.0" + }, + "witness_account": "1.3.16", + "url": "http://foo.bar.com/", + "block_signing_key": "1.2.0", + "initial_secret": "00000000000000000000000000000000000000000000000000000000" + } + ] + ], + "signatures": [[ + "1.2.23", + "1f2ad5597af2ac4bf7a50f1eef2db49c9c0f7616718776624c2c09a2dd72a0c53a26e8c2bc928f783624c4632924330fc03f08345c8f40b9790efa2e4157184a37" + ] + ], + "extra_signatures": [] +} +``` +Our witness is registered, but it can't produce blocks because nobody has voted +it in. You can see the current list of active witnesses with +`get_global_properties`: +``` +unlocked >>> get_global_properties +{ + "active_witnesses": [ + "1.7.0", + "1.7.1", + "1.7.2", + "1.7.3", + "1.7.4", + "1.7.5", + "1.7.7", + "1.7.8", + "1.7.9" + ], + ... +``` +Now, we should vote our witness in. Vote all of the shares in both +`my-account` and `nathan` in favor of your new witness. +``` +unlocked >>> vote_for_witness my-account my-account true true +unlocked >>> vote_for_witness nathan my-account true true +``` +Now we wait until the next maintenance interval. +`get_dynamic_global_properties` tells us when that will be in +`next_maintenance_time`. Once the next maintenance interval passes, run +`get_global_properties` again and you should see that your new witness has been +voted in. + +Even though it's voted in, it isn't producing any blocks yet because we only +told the witness_node to produce blocks for 1.7.0 - 1.7.9 on the command line, +and it doesn't know the private key for the witness. Get the witness object to +find out its id and the key we need, then find the the private key. + +Warning: `dump_private_keys` will display your keys unencrypted on the +terminal, don't do this with someone looking over your shoulder. +``` +unlocked >>> get_witness my-account +{ + "id": "1.7.10", + ... + "signing_key": "1.2.25", + ... +} +unlocked >>> dump_private_keys +[[ + ... + ],[ + "1.2.25", + "5JGi7DM7J8fSTizZ4D9roNgd8dUc5pirUe9taxYCUUsnvQ4zCaQ" + ] +] +``` +Now we need to re-start the witness, so shut down the wallet (ctrl-d), and +shut down the witness (ctrl-c). Re-launch the witness, now mentioning the new +witness 1.7.0 and its key 1.2.25: +``` +./witness_node --rpc-endpoint --enable-stale-production --witness-id \""1.7.0"\" \""1.7.1"\" \""1.7.2"\" \""1.7.3"\" \""1.7.4"\" \""1.7.5"\" \""1.7.6"\" \""1.7.7"\" \""1.7.8"\" \""1.7.9"\" \""1.7.10"\" --private-key "[\"1.2.0\",\"5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3\"]" "[\"1.2.25\",\"5JGi7DM7J8fSTizZ4D9roNgd8dUc5pirUe9taxYCUUsnvQ4zCaQ\"]" +```