diff --git a/crates/store/src/db/migrations/005_incremental_code_pruning.sql b/crates/store/src/db/migrations/005_incremental_code_pruning.sql new file mode 100644 index 000000000..a346723a1 --- /dev/null +++ b/crates/store/src/db/migrations/005_incremental_code_pruning.sql @@ -0,0 +1,19 @@ +-- Make account-code pruning churn-driven. Previously each prune re-scanned every account row still +-- valid past the cutoff. A code pinned at one prune can only become collectable at a later prune if +-- its maximal referencing interval ends between the two cutoffs, so it suffices to scan rows whose +-- `valid_until` crossed the cutoff since the previous prune and probe each candidate code for a +-- surviving reference. + +-- Existence probe for candidate codes: for a given `code_commitment`, is there any row whose +-- validity interval reaches past the cutoff (`valid_until > cutoff`)? +CREATE INDEX idx_accounts_code_probe + ON accounts(code_commitment, valid_until) + WHERE code_commitment IS NOT NULL; + +-- Single-row (id = 0) record of the cutoff through which account-code pruning has completed. +-- Updated in the same transaction as the prune itself, so it is exact and crash-consistent. The +-- row is absent until the first prune under this schema, which runs a full (non-windowed) pass. +CREATE TABLE prune_progress ( + id INTEGER NOT NULL PRIMARY KEY CHECK (id = 0), + codes_cutoff BIGINT NOT NULL +); diff --git a/crates/store/src/db/migrations/tests/mod.rs b/crates/store/src/db/migrations/tests/mod.rs index ddb9c034e..9584a32f7 100644 --- a/crates/store/src/db/migrations/tests/mod.rs +++ b/crates/store/src/db/migrations/tests/mod.rs @@ -10,11 +10,12 @@ use super::*; use crate::db::models::queries::VALID_FOREVER; use crate::db::schema; -const EXPECTED_SCHEMA_HASHES: [SchemaHash; 4] = [ +const EXPECTED_SCHEMA_HASHES: [SchemaHash; 5] = [ SchemaHash::from_hex("cc92cb332410e6f63036b52cf953acb446c142d5c0fbbdbd6d3b4f466510b210"), SchemaHash::from_hex("7c783947d0bb2c9745d28f4bdcf329f84ad970c36aa07ea85441e62718d8bbbb"), SchemaHash::from_hex("e026a70464e897ae9a217f45c80d72341b1bfb757200e57e41145348473a9961"), SchemaHash::from_hex("a581a13b00e4aa1d4539459e2b351c0585fad33c5a876f830c9b943adac92dea"), + SchemaHash::from_hex("34bd293251a2647715dd91fa245bcd98d635e8070871b4f8335b3a3db364fc1e"), ]; #[test] diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 24d644224..90a040122 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -1643,7 +1643,7 @@ pub(crate) struct AccountStorageMapRowInsert { // ================================================================================================ /// Number of historical blocks to retain for vault assets, storage map values, and account codes. -/// Rows whose validity interval ends at or below `chain_tip - HISTORICAL_BLOCK_RETENTION` will be +/// Rows whose validity interval ends at or below `prune_tip - HISTORICAL_BLOCK_RETENTION` will be /// deleted; rows still valid anywhere inside the retention window (including all open-ended rows) /// are retained. pub const HISTORICAL_BLOCK_RETENTION: u32 = 50; @@ -1652,9 +1652,11 @@ pub const HISTORICAL_BLOCK_RETENTION: u32 = 50; /// reconstruction at any block within the retention window. /// /// A row is applicable for blocks in `[block_num, valid_until)`, so it is deletable exactly when -/// its interval ends at or below the cutoff (`chain_tip - HISTORICAL_BLOCK_RETENTION`): it then -/// cannot cover any block inside the window. Account codes follow the same rule — a code is -/// deleted only when no account row whose interval reaches past the cutoff references it. +/// its interval ends at or below the cutoff (`prune_tip - HISTORICAL_BLOCK_RETENTION`): it then +/// cannot cover any block inside the window. `prune_tip` is the effective tip for retention — it +/// lags the chain tip while old snapshot generations are still pinned by readers (see +/// [`crate::db::Db::apply_block`]). Account codes follow the same rule — a code is deleted only +/// when no account row whose interval reaches past the cutoff references it. /// /// # Returns /// A tuple of `(vault_assets_deleted, storage_map_values_deleted, account_codes_deleted)` @@ -1667,9 +1669,9 @@ pub const HISTORICAL_BLOCK_RETENTION: u32 = 50; )] pub(crate) fn prune_history( conn: &mut SqliteConnection, - chain_tip: BlockNumber, + prune_tip: BlockNumber, ) -> Result<(usize, usize, usize), DatabaseError> { - let cutoff_block = i64::from(chain_tip.as_u32().saturating_sub(HISTORICAL_BLOCK_RETENTION)); + let cutoff_block = i64::from(prune_tip.as_u32().saturating_sub(HISTORICAL_BLOCK_RETENTION)); tracing::Span::current().record("cutoff_block", cutoff_block); let vault_deleted = prune_account_vault_assets(conn, cutoff_block)?; let storage_deleted = prune_account_storage_map_values(conn, cutoff_block)?; @@ -1736,8 +1738,25 @@ fn prune_account_storage_map_values( /// inside the window, all open-ended (current) rows, and each account's baseline row — the row /// still valid at the cutoff even though it was written before it. /// -/// The forced `idx_accounts_code_validity` covering index keeps the subquery an index-only range -/// scan, sized by rows valid at or after the cutoff rather than total history. +/// Rather than re-checking every code on every prune, only codes whose deletability could have +/// changed since the previous prune are examined. A code survived the previous prune because at +/// least one `accounts` row with `valid_until > prev_cutoff` referenced it. For it to be +/// deletable now, all such rows must have expired by the new cutoff — including the longest-lived +/// one, whose `valid_until` therefore lands inside `(prev_cutoff, cutoff_block]`. Scanning the +/// rows that expired in that window thus finds every code that could have become deletable. The +/// scan is an `idx_accounts_code_validity` index range, so its cost scales with the number of +/// account updates since the previous prune, not with total history. Each candidate is deleted +/// only if the `idx_accounts_code_probe` existence probe finds no row still referencing it with +/// `valid_until > cutoff_block`. The previous cutoff is persisted in `prune_progress` within the +/// same transaction; when absent (first prune after migration, or a fresh database) a full pass +/// over all rows valid past the cutoff runs instead. +/// +/// Correctness of the windowed candidate set rests on two invariants: +/// - Rows are only ever closed to the `block_num` of the block currently being applied, which is +/// always above the cutoff, so every expiry crosses the window of some later prune. A write path +/// that back-dated `valid_until` below the current cutoff would leak the code forever. +/// - Every `account_codes` row is inserted alongside an `accounts` row referencing it (see +/// [`upsert_accounts`]); an orphan code with no referencing row would never become a candidate. #[miden_instrument( target = COMPONENT, err, @@ -1751,16 +1770,67 @@ fn prune_account_codes( ) -> Result { use diesel::sql_types::BigInt; - diesel::sql_query( - "DELETE FROM account_codes \ - WHERE code_commitment NOT IN ( \ - SELECT DISTINCT code_commitment \ - FROM accounts INDEXED BY idx_accounts_code_validity \ - WHERE code_commitment IS NOT NULL \ - AND valid_until > ?1 \ - )", - ) - .bind::(cutoff_block) - .execute(conn) - .map_err(DatabaseError::Diesel) + let prev_cutoff: Option = + SelectDsl::select(schema::prune_progress::table, schema::prune_progress::codes_cutoff) + .first(conn) + .optional() + .map_err(DatabaseError::Diesel)?; + + let deleted = match prev_cutoff { + // Codes are already pruned through this cutoff and nothing can become collectable while the + // cutoff stands still. Equality is the common case: the cutoff is clamped to zero for the + // first `HISTORICAL_BLOCK_RETENTION` blocks, and a pinned snapshot freezes the prune tip + // across consecutive blocks. A strictly greater `prev_cutoff` is unreachable through + // `apply_block` (the prune tip never regresses) but is guarded against so an out-of-order + // caller cannot move the marker backwards or run the delete with an inverted window. + Some(prev_cutoff) if prev_cutoff >= cutoff_block => return Ok(0), + Some(prev_cutoff) => diesel::sql_query( + "DELETE FROM account_codes \ + WHERE code_commitment IN ( \ + SELECT DISTINCT code_commitment \ + FROM accounts INDEXED BY idx_accounts_code_validity \ + WHERE code_commitment IS NOT NULL \ + AND valid_until > ?1 \ + AND valid_until <= ?2 \ + ) \ + AND NOT EXISTS ( \ + SELECT 1 \ + FROM accounts INDEXED BY idx_accounts_code_probe \ + WHERE accounts.code_commitment = account_codes.code_commitment \ + AND accounts.valid_until > ?2 \ + )", + ) + .bind::(prev_cutoff) + .bind::(cutoff_block) + .execute(conn) + .map_err(DatabaseError::Diesel)?, + // No recorded cutoff: full pass. The forced `idx_accounts_code_validity` covering index + // keeps the subquery an index-only range scan, sized by rows valid at or after the cutoff + // rather than total history. + None => diesel::sql_query( + "DELETE FROM account_codes \ + WHERE code_commitment NOT IN ( \ + SELECT DISTINCT code_commitment \ + FROM accounts INDEXED BY idx_accounts_code_validity \ + WHERE code_commitment IS NOT NULL \ + AND valid_until > ?1 \ + )", + ) + .bind::(cutoff_block) + .execute(conn) + .map_err(DatabaseError::Diesel)?, + }; + + diesel::insert_into(schema::prune_progress::table) + .values(( + schema::prune_progress::id.eq(0), + schema::prune_progress::codes_cutoff.eq(cutoff_block), + )) + .on_conflict(schema::prune_progress::id) + .do_update() + .set(schema::prune_progress::codes_cutoff.eq(cutoff_block)) + .execute(conn) + .map_err(DatabaseError::Diesel)?; + + Ok(deleted) } diff --git a/crates/store/src/db/models/queries/accounts/tests.rs b/crates/store/src/db/models/queries/accounts/tests.rs index 282c94663..cecdf0537 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -269,7 +269,7 @@ fn assert_storage_map_slot_entries( // ================================================================================================ #[test] -fn test_select_account_header_at_block_returns_none_for_nonexistent() { +fn select_account_header_at_block_returns_none_for_nonexistent() { let mut conn = setup_test_db(); let block_num = BlockNumber::from_epoch(0); insert_block_header(&mut conn, block_num); @@ -290,7 +290,7 @@ fn test_select_account_header_at_block_returns_none_for_nonexistent() { } #[test] -fn test_select_account_header_at_block_returns_correct_header() { +fn select_account_header_at_block_returns_correct_header() { let mut conn = setup_test_db(); let (account, _) = create_test_account_with_storage(); let account_id = account.id(); @@ -330,7 +330,7 @@ fn test_select_account_header_at_block_returns_correct_header() { } #[test] -fn test_select_account_header_at_block_historical_query() { +fn select_account_header_at_block_historical_query() { let mut conn = setup_test_db(); let (account, _) = create_test_account_with_storage(); let account_id = account.id(); @@ -378,7 +378,7 @@ fn test_select_account_header_at_block_historical_query() { // ================================================================================================ #[test] -fn test_select_account_vault_at_block_empty() { +fn select_account_vault_at_block_empty() { let mut conn = setup_test_db(); let (account, _) = create_test_account_with_storage(); let account_id = account.id(); @@ -413,7 +413,7 @@ fn test_select_account_vault_at_block_empty() { // ================================================================================================ #[test] -fn test_upsert_accounts_inserts_storage_header() { +fn upsert_accounts_inserts_storage_header() { let mut conn = setup_test_db(); let (account, account_id) = create_test_account_with_storage(); @@ -472,7 +472,7 @@ fn test_upsert_accounts_inserts_storage_header() { } #[test] -fn test_upsert_accounts_closes_previous_validity_interval() { +fn upsert_accounts_closes_previous_validity_interval() { let mut conn = setup_test_db(); let (account, account_id) = create_test_account_with_storage(); @@ -589,7 +589,7 @@ fn test_upsert_accounts_closes_previous_validity_interval() { } #[test] -fn test_upsert_accounts_with_multiple_storage_slots() { +fn upsert_accounts_with_multiple_storage_slots() { let mut conn = setup_test_db(); // Create account with 3 storage slots @@ -675,7 +675,7 @@ fn test_upsert_accounts_with_multiple_storage_slots() { } #[test] -fn test_upsert_accounts_with_empty_storage() { +fn upsert_accounts_with_empty_storage() { let mut conn = setup_test_db(); // Create account with no component storage slots (only auth slot) @@ -763,7 +763,7 @@ fn test_upsert_accounts_with_empty_storage() { // ================================================================================================ #[test] -fn test_select_latest_account_storage_ordering_semantics() { +fn select_latest_account_storage_ordering_semantics() { let mut conn = setup_test_db(); let block_num = BlockNumber::from_epoch(0); insert_block_header(&mut conn, block_num); @@ -816,7 +816,7 @@ fn test_select_latest_account_storage_ordering_semantics() { } #[test] -fn test_select_latest_account_storage_multiple_slots() { +fn select_latest_account_storage_multiple_slots() { let mut conn = setup_test_db(); let block_num = BlockNumber::from_epoch(0); insert_block_header(&mut conn, block_num); @@ -887,7 +887,7 @@ fn test_select_latest_account_storage_multiple_slots() { } #[test] -fn test_select_latest_account_storage_slot_updates() { +fn select_latest_account_storage_slot_updates() { let mut conn = setup_test_db(); let block_1 = BlockNumber::from_epoch(0); let block_2 = BlockNumber::from_epoch(1); @@ -963,7 +963,7 @@ fn test_select_latest_account_storage_slot_updates() { /// Focuses on deduplication logic that relies on ordering by (`vault_key` ASC and `block_num` /// DESC). #[test] -fn test_select_account_vault_at_block_historical_with_updates() { +fn select_account_vault_at_block_historical_with_updates() { use assert_matches::assert_matches; use miden_protocol::asset::FungibleAsset; use miden_protocol::testing::account_id::{ @@ -1068,7 +1068,7 @@ fn test_select_account_vault_at_block_historical_with_updates() { /// Tests that the query bounds the number of rows it reads, so an over-the-limit vault is detected /// without materializing the whole set. #[test] -fn test_select_account_vault_at_block_bounds_read_to_limit() { +fn select_account_vault_at_block_bounds_read_to_limit() { let mut conn = setup_test_db(); let (account, _) = create_test_account_with_storage(); let account_id = account.id(); @@ -1111,7 +1111,7 @@ fn test_select_account_vault_at_block_bounds_read_to_limit() { /// Tests that a 5-block history returns the correct asset per block. #[test] -fn test_select_account_vault_at_block_exponential_updates() { +fn select_account_vault_at_block_exponential_updates() { const BLOCK_COUNT: u32 = 5; use assert_matches::assert_matches; @@ -1172,7 +1172,7 @@ fn test_select_account_vault_at_block_exponential_updates() { /// Tests that deleted vault assets (asset = None) are correctly excluded from results, and that the /// deduplication handles deletion entries properly. #[test] -fn test_select_account_vault_at_block_with_deletion() { +fn select_account_vault_at_block_with_deletion() { use assert_matches::assert_matches; use miden_protocol::asset::FungibleAsset; use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; @@ -1287,6 +1287,17 @@ fn make_full_state_update(account: &Account) -> BlockAccountUpdate { /// The `push_value` must be different for each variant to produce a distinct MAST root and thus a /// distinct [`AccountCode::commitment`]. fn build_account_with_code(push_value: u32) -> Account { + // The seed alone determines the account ID, so every `push_value` variant maps to the same + // account — the property the prune tests rely on to model one account changing its code. + // Keeping the seed distinct from the other helpers' ([1u8; 32], [9u8; 32], ...) is only a + // precaution: each test runs on a fresh database, so a collision would matter only if a single + // test mixed this helper with another one. + build_account_with_code_seeded(push_value, [2u8; 32]) +} + +/// Same as [`build_account_with_code`] but with a caller-chosen ID seed, for tests that need +/// multiple distinct accounts sharing the same code. +fn build_account_with_code_seeded(push_value: u32, seed: [u8; 32]) -> Account { let code_src = format!("@account_procedure pub proc variant push.{push_value} end"); let component_code = CodeBuilder::default() .compile_component_code("test::code_prune", &code_src) @@ -1301,8 +1312,7 @@ fn build_account_with_code(push_value: u32) -> Account { ) .unwrap(); - // Seed [2u8; 32] keeps the account ID distinct from the other test helpers. - AccountBuilder::new([2u8; 32]) + AccountBuilder::new(seed) .account_type(AccountType::Public) .with_component(component) .with_component(AuthSingleSig::new(Approver::new( @@ -1316,7 +1326,7 @@ fn build_account_with_code(push_value: u32) -> Account { /// Prune test 2: when an account's code changes, the old code must be pruned after the retention /// window, while the new (latest) code is retained. #[test] -fn test_prune_account_code_retains_latest_after_code_change() { +fn prune_account_code_retains_latest_after_code_change() { let mut conn = setup_test_db(); // Block 0: account created with code A. @@ -1395,7 +1405,7 @@ fn test_prune_account_code_retains_latest_after_code_change() { /// Prune test 3: code A → code B → code A; after the retention window, code B must be pruned but /// code A must be retained because it is still the latest. #[test] -fn test_prune_account_code_retains_revisited_code() { +fn prune_account_code_retains_revisited_code() { let mut conn = setup_test_db(); // Block 0: code A. @@ -1485,7 +1495,7 @@ fn test_prune_account_code_retains_revisited_code() { /// preceding the account's first in-window update. Once a newer row falls below the cutoff, the /// code becomes prunable. #[test] -fn test_prune_account_code_retains_baseline_code() { +fn prune_account_code_retains_baseline_code() { let mut conn = setup_test_db(); // Block 0: code A. @@ -1558,6 +1568,156 @@ fn test_prune_account_code_retains_baseline_code() { ); } +/// Returns the cutoff recorded in `prune_progress`, if any. +fn codes_prune_cutoff(conn: &mut SqliteConnection) -> Option { + SelectDsl::select(schema::prune_progress::table, schema::prune_progress::codes_cutoff) + .first(conn) + .optional() + .expect("Failed to query prune_progress") +} + +/// Prune test 5: the incremental (windowed) codes prune must not delete a code whose expiring +/// reference crosses the cutoff window while another account still references it, and must delete +/// it once the last reference expires in a later window. +#[test] +fn prune_account_code_incremental_cross_account_reference() { + let mut conn = setup_test_db(); + + // The "switcher" account changes code first; the "holdout" account keeps code A pinned. + // Both accounts are created with code A at block 0. + // Prune 1 (tip 2R+1 → cutoff R+1): no marker yet, full pass; nothing is collectable. + // Block 2R+2: the switcher moves to code B — its code-A row expires at 2R+2. + // Prune 2 (tip 3R+2 → cutoff 2R+2): the expired row crosses the window `(R+1, 2R+2]`, making + // code A a candidate, but the holdout's open row still references it → retained. + // Block 3R+3: the holdout moves to code B — its code-A row expires at 3R+3. + // Prune 3 (tip 4R+3 → cutoff 3R+3): code A is a candidate again and no row references it past + // the cutoff → pruned. Code B is retained. + let block_0 = BlockNumber::from(0u32); + let block_first_prune = BlockNumber::from(2 * HISTORICAL_BLOCK_RETENTION + 1); + let block_switcher_to_b = BlockNumber::from(2 * HISTORICAL_BLOCK_RETENTION + 2); + let block_second_prune = BlockNumber::from(3 * HISTORICAL_BLOCK_RETENTION + 2); + let block_holdout_to_b = BlockNumber::from(3 * HISTORICAL_BLOCK_RETENTION + 3); + let block_third_prune = BlockNumber::from(4 * HISTORICAL_BLOCK_RETENTION + 3); + + for block in [block_0, block_switcher_to_b, block_holdout_to_b] { + insert_block_header(&mut conn, block); + } + + let switcher_on_a = build_account_with_code(1); + let switcher_on_b = build_account_with_code(2); + let holdout_on_a = build_account_with_code_seeded(1, [4u8; 32]); + let holdout_on_b = build_account_with_code_seeded(2, [4u8; 32]); + + assert_ne!(switcher_on_a.id(), holdout_on_a.id(), "accounts must be distinct"); + let code_commitment_a = switcher_on_a.code().commitment(); + let code_commitment_b = switcher_on_b.code().commitment(); + assert_eq!( + code_commitment_a, + holdout_on_a.code().commitment(), + "both accounts must share code A" + ); + + for account in [&switcher_on_a, &holdout_on_a] { + upsert_accounts( + &mut conn, + &[make_full_state_update(account)], + block_0, + &precomputed_states_from_account(account), + ) + .expect("block 0 upsert failed"); + } + + let (_, _, codes_deleted) = + prune_history(&mut conn, block_first_prune).expect("prune_history failed"); + assert_eq!(codes_deleted, 0, "no code is collectable while both accounts run code A"); + + upsert_accounts( + &mut conn, + &[make_full_state_update(&switcher_on_b)], + block_switcher_to_b, + &precomputed_states_from_account(&switcher_on_b), + ) + .expect("switcher code-change upsert failed"); + + let (_, _, codes_deleted) = + prune_history(&mut conn, block_second_prune).expect("prune_history failed"); + assert_eq!(codes_deleted, 0, "code A must survive while the holdout still references it"); + assert!( + account_code_exists(&mut conn, code_commitment_a), + "code A must be retained while the holdout references it" + ); + + upsert_accounts( + &mut conn, + &[make_full_state_update(&holdout_on_b)], + block_holdout_to_b, + &precomputed_states_from_account(&holdout_on_b), + ) + .expect("holdout code-change upsert failed"); + + let (_, _, codes_deleted) = + prune_history(&mut conn, block_third_prune).expect("prune_history failed"); + assert_eq!(codes_deleted, 1, "exactly one code (A) must be pruned"); + assert!(!account_code_exists(&mut conn, code_commitment_a), "code A must be pruned"); + assert!( + account_code_exists(&mut conn, code_commitment_b), + "current code B must be retained" + ); +} + +/// Prune test 6: `prune_progress` records the cutoff of the last codes prune; re-pruning at the +/// same or a lower cutoff deletes nothing and never moves the marker backwards. +#[test] +fn prune_account_codes_marker_never_regresses() { + let mut conn = setup_test_db(); + + // Same shape as prune test 2: code A at block 0 is superseded by code B at block R+1, so a + // prune at tip 2R+1 (cutoff R+1) collects code A. + let block_0 = BlockNumber::from(0u32); + let block_code_b = BlockNumber::from(HISTORICAL_BLOCK_RETENTION + 1); + let block_prune = BlockNumber::from(2 * HISTORICAL_BLOCK_RETENTION + 1); + + insert_block_header(&mut conn, block_0); + insert_block_header(&mut conn, block_code_b); + + let account_a = build_account_with_code(1); + let account_b = build_account_with_code(2); + + upsert_accounts( + &mut conn, + &[make_full_state_update(&account_a)], + block_0, + &precomputed_states_from_account(&account_a), + ) + .expect("block 0 upsert failed"); + upsert_accounts( + &mut conn, + &[make_full_state_update(&account_b)], + block_code_b, + &precomputed_states_from_account(&account_b), + ) + .expect("code-change upsert failed"); + + assert_eq!(codes_prune_cutoff(&mut conn), None, "no marker before the first prune"); + + let cutoff = i64::from(HISTORICAL_BLOCK_RETENTION + 1); + let (_, _, codes_deleted) = prune_history(&mut conn, block_prune).expect("first prune failed"); + assert_eq!(codes_deleted, 1, "exactly one code (A) must be pruned"); + assert_eq!(codes_prune_cutoff(&mut conn), Some(cutoff), "marker must record the cutoff"); + + // Re-pruning at the same tip is a no-op. + let (_, _, codes_deleted) = prune_history(&mut conn, block_prune).expect("second prune failed"); + assert_eq!(codes_deleted, 0, "re-pruning at the same cutoff must delete nothing"); + assert_eq!(codes_prune_cutoff(&mut conn), Some(cutoff), "marker must be unchanged"); + + // Pruning at a lower tip (cutoff 0) must not move the marker backwards. + let (_, _, codes_deleted) = + prune_history(&mut conn, BlockNumber::from(HISTORICAL_BLOCK_RETENTION)) + .expect("stale prune failed"); + assert_eq!(codes_deleted, 0, "pruning below the marker must delete nothing"); + assert_eq!(codes_prune_cutoff(&mut conn), Some(cutoff), "marker must never regress"); +} + #[test] #[miden_node_test_macro::enable_logging] fn network_accounts_subset_classifies_correctly() { diff --git a/crates/store/src/db/schema.rs b/crates/store/src/db/schema.rs index e571333d1..ebeeffaed 100644 --- a/crates/store/src/db/schema.rs +++ b/crates/store/src/db/schema.rs @@ -89,6 +89,13 @@ diesel::table! { } } +diesel::table! { + prune_progress (id) { + id -> Integer, + codes_cutoff -> BigInt, + } +} + diesel::table! { transactions (transaction_id) { transaction_id -> Binary, @@ -111,5 +118,6 @@ diesel::allow_tables_to_appear_in_same_query!( note_scripts, notes, nullifiers, + prune_progress, transactions, );