Skip to content

Commit 0504583

Browse files
authored
fix: TransactionHeader serialization (#1701)
* fix: TransactionHeader db serialization fix * docs: update changelog
1 parent d970008 commit 0504583

3 files changed

Lines changed: 100 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Validator now persists validated transactions ([#1614](https://github.com/0xMiden/miden-node/pull/1614)).
1010
- [BREAKING] Remove `SynState` and introduce `SyncChainMmr` ([#1591](https://github.com/0xMiden/miden-node/issues/1591)).
1111
- Introduce `SyncChainMmr` RPC endpoint to sync chain MMR deltas within specified block ranges ([#1591](https://github.com/0xMiden/miden-node/issues/1591)).
12+
- Fixed `TransactionHeader` serialization for row insertion on database & fixed transaction cursor on retrievals ([#1701](https://github.com/0xMiden/miden-node/issues/1701)).
1213

1314
### Changes
1415

crates/store/src/db/models/queries/transactions.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,25 @@ impl TransactionSummaryRowInsert {
125125
) -> Self {
126126
const HEADER_BASE_SIZE: usize = 4 + 32 + 16 + 64; // block_num + tx_id + account_id + commitments
127127

128-
// Serialize input notes using binary format (store nullifiers)
129-
let nullifiers_binary = transaction_header.input_notes().to_bytes();
128+
// Extract nullifiers from input notes and serialize them.
129+
// We only store the nullifiers (not the full `InputNoteCommitment`) since
130+
// that's all that's needed when reading back `TransactionRecords`.
131+
let nullifiers: Vec<Nullifier> = transaction_header
132+
.input_notes()
133+
.iter()
134+
.map(miden_protocol::transaction::InputNoteCommitment::nullifier)
135+
.collect();
136+
let nullifiers_binary = nullifiers.to_bytes();
130137

131-
// Serialize output notes using binary format (store note IDs)
132-
let output_notes_binary = transaction_header.output_notes().to_bytes();
138+
// Extract note IDs from output note headers and serialize them.
139+
// We only store the `NoteId`s (not the full `NoteHeader` with metadata) since
140+
// that's all that's needed when reading back `TransactionRecords`.
141+
let output_note_ids: Vec<NoteId> = transaction_header
142+
.output_notes()
143+
.iter()
144+
.map(miden_protocol::note::NoteHeader::id)
145+
.collect();
146+
let output_notes_binary = output_note_ids.to_bytes();
133147

134148
// Manually calculate the estimated size of the transaction header to avoid
135149
// the cost of serialization. The size estimation includes:
@@ -269,26 +283,20 @@ pub fn select_transactions_records(
269283

270284
// Add transactions from this chunk one by one until we hit the limit
271285
let mut added_from_chunk = 0;
272-
let mut last_added_tx: Option<TransactionRecordRaw> = None;
273286

274287
for tx in chunk {
275288
if total_size + tx.size_in_bytes <= max_payload_bytes {
276289
total_size += tx.size_in_bytes;
277-
last_added_tx = Some(tx);
290+
last_block_num = Some(tx.block_num);
291+
last_transaction_id = Some(tx.transaction_id.clone());
292+
all_transactions.push(tx);
278293
added_from_chunk += 1;
279294
} else {
280295
// Can't fit this transaction, stop here
281296
break;
282297
}
283298
}
284299

285-
// Update cursor position only for the last transaction that was actually added
286-
if let Some(tx) = last_added_tx {
287-
last_block_num = Some(tx.block_num);
288-
last_transaction_id = Some(tx.transaction_id.clone());
289-
all_transactions.push(tx);
290-
}
291-
292300
// Break if chunk incomplete (size limit hit or data exhausted)
293301
if added_from_chunk < NUM_TXS_PER_CHUNK {
294302
break;

crates/store/src/db/tests.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2398,3 +2398,81 @@ fn test_prune_history() {
23982398
"is_latest=true entry should be retained even if old"
23992399
);
24002400
}
2401+
2402+
#[test]
2403+
#[miden_node_test_macro::enable_logging]
2404+
fn db_roundtrip_transactions() {
2405+
let mut conn = create_db();
2406+
let block_num = BlockNumber::from(1);
2407+
create_block(&mut conn, block_num);
2408+
2409+
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
2410+
queries::upsert_accounts(&mut conn, &[mock_block_account_update(account_id, 0)], block_num)
2411+
.unwrap();
2412+
2413+
// Build two transaction headers with distinct data
2414+
let tx1 = mock_block_transaction(account_id, 1);
2415+
let tx2 = mock_block_transaction(account_id, 2);
2416+
let ordered = OrderedTransactionHeaders::new_unchecked(vec![tx1.clone(), tx2.clone()]);
2417+
2418+
// Insert
2419+
let count = queries::insert_transactions(&mut conn, block_num, &ordered).unwrap();
2420+
assert_eq!(count, 2, "Should insert 2 transactions");
2421+
2422+
// Retrieve
2423+
let (last_block, records) = queries::select_transactions_records(
2424+
&mut conn,
2425+
&[account_id],
2426+
BlockNumber::GENESIS..=block_num,
2427+
)
2428+
.unwrap();
2429+
assert_eq!(last_block, block_num, "Last block should match");
2430+
assert_eq!(records.len(), 2, "Should retrieve 2 transactions");
2431+
2432+
// Verify each transaction roundtrips correctly.
2433+
// Records are ordered by (block_num, transaction_id), so match by ID.
2434+
let originals = [&tx1, &tx2];
2435+
for record in &records {
2436+
let original = originals
2437+
.iter()
2438+
.find(|tx| tx.id() == record.transaction_id)
2439+
.expect("Retrieved transaction should match one of the originals");
2440+
assert_eq!(
2441+
record.transaction_id,
2442+
original.id(),
2443+
"TransactionId DB roundtrip must be symmetric"
2444+
);
2445+
assert_eq!(
2446+
record.account_id,
2447+
original.account_id(),
2448+
"AccountId DB roundtrip must be symmetric"
2449+
);
2450+
assert_eq!(record.block_num, block_num, "Block number must match");
2451+
assert_eq!(
2452+
record.initial_state_commitment,
2453+
original.initial_state_commitment(),
2454+
"Initial state commitment DB roundtrip must be symmetric"
2455+
);
2456+
assert_eq!(
2457+
record.final_state_commitment,
2458+
original.final_state_commitment(),
2459+
"Final state commitment DB roundtrip must be symmetric"
2460+
);
2461+
2462+
// Input notes are stored as nullifiers only
2463+
let expected_nullifiers: Vec<Nullifier> =
2464+
original.input_notes().iter().map(InputNoteCommitment::nullifier).collect();
2465+
assert_eq!(
2466+
record.nullifiers, expected_nullifiers,
2467+
"Nullifiers (from input notes) DB roundtrip must be symmetric"
2468+
);
2469+
2470+
// Output notes are stored as note IDs only
2471+
let expected_note_ids: Vec<NoteId> =
2472+
original.output_notes().iter().map(NoteHeader::id).collect();
2473+
assert_eq!(
2474+
record.output_notes, expected_note_ids,
2475+
"Output note IDs DB roundtrip must be symmetric"
2476+
);
2477+
}
2478+
}

0 commit comments

Comments
 (0)