wallet: complete hot-cold implementation for Carrot/FCMP++ - #52
wallet: complete hot-cold implementation for Carrot/FCMP++#52jeffro256 wants to merge 3 commits into
Conversation
|
Legacy |
7153239 to
bad06a9
Compare
d86ae71 to
1e7fc6a
Compare
1e7fc6a to
805a504
Compare
| const std::size_t prefix_size = magic_size + 1; | ||
| THROW_WALLET_EXCEPTION_IF(payload.size() <= prefix_size, | ||
| error::wallet_internal_error, "outputs payload too short"); | ||
| THROW_WALLET_EXCEPTION_IF(memcmp(payload.data(), KEY_IMAGE_EXPORT_FILE_MAGIC.data(), magic_size), |
|
Not sure if it's just not implemented yet or a bug but I can almost make and use an offline wallet with this PR but I get an error when running |
|
@ComputeryPony thanks for bringing that up, it was a bug. Does the latest commit fix it? It seems to work for me now |
Yup, that fixed it. Thanks! |
ae24a60 to
bb915d9
Compare
|
Rebased against |
53ca25a to
024fc9c
Compare
024fc9c to
0323828
Compare
5357be6 to
0d16773
Compare
0d16773 to
7909eca
Compare
7909eca to
98d00ef
Compare
b016ef2 to
c30b883
Compare
d96ffde to
8a3a68e
Compare
j-berman
left a comment
There was a problem hiding this comment.
Review part 1
I just noticed the cold_signing test is failing. #358 is passing so shouldn't be too bad to fix (I haven't looked into why).
I've reviewed the complete export / import outputs flow, and have reviewed some of the rest but haven't completed my review on the rest yet. This PR is a beast. I think there's a lot to really like here with many of the design decisions, thank you for the solid work on this.
There was a problem hiding this comment.
The pre-fork flow is currently borked on my end because of the following:
-
This default inserts Carrot outs into the front of
m_tx.vout. -
Causes
carrot::is_carrot_transaction_v1(td.m_tx)to return true here. -
Which incorrectly enters the Carrot flow for spending a non-Carrot out, ultimately throwing here. EDIT: tbc, I'm sure there are other spots where it's incorrectly entering the Carrot flow because of this issue, but this is where the test presently ends up throwing.
This could use some careful fixing. Presumably we should be assuming the m_tx saved on legacy wallet outs may already have incorrect/inconsistent outs since legacy wallets do the same thing. So we may want to avoid using carrot::is_carrot_transaction_v1 for m_tx, and instead we do actually want a helper to determine output type from a td. (EDIT: we could also de-serialize m_tx manually making sure it always uses the right type for the front outs, and make sure to always use the right out type)
j-berman
left a comment
There was a problem hiding this comment.
Review part 2/3
Exporting/importing key images looks good
| for (size_t n = offset; n < m_transfers.size(); ++n) | ||
| { | ||
| const transfer_details &td = m_transfers[n]; | ||
| const carrot::OutputOpeningHintVariant opening_hint = wallet::make_sal_opening_hint_from_transfer_details(td); |
There was a problem hiding this comment.
It looks like there's an inconsistency inside make_sal_opening_hint_from_transfer_details for legacy.
This section uses the additional tx pub key if it's there and the output is received to a subaddress. However, the normal scan flow detects receives using either tx pub key or additional, checking the tx pub key first. So someone could theoretically receive to a subaddress but using the tx pub key for derivation, and make_sal_opening_hint_from_transfer_details would still use the additional tx pub key.
There was a problem hiding this comment.
The behavior in make_sal_opening_hint_from_transfer_details() matches the legacy transaction construction behavior: https://github.com/monero-project/monero/blob/2c48374ecd2449c02bb400e5bcf20b7c6f11649b/src/device/device_default.cpp#L325-L326. I should either modify the scanning code to reject non-standard derivation paths, or make_sal_opening_hint_from_transfer_details() will require a view key device.
There was a problem hiding this comment.
Right it's only in the event someone makes a tx using something other than that exact flow that this would surface as a potential issue. The problem is a sender might have done this already, and an honest recipient's wallet2 would have detected it / been able to spend it, and so I think we need to account for that.
Couple more options to consider:
- Restructure the flow to not need to do crypto scanning ops on the opening hint?
- Store an optional
additional_ephemeral_tx_pubkeyon the legacy opening hint as well, then downstream crypto scanning ops on legacy opening hints could use it matching it to the normal scan flow behavior.
Dang it, this is a very good find. So we need to keep backwards-compatible support for deserializing |
I think that solution will work. It's a little uncomfortable there will be legacy Maybe would be nice to note somewhere that legacy |
|
EDIT: whoops, ok to ignore this. I see you already did this in the latest.
Because beta won't include #417, I think we would need to modify this logic to fill |
j-berman
left a comment
There was a problem hiding this comment.
Looks solid! Great work!!! Really just small comments remaining and design questions.
It's really a lot of code and not the simplest to navigate (not your fault, it's just a lot). In case it may be helpful for anyone wanting to review this code later, I wrote this guide as I worked through the call stack for the CLI for a complete Hot/Cold wallet tx construction flow:
Hot wallet
-
wallet2::create_transactions_2- Carrot:
make_pending_carrot_tx- mainly handles setting sources, dests, and change
- no ephemeral priv keys generated yet
- Legacy: basically same exact code as before
- Carrot:
-
wallet2::save_tx
wallet2::dump_tx_to_strwallet::cold::generate_unsigned_tx_set_from_pending_txs- Carrot:
- Generates a random 32 byte hot-cold seed used to derive many tx components downstream deterministically
compress_carrot_transaction_proposal_lossy- Selects only the necessary data the cold wallet needs to sign txs
- Calls
compress_carrot_selfsend_payment_proposal_lossy, which does the same
- Legacy: mainly just grabs expected data type
- Carrot:
wallet::cold::encrypt_unsigned_tx_set
wallet2::save_to_file- Saves file as
unsigned_monero_tx
- Saves file as
Cold wallet
wallet2::sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::vector<wallet2::pending_tx> &txs, std::function<bool(const wallet::cold::UnsignedTransactionSetVariant&)> accept_func, bool export_raw)
wallet2::load_unsigned_txwallet2::parse_unsigned_tx_from_strwallet::cold::decrypt_unsigned_tx_set
accept_func- Carrot:
wallet2::get_transaction_proposals_from_unsigned_txwallet::cold::make_supplemental_input_proposals_fetcherlambda- This makes a custom function that gets the best output to spend, and makes an opening hint for it
wallet::cold::get_transaction_proposals_from_unsigned_tx_setexpand_carrot_transaction_proposalsextend_supplemental_input_proposals_fetcher- extends lambda for best inputs also checking newly imported outputs
expand_carrot_transaction_proposal- Gets the best inputs to spend using extended lambda from above
expand_carrot_transaction_proposalexpand_carrot_normal_payment_proposal- Uses hot-cold seed to derive ephemeral tx priv key
expand_carrot_selfsend_payment_proposal- Uses hot-cold seed to derive ephemeral tx priv key
- Derives address from subaddr index
- Legacy: same as before
- Carrot:
wallet2::sign_tx(const wallet::cold::UnsignedTransactionSetVariant &exported_txs, const std::string &signed_filename, std::vector<wallet2::pending_tx> &txs, bool export_raw)wallet2::sign_tx_dump_to_strwallet2::sign_tx(const wallet::cold::UnsignedTransactionSetVariant &exported_txs, std::vector<wallet2::pending_tx> &ptx, wallet::cold::SignedTransactionSetVariant &signed_txs)- Legacy:
wallet2::sign_tx(const wallet::cold::UnsignedPreCarrotTransactionSet &exported_txs, std::vector<wallet2::pending_tx> &txs, wallet::cold::SignedFullTransactionSet &signed_txes)- Imports any exported outputs into the wallet
wallet::cold::sign_pre_carrot_tx_set- Basically all same code as before pulled out into
hot_cold.cpp(nice)
- Basically all same code as before pulled out into
- Add all tx keys to pending txs
- Add all of the wallet's key images to the final signed txs
- Carrot:
wallet2::sign_tx(const wallet::cold::UnsignedCarrotTransactionSetV1 &exported_txs, wallet::cold::SignedCarrotTransactionSetV1 &signed_txs)- Imports new outputs and sets
signed_txs.other_key_images(which are the newly imported outputs' key images) wallet::cold::sign_carrot_tx_set_v1- Uses the same lambda as the one implemented above to get the best output to spend and make an opening hint for it (Why's this duplicated?)
expand_carrot_transaction_proposal_and_rerandomized_outputs- Re-calls
expand_carrot_transaction_proposal expand_rerandomization_scalars- Uses hot-cold seed to derive scalars used for re-randomized output for each input
carrot::get_sorted_input_key_images_from_proposal_v1carrot::get_output_enote_proposals_from_proposal_v1- Gets all output enotes from the proposal using a Carrot core function, all randomness is expected set by this point including ephemeral priv keys
- Offsets the last
r_cin the re-randomized output to make sure we'll have a balanced pedersen commitment.- sums output amount blinding factors
- subtracts input amount blinding factors
- subtracts all re-randomized
r_cexcept the last - now left with what the last
r_cshould become
- Uses balanced re-randomized scalars to calculate final re-randomized outputs (includes the tx's pseudo out)
- Re-calls
try_sign_carrot_transaction_proposal_v1- Makes the SAL proofs
- Imports ephemeral priv keys into the wallet.
- Note: FCMP++ doesn't import the priv keys tied to tx id here, but to signable tx hash.
- Imports new outputs and sets
- Legacy:
wallet::cold::encrypt_signed_tx_set
wallet2::save_to_file- Saves file as
signed_monero_tx
- Saves file as
Hot wallet
wallet2::load_txwallet2::load_from_file
wallet2::parse_tx_from_strwallet::cold::decrypt_signed_tx_set- Prepares tx from a str
- Legacy: same
- Carrot:
wallet::cold::finalize_signed_carrot_tx_set_v1_into_full_set- Does 1 million collections again
expand_carrot_transaction_proposal_and_rerandomized_outputs- Does all the re-expansion done above, using hot-cold seed as before for all the randomness
get_output_enote_proposals_from_proposal_v1- More expansions / copies / crypto ops
finalize_fcmps_and_range_proofs- Shared code with normal tx flow for getting membership proof with tree cache, constructing FCMP++, and constructing BP+
make_pending_carrot_tx- Pre-existing code for constructing the final pending tx ready for submission to the daemon
- Sets all the imported wallet's key images on
full_signed_txs
accept_loaded_tx- Takes in the final signed tx and prints useful info back to user.
- Imports all key images both from the signed txs, and any new ones exported by the cold wallet.
wallet2::commit_tx
- Basically same as before, just submits the tx to the daemon.
- The one main difference here is that for FCMP++, the hot wallet also imports tx keys indexed by signable tx hash, in addition to the tx hash.
| m_tx_keys[signable_tx_hash] = ptx.tx_key; | ||
| m_additional_tx_keys[signable_tx_hash] = ptx.additional_tx_keys; |
There was a problem hiding this comment.
nit: This seems like it would only be reachable potentially for full wallets (since hot wallets set the tx key on pending txs to identity in finalize_signed_carrot_tx_set_v1_into_full_set). Do you have a use case in mind for this section?
There was a problem hiding this comment.
This unifies behavior between full and cold wallets: tx keys from both can be fetched using the signable tx hash. So upstream users can switch to only using signable tx hash to fetch tx keys and it'll work for everything.
| { | ||
| LOG_PRINT_L0("Not loading deprecated format"); | ||
| return false; | ||
| tx_proposals_out = wallet::cold::get_transaction_proposals_from_unsigned_tx_set(exported_txs, |
There was a problem hiding this comment.
Context: the cold wallet calls this function to import an unsigned tx. Then after calling this, the cold wallet signs the tx.
For the Carrot flow, this function appears to be doing everything that sign_carrot_tx_set_v1 then does again after this. Specifically the call to expand_carrot_transaction_proposal is duplicated, which seems unnecessary.
| const carrot::cryptonote_hierarchy_address_device &addr_dev, | ||
| const carrot::key_image_device &key_image_dev); | ||
| /** | ||
| * @brief Convert Carrot transaction intent into compressed hot-cold form, losing random fields |
There was a problem hiding this comment.
Checking my understanding: you "compress" to strictly data the cold wallet needs in order to construct the tx. The "random" fields are re-derived using the hot_cold_seed, so those fields don't need to be exported to the cold wallet. So you strip those fields from the proposal, hence "lossy."
I'm finding it a little confusing/hard to see what fields from the tx proposal that the hot wallet really didn't even need in the first place at this stage. Maybe it would be nice if the hot wallet was passing around a type that already didn't have the extraneous types to reduce cognitive load here/room for error, but I get how that might be a significant restructuring.
There was a problem hiding this comment.
One of the core design decisions of CarrotTransactionProposalV1 is that it contains everything needed to reconstruct a signable transaction hash, which is what the SA/L proof binds to. This means that e.g. HW devices talking to hot devices can "talk" to each other with a cold signing protocol, and come to a shared 100% exact same description of all parts of a tx that the signature signs for. HotColdCarrotTransactionProposalV1 does not adhere to this. For one, it doesn't have full opening hints like CarrotTransactionProposalV1 has, rather, it has one-time addresses references to some inferred set of opening hints.
There was a problem hiding this comment.
Also, the fact that a view-key device is required to view output information is not ideal for some the call sites elsewhere. In other words, I think that trying to use HotColdCarrotTransactionProposalV1 everywhere would shift a lot of the mess somewhere else. I like that it is compartamentalized here.
| std::vector<tx_reconstruct_variant_t> operator()(const UnsignedCarrotTransactionSetV1 &u) const | ||
| { | ||
| std::vector<carrot::CarrotTransactionProposalV1> carrot_tx_proposals; | ||
| expand_carrot_transaction_proposals(u, supplemental_input_proposals, addr_dev, carrot_tx_proposals); |
There was a problem hiding this comment.
So in the CLI, a cold wallet importing an unsigned tx set expands the Carrot tx proposals here when importing the unsigned tx set (accept_loaded_tx calls get_transaction_proposals_from_unsigned_tx_set).
And then it does it again when signing the unsigned tx set (sign_carrot_tx_set_v1 calls expand_carrot_transaction_proposal_and_rerandomized_outputs).
It seems like a major duplication of logic that also adds a lotta surface to the code. Any room here to avoid that duplication/only expand once?
| } | ||
| } | ||
| m_tx_keys.merge(main_tx_keys); | ||
| m_additional_tx_keys.merge(additional_tx_keys); |
There was a problem hiding this comment.
I'm wondering if cold wallets not being able to query for keys using tx id might lead to some disrupted flows. I think this is fine as is, but maybe we'll want signable tx hashes to be a more prominently featured item in the future (displayed in wallets / block explorer / queryable from the wallet) to work around this later on
There was a problem hiding this comment.
Yeah it probably will. It's the one breaking API change AFAIK in this whole chageset. At least one the live side, if you have the TXID, you can lookup its signable tx hash...
191e607 to
6794c57
Compare
6794c57 to
ddaf6d5
Compare
ddaf6d5 to
2b84550
Compare
|
Rebased due to merge of #424 |
|
i have been able to create , sign and broadcast transactions on stressnet with an implementation of this, not a detailed test/review but i just wanted to note something annoying about walet2 and unsigned tx set : it will stuff any unknown outputs into the unsigned set (for convenience presumably) so if you e.g. restore a large wallet using the view-key - your first cold signing will create a massive file - then the following tx's wont - like a force key image export/import. it seems that we are not doing this here? only the outputs that to be signed are included in the unsigned set yes? |
|
@plowsof It should at least export all unspent outputs on the sign request, by default. Did your test wallet have a lot of spent outputs, but few unspent? |
yep and i had used the same cache, indeed, and the idea to save space on unsigned tx set size by attempting to spend unverified outputs could only be done on a local node (trusted) as it would reveal too much for convenience. |
Carrot/FCMP++ hot-cold wallets
Design goals
Protocol goals
Wallet implementation goals
Testing
wallet2integration testsNew quirks
submit_transfermethod. This is a breaking change.Depends: #74, #75, #76, #77, #97, #98, #214, #216, #296, #404, #424, monero-project#10790
Will break this PR up into smaller pieces once the above are merged.