-
Notifications
You must be signed in to change notification settings - Fork 260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
zcash_client_sqlite: Use consistent criteria for detecting what accounts fund a transaction. #1306
Conversation
let mut account_q = conn.prepare_cached( | ||
"SELECT rn.account_id | ||
FROM orchard_received_notes rn | ||
WHERE rn.nf IN :nf_ptr", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AArnott one question about this: should an account be detected as funding the transaction even if it's an imported account? Or should this only apply to derived accounts? Same question for Sapling and transparent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My initial answer is "yes, of course", which makes me wonder if there's a downside that you see that I'm missing. Isn't scanning the same in only requiring the UFVK whether the account is derived or imported? I would expect downloaded or transmitted transactions to be recorded in the db exactly the same whether the account derivation information is in the db or not (as in the imported case).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For imported UFVK accounts, we definitely should do this tracking, because it's just metadata between accounts for which we have complete information. I don't believe we will ever select imported UIVK accounts here, as we don't have nullifier-deriving keys for those accounts, and thus can never populate rn.nf
.
We should separately ensure that we set the "to account" correctly on the send side for any source account that we would be fetching here (to ensure symmetry in the reported data), but that's likely going to require a migration to the sent_notes
table (as we currently use the account column there as a proxy for "account-internal send", which would be invalid in this context), so that does not need to happen in this PR (or even in the next release).
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1306 +/- ##
==========================================
+ Coverage 63.44% 63.73% +0.29%
==========================================
Files 121 122 +1
Lines 14142 14258 +116
==========================================
+ Hits 8972 9087 +115
- Misses 5170 5171 +1 ☔ View full report in Codecov by Sentry. |
I applied this change to my branch (combined with your tree panic fix), and early in sync, it failed with:
|
9278aab
to
6f876e5
Compare
Oops, fixed, thanks. |
That's much better. Everything in the sqlite table looks correct now. There is one remaining error in the balance impact calculation for the send-to-self transaction, but I believe that is in my own code. |
I just confirmed the remaining discrepancy was in my code and I have a fix for that too. Thanks for fixing this! |
6f876e5
to
9b4e9ea
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 9b4e9ea. The sent_notes
table migration is blocking, because the data inconsistency introduced in this PR will affect Zashi 1.0; the other comments would not because Zashi 1.0 will not support multiple accounts.
If the sent_notes
table migration is not feasible on the Zashi 1.0 timeline, then for this PR to be merged as part of Zashi 1.0 the new WalletInternal
parts that insert into sent_notes
must be gated on the wallet only containing a single account (which ensures that the cross-account data inconsistency cannot occur). This restriction could then be lifted in a subsequent post-Zashi-1.0 crate release when the necessary migrations are introduced.
assert_matches!( | ||
decrypt_and_store_transaction(&st.network(), st.wallet_mut(), &tx), | ||
Ok(_) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot determine what purpose this addition serves, given the lack of subsequent assertions to check the resulting funding account state. The changes in this PR to store_decrypted_transaction
do not contain any new error states, only warnings; is this addition left over from debugging?
Please either document what this assertion is verifying, or add additional subsequent assertions that make relevant checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's here to ensure that the view query is valid. When I have more time I will verify the return values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #1315
zcash_client_sqlite/src/lib.rs
Outdated
wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?; | ||
|
||
if let Some(account_id) = funding_account { | ||
// Even if the recipient address is external, record the send as internal. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocking:
This is the exact misuse of the sent_notes
account column that I mentioned earlier. The enum is named WalletInternal
, but prior to now AFAICT this has always been used as "account-internal". Given that any account in the wallet can be the funding account here, "wallet-internal" is ill-defined; the two accounts may have no common controller of spend authority (e.g. not be derived from the same seed). For example, it would make no sense for a note sent from a wallet account to ZecPages to be considered a "wallet-internal" transaction just because the ZecPages full viewing key is imported.
As an additional datapoint for this column currently meaning "account-internal", if we detect a note as going to an external address, and then later call create_account
such that the external address is learned to actually be a wallet address, we do not update the sent_notes
table to correct the information.
If we are going to make this change to how funding accounts are tracked, we need to do so in a way that ensures data consistency. That means either writing a migration to distinguish "account-internal" from "wallet-internal" in the sent_notes
table, or writing a migration to redefine the to_account_id
column as "amongst the accounts derived within or imported into this wallet database" and add code to rescan the entire wallet to update relationships whenever a new account is created or imported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The easy fix here is to restrict this check to verify that the funding account is the receiving account. When I refer to "external" here I'm simply meaning the zip32 external scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait a minute though, this is just populating the "from_account" column of the sent_notes table correctly; the to_account column should already be getting the correct data. So I don't think there is a problem here at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The recipient value being constructed in this fashion ensures that the to_account field gets set correctly. You're correct though that reusing the "WalletInternal" variant in this fashion is a change in semantics. It may be that we just need an additional Recipient variant, that can carry both the address (for external addresses) and the recipient account ID.
if let Some(account_id) = funding_account { | ||
if funding_accounts.len() > 1 { | ||
warn!( | ||
"More than one wallet account detected as funding transaction {:?}, selecting {:?}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a slight difference in the new logic: before if a transaction spent Sapling funds from one account and Orchard funds from another, the correct data would be recorded. Now, one of the accounts will be used for both. This is probably fine for the same reason as above: a future migration could insert the other accounts after changing the tables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utACK; no additional comments beyond @str4d's. Please at least file tickets for remaining issues.
…n output is recorded as sent.
…actions. Previously, if the funding account for a received transaction output was determined to be an account known to the wallet, the output was recorded as though it were sent to an internal (change) address of the wallet.
ab1f9a2
to
151e6e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed as of 151e6e5
zcash_client_sqlite/src/lib.rs
Outdated
) | ||
Recipient::InternalAccount { | ||
receiving_account: *output.account(), | ||
external_address: None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we ensure this is set correctly for cross-account transfers instead of setting it to None
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is correct because of the meaning of TransferType::Outgoing
and TransferType::WalletInternal
(and the corresponding TransferType::Incoming
branch guarantees that the transfer was decrypted with the wallet's external IVK).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean is, don't we need a separate TransferType
case to distinguish between wallet-internal (cross-account) and account-internal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I guess that for correct cross-account transfers (i.e. correctly treated as separate accounts, so that each account's external FVK sees the correct view), they should show up here as TransferType::Outgoing
. In that case, we should have external_address
available already for cross-account transfers and can set it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, and now going back into the PR diff, we're currently in the TransferType::WalletInternal
branch. Okay, so the change I think is missing here is that the other side of this branch should be tracking the receiving account for cross-account transfers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussion from offline: the received_notes tables would then duplicate exactly information that's available from the sent_notes table, and such duplication would be a potential source of error. So this should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we scan, we scan with all of the available IVKs, so in the case of a cross-account transfer it will always be detected as TransferType::Incoming
and will get handled by that branch, which will correctly find and set the source account. And the Outgoing
case is already handled correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's nothing that guarantees that (from the perspective of an implementation of the WalletWrite
trait), but I agree that's what the current code does in zcash_client_backend
which is what callers are most likely using, as I said in my subsequent review. It's just brittle.
531d21b
to
de0c7a2
Compare
de0c7a2
to
4ba7fbd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 4ba7fbd
*output.account(), | ||
tx_ref, | ||
output.index(), | ||
&recipient, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, the 3-way match now makes clear that there's an asymmetry currently between TransferType::Outgoing
and TransferType::Incoming
when it comes to cross-account transfers. Both will put an entry into sent_notes
, but only Incoming
fills in both the funding and receiving accounts; Outgoing
just sets the funding account but not the receiving account.
In general I think we are "probably okay" here, because we scan with IVKs before OVKs, so in the case of a cross-account transfer we will see Incoming
here instead of Outgoing
. However, in the future case of actually supporting imported accounts, there will be a bug here depending on the order in which accounts are imported. wallet::put_sent_output
overwrites the contents of the to_account_id
column, so if the receiving account gets handled before the sending account, then the collected data will be erased.
This is not simple to recover from: we cannot fix the data inconsistency in a migration if we don't have the raw transaction (to re-decrypt), and we are not guaranteed to have that if the caller is not running enhancement. So I think there are two possible options here:
- Track the receiving account in
TransferType::Outgoing
, as I had previously suggested. - Change
wallet::put_sent_output
to not overwrite knownto_account_id
values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the latter solution, because in general for outgoing transfers we don't know anything about the recipient (and it would be a pain to recover that in the send flow), whereas our usual upsert functionality is to not overwrite known data with nulls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main issue here is that our usual upsert functionality is avoiding overwriting stable known data; account IDs are not stable. But as long as we are aware of this hazard, then it should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Within the lifetime of an a wallet database, I can't see how account IDs could ever become unstable; if you swapped them around, you'd have to update all the foreign keys everywhere and this is no exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utACK 1719b01. Please open issues for the remaining multi-account comments.
Fixes #1304, #1282, #1238