-
-
Notifications
You must be signed in to change notification settings - Fork 128
Migrate vault entries to new schema #2092
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
base: master
Are you sure you want to change the base?
Conversation
5729ce6
to
b35f1f7
Compare
1423528
to
5f09dea
Compare
c56113b
to
7a4832a
Compare
api/resolvers/vault.js
Outdated
where: { userId_key: { userId: me.id, key: entry.key } }, | ||
data: { value: entry.value, iv: entry.iv } | ||
})) | ||
const wallets = await models.wallet.findMany({ where: { userId: me.id } }) |
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 think fetching the wallets outside of the transaction is fine since it's a read-only operation.
Else this needs to become an interactive transaction 👀
I tried to do this with a bulk update query instead of individual updates per wallet but Prisma doesn't seem to support that and I don't know how to update across multiple subtables via raw SQL depending on the wallet type.
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.
Regardless of whether this is interactive, this code is/was vulnerable to race conditions if a wallet is upserted while the vault key is updated.
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, I assume this is blocking this PR
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.
Fixed in bc7c722
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 wasn't blocking because even after your changes the code is vulnerable to a race "if a wallet is upserted while the vault key is updated." Your change only protects against hash change races on the user row.
e.g.
tx1: read wallets (wallets with ids [1,2])
tx2: insert wallet (wallet [3])
tx2: commit
tx1: update hash
tx1: commit
Wallet [3] is broken because it's encrypted with a different key.
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 above race is very unlikely to occur, but that's why it wasn't blocking. The real fix is nontrivial too: the only way to afaict is to give each vault entry a foreign key constraint on the correct key hash. That way, if the hash is updated any concurrent inserts will fail.
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.
Both transactions read the old vault key hash first, so one of them should fail when they try to update it at the end because they won't find the row anymore, no?
edit: Oh, I think I see what you mean. We can insert wallets without ever checking the current vault key hash.
5fa5ab2
to
bc7c722
Compare
Oh, forgot to put this out of draft yesterday |
bc7c722
to
efe7c1d
Compare
export function hasVault (wallet) { | ||
const def = getWalletByType(wallet.type) | ||
const vaultNames = vaultFieldNames(def) | ||
return vaultNames.some(name => get(wallet, `wallet.${name}Id`)) |
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.
Couldn't this just be written as
wallet.wallet[`${name}Id`]
The only other usage is on 93 which can be written as:
wallet[def.walletField][name]
Where else do you imagine using this? And why?
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.
Mhh, I don't really remember.
Might had something to do with me wanting to have a move
function in vaultNewSchematoTypedef
since it essentially requires "moving" object values around but I didn't end up using the move
I wrote after I wrote some primitives (get
, set
, remove
) for such a object helper function.
I'll remove it.
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 main gripe is the JSON trigger stuff. We'll need to figure out what to do there. Regardless of that, I'll need to test the migration before merging this.
You've described this PR as an intermediate step to something else so I understand this isn't "the end." I think it might be helpful for me (or any other reviewer) for you to describe "the end" that you're after so you can get feedback on it and the steps (like this) that you'll be taking to get there. Like, what's next after this PR? What will this PR allow you to objectively do that you weren't able to before? And, what will that allow you to do? And so on.
function includeFragment (wallet) { | ||
const include = walletDefs.reduce((acc, def) => { | ||
const names = vaultFieldNames(def) | ||
if (names.length === 0) return acc | ||
|
||
return { | ||
...acc, | ||
[def.walletField]: { | ||
include: names.reduce((acc2, name) => ({ | ||
...acc2, | ||
[name]: true | ||
}), {}) | ||
} | ||
} | ||
}, {}) | ||
|
||
if (wallet) { | ||
const def = getWalletByType(wallet.type) | ||
const names = vaultFieldNames(def) | ||
if (names.length === 0) return {} | ||
return { [def.walletField]: include[def.walletField] } | ||
} | ||
|
||
return include | ||
} |
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 reason we do the JSON trigger is to avoid doing a join with every wallet table like this.
If we're not going to use the JSON trigger, we should remove it. Alternatively, we should remove this N-table join and refactor the JSON triggers.
The con of the N-table join is that we pay the variable (and expected to grow) cost of reading all the wallets very frequently (even your update code does the joins). The pro is that we don't have to maintain triggers.
The con of the trigger is that maintaining the triggers can't be done in javascript and will require DB introspection to work with the changes you are trying to make in this PR (i.e. how do we know the wallet column references a vault entry? Probably by checking for a foreign key relationship.). The pro is that wallets are trivial to read from the DB both in terms of performance and the DB query to retrieve them.
tldr your changes can probably be made to make only reads expensive and writes efficient - relatively. The trigger can be made to make reads efficient and writes expensive - relatively.
f254015
to
1b85cde
Compare
Description
This PR replaces the existing
VaultEntry
table which links to a row in the genericWallet
table (and to a row in theusers
table) with a new tableVault
where columns in the specific wallet tables likeWalletNWC
link to it.TODO:
upsertWalletXXX
mutationVault
tableVaultEntry
tablewallets
queryVault
table instead of from oldVaultEntry
tablemodels.vaultEntry
callsinjectResolvers
(Remove unused vaultEntries during upsertWallet #2114)getVaultEntries
queryupdateVaultKey
mutationclearVault
mutationcheckWallet
jobTODO_newName
VaultEntry
tableChecklist
Are your changes backwards compatible? Please answer below:
Yes. All rows from
VaultEntry
are migrated toVault
with a link to them from their respective wallet tables and all LNC and WebLN rows fromWallet
are now also inserted into their new wallet tables.On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:
8
(7a4832a). Tested saving recv-only/send-only/recv+send of following wallets with/without device sync:(= every wallet except Blink)
since my internet is kind of bad here (attaching NWC times out Phoenixd
Tried to test migration with production data but metabase CSV export is broken. It returns NULL for empty strings so it broke on
users."vaultKeyHash"
being NULL.1 Didn't bother further if the import already doesn't work because of something like this.Okay, I haven't tested 727d931 as much as I did 7a4832a but it should still be a solid 7 at least!
I tested WebLN and various combinations of LNbits with device sync and not etc.
For frontend changes: Tested on mobile, light and dark mode? Please answer below:
n/a
Did you introduce any new environment variables? If so call them out explicitly here:
no
Footnotes
not sure why this isn't a nullable column but uses an empty string as the default but I'll look into this in a future PR ... ↩