Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 7 additions & 2 deletions core/src/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2485,12 +2485,17 @@ fn maybe_warp_slot(
SlotLeader::default(),
warp_slot,
));
// The bank must have a block id set to take a snapshot.
// Also must be set before calling set_root() just incase the warp slot triggers a
// snapshot request based on the snapshot config inside snapshot_controller.
let warp_bank = bank_forks.get(warp_slot).unwrap();
Bank::calculate_and_set_block_id_for_dcou(&warp_bank);
bank_forks.set_root(warp_slot, Some(snapshot_controller), Some(warp_slot));
leader_schedule_cache.set_root(&bank_forks.root_bank());
leader_schedule_cache.set_root(&warp_bank);

let full_snapshot_archive_info = match snapshot_bank_utils::bank_to_full_snapshot_archive(
ledger_path,
&bank_forks.root_bank(),
&warp_bank,
None,
&config.snapshot_config.full_snapshot_archives_dir,
&config.snapshot_config.incremental_snapshot_archives_dir,
Expand Down
11 changes: 11 additions & 0 deletions core/tests/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use {
solana_core::snapshot_packager_service::SnapshotPackagerService,
solana_genesis_config::GenesisConfig,
solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo},
solana_hash::Hash,
solana_keypair::Keypair,
solana_net_utils::SocketAddrSpace,
solana_runtime::{
Expand Down Expand Up @@ -194,6 +195,9 @@ where
if !bank.is_complete() {
bank.fill_bank_with_ticks_for_tests();
}
if bank.block_id().is_none() {
bank.set_block_id(Some(Hash::default()));
}
bank_forks.read().unwrap().prune_program_cache(bank.slot());
// set_root should send a snapshot request
bank_forks
Expand Down Expand Up @@ -439,6 +443,7 @@ fn test_bank_forks_incremental_snapshot() {
assert_eq!(bank.process_transaction(&tx), Ok(()));

bank.fill_bank_with_ticks_for_tests();
bank.set_block_id(Some(Hash::default()));

bank_scheduler
};
Expand Down Expand Up @@ -503,6 +508,8 @@ fn make_full_snapshot_archive(
"Making full snapshot archive from bank at slot: {}",
bank.slot(),
);
// The bank must have a block id set to take a snapshot.
Bank::calculate_and_set_block_id_for_dcou(bank);
snapshot_bank_utils::bank_to_full_snapshot_archive(
&snapshot_config.bank_snapshots_dir,
bank,
Expand All @@ -524,6 +531,8 @@ fn make_incremental_snapshot_archive(
bank.slot(),
incremental_snapshot_base_slot,
);
// The bank must have a block id set to take a snapshot.
Bank::calculate_and_set_block_id_for_dcou(bank);
snapshot_bank_utils::bank_to_incremental_snapshot_archive(
&snapshot_config.bank_snapshots_dir,
bank,
Expand Down Expand Up @@ -680,6 +689,7 @@ fn test_snapshots_with_background_services() {
assert_eq!(bank.process_transaction(&tx), Ok(()));

bank.fill_bank_with_ticks_for_tests();
bank.set_block_id(Some(Hash::default()));
}

// Call `BankForks::set_root()` to cause snapshots to be taken
Expand Down Expand Up @@ -839,6 +849,7 @@ fn test_fastboot_snapshots_teardown(exit_backpressure: bool) {
assert_eq!(bank.process_transaction(&tx), Ok(()));

bank.fill_bank_with_ticks_for_tests();
bank.set_block_id(Some(Hash::default()));

// Inject a fastboot snapshot at a specific slot
if slot % FASTBOOT_SNAPSHOT_INTERVAL_SLOTS == 0 {
Expand Down
3 changes: 3 additions & 0 deletions ledger-tool/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2529,6 +2529,9 @@ fn main() {
bank.slot(),
);

// The bank must have a block id set to take a snapshot.
Bank::calculate_and_set_block_id_for_dcou(&bank);

if is_incremental {
if starting_snapshot_hashes.is_none() {
eprintln!(
Expand Down
45 changes: 44 additions & 1 deletion runtime/src/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ pub struct BankFieldsToDeserialize {
pub(crate) accounts_data_len: u64,
pub(crate) accounts_lt_hash: AccountsLtHash,
pub(crate) bank_hash_stats: BankHashStats,
pub(crate) block_id: Option<Hash>, // Option wrapper can be removed in version after v4.1
}

/// Bank's common fields shared by all supported snapshot versions for serialization.
Expand Down Expand Up @@ -526,6 +527,7 @@ pub struct BankFieldsToSerialize {
pub accounts_data_len: u64,
pub versioned_epoch_stakes: HashMap<u64, VersionedEpochStakes>,
pub accounts_lt_hash: AccountsLtHash,
pub block_id: Hash,
}

// Can't derive PartialEq because RwLock doesn't implement PartialEq
Expand Down Expand Up @@ -677,6 +679,7 @@ impl BankFieldsToSerialize {
accounts_data_len: u64::default(),
versioned_epoch_stakes: HashMap::default(),
accounts_lt_hash: AccountsLtHash(LtHash([0x7E57; LtHash::NUM_ELEMENTS])),
block_id: Hash::default(),
}
}
}
Expand Down Expand Up @@ -2020,7 +2023,7 @@ impl Bank {
accounts_lt_hash: Mutex::new(fields.accounts_lt_hash),
cache_for_accounts_lt_hash: DashMap::default(),
stats_for_accounts_lt_hash: AccountsLtHashStats::default(),
block_id: RwLock::new(None),
block_id: RwLock::new(fields.block_id),
bank_hash_stats: AtomicBankHashStats::new(&fields.bank_hash_stats),
epoch_rewards_calculation_cache: Arc::new(Mutex::new(HashMap::default())),
expected_bank_hash: RwLock::new(None),
Expand Down Expand Up @@ -2128,6 +2131,7 @@ impl Bank {
accounts_data_len: self.load_accounts_data_size(),
versioned_epoch_stakes: self.epoch_stakes.clone(),
accounts_lt_hash: self.accounts_lt_hash.lock().unwrap().clone(),
block_id: self.block_id().expect("block id must be set"),
Comment thread
AshwinSekar marked this conversation as resolved.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@AshwinSekar - here we require that block_id is set in order to take a snapshot.

Codex told me this may not be safe though. Can you confirm, please? Here's its review comments:

Avoid requiring block_id on banks that still replay with None

On clusters where some peers still emit the legacy shred format, Blockstore::check_last_fec_set_and_get_block_id() legitimately returns Ok(None), and replay stores that None on rooted non-leader banks (the existing comments in Consensus::record_bank_vote call this out). This new expect means the accounts background service will panic as soon as one of those banks is selected for a full or incremental snapshot, because the normal validator snapshot path never backfills a synthetic ID the way the warp/ledger-tool paths now do.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy shreds have been removed on all live clusters and we explicitly drop such shreds on ingest.
We still have some code that refers to legacy shreds, but those are in the process of being ripped out.

Also if Blockstore::check_last_fec_set_and_get_block_id() fails we return an Err and the slot is marked dead meaning it can't be rooted and snapshotted in the first place.

}
}

Expand Down Expand Up @@ -6162,6 +6166,45 @@ impl Bank {
self.stakes_cache.stakes().clone()
}
}

/// Calculates and sets block id for `bank`.
///
/// This fn operates recursively. Since calculating the block id requires
/// the bank's parent's block id, if the bank's parent's block id is unset,
/// it will be calculated and set first.
///
/// Note this fn will also freeze `bank`.
///
/// Only to be called from dev contexts.
/// Couldn't make the fn actually DCOU, since it is called by
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

So is the issue here that warping a slot should in fact be dcou, but it's not due to how our crates are layed out?

Is this something that can be fixed by moving the test-validator to dev-bins?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

So is the issue here that warping a slot should in fact be dcou, but it's not due to how our crates are layed out?

I think the only use for warping is for dev context stuff, yes? I reserve the right to be wrong here though 😸

Is this something that can be fixed by moving the test-validator to dev-bins?

Maybe? I dunno. Just gotta put everything that calls calculate_and_set_block_id_for_dcou() into a DCOU block. Only moving test-validator doesn't do that, but may enable moving everything else. I haven't looked into this though.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This seems like trying to put a bandaid over the real issue: That warp_slot is DCOU and isn't documented as so because no ones fixed it.

So it should be fixed first, which might provide some other benefits as well.

Or it's not actually DCOU and the _for_dcou should be removed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, might be able to refactor code to mark warping as dcou. IMO seems quite a large endeavor to block this PR on though. I can rename calculate_and_set_block_id_for_dcou() if that is sufficient.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I guess it really depends how time pressing this feature is.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This PR doesn't change any of the semantics around warping. I don't understand why it should be gated on solana-test-validator.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

From the other conversation, it sounds like warping in the validator can be removed entirely. Would that change this PR at all?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

If validator.rs maybe_warp_slot() can be removed (or put into a DCOU block), then I think calculate_and_set_block_id_for_dcou() should be able to be marked with DCOU too.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Turning into a mess. Merge this and solve the DCOU issue later

/// Validator::new() when warping a slot.
pub fn calculate_and_set_block_id_for_dcou(bank: &Bank) {
if bank.block_id().is_some() {
// done!
return;
}

let Some(parent) = bank.parent() else {
// If bank doesn't have a parent, then use bank hash for block id,
// as parent's block id is not available for the calculation below.
// Must freeze() to ensure bank hash has been calculated.
bank.freeze();
bank.set_block_id(Some(bank.hash()));
return;
};

let parent_block_id = parent.block_id().unwrap_or_else(|| {
// if the parent's block id isn't set, we recurse so it gets set
Self::calculate_and_set_block_id_for_dcou(&parent);
parent.block_id().unwrap()
});

// must freeze() to ensure bank hash has been calculated
bank.freeze();
let block_id =
solana_sha256_hasher::hashv(&[parent_block_id.as_ref(), bank.hash().as_ref()]);
Comment thread
AshwinSekar marked this conversation as resolved.
bank.set_block_id(Some(block_id));
}
}

impl InvokeContextCallback for Bank {
Expand Down
2 changes: 2 additions & 0 deletions runtime/src/bank/accounts_lt_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ mod tests {
bank.squash();
bank.force_flush_accounts_cache();
}
bank.set_block_id(Some(Hash::default()));

// verification happens at startup, so mimic the behavior by loading from a snapshot
let snapshot_config = SnapshotConfig::default();
Expand Down Expand Up @@ -991,6 +992,7 @@ mod tests {
bank.squash();
bank.force_flush_accounts_cache();
}
bank.set_block_id(Some(Hash::default()));

let snapshot_config = SnapshotConfig::default();
let bank_snapshots_dir = TempDir::new().unwrap();
Expand Down
1 change: 1 addition & 0 deletions runtime/src/bank/builtins/core_bpf_migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2150,6 +2150,7 @@ pub(crate) mod tests {
test_context.run_program_checks(&bank, upgrade_slot);

bank.fill_bank_with_ticks_for_tests();
bank.set_block_id(Some(Hash::default()));
// Force flush the bank to create the account storage entry
bank.squash();
bank.force_flush_accounts_cache();
Expand Down
10 changes: 9 additions & 1 deletion runtime/src/bank/serde_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod tests {
accounts_file::{AccountsFile, AccountsFileError, StorageAccess},
},
solana_epoch_schedule::EpochSchedule,
solana_hash::Hash,
solana_native_token::LAMPORTS_PER_SOL,
solana_pubkey::Pubkey,
solana_stake_interface::state::Stake,
Expand Down Expand Up @@ -108,6 +109,7 @@ mod tests {

let accounts_db = &bank2.rc.accounts.accounts_db;

bank2.set_block_id(Some(Hash::default()));
bank2.squash();
bank2.force_flush_accounts_cache();

Expand All @@ -120,6 +122,7 @@ mod tests {
let mut bank_fields = bank2.get_fields_to_serialize();
let versioned_epoch_stakes = mem::take(&mut bank_fields.versioned_epoch_stakes);
let accounts_lt_hash = Some(bank_fields.accounts_lt_hash.clone().into());
let block_id = Some(bank_fields.block_id);
serde_snapshot::serialize_bank_snapshot_into(
&mut writer,
bank_fields,
Expand All @@ -131,6 +134,7 @@ mod tests {
unused_epoch_accounts_hash: None,
versioned_epoch_stakes,
accounts_lt_hash,
block_id,
},
)
.unwrap();
Expand Down Expand Up @@ -193,6 +197,7 @@ mod tests {
let bank0 = Arc::new(Bank::new_for_tests(&genesis_config));
bank0.squash();
let mut bank = Bank::new_from_parent(bank0.clone(), *bank0.leader(), 1);
bank.set_block_id(Some(Hash::default()));
bank.freeze();
add_root_and_flush_write_cache(&bank0);

Expand Down Expand Up @@ -277,6 +282,7 @@ mod tests {
while !bank.is_complete() {
bank.fill_bank_with_ticks_for_tests();
}
bank.set_block_id(Some(Hash::default()));

// Set extra field
bank.fee_rate_governor.lamports_per_signature = 7000;
Expand Down Expand Up @@ -356,7 +362,7 @@ mod tests {
#[cfg_attr(
feature = "frozen-abi",
derive(AbiExample),
frozen_abi(digest = "7NL9Sugo2js3PtueK7izqSwX5dGWKDMDVnjo6MirVPWE")
frozen_abi(digest = "ELfWa1ABrJu9sQABjieyqnWioDaHNykbmYqsQr7yYYBb")
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Digest changed because we added block_id.

❯ diff -u ./*7NL9Sugo2js3PtueK7izqSwX5dGWKDMDVnjo6MirVPWE* ./*ELfWa1ABrJu9sQABjieyqnWioDaHNykbmYqsQr7yYYBb*
--- ./bank__serde_snapshot__tests__test_bank_serialize__BankAbiTestWrapper_frozen_abi__test_api_digest_7NL9Sugo2js3PtueK7izqSwX5dGWKDMDVnjo6MirVPWE	2026-03-17 16:41:50
+++ ./bank__serde_snapshot__tests__test_bank_serialize__BankAbiTestWrapper_frozen_abi__test_api_digest_ELfWa1ABrJu9sQABjieyqnWioDaHNykbmYqsQr7yYYBb	2026-03-17 16:40:05
@@ -1275,7 +1275,7 @@
                                                 element u8
                                                     primitive u8
             element solana_runtime::serde_snapshot::ExtraFieldsToSerialize
-                struct ExtraFieldsToSerialize (fields = 5)
+                struct ExtraFieldsToSerialize (fields = 6)
                     field lamports_per_signature: u64
                         primitive u64
                     field unused_incremental_snapshot_persistence: core::option::Option<solana_runtime::serde_snapshot::UnusedIncrementalSnapshotPersistence>
@@ -4480,3 +4480,73 @@
                                             primitive u16
                                         element serde_with::ser::SerializeAsWrap<'_, u16, serde_with::Same>
                                             primitive u16
+                    field block_id: core::option::Option<solana_hash::Hash>
+                        enum Option (variants = 2)
+                            variant(0) None (unit)
+                            variant(1) Some(solana_hash::Hash) (newtype)
+                                struct Hash([u8; 32]) (newtype)
+                                    tuple (elements = 32)
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8
+                                        element u8
+                                            primitive u8

)]
#[derive(serde::Serialize)]
pub struct BankAbiTestWrapper {
Expand All @@ -369,6 +375,7 @@ mod tests {
S: serde::Serializer,
{
let bank = Bank::default_for_tests();
bank.set_block_id(Some(Hash::default()));
let snapshot_storages = AccountsDb::example().get_storages(0..1).0;
// ensure there is at least one snapshot storage example for ABI digesting
assert!(!snapshot_storages.is_empty());
Expand All @@ -394,6 +401,7 @@ mod tests {
unused_epoch_accounts_hash: Some(Hash::new_unique()),
versioned_epoch_stakes,
accounts_lt_hash: Some(AccountsLtHash(LtHash::identity()).into()),
block_id: Some(Hash::new_unique()),
},
)
}
Expand Down
68 changes: 68 additions & 0 deletions runtime/src/bank/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12469,6 +12469,7 @@ fn test_new_from_snapshot_uses_rent_from_sysvar() {

let mut bank = Bank::new_for_tests(&genesis_config);
bank.rent_collector.rent = wrong_rent.clone();
bank.set_block_id(Some(Hash::default()));

// Serialize bank to snapshot
let snapshot_storages = bank.get_snapshot_storages(None);
Expand Down Expand Up @@ -12508,3 +12509,70 @@ fn test_new_from_snapshot_uses_rent_from_sysvar() {
assert_eq!(new_bank.rent_collector.rent, expected_rent);
assert_ne!(new_bank.rent_collector.rent, wrong_rent);
}

#[test]
fn test_calculate_and_set_block_id_for_dcou() {
// scenario 1: block id already set
{
let bank = create_simple_test_bank(123);
let block_id = Hash::new_unique();
bank.set_block_id(Some(block_id));
Bank::calculate_and_set_block_id_for_dcou(&bank);
assert_eq!(bank.block_id(), Some(block_id));
}

// scenario 2: block id unset
{
let bank1 = Arc::new(create_simple_test_bank(123));
let block_id1 = Hash::new_unique();
// oldest ancestor must have block_id set
bank1.set_block_id(Some(block_id1));
bank1.fill_bank_with_ticks_for_tests();

let bank2 = new_from_parent(bank1);
Bank::calculate_and_set_block_id_for_dcou(&bank2);

// ensure expected block_id is correct
assert_eq!(
bank2.block_id(),
Some(hashv(&[block_id1.as_ref(), bank2.hash().as_ref()])),
);
}

// scenario 3: block id unset, multiple parents
{
let mut bank = Arc::new(create_simple_test_bank(123));
// oldest ancestor must have block id set
bank.set_block_id(Some(Hash::new_unique()));
for _ in 0..7 {
bank.fill_bank_with_ticks_for_tests();
bank = new_from_parent(bank).into();
}

Bank::calculate_and_set_block_id_for_dcou(&bank);

// we don't calculate the expected parent block id
// out-of-band, since scenario 2 covers that
assert_eq!(
bank.block_id(),
Some(hashv(&[
bank.parent_block_id().unwrap().as_ref(),
bank.hash().as_ref(),
])),
);
}

// scenario 4: no parent
{
let bank = create_simple_test_bank(123);
assert!(bank.parent().is_none());
assert!(bank.block_id().is_none());

// must freeze() to ensure bank hash is calculated
bank.freeze();
let expected_block_id = bank.hash();

Bank::calculate_and_set_block_id_for_dcou(&bank);
assert_eq!(bank.block_id(), Some(expected_block_id));
}
}
Loading
Loading