diff --git a/README.md b/README.md index e80a332..a4f44d1 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Useful for displaying an estimated fee – To obtain exact fees, see "Creating a As mentioned, implementing the Send procedure without making use of one of our existing libraries or examples involves two bridge calls surrounded by server API calls, and mandatory reconstruction logic, and is simplified by various opportunities to pass values directly between the steps. -The values which must be passed between functions have (almost entirely) consistent names, simplifying integration. The only current exception is the name of the explicit `fee_actually_needed` which should be passed to step1 as the optional `passedIn_attemptAt_fee` after being received by calling step2 (see below). +The values which must be passed between functions have (almost entirely) consistent names, simplifying integration. The only current exceptions are the names of the explicit `fee_actually_needed` and `outs_to_mix_outs`, which should be passed to step1 as the optional `prior_attempt_size_calcd_fee` and `prior_attempt_unspent_outs_to_mix_outs` respectively after calling step2, when the transaction must be reconstructed (see below). ##### Examples * [JS implementation of SendFunds](https://github.com/mymonero/mymonero-core-js/blob/master/monero_utils/monero_sendingFunds_utils.js#L100) @@ -323,7 +323,8 @@ The values which must be passed between functions have (almost entirely) consist * `fork_version: UInt8String` * `unspent_outs: [UnspentOutput]` - fully parsed server response * `payment_id_string: Optional` - * `passedIn_attemptAt_fee: Optional` + * `prior_attempt_size_calcd_fee: Optional` + * `prior_attempt_unspent_outs_to_mix_outs: Optional>` - map of output public keys to mix outs, explained below * Returns: @@ -345,6 +346,28 @@ The values which must be passed between functions have (almost entirely) consist * `using_outs: [UnspentOutput]` passable directly to step2 * `final_total_wo_fee: UInt64String` +##### `pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts` + +`prior_attempt_unspent_outs_to_mix_outs` functions as a cache, tying used outputs to a constant set of mix outs across construction attempts. If the transaction construction steps need to be repated, you should re-use the same outs and their respective selected mix outs as used in prior attempts. This has 2 benefits: (1) it ensures the fee calculation is done correctly to prevent leaving transactions on chain that are fingerprintable by their fee, (2) no need to keep re-querying the server for decoys. This step should be called *after* receiving `mix_outs` from an API call, and before step2. The resulting `prior_attempt_unspent_outs_to_mix_outs_new` should become the new `prior_attempt_unspent_outs_to_mix_outs` in future tx construction attempts. + +* Args: + * `using_outs: [UnspentOutput]` returned by step1 + * `mix_outs_from_server: [MixAmountAndOuts]` defined below + * `prior_attempt_unspent_outs_to_mix_outs: Optional>` + +* Returns: + + * `err_code: CreateTransactionErrorCode`==`notEnoughUsableDecoysFound(22)` + + *OR* + + * `err_code: CreateTransactionErrorCode`==`tooManyDecoysRemaining(23)` + + *OR* + + * `mix_outs: [MixAmountAndOuts]` passable directly to step2 + * `prior_attempt_unspent_outs_to_mix_outs_new: Map` + ##### `send_step2__try_create_transaction` @@ -366,7 +389,7 @@ The values which must be passed between functions have (almost entirely) consist * `nettype_string: NettypeString` * `payment_id_string: Optional` - * `MixAmountAndOuts: Dictionary` decoys obtained from API call with + * `MixAmountAndOuts: Dictionary` decoys obtained from API call with, or from `pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts` * `amount: UInt64String` * `outputs: [MixOut]` where * `MixOut: Dictionary` with @@ -377,7 +400,7 @@ The values which must be passed between functions have (almost entirely) consist * Returns: * `tx_must_be_reconstructed: BoolString`==`true` - * `fee_actually_needed: UInt64String` pass this back to step1 as `passedIn_attemptAt_fee` + * `fee_actually_needed: UInt64String` pass this back to step1 as `prior_attempt_size_calcd_fee` *OR* diff --git a/src/monero_send_routine.cpp b/src/monero_send_routine.cpp index ef8a41b..f479683 100644 --- a/src/monero_send_routine.cpp +++ b/src/monero_send_routine.cpp @@ -86,10 +86,24 @@ LightwalletAPI_Req_GetUnspentOuts monero_send_routine::new__req_params__get_unsp }; } LightwalletAPI_Req_GetRandomOuts monero_send_routine::new__req_params__get_random_outs( - vector &step1__using_outs + const vector &step1__using_outs, + const optional &prior_attempt_unspent_outs_to_mix_outs ) { + // request decoys for any newly selected inputs + std::vector decoy_requests; + if (prior_attempt_unspent_outs_to_mix_outs) { + for (size_t i = 0; i < step1__using_outs.size(); ++i) { + // only need to request decoys for outs that were not already passed in + if (prior_attempt_unspent_outs_to_mix_outs->find(step1__using_outs[i].public_key) == prior_attempt_unspent_outs_to_mix_outs->end()) { + decoy_requests.push_back(step1__using_outs[i]); + } + } + } else { + decoy_requests = step1__using_outs; + } + vector decoy_req__amounts; - BOOST_FOREACH(SpendableOutput &using_out, step1__using_outs) + BOOST_FOREACH(SpendableOutput &using_out, decoy_requests) { if (using_out.rct != none && (*(using_out.rct)).size() > 0) { decoy_req__amounts.push_back("0"); @@ -320,15 +334,17 @@ struct _SendFunds_ConstructAndSendTx_Args const secret_key &sec_viewKey; const secret_key &sec_spendKey; // - optional passedIn_attemptAt_fee; + optional prior_attempt_size_calcd_fee; + optional prior_attempt_unspent_outs_to_mix_outs; size_t constructionAttempt; }; void _reenterable_construct_and_send_tx( const _SendFunds_ConstructAndSendTx_Args &args, // // re-entry params - optional passedIn_attemptAt_fee = none, - size_t constructionAttempt = 0 + optional prior_attempt_size_calcd_fee = none, + optional prior_attempt_unspent_outs_to_mix_outs = none, + size_t constructionAttempt = 0 ) { args.status_update_fn(calculatingFee); // @@ -347,7 +363,8 @@ void _reenterable_construct_and_send_tx( args.fee_per_b, args.fee_quantization_mask, // - passedIn_attemptAt_fee // use this for passing step2 "must-reconstruct" return values back in, i.e. re-entry; when nil, defaults to attempt at network min + prior_attempt_size_calcd_fee, // use this for passing step2 "must-reconstruct" return values back in, i.e. re-entry; when nil, defaults to attempt at network min + prior_attempt_unspent_outs_to_mix_outs // on re-entry, re-use the same outs and requested decoys, in order to land on the correct calculated fee ); if (step1_retVals.errCode != noError) { SendFunds_Error_RetVals error_retVals; @@ -360,18 +377,38 @@ void _reenterable_construct_and_send_tx( api_fetch_cb_fn get_random_outs_fn__cb_fn = [ args, step1_retVals, + prior_attempt_unspent_outs_to_mix_outs, constructionAttempt, use_fork_rules ] ( const property_tree::ptree &res ) -> void { - auto parsed_res = new__parsed_res__get_random_outs(res); + auto parsed_res = (res != boost::property_tree::ptree{}) + ? new__parsed_res__get_random_outs(res) + : LightwalletAPI_Res_GetRandomOuts{ boost::none/*err_msg*/, vector{}/*mix_outs*/ }; if (parsed_res.err_msg != none) { SendFunds_Error_RetVals error_retVals; error_retVals.explicit_errMsg = std::move(*(parsed_res.err_msg)); args.error_cb_fn(error_retVals); return; } + + Tie_Outs_to_Mix_Outs_RetVals tie_outs_to_mix_outs_retVals; + monero_transfer_utils::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts( + tie_outs_to_mix_outs_retVals, + // + step1_retVals.using_outs, + *(parsed_res.mix_outs), + // + prior_attempt_unspent_outs_to_mix_outs + ); + if (tie_outs_to_mix_outs_retVals.errCode != noError) { + SendFunds_Error_RetVals error_retVals; + error_retVals.errCode = tie_outs_to_mix_outs_retVals.errCode; + args.error_cb_fn(error_retVals); + return; + } + Send_Step2_RetVals step2_retVals; monero_transfer_utils::send_step2__try_create_transaction( step2_retVals, @@ -388,7 +425,7 @@ void _reenterable_construct_and_send_tx( step1_retVals.using_outs, args.fee_per_b, args.fee_quantization_mask, - *(parsed_res.mix_outs), + tie_outs_to_mix_outs_retVals.mix_outs, std::move(use_fork_rules), args.unlock_time, args.nettype @@ -411,7 +448,8 @@ void _reenterable_construct_and_send_tx( _reenterable_construct_and_send_tx( args, // - step2_retVals.fee_actually_needed, // -> reconstruction attempt's step1's passedIn_attemptAt_fee + step2_retVals.fee_actually_needed, // -> reconstruction attempt's step1's prior_attempt_size_calcd_fee + tie_outs_to_mix_outs_retVals.prior_attempt_unspent_outs_to_mix_outs_new, constructionAttempt+1 ); return; @@ -462,10 +500,16 @@ void _reenterable_construct_and_send_tx( // args.status_update_fn(fetchingDecoyOutputs); // - args.get_random_outs_fn( - new__req_params__get_random_outs(step1_retVals.using_outs), - get_random_outs_fn__cb_fn + // we won't need to make request for random outs every tx construction attempt, if already passed in out for all outs + auto req_params = new__req_params__get_random_outs( + step1_retVals.using_outs, + prior_attempt_unspent_outs_to_mix_outs ); + if (req_params.amounts.size() > 0) { + args.get_random_outs_fn(req_params, get_random_outs_fn__cb_fn); + } else { + get_random_outs_fn__cb_fn(boost::property_tree::ptree{}); + } } // // diff --git a/src/monero_send_routine.hpp b/src/monero_send_routine.hpp index 4b0e2a4..08a68af 100644 --- a/src/monero_send_routine.hpp +++ b/src/monero_send_routine.hpp @@ -115,7 +115,8 @@ namespace monero_send_routine return req_params_ss.str(); } LightwalletAPI_Req_GetRandomOuts new__req_params__get_random_outs( // used internally and by emscr async send impl - vector &step1__using_outs + const vector &step1__using_outs, + const optional &prior_attempt_unspent_outs_to_mix_outs ); typedef std::function passedIn_attemptAt_fee + optional prior_attempt_size_calcd_fee, + optional prior_attempt_unspent_outs_to_mix_outs ) { retVals = {}; // @@ -274,12 +275,12 @@ void monero_transfer_utils::send_step1__prepare_params_for_get_decoys( const uint64_t fee_multiplier = get_fee_multiplier(simple_priority, default_priority(), get_fee_algorithm(use_fork_rules_fn), use_fork_rules_fn); // uint64_t attempt_at_min_fee; - if (passedIn_attemptAt_fee == none) { + if (prior_attempt_size_calcd_fee == none) { attempt_at_min_fee = estimate_fee(true/*use_per_byte_fee*/, true/*use_rct*/, 1/*est num inputs*/, fake_outs_count, 2, extra.size(), bulletproof, clsag, base_fee, fee_multiplier, fee_quantization_mask); // use a minimum viable estimate_fee() with 1 input. It would be better to under-shoot this estimate, and then need to use a higher fee from calculate_fee() because the estimate is too low, // versus the worse alternative of over-estimating here and getting stuck using too high of a fee that leads to fingerprinting } else { - attempt_at_min_fee = *passedIn_attemptAt_fee; + attempt_at_min_fee = *prior_attempt_size_calcd_fee; } struct Total { @@ -299,6 +300,20 @@ void monero_transfer_utils::send_step1__prepare_params_for_get_decoys( // Gather outputs and amount to use for getting decoy outputs… uint64_t using_outs_amount = 0; vector remaining_unusedOuts = unspent_outs; // take copy so not to modify original + + // start by using all the passed in outs that were selected in a prior tx construction attempt + if (prior_attempt_unspent_outs_to_mix_outs != none) { + for (size_t i = 0; i < remaining_unusedOuts.size(); ++i) { + SpendableOutput &out = remaining_unusedOuts[i]; + + // search for out by public key to see if it should be re-used in an attempt + if (prior_attempt_unspent_outs_to_mix_outs->find(out.public_key) != prior_attempt_unspent_outs_to_mix_outs->end()) { + using_outs_amount += out.amount; + retVals.using_outs.push_back(std::move(pop_index(remaining_unusedOuts, i))); + } + } + } + // TODO: factor this out to get spendable balance for display in the MM wallet: while (using_outs_amount < potential_total && remaining_unusedOuts.size() > 0) { auto out = pop_random_value(remaining_unusedOuts); @@ -328,7 +343,7 @@ void monero_transfer_utils::send_step1__prepare_params_for_get_decoys( bulletproof, clsag, base_fee, fee_multiplier, fee_quantization_mask ); // if newNeededFee < neededFee, use neededFee instead (should only happen on the 2nd or later times through (due to estimated fee being too low)) - if (passedIn_attemptAt_fee != none && needed_fee < attempt_at_min_fee) { + if (prior_attempt_size_calcd_fee != none && needed_fee < attempt_at_min_fee) { needed_fee = attempt_at_min_fee; } // @@ -392,6 +407,75 @@ void monero_transfer_utils::send_step1__prepare_params_for_get_decoys( // // TODO? // } } +// +// +void monero_transfer_utils::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts( + Tie_Outs_to_Mix_Outs_RetVals &retVals, + // + const vector &using_outs, + vector mix_outs_from_server, + // + const optional &prior_attempt_unspent_outs_to_mix_outs +) { + retVals.errCode = noError; + // + // combine newly requested mix outs returned from the server, with the already known decoys from prior tx construction attempts, + // so that the same decoys will be re-used with the same outputs in all tx construction attempts. This ensures fee returned + // by calculate_fee() will be correct in the final tx, and also reduces number of needed trips to the server during tx construction. + SpendableOutputToRandomAmountOutputs prior_attempt_unspent_outs_to_mix_outs_new; + if (prior_attempt_unspent_outs_to_mix_outs) { + prior_attempt_unspent_outs_to_mix_outs_new = *prior_attempt_unspent_outs_to_mix_outs; + } + + std::vector mix_outs; + mix_outs.reserve(using_outs.size()); + + for (size_t i = 0; i < using_outs.size(); ++i) { + auto out = using_outs[i]; + + // if we don't already know of a particular out's mix outs (from a prior attempt), + // then tie out to a set of mix outs retrieved from the server + if (prior_attempt_unspent_outs_to_mix_outs_new.find(out.public_key) == prior_attempt_unspent_outs_to_mix_outs_new.end()) { + for (size_t j = 0; j < mix_outs_from_server.size(); ++j) { + if ((out.rct != none && mix_outs_from_server[j].amount != 0) || + (out.rct == none && mix_outs_from_server[j].amount != out.amount)) { + continue; + } + + RandomAmountOutputs output_mix_outs = pop_index(mix_outs_from_server, j); + + // if we need to retry constructing tx, will remember to use same mix outs for this out on subsequent attempt(s) + prior_attempt_unspent_outs_to_mix_outs_new[out.public_key] = output_mix_outs.outputs; + mix_outs.push_back(std::move(output_mix_outs)); + + break; + } + } else { + RandomAmountOutputs output_mix_outs; + output_mix_outs.outputs = prior_attempt_unspent_outs_to_mix_outs_new[out.public_key]; + output_mix_outs.amount = out.amount; + mix_outs.push_back(std::move(output_mix_outs)); + } + } + + // we expect to have a set of mix outs for every output in the tx + if (mix_outs.size() != using_outs.size()) { + retVals.errCode = notEnoughUsableDecoysFound; + return; + } + + // we expect to use up all mix outs returned by the server + if (!mix_outs_from_server.empty()) { + retVals.errCode = tooManyDecoysRemaining; + return; + } + + retVals.mix_outs = std::move(mix_outs); + retVals.prior_attempt_unspent_outs_to_mix_outs_new = std::move(prior_attempt_unspent_outs_to_mix_outs_new); +} +// +// +// void monero_transfer_utils::send_step2__try_create_transaction( Send_Step2_RetVals &retVals, // diff --git a/src/monero_transfer_utils.hpp b/src/monero_transfer_utils.hpp index d791f91..f52a9c2 100644 --- a/src/monero_transfer_utils.hpp +++ b/src/monero_transfer_utils.hpp @@ -80,6 +80,7 @@ namespace monero_transfer_utils uint64_t amount; vector outputs; }; + typedef std::unordered_map> SpendableOutputToRandomAmountOutputs; // // Types - Return value enum CreateTransactionErrorCode // TODO: switch to enum class to fix namespacing @@ -107,6 +108,8 @@ namespace monero_transfer_utils invalidPID = 19, enteredAmountTooLow = 20, cantGetDecryptedMaskFromRCTHex = 21, + notEnoughUsableDecoysFound = 22, + tooManyDecoysRemaining = 23, needMoreMoneyThanFound = 90 }; static inline const char *err_msg_from_err_code__create_transaction(CreateTransactionErrorCode code) @@ -156,6 +159,10 @@ namespace monero_transfer_utils return "Invalid payment ID"; case enteredAmountTooLow: return "The amount you've entered is too low"; + case notEnoughUsableDecoysFound: + return "Not enough usable decoys found"; + case tooManyDecoysRemaining: + return "Too many unused decoys remaining"; case cantGetDecryptedMaskFromRCTHex: return "Can't get decrypted mask from 'rct' hex"; } @@ -168,9 +175,11 @@ namespace monero_transfer_utils // Send_Step* functions procedure for integrators: // 1. call GetUnspentOuts endpoint // 2. call step1__prepare_params_for_get_decoys to get params for calling RandomOuts; call GetRandomOuts - // 3. call step2__try_… with retVals from Step1 (incl using_outs, RandomOuts) - // 3a. While tx must be reconstructed, re-call step1 passing step2 fee_actually_needed as passedIn_attemptAt_fee, then re-request RandomOuts again, and call step2 again - // 3b. If good tx constructed, proceed to submit/save the tx + // 3. call pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts to use constant set of mix outs for each unpsent out across tx construction attempts + // 4. call step2__try_… with retVals from Step1 and pre_Step2 (incl using_outs, RandomOuts) + // 4a. While tx must be reconstructed, re-call step1 passing step2 fee_actually_needed as prior_attempt_size_calcd_fee AND + // passing pre_step2 unspent_outs_to_mix_outs_new as prior_attempt_unspent_outs_to_mix_outs, then repeat steps 2-4 + // 4b. If good tx constructed, proceed to submit/save the tx // Note: This separation of steps fully encodes SendFunds_ProcessStep // struct Send_Step1_RetVals @@ -200,7 +209,24 @@ namespace monero_transfer_utils uint64_t fee_per_b, // per v8 uint64_t fee_quantization_mask, // - optional passedIn_attemptAt_fee // use this for passing step2 "must-reconstruct" return values back in, i.e. re-entry; when nil, defaults to attempt at network min + optional prior_attempt_size_calcd_fee, // use this for passing step2 "must-reconstruct" return values back in, i.e. re-entry; when nil, defaults to attempt at network min + optional prior_attempt_unspent_outs_to_mix_outs = none // use this to make sure upon re-attempting, the calculated fee will be the result of calculate_fee() + ); + struct Tie_Outs_to_Mix_Outs_RetVals + { + CreateTransactionErrorCode errCode; // if != noError, abort Send process + // + // Success parameters + vector mix_outs; + SpendableOutputToRandomAmountOutputs prior_attempt_unspent_outs_to_mix_outs_new; + }; + void pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts( + Tie_Outs_to_Mix_Outs_RetVals &retVals, + // + const vector &using_outs, + vector mix_outs_from_server, + // + const optional &prior_attempt_unspent_outs_to_mix_outs ); // struct Send_Step2_RetVals diff --git a/src/serial_bridge_index.cpp b/src/serial_bridge_index.cpp index 22fe45a..89250f0 100644 --- a/src/serial_bridge_index.cpp +++ b/src/serial_bridge_index.cpp @@ -470,10 +470,32 @@ string serial_bridge::send_step1__prepare_params_for_get_decoys(const string &ar // unspent_outs.push_back(std::move(out)); } - optional optl__passedIn_attemptAt_fee_string = json_root.get_optional("passedIn_attemptAt_fee"); - optional optl__passedIn_attemptAt_fee = none; - if (optl__passedIn_attemptAt_fee_string != none) { - optl__passedIn_attemptAt_fee = stoull(*optl__passedIn_attemptAt_fee_string); + optional optl__prior_attempt_size_calcd_fee_string = json_root.get_optional("prior_attempt_size_calcd_fee"); + optional optl__prior_attempt_size_calcd_fee = none; + if (optl__prior_attempt_size_calcd_fee_string != none) { + optl__prior_attempt_size_calcd_fee = stoull(*optl__prior_attempt_size_calcd_fee_string); + } + optional optl__prior_attempt_unspent_outs_to_mix_outs; + SpendableOutputToRandomAmountOutputs prior_attempt_unspent_outs_to_mix_outs; + optional optl__prior_attempt_unspent_outs_to_mix_outs_json = json_root.get_child_optional("prior_attempt_unspent_outs_to_mix_outs"); + if (optl__prior_attempt_unspent_outs_to_mix_outs_json != none) + { + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *optl__prior_attempt_unspent_outs_to_mix_outs_json) + { + string out_pub_key = outs_to_mix_outs_desc.first; + RandomAmountOutputs amountAndOuts{}; + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_output_desc, outs_to_mix_outs_desc.second) + { + assert(mix_out_output_desc.first.empty()); // array elements have no names + auto amountOutput = monero_transfer_utils::RandomAmountOutput{}; + amountOutput.global_index = stoull(mix_out_output_desc.second.get("global_index")); + amountOutput.public_key = mix_out_output_desc.second.get("public_key"); + amountOutput.rct = mix_out_output_desc.second.get_optional("rct"); + amountAndOuts.outputs.push_back(std::move(amountOutput)); + } + prior_attempt_unspent_outs_to_mix_outs[out_pub_key] = std::move(amountAndOuts.outputs); + } + optl__prior_attempt_unspent_outs_to_mix_outs = std::move(prior_attempt_unspent_outs_to_mix_outs); } uint8_t fork_version = 0; // if missing optional optl__fork_version_string = json_root.get_optional("fork_version"); @@ -493,7 +515,8 @@ string serial_bridge::send_step1__prepare_params_for_get_decoys(const string &ar stoull(json_root.get("fee_per_b")), // per v8 stoull(json_root.get("fee_mask")), // - optl__passedIn_attemptAt_fee // use this for passing step2 "must-reconstruct" return values back in, i.e. re-entry; when nil, defaults to attempt at network min + optl__prior_attempt_size_calcd_fee, // use this for passing step2 "must-reconstruct" return values back in, i.e. re-entry; when nil, defaults to attempt at network min + optl__prior_attempt_unspent_outs_to_mix_outs // on re-entry, re-use the same outs and requested decoys, in order to land on the correct calculated fee ); boost::property_tree::ptree root; if (retVals.errCode != noError) { @@ -529,6 +552,139 @@ string serial_bridge::send_step1__prepare_params_for_get_decoys(const string &ar } return ret_json_from_root(root); } +// +string serial_bridge::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts(const string &args_string) +{ + boost::property_tree::ptree json_root; + if (!parsed_json_root(args_string, json_root)) { + // it will already have thrown an exception + return error_ret_json_from_message("Invalid JSON"); + } + // + vector using_outs; + BOOST_FOREACH(boost::property_tree::ptree::value_type &output_desc, json_root.get_child("using_outs")) + { + assert(output_desc.first.empty()); // array elements have no names + SpendableOutput out{}; + out.amount = stoull(output_desc.second.get("amount")); + out.public_key = output_desc.second.get("public_key"); + out.rct = output_desc.second.get_optional("rct"); + if (out.rct != none && (*out.rct).empty() == true) { + out.rct = none; // just in case it's an empty string, send to 'none' (even though receiving code now handles empty strs) + } + out.global_index = stoull(output_desc.second.get("global_index")); + out.index = stoull(output_desc.second.get("index")); + out.tx_pub_key = output_desc.second.get("tx_pub_key"); + // + using_outs.push_back(std::move(out)); + } + // + vector mix_outs_from_server; + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_desc, json_root.get_child("mix_outs")) + { + assert(mix_out_desc.first.empty()); // array elements have no names + auto amountAndOuts = RandomAmountOutputs{}; + amountAndOuts.amount = stoull(mix_out_desc.second.get("amount")); + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_output_desc, mix_out_desc.second.get_child("outputs")) + { + assert(mix_out_output_desc.first.empty()); // array elements have no names + auto amountOutput = RandomAmountOutput{}; + amountOutput.global_index = stoull(mix_out_output_desc.second.get("global_index")); + amountOutput.public_key = mix_out_output_desc.second.get("public_key"); + amountOutput.rct = mix_out_output_desc.second.get_optional("rct"); + amountAndOuts.outputs.push_back(std::move(amountOutput)); + } + mix_outs_from_server.push_back(std::move(amountAndOuts)); + } + // + optional optl__prior_attempt_unspent_outs_to_mix_outs; + SpendableOutputToRandomAmountOutputs prior_attempt_unspent_outs_to_mix_outs; + optional optl__prior_attempt_unspent_outs_to_mix_outs_json = json_root.get_child_optional("prior_attempt_unspent_outs_to_mix_outs"); + if (optl__prior_attempt_unspent_outs_to_mix_outs_json != none) + { + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *optl__prior_attempt_unspent_outs_to_mix_outs_json) + { + string out_pub_key = outs_to_mix_outs_desc.first; + RandomAmountOutputs amountAndOuts{}; + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_output_desc, outs_to_mix_outs_desc.second) + { + assert(mix_out_output_desc.first.empty()); // array elements have no names + auto amountOutput = monero_transfer_utils::RandomAmountOutput{}; + amountOutput.global_index = stoull(mix_out_output_desc.second.get("global_index")); + amountOutput.public_key = mix_out_output_desc.second.get("public_key"); + amountOutput.rct = mix_out_output_desc.second.get_optional("rct"); + amountAndOuts.outputs.push_back(std::move(amountOutput)); + } + prior_attempt_unspent_outs_to_mix_outs[out_pub_key] = std::move(amountAndOuts.outputs); + } + optl__prior_attempt_unspent_outs_to_mix_outs = std::move(prior_attempt_unspent_outs_to_mix_outs); + } + // + Tie_Outs_to_Mix_Outs_RetVals retVals; + monero_transfer_utils::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts( + retVals, + // + using_outs, + mix_outs_from_server, + // + optl__prior_attempt_unspent_outs_to_mix_outs + ); + boost::property_tree::ptree root; + if (retVals.errCode != noError) { + root.put(ret_json_key__any__err_code(), retVals.errCode); + root.put(ret_json_key__any__err_msg(), err_msg_from_err_code__create_transaction(retVals.errCode)); + } else { + { + boost::property_tree::ptree mix_outs_ptree; + BOOST_FOREACH(RandomAmountOutputs &mix_outs, retVals.mix_outs) + { + auto mix_outs_amount_ptree_pair = std::make_pair("", boost::property_tree::ptree{}); + auto& mix_outs_amount_ptree = mix_outs_amount_ptree_pair.second; + mix_outs_amount_ptree.put("amount", RetVals_Transforms::str_from(mix_outs.amount)); + auto outputs_ptree_pair = std::make_pair("", boost::property_tree::ptree{}); + auto& outputs_ptree = outputs_ptree_pair.second; + BOOST_FOREACH(RandomAmountOutput &out, mix_outs.outputs) + { + auto mix_out_ptree_pair = std::make_pair("", boost::property_tree::ptree{}); + auto& mix_out_ptree = mix_out_ptree_pair.second; + mix_out_ptree.put("global_index", RetVals_Transforms::str_from(out.global_index)); + mix_out_ptree.put("public_key", out.public_key); + if (out.rct != none && (*out.rct).empty() == false) { + mix_out_ptree.put("rct", *out.rct); + } + outputs_ptree.push_back(mix_out_ptree_pair); + } + mix_outs_amount_ptree.add_child("outputs", outputs_ptree); + mix_outs_ptree.push_back(mix_outs_amount_ptree_pair); + } + root.add_child(ret_json_key__send__mix_outs(), mix_outs_ptree); + } + // + { + boost::property_tree::ptree prior_attempt_unspent_outs_to_mix_outs_new_ptree; + for (const auto &out_pub_key_to_mix_outs : retVals.prior_attempt_unspent_outs_to_mix_outs_new) + { + auto outs_ptree_pair = std::make_pair(out_pub_key_to_mix_outs.first, boost::property_tree::ptree{}); + auto& outs_ptree = outs_ptree_pair.second; + for (const auto &mix_out : out_pub_key_to_mix_outs.second) + { + auto mix_out_ptree_pair = std::make_pair("", boost::property_tree::ptree{}); + auto& mix_out_ptree = mix_out_ptree_pair.second; + mix_out_ptree.put("global_index", RetVals_Transforms::str_from(mix_out.global_index)); + mix_out_ptree.put("public_key", mix_out.public_key); + if (mix_out.rct != none && (*mix_out.rct).empty() == false) { + mix_out_ptree.put("rct", *mix_out.rct); + } + outs_ptree.push_back(mix_out_ptree_pair); + } + prior_attempt_unspent_outs_to_mix_outs_new_ptree.push_back(outs_ptree_pair); + } + root.add_child(ret_json_key__send__prior_attempt_unspent_outs_to_mix_outs_new(), prior_attempt_unspent_outs_to_mix_outs_new_ptree); + } + } + return ret_json_from_root(root); +} +// string serial_bridge::send_step2__try_create_transaction(const string &args_string) { boost::property_tree::ptree json_root; diff --git a/src/serial_bridge_index.hpp b/src/serial_bridge_index.hpp index f0c3494..c6f6ab1 100644 --- a/src/serial_bridge_index.hpp +++ b/src/serial_bridge_index.hpp @@ -45,6 +45,7 @@ namespace serial_bridge // // Bridging Functions - these take and return JSON strings string send_step1__prepare_params_for_get_decoys(const string &args_string); + string pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts(const string &args_string); string send_step2__try_create_transaction(const string &args_string); // string decode_address(const string &args_string); diff --git a/src/serial_bridge_utils.hpp b/src/serial_bridge_utils.hpp index 7a58597..c1b6a15 100644 --- a/src/serial_bridge_utils.hpp +++ b/src/serial_bridge_utils.hpp @@ -95,6 +95,8 @@ namespace serial_bridge_utils static inline string ret_json_key__send__final_total_wo_fee() { return "final_total_wo_fee"; } static inline string ret_json_key__send__change_amount() { return "change_amount"; } static inline string ret_json_key__send__using_outs() { return "using_outs"; } // this list's members' keys should probably be declared (is this the best way to do this?) + static inline string ret_json_key__send__mix_outs() { return "mix_outs"; } + static inline string ret_json_key__send__prior_attempt_unspent_outs_to_mix_outs_new() { return "prior_attempt_unspent_outs_to_mix_outs_new"; } // static inline string ret_json_key__send__tx_must_be_reconstructed() { return "tx_must_be_reconstructed"; } static inline string ret_json_key__send__fee_actually_needed() { return "fee_actually_needed"; } diff --git a/test/test_all.cpp b/test/test_all.cpp index 92ba2ec..d400468 100644 --- a/test/test_all.cpp +++ b/test/test_all.cpp @@ -102,6 +102,11 @@ BOOST_AUTO_TEST_CASE(wallet) // #include "../src/monero_transfer_utils.hpp" #include "../src/monero_fork_rules.hpp" +// +string pre_step2__unspent_outs_json = "{\"unspent_outs\":[{\"amount\":\"210000000\",\"public_key\":\"89eb08cf704d4473a17646331d2c425307ef03477e5f18ee6a31a3601ba9cdd0\",\"index\":0,\"global_index\":7510705,\"rct\":\"befe623ad1dcae239e4d9d31e3080db5c339ea8c5c2894444966967a051f27839f1f713d6f6bdc13fec3c20f78bbae6cf08ce185273fa6c913db6ae1f44e270ea9dcfa48ecbae364125e0c4b0cb7a11fe6c250ec9aca1a668a0708e821d6550b\",\"tx_id\":5292354,\"tx_hash\":\"22fa4aaee9399901ece7d9521067aa7791a727ade2dfe9d5e17481800ccbc625\",\"tx_pub_key\":\"4f151192723d3d45372b43e4bf93df8ad7ba5283513c09226fd0603c60683e00\",\"tx_prefix_hash\":\"689580f0804eff0fd9bd76587ed9656e4cda8e70a33f065b5461206bcf9051b7\",\"height\":1681636},{\"amount\":\"230000000\",\"public_key\":\"f659694299d97fc93db504122d40dea1681a896567933635dc6337abc4339c10\",\"index\":1,\"global_index\":7551823,\"rct\":\"dd06d546553044cda0f083fd189cd8ad93ebeca557169eefe1e34dc48c6fac27110a3ff8dc24a61b595a03a034009a6d1f0ced61f19fb6e0d7c2b1a67bb39d06c7d5713e0a394551ec978b64927802f9307ac29c8ddec3857f551b945ef6a407\",\"tx_id\":5309604,\"tx_hash\":\"05704e7402d1373d14dccd383e4071bfae0c2af6eb075e67075b43fd7d26b4c4\",\"tx_pub_key\":\"3511d9117fdeac0423314827188aa187f1eb742a44ab0c01390053b68b00909c\",\"tx_prefix_hash\":\"1b89ac0c818454806686073cd2d6bd501923d6eec2c0e54e300e3ae68a2c5344\",\"height\":1684479}]}"; +// +string pre_step2__mix_outs_from_server_json = "{\"mix_outs\":[{\"amount\":\"0\",\"outputs\":[{\"global_index\":\"6986524\",\"public_key\":\"3ce9f1231ecebf100a8d0e9c165a2b88a766249cb03eac2c6dbe7587a1f0e9ae\",\"rct\":\"c3b81a937c12c017b4c4eee0ab9acbd10d83f28c1586971b13791c7b475e469b\"},{\"global_index\":\"7282304\",\"public_key\":\"278450b855e4d66dbc1a9ae2801a2f101a10afd22c27466c3cfcc3b434a25047\",\"rct\":\"dd05d1d973be19b4e754c24c6d21e9252a9b99db52ff291930d4cd8c1cd344df\"},{\"global_index\":\"7386837\",\"public_key\":\"0d3cf94dd4e9059900f14bd8d5b71ce43e444efd2b8a1a63a1f9705851d195a1\",\"rct\":\"5c124e0c007e8a2f6371a6d35d50165178667fa9470270e8d7a95ffda34df30d\"},{\"global_index\":\"7459325\",\"public_key\":\"badabeeb71f08917b0cb76ae128e869dab7291d58c7a6b2fbd31d3eed0f003df\",\"rct\":\"a5ca005346fad19624c185dfefb2c4013f6b769f0f0de4b2c8f507ede1cb46a5\"},{\"global_index\":\"7507948\",\"public_key\":\"6f08278bc9d064cfdaa6d896ef70d28fbb3dca84e0a99ea21325f9aaef3bd783\",\"rct\":\"4a70f95a4cc19d9e43cc6b60f30f60571029240df21fb06188766bf92e8d8738\"},{\"global_index\":\"7529692\",\"public_key\":\"8b13f88507f5ca60c72c076ce6bc8ee142abc6e5115ab0c08e10a919c93f912a\",\"rct\":\"6055a2a847938471bd6f00a4d9789e6dc9d70962bb1dc2f51879d04211aaa0b7\"},{\"global_index\":\"7563051\",\"public_key\":\"d44a722cdca3c372081af6e32b758a2bbab9f2534f68a08b71d38c3540209c50\",\"rct\":\"b5ebd41d0c75877cdf109d6b5939072c22a84aee4c46a8299bec8eafc82789e9\"},{\"global_index\":\"7564143\",\"public_key\":\"c12f9e3c53dee0d1327dbca66129b27f8c6174a777976615ee442278960ba369\",\"rct\":\"a8423b9491162813589d3af5e18677f2f38050c10cb5074c097f101ccef089c5\"},{\"global_index\":\"7567982\",\"public_key\":\"9e4347089b0e1cb065cb443899d77b4bd4d61598e80a8946336440920c8a6731\",\"rct\":\"00fc0e9c631a4a2538785b647e6146ba39743d9dc987059f850d1c5a4f97bd2b\"},{\"global_index\":\"7570259\",\"public_key\":\"1be949046425c646a86ac37961a6301ea3d25711426d80a48b11e9282acd222b\",\"rct\":\"7db9d60ac0286189a1833f39db7f3e5372763c557fe2240b4537bf580a902798\"},{\"global_index\":\"7570451\",\"public_key\":\"82a27a521340220805de27aae18a4663b81067145c0b0c3e7ec42341067bf270\",\"rct\":\"a3f46fdc3e4a252604e3f3d082ab1d2cbc3ce34bf62b641b76849c5382199a32\"}]},{\"amount\":\"0\",\"outputs\":[{\"global_index\":\"7442603\",\"public_key\":\"ba89de37e26056629c89b14b3b05a73400c62149fa0de2794d3876f17faeb28f\",\"rct\":\"aa2edfca6622db354add0813ff2b471f6dc20f0d9e56d1f9b6c04b1369ceb1a9\"},{\"global_index\":\"7445670\",\"public_key\":\"a0c3a8bd0d6fa37e7bd514a10ebe6970609919e2f781dc489b771f305f1da4cc\",\"rct\":\"eb78b914307a54cd95481ba8844df3dd2d12cd14cee07de441c2c607b9cfcb24\"},{\"global_index\":\"7474646\",\"public_key\":\"3d325a1222b77d82192e1c051b241e0f79e1cc731c5f03749df33cf1a7165be8\",\"rct\":\"821bfcb255fc815aeab23d890ba252dc590c743c5733bcd278dbd1763e921e4d\"},{\"global_index\":\"7545722\",\"public_key\":\"ec62838ef1ab75055940fd8f31126698af9ff2128a53def09bdaa0d315174d80\",\"rct\":\"547de3a10658167afee6aaf8f3481921d2b1ee3014d40fa4cacc86940b244985\"},{\"global_index\":\"7556262\",\"public_key\":\"4dab027c001473b775f70503b9d68c156d2a8bfa0d7534aaff12a2ab1d8d5f89\",\"rct\":\"5aa838a2f5450408932b53181899861600d3cac864dee8197ac7e9543fbab148\"},{\"global_index\":\"7557709\",\"public_key\":\"bd1813a780e4df3c8ba25b825c3d7be12ce8c5d05f6731384e0d2d8cb8bf3134\",\"rct\":\"49ce757933cdca4a51f77ae41b951a2175d0a0a0378c10c3a02432e5aeb9f79f\"},{\"global_index\":\"7560040\",\"public_key\":\"ea53143df34ccba3c29743964ddc14094f224fa92d45c8fa8e86d7ff1394e51a\",\"rct\":\"455a6083ab6c3d4f026d2b4e1545467666f7affa0cdec365a295c097eefeac46\"},{\"global_index\":\"7563671\",\"public_key\":\"9af80a727bdb148851e79a9a11f55e97435daf65b3d57b54f4d64833cd483f2b\",\"rct\":\"622855010cd03a04d66d71a20d6113cb0507276b4c6ef050297a12e0a6767004\"},{\"global_index\":\"7564234\",\"public_key\":\"404aedc1c299e9a1538bdf7619f42cbf92cb3bb556e0356dce275945e318633d\",\"rct\":\"a1978e496622c2fac054939227a4edb31c4a50215cf8db74b0f1a7ce3477e3cf\"},{\"global_index\":\"7565705\",\"public_key\":\"070c5adc791d0a33390fecb02376e8953e46661a0173a64c003b5ae5709eea3c\",\"rct\":\"09f6c3c9139eefa0ed9ff9613e57bf3fc1b7d2bc42bad4caeb9118cc768cc52f\"},{\"global_index\":\"7566892\",\"public_key\":\"76c03aad2fae21aa7d36bbda699c462b222a76359d92813c06e4ccf4508e77e2\",\"rct\":\"9905946004a01e2884aedfa41b2482ca309226166519c558b5c794eeae109f98\"}]}]}"; +// BOOST_AUTO_TEST_CASE(transfers__fee) { uint8_t fork_version = 10; @@ -112,6 +117,201 @@ BOOST_AUTO_TEST_CASE(transfers__fee) std::cout << "transfers__fee: est_fee with fee_per_b " << fee_per_b << ": " << est_fee << std::endl; BOOST_REQUIRE(est_fee > 0); } +BOOST_AUTO_TEST_CASE(pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts__use_all_server_mix_outs) +{ + // *** START SETUP *** + // this being input as JSON merely for convenience + boost::property_tree::ptree pt; + stringstream ss; + ss << pre_step2__unspent_outs_json; + boost::property_tree::json_parser::read_json(ss, pt); + // + vector unspent_outs; + BOOST_FOREACH(boost::property_tree::ptree::value_type &output_desc, pt.get_child("unspent_outs")) + { + assert(output_desc.first.empty()); // array elements have no names + monero_transfer_utils::SpendableOutput out{}; + out.amount = stoull(output_desc.second.get("amount")); + out.public_key = output_desc.second.get("public_key"); + out.rct = output_desc.second.get_optional("rct"); + if (out.rct != none && (*out.rct).empty() == true) { + out.rct = none; + } + out.global_index = stoull(output_desc.second.get("global_index")); + out.index = stoull(output_desc.second.get("index")); + out.tx_pub_key = output_desc.second.get("tx_pub_key"); + // + unspent_outs.push_back(std::move(out)); + } + // + vector mix_outs_from_server; + { + boost::property_tree::ptree pt; + stringstream ss; + ss << pre_step2__mix_outs_from_server_json; + boost::property_tree::json_parser::read_json(ss, pt); + + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_desc, pt.get_child("mix_outs")) + { + assert(mix_out_desc.first.empty()); // array elements have no names + auto amountAndOuts = monero_transfer_utils::RandomAmountOutputs{}; + amountAndOuts.amount = stoull(mix_out_desc.second.get("amount")); + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_output_desc, mix_out_desc.second.get_child("outputs")) + { + assert(mix_out_output_desc.first.empty()); // array elements have no names + auto amountOutput = monero_transfer_utils::RandomAmountOutput{}; + amountOutput.global_index = stoull(mix_out_output_desc.second.get("global_index")); + amountOutput.public_key = mix_out_output_desc.second.get("public_key"); + amountOutput.rct = mix_out_output_desc.second.get_optional("rct"); + amountAndOuts.outputs.push_back(std::move(amountOutput)); + } + mix_outs_from_server.push_back(std::move(amountAndOuts)); + } + } + assert(unspent_outs.size() == mix_outs_from_server.size()); + // *** END SETUP *** + // + monero_transfer_utils::Tie_Outs_to_Mix_Outs_RetVals tie_outs_to_mix_outs_retVals; + monero_transfer_utils::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts( + tie_outs_to_mix_outs_retVals, + unspent_outs, + mix_outs_from_server, + boost::none/*prior_attempt_unspent_outs_to_mix_outs*/ + ); + // + BOOST_REQUIRE_MESSAGE(tie_outs_to_mix_outs_retVals.errCode == monero_transfer_utils::noError, "expected no error"); + BOOST_REQUIRE_MESSAGE(tie_outs_to_mix_outs_retVals.mix_outs.size() == mix_outs_from_server.size(), "expected resulting mix outs to use for step 2 to be same as server response"); + // + for (size_t i = 0; i < unspent_outs.size(); ++i) + { + const vector &mix_outs = tie_outs_to_mix_outs_retVals.mix_outs[i].outputs; + const monero_transfer_utils::SpendableOutput &unspent_out = unspent_outs[i]; + const vector &tied_mix_outs = tie_outs_to_mix_outs_retVals.prior_attempt_unspent_outs_to_mix_outs_new[unspent_out.public_key]; + // + BOOST_REQUIRE_MESSAGE(mix_outs.size() == tied_mix_outs.size(), "mix outs from server size does not match tied mix outs size"); + for (size_t j = 0; j < mix_outs.size(); ++j) + { + BOOST_REQUIRE_MESSAGE(mix_outs[j].global_index == tied_mix_outs[j].global_index, "new outs to mix outs did not tie as expected: global index"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].public_key == tied_mix_outs[j].public_key, "new outs to mix outs did not tie as expected: public key"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].rct == tied_mix_outs[j].rct, "new outs to mix outs did not tie as expected: rct"); + // + BOOST_REQUIRE_MESSAGE(mix_outs[j].global_index == mix_outs_from_server[i].outputs[j].global_index, "mix outs to mix outs from server did not tie as expected: global index"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].public_key == mix_outs_from_server[i].outputs[j].public_key, "mix outs to mix outs from server did not tie as expected: public key"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].rct == mix_outs_from_server[i].outputs[j].rct, "mix outs to mix outs from server did not tie as expected: rct"); + } + } +} +// +BOOST_AUTO_TEST_CASE(pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts__use_prior_attempt_mix_outs) +{ + // *** START SETUP *** + // this being input as JSON merely for convenience + boost::property_tree::ptree pt; + stringstream ss; + ss << pre_step2__unspent_outs_json; + boost::property_tree::json_parser::read_json(ss, pt); + // + vector unspent_outs; + BOOST_FOREACH(boost::property_tree::ptree::value_type &output_desc, pt.get_child("unspent_outs")) + { + assert(output_desc.first.empty()); // array elements have no names + monero_transfer_utils::SpendableOutput out{}; + out.amount = stoull(output_desc.second.get("amount")); + out.public_key = output_desc.second.get("public_key"); + out.rct = output_desc.second.get_optional("rct"); + if (out.rct != none && (*out.rct).empty() == true) { + out.rct = none; + } + out.global_index = stoull(output_desc.second.get("global_index")); + out.index = stoull(output_desc.second.get("index")); + out.tx_pub_key = output_desc.second.get("tx_pub_key"); + // + unspent_outs.push_back(std::move(out)); + } + // + std::vector mix_outs_from_server; + monero_transfer_utils::SpendableOutputToRandomAmountOutputs prior_attempt_unspent_outs_to_mix_outs; + size_t index_of_unspent_out_used_in_prior_attempt = 0; + { + boost::property_tree::ptree pt; + stringstream ss; + ss << pre_step2__mix_outs_from_server_json; + boost::property_tree::json_parser::read_json(ss, pt); + // + size_t i = 0; + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_desc, pt.get_child("mix_outs")) + { + assert(mix_out_desc.first.empty()); // array elements have no names + auto amountAndOuts = monero_transfer_utils::RandomAmountOutputs{}; + amountAndOuts.amount = stoull(mix_out_desc.second.get("amount")); + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_output_desc, mix_out_desc.second.get_child("outputs")) + { + assert(mix_out_output_desc.first.empty()); // array elements have no names + auto amountOutput = monero_transfer_utils::RandomAmountOutput{}; + amountOutput.global_index = stoull(mix_out_output_desc.second.get("global_index")); + amountOutput.public_key = mix_out_output_desc.second.get("public_key"); + amountOutput.rct = mix_out_output_desc.second.get_optional("rct"); + amountAndOuts.outputs.push_back(std::move(amountOutput)); + } + if (i == index_of_unspent_out_used_in_prior_attempt) + { + // will tie the first unspent output to the first set of mix outs returned from the server + prior_attempt_unspent_outs_to_mix_outs[unspent_outs[i].public_key] = std::move(amountAndOuts.outputs); + } + else + { + mix_outs_from_server.push_back(std::move(amountAndOuts)); + } + ++i; + } + } + assert(unspent_outs.size() == (1 + mix_outs_from_server.size())); + // *** END SETUP *** + // + monero_transfer_utils::Tie_Outs_to_Mix_Outs_RetVals tie_outs_to_mix_outs_retVals; + monero_transfer_utils::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts( + tie_outs_to_mix_outs_retVals, + unspent_outs, + mix_outs_from_server, + prior_attempt_unspent_outs_to_mix_outs + ); + // + BOOST_REQUIRE_MESSAGE(tie_outs_to_mix_outs_retVals.errCode == monero_transfer_utils::noError, "expected no error"); + BOOST_REQUIRE_MESSAGE(tie_outs_to_mix_outs_retVals.mix_outs.size() == unspent_outs.size(), "expected resulting mix outs to use for step 2 to be same as unspent_outs"); + // + for (size_t i = 0; i < unspent_outs.size(); ++i) + { + const vector &mix_outs = tie_outs_to_mix_outs_retVals.mix_outs[i].outputs; + const monero_transfer_utils::SpendableOutput &unspent_out = unspent_outs[i]; + const vector &tied_mix_outs = tie_outs_to_mix_outs_retVals.prior_attempt_unspent_outs_to_mix_outs_new[unspent_out.public_key]; + // + vector prior_tied_mix_outs; + if (i == index_of_unspent_out_used_in_prior_attempt) + prior_tied_mix_outs = prior_attempt_unspent_outs_to_mix_outs[unspent_out.public_key]; + // + BOOST_REQUIRE_MESSAGE(mix_outs.size() == tied_mix_outs.size(), "mix outs from server size does not match tied mix outs size"); + for (size_t j = 0; j < mix_outs.size(); ++j) + { + BOOST_REQUIRE_MESSAGE(mix_outs[j].global_index == tied_mix_outs[j].global_index, "new outs to mix outs did not tie as expected: global index"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].public_key == tied_mix_outs[j].public_key, "new outs to mix outs did not tie as expected: public key"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].rct == tied_mix_outs[j].rct, "new outs to mix outs did not tie as expected: rct"); + // + if (i == index_of_unspent_out_used_in_prior_attempt) + { + BOOST_REQUIRE_MESSAGE(prior_tied_mix_outs[j].global_index == tied_mix_outs[j].global_index, "prior tied mix outs to tied mix outs from server did not tie as expected: global index"); + BOOST_REQUIRE_MESSAGE(prior_tied_mix_outs[j].public_key == tied_mix_outs[j].public_key, "prior tied mix outs to tied mix outs from server did not tie as expected: public key"); + BOOST_REQUIRE_MESSAGE(prior_tied_mix_outs[j].rct == tied_mix_outs[j].rct, "prior tied mix outs to tied mix outs from server did not tie as expected: rct"); + } + else + { + monero_transfer_utils::RandomAmountOutput server_mix_out = mix_outs_from_server[i - 1].outputs[j]; + BOOST_REQUIRE_MESSAGE(mix_outs[j].global_index == server_mix_out.global_index, "mix outs to mix outs from server did not tie as expected: global index"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].public_key == server_mix_out.public_key, "mix outs to mix outs from server did not tie as expected: public key"); + BOOST_REQUIRE_MESSAGE(mix_outs[j].rct == server_mix_out.rct, "mix outs to mix outs from server did not tie as expected: rct"); + } + } + } +} // // // Serialization bridge @@ -143,17 +343,8 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send__sweepDust) ss << DG_presweep__unspent_outs_json; boost::property_tree::json_parser::read_json(ss, pt); boost::property_tree::ptree unspent_outs = pt.get_child("unspent_outs"); + optional prior_attempt_unspent_outs_to_mix_outs = none; // - // NOTE: in the real algorithm you should re-request this _each time step2 must be called_ - // this being input as JSON merely for convenience - boost::property_tree::ptree mix_outs; - { - boost::property_tree::ptree pt; - stringstream ss; - ss << DG_presweep__rand_outs_json; - boost::property_tree::json_parser::read_json(ss, pt); - mix_outs = pt.get_child("mix_outs"); - } // // Send algorithm: // (Not implemented in C++ b/c the algorithm is split at the points (function interfaces) where requests must be done in e.g. JS-land, and implementing the retry integration in C++ would effectively be emscripten-only since it'd have to call out to C++. Plus this lets us retain the choice to retain synchrony @@ -180,9 +371,17 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send__sweepDust) root.add_child("unspent_outs", unspent_outs); if (fee_actually_needed_string != none) { BOOST_REQUIRE(construction_attempt_n > 1); + BOOST_REQUIRE(prior_attempt_unspent_outs_to_mix_outs != none); // - // for next round's integration - if it needs to re-enter... arg "passedIn_attemptAt_fee" - root.put("passedIn_attemptAt_fee", *fee_actually_needed_string); + // for next round's integration - if it needs to re-enter... arg "prior_attempt_size_calcd_fee" and "prior_attempt_unspent_outs_to_mix_outs" + root.put("prior_attempt_size_calcd_fee", *fee_actually_needed_string); + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *prior_attempt_unspent_outs_to_mix_outs) + { + string out_pub_key = outs_to_mix_outs_desc.first; + cout << "bridge__transfers__send__sweepDust: step1: prior output " << out_pub_key << endl; + BOOST_REQUIRE(outs_to_mix_outs_desc.second.size() == 11); + } + root.add_child("prior_attempt_unspent_outs_to_mix_outs", *prior_attempt_unspent_outs_to_mix_outs); } auto ret_string = serial_bridge::send_step1__prepare_params_for_get_decoys(args_string_from_root(root)); stringstream ret_stream; @@ -246,6 +445,57 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send__sweepDust) cout << "bridge__transfers__send__sweepDust: step1: final_total_wo_fee " << *final_total_wo_fee_string << endl; // } + boost::property_tree::ptree mix_outs; + { + boost::property_tree::ptree root; + root.add_child("using_outs", using_outs); // from step1 + // NOTE: in the real algorithm you should request _previously unseen + // mixouts from prior attempts each time pre_step2 must be called_ + // this being input as JSON merely for convenience + boost::property_tree::ptree mix_outs_from_server; + if (construction_attempt_n == 1) + { + boost::property_tree::ptree pt; + stringstream ss; + ss << DG_presweep__rand_outs_json; + boost::property_tree::json_parser::read_json(ss, pt); + mix_outs_from_server = pt.get_child("mix_outs"); + } + else + { + root.add_child("prior_attempt_unspent_outs_to_mix_outs", *prior_attempt_unspent_outs_to_mix_outs); + } + root.add_child("mix_outs", mix_outs_from_server); + // + boost::property_tree::ptree ret_tree; + auto ret_string = serial_bridge::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts(args_string_from_root(root)); + stringstream ret_stream; + ret_stream << ret_string; + boost::property_tree::read_json(ret_stream, ret_tree); + optional err_code = ret_tree.get_optional(ret_json_key__any__err_code()); + if (err_code != none && (CreateTransactionErrorCode)*err_code != monero_transfer_utils::noError) { + auto err_msg = err_msg_from_err_code__create_transaction((CreateTransactionErrorCode)*err_code); + BOOST_REQUIRE_MESSAGE(false, err_msg); + } + mix_outs = ret_tree.get_child(ret_json_key__send__mix_outs()); + BOOST_REQUIRE(mix_outs.size() == using_outs.size()); + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_desc, mix_outs) + { + assert(mix_out_desc.first.empty()); // array elements have no names + cout << "bridge__transfers__send__sweepDust: pre_step2: amount " << mix_out_desc.second.get("amount") << endl; + BOOST_REQUIRE(mix_out_desc.second.get_child("outputs").size() == 11); + } + prior_attempt_unspent_outs_to_mix_outs = ret_tree.get_child(ret_json_key__send__prior_attempt_unspent_outs_to_mix_outs_new()); + size_t outs_to_mix_outs_count = 0; + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *prior_attempt_unspent_outs_to_mix_outs) + { + ++outs_to_mix_outs_count; + string out_pub_key = outs_to_mix_outs_desc.first; + cout << "bridge__transfers__send__sweepDust: pre_step2: output " << out_pub_key << endl; + BOOST_REQUIRE(outs_to_mix_outs_desc.second.size() == 11); + } + BOOST_REQUIRE(outs_to_mix_outs_count == using_outs.size()); + } { boost::property_tree::ptree root; root.put("final_total_wo_fee", *final_total_wo_fee_string); @@ -355,17 +605,8 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send__amount) ss << DG_postsweep__unspent_outs_json; boost::property_tree::json_parser::read_json(ss, pt); boost::property_tree::ptree unspent_outs = pt.get_child("unspent_outs"); + optional prior_attempt_unspent_outs_to_mix_outs = none; // - // NOTE: in the real algorithm you should re-request this _each time step2 must be called_ - // this being input as JSON merely for convenience - boost::property_tree::ptree mix_outs; - { - boost::property_tree::ptree pt; - stringstream ss; - ss << DG_postsweep__rand_outs_json; - boost::property_tree::json_parser::read_json(ss, pt); - mix_outs = pt.get_child("mix_outs"); - } // // Send algorithm: bool tx_must_be_reconstructed = true; // for ease of writing this code, start this off true & structure whole thing as while loop @@ -391,9 +632,17 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send__amount) root.add_child("unspent_outs", unspent_outs); if (fee_actually_needed_string != none) { BOOST_REQUIRE(construction_attempt_n > 1); + BOOST_REQUIRE(prior_attempt_unspent_outs_to_mix_outs != none); // - // for next round's integration - if it needs to re-enter... arg "passedIn_attemptAt_fee" - root.put("passedIn_attemptAt_fee", *fee_actually_needed_string); + // for next round's integration - if it needs to re-enter... arg "prior_attempt_size_calcd_fee" and "prior_attempt_unspent_outs_to_mix_outs" + root.put("prior_attempt_size_calcd_fee", *fee_actually_needed_string); + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *prior_attempt_unspent_outs_to_mix_outs) + { + string out_pub_key = outs_to_mix_outs_desc.first; + cout << "bridge__transfers__send__sweepDust: step1: prior output " << out_pub_key << endl; + BOOST_REQUIRE(outs_to_mix_outs_desc.second.size() == 11); + } + root.add_child("prior_attempt_unspent_outs_to_mix_outs", *prior_attempt_unspent_outs_to_mix_outs); } auto ret_string = serial_bridge::send_step1__prepare_params_for_get_decoys(args_string_from_root(root)); stringstream ret_stream; @@ -457,6 +706,57 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send__amount) cout << "bridge__transfers__send__amount: step1: final_total_wo_fee " << *final_total_wo_fee_string << endl; // } + boost::property_tree::ptree mix_outs; + { + boost::property_tree::ptree root; + root.add_child("using_outs", using_outs); // from step1 + // NOTE: in the real algorithm you should request _previously unseen + // mixouts from prior attempts each time pre_step2 must be called_ + // this being input as JSON merely for convenience + boost::property_tree::ptree mix_outs_from_server; + if (construction_attempt_n == 1) + { + boost::property_tree::ptree pt; + stringstream ss; + ss << DG_postsweep__rand_outs_json; + boost::property_tree::json_parser::read_json(ss, pt); + mix_outs_from_server = pt.get_child("mix_outs"); + } + else + { + root.add_child("prior_attempt_unspent_outs_to_mix_outs", *prior_attempt_unspent_outs_to_mix_outs); + } + root.add_child("mix_outs", mix_outs_from_server); + // + boost::property_tree::ptree ret_tree; + auto ret_string = serial_bridge::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts(args_string_from_root(root)); + stringstream ret_stream; + ret_stream << ret_string; + boost::property_tree::read_json(ret_stream, ret_tree); + optional err_code = ret_tree.get_optional(ret_json_key__any__err_code()); + if (err_code != none && (CreateTransactionErrorCode)*err_code != monero_transfer_utils::noError) { + auto err_msg = err_msg_from_err_code__create_transaction((CreateTransactionErrorCode)*err_code); + BOOST_REQUIRE_MESSAGE(false, err_msg); + } + mix_outs = ret_tree.get_child(ret_json_key__send__mix_outs()); + BOOST_REQUIRE(mix_outs.size() == using_outs.size()); + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_desc, mix_outs) + { + assert(mix_out_desc.first.empty()); // array elements have no names + cout << "bridge__transfers__send__amount: pre_step2: amount " << mix_out_desc.second.get("amount") << endl; + BOOST_REQUIRE(mix_out_desc.second.get_child("outputs").size() == 11); + } + prior_attempt_unspent_outs_to_mix_outs = ret_tree.get_child(ret_json_key__send__prior_attempt_unspent_outs_to_mix_outs_new()); + size_t outs_to_mix_outs_count = 0; + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *prior_attempt_unspent_outs_to_mix_outs) + { + ++outs_to_mix_outs_count; + string out_pub_key = outs_to_mix_outs_desc.first; + cout << "bridge__transfers__send__amount: pre_step2: output " << out_pub_key << endl; + BOOST_REQUIRE(outs_to_mix_outs_desc.second.size() == 11); + } + BOOST_REQUIRE(outs_to_mix_outs_count == using_outs.size()); + } { boost::property_tree::ptree root; root.put("final_total_wo_fee", *final_total_wo_fee_string); @@ -1446,17 +1746,8 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send_stagenet_coinbase) ss << OM_stagenet__unspent_outs_json; boost::property_tree::json_parser::read_json(ss, pt); boost::property_tree::ptree unspent_outs = pt.get_child("unspent_outs"); + optional prior_attempt_unspent_outs_to_mix_outs = none; // - // NOTE: in the real algorithm you should re-request this _each time step2 must be called_ - // this being input as JSON merely for convenience - boost::property_tree::ptree mix_outs; - { - boost::property_tree::ptree pt; - stringstream ss; - ss << OM_stagenet__rand_outs_json; - boost::property_tree::json_parser::read_json(ss, pt); - mix_outs = pt.get_child("mix_outs"); - } // // Send algorithm: bool tx_must_be_reconstructed = true; // for ease of writing this code, start this off true & structure whole thing as while loop @@ -1482,9 +1773,17 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send_stagenet_coinbase) root.add_child("unspent_outs", unspent_outs); if (fee_actually_needed_string != none) { BOOST_REQUIRE(construction_attempt_n > 1); + BOOST_REQUIRE(prior_attempt_unspent_outs_to_mix_outs != none); // - // for next round's integration - if it needs to re-enter... arg "passedIn_attemptAt_fee" - root.put("passedIn_attemptAt_fee", *fee_actually_needed_string); + // for next round's integration - if it needs to re-enter... arg "prior_attempt_size_calcd_fee" and "prior_attempt_unspent_outs_to_mix_outs" + root.put("prior_attempt_size_calcd_fee", *fee_actually_needed_string); + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *prior_attempt_unspent_outs_to_mix_outs) + { + string out_pub_key = outs_to_mix_outs_desc.first; + cout << "bridge__transfers__send__sweepDust: step1: prior output " << out_pub_key << endl; + BOOST_REQUIRE(outs_to_mix_outs_desc.second.size() == 11); + } + root.add_child("prior_attempt_unspent_outs_to_mix_outs", *prior_attempt_unspent_outs_to_mix_outs); } auto ret_string = serial_bridge::send_step1__prepare_params_for_get_decoys(args_string_from_root(root)); stringstream ret_stream; @@ -1548,6 +1847,57 @@ BOOST_AUTO_TEST_CASE(bridge__transfers__send_stagenet_coinbase) cout << "bridge__transfers__send_stagenet_coinbase: step1: final_total_wo_fee " << *final_total_wo_fee_string << endl; // } + boost::property_tree::ptree mix_outs; + { + boost::property_tree::ptree root; + root.add_child("using_outs", using_outs); // from step1 + // NOTE: in the real algorithm you should request _previously unseen + // mixouts from prior attempts each time pre_step2 must be called_ + // this being input as JSON merely for convenience + boost::property_tree::ptree mix_outs_from_server; + if (construction_attempt_n == 1) + { + boost::property_tree::ptree pt; + stringstream ss; + ss << DG_postsweep__rand_outs_json; + boost::property_tree::json_parser::read_json(ss, pt); + mix_outs_from_server = pt.get_child("mix_outs"); + } + else + { + root.add_child("prior_attempt_unspent_outs_to_mix_outs", *prior_attempt_unspent_outs_to_mix_outs); + } + root.add_child("mix_outs", mix_outs_from_server); + // + boost::property_tree::ptree ret_tree; + auto ret_string = serial_bridge::pre_step2_tie_unspent_outs_to_mix_outs_for_all_future_tx_attempts(args_string_from_root(root)); + stringstream ret_stream; + ret_stream << ret_string; + boost::property_tree::read_json(ret_stream, ret_tree); + optional err_code = ret_tree.get_optional(ret_json_key__any__err_code()); + if (err_code != none && (CreateTransactionErrorCode)*err_code != monero_transfer_utils::noError) { + auto err_msg = err_msg_from_err_code__create_transaction((CreateTransactionErrorCode)*err_code); + BOOST_REQUIRE_MESSAGE(false, err_msg); + } + mix_outs = ret_tree.get_child(ret_json_key__send__mix_outs()); + BOOST_REQUIRE(mix_outs.size() == using_outs.size()); + BOOST_FOREACH(boost::property_tree::ptree::value_type &mix_out_desc, mix_outs) + { + assert(mix_out_desc.first.empty()); // array elements have no names + cout << "bridge__transfers__send__amount: pre_step2: amount " << mix_out_desc.second.get("amount") << endl; + BOOST_REQUIRE(mix_out_desc.second.get_child("outputs").size() == 11); + } + prior_attempt_unspent_outs_to_mix_outs = ret_tree.get_child(ret_json_key__send__prior_attempt_unspent_outs_to_mix_outs_new()); + size_t outs_to_mix_outs_count = 0; + BOOST_FOREACH(boost::property_tree::ptree::value_type &outs_to_mix_outs_desc, *prior_attempt_unspent_outs_to_mix_outs) + { + ++outs_to_mix_outs_count; + string out_pub_key = outs_to_mix_outs_desc.first; + cout << "bridge__transfers__send__amount: pre_step2: output " << out_pub_key << endl; + BOOST_REQUIRE(outs_to_mix_outs_desc.second.size() == 11); + } + BOOST_REQUIRE(outs_to_mix_outs_count == using_outs.size()); + } { boost::property_tree::ptree root; root.put("final_total_wo_fee", *final_total_wo_fee_string);