From f8895303f4b579e9bd95790e18f0e102ed2b498a Mon Sep 17 00:00:00 2001 From: tomyrd Date: Thu, 16 May 2024 17:06:20 -0300 Subject: [PATCH 01/33] Add Consuming note status --- src/store/note_record/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index b39ae4d85..56702816b 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -51,6 +51,7 @@ pub enum NoteStatus { Pending, Committed, Consumed, + Consuming, } impl From for u8 { @@ -59,6 +60,7 @@ impl From for u8 { NoteStatus::Pending => 0, NoteStatus::Committed => 1, NoteStatus::Consumed => 2, + NoteStatus::Consuming => 3, } } } @@ -70,6 +72,7 @@ impl TryFrom for NoteStatus { 0 => Ok(NoteStatus::Pending), 1 => Ok(NoteStatus::Committed), 2 => Ok(NoteStatus::Consumed), + 3 => Ok(NoteStatus::Consuming), _ => Err(DeserializationError::InvalidValue(value.to_string())), } } @@ -94,6 +97,7 @@ impl Display for NoteStatus { NoteStatus::Pending => write!(f, "Pending"), NoteStatus::Committed => write!(f, "Committed"), NoteStatus::Consumed => write!(f, "Consumed"), + NoteStatus::Consuming => write!(f, "Consuming"), } } } From ff55be109eb00e8f007ec42517922443cb2df457 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Thu, 16 May 2024 17:19:12 -0300 Subject: [PATCH 02/33] Infer Consuming status from store information --- src/store/sqlite_store/notes.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 1d58dbfe8..b43e08abb 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -430,6 +430,14 @@ fn parse_input_note( Some(account_id) => Some(AccountId::try_from(account_id as u64)?), None => None, }; + + // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain + let status = if let (NoteStatus::Committed, Some(_)) = (status, consumer_account_id) { + NoteStatus::Consuming + } else { + status + }; + Ok(InputNoteRecord::new( id, recipient, From bd17b34e15175ce67b44481a88e423726a979aa2 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 20 May 2024 12:35:13 -0300 Subject: [PATCH 03/33] Add some useful comments --- src/client/sync.rs | 4 ++++ src/store/note_record/mod.rs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client/sync.rs b/src/client/sync.rs index 5a2ae1c39..53feff890 100644 --- a/src/client/sync.rs +++ b/src/client/sync.rs @@ -743,6 +743,10 @@ fn get_transactions_to_commit( // account be included in a single block. If that happens, we'll need to rewrite // this check. + // TODO: If all input notes were consumed but the account hashes don't match, it means that + // another transaction consumed them and that the current transaction failed. We should + // handle this case in the future and rollback the store accordingly. + t.input_note_nullifiers.iter().all(|n| nullifiers.contains(n)) && t.output_notes.iter().all(|n| note_ids.contains(&n.id())) && account_hash_updates.iter().any(|(account_id, account_hash)| { diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index 56702816b..816f9c0c7 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -48,10 +48,14 @@ pub use output_note_record::OutputNoteRecord; // ================================================================================================ #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum NoteStatus { + // Note is pending to be commited on chain Pending, + // Note has been commited on chain Committed, - Consumed, + // Note has been consumed locally but not yet nullified on chain Consuming, + // Note has been nullified on chain + Consumed, } impl From for u8 { From aa3ed6b8832b71555c1b066862756932d941788e Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 20 May 2024 12:40:52 -0300 Subject: [PATCH 04/33] Add change to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb186af68..6c3cc70bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +* New note status added to reflect more possible states. + ## v0.3.0 (2024-05-17) * Added swap transactions and example flows on integration tests. From 19015a7597d100de492a96586604d03b14068793 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 20 May 2024 12:43:15 -0300 Subject: [PATCH 05/33] Add PR to CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3cc70bc..9a40cc416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* New note status added to reflect more possible states. +* New note status added to reflect more possible states (#355). ## v0.3.0 (2024-05-17) From 6973f803482cf42d9f420935fb131e2ece47f0f3 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 20 May 2024 14:19:03 -0300 Subject: [PATCH 06/33] Remove consuming notes from list consumable --- src/client/notes.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/notes.rs b/src/client/notes.rs index 4a78c68c3..bbd35b8ab 100644 --- a/src/client/notes.rs +++ b/src/client/notes.rs @@ -46,6 +46,10 @@ impl Client let mut relevant_notes = Vec::new(); for input_note in commited_notes { + if input_note.consumer_account_id().is_some() { + continue; + } + let account_relevance = note_screener.check_relevance(&input_note.clone().try_into()?)?; From 9a3dc6955679d223d5dc26ddc09c03a5dcd543fa Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 20 May 2024 14:29:46 -0300 Subject: [PATCH 07/33] Remove hardcoded note status names from sqlite store code --- src/store/sqlite_store/notes.rs | 17 ++++++++++++----- src/store/sqlite_store/sync.rs | 25 ++++++++++++++++++------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index b43e08abb..f04496fef 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -117,9 +117,15 @@ impl<'a> NoteFilter<'a> { match self { NoteFilter::All => base, - NoteFilter::Committed => format!("{base} WHERE status = 'Committed'"), - NoteFilter::Consumed => format!("{base} WHERE status = 'Consumed'"), - NoteFilter::Pending => format!("{base} WHERE status = 'Pending'"), + NoteFilter::Committed => { + format!("{base} WHERE status = '{}'", NoteStatus::Committed) + }, + NoteFilter::Consumed => { + format!("{base} WHERE status = '{}'", NoteStatus::Consumed) + }, + NoteFilter::Pending => { + format!("{base} WHERE status = '{}'", NoteStatus::Pending) + }, NoteFilter::Unique(_) | NoteFilter::List(_) => { format!("{base} WHERE note.note_id IN rarray(?)") }, @@ -231,11 +237,12 @@ impl SqliteStore { /// Returns the nullifiers of all unspent input notes pub fn get_unspent_input_note_nullifiers(&self) -> Result, StoreError> { - const QUERY: &str = "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status = 'Committed'"; + const QUERY: &str = + "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status = ?"; self.db() .prepare(QUERY)? - .query_map([], |row| row.get(0)) + .query_map([NoteStatus::Committed.to_string()], |row| row.get(0)) .expect("no binding parameters used in query") .map(|result| { result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then( diff --git a/src/store/sqlite_store/sync.rs b/src/store/sqlite_store/sync.rs index 174da825c..76814f236 100644 --- a/src/store/sqlite_store/sync.rs +++ b/src/store/sqlite_store/sync.rs @@ -5,7 +5,10 @@ use super::SqliteStore; use crate::{ client::sync::StateSyncUpdate, errors::StoreError, - store::sqlite_store::{accounts::update_account, notes::insert_input_note_tx}, + store::{ + sqlite_store::{accounts::update_account, notes::insert_input_note_tx}, + NoteStatus, + }, }; impl SqliteStore { @@ -93,13 +96,19 @@ impl SqliteStore { // Update spent notes for nullifier in nullifiers.iter() { const SPENT_INPUT_NOTE_QUERY: &str = - "UPDATE input_notes SET status = 'Consumed' WHERE json_extract(details, '$.nullifier') = ?"; + "UPDATE input_notes SET status = ? WHERE json_extract(details, '$.nullifier') = ?"; let nullifier = nullifier.to_hex(); - tx.execute(SPENT_INPUT_NOTE_QUERY, params![nullifier])?; + tx.execute( + SPENT_INPUT_NOTE_QUERY, + params![NoteStatus::Consumed.to_string(), nullifier], + )?; const SPENT_OUTPUT_NOTE_QUERY: &str = - "UPDATE output_notes SET status = 'Consumed' WHERE json_extract(details, '$.nullifier') = ?"; - tx.execute(SPENT_OUTPUT_NOTE_QUERY, params![nullifier])?; + "UPDATE output_notes SET status = ? WHERE json_extract(details, '$.nullifier') = ?"; + tx.execute( + SPENT_OUTPUT_NOTE_QUERY, + params![NoteStatus::Consumed.to_string(), nullifier], + )?; } Self::insert_block_header_tx(&tx, block_header, new_mmr_peaks, block_has_relevant_notes)?; @@ -125,13 +134,14 @@ impl SqliteStore { // Update output notes const COMMITTED_OUTPUT_NOTES_QUERY: &str = - "UPDATE output_notes SET status = 'Committed', inclusion_proof = json(:inclusion_proof) WHERE note_id = :note_id"; + "UPDATE output_notes SET status = :status , inclusion_proof = json(:inclusion_proof) WHERE note_id = :note_id"; tx.execute( COMMITTED_OUTPUT_NOTES_QUERY, named_params! { ":inclusion_proof": inclusion_proof, ":note_id": note_id.inner().to_hex(), + ":status": NoteStatus::Committed.to_string(), }, )?; } @@ -147,7 +157,7 @@ impl SqliteStore { serde_json::to_string(metadata).map_err(StoreError::InputSerializationError)?; const COMMITTED_INPUT_NOTES_QUERY: &str = - "UPDATE input_notes SET status = 'Committed', inclusion_proof = json(:inclusion_proof), metadata = json(:metadata) WHERE note_id = :note_id"; + "UPDATE input_notes SET status = :status , inclusion_proof = json(:inclusion_proof), metadata = json(:metadata) WHERE note_id = :note_id"; tx.execute( COMMITTED_INPUT_NOTES_QUERY, @@ -155,6 +165,7 @@ impl SqliteStore { ":inclusion_proof": inclusion_proof, ":metadata": metadata, ":note_id": input_note.id().inner().to_hex(), + ":status": NoteStatus::Committed.to_string(), }, )?; } From 68534e0f76800c642c5987d45e815473a7bc75db Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 29 May 2024 14:44:40 -0300 Subject: [PATCH 08/33] Remove `ConsumableNote` type --- src/cli/notes.rs | 10 +++++----- src/client/mod.rs | 3 +-- src/client/note_screener.rs | 15 +++++++-------- src/client/notes.rs | 22 +++++----------------- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/cli/notes.rs b/src/cli/notes.rs index 9887c4acd..4bf928e53 100644 --- a/src/cli/notes.rs +++ b/src/cli/notes.rs @@ -6,7 +6,7 @@ use miden_client::{ client::{ rpc::NodeRpcClient, transactions::transaction_request::known_script_roots::{P2ID, P2IDR, SWAP}, - ConsumableNote, + NoteConsumability, }, errors::{ClientError, IdPrefixFetchError}, store::{InputNoteRecord, NoteFilter as ClientNoteFilter, NoteStatus, OutputNoteRecord, Store}, @@ -354,14 +354,14 @@ where fn print_consumable_notes_summary<'a, I>(notes: I) -> Result<(), String> where - I: IntoIterator, + I: IntoIterator)>, { let mut table = create_dynamic_table(&["Note ID", "Account ID", "Relevance"]); - for consumable_note in notes { - for relevance in &consumable_note.relevances { + for (note, relevances) in notes { + for relevance in relevances { table.add_row(vec![ - consumable_note.note.id().to_hex(), + note.id().to_hex(), relevance.0.to_string(), relevance.1.to_string(), ]); diff --git a/src/client/mod.rs b/src/client/mod.rs index 7254eaf00..766d8a6b4 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -21,9 +21,8 @@ mod notes; pub mod store_authenticator; pub mod sync; pub mod transactions; -pub use note_screener::NoteRelevance; +pub use note_screener::NoteConsumability; pub(crate) use note_screener::NoteScreener; -pub use notes::ConsumableNote; // MIDEN CLIENT // ================================================================================================ diff --git a/src/client/note_screener.rs b/src/client/note_screener.rs index c5618cac5..753b92e88 100644 --- a/src/client/note_screener.rs +++ b/src/client/note_screener.rs @@ -17,6 +17,8 @@ pub enum NoteRelevance { After(u32), } +pub type NoteConsumability = (AccountId, NoteRelevance); + impl fmt::Display for NoteRelevance { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -41,10 +43,7 @@ impl NoteScreener { /// Does a fast check for known scripts (P2ID, P2IDR, SWAP). We're currently /// unable to execute notes that are not committed so a slow check for other scripts is currently /// not available. - pub fn check_relevance( - &self, - note: &Note, - ) -> Result, ScreenerError> { + pub fn check_relevance(&self, note: &Note) -> Result, ScreenerError> { let account_ids = BTreeSet::from_iter(self.store.get_account_ids()?); let script_hash = note.script().hash().to_string(); @@ -61,7 +60,7 @@ impl NoteScreener { fn check_p2id_relevance( note: &Note, account_ids: &BTreeSet, - ) -> Result, ScreenerError> { + ) -> Result, ScreenerError> { let mut note_inputs_iter = note.inputs().values().iter(); let account_id_felt = note_inputs_iter .next() @@ -83,7 +82,7 @@ impl NoteScreener { fn check_p2idr_relevance( note: &Note, account_ids: &BTreeSet, - ) -> Result, ScreenerError> { + ) -> Result, ScreenerError> { let mut note_inputs_iter = note.inputs().values().iter(); let account_id_felt = note_inputs_iter .next() @@ -126,7 +125,7 @@ impl NoteScreener { &self, note: &Note, account_ids: &BTreeSet, - ) -> Result, ScreenerError> { + ) -> Result, ScreenerError> { let note_inputs = note.inputs().to_vec(); if note_inputs.len() != 9 { return Ok(Vec::new()); @@ -174,7 +173,7 @@ impl NoteScreener { &self, _note: &Note, account_ids: &BTreeSet, - ) -> Result, ScreenerError> { + ) -> Result, ScreenerError> { // TODO: try to execute the note script against relevant accounts; this will // require querying data from the store Ok(account_ids diff --git a/src/client/notes.rs b/src/client/notes.rs index bbd35b8ab..bf3159bd1 100644 --- a/src/client/notes.rs +++ b/src/client/notes.rs @@ -7,23 +7,13 @@ use miden_objects::{ use miden_tx::{ScriptTarget, TransactionAuthenticator}; use tracing::info; -use super::{note_screener::NoteRelevance, rpc::NodeRpcClient, Client}; +use super::{note_screener::NoteConsumability, rpc::NodeRpcClient, Client}; use crate::{ client::NoteScreener, errors::ClientError, store::{InputNoteRecord, NoteFilter, OutputNoteRecord, Store}, }; -// TYPES -// -------------------------------------------------------------------------------------------- -/// Contains information about a note that can be consumed -pub struct ConsumableNote { - /// The consumable note - pub note: InputNoteRecord, - /// Stores which accounts can consume the note and it's relevance - pub relevances: Vec<(AccountId, NoteRelevance)>, -} - impl Client { // INPUT NOTE DATA RETRIEVAL // -------------------------------------------------------------------------------------------- @@ -39,7 +29,7 @@ impl Client pub fn get_consumable_notes( &self, account_id: Option, - ) -> Result, ClientError> { + ) -> Result)>, ClientError> { let commited_notes = self.store.get_input_notes(NoteFilter::Committed)?; let note_screener = NoteScreener::new(self.store.clone()); @@ -57,14 +47,12 @@ impl Client continue; } - relevant_notes.push(ConsumableNote { - note: input_note, - relevances: account_relevance, - }); + relevant_notes.push((input_note, account_relevance)); } if let Some(account_id) = account_id { - relevant_notes.retain(|note| note.relevances.iter().any(|(id, _)| *id == account_id)); + relevant_notes + .retain(|(_, relevances)| relevances.iter().any(|(id, _)| *id == account_id)); } Ok(relevant_notes) From d064cbc250fd80f64edb60f5fee20a053a63ec6a Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 29 May 2024 15:02:23 -0300 Subject: [PATCH 09/33] Add function to get the consumability of a note --- src/client/notes.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/client/notes.rs b/src/client/notes.rs index bf3159bd1..f22cfdaec 100644 --- a/src/client/notes.rs +++ b/src/client/notes.rs @@ -58,6 +58,17 @@ impl Client Ok(relevant_notes) } + /// Returns the consumability of the provided note. + pub fn get_note_consumability( + &self, + note: InputNoteRecord, + ) -> Result, ClientError> { + let note_screener = NoteScreener::new(self.store.clone()); + note_screener + .check_relevance(¬e.clone().try_into()?) + .map_err(|err| err.into()) + } + /// Returns the input note with the specified hash. pub fn get_input_note(&self, note_id: NoteId) -> Result { Ok(self From a2add0faf6656a580f9e752177caaa1f19ca7b7d Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 29 May 2024 15:03:21 -0300 Subject: [PATCH 10/33] Refactor `get_consumable_notes` --- src/client/notes.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/client/notes.rs b/src/client/notes.rs index f22cfdaec..cc42702b6 100644 --- a/src/client/notes.rs +++ b/src/client/notes.rs @@ -23,7 +23,7 @@ impl Client self.store.get_input_notes(filter).map_err(|err| err.into()) } - /// Returns input notes that are able to be consumed by the account_id. + /// Returns the input notes and their consumability. /// /// If account_id is None then all consumable input notes are returned. pub fn get_consumable_notes( @@ -40,9 +40,13 @@ impl Client continue; } - let account_relevance = + let mut account_relevance = note_screener.check_relevance(&input_note.clone().try_into()?)?; + if let Some(account_id) = account_id { + account_relevance.retain(|(id, _)| *id == account_id); + } + if account_relevance.is_empty() { continue; } @@ -50,11 +54,6 @@ impl Client relevant_notes.push((input_note, account_relevance)); } - if let Some(account_id) = account_id { - relevant_notes - .retain(|(_, relevances)| relevances.iter().any(|(id, _)| *id == account_id)); - } - Ok(relevant_notes) } From d2f32adfe4e971fea20b36dfb9f1a7565253cbef Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 29 May 2024 15:21:06 -0300 Subject: [PATCH 11/33] Rename `Consuming` to `Processing` --- src/store/note_record/mod.rs | 8 ++++---- src/store/sqlite_store/notes.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index 816f9c0c7..47cbb6c0b 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -53,7 +53,7 @@ pub enum NoteStatus { // Note has been commited on chain Committed, // Note has been consumed locally but not yet nullified on chain - Consuming, + Processing, // Note has been nullified on chain Consumed, } @@ -64,7 +64,7 @@ impl From for u8 { NoteStatus::Pending => 0, NoteStatus::Committed => 1, NoteStatus::Consumed => 2, - NoteStatus::Consuming => 3, + NoteStatus::Processing => 3, } } } @@ -76,7 +76,7 @@ impl TryFrom for NoteStatus { 0 => Ok(NoteStatus::Pending), 1 => Ok(NoteStatus::Committed), 2 => Ok(NoteStatus::Consumed), - 3 => Ok(NoteStatus::Consuming), + 3 => Ok(NoteStatus::Processing), _ => Err(DeserializationError::InvalidValue(value.to_string())), } } @@ -101,7 +101,7 @@ impl Display for NoteStatus { NoteStatus::Pending => write!(f, "Pending"), NoteStatus::Committed => write!(f, "Committed"), NoteStatus::Consumed => write!(f, "Consumed"), - NoteStatus::Consuming => write!(f, "Consuming"), + NoteStatus::Processing => write!(f, "Processing"), } } } diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 0f1db71e1..8d7ddd97f 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -451,7 +451,7 @@ fn parse_input_note( // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain let status = if let (NoteStatus::Committed, Some(_)) = (status, consumer_account_id) { - NoteStatus::Consuming + NoteStatus::Processing } else { status }; From f39cfed6481fee44d822c36d91bbd037ceb71bf1 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 29 May 2024 15:46:35 -0300 Subject: [PATCH 12/33] Fix clippy --- src/client/mod.rs | 2 +- tests/integration/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 766d8a6b4..b836b5fb9 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -21,8 +21,8 @@ mod notes; pub mod store_authenticator; pub mod sync; pub mod transactions; -pub use note_screener::NoteConsumability; pub(crate) use note_screener::NoteScreener; +pub use note_screener::{NoteConsumability, NoteRelevance}; // MIDEN CLIENT // ================================================================================================ diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 3323acf76..8f4b50a7a 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -340,7 +340,7 @@ async fn test_get_consumable_notes() { // Check that note is consumable by both accounts let consumable_notes = client.get_consumable_notes(None).unwrap(); - let relevant_accounts = &consumable_notes.first().unwrap().relevances; + let relevant_accounts = &consumable_notes.first().unwrap().1; assert_eq!(relevant_accounts.len(), 2); assert!(!client.get_consumable_notes(Some(from_account_id)).unwrap().is_empty()); assert!(!client.get_consumable_notes(Some(to_account_id)).unwrap().is_empty()); From 3b9842074497539b5a1cc0feeeff54e55fbebb1b Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 29 May 2024 17:58:35 -0300 Subject: [PATCH 13/33] Fix doc comments --- src/client/note_screener.rs | 3 +++ src/store/note_record/mod.rs | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client/note_screener.rs b/src/client/note_screener.rs index 753b92e88..7a56c4cc4 100644 --- a/src/client/note_screener.rs +++ b/src/client/note_screener.rs @@ -17,6 +17,9 @@ pub enum NoteRelevance { After(u32), } +/// Represents the consumability of a note by a specific account. +/// +/// The tuple contains the account id that may consume the note and the moment it will become relevant. pub type NoteConsumability = (AccountId, NoteRelevance); impl fmt::Display for NoteRelevance { diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index 47cbb6c0b..fd8b90408 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -48,13 +48,13 @@ pub use output_note_record::OutputNoteRecord; // ================================================================================================ #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum NoteStatus { - // Note is pending to be commited on chain + /// Note is pending to be commited on chain Pending, - // Note has been commited on chain + /// Note has been commited on chain Committed, - // Note has been consumed locally but not yet nullified on chain + /// Note has been consumed locally but not yet nullified on chain Processing, - // Note has been nullified on chain + /// Note has been nullified on chain Consumed, } From d6db780aecc9cd34a691d5663b52d0305d191368 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Fri, 31 May 2024 14:09:44 -0300 Subject: [PATCH 14/33] Store `Processing` status in `SqliteStore` --- src/store/sqlite_store/notes.rs | 23 ++++++++++++----------- src/store/sqlite_store/store.sql | 8 ++++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 8d7ddd97f..bd4f77234 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -238,11 +238,17 @@ impl SqliteStore { /// Returns the nullifiers of all unspent input notes pub fn get_unspent_input_note_nullifiers(&self) -> Result, StoreError> { const QUERY: &str = - "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status = ?"; + "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status IN rarray(?)"; self.db() .prepare(QUERY)? - .query_map([NoteStatus::Committed.to_string()], |row| row.get(0)) + .query_map( + params_from_iter([ + NoteStatus::Committed.to_string(), + NoteStatus::Processing.to_string(), + ]), + |row| row.get(0), + ) .expect("no binding parameters used in query") .map(|result| { result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then( @@ -342,24 +348,26 @@ pub fn update_note_consumer_tx_id( note_id: NoteId, consumer_tx_id: TransactionId, ) -> Result<(), StoreError> { - const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; + const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET status = :status, consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; tx.execute( UPDATE_INPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), + ":status": NoteStatus::Processing.to_string(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; - const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; + const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET status = :status, consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; tx.execute( UPDATE_OUTPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), + ":status": NoteStatus::Processing.to_string(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; @@ -449,13 +457,6 @@ fn parse_input_note( None => None, }; - // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain - let status = if let (NoteStatus::Committed, Some(_)) = (status, consumer_account_id) { - NoteStatus::Processing - } else { - status - }; - Ok(InputNoteRecord::new( id, recipient, diff --git a/src/store/sqlite_store/store.sql b/src/store/sqlite_store/store.sql index b2fb3fd48..3c1aca4e2 100644 --- a/src/store/sqlite_store/store.sql +++ b/src/store/sqlite_store/store.sql @@ -74,7 +74,7 @@ CREATE TABLE input_notes ( recipient BLOB NOT NULL, -- the note recipient assets BLOB NOT NULL, -- the serialized NoteAssets, including vault hash and list of assets status TEXT CHECK( status IN ( -- the status of the note - either pending, committed or consumed - 'Pending', 'Committed', 'Consumed' + 'Pending', 'Committed', 'Processing', 'Consumed' )), inclusion_proof JSON NULL, -- JSON consisting of the following fields: @@ -107,7 +107,7 @@ CREATE TABLE input_notes ( json_extract(inclusion_proof, '$.note_path') IS NOT NULL )) CONSTRAINT check_valid_metadata_json CHECK (metadata IS NULL OR (json_extract(metadata, '$.sender') IS NOT NULL AND json_extract(metadata, '$.tag') IS NOT NULL)) - CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status != 'Pending') + CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status in ('Processing', 'Consumed')) ); -- Create output notes table @@ -116,7 +116,7 @@ CREATE TABLE output_notes ( recipient BLOB NOT NULL, -- the note recipient assets BLOB NOT NULL, -- the serialized NoteAssets, including vault hash and list of assets status TEXT CHECK( status IN ( -- the status of the note - either pending, committed or consumed - 'Pending', 'Committed', 'Consumed' + 'Pending', 'Committed', 'Processing', 'Consumed' )), inclusion_proof JSON NULL, -- JSON consisting of the following fields: @@ -156,7 +156,7 @@ CREATE TABLE output_notes ( json_extract(details, '$.inputs') IS NOT NULL AND json_extract(details, '$.serial_num') IS NOT NULL )) - CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status != 'Pending') + CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status in ('Processing', 'Consumed')) ); -- Create note's scripts table, used for both input and output notes From f8accf896eab675e0723348f236791b7c8c50d07 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Fri, 31 May 2024 14:14:20 -0300 Subject: [PATCH 15/33] Override `get_unspent_input_note_nullifiers` function from `Store` --- src/store/sqlite_store/mod.rs | 27 +++++++++++++++++++++++++-- src/store/sqlite_store/notes.rs | 27 +-------------------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/store/sqlite_store/mod.rs b/src/store/sqlite_store/mod.rs index 9794ce28f..a08fc24ec 100644 --- a/src/store/sqlite_store/mod.rs +++ b/src/store/sqlite_store/mod.rs @@ -1,13 +1,14 @@ use alloc::collections::BTreeMap; use core::cell::{RefCell, RefMut}; +use std::rc::Rc; use miden_objects::{ accounts::{Account, AccountId, AccountStub, AuthSecretKey}, crypto::merkle::{InOrderIndex, MmrPeaks}, - notes::NoteTag, + notes::{NoteTag, Nullifier}, BlockHeader, Digest, Word, }; -use rusqlite::{vtab::array, Connection}; +use rusqlite::{types::Value, vtab::array, Connection}; use super::{ ChainMmrNodeFilter, InputNoteRecord, NoteFilter, OutputNoteRecord, Store, TransactionFilter, @@ -19,6 +20,7 @@ use crate::{ }, config::StoreConfig, errors::StoreError, + store::NoteStatus, }; mod accounts; @@ -233,6 +235,27 @@ impl Store for SqliteStore { fn get_account_auth_by_pub_key(&self, pub_key: Word) -> Result { self.get_account_auth_by_pub_key(pub_key) } + + fn get_unspent_input_note_nullifiers(&self) -> Result, StoreError> { + const QUERY: &str = + "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status IN rarray(?)"; + let unspent_filters = Rc::new(vec![ + Value::from(NoteStatus::Committed.to_string()), + Value::from(NoteStatus::Processing.to_string()), + ]); + self.db() + .prepare(QUERY)? + .query_map([unspent_filters], |row| row.get(0)) + .expect("no binding parameters used in query") + .map(|result| { + result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then( + |v: String| { + Digest::try_from(v).map(Nullifier::from).map_err(StoreError::HexParseError) + }, + ) + }) + .collect::, _>>() + } } // TESTS diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index bd4f77234..17ce7f4de 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -5,7 +5,7 @@ use clap::error::Result; use miden_objects::{ accounts::AccountId, crypto::utils::{Deserializable, Serializable}, - notes::{NoteAssets, NoteId, NoteInclusionProof, NoteMetadata, NoteScript, Nullifier}, + notes::{NoteAssets, NoteId, NoteInclusionProof, NoteMetadata, NoteScript}, transaction::TransactionId, Digest, }; @@ -234,31 +234,6 @@ impl SqliteStore { Ok(tx.commit()?) } - - /// Returns the nullifiers of all unspent input notes - pub fn get_unspent_input_note_nullifiers(&self) -> Result, StoreError> { - const QUERY: &str = - "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status IN rarray(?)"; - - self.db() - .prepare(QUERY)? - .query_map( - params_from_iter([ - NoteStatus::Committed.to_string(), - NoteStatus::Processing.to_string(), - ]), - |row| row.get(0), - ) - .expect("no binding parameters used in query") - .map(|result| { - result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then( - |v: String| { - Digest::try_from(v).map(Nullifier::from).map_err(StoreError::HexParseError) - }, - ) - }) - .collect::, _>>() - } } // HELPERS From ff640e7b9ca0ecf20d8a524c0511f4e93709fef0 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Fri, 31 May 2024 14:22:24 -0300 Subject: [PATCH 16/33] Add note filter for `Processing` notes --- src/cli/notes.rs | 2 ++ src/store/mod.rs | 3 +++ src/store/sqlite_store/notes.rs | 3 +++ 3 files changed, 8 insertions(+) diff --git a/src/cli/notes.rs b/src/cli/notes.rs index efdbc01e0..85678a1c4 100644 --- a/src/cli/notes.rs +++ b/src/cli/notes.rs @@ -29,6 +29,7 @@ pub enum NoteFilter { Pending, Committed, Consumed, + Processing, Consumable, } @@ -41,6 +42,7 @@ impl TryInto> for NoteFilter { NoteFilter::Pending => Ok(ClientNoteFilter::Pending), NoteFilter::Committed => Ok(ClientNoteFilter::Committed), NoteFilter::Consumed => Ok(ClientNoteFilter::Consumed), + NoteFilter::Processing => Ok(ClientNoteFilter::Processing), NoteFilter::Consumable => Err("Consumable filter is not supported".to_string()), } } diff --git a/src/store/mod.rs b/src/store/mod.rs index 14e8764b4..f78ecd253 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -78,6 +78,7 @@ pub trait Store { let nullifiers = self .get_input_notes(NoteFilter::Committed)? .iter() + .chain(self.get_input_notes(NoteFilter::Processing)?.iter()) .map(|input_note| Ok(Nullifier::from(Digest::try_from(input_note.nullifier())?))) .collect::, _>>(); @@ -277,6 +278,8 @@ pub enum NoteFilter<'a> { /// Return a list of pending notes ([InputNoteRecord] or [OutputNoteRecord]). These represent notes for which the store /// does not have anchor data. Pending, + /// Return a list of notes that are currently being processed. + Processing, /// Return a list containing the note that matches with the provided [NoteId]. List(&'a [NoteId]), /// Return a list containing the note that matches with the provided [NoteId]. diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 17ce7f4de..0f56fa827 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -126,6 +126,9 @@ impl<'a> NoteFilter<'a> { NoteFilter::Pending => { format!("{base} WHERE status = '{}'", NoteStatus::Pending) }, + NoteFilter::Processing => { + format!("{base} WHERE status = '{}'", NoteStatus::Processing) + }, NoteFilter::Unique(_) | NoteFilter::List(_) => { format!("{base} WHERE note.note_id IN rarray(?)") }, From e2c46593b446ecd1ce57ea218cf89d309a547efe Mon Sep 17 00:00:00 2001 From: tomyrd Date: Fri, 31 May 2024 15:19:42 -0300 Subject: [PATCH 17/33] Fix comment --- src/client/note_screener.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/note_screener.rs b/src/client/note_screener.rs index 7a56c4cc4..ff6793b5a 100644 --- a/src/client/note_screener.rs +++ b/src/client/note_screener.rs @@ -19,7 +19,7 @@ pub enum NoteRelevance { /// Represents the consumability of a note by a specific account. /// -/// The tuple contains the account id that may consume the note and the moment it will become relevant. +/// The tuple contains the account ID that may consume the note and the moment it will become relevant. pub type NoteConsumability = (AccountId, NoteRelevance); impl fmt::Display for NoteRelevance { From 5388b3efcafcfcffd35e2fb4a287587b4bf586b9 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Tue, 4 Jun 2024 15:52:42 -0300 Subject: [PATCH 18/33] Remove `Processing` status from sql table --- src/store/sqlite_store/notes.rs | 23 +++++++++++++++++------ src/store/sqlite_store/store.sql | 8 ++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 0f56fa827..d415e44f9 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -118,7 +118,10 @@ impl<'a> NoteFilter<'a> { match self { NoteFilter::All => base, NoteFilter::Committed => { - format!("{base} WHERE status = '{}'", NoteStatus::Committed) + format!( + "{base} WHERE status = '{}' AND consumer_transaction_id IS NULL", + NoteStatus::Committed + ) }, NoteFilter::Consumed => { format!("{base} WHERE status = '{}'", NoteStatus::Consumed) @@ -127,7 +130,10 @@ impl<'a> NoteFilter<'a> { format!("{base} WHERE status = '{}'", NoteStatus::Pending) }, NoteFilter::Processing => { - format!("{base} WHERE status = '{}'", NoteStatus::Processing) + format!( + "{base} WHERE status = '{}' AND consumer_transaction_id IS NOT NULL", + NoteStatus::Committed, + ) }, NoteFilter::Unique(_) | NoteFilter::List(_) => { format!("{base} WHERE note.note_id IN rarray(?)") @@ -326,26 +332,24 @@ pub fn update_note_consumer_tx_id( note_id: NoteId, consumer_tx_id: TransactionId, ) -> Result<(), StoreError> { - const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET status = :status, consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; + const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; tx.execute( UPDATE_INPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), - ":status": NoteStatus::Processing.to_string(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; - const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET status = :status, consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; + const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; tx.execute( UPDATE_OUTPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), - ":status": NoteStatus::Processing.to_string(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; @@ -435,6 +439,13 @@ fn parse_input_note( None => None, }; + // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain + let status = if let (NoteStatus::Committed, Some(_)) = (status, consumer_account_id) { + NoteStatus::Processing + } else { + status + }; + Ok(InputNoteRecord::new( id, recipient, diff --git a/src/store/sqlite_store/store.sql b/src/store/sqlite_store/store.sql index 3c1aca4e2..b2fb3fd48 100644 --- a/src/store/sqlite_store/store.sql +++ b/src/store/sqlite_store/store.sql @@ -74,7 +74,7 @@ CREATE TABLE input_notes ( recipient BLOB NOT NULL, -- the note recipient assets BLOB NOT NULL, -- the serialized NoteAssets, including vault hash and list of assets status TEXT CHECK( status IN ( -- the status of the note - either pending, committed or consumed - 'Pending', 'Committed', 'Processing', 'Consumed' + 'Pending', 'Committed', 'Consumed' )), inclusion_proof JSON NULL, -- JSON consisting of the following fields: @@ -107,7 +107,7 @@ CREATE TABLE input_notes ( json_extract(inclusion_proof, '$.note_path') IS NOT NULL )) CONSTRAINT check_valid_metadata_json CHECK (metadata IS NULL OR (json_extract(metadata, '$.sender') IS NOT NULL AND json_extract(metadata, '$.tag') IS NOT NULL)) - CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status in ('Processing', 'Consumed')) + CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status != 'Pending') ); -- Create output notes table @@ -116,7 +116,7 @@ CREATE TABLE output_notes ( recipient BLOB NOT NULL, -- the note recipient assets BLOB NOT NULL, -- the serialized NoteAssets, including vault hash and list of assets status TEXT CHECK( status IN ( -- the status of the note - either pending, committed or consumed - 'Pending', 'Committed', 'Processing', 'Consumed' + 'Pending', 'Committed', 'Consumed' )), inclusion_proof JSON NULL, -- JSON consisting of the following fields: @@ -156,7 +156,7 @@ CREATE TABLE output_notes ( json_extract(details, '$.inputs') IS NOT NULL AND json_extract(details, '$.serial_num') IS NOT NULL )) - CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status in ('Processing', 'Consumed')) + CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status != 'Pending') ); -- Create note's scripts table, used for both input and output notes From a4746893dbcf837e8bc2cf8180675b34f6c4bc03 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 5 Jun 2024 15:42:09 -0300 Subject: [PATCH 19/33] Add additional info to `NoteStatus` enum --- src/store/note_record/mod.rs | 99 +++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 36 deletions(-) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index fd8b90408..ba2d45e25 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use miden_objects::{ + accounts::AccountId, assembly::{Assembler, ProgramAst}, notes::NoteScript, utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, @@ -46,62 +47,88 @@ pub use output_note_record::OutputNoteRecord; // NOTE STATUS // ================================================================================================ +pub const NOTE_STATUS_PENDING: &str = "Pending"; +pub const NOTE_STATUS_COMMITTED: &str = "Committed"; +pub const NOTE_STATUS_CONSUMED: &str = "Consumed"; +pub const NOTE_STATUS_PROCESSING: &str = "Processing"; + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum NoteStatus { - /// Note is pending to be commited on chain - Pending, + /// Note is pending to be commited on chain. + Pending { created_at: u64 }, /// Note has been commited on chain - Committed, + Committed { block_height: u64 }, /// Note has been consumed locally but not yet nullified on chain - Processing, + Processing { + consumer_account_id: AccountId, + submited_at: u64, + }, /// Note has been nullified on chain - Consumed, -} - -impl From for u8 { - fn from(value: NoteStatus) -> Self { - match value { - NoteStatus::Pending => 0, - NoteStatus::Committed => 1, - NoteStatus::Consumed => 2, - NoteStatus::Processing => 3, - } - } -} - -impl TryFrom for NoteStatus { - type Error = DeserializationError; - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(NoteStatus::Pending), - 1 => Ok(NoteStatus::Committed), - 2 => Ok(NoteStatus::Consumed), - 3 => Ok(NoteStatus::Processing), - _ => Err(DeserializationError::InvalidValue(value.to_string())), - } - } + Consumed { + consumer_account_id: Option, + block_height: u64, + }, } impl Serializable for NoteStatus { fn write_into(&self, target: &mut W) { - target.write_bytes(&[(*self).into()]); + match self { + NoteStatus::Pending { created_at } => { + target.write_u8(0); + target.write_u64(*created_at); + }, + NoteStatus::Committed { block_height } => { + target.write_u8(1); + target.write_u64(*block_height); + }, + NoteStatus::Processing { consumer_account_id, submited_at } => { + target.write_u8(2); + target.write_u64(*submited_at); + consumer_account_id.write_into(target); + }, + NoteStatus::Consumed { consumer_account_id, block_height } => { + target.write_u8(3); + target.write_u64(*block_height); + consumer_account_id.write_into(target); + }, + } } } impl Deserializable for NoteStatus { fn read_from(source: &mut R) -> Result { - let enum_byte = u8::read_from(source)?; - enum_byte.try_into() + let status = source.read_u8()?; + match status { + 0 => { + let created_at = source.read_u64()?; + Ok(NoteStatus::Pending { created_at }) + }, + 1 => { + let block_height = source.read_u64()?; + Ok(NoteStatus::Committed { block_height }) + }, + 2 => { + let submited_at = source.read_u64()?; + let consumer_account_id = AccountId::read_from(source)?; + Ok(NoteStatus::Processing { consumer_account_id, submited_at }) + }, + 3 => { + let block_height = source.read_u64()?; + let consumer_account_id = Option::::read_from(source)?; + Ok(NoteStatus::Consumed { consumer_account_id, block_height }) + }, + _ => Err(DeserializationError::InvalidValue("NoteStatus".to_string())), + } } } impl Display for NoteStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - NoteStatus::Pending => write!(f, "Pending"), - NoteStatus::Committed => write!(f, "Committed"), - NoteStatus::Consumed => write!(f, "Consumed"), - NoteStatus::Processing => write!(f, "Processing"), + NoteStatus::Pending { .. } => write!(f, "{NOTE_STATUS_PENDING}"), + NoteStatus::Committed { .. } => write!(f, "{NOTE_STATUS_COMMITTED}"), + NoteStatus::Processing { .. } => write!(f, "{NOTE_STATUS_PROCESSING}"), + NoteStatus::Consumed { .. } => write!(f, "{NOTE_STATUS_CONSUMED}"), } } } From d34ae2e79b6ec76d1da69735ee1c94a503c6520c Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 5 Jun 2024 15:54:30 -0300 Subject: [PATCH 20/33] Refactor store/cli to use the new `NoteStatus` --- src/cli/notes.rs | 28 ++--------- src/store/data_store.rs | 4 +- src/store/note_record/input_note_record.rs | 6 +-- src/store/note_record/output_note_record.rs | 2 +- src/store/sqlite_store/mod.rs | 6 +-- src/store/sqlite_store/notes.rs | 51 +++++++++++---------- src/store/sqlite_store/sync.rs | 10 ++-- 7 files changed, 46 insertions(+), 61 deletions(-) diff --git a/src/cli/notes.rs b/src/cli/notes.rs index 85678a1c4..3c756195d 100644 --- a/src/cli/notes.rs +++ b/src/cli/notes.rs @@ -355,21 +355,6 @@ fn note_summary( .or(output_note_record.map(|record| record.id())) .expect("One of the two records should be Some"); - let commit_height = input_note_record - .map(|record| { - record - .inclusion_proof() - .map(|proof| proof.origin().block_num.to_string()) - .unwrap_or("-".to_string()) - }) - .or(output_note_record.map(|record| { - record - .inclusion_proof() - .map(|proof| proof.origin().block_num.to_string()) - .unwrap_or("-".to_string()) - })) - .expect("One of the two records should be Some"); - let assets_hash_str = input_note_record .map(|record| record.assets().commitment().to_string()) .or(output_note_record.map(|record| record.assets().commitment().to_string())) @@ -414,20 +399,15 @@ fn note_summary( .or(output_note_record.map(|record| record.status())) .expect("One of the two records should be Some"); - let note_consumer = input_note_record - .map(|record| record.consumer_account_id()) - .or(output_note_record.map(|record| record.consumer_account_id())) - .expect("One of the two records should be Some"); - let status = match status { - NoteStatus::Committed => { - status.to_string() + format!(" (height {})", commit_height).as_str() + NoteStatus::Committed { block_height } => { + status.to_string() + format!(" (height {})", block_height).as_str() }, - NoteStatus::Consumed => { + NoteStatus::Consumed { consumer_account_id, .. } => { status.to_string() + format!( " (by {})", - note_consumer.map(|id| id.to_string()).unwrap_or("?".to_string()) + consumer_account_id.map(|id| id.to_string()).unwrap_or("?".to_string()) ) .as_str() }, diff --git a/src/store/data_store.rs b/src/store/data_store.rs index 2baf1a3ec..ce6296bcc 100644 --- a/src/store/data_store.rs +++ b/src/store/data_store.rs @@ -54,13 +54,13 @@ impl DataStore for ClientDataStore { let note_record = input_note_records.get(note_id).expect("should have key"); match note_record.status() { - NoteStatus::Pending => { + NoteStatus::Pending { .. } => { return Err(DataStoreError::InternalError(format!( "The input note ID {} does not contain a note origin.", note_id.to_hex() ))); }, - NoteStatus::Consumed => { + NoteStatus::Consumed { .. } => { return Err(DataStoreError::NoteAlreadyConsumed(*note_id)); }, _ => {}, diff --git a/src/store/note_record/input_note_record.rs b/src/store/note_record/input_note_record.rs index dee5509ee..8e5aa4336 100644 --- a/src/store/note_record/input_note_record.rs +++ b/src/store/note_record/input_note_record.rs @@ -111,7 +111,7 @@ impl From<&NoteDetails> for InputNoteRecord { recipient: note_details.recipient().digest(), metadata: None, inclusion_proof: None, - status: NoteStatus::Pending, + status: NoteStatus::Pending { created_at: 0 }, details: NoteRecordDetails { nullifier: note_details.nullifier().to_string(), script_hash: note_details.script().hash(), @@ -165,7 +165,7 @@ impl From for InputNoteRecord { id: note.id(), recipient: note.recipient().digest(), assets: note.assets().clone(), - status: NoteStatus::Pending, + status: NoteStatus::Pending { created_at: 0 }, metadata: Some(*note.metadata()), inclusion_proof: None, details: NoteRecordDetails::new( @@ -185,7 +185,7 @@ impl From for InputNoteRecord { id: recorded_note.note().id(), recipient: recorded_note.note().recipient().digest(), assets: recorded_note.note().assets().clone(), - status: NoteStatus::Pending, + status: NoteStatus::Pending { created_at: 0 }, metadata: Some(*recorded_note.note().metadata()), details: NoteRecordDetails::new( recorded_note.note().nullifier().to_string(), diff --git a/src/store/note_record/output_note_record.rs b/src/store/note_record/output_note_record.rs index 5b562d0de..db838305f 100644 --- a/src/store/note_record/output_note_record.rs +++ b/src/store/note_record/output_note_record.rs @@ -101,7 +101,7 @@ impl From for OutputNoteRecord { id: note.id(), recipient: note.recipient().digest(), assets: note.assets().clone(), - status: NoteStatus::Pending, + status: NoteStatus::Pending { created_at: 0 }, metadata: *note.metadata(), inclusion_proof: None, details: Some(NoteRecordDetails::new( diff --git a/src/store/sqlite_store/mod.rs b/src/store/sqlite_store/mod.rs index a08fc24ec..67b8a8b5e 100644 --- a/src/store/sqlite_store/mod.rs +++ b/src/store/sqlite_store/mod.rs @@ -20,7 +20,7 @@ use crate::{ }, config::StoreConfig, errors::StoreError, - store::NoteStatus, + store::note_record::{NOTE_STATUS_COMMITTED, NOTE_STATUS_PROCESSING}, }; mod accounts; @@ -240,8 +240,8 @@ impl Store for SqliteStore { const QUERY: &str = "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status IN rarray(?)"; let unspent_filters = Rc::new(vec![ - Value::from(NoteStatus::Committed.to_string()), - Value::from(NoteStatus::Processing.to_string()), + Value::from(NOTE_STATUS_COMMITTED.to_string()), + Value::from(NOTE_STATUS_PROCESSING.to_string()), ]); self.db() .prepare(QUERY)? diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index d415e44f9..5aea370b3 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -9,12 +9,16 @@ use miden_objects::{ transaction::TransactionId, Digest, }; +use miden_tx::utils::DeserializationError; use rusqlite::{named_params, params, params_from_iter, types::Value, Transaction}; use super::SqliteStore; use crate::{ errors::StoreError, - store::{InputNoteRecord, NoteFilter, NoteRecordDetails, NoteStatus, OutputNoteRecord}, + store::{ + note_record::{NOTE_STATUS_COMMITTED, NOTE_STATUS_CONSUMED, NOTE_STATUS_PENDING}, + InputNoteRecord, NoteFilter, NoteRecordDetails, NoteStatus, OutputNoteRecord, + }, }; fn insert_note_query(table_name: NoteTable) -> String { @@ -120,19 +124,19 @@ impl<'a> NoteFilter<'a> { NoteFilter::Committed => { format!( "{base} WHERE status = '{}' AND consumer_transaction_id IS NULL", - NoteStatus::Committed + NOTE_STATUS_COMMITTED ) }, NoteFilter::Consumed => { - format!("{base} WHERE status = '{}'", NoteStatus::Consumed) + format!("{base} WHERE status = '{}'", NOTE_STATUS_CONSUMED) }, NoteFilter::Pending => { - format!("{base} WHERE status = '{}'", NoteStatus::Pending) + format!("{base} WHERE status = '{}'", NOTE_STATUS_PENDING) }, NoteFilter::Processing => { format!( "{base} WHERE status = '{}' AND consumer_transaction_id IS NOT NULL", - NoteStatus::Committed, + NOTE_STATUS_COMMITTED, ) }, NoteFilter::Unique(_) | NoteFilter::List(_) => { @@ -432,18 +436,27 @@ fn parse_input_note( let recipient = Digest::try_from(recipient)?; let id = NoteId::new(recipient, note_assets.commitment()); - let status: NoteStatus = serde_json::from_str(&format!("\"{status}\"")) - .map_err(StoreError::JsonDataDeserializationError)?; let consumer_account_id: Option = match consumer_account_id { Some(account_id) => Some(AccountId::try_from(account_id as u64)?), None => None, }; // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain - let status = if let (NoteStatus::Committed, Some(_)) = (status, consumer_account_id) { - NoteStatus::Processing - } else { - status + let status = match status.as_str() { + NOTE_STATUS_PENDING => NoteStatus::Pending { created_at: 0 }, + NOTE_STATUS_COMMITTED => { + if let Some(consumer_account_id) = consumer_account_id { + NoteStatus::Processing { consumer_account_id, submited_at: 0 } + } else { + NoteStatus::Committed { block_height: 0 } + } + }, + NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, block_height: 0 }, + _ => { + return Err(StoreError::DataDeserializationError(DeserializationError::InvalidValue( + format!("NoteStatus: {}", status), + ))) + }, }; Ok(InputNoteRecord::new( @@ -481,15 +494,11 @@ pub(crate) fn serialize_input_note( )?) .map_err(StoreError::InputSerializationError)?; - let status = serde_json::to_string(&NoteStatus::Committed) - .map_err(StoreError::InputSerializationError)? - .replace('\"', ""); + let status = NOTE_STATUS_COMMITTED.to_string(); (Some(inclusion_proof), status) }, None => { - let status = serde_json::to_string(&NoteStatus::Pending) - .map_err(StoreError::InputSerializationError)? - .replace('\"', ""); + let status = NOTE_STATUS_PENDING.to_string(); (None, status) }, @@ -639,16 +648,12 @@ pub(crate) fn serialize_output_note( )?) .map_err(StoreError::InputSerializationError)?; - let status = serde_json::to_string(&NoteStatus::Committed) - .map_err(StoreError::InputSerializationError)? - .replace('\"', ""); + let status = NOTE_STATUS_COMMITTED.to_string(); (Some(inclusion_proof), status) }, None => { - let status = serde_json::to_string(&NoteStatus::Pending) - .map_err(StoreError::InputSerializationError)? - .replace('\"', ""); + let status = NOTE_STATUS_PENDING.to_string(); (None, status) }, diff --git a/src/store/sqlite_store/sync.rs b/src/store/sqlite_store/sync.rs index 76814f236..7eb8851d1 100644 --- a/src/store/sqlite_store/sync.rs +++ b/src/store/sqlite_store/sync.rs @@ -6,8 +6,8 @@ use crate::{ client::sync::StateSyncUpdate, errors::StoreError, store::{ + note_record::{NOTE_STATUS_COMMITTED, NOTE_STATUS_CONSUMED}, sqlite_store::{accounts::update_account, notes::insert_input_note_tx}, - NoteStatus, }, }; @@ -100,14 +100,14 @@ impl SqliteStore { let nullifier = nullifier.to_hex(); tx.execute( SPENT_INPUT_NOTE_QUERY, - params![NoteStatus::Consumed.to_string(), nullifier], + params![NOTE_STATUS_CONSUMED.to_string(), nullifier], )?; const SPENT_OUTPUT_NOTE_QUERY: &str = "UPDATE output_notes SET status = ? WHERE json_extract(details, '$.nullifier') = ?"; tx.execute( SPENT_OUTPUT_NOTE_QUERY, - params![NoteStatus::Consumed.to_string(), nullifier], + params![NOTE_STATUS_CONSUMED.to_string(), nullifier], )?; } @@ -141,7 +141,7 @@ impl SqliteStore { named_params! { ":inclusion_proof": inclusion_proof, ":note_id": note_id.inner().to_hex(), - ":status": NoteStatus::Committed.to_string(), + ":status": NOTE_STATUS_COMMITTED.to_string(), }, )?; } @@ -165,7 +165,7 @@ impl SqliteStore { ":inclusion_proof": inclusion_proof, ":metadata": metadata, ":note_id": input_note.id().inner().to_hex(), - ":status": NoteStatus::Committed.to_string(), + ":status": NOTE_STATUS_COMMITTED.to_string(), }, )?; } From 6b4834d40a2eda2b2ecc3b639c69698c1a7da5c6 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 5 Jun 2024 16:14:30 -0300 Subject: [PATCH 21/33] Remove `consumer_account_id` from note record --- src/client/notes.rs | 5 ----- src/store/note_record/input_note_record.rs | 13 ------------ src/store/note_record/output_note_record.rs | 10 ---------- src/store/sqlite_store/notes.rs | 22 +++++++++++++++++---- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/client/notes.rs b/src/client/notes.rs index cc42702b6..cb93a3b2e 100644 --- a/src/client/notes.rs +++ b/src/client/notes.rs @@ -36,10 +36,6 @@ impl Client let mut relevant_notes = Vec::new(); for input_note in commited_notes { - if input_note.consumer_account_id().is_some() { - continue; - } - let mut account_relevance = note_screener.check_relevance(&input_note.clone().try_into()?)?; @@ -165,7 +161,6 @@ impl Client note.metadata().copied(), inclusion_proof, note.details().clone(), - None, ); self.store.insert_input_note(¬e).map_err(|err| err.into()) } diff --git a/src/store/note_record/input_note_record.rs b/src/store/note_record/input_note_record.rs index 8e5aa4336..1953bc1fe 100644 --- a/src/store/note_record/input_note_record.rs +++ b/src/store/note_record/input_note_record.rs @@ -1,5 +1,4 @@ use miden_objects::{ - accounts::AccountId, notes::{ Note, NoteAssets, NoteDetails, NoteId, NoteInclusionProof, NoteInputs, NoteMetadata, NoteRecipient, @@ -40,7 +39,6 @@ pub struct InputNoteRecord { metadata: Option, recipient: Digest, status: NoteStatus, - consumer_account_id: Option, } impl InputNoteRecord { @@ -52,7 +50,6 @@ impl InputNoteRecord { metadata: Option, inclusion_proof: Option, details: NoteRecordDetails, - consumer_account_id: Option, ) -> InputNoteRecord { InputNoteRecord { id, @@ -62,7 +59,6 @@ impl InputNoteRecord { metadata, inclusion_proof, details, - consumer_account_id, } } @@ -97,10 +93,6 @@ impl InputNoteRecord { pub fn details(&self) -> &NoteRecordDetails { &self.details } - - pub fn consumer_account_id(&self) -> Option { - self.consumer_account_id - } } impl From<&NoteDetails> for InputNoteRecord { @@ -119,7 +111,6 @@ impl From<&NoteDetails> for InputNoteRecord { inputs: note_details.inputs().to_vec(), serial_num: note_details.serial_num(), }, - consumer_account_id: None, } } } @@ -154,7 +145,6 @@ impl Deserializable for InputNoteRecord { metadata, inclusion_proof, details, - consumer_account_id: None, }) } } @@ -174,7 +164,6 @@ impl From for InputNoteRecord { note.inputs().to_vec(), note.serial_num(), ), - consumer_account_id: None, } } } @@ -194,7 +183,6 @@ impl From for InputNoteRecord { recorded_note.note().serial_num(), ), inclusion_proof: Some(recorded_note.proof().clone()), - consumer_account_id: None, } } } @@ -255,7 +243,6 @@ impl TryFrom for InputNoteRecord { metadata: Some(*output_note.metadata()), recipient: output_note.recipient(), status: output_note.status(), - consumer_account_id: output_note.consumer_account_id(), }), None => Err(ClientError::NoteError(miden_objects::NoteError::invalid_origin_index( "Output Note Record contains no details".to_string(), diff --git a/src/store/note_record/output_note_record.rs b/src/store/note_record/output_note_record.rs index db838305f..20fef703a 100644 --- a/src/store/note_record/output_note_record.rs +++ b/src/store/note_record/output_note_record.rs @@ -1,5 +1,4 @@ use miden_objects::{ - accounts::AccountId, notes::{Note, NoteAssets, NoteId, NoteInclusionProof, NoteMetadata}, Digest, }; @@ -32,7 +31,6 @@ pub struct OutputNoteRecord { metadata: NoteMetadata, recipient: Digest, status: NoteStatus, - consumer_account_id: Option, } impl OutputNoteRecord { @@ -44,7 +42,6 @@ impl OutputNoteRecord { metadata: NoteMetadata, inclusion_proof: Option, details: Option, - consumer_account_id: Option, ) -> OutputNoteRecord { OutputNoteRecord { id, @@ -54,7 +51,6 @@ impl OutputNoteRecord { metadata, inclusion_proof, details, - consumer_account_id, } } @@ -85,10 +81,6 @@ impl OutputNoteRecord { pub fn details(&self) -> Option<&NoteRecordDetails> { self.details.as_ref() } - - pub fn consumer_account_id(&self) -> Option { - self.consumer_account_id - } } // CONVERSIONS @@ -110,7 +102,6 @@ impl From for OutputNoteRecord { note.inputs().to_vec(), note.serial_num(), )), - consumer_account_id: None, } } } @@ -128,7 +119,6 @@ impl TryFrom for OutputNoteRecord { metadata: *metadata, recipient: input_note.recipient(), status: input_note.status(), - consumer_account_id: input_note.consumer_account_id(), }), None => Err(ClientError::NoteError(miden_objects::NoteError::invalid_origin_index( "Input Note Record contains no metadata".to_string(), diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 5aea370b3..fcac5227f 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -467,7 +467,6 @@ fn parse_input_note( note_metadata, inclusion_proof, note_details, - consumer_account_id, )) } @@ -606,14 +605,30 @@ fn parse_output_note( let recipient = Digest::try_from(recipient)?; let id = NoteId::new(recipient, note_assets.commitment()); - let status: NoteStatus = serde_json::from_str(&format!("\"{status}\"")) - .map_err(StoreError::JsonDataDeserializationError)?; let consumer_account_id: Option = match consumer_account_id { Some(account_id) => Some(AccountId::try_from(account_id as u64)?), None => None, }; + // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain + let status = match status.as_str() { + NOTE_STATUS_PENDING => NoteStatus::Pending { created_at: 0 }, + NOTE_STATUS_COMMITTED => { + if let Some(consumer_account_id) = consumer_account_id { + NoteStatus::Processing { consumer_account_id, submited_at: 0 } + } else { + NoteStatus::Committed { block_height: 0 } + } + }, + NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, block_height: 0 }, + _ => { + return Err(StoreError::DataDeserializationError(DeserializationError::InvalidValue( + format!("NoteStatus: {}", status), + ))) + }, + }; + Ok(OutputNoteRecord::new( id, recipient, @@ -622,7 +637,6 @@ fn parse_output_note( note_metadata, inclusion_proof, note_details, - consumer_account_id, )) } From 20bea45f843fd439b1407e288c334b84cfdb4e1b Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 5 Jun 2024 17:43:40 -0300 Subject: [PATCH 22/33] Add additional information to string --- Cargo.toml | 1 + src/cli/notes.rs | 20 +++----------------- src/store/note_record/mod.rs | 26 ++++++++++++++++++++++---- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b8678e99..c7256ed61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ test_utils = ["miden-objects/testing"] [dependencies] async-trait = { version = "0.1" } +chrono = { version = "0.4" } clap = { version = "4.3", features = ["derive"] } comfy-table = "7.1.0" figment = { version = "0.10", features = ["toml", "env"] } diff --git a/src/cli/notes.rs b/src/cli/notes.rs index 3c756195d..83ee162d1 100644 --- a/src/cli/notes.rs +++ b/src/cli/notes.rs @@ -7,7 +7,7 @@ use miden_client::{ NoteConsumability, }, errors::{ClientError, IdPrefixFetchError}, - store::{InputNoteRecord, NoteFilter as ClientNoteFilter, NoteStatus, OutputNoteRecord, Store}, + store::{InputNoteRecord, NoteFilter as ClientNoteFilter, OutputNoteRecord, Store}, }; use miden_objects::{ accounts::AccountId, @@ -397,22 +397,8 @@ fn note_summary( let status = input_note_record .map(|record| record.status()) .or(output_note_record.map(|record| record.status())) - .expect("One of the two records should be Some"); - - let status = match status { - NoteStatus::Committed { block_height } => { - status.to_string() + format!(" (height {})", block_height).as_str() - }, - NoteStatus::Consumed { consumer_account_id, .. } => { - status.to_string() - + format!( - " (by {})", - consumer_account_id.map(|id| id.to_string()).unwrap_or("?".to_string()) - ) - .as_str() - }, - _ => status.to_string(), - }; + .expect("One of the two records should be Some") + .to_string(); let note_metadata = input_note_record .map(|record| record.metadata()) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index ba2d45e25..4b502f12d 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -1,5 +1,7 @@ use std::fmt::Display; +extern crate chrono; +use chrono::DateTime; use miden_objects::{ accounts::AccountId, assembly::{Assembler, ProgramAst}, @@ -125,10 +127,26 @@ impl Deserializable for NoteStatus { impl Display for NoteStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - NoteStatus::Pending { .. } => write!(f, "{NOTE_STATUS_PENDING}"), - NoteStatus::Committed { .. } => write!(f, "{NOTE_STATUS_COMMITTED}"), - NoteStatus::Processing { .. } => write!(f, "{NOTE_STATUS_PROCESSING}"), - NoteStatus::Consumed { .. } => write!(f, "{NOTE_STATUS_CONSUMED}"), + NoteStatus::Pending { created_at } => write!( + f, + "{NOTE_STATUS_PENDING} (created at {})", + DateTime::from_timestamp(*created_at as i64, 0).expect("timestamp should be valid") + ), + NoteStatus::Committed { block_height } => { + write!(f, "{NOTE_STATUS_COMMITTED} (block height {block_height})") + }, + NoteStatus::Processing { consumer_account_id, submited_at } => write!( + f, + "{NOTE_STATUS_PROCESSING} (submited at {} by account {})", + DateTime::from_timestamp(*submited_at as i64, 0) + .expect("timestamp should be valid"), + consumer_account_id.to_hex() + ), + NoteStatus::Consumed { consumer_account_id, block_height } => write!( + f, + "{NOTE_STATUS_CONSUMED} (consumed at block height {block_height} by account {} )", + consumer_account_id.map(|id| id.to_hex()).unwrap_or("?".to_string()) + ), } } } From e627c4876131a0abc9f2be58cacbcedb8ea6f0f6 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 5 Jun 2024 17:44:57 -0300 Subject: [PATCH 23/33] Add new fields to database --- src/store/sqlite_store/notes.rs | 78 ++++++++++++++++++++++++++------ src/store/sqlite_store/store.sql | 6 +++ src/store/sqlite_store/sync.rs | 8 ++-- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index fcac5227f..71410af6c 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -1,6 +1,7 @@ use alloc::rc::Rc; use std::fmt; +use chrono::Utc; use clap::error::Result; use miden_objects::{ accounts::AccountId, @@ -24,8 +25,8 @@ use crate::{ fn insert_note_query(table_name: NoteTable) -> String { format!("\ INSERT INTO {table_name} - (note_id, assets, recipient, status, metadata, details, inclusion_proof, consumer_transaction_id) - VALUES (:note_id, :assets, :recipient, :status, json(:metadata), json(:details), json(:inclusion_proof), :consumer_transaction_id)", + (note_id, assets, recipient, status, metadata, details, inclusion_proof, consumer_transaction_id, created_at) + VALUES (:note_id, :assets, :recipient, :status, json(:metadata), json(:details), json(:inclusion_proof), :consumer_transaction_id, unixepoch(current_timestamp)|);", table_name = table_name) } @@ -64,6 +65,9 @@ type SerializedInputNoteParts = ( Option, Vec, Option, + u64, + Option, + Option, ); type SerializedOutputNoteParts = ( Vec, @@ -74,6 +78,9 @@ type SerializedOutputNoteParts = ( Option, Option>, Option, + u64, + Option, + Option, ); // NOTE TABLE @@ -109,7 +116,10 @@ impl<'a> NoteFilter<'a> { note.metadata, note.inclusion_proof, script.serialized_note_script, - tx.account_id + tx.account_id, + note.created_at, + note.submited_at, + note.nullifier_height from {notes_table} AS note LEFT OUTER JOIN notes_scripts AS script ON note.details IS NOT NULL AND @@ -336,24 +346,26 @@ pub fn update_note_consumer_tx_id( note_id: NoteId, consumer_tx_id: TransactionId, ) -> Result<(), StoreError> { - const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; + const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET consumer_transaction_id = :consumer_transaction_id, submited_at = :submited_at WHERE note_id = :note_id;"; tx.execute( UPDATE_INPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), + ":submited_at": Utc::now().timestamp(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; - const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET consumer_transaction_id = :consumer_transaction_id WHERE note_id = :note_id;"; + const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET consumer_transaction_id = :consumer_transaction_id, submited_at = :submited_at WHERE note_id = :note_id;"; tx.execute( UPDATE_OUTPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), + ":submited_at": Utc::now().timestamp(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; @@ -373,6 +385,9 @@ fn parse_input_note_columns( let inclusion_proof: Option = row.get(5)?; let serialized_note_script: Vec = row.get(6)?; let consumer_account_id: Option = row.get(7)?; + let created_at: u64 = row.get(8)?; + let submited_at: Option = row.get(9)?; + let nullifier_height: Option = row.get(10)?; Ok(( assets, @@ -383,6 +398,9 @@ fn parse_input_note_columns( inclusion_proof, serialized_note_script, consumer_account_id, + created_at, + submited_at, + nullifier_height, )) } @@ -399,6 +417,9 @@ fn parse_input_note( note_inclusion_proof, serialized_note_script, consumer_account_id, + created_at, + submited_at, + nullifier_height, ) = serialized_input_note_parts; // Merge the info that comes from the input notes table and the notes script table @@ -443,15 +464,26 @@ fn parse_input_note( // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain let status = match status.as_str() { - NOTE_STATUS_PENDING => NoteStatus::Pending { created_at: 0 }, + NOTE_STATUS_PENDING => NoteStatus::Pending { created_at }, NOTE_STATUS_COMMITTED => { if let Some(consumer_account_id) = consumer_account_id { - NoteStatus::Processing { consumer_account_id, submited_at: 0 } + NoteStatus::Processing { + consumer_account_id, + submited_at: submited_at.unwrap_or(0), + } } else { - NoteStatus::Committed { block_height: 0 } + NoteStatus::Committed { + block_height: inclusion_proof + .clone() + .map(|proof| proof.origin().block_num as u64) + .unwrap_or(0), + } } }, - NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, block_height: 0 }, + NOTE_STATUS_CONSUMED => NoteStatus::Consumed { + consumer_account_id, + block_height: nullifier_height.unwrap_or(0), + }, _ => { return Err(StoreError::DataDeserializationError(DeserializationError::InvalidValue( format!("NoteStatus: {}", status), @@ -540,6 +572,9 @@ fn parse_output_note_columns( let inclusion_proof: Option = row.get(5)?; let serialized_note_script: Option> = row.get(6)?; let consumer_account_id: Option = row.get(7)?; + let created_at: u64 = row.get(8)?; + let submited_at: Option = row.get(9)?; + let nullifier_height: Option = row.get(10)?; Ok(( assets, @@ -550,6 +585,9 @@ fn parse_output_note_columns( inclusion_proof, serialized_note_script, consumer_account_id, + created_at, + submited_at, + nullifier_height, )) } @@ -566,6 +604,9 @@ fn parse_output_note( note_inclusion_proof, serialized_note_script, consumer_account_id, + created_at, + submited_at, + nullifier_height, ) = serialized_output_note_parts; let note_details: Option = if let Some(details_as_json_str) = note_details { @@ -613,15 +654,26 @@ fn parse_output_note( // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain let status = match status.as_str() { - NOTE_STATUS_PENDING => NoteStatus::Pending { created_at: 0 }, + NOTE_STATUS_PENDING => NoteStatus::Pending { created_at }, NOTE_STATUS_COMMITTED => { if let Some(consumer_account_id) = consumer_account_id { - NoteStatus::Processing { consumer_account_id, submited_at: 0 } + NoteStatus::Processing { + consumer_account_id, + submited_at: submited_at.unwrap_or(0), + } } else { - NoteStatus::Committed { block_height: 0 } + NoteStatus::Committed { + block_height: inclusion_proof + .clone() + .map(|proof| proof.origin().block_num as u64) + .unwrap_or(0), + } } }, - NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, block_height: 0 }, + NOTE_STATUS_CONSUMED => NoteStatus::Consumed { + consumer_account_id, + block_height: nullifier_height.unwrap_or(0), + }, _ => { return Err(StoreError::DataDeserializationError(DeserializationError::InvalidValue( format!("NoteStatus: {}", status), diff --git a/src/store/sqlite_store/store.sql b/src/store/sqlite_store/store.sql index b2fb3fd48..86909ea79 100644 --- a/src/store/sqlite_store/store.sql +++ b/src/store/sqlite_store/store.sql @@ -94,6 +94,9 @@ CREATE TABLE input_notes ( -- inputs -- the serialized NoteInputs, including inputs hash and list of inputs -- serial_num -- the note serial number consumer_transaction_id BLOB NULL, -- the transaction ID of the transaction that consumed the note + created_at UNSIGNED BIG INT NOT NULL, -- timestamp of the note creation/import + submited_at UNSIGNED BIG INT NULL, -- timestamp of the note submission to node + nullifier_height UNSIGNED BIG INT NULL, -- block height when the nullifier arrived FOREIGN KEY (consumer_transaction_id) REFERENCES transactions(id) PRIMARY KEY (note_id) @@ -136,6 +139,9 @@ CREATE TABLE output_notes ( -- inputs -- the serialized NoteInputs, including inputs hash and list of inputs -- serial_num -- the note serial number consumer_transaction_id BLOB NULL, -- the transaction ID of the transaction that consumed the note + created_at UNSIGNED BIG INT NOT NULL, -- timestamp of the note creation/import + submited_at UNSIGNED BIG INT NULL, -- timestamp of the note submission to node + nullifier_height UNSIGNED BIG INT NULL, -- block height when the nullifier arrived FOREIGN KEY (consumer_transaction_id) REFERENCES transactions(id) PRIMARY KEY (note_id) diff --git a/src/store/sqlite_store/sync.rs b/src/store/sqlite_store/sync.rs index 7eb8851d1..388b2fbbf 100644 --- a/src/store/sqlite_store/sync.rs +++ b/src/store/sqlite_store/sync.rs @@ -96,18 +96,18 @@ impl SqliteStore { // Update spent notes for nullifier in nullifiers.iter() { const SPENT_INPUT_NOTE_QUERY: &str = - "UPDATE input_notes SET status = ? WHERE json_extract(details, '$.nullifier') = ?"; + "UPDATE input_notes SET status = ?, nullifier_height = ? WHERE json_extract(details, '$.nullifier') = ?"; let nullifier = nullifier.to_hex(); tx.execute( SPENT_INPUT_NOTE_QUERY, - params![NOTE_STATUS_CONSUMED.to_string(), nullifier], + params![NOTE_STATUS_CONSUMED.to_string(), block_header.block_num(), nullifier], )?; const SPENT_OUTPUT_NOTE_QUERY: &str = - "UPDATE output_notes SET status = ? WHERE json_extract(details, '$.nullifier') = ?"; + "UPDATE output_notes SET status = ?, nullifier_height = ? WHERE json_extract(details, '$.nullifier') = ?"; tx.execute( SPENT_OUTPUT_NOTE_QUERY, - params![NOTE_STATUS_CONSUMED.to_string(), nullifier], + params![NOTE_STATUS_CONSUMED.to_string(), block_header.block_num(), nullifier], )?; } From 92fabd1c91670fda2d550dffaa01a3c6268d9703 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Fri, 7 Jun 2024 14:53:08 -0300 Subject: [PATCH 24/33] Use local timezone for timestamp --- src/store/note_record/mod.rs | 13 +++++++++---- src/store/sqlite_store/notes.rs | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index 4b502f12d..a6c8b49cc 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -1,7 +1,7 @@ use std::fmt::Display; extern crate chrono; -use chrono::DateTime; +use chrono::{Local, TimeZone}; use miden_objects::{ accounts::AccountId, assembly::{Assembler, ProgramAst}, @@ -130,7 +130,10 @@ impl Display for NoteStatus { NoteStatus::Pending { created_at } => write!( f, "{NOTE_STATUS_PENDING} (created at {})", - DateTime::from_timestamp(*created_at as i64, 0).expect("timestamp should be valid") + Local + .timestamp_opt(*created_at as i64, 0) + .single() + .expect("timestamp should be valid") ), NoteStatus::Committed { block_height } => { write!(f, "{NOTE_STATUS_COMMITTED} (block height {block_height})") @@ -138,13 +141,15 @@ impl Display for NoteStatus { NoteStatus::Processing { consumer_account_id, submited_at } => write!( f, "{NOTE_STATUS_PROCESSING} (submited at {} by account {})", - DateTime::from_timestamp(*submited_at as i64, 0) + Local + .timestamp_opt(*submited_at as i64, 0) + .single() .expect("timestamp should be valid"), consumer_account_id.to_hex() ), NoteStatus::Consumed { consumer_account_id, block_height } => write!( f, - "{NOTE_STATUS_CONSUMED} (consumed at block height {block_height} by account {} )", + "{NOTE_STATUS_CONSUMED} (consumed at block height {block_height} by account {})", consumer_account_id.map(|id| id.to_hex()).unwrap_or("?".to_string()) ), } diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 71410af6c..f9bfc2509 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -26,7 +26,7 @@ fn insert_note_query(table_name: NoteTable) -> String { format!("\ INSERT INTO {table_name} (note_id, assets, recipient, status, metadata, details, inclusion_proof, consumer_transaction_id, created_at) - VALUES (:note_id, :assets, :recipient, :status, json(:metadata), json(:details), json(:inclusion_proof), :consumer_transaction_id, unixepoch(current_timestamp)|);", + VALUES (:note_id, :assets, :recipient, :status, json(:metadata), json(:details), json(:inclusion_proof), :consumer_transaction_id, unixepoch(current_timestamp));", table_name = table_name) } From 99332b8c50b97b7274869d8f708d871092e4a29c Mon Sep 17 00:00:00 2001 From: tomyrd Date: Fri, 7 Jun 2024 16:16:20 -0300 Subject: [PATCH 25/33] Fix status display messages --- src/store/note_record/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index a6c8b49cc..13baac8dc 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -136,7 +136,7 @@ impl Display for NoteStatus { .expect("timestamp should be valid") ), NoteStatus::Committed { block_height } => { - write!(f, "{NOTE_STATUS_COMMITTED} (block height {block_height})") + write!(f, "{NOTE_STATUS_COMMITTED} (at block height {block_height})") }, NoteStatus::Processing { consumer_account_id, submited_at } => write!( f, @@ -149,7 +149,7 @@ impl Display for NoteStatus { ), NoteStatus::Consumed { consumer_account_id, block_height } => write!( f, - "{NOTE_STATUS_CONSUMED} (consumed at block height {block_height} by account {})", + "{NOTE_STATUS_CONSUMED} (at block height {block_height} by account {})", consumer_account_id.map(|id| id.to_hex()).unwrap_or("?".to_string()) ), } From 8f304f8466190f040ddcedab713956cb26222da0 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 10 Jun 2024 11:47:02 -0300 Subject: [PATCH 26/33] Fix submitted typo --- src/store/note_record/mod.rs | 16 +++++++------- src/store/sqlite_store/notes.rs | 38 ++++++++++++++++---------------- src/store/sqlite_store/store.sql | 4 ++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index 13baac8dc..622cb460e 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -63,7 +63,7 @@ pub enum NoteStatus { /// Note has been consumed locally but not yet nullified on chain Processing { consumer_account_id: AccountId, - submited_at: u64, + submitted_at: u64, }, /// Note has been nullified on chain Consumed { @@ -83,9 +83,9 @@ impl Serializable for NoteStatus { target.write_u8(1); target.write_u64(*block_height); }, - NoteStatus::Processing { consumer_account_id, submited_at } => { + NoteStatus::Processing { consumer_account_id, submitted_at } => { target.write_u8(2); - target.write_u64(*submited_at); + target.write_u64(*submitted_at); consumer_account_id.write_into(target); }, NoteStatus::Consumed { consumer_account_id, block_height } => { @@ -110,9 +110,9 @@ impl Deserializable for NoteStatus { Ok(NoteStatus::Committed { block_height }) }, 2 => { - let submited_at = source.read_u64()?; + let submitted_at = source.read_u64()?; let consumer_account_id = AccountId::read_from(source)?; - Ok(NoteStatus::Processing { consumer_account_id, submited_at }) + Ok(NoteStatus::Processing { consumer_account_id, submitted_at }) }, 3 => { let block_height = source.read_u64()?; @@ -138,11 +138,11 @@ impl Display for NoteStatus { NoteStatus::Committed { block_height } => { write!(f, "{NOTE_STATUS_COMMITTED} (at block height {block_height})") }, - NoteStatus::Processing { consumer_account_id, submited_at } => write!( + NoteStatus::Processing { consumer_account_id, submitted_at } => write!( f, - "{NOTE_STATUS_PROCESSING} (submited at {} by account {})", + "{NOTE_STATUS_PROCESSING} (submitted at {} by account {})", Local - .timestamp_opt(*submited_at as i64, 0) + .timestamp_opt(*submitted_at as i64, 0) .single() .expect("timestamp should be valid"), consumer_account_id.to_hex() diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index f9bfc2509..0d191b98c 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -25,7 +25,7 @@ use crate::{ fn insert_note_query(table_name: NoteTable) -> String { format!("\ INSERT INTO {table_name} - (note_id, assets, recipient, status, metadata, details, inclusion_proof, consumer_transaction_id, created_at) + (note_id, assets, recipient, status, metadata, details, inclusion_proof, consumer_transaction_id, created_at) VALUES (:note_id, :assets, :recipient, :status, json(:metadata), json(:details), json(:inclusion_proof), :consumer_transaction_id, unixepoch(current_timestamp));", table_name = table_name) } @@ -108,9 +108,9 @@ impl<'a> NoteFilter<'a> { /// Returns a [String] containing the query for this Filter fn to_query(&self, notes_table: NoteTable) -> String { let base = format!( - "SELECT - note.assets, - note.details, + "SELECT + note.assets, + note.details, note.recipient, note.status, note.metadata, @@ -118,11 +118,11 @@ impl<'a> NoteFilter<'a> { script.serialized_note_script, tx.account_id, note.created_at, - note.submited_at, + note.submitted_at, note.nullifier_height - from {notes_table} AS note + from {notes_table} AS note LEFT OUTER JOIN notes_scripts AS script - ON note.details IS NOT NULL AND + ON note.details IS NOT NULL AND json_extract(note.details, '$.script_hash') = script.script_hash LEFT OUTER JOIN transactions AS tx ON note.consumer_transaction_id IS NOT NULL AND @@ -346,26 +346,26 @@ pub fn update_note_consumer_tx_id( note_id: NoteId, consumer_tx_id: TransactionId, ) -> Result<(), StoreError> { - const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET consumer_transaction_id = :consumer_transaction_id, submited_at = :submited_at WHERE note_id = :note_id;"; + const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET consumer_transaction_id = :consumer_transaction_id, submitted_at = :submitted_at WHERE note_id = :note_id;"; tx.execute( UPDATE_INPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), - ":submited_at": Utc::now().timestamp(), + ":submitted_at": Utc::now().timestamp(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; - const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET consumer_transaction_id = :consumer_transaction_id, submited_at = :submited_at WHERE note_id = :note_id;"; + const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET consumer_transaction_id = :consumer_transaction_id, submitted_at = :submitted_at WHERE note_id = :note_id;"; tx.execute( UPDATE_OUTPUT_NOTES_QUERY, named_params! { ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), - ":submited_at": Utc::now().timestamp(), + ":submitted_at": Utc::now().timestamp(), }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; @@ -386,7 +386,7 @@ fn parse_input_note_columns( let serialized_note_script: Vec = row.get(6)?; let consumer_account_id: Option = row.get(7)?; let created_at: u64 = row.get(8)?; - let submited_at: Option = row.get(9)?; + let submitted_at: Option = row.get(9)?; let nullifier_height: Option = row.get(10)?; Ok(( @@ -399,7 +399,7 @@ fn parse_input_note_columns( serialized_note_script, consumer_account_id, created_at, - submited_at, + submitted_at, nullifier_height, )) } @@ -418,7 +418,7 @@ fn parse_input_note( serialized_note_script, consumer_account_id, created_at, - submited_at, + submitted_at, nullifier_height, ) = serialized_input_note_parts; @@ -469,7 +469,7 @@ fn parse_input_note( if let Some(consumer_account_id) = consumer_account_id { NoteStatus::Processing { consumer_account_id, - submited_at: submited_at.unwrap_or(0), + submitted_at: submitted_at.unwrap_or(0), } } else { NoteStatus::Committed { @@ -573,7 +573,7 @@ fn parse_output_note_columns( let serialized_note_script: Option> = row.get(6)?; let consumer_account_id: Option = row.get(7)?; let created_at: u64 = row.get(8)?; - let submited_at: Option = row.get(9)?; + let submitted_at: Option = row.get(9)?; let nullifier_height: Option = row.get(10)?; Ok(( @@ -586,7 +586,7 @@ fn parse_output_note_columns( serialized_note_script, consumer_account_id, created_at, - submited_at, + submitted_at, nullifier_height, )) } @@ -605,7 +605,7 @@ fn parse_output_note( serialized_note_script, consumer_account_id, created_at, - submited_at, + submitted_at, nullifier_height, ) = serialized_output_note_parts; @@ -659,7 +659,7 @@ fn parse_output_note( if let Some(consumer_account_id) = consumer_account_id { NoteStatus::Processing { consumer_account_id, - submited_at: submited_at.unwrap_or(0), + submitted_at: submitted_at.unwrap_or(0), } } else { NoteStatus::Committed { diff --git a/src/store/sqlite_store/store.sql b/src/store/sqlite_store/store.sql index 86909ea79..def9832fa 100644 --- a/src/store/sqlite_store/store.sql +++ b/src/store/sqlite_store/store.sql @@ -95,7 +95,7 @@ CREATE TABLE input_notes ( -- serial_num -- the note serial number consumer_transaction_id BLOB NULL, -- the transaction ID of the transaction that consumed the note created_at UNSIGNED BIG INT NOT NULL, -- timestamp of the note creation/import - submited_at UNSIGNED BIG INT NULL, -- timestamp of the note submission to node + submitted_at UNSIGNED BIG INT NULL, -- timestamp of the note submission to node nullifier_height UNSIGNED BIG INT NULL, -- block height when the nullifier arrived FOREIGN KEY (consumer_transaction_id) REFERENCES transactions(id) PRIMARY KEY (note_id) @@ -140,7 +140,7 @@ CREATE TABLE output_notes ( -- serial_num -- the note serial number consumer_transaction_id BLOB NULL, -- the transaction ID of the transaction that consumed the note created_at UNSIGNED BIG INT NOT NULL, -- timestamp of the note creation/import - submited_at UNSIGNED BIG INT NULL, -- timestamp of the note submission to node + submitted_at UNSIGNED BIG INT NULL, -- timestamp of the note submission to node nullifier_height UNSIGNED BIG INT NULL, -- block height when the nullifier arrived FOREIGN KEY (consumer_transaction_id) REFERENCES transactions(id) PRIMARY KEY (note_id) From 5c0c6b0d18ac8407cf3e74af49d1e01db24429f9 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 10 Jun 2024 11:47:35 -0300 Subject: [PATCH 27/33] Fix tests --- tests/integration/main.rs | 14 +++++++++++--- tests/integration/onchain_tests.rs | 9 +++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/integration/main.rs b/tests/integration/main.rs index c17aed297..d15e4445a 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -135,7 +135,7 @@ async fn test_p2idr_transfer_consumed_by_target() { //Check that the note is not consumed by the target account assert!(matches!( client.get_input_note(note.id()).unwrap().status(), - NoteStatus::Committed + NoteStatus::Committed { .. } )); consume_notes(&mut client, from_account_id, &[note.clone()]).await; @@ -143,8 +143,16 @@ async fn test_p2idr_transfer_consumed_by_target() { // Check that the note is consumed by the target account let input_note = client.get_input_note(note.id()).unwrap(); - assert!(matches!(input_note.status(), NoteStatus::Consumed)); - assert_eq!(input_note.consumer_account_id().unwrap(), from_account_id); + assert!(matches!(input_note.status(), NoteStatus::Consumed { .. })); + if let NoteStatus::Consumed { + consumer_account_id: Some(consumer_account_id), + .. + } = input_note.status() + { + assert_eq!(consumer_account_id, from_account_id); + } else { + panic!("Note should be consumed"); + } // Do a transfer from first account to second account with Recall. In this situation we'll do // the happy path where the `to_account_id` consumes the note diff --git a/tests/integration/onchain_tests.rs b/tests/integration/onchain_tests.rs index 50d29b7fc..5d6c9b593 100644 --- a/tests/integration/onchain_tests.rs +++ b/tests/integration/onchain_tests.rs @@ -229,8 +229,13 @@ async fn test_onchain_accounts() { // Check that the client doesn't know who consumed the note let input_note = client_1.get_input_note(notes[0].id()).unwrap(); - assert!(matches!(input_note.status(), NoteStatus::Consumed)); - assert!(input_note.consumer_account_id().is_none()); + assert!(matches!( + input_note.status(), + NoteStatus::Consumed { + consumer_account_id: None, + block_height: _ + } + )); let new_from_account_balance = client_1 .get_account(from_account_id) From 8be45ea6b3c099b98928bdd2b56c04f23f929134 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 10 Jun 2024 15:24:54 -0300 Subject: [PATCH 28/33] Improve doc comments for `NoteStatus` --- src/store/note_record/mod.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index 622cb460e..bdf62c3c7 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -1,6 +1,5 @@ use std::fmt::Display; -extern crate chrono; use chrono::{Local, TimeZone}; use miden_objects::{ accounts::AccountId, @@ -57,17 +56,27 @@ pub const NOTE_STATUS_PROCESSING: &str = "Processing"; #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum NoteStatus { /// Note is pending to be commited on chain. - Pending { created_at: u64 }, - /// Note has been commited on chain - Committed { block_height: u64 }, - /// Note has been consumed locally but not yet nullified on chain + Pending { + /// Timestamp (in seconds) when the note (either new or imported) started being tracked by the client. + created_at: u64, + }, + /// Note has been commited on chain. + Committed { + /// Block height at which the note was commited. + block_height: u64, + }, + /// Note has been consumed locally but not yet nullified on chain. Processing { + /// ID of account that is consuming the note. consumer_account_id: AccountId, + /// Timestamp (in seconds) of the note's consumption. submitted_at: u64, }, - /// Note has been nullified on chain + /// Note has been nullified on chain. Consumed { + /// ID of account that consumed the note. If the consumer account is not known, this field will be `None`. consumer_account_id: Option, + /// Block height at which the note was consumed. block_height: u64, }, } From 3ae7af85af0dd76db5ce8c47d6d55b5af2c9c2e8 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 10 Jun 2024 15:34:06 -0300 Subject: [PATCH 29/33] Remove incorrect `unwrap_or` --- src/store/sqlite_store/notes.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index 0d191b98c..c78a88889 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -132,22 +132,16 @@ impl<'a> NoteFilter<'a> { match self { NoteFilter::All => base, NoteFilter::Committed => { - format!( - "{base} WHERE status = '{}' AND consumer_transaction_id IS NULL", - NOTE_STATUS_COMMITTED - ) + format!("{base} WHERE status = '{NOTE_STATUS_COMMITTED}' AND consumer_transaction_id IS NULL") }, NoteFilter::Consumed => { - format!("{base} WHERE status = '{}'", NOTE_STATUS_CONSUMED) + format!("{base} WHERE status = '{NOTE_STATUS_CONSUMED}'") }, NoteFilter::Pending => { - format!("{base} WHERE status = '{}'", NOTE_STATUS_PENDING) + format!("{base} WHERE status = '{NOTE_STATUS_PENDING}'") }, NoteFilter::Processing => { - format!( - "{base} WHERE status = '{}' AND consumer_transaction_id IS NOT NULL", - NOTE_STATUS_COMMITTED, - ) + format!("{base} WHERE status = '{NOTE_STATUS_COMMITTED}' AND consumer_transaction_id IS NOT NULL") }, NoteFilter::Unique(_) | NoteFilter::List(_) => { format!("{base} WHERE note.note_id IN rarray(?)") @@ -469,20 +463,21 @@ fn parse_input_note( if let Some(consumer_account_id) = consumer_account_id { NoteStatus::Processing { consumer_account_id, - submitted_at: submitted_at.unwrap_or(0), + submitted_at: submitted_at + .expect("Processing note should have submition timestamp"), } } else { NoteStatus::Committed { block_height: inclusion_proof .clone() .map(|proof| proof.origin().block_num as u64) - .unwrap_or(0), + .expect("Committed note should have inclusion proof"), } } }, NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, - block_height: nullifier_height.unwrap_or(0), + block_height: nullifier_height.expect("Consumed note should have nullifier height"), }, _ => { return Err(StoreError::DataDeserializationError(DeserializationError::InvalidValue( @@ -659,20 +654,21 @@ fn parse_output_note( if let Some(consumer_account_id) = consumer_account_id { NoteStatus::Processing { consumer_account_id, - submitted_at: submitted_at.unwrap_or(0), + submitted_at: submitted_at + .expect("Processing note should have submition timestamp"), } } else { NoteStatus::Committed { block_height: inclusion_proof .clone() .map(|proof| proof.origin().block_num as u64) - .unwrap_or(0), + .expect("Committed note should have inclusion proof"), } } }, NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, - block_height: nullifier_height.unwrap_or(0), + block_height: nullifier_height.expect("Consumed note should have nullifier height"), }, _ => { return Err(StoreError::DataDeserializationError(DeserializationError::InvalidValue( From eff88622d599a7e68f72a6a712d4cf9d47b91c75 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Mon, 10 Jun 2024 16:53:57 -0300 Subject: [PATCH 30/33] Add `Pending` status to `SqliteStore` --- src/store/sqlite_store/notes.rs | 65 +++++++++++++++----------------- src/store/sqlite_store/store.sql | 8 ++-- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index c78a88889..d95bef30d 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -17,7 +17,10 @@ use super::SqliteStore; use crate::{ errors::StoreError, store::{ - note_record::{NOTE_STATUS_COMMITTED, NOTE_STATUS_CONSUMED, NOTE_STATUS_PENDING}, + note_record::{ + NOTE_STATUS_COMMITTED, NOTE_STATUS_CONSUMED, NOTE_STATUS_PENDING, + NOTE_STATUS_PROCESSING, + }, InputNoteRecord, NoteFilter, NoteRecordDetails, NoteStatus, OutputNoteRecord, }, }; @@ -132,7 +135,7 @@ impl<'a> NoteFilter<'a> { match self { NoteFilter::All => base, NoteFilter::Committed => { - format!("{base} WHERE status = '{NOTE_STATUS_COMMITTED}' AND consumer_transaction_id IS NULL") + format!("{base} WHERE status = '{NOTE_STATUS_COMMITTED}'") }, NoteFilter::Consumed => { format!("{base} WHERE status = '{NOTE_STATUS_CONSUMED}'") @@ -141,7 +144,7 @@ impl<'a> NoteFilter<'a> { format!("{base} WHERE status = '{NOTE_STATUS_PENDING}'") }, NoteFilter::Processing => { - format!("{base} WHERE status = '{NOTE_STATUS_COMMITTED}' AND consumer_transaction_id IS NOT NULL") + format!("{base} WHERE status = '{NOTE_STATUS_PROCESSING}'") }, NoteFilter::Unique(_) | NoteFilter::List(_) => { format!("{base} WHERE note.note_id IN rarray(?)") @@ -340,7 +343,7 @@ pub fn update_note_consumer_tx_id( note_id: NoteId, consumer_tx_id: TransactionId, ) -> Result<(), StoreError> { - const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET consumer_transaction_id = :consumer_transaction_id, submitted_at = :submitted_at WHERE note_id = :note_id;"; + const UPDATE_INPUT_NOTES_QUERY: &str = "UPDATE input_notes SET status = :status, consumer_transaction_id = :consumer_transaction_id, submitted_at = :submitted_at WHERE note_id = :note_id;"; tx.execute( UPDATE_INPUT_NOTES_QUERY, @@ -348,11 +351,12 @@ pub fn update_note_consumer_tx_id( ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), ":submitted_at": Utc::now().timestamp(), + ":status": NOTE_STATUS_PROCESSING, }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; - const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET consumer_transaction_id = :consumer_transaction_id, submitted_at = :submitted_at WHERE note_id = :note_id;"; + const UPDATE_OUTPUT_NOTES_QUERY: &str = "UPDATE output_notes SET status = :status, consumer_transaction_id = :consumer_transaction_id, submitted_at = :submitted_at WHERE note_id = :note_id;"; tx.execute( UPDATE_OUTPUT_NOTES_QUERY, @@ -360,6 +364,7 @@ pub fn update_note_consumer_tx_id( ":note_id": note_id.inner().to_string(), ":consumer_transaction_id": consumer_tx_id.to_string(), ":submitted_at": Utc::now().timestamp(), + ":status": NOTE_STATUS_PROCESSING, }, ) .map_err(|err| StoreError::QueryError(err.to_string()))?; @@ -459,21 +464,16 @@ fn parse_input_note( // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain let status = match status.as_str() { NOTE_STATUS_PENDING => NoteStatus::Pending { created_at }, - NOTE_STATUS_COMMITTED => { - if let Some(consumer_account_id) = consumer_account_id { - NoteStatus::Processing { - consumer_account_id, - submitted_at: submitted_at - .expect("Processing note should have submition timestamp"), - } - } else { - NoteStatus::Committed { - block_height: inclusion_proof - .clone() - .map(|proof| proof.origin().block_num as u64) - .expect("Committed note should have inclusion proof"), - } - } + NOTE_STATUS_COMMITTED => NoteStatus::Committed { + block_height: inclusion_proof + .clone() + .map(|proof| proof.origin().block_num as u64) + .expect("Committed note should have inclusion proof"), + }, + NOTE_STATUS_PROCESSING => NoteStatus::Processing { + consumer_account_id: consumer_account_id + .expect("Processing note should have consumer account id"), + submitted_at: submitted_at.expect("Processing note should have submition timestamp"), }, NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, @@ -650,21 +650,16 @@ fn parse_output_note( // If the note is committed and has a consumer account id, then it was consumed locally but the client is not synced with the chain let status = match status.as_str() { NOTE_STATUS_PENDING => NoteStatus::Pending { created_at }, - NOTE_STATUS_COMMITTED => { - if let Some(consumer_account_id) = consumer_account_id { - NoteStatus::Processing { - consumer_account_id, - submitted_at: submitted_at - .expect("Processing note should have submition timestamp"), - } - } else { - NoteStatus::Committed { - block_height: inclusion_proof - .clone() - .map(|proof| proof.origin().block_num as u64) - .expect("Committed note should have inclusion proof"), - } - } + NOTE_STATUS_COMMITTED => NoteStatus::Committed { + block_height: inclusion_proof + .clone() + .map(|proof| proof.origin().block_num as u64) + .expect("Committed note should have inclusion proof"), + }, + NOTE_STATUS_PROCESSING => NoteStatus::Processing { + consumer_account_id: consumer_account_id + .expect("Processing note should have consumer account id"), + submitted_at: submitted_at.expect("Processing note should have submition timestamp"), }, NOTE_STATUS_CONSUMED => NoteStatus::Consumed { consumer_account_id, diff --git a/src/store/sqlite_store/store.sql b/src/store/sqlite_store/store.sql index def9832fa..1e9d6f66c 100644 --- a/src/store/sqlite_store/store.sql +++ b/src/store/sqlite_store/store.sql @@ -73,8 +73,8 @@ CREATE TABLE input_notes ( note_id BLOB NOT NULL, -- the note id recipient BLOB NOT NULL, -- the note recipient assets BLOB NOT NULL, -- the serialized NoteAssets, including vault hash and list of assets - status TEXT CHECK( status IN ( -- the status of the note - either pending, committed or consumed - 'Pending', 'Committed', 'Consumed' + status TEXT CHECK( status IN ( -- the status of the note - either pending, committed, processing or consumed + 'Pending', 'Committed', 'Processing', 'Consumed' )), inclusion_proof JSON NULL, -- JSON consisting of the following fields: @@ -118,8 +118,8 @@ CREATE TABLE output_notes ( note_id BLOB NOT NULL, -- the note id recipient BLOB NOT NULL, -- the note recipient assets BLOB NOT NULL, -- the serialized NoteAssets, including vault hash and list of assets - status TEXT CHECK( status IN ( -- the status of the note - either pending, committed or consumed - 'Pending', 'Committed', 'Consumed' + status TEXT CHECK( status IN ( -- the status of the note - either pending, committed, processing or consumed + 'Pending', 'Committed', 'Processing', 'Consumed' )), inclusion_proof JSON NULL, -- JSON consisting of the following fields: From 098502c5f5ed5e9c5ff93dca5410c0181a849f22 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Fri, 14 Jun 2024 18:31:44 -0300 Subject: [PATCH 31/33] Fix merge --- src/client/notes.rs | 4 ++-- src/store/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/notes.rs b/src/client/notes.rs index bc6808118..15b6b3bfe 100644 --- a/src/client/notes.rs +++ b/src/client/notes.rs @@ -60,13 +60,13 @@ impl Client } /// Returns the consumability of the provided note. + #[maybe_async] pub fn get_note_consumability( &self, note: InputNoteRecord, ) -> Result, ClientError> { let note_screener = NoteScreener::new(self.store.clone()); - note_screener - .check_relevance(¬e.clone().try_into()?) + maybe_await!(note_screener.check_relevance(¬e.clone().try_into()?)) .map_err(|err| err.into()) } diff --git a/src/store/mod.rs b/src/store/mod.rs index c99752c7f..07bfd66e3 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -85,7 +85,7 @@ pub trait Store { fn get_unspent_input_note_nullifiers(&self) -> Result, StoreError> { let nullifiers = maybe_await!(self.get_input_notes(NoteFilter::Committed))? .iter() - .chain(self.get_input_notes(NoteFilter::Processing)?.iter()) + .chain(maybe_await!(self.get_input_notes(NoteFilter::Processing))?.iter()) .map(|input_note| Ok(Nullifier::from(Digest::try_from(input_note.nullifier())?))) .collect::, _>>(); From f07079e20092b568caad5b1d0ea70e417c484ffe Mon Sep 17 00:00:00 2001 From: tomyrd Date: Tue, 18 Jun 2024 12:13:38 -0300 Subject: [PATCH 32/33] Fix sqlite_store errors --- src/store/sqlite_store/mod.rs | 24 +++--------------------- src/store/sqlite_store/notes.rs | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/store/sqlite_store/mod.rs b/src/store/sqlite_store/mod.rs index 784b383b2..2e60e3869 100644 --- a/src/store/sqlite_store/mod.rs +++ b/src/store/sqlite_store/mod.rs @@ -1,6 +1,5 @@ use alloc::collections::BTreeMap; use core::cell::{RefCell, RefMut}; -use std::rc::Rc; use miden_objects::{ accounts::{Account, AccountId, AccountStub, AuthSecretKey}, @@ -8,7 +7,7 @@ use miden_objects::{ notes::{NoteTag, Nullifier}, BlockHeader, Digest, Word, }; -use rusqlite::{types::Value, vtab::array, Connection}; +use rusqlite::{vtab::array, Connection}; use winter_maybe_async::maybe_async; use self::config::SqliteStoreConfig; @@ -21,7 +20,6 @@ use crate::{ transactions::{TransactionRecord, TransactionResult}, }, errors::StoreError, - store::note_record::{NOTE_STATUS_COMMITTED, NOTE_STATUS_PROCESSING}, }; mod accounts; @@ -263,25 +261,9 @@ impl Store for SqliteStore { self.get_account_auth_by_pub_key(pub_key) } + #[maybe_async] fn get_unspent_input_note_nullifiers(&self) -> Result, StoreError> { - const QUERY: &str = - "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status IN rarray(?)"; - let unspent_filters = Rc::new(vec![ - Value::from(NOTE_STATUS_COMMITTED.to_string()), - Value::from(NOTE_STATUS_PROCESSING.to_string()), - ]); - self.db() - .prepare(QUERY)? - .query_map([unspent_filters], |row| row.get(0)) - .expect("no binding parameters used in query") - .map(|result| { - result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then( - |v: String| { - Digest::try_from(v).map(Nullifier::from).map_err(StoreError::HexParseError) - }, - ) - }) - .collect::, _>>() + self.get_unspent_input_note_nullifiers() } } diff --git a/src/store/sqlite_store/notes.rs b/src/store/sqlite_store/notes.rs index ebe41af44..77c389f08 100644 --- a/src/store/sqlite_store/notes.rs +++ b/src/store/sqlite_store/notes.rs @@ -21,7 +21,7 @@ use crate::{ NOTE_STATUS_COMMITTED, NOTE_STATUS_CONSUMED, NOTE_STATUS_PENDING, NOTE_STATUS_PROCESSING, }, - InputNoteRecord, NoteFilter, NoteRecordDetails, NoteStatus, OutputNoteRecord, + InputNoteRecord, NoteFilter, NoteRecordDetails, NoteStatus, Nullifier, OutputNoteRecord, }, }; @@ -254,6 +254,27 @@ impl SqliteStore { Ok(tx.commit()?) } + + pub(crate) fn get_unspent_input_note_nullifiers(&self) -> Result, StoreError> { + const QUERY: &str = + "SELECT json_extract(details, '$.nullifier') FROM input_notes WHERE status IN rarray(?)"; + let unspent_filters = Rc::new(vec![ + Value::from(NOTE_STATUS_COMMITTED.to_string()), + Value::from(NOTE_STATUS_PROCESSING.to_string()), + ]); + self.db() + .prepare(QUERY)? + .query_map([unspent_filters], |row| row.get(0)) + .expect("no binding parameters used in query") + .map(|result| { + result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then( + |v: String| { + Digest::try_from(v).map(Nullifier::from).map_err(StoreError::HexParseError) + }, + ) + }) + .collect::, _>>() + } } // HELPERS From a80bcc5995913a9fbadef06755befcaacb5094e0 Mon Sep 17 00:00:00 2001 From: tomyrd Date: Wed, 19 Jun 2024 11:44:41 -0300 Subject: [PATCH 33/33] Address suggestions --- src/store/note_record/mod.rs | 4 ++-- src/store/sqlite_store/store.sql | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/store/note_record/mod.rs b/src/store/note_record/mod.rs index c5680b373..ceab6f004 100644 --- a/src/store/note_record/mod.rs +++ b/src/store/note_record/mod.rs @@ -57,7 +57,7 @@ pub const NOTE_STATUS_PROCESSING: &str = "Processing"; pub enum NoteStatus { /// Note is pending to be commited on chain. Pending { - /// Timestamp (in seconds) when the note (either new or imported) started being tracked by the client. + /// UNIX epoch-based timestamp (in seconds) when the note (either new or imported) started being tracked by the client. created_at: u64, }, /// Note has been commited on chain. @@ -69,7 +69,7 @@ pub enum NoteStatus { Processing { /// ID of account that is consuming the note. consumer_account_id: AccountId, - /// Timestamp (in seconds) of the note's consumption. + /// UNIX epoch-based timestamp (in seconds) of the note's consumption. submitted_at: u64, }, /// Note has been nullified on chain. diff --git a/src/store/sqlite_store/store.sql b/src/store/sqlite_store/store.sql index 1e9d6f66c..d62e35d8e 100644 --- a/src/store/sqlite_store/store.sql +++ b/src/store/sqlite_store/store.sql @@ -111,6 +111,8 @@ CREATE TABLE input_notes ( )) CONSTRAINT check_valid_metadata_json CHECK (metadata IS NULL OR (json_extract(metadata, '$.sender') IS NOT NULL AND json_extract(metadata, '$.tag') IS NOT NULL)) CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status != 'Pending') + CONSTRAINT check_valid_submitted_at CHECK (submitted_at IS NOT NULL OR status != 'Processing') + CONSTRAINT check_valid_nullifier_height CHECK (nullifier_height IS NOT NULL OR status != 'Consumed') ); -- Create output notes table @@ -163,6 +165,8 @@ CREATE TABLE output_notes ( json_extract(details, '$.serial_num') IS NOT NULL )) CONSTRAINT check_valid_consumer_transaction_id CHECK (consumer_transaction_id IS NULL OR status != 'Pending') + CONSTRAINT check_valid_submitted_at CHECK (submitted_at IS NOT NULL OR status != 'Processing') + CONSTRAINT check_valid_nullifier_height CHECK (nullifier_height IS NOT NULL OR status != 'Consumed') ); -- Create note's scripts table, used for both input and output notes