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: Utilize expected notes from ExecutedTransaction #329

Merged
merged 22 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 20 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
3 changes: 2 additions & 1 deletion Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ args = ["-rf", "miden-node"]
description = "Clone or update miden-node repository and clean up files"
script_runner = "bash"
script = [
'if [ -d miden-node ]; then cd miden-node && git checkout next; else git clone https://github.com/0xPolygonMiden/miden-node.git && cd miden-node && git checkout next; fi',
'if [ -d miden-node ]; then cd miden-node ; else git clone https://github.com/0xPolygonMiden/miden-node.git && cd miden-node; fi',
'git checkout next && git pull origin next && cargo update',
'rm -rf miden-store.sqlite3 miden-store.sqlite3-wal miden-store.sqlite3-shm',
'cd bin/node',
'cargo run --features $NODE_FEATURES_TESTING -- make-genesis --force'
Expand Down
2 changes: 1 addition & 1 deletion src/cli/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ mod tests {

let transaction_request = client.build_transaction_request(transaction_template).unwrap();
let transaction = client.new_transaction(transaction_request).unwrap();
let created_note = transaction.created_notes()[0].clone();
let created_note = transaction.created_notes().get_note(0).clone();
client.submit_transaction(transaction).await.unwrap();

// Ensure client has no input notes and one output note
Expand Down
2 changes: 1 addition & 1 deletion src/cli/notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ mod tests {

let transaction_request = client.build_transaction_request(transaction_template).unwrap();
let transaction = client.new_transaction(transaction_request).unwrap();
let created_note = transaction.created_notes()[0].clone();
let created_note = transaction.created_notes().get_note(0).clone();
client.submit_transaction(transaction).await.unwrap();

// Ensure client has no input notes and one output note
Expand Down
10 changes: 5 additions & 5 deletions src/client/note_screener.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use alloc::collections::BTreeSet;
use alloc::{collections::BTreeSet, rc::Rc};
use core::fmt;

use miden_objects::{accounts::AccountId, assets::Asset, notes::Note, Word};
Expand Down Expand Up @@ -26,12 +26,12 @@ impl fmt::Display for NoteRelevance {
}
}

pub struct NoteScreener<'a, S: Store> {
store: &'a S,
pub struct NoteScreener<S: Store> {
store: Rc<S>,
}

impl<'a, S: Store> NoteScreener<'a, S> {
pub fn new(store: &'a S) -> Self {
impl<S: Store> NoteScreener<S> {
pub fn new(store: Rc<S>) -> Self {
Self { store }
}

Expand Down
2 changes: 1 addition & 1 deletion src/client/notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
) -> Result<Vec<ConsumableNote>, ClientError> {
let commited_notes = self.store.get_input_notes(NoteFilter::Committed)?;

let note_screener = NoteScreener::new(self.store.as_ref());
let note_screener = NoteScreener::new(self.store.clone());

let mut relevant_notes = Vec::new();
for input_note in commited_notes {
Expand Down
2 changes: 1 addition & 1 deletion src/client/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
// We'll only do the check for either incoming public notes or pending input notes as
// output notes are not really candidates to be consumed here.

let note_screener = NoteScreener::new(self.store.as_ref());
let note_screener = NoteScreener::new(self.store.clone());

// Find all relevant Input Notes using the note checker
for input_note in committed_notes.updated_input_notes() {
Expand Down
122 changes: 63 additions & 59 deletions src/client/transactions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use miden_objects::{
crypto::rand::RpoRandomCoin,
notes::{Note, NoteId, NoteType},
transaction::{
ExecutedTransaction, InputNotes, OutputNotes, ProvenTransaction, TransactionArgs,
TransactionId, TransactionScript,
ExecutedTransaction, InputNotes, OutputNote, OutputNotes, ProvenTransaction,
TransactionArgs, TransactionId, TransactionScript,
},
Digest, Felt, Word,
};
Expand All @@ -20,78 +20,75 @@ use rand::Rng;
use tracing::info;

use self::transaction_request::{PaymentTransactionData, TransactionRequest, TransactionTemplate};
use super::{note_screener::NoteRelevance, rpc::NodeRpcClient, Client, FeltRng};
use super::{rpc::NodeRpcClient, Client, FeltRng};
use crate::{
client::NoteScreener,
errors::ClientError,
store::{Store, TransactionFilter},
store::{InputNoteRecord, Store, TransactionFilter},
};

pub mod transaction_request;

// TRANSACTION RESULT
// --------------------------------------------------------------------------------------------

/// Represents the result of executing a transaction by the client
/// Represents the result of executing a transaction by the client.
///
/// It contains an [ExecutedTransaction], a list of [Note] that describe the details of the notes
/// created by the transaction execution, and a list of `usize` `relevant_notes` that contain the
/// indices of `output_notes` that are relevant to the client
/// It contains an [ExecutedTransaction], and a list of `relevant_notes` that contains the
/// `output_notes` that the client has to store as input notes, based on the NoteScreener
/// output from filtering the transaction's output notes.
pub struct TransactionResult {
executed_transaction: ExecutedTransaction,
output_notes: Vec<Note>,
relevant_notes: Option<BTreeMap<usize, Vec<(AccountId, NoteRelevance)>>>,
transaction: ExecutedTransaction,
relevant_notes: Vec<InputNoteRecord>,
}

impl TransactionResult {
pub fn new(executed_transaction: ExecutedTransaction, created_notes: Vec<Note>) -> Self {
Self {
executed_transaction,
output_notes: created_notes,
relevant_notes: None,
/// Screens the output notes to store and track the relevant ones, and instantiates a [TransactionResult]
pub fn new<S: Store>(
transaction: ExecutedTransaction,
note_screener: NoteScreener<S>,
) -> Result<Self, ClientError> {
let mut relevant_notes = vec![];

for note in notes_from_output(transaction.output_notes()) {
let account_relevance = note_screener.check_relevance(note)?;

if !account_relevance.is_empty() {
relevant_notes.push(note.clone().into());
}
}
}

pub fn executed_transaction(&self) -> &ExecutedTransaction {
&self.executed_transaction
let tx_result = Self { transaction, relevant_notes };

Ok(tx_result)
}
Comment on lines +47 to 64
Copy link
Contributor

Choose a reason for hiding this comment

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

should we also provide a way to create a TransactionResult without filtering by note relevance?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Might be a good idea, but since we provide and use this TransactionResult type, I think we should add it if we end up needing it.


pub fn created_notes(&self) -> &Vec<Note> {
&self.output_notes
pub fn executed_transaction(&self) -> &ExecutedTransaction {
&self.transaction
}

pub fn relevant_notes(&self) -> Vec<&Note> {
if let Some(relevant_notes) = &self.relevant_notes {
relevant_notes
.keys()
.map(|note_index| &self.output_notes[*note_index])
.collect()
} else {
self.created_notes().iter().collect()
}
pub fn created_notes(&self) -> &OutputNotes {
self.transaction.output_notes()
}

pub fn set_relevant_notes(
&mut self,
relevant_notes: BTreeMap<usize, Vec<(AccountId, NoteRelevance)>>,
) {
self.relevant_notes = Some(relevant_notes);
pub fn relevant_notes(&self) -> &[InputNoteRecord] {
&self.relevant_notes
}

pub fn block_num(&self) -> u32 {
self.executed_transaction.block_header().block_num()
self.transaction.block_header().block_num()
}

pub fn transaction_arguments(&self) -> &TransactionArgs {
self.executed_transaction.tx_args()
self.transaction.tx_args()
}

pub fn account_delta(&self) -> &AccountDelta {
self.executed_transaction.account_delta()
self.transaction.account_delta()
}

pub fn consumed_notes(&self) -> &InputNotes {
self.executed_transaction.tx_inputs().input_notes()
self.transaction.tx_inputs().input_notes()
}
}

Expand Down Expand Up @@ -231,7 +228,6 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
let block_num = self.store.get_sync_height()?;

let note_ids = transaction_request.get_input_note_ids();

let output_notes = transaction_request.expected_output_notes().to_vec();

// Execute the transaction and get the witness
Expand All @@ -242,20 +238,28 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
transaction_request.into(),
)?;

// Check that the expected output notes is a subset of the transaction's output notes
let tx_note_ids: BTreeSet<NoteId> =
executed_transaction.output_notes().iter().map(|n| n.id()).collect();
// Check that the expected output notes matches the transaction outcome.
// We comprae authentication hashes where possible since that involves note IDs + metadata
// (as opposed to just note ID which remains the same regardless of metadata)
let tx_note_auth_hashes: BTreeSet<Digest> =
notes_from_output(executed_transaction.output_notes())
.map(Note::authentication_hash)
.collect();

let missing_note_ids: Vec<NoteId> = output_notes
.iter()
.filter_map(|n| (!tx_note_ids.contains(&n.id())).then_some(n.id()))
.filter_map(|n| {
(!tx_note_auth_hashes.contains(&n.authentication_hash())).then_some(n.id())
mFragaBA marked this conversation as resolved.
Show resolved Hide resolved
})
.collect();

if !missing_note_ids.is_empty() {
return Err(ClientError::MissingOutputNotes(missing_note_ids));
}

Ok(TransactionResult::new(executed_transaction, output_notes))
let screener = NoteScreener::new(self.store.clone());

TransactionResult::new(executed_transaction, screener)
}

/// Proves the specified transaction witness, submits it to the node, and stores the transaction in
Expand All @@ -273,19 +277,6 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client

self.submit_proven_transaction_request(proven_transaction.clone()).await?;

let note_screener = NoteScreener::new(self.store.as_ref());
let mut relevant_notes = BTreeMap::new();

for (idx, note) in tx_result.created_notes().iter().enumerate() {
let account_relevance = note_screener.check_relevance(note)?;
if !account_relevance.is_empty() {
relevant_notes.insert(idx, account_relevance);
}
}

let mut tx_result = tx_result;
tx_result.set_relevant_notes(relevant_notes);

// Transaction was proven and submitted to the node correctly, persist note details and update account
self.store.apply_transaction(tx_result)?;

Expand Down Expand Up @@ -405,7 +396,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
note_type,
random_coin,
)?;
println!("note tag {}", created_note.metadata().tag());

let recipient = created_note
.recipient()
.digest()
Expand Down Expand Up @@ -442,3 +433,16 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
pub(crate) fn prepare_word(word: &Word) -> String {
word.iter().map(|x| x.as_int().to_string()).collect::<Vec<_>>().join(".")
}

/// Extracts notes from [OutputNotes]
/// Used for:
/// - checking the relevance of notes to save them as input notes
/// - validate hashes versus expected output notes after a transaction is executed
pub(crate) fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
output_notes.iter().map(|n| match n {
OutputNote::Full(n) => n,
// The following todo!() applies until we have a way to support flows where we have
// partial details of the note
OutputNote::Header(_) => todo!("For now, all details should be held in OutputNote::Fulls"),
Copy link
Contributor

Choose a reason for hiding this comment

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

In which case do we get an OutputNote::Header?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The OutputNote::Header will contain only partial details of a note. When the transaction has to be proven, it is shrinked. At the point where we are using it though, all details will be held in OutputNote::Full after having been picked up from the advice map (after the transcation execution)

Copy link
Contributor

Choose a reason for hiding this comment

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

don't think it's blocking yet, but for swap flows I think we create an OutputNote::Header when we consume the swap note

})
}
6 changes: 3 additions & 3 deletions src/client/transactions/transaction_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ impl PaymentTransactionData {
// --------------------------------------------------------------------------------------------

pub mod known_script_roots {
pub const P2ID: &str = "0xcdfd70344b952980272119bc02b837d14c07bbfc54f86a254422f39391b77b35";
pub const P2IDR: &str = "0x41e5727b99a12b36066c09854d39d64dd09d9265c442a9be3626897572bf1745";
pub const SWAP: &str = "0x5852920f88985b651cf7ef5e48623f898b6c292f4a2c25dd788ff8b46dd90417";
pub const P2ID: &str = "0x0007b2229f7c8e3205a485a9879f1906798a2e27abd1706eaf58536e7cc3868b";
pub const P2IDR: &str = "0x418ae31e80b53ddc99179d3cacbc4140c7b36ab04ddb26908b3a6ed2e40061d5";
pub const SWAP: &str = "0x755c5dd2fd8d2aa8c2896e0e366a45bdc59c6d5ab543909db32ccfaf949c5826";
Copy link
Collaborator

Choose a reason for hiding this comment

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

For another PR: Should we find a way to change/calculate these automatically? I guess thanks to the tests we would never forget to update them but it would be a good addition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, we might have to. Originally this was a 'temporary' solution until the MAST refactors on the compiler. Additionally I thought that these scripts would not change too much, and even though the tests catch these errors there are a couple of things we can do to avoid having to recalculate them:

  • Provide a const way in miden-base to get these values (since the scripts live there).
  • Alternatively, we could provide a runtime method in miden-base to calculate these easily, because right now the only thing we can do is call create_p2id_note() and get the roots each time which is a bit hacky.

}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl fmt::Display for ClientError {
ClientError::MissingOutputNotes(note_ids) => {
write!(
f,
"transaction error: The transaction did not produce expected Note IDs: {}",
"transaction error: The transaction did not produce the expected notes corresponding to Note IDs: {}",
note_ids.iter().map(|&id| id.to_hex()).collect::<Vec<_>>().join(", ")
)
},
Expand Down
4 changes: 4 additions & 0 deletions src/store/note_record/output_note_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ impl OutputNoteRecord {
}
}

// CONVERSIONS
// ================================================================================================

// TODO: Improve conversions by implementing into_parts()
Copy link
Contributor

Choose a reason for hiding this comment

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

should we create an issue for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, we probably should. Although I think it would belong in miden-base so that we can do into_parts() with domain types.

impl From<Note> for OutputNoteRecord {
fn from(note: Note) -> Self {
OutputNoteRecord {
Expand Down
28 changes: 12 additions & 16 deletions src/store/sqlite_store/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ use super::{
SqliteStore,
};
use crate::{
client::transactions::{TransactionRecord, TransactionResult, TransactionStatus},
client::transactions::{
notes_from_output, TransactionRecord, TransactionResult, TransactionStatus,
},
errors::StoreError,
store::{InputNoteRecord, OutputNoteRecord, TransactionFilter},
store::{OutputNoteRecord, TransactionFilter},
};

pub(crate) const INSERT_TRANSACTION_QUERY: &str =
Expand Down Expand Up @@ -87,16 +89,13 @@ impl SqliteStore {

account.apply_delta(account_delta).map_err(StoreError::AccountError)?;

let created_input_notes = tx_result
.relevant_notes()
.into_iter()
.map(|note| InputNoteRecord::from(note.clone()))
.collect::<Vec<_>>();
// Save only input notes that we care for (based on the note screener assessment)
let created_input_notes = tx_result.relevant_notes().to_vec();

let created_output_notes = tx_result
.created_notes()
.iter()
.map(|note| OutputNoteRecord::from(note.clone()))
// Save all output notes
let created_output_notes = notes_from_output(tx_result.created_notes())
.cloned()
.map(OutputNoteRecord::from)
.collect::<Vec<_>>();

let consumed_note_ids =
Expand All @@ -112,11 +111,8 @@ impl SqliteStore {
update_account(&tx, &account)?;

// Updates for notes

// TODO: see if we should filter the input notes we store to keep notes we can consume with
// existing accounts
for note in &created_input_notes {
insert_input_note_tx(&tx, note)?;
for note in created_input_notes {
insert_input_note_tx(&tx, &note)?;
}

for note in &created_output_notes {
Expand Down
3 changes: 3 additions & 0 deletions tests/integration/asm/custom_p2id.masm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ proc.add_note_assets_to_account
end

begin
# drop the note script root
dropw

# => [NOTE_ARG]
push.{expected_note_arg} assert_eqw
# drop the note script root
Expand Down
3 changes: 1 addition & 2 deletions tests/integration/custom_transactions_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ async fn test_transaction_request() {

// Execute mint transaction in order to create custom note
let note = mint_custom_note(&mut client, fungible_faucet.id(), regular_account.id()).await;
println!("sda");
client.sync_state().await.unwrap();

// Prepare transaction
Expand Down Expand Up @@ -199,7 +198,7 @@ fn create_custom_note(
) -> Note {
let expected_note_arg = [Felt::new(9), Felt::new(12), Felt::new(18), Felt::new(3)]
.iter()
.map(|x| x.to_string())
.map(|x| x.as_int().to_string())
.collect::<Vec<_>>()
.join(".");

Expand Down
Loading