Skip to content

feat: add SyncTransaction endpoint#1224

Merged
SantiagoPittella merged 41 commits intonextfrom
santiagopittella-sync-transaction-endpoint
Sep 30, 2025
Merged

feat: add SyncTransaction endpoint#1224
SantiagoPittella merged 41 commits intonextfrom
santiagopittella-sync-transaction-endpoint

Conversation

@SantiagoPittella
Copy link
Collaborator

@SantiagoPittella SantiagoPittella commented Sep 10, 2025

closes #1207

@SantiagoPittella SantiagoPittella force-pushed the santiagopittella-sync-transaction-endpoint branch from f8b6e1e to 2f04a15 Compare September 10, 2025 20:44
@SantiagoPittella SantiagoPittella force-pushed the santiagopittella-sync-transaction-endpoint branch from 2f04a15 to cea5e7f Compare September 10, 2025 20:45
@SantiagoPittella SantiagoPittella requested review from Mirko-von-Leipzig, bobbinth and igamigo and removed request for bobbinth September 10, 2025 20:45
Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Thank you! Looks good. I left some comments inline. The main things are:

  • We need to store more data in the transactions table to avoid parsing raw blocks.
  • We need to figure out how to limit response size (or at least send an error if the response is too big).

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Thank you! Looks good. Not a full review but left some comments inline. The main thing is to fix the methodology for response size estimation. Once you come up with methodology, could you describe it before implementing?

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you. I left some more comments inline.

cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you. I left some more comments inline.

@bobbinth
Copy link
Contributor

One more general comment: there is a much more efficient way to serialize note inclusion paths which would guarantee that we fit into 5MB limit even if a block contained $2^{16}$ output notes. It is quite a bit more involved though - instead of sending a Merkle path with every node, we'd need to send a single partial SMT for an entire block. So, it is not something we should do now, but let's create an issue for this.

@SantiagoPittella
Copy link
Collaborator Author

@SantiagoPittella - what's outstanding in this PR? I think maybe just #1224 (comment) is left?

I just addressed the offset comment, replacing it with a cursor-based approach. Every other comment was previously addressed 👌🏼 .
cc @Mirko-von-Leipzig

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you. I left a couple more comments inline. The main one is that I'm not sure the logic for filtering out the last block is correct.

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you. I left a couple more comments inline.

Also, not for this PR, but it would be great to test this functionality somehow - but not really sure what's the best way to do it. Let's create an issue for this.

//
// Note: 500 bytes per output note is an over-estimate but ensures we don't
// exceed memory limits when these transactions are later converted to proto records.
let header_base_size = 4 + 32 + 16 + 64; // block_num + tx_id + account_id + commitments
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be const, right?

Comment on lines +520 to +524
let note_records = self
.state
.get_notes_by_id(tx_header.output_notes.clone())
.await
.map_err(internal_error)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This might have been discussed before (maybe for a follow up) but going to the store for every single transaction seems potentially wasteful if in a single query you can fit note responses for more than one of them. I think joining the tables is also not very desirable here but that could be another alternative.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd add it to the list of follow-ups. We could make a single query that brings back note data for all transactions in the response - but that's a bit more involved and shouldn't make a huge difference for an in-process database (but would be very important if if the database was on a different machine).

Comment on lines 343 to 344
last_block_num = Some(tx.block_num);
last_transaction_id = Some(tx.transaction_id.clone());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like the clone() (and the assignment) happen per-row but are really only needed for the last row.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I split a bit this logic to avoid the extra clones.

all_transactions
.into_iter()
.take_while(|row| row.block_num != last_block_num)
.collect::<Vec<_>>(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I think this collect could be avoided as well, right?

Comment on lines 378 to 380
let last_included_block = last_block_num.map_or(*block_range.end(), |block_num| {
BlockNumber::from_raw_sql(block_num).expect("valid block number from database")
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is the case where the max payload size was not reached, would this always be equal to the end of the block range?

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, makes sense. The response, in practice, is the same but in the next iteration we avoid going to blocks that we already checked and we saw that didn't matched our criteria. I;/m moving to block_range.end().

Comment on lines 1 to 4
#![allow(
clippy::cast_possible_wrap,
reason = "We will not approach the item count where i64 and usize cause issues"
)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would this be better to put into the function itself?

Comment on lines 332 to 335
// If no more data, we're done
if chunk.is_empty() {
break;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I think I'd remove this if because it's functionally very similar to the if after the for loop and it doesn't seem to be too beneficial to return before it since chunk would contain no transactions to loop through

@igamigo
Copy link
Collaborator

igamigo commented Sep 30, 2025

Overall logic looking good. Left a few comments mainly related to avoiding unnecessary allocations and a few minor nits (feel free to disregard if you think they don't apply)

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! I think after @igamigo's comments are addressed, this is good to merge.

@SantiagoPittella SantiagoPittella merged commit 7f51765 into next Sep 30, 2025
6 checks passed
@SantiagoPittella SantiagoPittella deleted the santiagopittella-sync-transaction-endpoint branch September 30, 2025 20:13
Comment on lines +448 to +449
// List of transaction records.
repeated TransactionRecord transaction_records = 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd name this just transactions - let's add it to the follow-up.

Comment on lines +457 to +458
// A transaction header.
transaction.TransactionHeader transaction_header = 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe I'd change this to just header. Let's add this to the follow-ups as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SyncTransactions endpoint

4 participants