Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: verify imported note existance in the chain #297

Merged
merged 20 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

* Added an option to verify note existence in the chain before importing.
* Add new store note filter to fetch multiple notes by their id in a single query.
* [BREAKING] `Client::new()` now does not need a `data_store_store` parameter, and `SqliteStore`'s implements interior mutability.
* [BREAKING] The store's `get_input_note` was replaced by `get_input_notes` and a `NoteFilter::Unique` was added.
Expand Down
2 changes: 2 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ You can call:
miden-client input-notes show 0x70b7ec
```

The `import` subcommand verifies that the note that is about to be imported exists on chain. The user can add an optional flag `--no-verify` that skips this verification.

### `sync`

Sync the client with the latest state of the Miden network.
Expand Down
31 changes: 20 additions & 11 deletions src/cli/input_notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ pub enum InputNotes {
/// Path to the file that contains the input note data
#[clap()]
filename: PathBuf,

mFragaBA marked this conversation as resolved.
Show resolved Hide resolved
/// Skip verification of note's existence in the chain
#[clap(short, long, default_value = "false")]
no_verify: bool,
},

/// List consumable input notes
Expand All @@ -90,7 +94,7 @@ pub enum InputNotes {
}

impl InputNotes {
pub fn execute<N: NodeRpcClient, R: FeltRng, S: Store>(
pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store>(
&self,
mut client: Client<N, R, S>,
) -> Result<(), String> {
Expand All @@ -112,8 +116,8 @@ impl InputNotes {
export_note(&client, id, filename.clone())?;
println!("Succesfully exported note {}", id);
},
InputNotes::Import { filename } => {
let note_id = import_note(&mut client, filename.clone())?;
InputNotes::Import { filename, no_verify } => {
let note_id = import_note(&mut client, filename.clone(), !(*no_verify)).await?;
println!("Succesfully imported note {}", note_id.inner());
},
InputNotes::ListConsumable { account_id } => {
Expand Down Expand Up @@ -162,9 +166,10 @@ pub fn export_note<N: NodeRpcClient, R: FeltRng, S: Store>(

// IMPORT INPUT NOTE
// ================================================================================================
pub fn import_note<N: NodeRpcClient, R: FeltRng, S: Store>(
pub async fn import_note<N: NodeRpcClient, R: FeltRng, S: Store>(
client: &mut Client<N, R, S>,
filename: PathBuf,
verify: bool,
) -> Result<NoteId, String> {
let mut contents = vec![];
let mut _file = File::open(filename)
Expand All @@ -177,7 +182,10 @@ pub fn import_note<N: NodeRpcClient, R: FeltRng, S: Store>(
InputNoteRecord::read_from_bytes(&contents).map_err(|err| err.to_string())?;

let note_id = input_note_record.id();
client.import_input_note(input_note_record)?;
client
.import_input_note(input_note_record, verify)
.await
.map_err(|err| err.to_string())?;

Ok(note_id)
}
Expand Down Expand Up @@ -388,8 +396,9 @@ mod tests {
let committed_note: InputNoteRecord = committed_notes.first().unwrap().clone().into();
let pending_note = InputNoteRecord::from(created_notes.first().unwrap().clone());

client.import_input_note(committed_note.clone()).unwrap();
client.import_input_note(pending_note.clone()).unwrap();
client.import_input_note(committed_note.clone(), false).await.unwrap();
assert!(client.import_input_note(pending_note.clone(), true).await.is_err());
client.import_input_note(pending_note.clone(), false).await.unwrap();
assert!(pending_note.inclusion_proof().is_none());
assert!(committed_note.inclusion_proof().is_some());

Expand Down Expand Up @@ -425,13 +434,13 @@ mod tests {
let mut client =
MockClient::new(MockRpcApi::new(&Endpoint::default().to_string()), rng, store, true);

import_note(&mut client, filename_path).unwrap();
import_note(&mut client, filename_path, false).await.unwrap();
let imported_note_record: InputNoteRecord =
client.get_input_note(committed_note.id()).unwrap();

assert_eq!(committed_note.id(), imported_note_record.id());

import_note(&mut client, filename_path_pending).unwrap();
import_note(&mut client, filename_path_pending, false).await.unwrap();
let imported_pending_note_record = client.get_input_note(pending_note.id()).unwrap();

assert_eq!(imported_pending_note_record.id(), pending_note.id());
Expand Down Expand Up @@ -470,8 +479,8 @@ mod tests {
let committed_note: InputNoteRecord = notes.first().unwrap().clone().into();
let pending_note = InputNoteRecord::from(created_notes.first().unwrap().clone());

client.import_input_note(committed_note.clone()).unwrap();
client.import_input_note(pending_note.clone()).unwrap();
client.import_input_note(committed_note.clone(), false).await.unwrap();
client.import_input_note(pending_note.clone(), false).await.unwrap();
assert!(pending_note.inclusion_proof().is_none());
assert!(committed_note.inclusion_proof().is_some());

Expand Down
2 changes: 1 addition & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ impl Cli {
Command::Account(account) => account.execute(client),
Command::Init => Ok(()),
Command::Info => info::print_client_info(&client),
Command::InputNotes(notes) => notes.execute(client),
Command::InputNotes(notes) => notes.execute(client).await,
Command::Sync => sync::sync_state(client).await,
Command::Tags(tags) => tags.execute(client).await,
Command::Transaction(transaction) => {
Expand Down
64 changes: 61 additions & 3 deletions src/client/notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use miden_objects::{
accounts::AccountId,
assembly::ProgramAst,
crypto::rand::FeltRng,
notes::{NoteId, NoteScript},
notes::{NoteId, NoteInclusionProof, NoteScript},
};
use miden_tx::ScriptTarget;

Expand Down Expand Up @@ -97,8 +97,66 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> {
// INPUT NOTE CREATION
// --------------------------------------------------------------------------------------------

/// Imports a new input note into the client's store.
pub fn import_input_note(&mut self, note: InputNoteRecord) -> Result<(), ClientError> {
/// Imports a new input note into the client's store. The `verify` parameter dictates whether or
/// not the method verifies the existence of the note in the chain.
///
/// If the imported note is verified to be on chain and it doesn't contain an inclusion proof
/// the method tries to build one if possible.
/// If the verification fails then a [ClientError::ExistenceVerificationError] is raised.
pub async fn import_input_note(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method doc comment should Also mention posible errors and explain the verify flag

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

&mut self,
mut note: InputNoteRecord,
verify: bool,
) -> Result<(), ClientError> {
if !verify {
return self.store.insert_input_note(&note).map_err(|err| err.into());
}

// Verify that note exists in chain
let mut chain_notes = self.rpc_api.get_notes_by_id(&[note.id()]).await?;

if chain_notes.is_empty() {
return Err(ClientError::ExistenceVerificationError(note.id()));
}

let note_details = chain_notes.pop().expect("chain_notes should have at least one element");

let inclusion_details = match note_details {
super::rpc::NoteDetails::OffChain(_, _, inclusion) => inclusion,
super::rpc::NoteDetails::Public(_, inclusion) => inclusion,
};

// Check to see if it's possible to create an inclusion proof if the note doesn't have one.
// Only do this if the note exists in the chain and the client is synced to a height equal or
// greater than the note's creation block.
if note.inclusion_proof().is_none()
&& self.get_sync_height()? >= inclusion_details.block_num
mFragaBA marked this conversation as resolved.
Show resolved Hide resolved
{
// Add the inclusion proof to the imported note
let block_header = self
.rpc_api
.get_block_header_by_number(Some(inclusion_details.block_num))
.await?;

let inclusion_proof = NoteInclusionProof::new(
inclusion_details.block_num,
block_header.sub_hash(),
block_header.note_root(),
inclusion_details.note_index.into(),
inclusion_details.merkle_path,
)?;

note = InputNoteRecord::new(
note.id(),
note.recipient(),
note.assets().clone(),
note.status(),
note.metadata().copied(),
Some(inclusion_proof),
note.details().clone(),
);
mFragaBA marked this conversation as resolved.
Show resolved Hide resolved
}

self.store.insert_input_note(&note).map_err(|err| err.into())
}

Expand Down
18 changes: 17 additions & 1 deletion src/client/sync.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use alloc::collections::BTreeSet;
use std::collections::HashMap;

use crypto::merkle::{InOrderIndex, MmrDelta, MmrPeaks, PartialMmr};
use miden_objects::{
Expand Down Expand Up @@ -160,7 +161,22 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> {

let stored_note_tags: Vec<NoteTag> = self.store.get_note_tags()?;

let note_tags = [account_note_tags, stored_note_tags].concat();
let uncommited_note_tags: Vec<NoteTag> = self
.store
.get_input_notes(NoteFilter::Pending)?
.iter()
.filter_map(|note| note.metadata().map(|metadata| metadata.tag()))
.collect();

//TODO: Use BTreeSet to remove duplicates more efficiently once `Ord` is implemented for `NoteTag`
let note_tags: Vec<NoteTag> = [account_note_tags, stored_note_tags, uncommited_note_tags]
.concat()
.into_iter()
.map(|tag| (tag.to_string(), tag))
.collect::<HashMap<String, NoteTag>>()
.values()
.cloned()
.collect();
Comment on lines +171 to +179
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that the related PR from base got merged, will we do this in a follow-up PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed offline, we'll do this on the branch that we'll use as a base for the release (that will point to base's next)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an issue for this change (#320).


// To receive information about added nullifiers, we reduce them to the higher 16 bits
// Note that besides filtering by nullifier prefixes, the node also filters by block number
Expand Down
4 changes: 4 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub enum ClientError {
StoreError(StoreError),
TransactionExecutorError(TransactionExecutorError),
TransactionProvingError(TransactionProverError),
ExistenceVerificationError(NoteId),
}

impl fmt::Display for ClientError {
Expand Down Expand Up @@ -65,6 +66,9 @@ impl fmt::Display for ClientError {
ClientError::TransactionProvingError(err) => {
write!(f, "transaction prover error: {err}")
},
ClientError::ExistenceVerificationError(note_id) => {
write!(f, "The note with ID {note_id} doesn't exist in the chain")
},
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,12 @@ pub async fn insert_mock_data(client: &mut MockClient) -> Vec<BlockHeader> {

// insert notes into database
for note in consumed_notes.clone() {
client.import_input_note(note.into()).unwrap();
client.import_input_note(note.into(), false).await.unwrap();
}

// insert notes into database
for note in created_notes.clone() {
client.import_input_note(note.into()).unwrap();
client.import_input_note(note.into(), false).await.unwrap();
}

// insert account
Expand Down
7 changes: 5 additions & 2 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async fn test_input_notes_round_trip() {

// insert notes into database
for note in consumed_notes.iter().cloned() {
client.import_input_note(note.into()).unwrap();
client.import_input_note(note.into(), false).await.unwrap();
}

// retrieve notes from database
Expand All @@ -59,7 +59,10 @@ async fn test_get_input_note() {
let (_consumed_notes, created_notes) = mock_notes(&assembler);

// insert Note into database
client.import_input_note(created_notes.first().unwrap().clone().into()).unwrap();
client
.import_input_note(created_notes.first().unwrap().clone().into(), false)
.await
.unwrap();

// retrieve note from database
let retrieved_note =
Expand Down
57 changes: 57 additions & 0 deletions tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,63 @@ async fn test_get_output_notes() {
assert!(client.get_input_note(output_note_id).is_err());
}

#[tokio::test]
async fn test_import_pending_notes() {
Comment on lines +400 to +401
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also try consuming the created notes to make sure the inclusion proofs paths are usable

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added a line that consumes the note. The test doesn't panic so the inclusion proof is usable.

let mut client_1 = create_test_client();
let (first_basic_account, _second_basic_account, faucet_account) =
setup(&mut client_1, AccountStorageMode::Local).await;
let mut client_2 = create_test_client();
wait_for_node(&mut client_2).await;
client_1.sync_state().await.unwrap();
client_2.sync_state().await.unwrap();

let tx_template = TransactionTemplate::MintFungibleAsset(
FungibleAsset::new(faucet_account.id(), MINT_AMOUNT).unwrap().into(),
first_basic_account.id(),
NoteType::OffChain,
);

let tx_request = client_1.build_transaction_request(tx_template).unwrap();
let note = tx_request.expected_output_notes()[0].clone();

// If the verification is requested before execution then the import should fail
assert!(client_2.import_input_note(note.clone().into(), true).await.is_err());
execute_tx_and_sync(&mut client_1, tx_request).await;
client_2.sync_state().await.unwrap();

client_2.import_input_note(note.clone().into(), true).await.unwrap();
let input_note = client_2.get_input_note(note.id()).unwrap();

// If imported after execution and syncing then the inclusion proof should be Some
assert!(input_note.inclusion_proof().is_some());

let tx_template = TransactionTemplate::MintFungibleAsset(
FungibleAsset::new(faucet_account.id(), MINT_AMOUNT).unwrap().into(),
first_basic_account.id(),
NoteType::OffChain,
);

let tx_request = client_1.build_transaction_request(tx_template).unwrap();
let note = tx_request.expected_output_notes()[0].clone();

// Import an uncommited note without verification
client_2.import_input_note(note.clone().into(), false).await.unwrap();
let input_note = client_2.get_input_note(note.id()).unwrap();

// If imported before execution then the inclusion proof should be None
assert!(input_note.inclusion_proof().is_none());

execute_tx_and_sync(&mut client_1, tx_request).await;
client_2.sync_state().await.unwrap();

// After sync, the imported note should have inclusion proof even if it's not relevant for its accounts.
let input_note = client_2.get_input_note(note.id()).unwrap();
assert!(input_note.inclusion_proof().is_some());

// If inclusion proof is invalid this should panic
consume_notes(&mut client_1, first_basic_account.id(), &[input_note.try_into().unwrap()]).await;
}

#[tokio::test]
async fn test_get_account_update() {
// Create a client with both public and private accounts.
Expand Down
Loading