-
Notifications
You must be signed in to change notification settings - Fork 4
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
feat(query): update block processor to include subaccounts into LPs #279
Conversation
): Promise< | ||
{ relevantTx: RelevantTx; recoveredSourceRecords: RecoveredSourceRecords } | undefined | ||
> => { | ||
let txId: TransactionId | undefined; // If set, that means this tx is relevant and should be returned to the caller | ||
let subaccount: AddressIndex | undefined; |
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 change collects subaccounts from SpendableNoteRecord
s but there's no way to get AddressIndex for all possible actions, that's why it might be undefined
. However, it is enough to get them for the purpose of the tasks – to identify LPs per subaccount
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.
comment: undefined AddressIndex
in this context can also mean that the SNR is associated with the main account. Maybe we should eventually just store the AddressIndex for the main account as zero.
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.
lgtm.
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 this is a great start, and I want to test it.
@@ -494,7 +494,7 @@ export class BlockProcessor implements BlockProcessorInterface { | |||
|
|||
// Nullifier is published in network when a note is spent or swap is claimed. | |||
private async resolveNullifiers(nullifiers: Nullifier[], height: bigint) { | |||
const spentNullifiers = new Set<Nullifier>(); | |||
const spentNullifiers = new Map<Nullifier, SpendableNoteRecord | SwapRecord>(); |
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.
Before we merge, I want to circle back to this and a few other things with the data modeling. What's the lifetime of the spent nullifier map? How big is a SNR? An OOM seems somewhat unlikely but I don't totally understand the approach yet.
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 a good flag, still thinking through the proposed data structure.
the spentNullifiers
map is a local variable that only lives for the duration of processing a single block in processBlock()
. It isn’t a class field or stored anywhere long term. SNRs are stored in the spendable_notes
indexeddb table using the saveSpendableNote()
storage method, and the state payload is marked as spent by appending a "heightSpent" field in storage.
SNRs are ~700 bytes.
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.
OK this seems totally fine, round it to 1KB, that's 1M SNRs. sgtm
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.
after reviewing the code, Map<Nullifier, SpendableNoteRecord | SwapRecord>
is actually the optimal and clever approach to minimize db fetches required to understand what subaccount a particular LP position is eventually associated with.
stepping through the code, prior to calling resolveNullifiers()
, indexdb stores the successfully trial decrypted spendable notes in a table who's primary key is the commitment and secondary index is the nullifier. So in theory, you have all the information to know which SNR is associated with what commitment and nullifier directly in storage, but don't yet know which transaction that commitment is associated with, because we haven't processed and saved the transaction information at this stage of the block processor.
resolveNullifiers()
will subsequently query any SNR / SwapRecord stored in indexeddb that match any nullifier in the incoming compact block, and the record <> nullifier pair are stored in the spentNullifiers map.
up until this point in the block processing, we know a few things:
- which SNR / SwapRecord is associated with the user
- what commitment + nullifier is associated with what SNR / SwapRecord
- don't know which transaction is associated with the SNR / SwapRecord or commitment
then, if we found a commitment or nullifier relevant to the user in that block, we request all the transactions for that block height from the full node, and filter the specific transaction relevant to the user. This is where the original spentNullifiers
map comes into play. We have a spentNullifiers
map (Map<Nullifier, SpendableNoteRecord | SwapRecord>) and commitmentRecords
map (Map<StateCommitment, SpendableNoteRecord | SwapRecord>), and once we know which commitment / nullifier inside the transaction's actions is associated with the user's commitment / nullifier (stored in those maps in memory), we know we transaction is the user's, and we return the transaction associated with it's relevant subacount by deriving the AddressIndex
from the SNR itself.
After identifying the transaction, we have a special method called processTransactions that will identify the LP position relevant to the user and save it alongside the subaccount.
anyways, if we didn't originally have this map, once we knew which nullifier is associated with the user, we'd need to perform another db query to fetch the relevant SNR from the table, and subsequently derive the AddressIndex.
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.
thanks for this great explanation, @TalDerei! This is exactly what the PR does in the smallest details
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 really great work! left some feedback and good to merge once we bump the deps and green CI + @erwanor unblocks
@@ -317,7 +317,7 @@ export class BlockProcessor implements BlockProcessorInterface { | |||
spentNullifiers, | |||
recordsByCommitment, | |||
blockTx, | |||
addr => this.viewServer.isControlledAddress(addr), | |||
addr => this.viewServer.getIndexByAddress(addr), |
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.
suggestion: should we actually remove this? It's used soley in getRelevantIbcRelay
to associate IBC actions with their respective sub-accounts indices, but if the objective of this PR is only handling LP positions, it might be unnecessary. We don't have a use case for filtering IBC actions by account indexes yet. thoughts?
that said, we can still keep the getIndexByAddress
modifications in the view service method already implemented in penumbra-zone/web#2006.
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.
put back the isControlledAddress
method. Truly, no need to know AddressIndex for IBC relay actions for now
): Promise< | ||
{ relevantTx: RelevantTx; recoveredSourceRecords: RecoveredSourceRecords } | undefined | ||
> => { | ||
let txId: TransactionId | undefined; // If set, that means this tx is relevant and should be returned to the caller | ||
let subaccount: AddressIndex | undefined; |
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.
comment: undefined AddressIndex
in this context can also mean that the SNR is associated with the main account. Maybe we should eventually just store the AddressIndex for the main account as zero.
* In terms of minting notes in the shielded pool, three IBC actions are relevant: | ||
* - MsgRecvPacket (containing an ICS20 inbound transfer) | ||
* - MsgAcknowledgement (containing an error acknowledgement, thus triggering a refund on our end) | ||
* - MsgTimeout | ||
*/ | ||
const hasRelevantIbcRelay = ( | ||
const getRelevantIbcRelay = ( |
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.
comment: see comments above about possibly not modifying this since we don't have a use case for it yet.
const recoveredSourceRecords: RecoveredSourceRecords = []; | ||
|
||
// for spend/swapClaim transaction actions |
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.
suggestion: can we please add better code comments here for extracting nullifiers + commitments from actions?
for (const [spentNullifier, spendableNoteRecord] of spentNullifiers) { | ||
const nullifier = txNullifiers.find(txNullifier => spentNullifier.equals(txNullifier)); | ||
if (nullifier) { | ||
txId ??= await generateTxId(tx); | ||
subaccount = getAddressIndexFromNote(spendableNoteRecord); | ||
} | ||
} | ||
|
||
// For output/swap/swapClaim transaction actions | ||
const txCommitments = getCommitmentsFromActions(tx); | ||
for (const [stateCommitment, spendableNoteRecord] of commitmentRecords) { | ||
if (txCommitments.some(txCommitment => stateCommitment.equals(txCommitment))) { | ||
txId ??= await generateTxId(tx); | ||
subaccount = getAddressIndexFromNote(spendableNoteRecord); |
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.
comment: minor optimization for the future; If getCommitmentsFromActions
is successful, we can skip getNullifiersFromActions
and relevantIbcRelay
because they calculate the same txId and subaccount fields.
we can address that separately when we conduct more in-depth block processor performance optimizations. I’ll open a separate issue for 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.
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 can test later, I think this has received enough eyes to not block on it
Relates to penumbra-zone/web#2006 (Web) and penumbra-zone/dex-explorer#329 (DEX)
To run this change:
pnpm dev:pack
PR in feat: #1989: subaccount filter inownedPositionIds
penumbra-zone/web#2006pnpm add:tgz $PATH_TO_WEB_REPO/packages/*/*.tgz
To test:
protobuf
package and run it locallyThis PR must be merged after the packages from Web are published and applied here.