diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 6fe3f47883..71d2fd2d2e 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -58,7 +58,7 @@ add_library( graphene_chain protocol/fee_schedule.cpp protocol/confidential.cpp protocol/vote.cpp - + protocol/htlc.cpp genesis_state.cpp get_config.cpp @@ -77,6 +77,7 @@ add_library( graphene_chain vesting_balance_evaluator.cpp withdraw_permission_evaluator.cpp worker_evaluator.cpp + htlc_evaluator.cpp confidential_evaluator.cpp special_authority.cpp buyback.cpp diff --git a/libraries/chain/committee_member_evaluator.cpp b/libraries/chain/committee_member_evaluator.cpp index 4e7eb827e5..9a258e6543 100644 --- a/libraries/chain/committee_member_evaluator.cpp +++ b/libraries/chain/committee_member_evaluator.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -77,6 +78,9 @@ void_result committee_member_update_global_parameters_evaluator::do_evaluate(con { try { FC_ASSERT(trx_state->_is_proposed_trx); + FC_ASSERT( db().head_block_time() > HARDFORK_CORE_1468_TIME || !o.new_parameters.extensions.value.updatable_htlc_options.valid(), + "Unable to set HTLC parameters until hardfork." ); + return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index efc5562a89..20022ce4c4 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -584,6 +584,7 @@ void database::_apply_block( const signed_block& next_block ) clear_expired_transactions(); clear_expired_proposals(); clear_expired_orders(); + clear_expired_htlcs(); update_expired_feeds(); // this will update expired feeds and some core exchange rates update_core_exchange_rates(); // this will update remaining core exchange rates update_withdraw_permissions(); diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 263c5df3e5..69d0f68f52 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include @@ -60,6 +61,7 @@ #include #include #include +#include #include @@ -125,6 +127,9 @@ const uint8_t witness_object::type_id; const uint8_t worker_object::space_id; const uint8_t worker_object::type_id; +const uint8_t htlc_object::space_id; +const uint8_t htlc_object::type_id; + void database::initialize_evaluators() { @@ -173,6 +178,9 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); } void database::initialize_indexes() @@ -201,6 +209,7 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index< htlc_index> >(); //Implementation object indexes add_index< primary_index >(); @@ -216,7 +225,6 @@ void database::initialize_indexes() add_index< primary_index< special_authority_index > >(); add_index< primary_index< buyback_index > >(); add_index< primary_index >(); - add_index< primary_index< simple_index< fba_accumulator_object > > >(); } diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index ff44a177df..17e71e044f 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -256,6 +257,27 @@ struct get_impacted_account_visitor { _impacted.insert( op.fee_payer() ); // account_id } + void operator()( const htlc_create_operation& op ) + { + _impacted.insert( op.fee_payer() ); + _impacted.insert( op.to ); + } + void operator()( const htlc_redeem_operation& op ) + { + _impacted.insert( op.fee_payer() ); + } + void operator()( const htlc_redeemed_operation& op ) + { + _impacted.insert( op.from ); + } + void operator()( const htlc_extend_operation& op ) + { + _impacted.insert( op.fee_payer() ); + } + void operator()( const htlc_refund_operation& op ) + { + _impacted.insert( op.fee_payer() ); + } }; void graphene::chain::operation_get_impacted_accounts( const operation& op, flat_set& result ) @@ -344,6 +366,12 @@ void get_relevant_accounts( const object* obj, flat_set& accoun } case balance_object_type:{ /** these are free from any accounts */ break; + } case htlc_object_type:{ + const auto& htlc_obj = dynamic_cast(obj); + FC_ASSERT( htlc_obj != nullptr ); + accounts.insert( htlc_obj->from ); + accounts.insert( htlc_obj->to ); + break; } } } diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index 9a5bcad1eb..05aae9d058 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -558,4 +559,22 @@ void database::update_withdraw_permissions() remove(*permit_index.begin()); } +void database::clear_expired_htlcs() +{ + const auto& htlc_idx = get_index_type().indices().get(); + while ( htlc_idx.begin() != htlc_idx.end() + && htlc_idx.begin()->expiration <= head_block_time() ) + { + const htlc_object& obj = *htlc_idx.begin(); + adjust_balance( obj.from, obj.amount ); + // virtual op + htlc_refund_operation vop( obj.id, obj.from ); + vop.htlc_id = htlc_idx.begin()->id; + push_applied_operation( vop ); + + // remove the db object + remove( *htlc_idx.begin() ); + } +} + } } diff --git a/libraries/chain/hardfork.d/core-1468.hf b/libraries/chain/hardfork.d/core-1468.hf new file mode 100644 index 0000000000..a669d57c84 --- /dev/null +++ b/libraries/chain/hardfork.d/core-1468.hf @@ -0,0 +1,4 @@ +// HTLC implementation +#ifndef HARDFORK_CORE_1468_TIME +#define HARDFORK_CORE_1468_TIME (fc::time_point_sec( 1600000000 ) ) +#endif diff --git a/libraries/chain/htlc_evaluator.cpp b/libraries/chain/htlc_evaluator.cpp new file mode 100644 index 0000000000..218558a98d --- /dev/null +++ b/libraries/chain/htlc_evaluator.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2018 jmjatlanta and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include +#include + +namespace graphene { + namespace chain { + + optional get_committee_htlc_options(graphene::chain::database& db) + { + return db.get_global_properties().parameters.extensions.value.updatable_htlc_options; + } + + void_result htlc_create_evaluator::do_evaluate(const htlc_create_operation& o) + { + optional htlc_options = get_committee_htlc_options(db()); + + FC_ASSERT(htlc_options, "HTLC Committee options are not set."); + + // make sure the expiration is reasonable + FC_ASSERT( o.claim_period_seconds <= htlc_options->max_timeout_secs, "HTLC Timeout exceeds allowed length" ); + // make sure the preimage length is reasonable + FC_ASSERT( o.preimage_size <= htlc_options->max_preimage_size, "HTLC preimage length exceeds allowed length" ); + // make sure the sender has the funds for the HTLC + FC_ASSERT( db().get_balance( o.from, o.amount.asset_id) >= (o.amount), "Insufficient funds") ; + return void_result(); + } + + object_id_type htlc_create_evaluator::do_apply(const htlc_create_operation& o) + { + try { + graphene::chain::database& dbase = db(); + dbase.adjust_balance( o.from, -o.amount ); + + const htlc_object& esc = db().create([&dbase,&o]( htlc_object& esc ) { + esc.from = o.from; + esc.to = o.to; + esc.amount = o.amount; + esc.preimage_hash = o.preimage_hash; + esc.preimage_size = o.preimage_size; + esc.expiration = dbase.head_block_time() + o.claim_period_seconds; + }); + return esc.id; + + } FC_CAPTURE_AND_RETHROW( (o) ) + } + + class htlc_redeem_visitor + { + //private: + const std::vector& data; + public: + typedef bool result_type; + + htlc_redeem_visitor( const std::vector& preimage ) + : data( preimage ) {} + + template + bool operator()( const T& preimage_hash )const + { + return T::hash( (const char*)data.data(), (uint32_t) data.size() ) == preimage_hash; + } + }; + + void_result htlc_redeem_evaluator::do_evaluate(const htlc_redeem_operation& o) + { + htlc_obj = &db().get(o.htlc_id); + + FC_ASSERT(o.preimage.size() == htlc_obj->preimage_size, "Preimage size mismatch."); + + const htlc_redeem_visitor vtor( o.preimage ); + FC_ASSERT( htlc_obj->preimage_hash.visit( vtor ), "Provided preimage does not generate correct hash."); + + return void_result(); + } + + void_result htlc_redeem_evaluator::do_apply(const htlc_redeem_operation& o) + { + db().adjust_balance(htlc_obj->to, htlc_obj->amount); + // notify related parties + htlc_redeemed_operation virt_op( htlc_obj->id, htlc_obj->from, htlc_obj->to, htlc_obj->amount ); + db().push_applied_operation( virt_op ); + db().remove(*htlc_obj); + return void_result(); + } + + void_result htlc_extend_evaluator::do_evaluate(const htlc_extend_operation& o) + { + htlc_obj = &db().get(o.htlc_id); + return void_result(); + } + + void_result htlc_extend_evaluator::do_apply(const htlc_extend_operation& o) + { + db().modify(*htlc_obj, [&o](htlc_object& db_obj) { + db_obj.expiration += o.seconds_to_add; + }); + + return void_result(); + } + + } // namespace chain +} // namespace graphene diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index f0fb8e11c6..5ad65950bc 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -466,6 +466,7 @@ namespace graphene { namespace chain { void update_withdraw_permissions(); bool check_for_blackswan( const asset_object& mia, bool enable_black_swan = true, const asset_bitasset_data_object* bitasset_ptr = nullptr ); + void clear_expired_htlcs(); ///Steps performed only at maintenance intervals ///@{ diff --git a/libraries/chain/include/graphene/chain/htlc_evaluator.hpp b/libraries/chain/include/graphene/chain/htlc_evaluator.hpp new file mode 100644 index 0000000000..08a24b2bdc --- /dev/null +++ b/libraries/chain/include/graphene/chain/htlc_evaluator.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018 jmjatlanta and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include + +namespace graphene { + namespace chain { + + class htlc_create_evaluator : public evaluator + { + public: + typedef htlc_create_operation operation_type; + + void_result do_evaluate( const htlc_create_operation& o); + object_id_type do_apply( const htlc_create_operation& o); + }; + + class htlc_redeem_evaluator : public evaluator + { + public: + typedef htlc_redeem_operation operation_type; + + void_result do_evaluate( const htlc_redeem_operation& o); + void_result do_apply( const htlc_redeem_operation& o); + const htlc_object* htlc_obj = nullptr; + }; + + class htlc_extend_evaluator : public evaluator + { + public: + typedef htlc_extend_operation operation_type; + + void_result do_evaluate( const htlc_extend_operation& o); + void_result do_apply( const htlc_extend_operation& o); + const htlc_object* htlc_obj = nullptr; + }; + } // namespace graphene +} // namespace graphene diff --git a/libraries/chain/include/graphene/chain/htlc_object.hpp b/libraries/chain/include/graphene/chain/htlc_object.hpp new file mode 100644 index 0000000000..e9c4515042 --- /dev/null +++ b/libraries/chain/include/graphene/chain/htlc_object.hpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018 jmjatlanta and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + + /** + * @brief database object to store HTLCs + * + * This object is stored in the database while an HTLC is active. The HTLC will + * become inactive at expiration or when unlocked via the preimage. + */ + class htlc_object : public graphene::db::abstract_object { + public: + // uniquely identify this object in the database + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = htlc_object_type; + + account_id_type from; + account_id_type to; + asset amount; + fc::time_point_sec expiration; + htlc_hash preimage_hash; + uint16_t preimage_size; + }; + + struct by_from_id; + struct by_expiration; + typedef multi_index_container< + htlc_object, + indexed_by< + ordered_unique< tag< by_id >, member< object, object_id_type, &object::id > >, + + ordered_unique< tag< by_expiration >, + composite_key< htlc_object, + member< htlc_object, fc::time_point_sec, &htlc_object::expiration >, + member< object, object_id_type, &object::id > > >, + + ordered_unique< tag< by_from_id >, + composite_key< htlc_object, + member< htlc_object, account_id_type, &htlc_object::from >, + member< object, object_id_type, &object::id > > > + > + + > htlc_object_index_type; + + typedef generic_index< htlc_object, htlc_object_index_type > htlc_index; + +} } // namespace graphene::chain + +FC_REFLECT_DERIVED( graphene::chain::htlc_object, (graphene::db::object), + (from)(to)(amount)(expiration) + (preimage_hash)(preimage_size) ); diff --git a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp index 695d9541ee..dba8281305 100644 --- a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp +++ b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp @@ -30,7 +30,12 @@ namespace graphene { namespace chain { struct fee_schedule; } } namespace graphene { namespace chain { - typedef static_variant<> parameter_extension; + struct htlc_options + { + uint32_t max_timeout_secs; + uint32_t max_preimage_size; + }; + struct chain_parameters { /** using a smart ref breaks the circular dependency created between operations and the fee schedule */ @@ -63,14 +68,30 @@ namespace graphene { namespace chain { uint16_t accounts_per_fee_scale = GRAPHENE_DEFAULT_ACCOUNTS_PER_FEE_SCALE; ///< number of accounts between fee scalings uint8_t account_fee_scale_bitshifts = GRAPHENE_DEFAULT_ACCOUNT_FEE_SCALE_BITSHIFTS; ///< number of times to left bitshift account registration fee at each scaling uint8_t max_authority_depth = GRAPHENE_MAX_SIG_CHECK_DEPTH; - extensions_type extensions; + + struct ext + { + optional< htlc_options > updatable_htlc_options; + }; + + extension extensions; /** defined in fee_schedule.cpp */ void validate()const; + }; } } // graphene::chain +FC_REFLECT( graphene::chain::htlc_options, + (max_timeout_secs) + (max_preimage_size) +) + +FC_REFLECT( graphene::chain::chain_parameters::ext, + (updatable_htlc_options) +) + FC_REFLECT( graphene::chain::chain_parameters, (current_fees) (block_interval) diff --git a/libraries/chain/include/graphene/chain/protocol/fee_schedule.hpp b/libraries/chain/include/graphene/chain/protocol/fee_schedule.hpp index a08ee98a2c..e578f1d9f4 100644 --- a/libraries/chain/include/graphene/chain/protocol/fee_schedule.hpp +++ b/libraries/chain/include/graphene/chain/protocol/fee_schedule.hpp @@ -107,6 +107,46 @@ namespace graphene { namespace chain { } }; + template<> + class fee_helper { + public: + const htlc_create_operation::fee_parameters_type& cget(const flat_set& parameters)const + { + auto itr = parameters.find( htlc_create_operation::fee_parameters_type() ); + if ( itr != parameters.end() ) + return itr->get(); + + static htlc_create_operation::fee_parameters_type htlc_create_operation_fee_dummy; + return htlc_create_operation_fee_dummy; + } + }; + + template<> + class fee_helper { + public: + const htlc_redeem_operation::fee_parameters_type& cget(const flat_set& parameters)const + { + auto itr = parameters.find( htlc_redeem_operation::fee_parameters_type() ); + if ( itr != parameters.end() ) + return itr->get(); + + static htlc_redeem_operation::fee_parameters_type htlc_redeem_operation_fee_dummy; + return htlc_redeem_operation_fee_dummy; + } + }; + template<> + class fee_helper { + public: + const htlc_extend_operation::fee_parameters_type& cget(const flat_set& parameters)const + { + auto itr = parameters.find( htlc_extend_operation::fee_parameters_type() ); + if ( itr != parameters.end() ) + return itr->get(); + + static htlc_extend_operation::fee_parameters_type htlc_extend_operation_fee_dummy; + return htlc_extend_operation_fee_dummy; + } + }; /** * @brief contains all of the parameters necessary to calculate the fee for any operation */ @@ -140,6 +180,12 @@ namespace graphene { namespace chain { { return fee_helper().get(parameters); } + template + const bool exists()const + { + auto itr = parameters.find(typename Operation::fee_parameters_type()); + return itr != parameters.end(); + } /** * @note must be sorted by fee_parameters.which() and have no duplicates diff --git a/libraries/chain/include/graphene/chain/protocol/htlc.hpp b/libraries/chain/include/graphene/chain/protocol/htlc.hpp new file mode 100644 index 0000000000..ef5f2b1469 --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/htlc.hpp @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2018 jmjatlanta and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include // std::max + +namespace graphene { + namespace chain { + + typedef fc::ripemd160 htlc_algo_ripemd160; + typedef fc::sha1 htlc_algo_sha1; + typedef fc::sha256 htlc_algo_sha256; + + typedef fc::static_variant< + htlc_algo_ripemd160, + htlc_algo_sha1, + htlc_algo_sha256 + > htlc_hash; + + struct htlc_create_operation : public base_operation + { + struct fee_parameters_type { + uint64_t fee = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; + uint64_t fee_per_day = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; + }; + // paid to network + asset fee; + // where the held monies are to come from + account_id_type from; + // where the held monies will go if the preimage is provided + account_id_type to; + // the amount to hold + asset amount; + // the (typed) hash of the preimage + htlc_hash preimage_hash; + // the size of the preimage + uint16_t preimage_size; + // The time the funds will be returned to the source if not claimed + uint32_t claim_period_seconds; + // for future expansion + extensions_type extensions; + + /*** + * @brief Does simple validation of this object + */ + void validate()const; + + /** + * @brief who will pay the fee + */ + account_id_type fee_payer()const { return from; } + + /**** + * @brief calculates the fee to be paid for this operation + */ + share_type calculate_fee(const fee_parameters_type& fee_params)const; + }; + + struct htlc_redeem_operation : public base_operation + { + struct fee_parameters_type { + uint64_t fee = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; + uint64_t fee_per_kb = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; + }; + + // paid to network + asset fee; + // the object we are attempting to update + htlc_id_type htlc_id; + // who is attempting to update the transaction + account_id_type redeemer; + // the preimage (not used if after epoch timeout) + std::vector preimage; + // for future expansion + extensions_type extensions; + + /*** + * @brief Perform obvious checks to validate this object + */ + void validate()const; + + /** + * @brief Who is to pay the fee + */ + account_id_type fee_payer()const { return redeemer; } + + /**** + * @brief calculates the fee to be paid for this operation + */ + share_type calculate_fee(const fee_parameters_type& fee_params)const; + }; + + /** + * virtual op to assist with notifying related parties + */ + struct htlc_redeemed_operation : public base_operation + { + struct fee_parameters_type {}; + + htlc_redeemed_operation() {} + htlc_redeemed_operation( htlc_id_type htlc_id, account_id_type from, account_id_type to, asset amount) : + htlc_id(htlc_id), from(from), to(to), amount(amount) {} + + account_id_type fee_payer()const { return to; } + void validate()const { FC_ASSERT( !"virtual operation" ); } + + share_type calculate_fee(const fee_parameters_type& k)const { return 0; } + + htlc_id_type htlc_id; + account_id_type from, to; + asset amount; + asset fee; + }; + + struct htlc_extend_operation : public base_operation + { + struct fee_parameters_type { + uint64_t fee = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; + uint64_t fee_per_day = 1 * GRAPHENE_BLOCKCHAIN_PRECISION; + }; + + // paid to network + asset fee; + // the object we are attempting to update + htlc_id_type htlc_id; + // who is attempting to update the transaction + account_id_type update_issuer; + // how much to add + uint32_t seconds_to_add; + // for future expansion + extensions_type extensions; + + /*** + * @brief Perform obvious checks to validate this object + */ + void validate()const; + + /** + * @brief Who is to pay the fee + */ + account_id_type fee_payer()const { return update_issuer; } + + /**** + * @brief calculates the fee to be paid for this operation + */ + share_type calculate_fee(const fee_parameters_type& fee_params)const; + }; + + struct htlc_refund_operation : public base_operation + { + struct fee_parameters_type {}; + + htlc_refund_operation(){} + htlc_refund_operation( const htlc_id_type& htlc_id, const account_id_type& to ) + : htlc_id(htlc_id), to(to) {} + + account_id_type fee_payer()const { return to; } + void validate()const { FC_ASSERT( !"virtual operation" ); } + + /// This is a virtual operation; there is no fee + share_type calculate_fee(const fee_parameters_type& k)const { return 0; } + + htlc_id_type htlc_id; + account_id_type to; + asset fee; + }; + } +} + +FC_REFLECT_TYPENAME( graphene::chain::htlc_hash ) + +FC_REFLECT( graphene::chain::htlc_create_operation::fee_parameters_type, (fee) (fee_per_day) ) +FC_REFLECT( graphene::chain::htlc_redeem_operation::fee_parameters_type, (fee) (fee_per_kb) ) +FC_REFLECT( graphene::chain::htlc_redeemed_operation::fee_parameters_type, ) // VIRTUAL +FC_REFLECT( graphene::chain::htlc_extend_operation::fee_parameters_type, (fee) (fee_per_day)) +FC_REFLECT( graphene::chain::htlc_refund_operation::fee_parameters_type, ) // VIRTUAL + +FC_REFLECT( graphene::chain::htlc_create_operation, + (fee)(from)(to)(amount)(preimage_hash)(preimage_size)(claim_period_seconds)(extensions)) +FC_REFLECT( graphene::chain::htlc_redeem_operation, (fee)(htlc_id)(redeemer)(preimage)(extensions)) +FC_REFLECT( graphene::chain::htlc_redeemed_operation, (fee)(htlc_id)(from)(to)(amount) ) +FC_REFLECT( graphene::chain::htlc_extend_operation, (fee)(htlc_id)(update_issuer)(seconds_to_add)(extensions)) +FC_REFLECT( graphene::chain::htlc_refund_operation, (fee)(htlc_id)(to)) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index de2cfa7fd9..9b5ffabb37 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -38,6 +38,7 @@ #include #include #include +#include namespace graphene { namespace chain { @@ -95,7 +96,12 @@ namespace graphene { namespace chain { bid_collateral_operation, execute_bid_operation, // VIRTUAL asset_claim_pool_operation, - asset_update_issuer_operation + asset_update_issuer_operation, + htlc_create_operation, + htlc_redeem_operation, + htlc_redeemed_operation, // VIRTUAL + htlc_extend_operation, + htlc_refund_operation // VIRTUAL > operation; /// @} // operations group diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index 06ab7ff291..a9202c32a8 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -36,6 +36,23 @@ #include +// TODO: move this to fc +#include +FC_REFLECT_TYPENAME( fc::sha1 ) +namespace fc { namespace raw { + template + inline void pack( T& ds, const fc::sha1& ep, uint32_t _max_depth = 1 ) { + ds << ep; + } + + template + inline void unpack( T& ds, sha1& ep, uint32_t _max_depth = 1 ) { + ds >> ep; + } + +} } +// /TODO: move to fc + #include #include #include @@ -140,6 +157,7 @@ namespace graphene { namespace chain { vesting_balance_object_type, worker_object_type, balance_object_type, + htlc_object_type, OBJECT_TYPE_COUNT ///< Sentry value which contains the number of different object types }; @@ -182,6 +200,7 @@ namespace graphene { namespace chain { class worker_object; class balance_object; class blinded_balance_object; + class htlc_object; typedef object_id< protocol_ids, account_object_type, account_object> account_id_type; typedef object_id< protocol_ids, asset_object_type, asset_object> asset_id_type; @@ -197,6 +216,7 @@ namespace graphene { namespace chain { typedef object_id< protocol_ids, vesting_balance_object_type, vesting_balance_object> vesting_balance_id_type; typedef object_id< protocol_ids, worker_object_type, worker_object> worker_id_type; typedef object_id< protocol_ids, balance_object_type, balance_object> balance_id_type; + typedef object_id< protocol_ids, htlc_object_type, htlc_object> htlc_id_type; // implementation types class global_property_object; @@ -343,6 +363,7 @@ FC_REFLECT_ENUM( graphene::chain::object_type, (vesting_balance_object_type) (worker_object_type) (balance_object_type) + (htlc_object_type) (OBJECT_TYPE_COUNT) ) FC_REFLECT_ENUM( graphene::chain::impl_object_type, @@ -395,6 +416,7 @@ FC_REFLECT_TYPENAME( graphene::chain::special_authority_id_type ) FC_REFLECT_TYPENAME( graphene::chain::buyback_id_type ) FC_REFLECT_TYPENAME( graphene::chain::fba_accumulator_id_type ) FC_REFLECT_TYPENAME( graphene::chain::collateral_bid_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::htlc_id_type ) FC_REFLECT( graphene::chain::void_t, ) diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index 8124cc4fda..f943cc5ad9 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -94,6 +94,23 @@ struct proposal_operation_hardfork_visitor FC_ASSERT(!"Virtual operation"); } } + void operator()(const graphene::chain::committee_member_update_global_parameters_operation &op) const { + if (block_time < HARDFORK_CORE_1468_TIME) { + FC_ASSERT(!op.new_parameters.extensions.value.updatable_htlc_options.valid(), "Unable to set HTLC options before hardfork 1468"); + FC_ASSERT(!op.new_parameters.current_fees->exists()); + FC_ASSERT(!op.new_parameters.current_fees->exists()); + FC_ASSERT(!op.new_parameters.current_fees->exists()); + } + } + void operator()(const graphene::chain::htlc_create_operation &op) const { + FC_ASSERT( block_time >= HARDFORK_CORE_1468_TIME, "Not allowed until hardfork 1468" ); + } + void operator()(const graphene::chain::htlc_redeem_operation &op) const { + FC_ASSERT( block_time >= HARDFORK_CORE_1468_TIME, "Not allowed until hardfork 1468" ); + } + void operator()(const graphene::chain::htlc_extend_operation &op) const { + FC_ASSERT( block_time >= HARDFORK_CORE_1468_TIME, "Not allowed until hardfork 1468" ); + } // loop and self visit in proposals void operator()(const graphene::chain::proposal_create_operation &v) const { for (const op_wrapper &op : v.proposed_ops) diff --git a/libraries/chain/protocol/htlc.cpp b/libraries/chain/protocol/htlc.cpp new file mode 100644 index 0000000000..645feb6dbf --- /dev/null +++ b/libraries/chain/protocol/htlc.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2018 jmjatlanta and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +#define SECONDS_PER_DAY (60 * 60 * 24) + +namespace graphene { namespace chain { + + void htlc_create_operation::validate()const { + FC_ASSERT( fee.amount >= 0, "Fee amount should not be negative" ); + FC_ASSERT( amount.amount > 0, "HTLC amount should be greater than zero" ); + } + + share_type htlc_create_operation::calculate_fee( const fee_parameters_type& fee_params )const + { + uint64_t days = ( claim_period_seconds + SECONDS_PER_DAY - 1 ) / SECONDS_PER_DAY; + // multiply with overflow check + uint64_t per_day_fee = fee_params.fee_per_day * days; + FC_ASSERT( days == 0 || per_day_fee / days == fee_params.fee_per_day, "Fee calculation overflow" ); + return fee_params.fee + per_day_fee; + } + + void htlc_redeem_operation::validate()const { + FC_ASSERT( fee.amount >= 0, "Fee amount should not be negative" ); + } + + share_type htlc_redeem_operation::calculate_fee( const fee_parameters_type& fee_params )const + { + uint64_t kb = ( preimage.size() + 1023 ) / 1024; + uint64_t product = kb * fee_params.fee_per_kb; + FC_ASSERT( kb == 0 || product / kb == fee_params.fee_per_kb, "Fee calculation overflow"); + return fee_params.fee + product; + } + + void htlc_extend_operation::validate()const { + FC_ASSERT( fee.amount >= 0 , "Fee amount should not be negative"); + } + + share_type htlc_extend_operation::calculate_fee( const fee_parameters_type& fee_params )const + { + uint32_t days = ( seconds_to_add + SECONDS_PER_DAY - 1 ) / SECONDS_PER_DAY; + uint64_t per_day_fee = fee_params.fee_per_day * days; + FC_ASSERT( days == 0 || per_day_fee / days == fee_params.fee_per_day, "Fee calculation overflow" ); + return fee_params.fee + per_day_fee; + } +} } diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index ca13357f0c..e069fbcdc3 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -490,6 +490,13 @@ class wallet_api */ asset_bitasset_data_object get_bitasset_data(string asset_name_or_id)const; + /** + * Returns information about the given HTLC object. + * @param htlc_id the id of the HTLC object. + * @returns the information about the HTLC object + */ + variant get_htlc(string htlc_id) const; + /** Lookup the id of a named account. * @param account_name_or_id the name of the account to look up * @returns the id of the named account @@ -1448,6 +1455,44 @@ class wallet_api bool broadcast = false ); + /** + * Create a hashed time lock contract + * + * @param source The account that will reserve the funds (and pay the fee) + * @param destination The account that will receive the funds if the preimage is presented + * @param amount the amount of the asset that is to be traded + * @param asset_symbol The asset that is to be traded + * @param hash_algorithm the algorithm used to generate the hash from the preimage. Can be RIPEMD160, SHA1 or SHA256. + * @param preimage_hash the hash of the preimage + * @param preimage_size the size of the preimage in bytes + * @param claim_period_seconds how long after creation until the lock expires + * @param broadcast true if you wish to broadcast the transaction + */ + signed_transaction htlc_create( string source, string destination, string amount, string asset_symbol, + string hash_algorithm, const std::string& preimage_hash, size_t preimage_size, + const uint32_t claim_period_seconds, bool broadcast = false ); + + /**** + * Update a hashed time lock contract + * + * @param htlc_id The object identifier of the HTLC on the blockchain + * @param issuer Who is performing this operation (and paying the fee) + * @param preimage the preimage that should evaluate to the preimage_hash + */ + signed_transaction htlc_redeem( string htlc_id, string issuer, const std::string& preimage, + bool broadcast = false ); + + /***** + * Increase the timelock on an existing HTLC + * + * @param htlc_id The object identifier of the HTLC on the blockchain + * @param issuer Who is performing this operation (and paying the fee) + * @param seconds_to_add how many seconds to add to the existing timelock + * @param broadcast true to broadcast to the network + */ + signed_transaction htlc_extend(string htlc_id, string issuer, const uint32_t seconds_to_add, + bool broadcast = false); + /** * Get information about a vesting balance object. * @@ -1777,6 +1822,7 @@ FC_API( graphene::wallet::wallet_api, (update_asset) (update_asset_issuer) (update_bitasset) + (get_htlc) (update_asset_feed_producers) (publish_asset_feed) (issue_asset) @@ -1798,6 +1844,9 @@ FC_API( graphene::wallet::wallet_api, (update_witness) (create_worker) (update_worker_votes) + (htlc_create) + (htlc_redeem) + (htlc_extend) (get_vesting_balances) (withdraw_vesting) (vote_for_committee_member) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index ac33c180e9..0eabbe35d1 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -60,10 +60,12 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -114,14 +116,16 @@ struct operation_printer ostream& out; const wallet_api_impl& wallet; operation_result result; + operation_history_object hist; std::string fee(const asset& a) const; public: - operation_printer( ostream& out, const wallet_api_impl& wallet, const operation_result& r = operation_result() ) + operation_printer( ostream& out, const wallet_api_impl& wallet, const operation_history_object& obj ) : out(out), wallet(wallet), - result(r) + result(obj.result), + hist(obj) {} typedef std::string result_type; @@ -134,6 +138,8 @@ struct operation_printer std::string operator()(const account_create_operation& op)const; std::string operator()(const account_update_operation& op)const; std::string operator()(const asset_create_operation& op)const; + std::string operator()(const htlc_create_operation& op)const; + std::string operator()(const htlc_redeem_operation& op)const; }; template @@ -259,6 +265,27 @@ struct op_prototype_visitor } }; +class htlc_hash_to_string_visitor +{ +public: + typedef string result_type; + + result_type operator()( const fc::ripemd160& hash )const + { + return "RIPEMD160 " + hash.str(); + } + + result_type operator()( const fc::sha1& hash )const + { + return "SHA1 " + hash.str(); + } + + result_type operator()( const fc::sha256& hash )const + { + return "SHA256 " + hash.str(); + } +}; + class wallet_api_impl { public: @@ -645,6 +672,15 @@ class wallet_api_impl return *opt; } + htlc_object get_htlc(string htlc_id) const + { + htlc_id_type id; + fc::from_variant(htlc_id, id); + auto obj = _remote_db->get_objects( { id }).front(); + htlc_object htlc = obj.template as(GRAPHENE_MAX_NESTED_OBJECTS); + return htlc; + } + asset_id_type get_asset_id(string asset_symbol_or_id) const { FC_ASSERT( asset_symbol_or_id.size() > 0 ); @@ -1735,6 +1771,95 @@ class wallet_api_impl return sign_transaction( tx, broadcast ); } + static htlc_hash do_hash( const string& algorithm, const std::string& hash ) + { + string name_upper; + std::transform( algorithm.begin(), algorithm.end(), std::back_inserter(name_upper), ::toupper); + if( name_upper == "RIPEMD160" ) + return fc::ripemd160( hash ); + if( name_upper == "SHA256" ) + return fc::sha256( hash ); + if( name_upper == "SHA1" ) + return fc::sha1( hash ); + FC_THROW_EXCEPTION( fc::invalid_arg_exception, "Unknown algorithm '${a}'", ("a",algorithm) ); + } + + signed_transaction htlc_create( string source, string destination, string amount, string asset_symbol, + string hash_algorithm, const std::string& preimage_hash, size_t preimage_size, + const uint32_t claim_period_seconds, bool broadcast = false ) + { + try + { + FC_ASSERT( !self.is_locked() ); + fc::optional asset_obj = get_asset(asset_symbol); + FC_ASSERT(asset_obj, "Could not find asset matching ${asset}", ("asset", asset_symbol)); + + htlc_create_operation create_op; + create_op.from = get_account(source).id; + create_op.to = get_account(destination).id; + create_op.amount = asset_obj->amount_from_string(amount); + create_op.claim_period_seconds = claim_period_seconds; + create_op.preimage_hash = do_hash( hash_algorithm, preimage_hash ); + create_op.preimage_size = preimage_size; + + signed_transaction tx; + tx.operations.push_back(create_op); + set_operation_fees( tx, _remote_db->get_global_properties().parameters.current_fees); + tx.validate(); + + return sign_transaction(tx, broadcast); + } FC_CAPTURE_AND_RETHROW( (source)(destination)(amount)(asset_symbol)(hash_algorithm) + (preimage_hash)(preimage_size)(claim_period_seconds)(broadcast) ) + } + + signed_transaction htlc_redeem( string htlc_id, string issuer, const std::vector& preimage, bool broadcast ) + { + try + { + FC_ASSERT( !self.is_locked() ); + fc::optional htlc_obj = get_htlc(htlc_id); + FC_ASSERT(htlc_obj, "Could not find HTLC matching ${htlc}", ("htlc", htlc_id)); + + account_object issuer_obj = get_account(issuer); + + htlc_redeem_operation update_op; + update_op.htlc_id = htlc_obj->id; + update_op.redeemer = issuer_obj.id; + update_op.preimage = preimage; + + signed_transaction tx; + tx.operations.push_back(update_op); + set_operation_fees( tx, _remote_db->get_global_properties().parameters.current_fees); + tx.validate(); + + return sign_transaction(tx, broadcast); + } FC_CAPTURE_AND_RETHROW( (htlc_id)(issuer)(preimage)(broadcast) ) + } + + signed_transaction htlc_extend ( string htlc_id, string issuer, const uint32_t seconds_to_add, bool broadcast) + { + try + { + FC_ASSERT( !self.is_locked() ); + fc::optional htlc_obj = get_htlc(htlc_id); + FC_ASSERT(htlc_obj, "Could not find HTLC matching ${htlc}", ("htlc", htlc_id)); + + account_object issuer_obj = get_account(issuer); + + htlc_extend_operation update_op; + update_op.htlc_id = htlc_obj->id; + update_op.update_issuer = issuer_obj.id; + update_op.seconds_to_add = seconds_to_add; + + signed_transaction tx; + tx.operations.push_back(update_op); + set_operation_fees( tx, _remote_db->get_global_properties().parameters.current_fees); + tx.validate(); + + return sign_transaction(tx, broadcast); + } FC_CAPTURE_AND_RETHROW( (htlc_id)(issuer)(seconds_to_add)(broadcast) ) + } + vector< vesting_balance_object_with_info > get_vesting_balances( string account_name ) { try { fc::optional vbid = maybe_id( account_name ); @@ -2225,7 +2350,7 @@ class wallet_api_impl auto b = _remote_db->get_block_header(i.block_num); FC_ASSERT(b); ss << b->timestamp.to_iso_string() << " "; - i.op.visit(operation_printer(ss, *this, i.result)); + i.op.visit(operation_printer(ss, *this, i)); ss << " \n"; } @@ -2242,7 +2367,7 @@ class wallet_api_impl auto b = _remote_db->get_block_header(i.block_num); FC_ASSERT(b); ss << b->timestamp.to_iso_string() << " "; - i.op.visit(operation_printer(ss, *this, i.result)); + i.op.visit(operation_printer(ss, *this, i)); ss << " \n"; } @@ -2263,7 +2388,7 @@ class wallet_api_impl auto b = _remote_db->get_block_header(i.block_num); FC_ASSERT(b); ss << b->timestamp.to_iso_string() << " "; - i.op.visit(operation_printer(ss, *this, i.result)); + i.op.visit(operation_printer(ss, *this, i)); ss << " transaction_id : "; ss << d.transaction_id.str(); ss << " \n"; @@ -2305,7 +2430,7 @@ class wallet_api_impl { auto r = result.as( GRAPHENE_MAX_NESTED_OBJECTS ); std::stringstream ss; - r.trx.operations[0].visit( operation_printer( ss, *this, operation_result() ) ); + r.trx.operations[0].visit( operation_printer( ss, *this, operation_history_object() ) ); ss << "\n"; for( const auto& out : r.outputs ) { @@ -2318,7 +2443,7 @@ class wallet_api_impl { auto r = result.as( GRAPHENE_MAX_NESTED_OBJECTS ); std::stringstream ss; - r.trx.operations[0].visit( operation_printer( ss, *this, operation_result() ) ); + r.trx.operations[0].visit( operation_printer( ss, *this, operation_history_object() ) ); ss << "\n"; for( const auto& out : r.outputs ) { @@ -2882,6 +3007,41 @@ std::string operation_printer::operator()(const asset_create_operation& op) cons return fee(op.fee); } +std::string operation_printer::operator()(const htlc_redeem_operation& op) const +{ + out << "Redeem HTLC with database id " + << std::to_string(op.htlc_id.space_id) + << "." << std::to_string(op.htlc_id.type_id) + << "." << std::to_string((uint64_t)op.htlc_id.instance) + << " with preimage \""; + for (unsigned char c : op.preimage) + out << c; + out << "\""; + return fee(op.fee); +} + +std::string operation_printer::operator()(const htlc_create_operation& op) const +{ + static htlc_hash_to_string_visitor vtor; + + auto fee_asset = wallet.get_asset( op.fee.asset_id ); + auto to = wallet.get_account( op.to ); + operation_result_printer rprinter(wallet); + std::string database_id = result.visit(rprinter); + + out << "Create HTLC to " << to.name + << " with id " << database_id + << " preimage hash: [" + << op.preimage_hash.visit( vtor ) + << "] (Fee: " << fee_asset.amount_to_pretty_string( op.fee ) << ")"; + // determine if the block that the HTLC is in is before or after LIB + int32_t pending_blocks = hist.block_num - wallet.get_dynamic_global_properties().last_irreversible_block_num; + if (pending_blocks > 0) + out << " (pending " << std::to_string(pending_blocks) << " blocks)"; + + return ""; +} + std::string operation_result_printer::operator()(const void_result& x) const { return ""; @@ -3012,6 +3172,44 @@ uint64_t wallet_api::get_asset_count()const return my->_remote_db->get_asset_count(); } +signed_transaction wallet_api::htlc_create( string source, string destination, string amount, string asset_symbol, + string hash_algorithm, const std::string& preimage_hash, size_t preimage_size, + const uint32_t claim_period_seconds, bool broadcast) +{ + return my->htlc_create(source, destination, amount, asset_symbol, hash_algorithm, preimage_hash, preimage_size, + claim_period_seconds, broadcast); +} + +variant wallet_api::get_htlc(std::string htlc_id) const +{ + static detail::htlc_hash_to_string_visitor vtor; + + graphene::chain::htlc_object obj = my->get_htlc(htlc_id); + fc::mutable_variant_object ret_val; + ret_val["database_id"] = (std::string)obj.id; + ret_val["from"] = (std::string)((graphene::db::object_id_type)obj.from); + ret_val["to"] = (std::string)((graphene::db::object_id_type)obj.to); + ret_val["amount"] = obj.amount.amount.value; + ret_val["asset"] = (std::string)((graphene::db::object_id_type)obj.amount.asset_id); + ret_val["expiration"] = fc::get_approximate_relative_time_string(obj.expiration); + ret_val["preimage_hash"] = obj.preimage_hash.visit( vtor ); + ret_val["preimage_size"] = obj.preimage_size; + return ret_val; +} + +signed_transaction wallet_api::htlc_redeem( std::string htlc_id, std::string issuer, const std::string& preimage, + bool broadcast) +{ + + return my->htlc_redeem(htlc_id, issuer, std::vector(preimage.begin(), preimage.end()), broadcast); +} + +signed_transaction wallet_api::htlc_extend ( std::string htlc_id, std::string issuer, const uint32_t seconds_to_add, + bool broadcast) +{ + return my->htlc_extend(htlc_id, issuer, seconds_to_add, broadcast); +} + vector wallet_api::get_account_history(string name, int limit)const { vector result; @@ -3060,7 +3258,7 @@ vector wallet_api::get_account_history(string name, int limit) } } std::stringstream ss; - auto memo = o.op.visit(detail::operation_printer(ss, *my, o.result)); + auto memo = o.op.visit(detail::operation_printer(ss, *my, o)); result.push_back( operation_detail{ memo, ss.str(), o } ); } @@ -3108,7 +3306,7 @@ vector wallet_api::get_relative_account_history( start); for (auto &o : current) { std::stringstream ss; - auto memo = o.op.visit(detail::operation_printer(ss, *my, o.result)); + auto memo = o.op.visit(detail::operation_printer(ss, *my, o)); result.push_back(operation_detail{memo, ss.str(), o}); } if (current.size() < std::min(100, limit)) @@ -3152,7 +3350,7 @@ account_history_operation_detail wallet_api::get_account_history_by_operations( auto current = my->_remote_hist->get_account_history_by_operations(always_id, operation_types, start, min_limit); for (auto& obj : current.operation_history_objs) { std::stringstream ss; - auto memo = obj.op.visit(detail::operation_printer(ss, *my, obj.result)); + auto memo = obj.op.visit(detail::operation_printer(ss, *my, obj)); transaction_id_type transaction_id; auto block = get_block(obj.block_num); diff --git a/programs/build_helpers/member_enumerator.cpp b/programs/build_helpers/member_enumerator.cpp index 16452c5bf8..5dcc6156a3 100644 --- a/programs/build_helpers/member_enumerator.cpp +++ b/programs/build_helpers/member_enumerator.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include diff --git a/programs/js_operation_serializer/main.cpp b/programs/js_operation_serializer/main.cpp index e955f8d7fa..24a4065e64 100644 --- a/programs/js_operation_serializer/main.cpp +++ b/programs/js_operation_serializer/main.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include diff --git a/tests/cli/main.cpp b/tests/cli/main.cpp index 393dce4d83..7968f75349 100644 --- a/tests/cli/main.cpp +++ b/tests/cli/main.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #ifdef _WIN32 #ifndef _WIN32_WINNT @@ -49,6 +50,7 @@ #include #include #endif +#include #include @@ -143,13 +145,15 @@ std::shared_ptr start_application(fc::temp_directory /////////// /// Send a block to the db /// @param app the application +/// @param returned_block the signed block /// @returns true on success /////////// -bool generate_block(std::shared_ptr app) { +bool generate_block(std::shared_ptr app, graphene::chain::signed_block& returned_block) +{ try { fc::ecc::private_key committee_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan"))); auto db = app->chain_database(); - auto block_1 = db->generate_block( db->get_slot_time(1), + returned_block = db->generate_block( db->get_slot_time(1), db->get_scheduled_witness(1), committee_key, database::skip_nothing ); @@ -159,6 +163,12 @@ bool generate_block(std::shared_ptr app) { } } +bool generate_block(std::shared_ptr app) +{ + graphene::chain::signed_block returned_block; + return generate_block(app, returned_block); +} + /////////// /// @brief Skip intermediate blocks, and generate a maintenance block /// @param app the application @@ -566,3 +576,188 @@ BOOST_FIXTURE_TEST_CASE( account_history_pagination, cli_fixture ) throw; } } + +/////////////////////// +// Start a server and connect using the same calls as the CLI +// Create an HTLC +/////////////////////// +BOOST_AUTO_TEST_CASE( cli_create_htlc ) +{ + using namespace graphene::chain; + using namespace graphene::app; + std::shared_ptr app1; + try { + fc::temp_directory app_dir( graphene::utilities::temp_directory_path() ); + + int server_port_number = 0; + app1 = start_application(app_dir, server_port_number); + // set committee parameters + app1->chain_database()->modify(app1->chain_database()->get_global_properties(), [](global_property_object& p) { + graphene::chain::htlc_options params; + params.max_preimage_size = 1024; + params.max_timeout_secs = 60 * 60 * 24 * 28; + p.parameters.extensions.value.updatable_htlc_options = params; + }); + + // connect to the server + client_connection con(app1, app_dir, server_port_number); + + BOOST_TEST_MESSAGE("Setting wallet password"); + con.wallet_api_ptr->set_password("supersecret"); + con.wallet_api_ptr->unlock("supersecret"); + + // import Nathan account + BOOST_TEST_MESSAGE("Importing nathan key"); + std::vector nathan_keys{"5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"}; + BOOST_CHECK_EQUAL(nathan_keys[0], "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"); + BOOST_CHECK(con.wallet_api_ptr->import_key("nathan", nathan_keys[0])); + + BOOST_TEST_MESSAGE("Importing nathan's balance"); + std::vector import_txs = con.wallet_api_ptr->import_balance("nathan", nathan_keys, true); + account_object nathan_acct_before_upgrade = con.wallet_api_ptr->get_account("nathan"); + + // upgrade nathan + BOOST_TEST_MESSAGE("Upgrading Nathan to LTM"); + signed_transaction upgrade_tx = con.wallet_api_ptr->upgrade_account("nathan", true); + account_object nathan_acct_after_upgrade = con.wallet_api_ptr->get_account("nathan"); + + // verify that the upgrade was successful + BOOST_CHECK_PREDICATE( std::not_equal_to(), (nathan_acct_before_upgrade.membership_expiration_date.sec_since_epoch()) + (nathan_acct_after_upgrade.membership_expiration_date.sec_since_epoch()) ); + BOOST_CHECK(nathan_acct_after_upgrade.is_lifetime_member()); + + // Create new asset called BOBCOIN + try + { + graphene::chain::asset_options asset_ops; + asset_ops.max_supply = 1000000; + asset_ops.core_exchange_rate = price(asset(2),asset(1,asset_id_type(1))); + fc::optional bit_opts; + con.wallet_api_ptr->create_asset("nathan", "BOBCOIN", 5, asset_ops, bit_opts, true); + } + catch(exception& e) + { + BOOST_FAIL(e.what()); + } + catch(...) + { + BOOST_FAIL("Unknown exception creating BOBCOIN"); + } + + // create a new account for Alice + { + graphene::wallet::brain_key_info bki = con.wallet_api_ptr->suggest_brain_key(); + BOOST_CHECK(!bki.brain_priv_key.empty()); + signed_transaction create_acct_tx = con.wallet_api_ptr->create_account_with_brain_key(bki.brain_priv_key, "alice", + "nathan", "nathan", true); + // save the private key for this new account in the wallet file + BOOST_CHECK(con.wallet_api_ptr->import_key("alice", bki.wif_priv_key)); + con.wallet_api_ptr->save_wallet_file(con.wallet_filename); + // attempt to give alice some bitsahres + BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to alice"); + signed_transaction transfer_tx = con.wallet_api_ptr->transfer("nathan", "alice", "10000", "1.3.0", + "Here are some CORE token for your new account", true); + } + + // create a new account for Bob + { + graphene::wallet::brain_key_info bki = con.wallet_api_ptr->suggest_brain_key(); + BOOST_CHECK(!bki.brain_priv_key.empty()); + signed_transaction create_acct_tx = con.wallet_api_ptr->create_account_with_brain_key(bki.brain_priv_key, "bob", + "nathan", "nathan", true); + // save the private key for this new account in the wallet file + BOOST_CHECK(con.wallet_api_ptr->import_key("bob", bki.wif_priv_key)); + con.wallet_api_ptr->save_wallet_file(con.wallet_filename); + // attempt to give bob some bitsahres + BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to Bob"); + signed_transaction transfer_tx = con.wallet_api_ptr->transfer("nathan", "bob", "10000", "1.3.0", + "Here are some CORE token for your new account", true); + con.wallet_api_ptr->issue_asset("bob", "5", "BOBCOIN", "Here are your BOBCOINs", true); + } + + + BOOST_TEST_MESSAGE("Alice has agreed to buy 3 BOBCOIN from Bob for 3 BTS. Alice creates an HTLC"); + // create an HTLC + std::string preimage_string = "My Secret"; + fc::sha256 preimage_md = fc::sha256::hash(preimage_string); + std::stringstream ss; + for(int i = 0; i < preimage_md.data_size(); i++) + { + char d = preimage_md.data()[i]; + unsigned char uc = static_cast(d); + ss << std::setfill('0') << std::setw(2) << std::hex << (int)uc; + } + std::string hash_str = ss.str(); + BOOST_TEST_MESSAGE("Secret is " + preimage_string + " and hash is " + hash_str); + uint32_t timelock = fc::days(1).to_seconds(); + graphene::chain::signed_transaction result_tx + = con.wallet_api_ptr->htlc_create("alice", "bob", + "3", "1.3.0", "SHA256", hash_str, preimage_string.size(), timelock, true); + + // normally, a wallet would watch block production, and find the transaction. Here, we can cheat: + std::string alice_htlc_id_as_string; + { + BOOST_TEST_MESSAGE("The system is generating a block"); + graphene::chain::signed_block result_block; + BOOST_CHECK(generate_block(app1, result_block)); + + // get the ID: + htlc_id_type htlc_id = result_block.transactions[result_block.transactions.size()-1].operation_results[0].get(); + alice_htlc_id_as_string = (std::string)(object_id_type)htlc_id; + BOOST_TEST_MESSAGE("Alice shares the HTLC ID with Bob. The HTLC ID is: " + alice_htlc_id_as_string); + } + + // Bob can now look over Alice's HTLC, to see if it is what was agreed to. + BOOST_TEST_MESSAGE("Bob retrieves the HTLC Object by ID to examine it."); + auto alice_htlc = con.wallet_api_ptr->get_htlc(alice_htlc_id_as_string); + BOOST_TEST_MESSAGE("The HTLC Object is: " + fc::json::to_pretty_string(alice_htlc)); + + // Bob likes what he sees, so he creates an HTLC, using the info he retrieved from Alice's HTLC + con.wallet_api_ptr->htlc_create("bob", "alice", + "3", "BOBCOIN", "SHA256", hash_str, preimage_string.size(), timelock, true); + + // normally, a wallet would watch block production, and find the transaction. Here, we can cheat: + std::string bob_htlc_id_as_string; + { + BOOST_TEST_MESSAGE("The system is generating a block"); + graphene::chain::signed_block result_block; + BOOST_CHECK(generate_block(app1, result_block)); + + // get the ID: + htlc_id_type htlc_id = result_block.transactions[result_block.transactions.size()-1].operation_results[0].get(); + bob_htlc_id_as_string = (std::string)(object_id_type)htlc_id; + BOOST_TEST_MESSAGE("Bob shares the HTLC ID with Alice. The HTLC ID is: " + bob_htlc_id_as_string); + } + + // Alice can now look over Bob's HTLC, to see if it is what was agreed to: + BOOST_TEST_MESSAGE("Alice retrieves the HTLC Object by ID to examine it."); + auto bob_htlc = con.wallet_api_ptr->get_htlc(bob_htlc_id_as_string); + BOOST_TEST_MESSAGE("The HTLC Object is: " + fc::json::to_pretty_string(bob_htlc)); + + // Alice likes what she sees, so uses her preimage to get her BOBCOIN + { + BOOST_TEST_MESSAGE("Alice uses her preimage to retrieve the BOBCOIN"); + std::string secret = "My Secret"; + con.wallet_api_ptr->htlc_redeem(bob_htlc_id_as_string, "alice", secret, true); + BOOST_TEST_MESSAGE("The system is generating a block"); + BOOST_CHECK(generate_block(app1)); + } + + // TODO: Bob can look at Alice's history to see her preimage + // Bob can use the preimage to retrieve his BTS + { + BOOST_TEST_MESSAGE("Bob uses Alice's preimage to retrieve the BOBCOIN"); + std::string secret = "My Secret"; + con.wallet_api_ptr->htlc_redeem(alice_htlc_id_as_string, "bob", secret, true); + BOOST_TEST_MESSAGE("The system is generating a block"); + BOOST_CHECK(generate_block(app1)); + } + + // wait for everything to finish up + fc::usleep(fc::seconds(1)); + } catch( fc::exception& e ) { + edump((e.to_detail_string())); + throw; + } + app1->shutdown(); +} diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 534c03de33..d1e62ea197 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include @@ -285,6 +286,13 @@ void database_fixture::verify_asset_supplies( const database& db ) BOOST_CHECK_EQUAL(item.first(db).dynamic_asset_data_id(db).current_supply.value, item.second.value); } + // htlc + const auto& htlc_idx = db.get_index_type< htlc_index >().indices().get< by_id >(); + for( auto itr = htlc_idx.begin(); itr != htlc_idx.end(); ++itr ) + { + total_balances[itr->amount.asset_id] += itr->amount.amount; + } + for( const asset_object& asset_obj : db.get_index_type().indices() ) { BOOST_CHECK_EQUAL(total_balances[asset_obj.id].value, asset_obj.dynamic_asset_data_id(db).current_supply.value); diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index 78f7b65d19..7ece7ab41d 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -330,6 +330,10 @@ struct database_fixture { void transfer( account_id_type from, account_id_type to, const asset& amount, const asset& fee = asset() ); void transfer( const account_object& from, const account_object& to, const asset& amount, const asset& fee = asset() ); void fund_fee_pool( const account_object& from, const asset_object& asset_to_fund, const share_type amount ); + /** + * NOTE: This modifies the database directly. You will probably have to call this each time you + * finish creating a block + */ void enable_fees(); void change_fees( const flat_set< fee_parameters >& new_params, uint32_t new_scale = 0 ); void upgrade_to_lifetime_member( account_id_type account ); diff --git a/tests/tests/htlc_tests.cpp b/tests/tests/htlc_tests.cpp new file mode 100644 index 0000000000..98518f6484 --- /dev/null +++ b/tests/tests/htlc_tests.cpp @@ -0,0 +1,639 @@ +/* + * Copyright (c) 2018 jmjatlanta and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// below are for random bytes for htlc +#include +#include +#include +#include +#include +// for htlc timeout +#include +#include + +#include + +#include + +#include + +#include + +#include +#include + +#include +#include +#include + +#include + +#include "../common/database_fixture.hpp" + +using namespace graphene::chain; +using namespace graphene::chain::test; + +BOOST_FIXTURE_TEST_SUITE( htlc_tests, database_fixture ) + +void generate_random_preimage(uint16_t key_size, std::vector& vec) +{ + std::independent_bits_engine rbe; + std::generate(begin(vec), end(vec), std::ref(rbe)); + return; +} + +/**** + * Hash the preimage and put it in a vector + * @param preimage the preimage + * @returns a vector that cointains the sha256 hash of the preimage + */ +template +H hash_it(std::vector preimage) +{ + return H::hash( (char*)preimage.data(), preimage.size() ); +} + +flat_map< uint64_t, graphene::chain::fee_parameters > get_htlc_fee_parameters() +{ + flat_map ret_val; + + htlc_create_operation::fee_parameters_type create_param; + create_param.fee_per_day = 2 * GRAPHENE_BLOCKCHAIN_PRECISION; + create_param.fee = 2 * GRAPHENE_BLOCKCHAIN_PRECISION; + ret_val[((operation)htlc_create_operation()).which()] = create_param; + + htlc_redeem_operation::fee_parameters_type redeem_param; + redeem_param.fee = 2 * GRAPHENE_BLOCKCHAIN_PRECISION; + redeem_param.fee_per_kb = 2 * GRAPHENE_BLOCKCHAIN_PRECISION; + ret_val[((operation)htlc_redeem_operation()).which()] = redeem_param; + + htlc_extend_operation::fee_parameters_type extend_param; + extend_param.fee = 2 * GRAPHENE_BLOCKCHAIN_PRECISION; + extend_param.fee_per_day = 2 * GRAPHENE_BLOCKCHAIN_PRECISION; + ret_val[((operation)htlc_extend_operation()).which()] = extend_param; + + return ret_val; +} + +/**** + * @brief push through a proposal that sets htlc parameters and fees + * @param db_fixture the database connection + */ +void set_committee_parameters(database_fixture* db_fixture) +{ + // htlc fees + // get existing fee_schedule + const chain_parameters& existing_params = db_fixture->db.get_global_properties().parameters; + const fee_schedule_type& existing_fee_schedule = *(existing_params.current_fees); + // create a new fee_shedule + fee_schedule_type new_fee_schedule; + new_fee_schedule.scale = GRAPHENE_100_PERCENT; + // replace the old with the new + flat_map params_map = get_htlc_fee_parameters(); + for(auto param : existing_fee_schedule.parameters) + { + auto itr = params_map.find(param.which()); + if (itr == params_map.end()) + new_fee_schedule.parameters.insert(param); + else + { + new_fee_schedule.parameters.insert( (*itr).second); + } + } + // htlc parameters + proposal_create_operation cop = proposal_create_operation::committee_proposal( + db_fixture->db.get_global_properties().parameters, db_fixture->db.head_block_time()); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db_fixture->db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation uop; + graphene::chain::htlc_options new_params; + new_params.max_preimage_size = 19200; + new_params.max_timeout_secs = 60 * 60 * 24 * 28; + uop.new_parameters.extensions.value.updatable_htlc_options = new_params; + uop.new_parameters.current_fees = new_fee_schedule; + cop.proposed_ops.emplace_back(uop); + + db_fixture->trx.operations.push_back(cop); + graphene::chain::processed_transaction proc_trx =db_fixture->db.push_transaction(db_fixture->trx); + proposal_id_type good_proposal_id = proc_trx.operation_results[0].get(); + + proposal_update_operation puo; + puo.proposal = good_proposal_id; + puo.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + puo.active_approvals_to_add = { + db_fixture->get_account("init0").get_id(), db_fixture->get_account("init1").get_id(), + db_fixture->get_account("init2").get_id(), db_fixture->get_account("init3").get_id(), + db_fixture->get_account("init4").get_id(), db_fixture->get_account("init5").get_id(), + db_fixture->get_account("init6").get_id(), db_fixture->get_account("init7").get_id()}; + db_fixture->trx.operations.push_back(puo); + db_fixture->sign( db_fixture->trx, db_fixture->init_account_priv_key ); + db_fixture->db.push_transaction(db_fixture->trx); + db_fixture->trx.clear(); + + db_fixture->generate_blocks( good_proposal_id( db_fixture->db ).expiration_time + 5 ); + db_fixture->generate_blocks( db_fixture->db.get_dynamic_global_properties().next_maintenance_time ); + db_fixture->generate_block(); // get the maintenance skip slots out of the way + +} + +void advance_past_hardfork(database_fixture* db_fixture) +{ + db_fixture->generate_blocks(HARDFORK_CORE_1468_TIME); + set_expiration(db_fixture->db, db_fixture->trx); + set_committee_parameters(db_fixture); + set_expiration(db_fixture->db, db_fixture->trx); +} + +BOOST_AUTO_TEST_CASE( htlc_expires ) +{ +try { + ACTORS((alice)(bob)); + + int64_t init_balance(100 * GRAPHENE_BLOCKCHAIN_PRECISION); + + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + + advance_past_hardfork(this); + + uint16_t preimage_size = 256; + std::vector pre_image(256); + generate_random_preimage(preimage_size, pre_image); + + graphene::chain::htlc_id_type alice_htlc_id; + // cler everything out + generate_block(); + trx.clear(); + // Alice puts a contract on the blockchain + { + graphene::chain::htlc_create_operation create_operation; + BOOST_TEST_MESSAGE("Alice (who has 100 coins, is transferring 2 coins to Bob"); + create_operation.amount = graphene::chain::asset( 3 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.to = bob_id; + create_operation.claim_period_seconds = 60; + create_operation.preimage_hash = hash_it( pre_image ); + create_operation.preimage_size = preimage_size; + create_operation.from = alice_id; + create_operation.fee = db.get_global_properties().parameters.current_fees->calculate_fee(create_operation); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + graphene::chain::signed_block blk = generate_block(); + processed_transaction alice_trx = blk.transactions[0]; + alice_htlc_id = alice_trx.operation_results[0].get(); + generate_block(); + } + + // verify funds on hold... 100 - 3 = 97, minus the 4 coin fee = 93 + BOOST_CHECK_EQUAL( get_balance(alice_id, graphene::chain::asset_id_type()), 93 * GRAPHENE_BLOCKCHAIN_PRECISION ); + + // make sure Bob (or anyone) can see the details of the transaction + graphene::app::database_api db_api(db); + auto obj = db_api.get_objects( {alice_htlc_id }).front(); + graphene::chain::htlc_object htlc = obj.template as(GRAPHENE_MAX_NESTED_OBJECTS); + + // let it expire (wait for timeout) + generate_blocks( db.head_block_time() + fc::seconds(120) ); + // verify funds return (minus the fees) + BOOST_CHECK_EQUAL( get_balance(alice_id, graphene::chain::asset_id_type()), 96 * GRAPHENE_BLOCKCHAIN_PRECISION ); + // verify Bob cannot execute the contract after the fact +} FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( htlc_fulfilled ) +{ +try { + ACTORS((alice)(bob)); + + int64_t init_balance(100 * GRAPHENE_BLOCKCHAIN_PRECISION); + + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + transfer( committee_account, bob_id, graphene::chain::asset(init_balance) ); + + advance_past_hardfork(this); + + uint16_t preimage_size = 256; + std::vector pre_image(preimage_size); + generate_random_preimage(preimage_size, pre_image); + + graphene::chain::htlc_id_type alice_htlc_id; + // clear everything out + generate_block(); + trx.clear(); + // Alice puts a contract on the blockchain + { + graphene::chain::htlc_create_operation create_operation; + + create_operation.amount = graphene::chain::asset( 20 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.to = bob_id; + create_operation.claim_period_seconds = 86400; + create_operation.preimage_hash = hash_it( pre_image ); + create_operation.preimage_size = preimage_size; + create_operation.from = alice_id; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.operations.push_back( create_operation ); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + graphene::chain::signed_block blk = generate_block(); + processed_transaction alice_trx = blk.transactions[0]; + alice_htlc_id = alice_trx.operation_results[0].get(); + } + + // make sure Alice's money gets put on hold (100 - 20 - 4(fee) ) + BOOST_CHECK_EQUAL( get_balance( alice_id, graphene::chain::asset_id_type()), 76 * GRAPHENE_BLOCKCHAIN_PRECISION ); + + // extend the timeout so that Bob has more time + { + graphene::chain::htlc_extend_operation extend_operation; + extend_operation.htlc_id = alice_htlc_id; + extend_operation.seconds_to_add = 86400; + extend_operation.update_issuer = alice_id; + extend_operation.fee = db.current_fee_schedule().calculate_fee( extend_operation ); + trx.operations.push_back( extend_operation ); + sign( trx, alice_private_key ); + PUSH_TX( db, trx, ~0 ); + trx.clear(); + generate_blocks( db.head_block_time() + fc::seconds(87000) ); + set_expiration( db, trx ); + } + + // make sure Alice's money is still on hold, and account for extra fee + BOOST_CHECK_EQUAL( get_balance( alice_id, graphene::chain::asset_id_type()), 72 * GRAPHENE_BLOCKCHAIN_PRECISION ); + + // send a redeem operation to claim the funds + { + graphene::chain::htlc_redeem_operation update_operation; + update_operation.redeemer = bob_id; + update_operation.htlc_id = alice_htlc_id; + update_operation.preimage = pre_image; + update_operation.fee = db.current_fee_schedule().calculate_fee( update_operation ); + trx.operations.push_back( update_operation ); + sign(trx, bob_private_key); + PUSH_TX( db, trx, ~0 ); + generate_block(); + trx.clear(); + } + // verify funds end up in Bob's account (100 + 20 - 4(fee) ) + BOOST_CHECK_EQUAL( get_balance(bob_id, graphene::chain::asset_id_type()), 116 * GRAPHENE_BLOCKCHAIN_PRECISION ); + // verify funds remain out of Alice's acount ( 100 - 20 - 4 ) + BOOST_CHECK_EQUAL( get_balance(alice_id, graphene::chain::asset_id_type()), 72 * GRAPHENE_BLOCKCHAIN_PRECISION ); +} FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( other_peoples_money ) +{ +try { + advance_past_hardfork(this); + + ACTORS((alice)(bob)); + + int64_t init_balance(100 * GRAPHENE_BLOCKCHAIN_PRECISION ); + + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + + uint16_t preimage_size = 256; + std::vector pre_image(256); + generate_random_preimage(preimage_size, pre_image); + + graphene::chain::htlc_id_type alice_htlc_id; + // cler everything out + generate_block(); + trx.clear(); + // Bob attempts to put a contract on the blockchain using Alice's funds + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 1 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.to = bob_id; + create_operation.claim_period_seconds = 3; + create_operation.preimage_hash = hash_it( pre_image ); + create_operation.preimage_size = preimage_size; + create_operation.from = alice_id; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.operations.push_back(create_operation); + sign(trx, bob_private_key); + GRAPHENE_CHECK_THROW( PUSH_TX( db, trx ), fc::exception); + trx.clear(); + } + // now try the same but with Alice's signature (should work) + { + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 1 * GRAPHENE_BLOCKCHAIN_PRECISION ); + create_operation.to = bob_id; + create_operation.claim_period_seconds = 3; + create_operation.preimage_hash = hash_it( pre_image ); + create_operation.preimage_size = preimage_size; + create_operation.from = alice_id; + create_operation.fee = db.current_fee_schedule().calculate_fee( create_operation ); + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + PUSH_TX( db, trx ); + trx.clear(); + } +} FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( htlc_hardfork_test ) +{ + try { + { + // try to set committee parameters before hardfork + proposal_create_operation cop = proposal_create_operation::committee_proposal( + db.get_global_properties().parameters, db.head_block_time()); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation cmuop; + graphene::chain::htlc_options new_params; + new_params.max_preimage_size = 2048; + new_params.max_timeout_secs = 60 * 60 * 24 * 28; + cmuop.new_parameters.extensions.value.updatable_htlc_options = new_params; + cop.proposed_ops.emplace_back(cmuop); + trx.operations.push_back(cop); + // update with signatures + proposal_update_operation uop; + uop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + uop.active_approvals_to_add = {get_account("init0").get_id(), get_account("init1").get_id(), + get_account("init2").get_id(), get_account("init3").get_id(), + get_account("init4").get_id(), get_account("init5").get_id(), + get_account("init6").get_id(), get_account("init7").get_id()}; + trx.operations.push_back(uop); + sign( trx, init_account_priv_key ); + BOOST_TEST_MESSAGE("Sending proposal."); + GRAPHENE_CHECK_THROW(db.push_transaction(trx), fc::exception); + BOOST_TEST_MESSAGE("Verifying that proposal did not succeeed."); + BOOST_CHECK(!db.get_global_properties().parameters.extensions.value.updatable_htlc_options.valid()); + trx.clear(); + } + + { + BOOST_TEST_MESSAGE("Attempting to set HTLC fees before hard fork."); + + // get existing fee_schedule + const chain_parameters& existing_params = db.get_global_properties().parameters; + const fee_schedule_type& existing_fee_schedule = *(existing_params.current_fees); + // create a new fee_shedule + fee_schedule_type new_fee_schedule; + new_fee_schedule.scale = existing_fee_schedule.scale; + // replace the old with the new + flat_map params_map = get_htlc_fee_parameters(); + for(auto param : existing_fee_schedule.parameters) + { + auto itr = params_map.find(param.which()); + if (itr == params_map.end()) + new_fee_schedule.parameters.insert(param); + else + { + new_fee_schedule.parameters.insert( (*itr).second); + } + } + proposal_create_operation cop = proposal_create_operation::committee_proposal( + db.get_global_properties().parameters, db.head_block_time()); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation uop; + uop.new_parameters.current_fees = new_fee_schedule; + cop.proposed_ops.emplace_back(uop); + cop.fee = asset( 100000 ); + trx.operations.push_back( cop ); + GRAPHENE_CHECK_THROW(db.push_transaction( trx ), fc::exception); + trx.clear(); + } + + // now things should start working... + BOOST_TEST_MESSAGE("Advancing to HTLC hardfork time."); + advance_past_hardfork(this); + + proposal_id_type good_proposal_id; + BOOST_TEST_MESSAGE( "Creating a proposal to change the max_preimage_size to 2048 and set higher fees" ); + { + // get existing fee_schedule + const chain_parameters& existing_params = db.get_global_properties().parameters; + const fee_schedule_type& existing_fee_schedule = *(existing_params.current_fees); + // create a new fee_shedule + fee_schedule_type new_fee_schedule; + new_fee_schedule.scale = existing_fee_schedule.scale; + // replace the old with the new + flat_map params_map = get_htlc_fee_parameters(); + for(auto param : existing_fee_schedule.parameters) + { + auto itr = params_map.find(param.which()); + if (itr == params_map.end()) + new_fee_schedule.parameters.insert(param); + else + { + new_fee_schedule.parameters.insert( (*itr).second); + } + } + proposal_create_operation cop = proposal_create_operation::committee_proposal(db.get_global_properties().parameters, db.head_block_time()); + cop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + cop.expiration_time = db.head_block_time() + *cop.review_period_seconds + 10; + committee_member_update_global_parameters_operation uop; + graphene::chain::htlc_options new_params; + new_params.max_preimage_size = 2048; + new_params.max_timeout_secs = 60 * 60 * 24 * 28; + uop.new_parameters.extensions.value.updatable_htlc_options = new_params; + uop.new_parameters.current_fees = new_fee_schedule; + cop.proposed_ops.emplace_back(uop); + trx.operations.push_back(cop); + graphene::chain::processed_transaction proc_trx =db.push_transaction(trx); + good_proposal_id = proc_trx.operation_results[0].get(); + } + + BOOST_TEST_MESSAGE( "Updating proposal by signing with the committee_member private key" ); + { + proposal_update_operation uop; + uop.proposal = good_proposal_id; + uop.fee_paying_account = GRAPHENE_TEMP_ACCOUNT; + uop.active_approvals_to_add = {get_account("init0").get_id(), get_account("init1").get_id(), + get_account("init2").get_id(), get_account("init3").get_id(), + get_account("init4").get_id(), get_account("init5").get_id(), + get_account("init6").get_id(), get_account("init7").get_id()}; + trx.operations.push_back(uop); + sign( trx, init_account_priv_key ); + db.push_transaction(trx); + BOOST_CHECK(good_proposal_id(db).is_authorized_to_execute(db)); + } + BOOST_TEST_MESSAGE( "Verifying that the parameters didn't change immediately" ); + + BOOST_CHECK_EQUAL(db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_preimage_size, 19200u); + + BOOST_TEST_MESSAGE( "Generating blocks until proposal expires" ); + generate_blocks(good_proposal_id(db).expiration_time + 5); + BOOST_TEST_MESSAGE( "Verify that the parameters still have not changed" ); + BOOST_CHECK_EQUAL(db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_preimage_size, 19200u); + + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + + BOOST_TEST_MESSAGE( "Verify that the change has been implemented" ); + BOOST_CHECK_EQUAL(db.get_global_properties().parameters.extensions.value.updatable_htlc_options->max_preimage_size, 2048u); + BOOST_CHECK_EQUAL(db.get_global_properties().parameters.current_fees->get().fee, 2 * GRAPHENE_BLOCKCHAIN_PRECISION); + +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE( htlc_before_hardfork ) +{ try { + ACTORS((alice)(bob)); + + int64_t init_balance(100000); + + transfer( committee_account, alice_id, graphene::chain::asset(init_balance) ); + + uint16_t preimage_size = 256; + std::vector pre_image(256); + generate_random_preimage(preimage_size, pre_image); + + graphene::chain::htlc_id_type alice_htlc_id; + // clear everything out + generate_block(); + trx.clear(); + + // Alice tries to put a contract on the blockchain + { + graphene::chain::htlc_create_operation create_operation; + + create_operation.amount = graphene::chain::asset( 10000 ); + create_operation.to = bob_id; + create_operation.claim_period_seconds = 60; + create_operation.preimage_hash = hash_it( pre_image ); + create_operation.preimage_size = preimage_size; + create_operation.from = alice_id; + trx.operations.push_back(create_operation); + sign(trx, alice_private_key); + GRAPHENE_CHECK_THROW(PUSH_TX(db, trx, ~0), fc::exception); + trx.clear(); + } + + // Propose htlc_create + { + proposal_create_operation pco; + pco.expiration_time = db.head_block_time() + fc::minutes(1); + pco.fee_paying_account = alice_id; + + graphene::chain::htlc_create_operation create_operation; + create_operation.amount = graphene::chain::asset( 10000 ); + create_operation.to = bob_id; + create_operation.claim_period_seconds = 60; + create_operation.preimage_hash = hash_it( pre_image ); + create_operation.preimage_size = preimage_size; + create_operation.from = alice_id; + + pco.proposed_ops.emplace_back( create_operation ); + trx.operations.push_back( pco ); + GRAPHENE_CHECK_THROW( PUSH_TX( db, trx, ~0 ), fc::assert_exception ); + trx.clear(); + } + + // Propose htlc_redeem + { + proposal_create_operation pco; + pco.expiration_time = db.head_block_time() + fc::minutes(1); + pco.fee_paying_account = alice_id; + + graphene::chain::htlc_redeem_operation rop; + rop.redeemer = bob_id; + rop.htlc_id = alice_htlc_id; + string preimage_str = "Arglebargle"; + rop.preimage.insert( rop.preimage.begin(), preimage_str.begin(), preimage_str.end() ); + + pco.proposed_ops.emplace_back( rop ); + trx.operations.push_back( pco ); + GRAPHENE_CHECK_THROW( PUSH_TX( db, trx, ~0 ), fc::assert_exception ); + trx.clear(); + } + + // Propose htlc_extend + { + proposal_create_operation pco; + pco.expiration_time = db.head_block_time() + fc::minutes(1); + pco.fee_paying_account = alice_id; + + graphene::chain::htlc_extend_operation xop; + xop.htlc_id = alice_htlc_id; + xop.seconds_to_add = 100; + xop.update_issuer = alice_id; + + pco.proposed_ops.emplace_back( xop ); + trx.operations.push_back( pco ); + GRAPHENE_CHECK_THROW( PUSH_TX( db, trx, ~0 ), fc::assert_exception ); + trx.clear(); + } +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_CASE( fee_calculations ) +{ + // create + { + htlc_create_operation::fee_parameters_type create_fee; + create_fee.fee = 2; + create_fee.fee_per_day = 2; + htlc_create_operation create; + // no days + create.claim_period_seconds = 0; + BOOST_CHECK_EQUAL( create.calculate_fee(create_fee).value, 2 ); + // exactly 1 day + create.claim_period_seconds = 60 * 60 * 24; + BOOST_CHECK_EQUAL( create.calculate_fee(create_fee).value, 4 ); + // tad over a day + create.claim_period_seconds++; + BOOST_CHECK_EQUAL( create.calculate_fee(create_fee).value, 6 ); + } + // redeem + { + htlc_redeem_operation::fee_parameters_type redeem_fee; + redeem_fee.fee_per_kb = 2; + redeem_fee.fee = 2; + htlc_redeem_operation redeem; + // no preimage + redeem.preimage = std::vector(); + BOOST_CHECK_EQUAL( redeem.calculate_fee( redeem_fee ).value, 2 ) ; + // exactly 1KB + std::string test(1024, 'a'); + redeem.preimage = std::vector( test.begin(), test.end() ); + BOOST_CHECK_EQUAL( redeem.calculate_fee( redeem_fee ).value, 4 ) ; + // just 1 byte over 1KB + std::string larger(1025, 'a'); + redeem.preimage = std::vector( larger.begin(), larger.end() ); + BOOST_CHECK_EQUAL( redeem.calculate_fee( redeem_fee ).value, 6 ) ; + } + // extend + { + htlc_extend_operation::fee_parameters_type extend_fee; + extend_fee.fee = 2; + extend_fee.fee_per_day = 2; + htlc_extend_operation extend; + // no days + extend.seconds_to_add = 0; + BOOST_CHECK_EQUAL( extend.calculate_fee( extend_fee ).value, 2 ); + // exactly 1 day + extend.seconds_to_add = 60 * 60 * 24; + BOOST_CHECK_EQUAL( extend.calculate_fee( extend_fee ).value, 4 ); + // 1 day and 1 second + extend.seconds_to_add++; + BOOST_CHECK_EQUAL( extend.calculate_fee( extend_fee ).value, 6 ); + } +} + +BOOST_AUTO_TEST_SUITE_END()