From 8126d38a79022a057cde4c4e531608447c9f223e Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Sat, 21 Mar 2026 11:07:12 +0000 Subject: [PATCH 1/7] test: add failing test for AllEntries on genesis block storage maps --- crates/store/src/db/tests.rs | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index a9f72b99a..e64aff351 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -1219,6 +1219,54 @@ fn select_storage_map_sync_values_paginates_until_last_block() { assert_eq!(page.values.len(), 1, "should include block 1 only"); } +/// Tests that `select_account_storage_map_values_paged` does not panic when all entries +/// exceed the limit and are in genesis block (block 0). Previously, this caused +/// `last_block_num.saturating_sub(1) = -1` which failed `BlockNumber::from_raw_sql`. +#[test] +fn select_storage_map_sync_values_all_entries_in_genesis_block() { + let mut conn = create_db(); + let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let slot_name = StorageSlotName::mock(8); + + let genesis = BlockNumber::GENESIS; + create_block(&mut conn, genesis); + + queries::upsert_accounts(&mut conn, &[mock_block_account_update(account_id, 0)], genesis) + .unwrap(); + + // Insert 3 entries, all in genesis block + for i in 0..3 { + queries::insert_account_storage_map_value( + &mut conn, + account_id, + genesis, + slot_name.clone(), + StorageMapKey::from_index(i), + num_to_word(i as u64 + 100), + ) + .unwrap(); + } + + // Query with limit=1 so that raw.len() (3) > limit (1), triggering the + // pagination branch. All entries are in block 0, so take_while produces + // nothing and last_block_num.saturating_sub(1) = -1. + let result = queries::select_account_storage_map_values_paged( + &mut conn, + account_id, + genesis..=genesis, + 1, + ); + + // Should not error - should return a valid page (possibly with empty values + // indicating no progress, which the caller interprets as limit_exceeded) + let page = result.expect("should not return an internal error for genesis block entries"); + // The page should indicate no progress was made (stuck at genesis) + assert!( + page.values.is_empty() || page.last_block_included == genesis, + "should indicate pagination did not make progress" + ); +} + #[tokio::test] #[miden_node_test_macro::enable_logging] async fn reconstruct_storage_map_from_db_pages_until_latest() { From 8934fa02cb50b4905dfff1f60a5661dc913da711 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Sat, 21 Mar 2026 11:08:28 +0000 Subject: [PATCH 2/7] fix(store): handle AllEntries pagination when all entries are in genesis block When all storage map entries exceed the pagination limit and reside in a single block (e.g. genesis block 0), `take_while` produces empty results since all rows share the same block_num. Previously this led to `last_block_num.saturating_sub(1)` = -1 (i64) which failed BlockNumber::from_raw_sql, returning an internal error to the client. Now when take_while yields no values (all entries in one block), we return block_range.start() as last_block_included with empty values. The caller (reconstruct_storage_map_from_db) interprets this as no pagination progress and returns limit_exceeded, which is the correct response for maps exceeding the entry limit. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/store/src/db/models/queries/accounts.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 0e8ae47b6..e630300ba 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -721,17 +721,20 @@ pub(crate) fn select_account_storage_map_values_paged( let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last() && raw.len() > limit { - // NOTE: If the query contains at least one more row than the amount of storage map updates - // allowed in a single block for an account, then the response is guaranteed to have at - // least two blocks - let values = raw .into_iter() .take_while(|(bn, ..)| *bn != last_block_num) .map(StorageMapValue::from_raw_row) .collect::, DatabaseError>>()?; - (BlockNumber::from_raw_sql(last_block_num.saturating_sub(1))?, values) + if values.is_empty() { + // All entries are in the same block and exceed the limit. + // Return the range start to signal no progress was made, + // which the caller interprets as limit_exceeded. + (*block_range.start(), values) + } else { + (BlockNumber::from_raw_sql(last_block_num.saturating_sub(1))?, values) + } } else { ( *block_range.end(), From e0a7ba8bc42a6679f106d8d5fd6128ce68fcdf7a Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Sun, 22 Mar 2026 13:38:51 +0000 Subject: [PATCH 3/7] fix(store): derive last_block_included from kept rows instead of arithmetic Replace `last_block_num.saturating_sub(1)` with reading the block number from the last kept row after take_while. This is correct for all cases: - Multiple blocks: uses the actual last block we're returning - Single block overflow: returns block_range.start() (no progress) - No assumption about contiguous block numbers Also adds tests for single non-genesis block overflow and multi-block pagination to verify correctness. Co-Authored-By: Claude Opus 4.6 (1M context) fix: address clippy lints and rename variable Co-Authored-By: Claude Opus 4.6 (1M context) --- .../store/src/db/models/queries/accounts.rs | 26 +++-- crates/store/src/db/tests.rs | 103 +++++++++++++++++- 2 files changed, 116 insertions(+), 13 deletions(-) diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index e630300ba..51e988c1e 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -716,25 +716,27 @@ pub(crate) fn select_account_storage_map_values_paged( .limit(i64::try_from(limit + 1).expect("limit fits within i64")) .load(conn)?; - // Discard the last block in the response (assumes more than one block may be present) - + // If we got more rows than the limit, the last block may be incomplete so we + // drop it entirely and derive last_block_included from the remaining rows. let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last() && raw.len() > limit { - let values = raw + let values: Vec<_> = raw.into_iter().take_while(|(bn, ..)| *bn != last_block_num).collect(); + + let last_block_included = match values.last() { + Some(&(bn, ..)) => BlockNumber::from_raw_sql(bn)?, + // All rows are in the same block and exceed the limit. + // Return the range start to signal no progress was made, + // which the caller interprets as limit_exceeded. + None => *block_range.start(), + }; + + let values = values .into_iter() - .take_while(|(bn, ..)| *bn != last_block_num) .map(StorageMapValue::from_raw_row) .collect::, DatabaseError>>()?; - if values.is_empty() { - // All entries are in the same block and exceed the limit. - // Return the range start to signal no progress was made, - // which the caller interprets as limit_exceeded. - (*block_range.start(), values) - } else { - (BlockNumber::from_raw_sql(last_block_num.saturating_sub(1))?, values) - } + (last_block_included, values) } else { ( *block_range.end(), diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index e64aff351..0d885123b 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -1242,7 +1242,7 @@ fn select_storage_map_sync_values_all_entries_in_genesis_block() { genesis, slot_name.clone(), StorageMapKey::from_index(i), - num_to_word(i as u64 + 100), + num_to_word(u64::from(i) + 100), ) .unwrap(); } @@ -1267,6 +1267,107 @@ fn select_storage_map_sync_values_all_entries_in_genesis_block() { ); } +/// Tests that single-block overflow works for non-genesis blocks too. +/// All entries are in block 5 and exceed the limit. The function should +/// signal no progress rather than returning incorrect data. +#[test] +fn select_storage_map_sync_values_all_entries_in_single_non_genesis_block() { + let mut conn = create_db(); + let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let slot_name = StorageSlotName::mock(10); + + let block5 = BlockNumber::from(5); + create_block(&mut conn, block5); + + queries::upsert_accounts(&mut conn, &[mock_block_account_update(account_id, 0)], block5) + .unwrap(); + + for i in 0..3 { + queries::insert_account_storage_map_value( + &mut conn, + account_id, + block5, + slot_name.clone(), + StorageMapKey::from_index(i), + num_to_word(u64::from(i) + 200), + ) + .unwrap(); + } + + // limit=1, so 3 rows > 1 triggers pagination. All in block 5. + let page = + queries::select_account_storage_map_values_paged(&mut conn, account_id, block5..=block5, 1) + .unwrap(); + + assert!(page.values.is_empty(), "should have no values when single block exceeds limit"); + assert_eq!(page.last_block_included, block5, "should signal no progress at block 5"); +} + +/// Tests that normal multi-block pagination still works correctly: +/// entries in blocks 1, 2, 3 with limit causing block 3 to be dropped. +#[test] +fn select_storage_map_sync_values_multi_block_pagination() { + let mut conn = create_db(); + let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(); + let slot_name = StorageSlotName::mock(11); + + let block1 = BlockNumber::from(1); + let block2 = BlockNumber::from(2); + let block3 = BlockNumber::from(3); + + create_block(&mut conn, block1); + create_block(&mut conn, block2); + create_block(&mut conn, block3); + + queries::upsert_accounts(&mut conn, &[mock_block_account_update(account_id, 0)], block1) + .unwrap(); + queries::upsert_accounts(&mut conn, &[mock_block_account_update(account_id, 1)], block2) + .unwrap(); + queries::upsert_accounts(&mut conn, &[mock_block_account_update(account_id, 2)], block3) + .unwrap(); + + // 1 entry in block 1, 1 in block 2, 1 in block 3 + queries::insert_account_storage_map_value( + &mut conn, + account_id, + block1, + slot_name.clone(), + StorageMapKey::from_index(1), + num_to_word(11), + ) + .unwrap(); + queries::insert_account_storage_map_value( + &mut conn, + account_id, + block2, + slot_name.clone(), + StorageMapKey::from_index(2), + num_to_word(22), + ) + .unwrap(); + queries::insert_account_storage_map_value( + &mut conn, + account_id, + block3, + slot_name.clone(), + StorageMapKey::from_index(3), + num_to_word(33), + ) + .unwrap(); + + // limit=2: query fetches 3 rows (limit+1), drops block 3, keeps blocks 1-2 + let page = queries::select_account_storage_map_values_paged( + &mut conn, + account_id, + BlockNumber::GENESIS..=block3, + 2, + ) + .unwrap(); + + assert_eq!(page.values.len(), 2, "should include entries from blocks 1 and 2"); + assert_eq!(page.last_block_included, block2, "last included block should be 2"); +} + #[tokio::test] #[miden_node_test_macro::enable_logging] async fn reconstruct_storage_map_from_db_pages_until_latest() { From 3d13100cf1a5d38b64d2975fe9d579e90278368b Mon Sep 17 00:00:00 2001 From: Marti Date: Sun, 22 Mar 2026 14:24:24 +0000 Subject: [PATCH 4/7] chore: add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 904db239b..0aea5e51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - Fixed `bundled bootstrap` requiring `--validator.key.hex` or `--validator.key.kms-id` despite a default key being configured ([#1732](https://github.com/0xMiden/node/pull/1732)). - Fixed incorrectly classifying private notes with the network attachment as network notes ([#1378](https://github.com/0xMiden/node/pull/1738)). - Fixed accept header version negotiation rejecting all pre-release versions; pre-release label matching is now lenient, accepting any numeric suffix within the same label (e.g. `alpha.3` accepts `alpha.1`) ([#1755](https://github.com/0xMiden/node/pull/1755)). +- Fixed `GetAccount` returning an internal error for `AllEntries` requests on storage maps where all entries are in a single block (e.g. genesis accounts) ([#1816](https://github.com/0xMiden/node/pull/1816)). ## v0.13.8 (2026-03-12) From a6016ed90bd4637225304004f071a85c81716485 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 23 Mar 2026 07:27:49 +0000 Subject: [PATCH 5/7] refactor: single-pass collect to avoid double allocation Map raw rows to StorageMapValue in one pass, then read last_block_included from the last element's block_num field. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/store/src/db/models/queries/accounts.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 51e988c1e..0d5218746 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -721,21 +721,20 @@ pub(crate) fn select_account_storage_map_values_paged( let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last() && raw.len() > limit { - let values: Vec<_> = raw.into_iter().take_while(|(bn, ..)| *bn != last_block_num).collect(); + let values = raw + .into_iter() + .take_while(|(bn, ..)| *bn != last_block_num) + .map(StorageMapValue::from_raw_row) + .collect::, DatabaseError>>()?; let last_block_included = match values.last() { - Some(&(bn, ..)) => BlockNumber::from_raw_sql(bn)?, + Some(v) => v.block_num, // All rows are in the same block and exceed the limit. // Return the range start to signal no progress was made, // which the caller interprets as limit_exceeded. None => *block_range.start(), }; - let values = values - .into_iter() - .map(StorageMapValue::from_raw_row) - .collect::, DatabaseError>>()?; - (last_block_included, values) } else { ( From 7f6a3e1866865242df6659c72db6003cdea6f651 Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 23 Mar 2026 10:48:10 +0100 Subject: [PATCH 6/7] Update crates/store/src/db/models/queries/accounts.rs Co-authored-by: Mirko <48352201+Mirko-von-Leipzig@users.noreply.github.com> --- crates/store/src/db/models/queries/accounts.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 0d5218746..a08bb183a 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -727,13 +727,7 @@ pub(crate) fn select_account_storage_map_values_paged( .map(StorageMapValue::from_raw_row) .collect::, DatabaseError>>()?; - let last_block_included = match values.last() { - Some(v) => v.block_num, - // All rows are in the same block and exceed the limit. - // Return the range start to signal no progress was made, - // which the caller interprets as limit_exceeded. - None => *block_range.start(), - }; + let last_block_included = values.last().unwrap_or_else(|| *block_range.start()); (last_block_included, values) } else { From 5171b0db6786b839f2f52083d63f05cb1e44d7af Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 23 Mar 2026 10:27:42 +0000 Subject: [PATCH 7/7] fix: use map_or not unwrap_or_else --- crates/store/src/db/models/queries/accounts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index a08bb183a..018ab7389 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -727,7 +727,7 @@ pub(crate) fn select_account_storage_map_values_paged( .map(StorageMapValue::from_raw_row) .collect::, DatabaseError>>()?; - let last_block_included = values.last().unwrap_or_else(|| *block_range.start()); + let last_block_included = values.last().map_or(*block_range.start(), |v| v.block_num); (last_block_included, values) } else {