Skip to content
Merged
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
68 changes: 67 additions & 1 deletion runtime/src/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ use {
AtomicBool, AtomicI64, AtomicU64,
Ordering::{self, AcqRel, Acquire, Relaxed},
},
Arc, LockResult, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak,
Arc, LazyLock, LockResult, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak,
},
time::{Duration, Instant},
},
Expand Down Expand Up @@ -236,6 +236,14 @@ pub const MAX_LEADER_SCHEDULE_STAKES: Epoch = 5;
/// only the top 2000 validators by stake will be present in vote account structures.
pub const MAX_ALPENGLOW_VOTE_ACCOUNTS: usize = 2000;

/// The off-curve account where we store the Alpenglow clock. The clock sysvar has seconds
/// resolution while the Alpenglow clock has nanosecond resolution.
static NANOSECOND_CLOCK_ACCOUNT: LazyLock<Pubkey> = LazyLock::new(|| {
let (pubkey, _) =
Pubkey::find_program_address(&[b"alpenclock"], &agave_feature_set::alpenglow::id());
pubkey
});

pub type BankStatusCache = StatusCache<Result<()>>;
#[cfg_attr(
feature = "frozen-abi",
Expand Down Expand Up @@ -2911,6 +2919,64 @@ impl Bank {
self.store_account_and_update_capitalization(&GENESIS_CERTIFICATE_ACCOUNT, &cert_acct);
}

/// Update the clock sysvar from a block footer's nanosecond timestamp.
/// Also stores the nanosecond value for later retrieval via `get_nanosecond_clock`.
pub fn update_clock_from_footer(&self, unix_timestamp_nanos: i64) {
if !self.feature_set.is_active(&feature_set::alpenglow::id()) {
return;
}
Comment on lines +2925 to +2927
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.

Added these lines in to prevent accidental invocation.


// On epoch boundaries, update epoch_start_timestamp
//
// Note: the genesis block's bank is created via new_from_genesis, which calls update_clock
// unconditionally. In update_clock, we have a check for whether slot == 0, and if that's
// the case, the clock is set to self.unix_timestamp_from_genesis().
//
// As a result, we don't actually need the (0, _) case below, since it's never invoked.
// However, include this for completeness in the match statement.
let unix_timestamp_s = unix_timestamp_nanos / 1_000_000_000;
let epoch_start_timestamp = match (self.slot, self.parent()) {
(0, _) => self.unix_timestamp_from_genesis(),
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 line is basically just for tests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

is this function actually called for tests for the genesis block?
If we start a cluster with alpenglow enabled at genesis how is the initial value of the clock set?

Just want to make sure I completely understand the corner cases before we upstream

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.

is this function actually called for tests for the genesis block?

It's not; the genesis block's bank is created via new_from_genesis, which calls update_clock unconditionally. In update_clock, we have a check for whether slot == 0, and if that's the case, the clock is set to self.unix_timestamp_from_genesis().

You're correct in that we can probably get rid of this first match statement, though I'd rather keep this here for completeness.

I'll add in a comment for clarity, though.

Just want to make sure I completely understand the corner cases before we upstream

Writing this here for completeness - the rest of the banks are initialized via _new_from_parent, where there's this logic:

if new.get_alpenglow_genesis_certificate().is_none() {
    new.update_clock(Some(parent.epoch()));
}

During migration, this is the line that runs; concretely, on the zero'th slot (i.e., the zero'th Alpenglow slot), the genesis certificate should be set to None, meaning that we update the clock via the classic stake-weighted approach.

Later in the block, we then observe the footer, which updates the clock for the second time in the zero'th Alpenglow slot. So the clock is actually updated twice in the zero'th Alpenglow slot, when migrating from TowerBFT.

For Alpenglow slots 1+, the new.update_clock logic doesn't fire, because the Alpenglow genesis certificate is present; it's purely footer-based after that.

(_, Some(parent)) if parent.epoch() != self.epoch() => unix_timestamp_s,
_ => self.clock().epoch_start_timestamp,
};

// Update clock sysvar
// NOTE: block footer UNIX timestamps are in nanoseconds, but clock sysvar stores timestamps
// in seconds
let clock = sysvar::clock::Clock {
slot: self.slot,
epoch_start_timestamp,
epoch: self.epoch_schedule().get_epoch(self.slot),
leader_schedule_epoch: self.epoch_schedule().get_leader_schedule_epoch(self.slot),
unix_timestamp: unix_timestamp_s,
};

self.update_sysvar_account(&sysvar::clock::id(), |account| {
create_account(
&clock,
self.inherit_specially_retained_account_fields(account),
)
});

// Update Alpenglow clock
let data = wincode::serialize(&unix_timestamp_nanos).unwrap();
let lamports = Rent::default().minimum_balance(data.len());
let mut alpenclock_acct = AccountSharedData::new(lamports, data.len(), &system_program::ID);
alpenclock_acct.set_data_from_slice(&data);
Copy link
Copy Markdown

@AshwinSekar AshwinSekar Feb 20, 2026

Choose a reason for hiding this comment

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

no action for this PR, but since this wincode::serialize -> AccountSharedData::set_data_from_slice has popped up a couple times maybe we make a new constructor for it

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.

sg - third time's the charm


self.store_account_and_update_capitalization(&NANOSECOND_CLOCK_ACCOUNT, &alpenclock_acct);
}

/// Get the nanosecond clock value. Returns `None` if the nanosecond clock has not been
/// populated (i.e., before Alpenglow migration completes).
pub fn get_nanosecond_clock(&self) -> Option<i64> {
self.get_account(&NANOSECOND_CLOCK_ACCOUNT).map(|acct| {
wincode::deserialize(acct.data())
.expect("Couldn't deserialize nanosecond resolution clock")
})
}

pub fn confirmed_last_blockhash(&self) -> Hash {
const NUM_BLOCKHASH_CONFIRMATIONS: usize = 3;

Expand Down
Loading