-
Notifications
You must be signed in to change notification settings - Fork 34
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: import NoteFile
in client
#375
Conversation
9df2da3
to
3ef36d9
Compare
2e1c42f
to
2286e8d
Compare
3680cef
to
cd11167
Compare
4bfedc8
to
9769be1
Compare
215f26a
to
e4a2565
Compare
4d74c9d
to
48defb2
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.
Looking good! I left some comments, some related to code structure. Most of them don't need to be tackled now and can be left for a follow-up issue.
bbfe6ad
to
51d2a8b
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.
We should also test the flow for ignored notes (I think we have at least one test covering each tracked node variant but don't have any for ignored notes)
We may probably need a PR to update documentation in miden Base as well.
#[maybe_async] | ||
fn insert_input_note(&self, note: &InputNoteRecord) -> Result<(), StoreError>; | ||
fn insert_input_note(&self, note: InputNoteRecord) -> Result<(), StoreError>; | ||
|
||
/// Updates the inclusion proof of the input note with the provided ID | ||
fn update_note_inclusion_proof( | ||
&self, | ||
note_id: NoteId, | ||
inclusion_proof: NoteInclusionProof, | ||
) -> Result<(), StoreError>; | ||
|
||
/// Updates the metadata of the input note with the provided ID | ||
fn update_note_metadata( | ||
&self, | ||
note_id: NoteId, | ||
metadata: NoteMetadata, | ||
) -> Result<(), StoreError>; |
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 wouldn't do it in this PR, but it might be better to have a single update_note
in the public API
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.
At first I implemented it this way but I felt like we could end up with an invalid state. Maybe it's better if we join them as what was stated in this comment.
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.
why would you end up with an invalid state?
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 example, having an inclusion proof but with an "Expected" status
@@ -246,7 +253,7 @@ impl SqliteStore { | |||
Ok(notes) | |||
} | |||
|
|||
pub(crate) fn insert_input_note(&self, note: &InputNoteRecord) -> Result<(), StoreError> { | |||
pub(crate) fn insert_input_note(&self, note: InputNoteRecord) -> Result<(), StoreError> { |
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.
why did we change from reference to owned?
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 took the idea from the leftover notes PR. I feel like the insert_input_note
should consume the InputNoteRecord
when inserting. This is because internally, the note record is stored with some additional information that the original InputNoteRecord
may not have so the user may end up using it instead of the note you get from get_input_note
. Maybe it doesn't make too much sense, I could revert the decision if that's the 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.
Yeah, I think it makes sense that this consumes the note. I think maybe originally the motivation was to avoid some cloning. I don't mind if we want to roll this back though, we might have to make more changes to the API to make this more consistent all the way.
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'm ok either way, but we should document that behavior then. We shouldn't expect that a user just because the input note gets consumed won't clone the note beforehand and use the outdated note.
At least I imagine the situation:
- I call the function and use the note afterwards
- I get an error
- I clone and don't get the error anymore
- I work with the outdated note
Also, I agree with the fact that we should ideally standardize a way to interact with the API.
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! Left some comments and notes for other reviewers but overall I think we can merge this and tackle any remaining work in future PRs.
let expected_notes = maybe_await!(self.store.get_input_notes(NoteFilter::Expected))?; | ||
|
||
let note_tags: Vec<NoteTag> = [account_note_tags, stored_note_tags, uncommited_note_tags] | ||
.concat() | ||
.into_iter() | ||
.collect::<BTreeSet<NoteTag>>() | ||
.into_iter() | ||
let uncommited_note_tags: Vec<NoteTag> = expected_notes | ||
.iter() | ||
.filter_map(|note| note.metadata().map(|metadata| metadata.tag())) |
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 we could remove this, no?
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.
As discussed offline, this might still apply in some cases. I thought that uncommitted note tags could fall under one of the two:
- Ignored (tag is not on the persistent tag list): Would be updated separately
- Not ignored: Since tag is on the persistent tag list, these lines would not be needed because the persistent tag list is sent to the node anyway
But there could be cases where this does not fully apply (like notes created by transactions that wouldn't be ignored, or a case where the tag is removed after importing a note) and these cases are not currently covered by any other flow. So for completeness, I think we can leave this for now (and also considering that this would very likely be reworked in the future). ccing @bobbinth just in case
fn update_note_inclusion_proof( | ||
&self, | ||
note_id: NoteId, | ||
inclusion_proof: NoteInclusionProof, | ||
) -> Result<(), StoreError>; | ||
|
||
/// Updates the metadata of the input note with the provided ID | ||
fn update_note_metadata( | ||
&self, | ||
note_id: NoteId, | ||
metadata: NoteMetadata, | ||
) -> Result<(), StoreError>; |
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.
@bobbinth @dagarcia7 these are the new methods in the store. There are some alternatives to adding these functions:
- Using
insert_input_note()
to insert or update notes instead of assuming it's always a new note. - Adding a single
update_input_note()
that replaces the data for a single note ID with a new one instead of having 2 different functions.
Personally I think the first one is best but I see the value in having separate functions with clearer intentions.
@@ -92,9 +92,46 @@ pub trait Store { | |||
nullifiers | |||
} | |||
|
|||
/// Returns the notes that don't have their block header tracked | |||
#[maybe_async] | |||
fn get_notes_without_block_header(&self) -> Result<Vec<InputNoteRecord>, StoreError> { |
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.
Note for @bobbinth @dagarcia7: This is a new store function as well. It has a default implementation so it should not be much of a blocker. An alternative to this could be to create a loose function instead of this being a store method, but having it in the store makes it easier to optimize for each instance.
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 @tomyrd: We should document this trait function to mention how the default implementation works as we do in other functions.
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.
Done!
@@ -246,7 +253,7 @@ impl SqliteStore { | |||
Ok(notes) | |||
} | |||
|
|||
pub(crate) fn insert_input_note(&self, note: &InputNoteRecord) -> Result<(), StoreError> { | |||
pub(crate) fn insert_input_note(&self, note: InputNoteRecord) -> Result<(), StoreError> { |
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.
Yeah, I think it makes sense that this consumes the note. I think maybe originally the motivation was to avoid some cloning. I don't mind if we want to roll this back though, we might have to make more changes to the API to make this more consistent all the way.
#[derive(Debug, Parser, Clone)] | ||
#[clap(about = "Sync this client with the latest state of the Miden network.")] | ||
pub struct SyncCmd { | ||
/// If enabled, the ignored notes will also be updated by fetching them directly from the node. | ||
#[clap(short, long)] | ||
update_ignored: bool, | ||
} |
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 @bobbinth: It was not entirely clear from the issue discussion (unless I missed something) whether we wanted something like this flag or not, but considering whether we are OK with the CLI being more opinionated I think it's 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.
I don't think this flag was specifically mentioned in the issue but I felt like maybe updating ignored notes with each sync maybe defeated the purpose. I can remove it and always call client.update_ignored_notes
for the sync command if we want.
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 being more opinionated on the CLI side is fine - so, no issues with keeping this param.
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 are some minor issues to revisit but I think overall it's working as expected and we can tackle them in a follow up PR:
- note should show whether it is ignored or not (easier for the user of the CLI to track)
- if I export a committed note as partial, then I consume it and I import it in another client, the note goes from Expected => Committed but never gets marked as consumed
- also, every sync says:
2024-06-28T18:57:40.207197Z WARN miden_client::client::sync: Current partial MMR already contains the requested data State synced to block 167 New public notes: 0 Tracked notes updated: 1 Tracked notes consumed: 0 Tracked accounts updated: 0 Commited transactions: 0
- ... but no notes got actually updated
- this only happens for partial notes, since full notes are never ignored and id notes can't be imported (if they're private)
- I tried exporting a public committed note with export type
id
, then consumed the note, then imported it in another client and it failed.
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! Created an issue for follow-ups but we should be good to merge for now.
* Update import in cli to use `NoteFile` * Update CHANGELOG * Update cli export to use `NoteFile` * Fix build errors with file tag * Rename `import_input_note` to `import_note` * Implement `From<InputNoteRecord>` for `NoteFile` * Change `import_note` parameter to `NoteFile` * Fix integration tests * Implement `metadata` for `NoteDetails` * Add `get_notes_without_block_header` method to store * Add `ignored` and `imported_tag` to `InputNoteRecord` * Update `import_note` with new functionality * Fix integration tests * Update `ignored` field on note tag add * Update `export` to accept type of note file * Update MMR data for committed notes on sync * Update ignored notes on sync * Fix after rebase * Update tracked note when importing `NoteFile::NoteId` * Remove `NoteFile` conversion from `InputNoteRecord` * Check ephemeral tags when importing * Fix rebase * Fix integration tests * Remove tag from export cli command * Remove `--no-verify` flag from import cli command * Remove `InputNoteRecord` conversion on cli export * Update doc comments for `import_note` * Change suggestions * Add `maybe_await` * Update note when importing `NoteId` even if private * Only update ignored notes if specified in `sync` command * Change suggestions * Update `cli-reference` * Change suggestions * Add tests for ignored notes * Improve doc for `get_notes_without_block_header` * Change suggestions * Fix authentication hash calls for note * Temporarily change `NODE_BRANCH` in Makefile * force CI reset --------- Co-authored-by: igamigo <[email protected]>
closes #371
This PR implements the required logic to handle each type of imported
NoteFile
. Broadly, here's how the client handles each type:NoteId
: This notes are fetched from the node viaGetNotesById
(only if public) with it's metadata and inclusion proof. If the note was already tracked then its updated with the node information.NoteDetails+Option<Tag>
: The concept of "ignored" notes is introduced. If the note imported viaNoteDetails
has a tag that was already being tracked then it behaves like a normalExpected
imported note, updated after sync. If the tag is not tracked (or isNone
) then the note is "ignored", this means it's not updated with the rest of the notes via sync. These notes are updated "manually" by fetching the metadata+inclusion proof from the node viaGetNotesById
. Notes can have their ignored status removed if the tag that was imported with them starts being tracked.NoteWithProof
: These notes are imported as committed notes without fetching the mmr data. This means that they are committed but they can't be consumed. This required changing the sync so that it updates mmr information for committed notes that don't have it.A more in depth description of each case can be found in this comment and in the issue discussion.