Replies: 6 comments 19 replies
-
|
Caution Disclaimer: my current (likely incorrect) understanding of the issue. Does the following statement cover the requirements:
and this our current design dilemma
I'm hopeful we can carve out a sensible design by adding some restrictions. e.g. you can't target ancient block heights, only the most recent |
Beta Was this translation helpful? Give feedback.
-
|
Why are only note and nullifier queries considered for privacy, but not accounts and transactions that target them? |
Beta Was this translation helpful? Give feedback.
-
Account state syncingThis is not directly related to the current sync endpoints, but if we do decide to split the state sync into 3 different endpoints, we'll need to sync account states separately. But even without this, we need to figure out how to handle account syncing, especially when account state could be large. We currently have 3 endpoints related to account syncing:
Overall, I think we should get rid of The new
|
Beta Was this translation helpful? Give feedback.
-
|
@bobbinth replying out-of-thread to split the topics up a bit.
|
Beta Was this translation helpful? Give feedback.
-
Syncing mapsI think the idea sounds great. We could probably elide the block number from message SyncStorageMapsResponse {
repeated StorageMapUpdate updates = 1;
fixed32 chain_tip = 2;
}
message StorageMapUpdate {
uint32 slot_idx = 1;
Digest key = 2;
Digest value = 3;
} |
Beta Was this translation helpful? Give feedback.
-
|
Once we have #1185 and #1186 implemented, I think we'll be in a good position to finish this refactoring. Specifically, we'll have the following endpoints to help us sync the state of the client:
Out of these
I think there are two things we can change about this:
The new message SyncTransactionsRequest {
// Block number from which to start sync (i.e., transactions before this block would not be returned)
fixed32 block_from = 1;
// A list of accounts against which the requested transactions have been executed.
repeated AccountId account_ids = 2;
}And the response message could look like so: message SyncTransactionsResponse {
// The block number of the last transaction included in this response.
fixed32 block_num = 1;
// Chain tip at the moment of the request.
fixed32 chain_tip = 2;
// Headers of the transactions that were executed against the specified accounts
repeated TransactionHeader = 3;
}
// This message differs slightly from the `TransactionHeader` struct in miden-base in that it includes
// block_num and more data about output notes - but maybe that's fine.
message TranactionHeader {
// ID of the account against which the transaction was executed
AccountId accountId = 1;
// Block number at which the transaction was included into the chain.
fixed32 block_num = 2;
// Commitment to the account state before the transaction was executed.
Digest initial_state_commitment = 3;
// Commitment to the account state after the transaction was executed.
Digest final_state_commitment = 4;
// A list of nullifiers for notes consumed by the transaction.
repeated Nullifier input_notes = 5;
// Info about notes created in the transaction.
repeated NoteSyncRecord output_notes = 6;
}This should cover all the data reaming from the removal of
cc @igamigo in case I'm missing something. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
SyncStateThe
SyncStateendpoint is the main endpoint utilized by clients to get data related to blocks and the MMR. It is the main gateway for clients to synchronize in order to maintain a valid view of the chain. Up until recently, this process would work something like the following:requestto the RPC server. It contains:block_num: Latest block known to the calleraccount_ids: List of account IDs that the caller is interested innote_tags: List of tags that the caller is looking fornullifiers: List of nullifier of notes for which the caller wants updatesresponsethat contains the first block header where the note tree contains a note that matches any of the request'snote_tags:chain_tip: Latest block in the chainblock_header: Contains a specific block headermmr_delta: Contains the MMR delta to update the caller's MMR, from the child ofrequest.block_numuntil the response'sblock_header.block_numaccounts: Contains updated account data (ID, block number and account hash) for any account inrequest.accountstransactions: List of transaction IDs, alongside account IDs that match any of the request'saccount_idsnotes: List of note inclusion proofs that belong to notes created by any of the request'saccount_ids, or notes that have tags that match any of the request'snote_tags. These proofs validate against the block header's note rootnullifiers: List of nullifiers that match any of the request'snullifiers, alongside the block number in which they were spentIf no block matches against the caller's request, the block header for the current chain tip is returned
PartialMmrby instantiating the latest known peaks (corresponds to the MMR at the parent of the latest block number), adding the latest known block number, and then applyingresponse.mmr_deltaThe above can be executed in a loop until the caller reaches the chain tip.
Some details worth noting here:
SyncStateis getting MMR-related data. A user can know all details of aNoteand have a valid inclusion proof against the block header's note root, but it also needs the block header itself alongside an MMR authentication path to execute a transaction that consumes it-
request.nullifiersare prefixes of the actual identifiers that the caller is interested in (16-bit). Additionallyrequest.note_tagsare 32-bit identifiers (which in turn can contain a subset of the account ID's). This allows for matching against a larger set of identifiers which provides privacy. This means that as callers, we can receive a bunch of MMR-related data that we don't care about. For instance, we might be expecting the inclusion of a specific private note of which the note tag matches some other set of notes. Callers are then responsible of filtering the data (eg, this may require analyzing data and figuring out if our account could possibly consume these notes, etc.)SyncStatestep, the caller can add its nullifier to therequest.nullifiersfield on subsequent sync requests before reaching the chain tip in order to reflect that the caller now wants to know if this note was consumed. Same applies forrequest.note_tags: If the caller was expecting an imported note to be recorded on-chain, itsnote_tagmay be taken out from therequeston successive requests.SyncNotesThere is also another syncing endpoint:$Client A$ wants to share a private note ($Client B$ can be divided in two different scenarios:
SyncNotes. This endpoint was introduced to support scenarios related to sharing notes between clients. For example, a case whereNoteFile::NoteDetails) toNoteFile::NoteDetails's expected block number. This client can now add the note's tag to the list of tags andSyncStatewill eventually yield this note's inclusion details alongside MMR dataSyncStatedoes not have a way to support this. The client could runGetNotesById, but this leaks data and would also lack the block header with the MMR inclusion proof (for whichGetBlockHeaderByNumbercould be used).In the second case, we can use
SyncNotesto request specific note tags added from a starting block, and receive a note inclusion proof with the related MMR/block data.CheckNullifiersByPrefixThis is the last related RPC endpoint. This one essentially receives a list of nullifier prefixes, and returns a list of consumed nullifiers.
Initially this was also introduced to support scenarios related to importing (similar to the above): If the importing user's client is behind relative the note's inclusion block, the nullifier will naturally be received on
SyncState. However, if the client has moved past the note's inclusion block, the nullifier might have already been included in the chain and the user would have no way of knowing this based onSyncStatealone.Recent changes
Recently, there were some changes to enable streaming
SyncStateresponses (#174). One consequence of streaming responses from the node is that the caller cannot modify therequest.nullifiersbetween sync state steps anymore, because a single request yields multiple streamed/pushed responses.To account for this, it was decided that we could remove nullifiers from
SyncStaterequests and responses. The client could then stream sync state responses, and once this process is done, they can callCheckNullifiersByPrefixto request all nullifiers at once (#707 and #713) starting fromrequest.block_num.The probability of a 16-bit prefix matching a random nullifier is
1 / 2^16. If you haveNblocks,Mrequested prefixes, andKnullifiers per block, the expected number of matches isN * M * K / 2^16.Assuming 32 bytes per nullifiers: A lower bound of 500 nullifiers per block matching against 10 prefixes yields roughly 0.076 matches per block. Syncing over a day of blocks (~3 blocks per second, 28,880 blocks per day), this becomes ~2,200 matches or ~70 KB.
A higher bound (1,000 nullifiers per block with 50 prefixes, 10x the previous scenario) gives about ~22,000 matches for a day of blocks, or ~704 KB. An average scenario between these two would be around around ~9,900 matches/day, so ~316 KB.
There might be something I'm missing in terms of the math (and maybe the estimates are not realistic at all), but it's something we want to keep in mind when comparing the new approach to the old one:
SyncStaterequest and oneCheckNullifiersByPrefixrequest. As analyzed above, these could be within the 0.5-1 MB range regularly, which does not seem desirable.SyncStateresponses. Responses are smaller because there are as many requests as there are matching blocks betweenrequest.block_numand the chain tip (in turn, these are based exclusively on whetherrequest.note_tagsmatch or any of therequest.account_idscreated a note)The implications here affect mostly the node in the sense that the database is now required to handle two distinct data retrieval patterns. With the new approach, the streaming
SyncStateresponses remain comparatively small, but theCheckNullifiersByPrefixrequest can return a large, aggregated set of data if many nullifiers match the requested prefixes (and if we are requesting data starting from an early block number). This shift may lead to increased I/O, locked DB, and processing overhead on the node's database. As a result, we might want to analyze additional indexing, caching, or query optimizations (both at the RPC and DB level) to ensure that the node can efficiently serve this data without it becoming abottleneck.So overall, it might not be entirely clear whether the recent changes make sense in terms of optimizing the whole flow. Some alternatives that we could explore and that were briefly discussed before:
SyncState, maybe having one for accounts, one for notes, one for nullifiers. They could probably work independently and the client would have to be responsible of bookkeeping to make sure data is coherent. Each should probably have from-to parameters.There are tradeoffs to each of these and so, while designs should be discussed we also probably want to be able to measure the impact of any changes in the design overall
Beta Was this translation helpful? Give feedback.
All reactions