diff --git a/Cargo.lock b/Cargo.lock index e4c21d437..02ec99e95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3875,6 +3875,8 @@ dependencies = [ "solana-program 3.0.0", "solana-signature 2.3.0", "solana-signature 3.3.0", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -6266,7 +6268,7 @@ dependencies = [ [[package]] name = "solana-account" version = "3.4.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "bincode", "qualifier_attr", @@ -7846,7 +7848,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "base64 0.22.1", "bincode", @@ -8542,7 +8544,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "ahash 0.8.12", "log", @@ -8858,7 +8860,7 @@ dependencies = [ [[package]] name = "solana-transaction-context" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "bincode", "qualifier_attr", diff --git a/Cargo.toml b/Cargo.toml index 451715d7d..1682d3c6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,8 +140,9 @@ paste = "1.0" prometheus = "0.13.4" # Keep in sync with `solana-storage-proto` codegen. +# TODO: Update to the merged version when ready -solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e", features = [ +solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc", features = [ "dev-context-only-utils", ] } solana-transaction-error = { version = "3.0" } @@ -194,7 +195,8 @@ serde_json = "1.0" serde_with = "3.16" serial_test = "3.2" sha3 = "0.10.8" -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } +# TODO: Update to the merged version when ready +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } solana-account-decoder = { version = "4.0" } solana-account-decoder-client-types = { version = "4.0" } solana-account-info = { version = "3.1" } @@ -246,7 +248,8 @@ solana-program = "3.0" solana-program-error = { version = "3.0" } solana-program-option = { version = "3.0" } solana-program-pack = { version = "3.0" } -solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } +# TODO: Update to the merged version when ready +solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } solana-pubkey = { version = "4.1" } solana-pubsub-client = { version = "4.0" } solana-rent = { version = "3.0" } @@ -270,22 +273,28 @@ solana-system-program = { version = "=4.0.0", features = [ ] } solana-system-transaction = { version = "3.0" } solana-sysvar = { version = "3.0" } +solana-sysvar-id = { version = "3.0" } solana-timings = { package = "solana-svm-timings", version = "=4.0.0", features = [ "agave-unstable-api", ] } solana-transaction = { version = "3.0" } +# TODO: Update to the merged version when ready [workspace.dependencies.solana-svm] features = ["dev-context-only-utils"] git = "https://github.com/magicblock-labs/magicblock-svm.git" -rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" +rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" [patch.crates-io] -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } -solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } +# TODO: Update to the merged version when ready +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } +# TODO: Update to the merged version when ready +solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } solana-storage-proto = { path = "storage-proto" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } -solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } +# TODO: Update to the merged version when ready +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } +# TODO: Update to the merged version when ready +solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } # Fork is used to enable `disable_manual_compaction` usage # Fork is based on commit d4e9e16 of rocksdb (parent commit of 0.23.0 release) # without patching update isn't possible due to conflict with solana deps diff --git a/magicblock-core/src/link/replication.rs b/magicblock-core/src/link/replication.rs index 2825bc1ab..41d3fc948 100644 --- a/magicblock-core/src/link/replication.rs +++ b/magicblock-core/src/link/replication.rs @@ -77,8 +77,10 @@ pub struct Block { pub slot: Slot, /// Blockhash for this slot. pub hash: Hash, - /// Unix timestamp (seconds). - pub timestamp: i64, + /// Unix timestamp in milliseconds. The whole-second `Clock::unix_timestamp` + /// is derived from this; the sub-second remainder is the precision exposed + /// via the `HighPrecisionClock` sysvar. + pub timestamp_millis: i64, } /// Periodic checkpoint for state verification and catch-up. diff --git a/magicblock-ledger/src/blockstore_processor/mod.rs b/magicblock-ledger/src/blockstore_processor/mod.rs index 9e60bf1a4..ec2c807e7 100644 --- a/magicblock-ledger/src/blockstore_processor/mod.rs +++ b/magicblock-ledger/src/blockstore_processor/mod.rs @@ -110,15 +110,22 @@ async fn replay_blocks( transactions: successfull_txs, }; - let Some(timestamp) = block.block_time else { + if block.block_time.is_none() { return Err(LedgerError::BlockStoreProcessor(format!( "Block has no timestamp, {block:?}", ))); - }; + } { - let block = - LatestBlockInner::new(block.slot, block.blockhash, timestamp); - ledger.latest_block().store(block); + // Restore the millisecond precision so the HighPrecisionClock + // sysvar is reproduced exactly during replay. + let latest = LatestBlockInner::new_with_millis( + block.slot, + block.blockhash, + ledger + .get_block_time_millis(block.slot)? + .unwrap_or_default(), + ); + ledger.latest_block().store(latest); } // Transactions are stored in the ledger ordered by most recent to latest // such to replay them in the order they executed we need to reverse them diff --git a/magicblock-ledger/src/database/columns.rs b/magicblock-ledger/src/database/columns.rs index 984172a2e..a829f01a0 100644 --- a/magicblock-ledger/src/database/columns.rs +++ b/magicblock-ledger/src/database/columns.rs @@ -56,8 +56,14 @@ pub struct SlotSignatures; /// The block time column /// +/// Stores the block timestamp as **milliseconds** since the Unix epoch. The +/// sub-second remainder preserves the precision exposed via the +/// `HighPrecisionClock` sysvar so it can be reproduced deterministically during +/// ledger replay; [`Ledger::get_block_time`] rounds it down to whole seconds +/// for consumers that expect a [`UnixTimestamp`] (RPC `getBlockTime`, etc.). +/// /// * index type: `u64` (see [`SlotColumn`]) -/// * value type: [`UnixTimestamp`] +/// * value type: [`UnixTimestamp`] (milliseconds, despite the type name) pub struct Blocktime; /// The block hash column diff --git a/magicblock-ledger/src/lib.rs b/magicblock-ledger/src/lib.rs index 77b9e4210..a6f417421 100644 --- a/magicblock-ledger/src/lib.rs +++ b/magicblock-ledger/src/lib.rs @@ -16,6 +16,7 @@ pub struct LatestBlockInner { pub slot: u64, pub blockhash: Hash, pub clock: Clock, + pub timestamp_millis: i64, } /// Atomically updated, shared, latest block information @@ -36,16 +37,29 @@ pub struct LatestBlock { } impl LatestBlockInner { + /// Creates a block from a whole-second timestamp. The millisecond timestamp + /// is that value with a zero sub-second component. pub fn new(slot: u64, blockhash: Hash, timestamp: i64) -> Self { + Self::new_with_millis(slot, blockhash, timestamp * 1000) + } + + /// Creates a block from a millisecond-accurate timestamp, deriving the + /// whole-second `Clock::unix_timestamp` from it. + pub fn new_with_millis( + slot: u64, + blockhash: Hash, + timestamp_millis: i64, + ) -> Self { let clock = Clock { slot: slot + 1, - unix_timestamp: timestamp, + unix_timestamp: timestamp_millis.div_euclid(1000), ..Default::default() }; Self { slot, blockhash, clock, + timestamp_millis, } } } diff --git a/magicblock-ledger/src/store/api.rs b/magicblock-ledger/src/store/api.rs index 7809cbd23..e831becb5 100644 --- a/magicblock-ledger/src/store/api.rs +++ b/magicblock-ledger/src/store/api.rs @@ -167,8 +167,13 @@ impl Ledger { latest_block, }; let (slot, blockhash) = ledger.get_max_blockhash()?; - let time = ledger.get_block_time(slot)?.unwrap_or_default(); - let block = LatestBlockInner::new(slot, blockhash, time); + let timestamp_millis = + ledger.get_block_time_millis(slot)?.unwrap_or_default(); + let block = LatestBlockInner::new_with_millis( + slot, + blockhash, + timestamp_millis, + ); ledger.latest_block.store(block); ledger.initialize_lowest_cleanup_slot()?; @@ -279,6 +284,19 @@ impl Ledger { &self, slot: Slot, ) -> LedgerResult> { + let _lock = self.check_lowest_cleanup_slot(slot)?; + // The column stores milliseconds; consumers of this API expect whole + // seconds (RPC `getBlockTime`, block metadata, replay, ...). + Ok(self + .blocktime_cf + .get(slot)? + .map(|millis| millis.div_euclid(1000))) + } + + pub(crate) fn get_block_time_millis( + &self, + slot: Slot, + ) -> LedgerResult> { let _lock = self.check_lowest_cleanup_slot(slot)?; self.blocktime_cf.get(slot) } @@ -368,8 +386,7 @@ impl Ledger { // As far as we are concerned these are just the time when we advanced to // a specific slot. pub fn write_block(&self, block: LatestBlockInner) -> LedgerResult<()> { - self.blocktime_cf - .put(block.slot, &block.clock.unix_timestamp)?; + self.blocktime_cf.put(block.slot, &block.timestamp_millis)?; self.blocktime_cf.try_increase_entry_counter(1); self.blockhash_cf.put(block.slot, &block.blockhash)?; @@ -2784,4 +2801,53 @@ mod tests { let result = store.verify_transaction_signature(&real_sig).unwrap(); assert_eq!(result, None); } + + #[test] + fn test_block_time_millis_persist_and_reload() { + init_logger!(); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + + let slot = 7; + let secs = 1_700_000_000; + let millis = secs * 1000 + 123; + + { + let store = Ledger::open(ledger_path.path()).unwrap(); + let block = LatestBlockInner::new_with_millis( + slot, + Hash::new_unique(), + millis, + ); + store.write_block(block).unwrap(); + + // The single Blocktime column holds the full millisecond timestamp, + assert_eq!( + store.get_block_time_millis(slot).unwrap(), + Some(millis) + ); + // ...which reads back as whole seconds for second-precision consumers. + assert_eq!(store.get_block_time(slot).unwrap(), Some(secs)); + assert_eq!(store.latest_block().load().timestamp_millis, millis); + assert_eq!(store.latest_block().load().clock.unix_timestamp, secs); + } + + // Reopening reconstructs the millisecond precision for the tip block. + { + let store = Ledger::open(ledger_path.path()).unwrap(); + assert_eq!(store.latest_block().load().timestamp_millis, millis); + assert_eq!(store.latest_block().load().clock.unix_timestamp, secs); + } + } + + #[test] + fn test_block_time_absent_returns_none() { + init_logger!(); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let store = Ledger::open(ledger_path.path()).unwrap(); + + // A slot with no recorded block time reports no time in either form. + let slot = 3; + assert_eq!(store.get_block_time(slot).unwrap(), None); + assert_eq!(store.get_block_time_millis(slot).unwrap(), None); + } } diff --git a/magicblock-magic-program-api/Cargo.toml b/magicblock-magic-program-api/Cargo.toml index 4c6358131..3b64f0469 100644 --- a/magicblock-magic-program-api/Cargo.toml +++ b/magicblock-magic-program-api/Cargo.toml @@ -10,16 +10,17 @@ edition.workspace = true [features] default = [] -backward-compat = [ - "dep:solana-program-compat", - "dep:solana-signature-compat", -] +backward-compat = ["dep:solana-program-compat", "dep:solana-signature-compat"] [dependencies] solana-program = { workspace = true } solana-program-compat = { package = "solana-program", version = ">=2.0, <3", default-features = false, optional = true } solana-signature = { workspace = true, features = ["serde"] } -solana-signature-compat = { package = "solana-signature", version = ">=2.0, <3", default-features = false, features = ["serde"], optional = true } +solana-signature-compat = { package = "solana-signature", version = ">=2.0, <3", default-features = false, features = [ + "serde", +], optional = true } +solana-sysvar = { workspace = true } +solana-sysvar-id = { workspace = true } bincode = "^1.3.3" serde = { version = "^1.0.228", features = ["derive"] } const-crypto = "0.3.0" diff --git a/magicblock-magic-program-api/src/lib.rs b/magicblock-magic-program-api/src/lib.rs index 25b614b7e..2d7d1136b 100644 --- a/magicblock-magic-program-api/src/lib.rs +++ b/magicblock-magic-program-api/src/lib.rs @@ -3,6 +3,7 @@ pub mod compat; pub mod instruction; pub mod pda; pub mod response; +pub mod sysvar; pub use compat::{declare_id, pubkey, Pubkey}; diff --git a/magicblock-magic-program-api/src/sysvar.rs b/magicblock-magic-program-api/src/sysvar.rs new file mode 100644 index 000000000..fecdb9be6 --- /dev/null +++ b/magicblock-magic-program-api/src/sysvar.rs @@ -0,0 +1,71 @@ +//! MagicBlock-specific sysvars. + +use serde::{Deserialize, Serialize}; +use solana_program::program_error::ProgramError; +use solana_sysvar::{get_sysvar, Sysvar, SysvarSerialize}; +use solana_sysvar_id::SysvarId; + +use crate::{pubkey, Pubkey}; + +/// Address of the [`HighPrecisionClock`] sysvar. +/// +/// This is a MagicBlock-specific sysvar and has no counterpart on Solana +/// mainnet. It is owned by the sysvar program like every other sysvar. +pub const HIGH_PRECISION_CLOCK_ID: Pubkey = + pubkey!("SysvarHighPrecisionC1ock1111111111111111111"); + +/// A high-precision companion to the [`Clock`](solana_program::clock::Clock) +/// sysvar. +/// +/// The standard `Clock` can only carry whole seconds (`unix_timestamp: i64`), +/// discarding the sub-second precision the validator samples from wall-clock +/// time. This sysvar exposes the same timestamp with **millisecond** accuracy: +/// `unix_timestamp_millis` is the Unix time in milliseconds, and +/// `unix_timestamp_millis / 1000` equals `Clock::unix_timestamp`. +/// +/// On-chain programs can read it without passing the account, straight from the +/// sysvar cache: +/// +/// ```ignore +/// use magicblock_magic_program_api::sysvar::HighPrecisionClock; +/// use solana_program::sysvar::Sysvar; +/// +/// let millis = HighPrecisionClock::get()?.unix_timestamp_millis; +/// ``` +#[derive( + Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct HighPrecisionClock { + /// Milliseconds since the Unix epoch. + pub unix_timestamp_millis: i64, +} + +impl HighPrecisionClock { + /// Size in bytes of the bincode-serialized sysvar (a single `i64`). + pub const SIZE: usize = 8; +} + +impl SysvarId for HighPrecisionClock { + fn id() -> Pubkey { + HIGH_PRECISION_CLOCK_ID + } + + fn check_id(id: &Pubkey) -> bool { + id == &HIGH_PRECISION_CLOCK_ID + } +} + +impl Sysvar for HighPrecisionClock { + /// Reads the sysvar directly from the runtime cache via the `sol_get_sysvar` + /// syscall, without requiring the account to be passed to the program. + fn get() -> Result { + let mut data = [0u8; Self::SIZE]; + get_sysvar(&mut data, &HIGH_PRECISION_CLOCK_ID, 0, Self::SIZE as u64)?; + let unix_timestamp_millis = i64::from_le_bytes(data); + Ok(Self { + unix_timestamp_millis, + }) + } +} + +impl SysvarSerialize for HighPrecisionClock {} diff --git a/magicblock-processor/src/executor/mod.rs b/magicblock-processor/src/executor/mod.rs index 0a2edab46..97016cb6c 100644 --- a/magicblock-processor/src/executor/mod.rs +++ b/magicblock-processor/src/executor/mod.rs @@ -17,6 +17,7 @@ use magicblock_core::{ Slot, }; use magicblock_ledger::{LatestBlock, LatestBlockInner, Ledger}; +use magicblock_program::sysvar::HighPrecisionClock; use solana_feature_set::FeatureSet; use solana_program::slot_hashes::SlotHashes; use solana_program_runtime::loaded_programs::{ @@ -252,6 +253,9 @@ impl TransactionExecutor { fn set_sysvars(&self, block: &LatestBlockInner) { let mut cache = self.processor.writable_sysvar_cache().write().unwrap(); cache.set_sysvar_for_tests(&block.clock); + cache.set_sysvar_for_tests(&HighPrecisionClock { + unix_timestamp_millis: block.timestamp_millis, + }); if let Ok(hashes) = cache.get_slot_hashes() { let mut hashes = SlotHashes::new(hashes.slot_hashes()); diff --git a/magicblock-processor/src/scheduler/mod.rs b/magicblock-processor/src/scheduler/mod.rs index 8dc5ab2c5..3df7cb2ee 100644 --- a/magicblock-processor/src/scheduler/mod.rs +++ b/magicblock-processor/src/scheduler/mod.rs @@ -66,6 +66,7 @@ use magicblock_core::{ }; use magicblock_ledger::{LatestBlock, LatestBlockInner, Ledger}; use magicblock_metrics::metrics; +use magicblock_program::sysvar::{HighPrecisionClock, HIGH_PRECISION_CLOCK_ID}; use solana_account::{from_account, to_account}; use solana_program::{clock::Clock, hash::Hash, slot_hashes::SlotHashes}; use solana_program_runtime::loaded_programs::ProgramCache; @@ -391,8 +392,11 @@ impl TransactionScheduler { .await .map_err(|_| "scheduler semaphore closed".to_string())?; - let block = - LatestBlockInner::new(block.slot, block.hash, block.timestamp); + let block = LatestBlockInner::new_with_millis( + block.slot, + block.hash, + block.timestamp_millis, + ); self.verify_block_as_replica(&block); self.ledger .write_block(block.clone()) @@ -578,17 +582,20 @@ impl TransactionScheduler { // NOTE: // As we have a single node network, we have no // option but to use the time from host machine - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - // NOTE: since we can tick very frequently, a lot - // of blocks might have identical timestamps - .as_secs() as i64; - let block = LatestBlockInner::new(self.slot, blockhash, timestamp); + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + // Sampled at millisecond accuracy: this is the value persisted in the + // ledger and exposed via the HighPrecisionClock sysvar, so live + // execution and a restart/replay observe exactly the same timestamp. + let timestamp_millis = now.as_millis() as i64; + let block = LatestBlockInner::new_with_millis( + self.slot, + blockhash, + timestamp_millis, + ); let msg = Message::Block(replication::Block { slot: block.slot, hash: block.blockhash, - timestamp: block.clock.unix_timestamp, + timestamp_millis: block.timestamp_millis, }); self.send_replication(msg).await; block @@ -639,6 +646,7 @@ impl TransactionScheduler { self.update_program_cache(block.slot); self.update_clock_sysvar(&block.clock); + self.update_high_precision_clock_sysvar(block); self.update_slot_hashes_sysvar(block.slot, &block.blockhash); } @@ -659,6 +667,21 @@ impl TransactionScheduler { } } + /// Updates the HighPrecisionClock sysvar account. + fn update_high_precision_clock_sysvar(&self, block: &LatestBlockInner) { + if let Some(mut account) = + self.accountsdb.get_account(&HIGH_PRECISION_CLOCK_ID) + { + let high_precision_clock = HighPrecisionClock { + unix_timestamp_millis: block.timestamp_millis, + }; + let _ = account.serialize_data(&high_precision_clock); + let _ = self + .accountsdb + .insert_account(&HIGH_PRECISION_CLOCK_ID, &account); + } + } + /// Updates the SlotHashes sysvar account with the new slot/blockhash pair. fn update_slot_hashes_sysvar(&self, slot: Slot, blockhash: &Hash) { if let Some(mut acc) = self.accountsdb.get_account(&slot_hashes::ID) { diff --git a/magicblock-processor/src/scheduler/state.rs b/magicblock-processor/src/scheduler/state.rs index f9c202b03..1abab51dd 100644 --- a/magicblock-processor/src/scheduler/state.rs +++ b/magicblock-processor/src/scheduler/state.rs @@ -19,6 +19,7 @@ use magicblock_core::link::{ }, }; use magicblock_ledger::Ledger; +use magicblock_program::sysvar::{HighPrecisionClock, HIGH_PRECISION_CLOCK_ID}; use serde::Serialize; use solana_account::AccountSharedData; use solana_feature_set::FeatureSet; @@ -92,6 +93,10 @@ impl TransactionSchedulerState { // Mutable sysvars (updated on each slot transition) self.ensure_sysvar(&sysvar::clock::ID, &block.clock); + let high_precision_clock = HighPrecisionClock { + unix_timestamp_millis: block.timestamp_millis, + }; + self.ensure_sysvar(&HIGH_PRECISION_CLOCK_ID, &high_precision_clock); let slot_hashes = SlotHashes::new(&[(block.slot, block.blockhash); MAX_ENTRIES]); self.ensure_sysvar(&sysvar::slot_hashes::ID, &slot_hashes); diff --git a/magicblock-processor/tests/high_precision_clock.rs b/magicblock-processor/tests/high_precision_clock.rs new file mode 100644 index 000000000..0353d7f60 --- /dev/null +++ b/magicblock-processor/tests/high_precision_clock.rs @@ -0,0 +1,56 @@ +use std::time::Duration; + +use magicblock_magic_program_api::sysvar::{ + HighPrecisionClock, HIGH_PRECISION_CLOCK_ID, +}; +use solana_account::ReadableAccount; +use solana_program::clock::Clock; +use solana_sdk_ids::sysvar::clock; +use test_kit::ExecutionTestEnv; + +fn read_clock(env: &ExecutionTestEnv) -> Clock { + let account = env.get_account(clock::ID); + bincode::deserialize(account.data()) + .expect("failed to deserialize Clock sysvar") +} + +fn read_high_precision_clock(env: &ExecutionTestEnv) -> HighPrecisionClock { + let account = env.get_account(HIGH_PRECISION_CLOCK_ID); + bincode::deserialize(account.data()) + .expect("failed to deserialize HighPrecisionClock sysvar") +} + +#[tokio::test] +async fn test_high_precision_clock_tracks_clock() { + let env = ExecutionTestEnv::new(); + + // Let the primary scheduler produce a few blocks so the sysvar is updated + // from real wall-clock time rather than only the genesis value. + tokio::time::sleep(Duration::from_millis(200)).await; + + let mut hpc = read_high_precision_clock(&env); + // A real timestamp must have been sampled from wall-clock time. + assert!( + hpc.unix_timestamp_millis > 0, + "unix_timestamp_millis not set: {}", + hpc.unix_timestamp_millis + ); + + // Read a consistent (same-block) snapshot of both sysvars. A slot boundary + // may land between the two reads, so retry until they agree. The + // millisecond timestamp divided down to whole seconds must equal Clock. + let mut consistent = false; + for _ in 0..50 { + let clock = read_clock(&env); + hpc = read_high_precision_clock(&env); + if hpc.unix_timestamp_millis.div_euclid(1000) == clock.unix_timestamp { + consistent = true; + break; + } + tokio::time::sleep(Duration::from_millis(5)).await; + } + assert!( + consistent, + "HighPrecisionClock timestamp never matched Clock.unix_timestamp" + ); +} diff --git a/magicblock-processor/tests/replica_ordering.rs b/magicblock-processor/tests/replica_ordering.rs index de72f249d..a8b93050b 100644 --- a/magicblock-processor/tests/replica_ordering.rs +++ b/magicblock-processor/tests/replica_ordering.rs @@ -220,7 +220,7 @@ async fn test_replay_block_waits_for_queued_transactions() { .replay_block(Block { slot, hash, - timestamp: slot as i64, + timestamp_millis: slot as i64 * 1000, }) .await .expect("failed to apply replayed block"); @@ -250,7 +250,7 @@ async fn test_replica_replayed_superblock_takes_snapshot_without_publishing() { .replay_block(Block { slot, hash, - timestamp: slot as i64, + timestamp_millis: slot as i64 * 1000, }) .await .expect("failed to apply replayed superblock boundary"); diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 04305a12b..5caae099f 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3965,6 +3965,8 @@ dependencies = [ "serde", "solana-program 3.0.0", "solana-signature", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -6442,7 +6444,7 @@ dependencies = [ [[package]] name = "solana-account" version = "3.4.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "bincode", "qualifier_attr", @@ -8022,7 +8024,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "base64 0.22.1", "bincode", @@ -8706,7 +8708,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "ahash 0.8.12", "log", @@ -9022,7 +9024,7 @@ dependencies = [ [[package]] name = "solana-transaction-context" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=190146af2ac0890848b50e8fc9d6c926c8205b5e#190146af2ac0890848b50e8fc9d6c926c8205b5e" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "bincode", "qualifier_attr", diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index 05da5a293..3fac22868 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -80,7 +80,8 @@ schedulecommit-client = { path = "schedulecommit/client" } serde = "1.0.217" serial_test = "3.2.0" shlex = "1.3.0" -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } +# TODO: Update to the merged version when ready +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } solana-address-lookup-table-interface = "3.0" solana-commitment-config = "3.1.1" solana-compute-budget-interface = "3.0" @@ -121,9 +122,13 @@ url = "2.5.0" solana-storage-proto = { path = "../storage-proto" } # same reason as above rocksdb = { git = "https://github.com/magicblock-labs/rust-rocksdb.git", rev = "6d975197" } -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } -solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } -solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "190146af2ac0890848b50e8fc9d6c926c8205b5e" } +# TODO: Update to the merged version when ready +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } +# TODO: Update to the merged version when ready +solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } +# TODO: Update to the merged version when ready +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } +# TODO: Update to the merged version when ready +solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } # Fix libsodium for ARM builds - upstream crates.io version breaks Linux ARM binaries libsodium-rs = { git = "https://github.com/jedisct1/libsodium-rs.git", rev = "0397a6c5785233f9f2ac91f3eedc3cceb74e0060" } diff --git a/test-integration/programs/flexi-counter/src/instruction.rs b/test-integration/programs/flexi-counter/src/instruction.rs index 795458892..235223521 100644 --- a/test-integration/programs/flexi-counter/src/instruction.rs +++ b/test-integration/programs/flexi-counter/src/instruction.rs @@ -267,6 +267,18 @@ pub enum FlexiCounterInstruction { num_commit: u8, num_commit_finalize: u8, }, + + /// Reads the MagicBlock `HighPrecisionClock` sysvar (from the runtime cache + /// via `HighPrecisionClock::get()`, no account passed) and records the + /// observed value into the counter PDA so it can be verified to survive + /// ledger replay deterministically: from `unix_timestamp_millis`, `count` + /// receives the sub-second component in milliseconds (`[0, 1000)`) and + /// `updates` receives the whole seconds. + /// + /// Accounts: + /// 0. `[signer]` The payer that created the account. + /// 1. `[write]` The counter PDA account that will be updated. + RecordHighPrecisionClock, } pub fn create_init_ix(payer: Pubkey, label: String) -> Instruction { @@ -320,6 +332,22 @@ pub fn create_add_ix(payer: Pubkey, count: u8) -> Instruction { ) } +pub fn create_record_high_precision_clock_ix(payer: Pubkey) -> Instruction { + let program_id = &crate::id(); + let (pda, _) = FlexiCounter::pda(&payer); + // No sysvar account is passed: the program reads the HighPrecisionClock + // straight from the runtime cache via `HighPrecisionClock::get()`. + let accounts = vec![ + AccountMeta::new_readonly(payer, true), + AccountMeta::new(pda, false), + ]; + Instruction::new_with_borsh( + *program_id, + &FlexiCounterInstruction::RecordHighPrecisionClock, + accounts, + ) +} + pub fn create_add_unsigned_ix(payer: Pubkey, count: u8) -> Instruction { let program_id = &crate::id(); let (pda, _) = FlexiCounter::pda(&payer); diff --git a/test-integration/programs/flexi-counter/src/processor.rs b/test-integration/programs/flexi-counter/src/processor.rs index 6e4a6b6fb..6483c1811 100644 --- a/test-integration/programs/flexi-counter/src/processor.rs +++ b/test-integration/programs/flexi-counter/src/processor.rs @@ -13,6 +13,7 @@ use ephemeral_rollups_sdk::{ }; use magicblock_magic_program_api::{ args::ScheduleTaskArgs, instruction::MagicBlockInstruction, + sysvar::HighPrecisionClock, }; use solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -148,10 +149,51 @@ pub fn process( TransferActionHandler { amount, fail } => { process_transfer_action_handler(accounts, amount, fail) } + RecordHighPrecisionClock => { + process_record_high_precision_clock(accounts) + } }?; Ok(()) } +fn process_record_high_precision_clock( + accounts: &[AccountInfo], +) -> ProgramResult { + msg!("RecordHighPrecisionClock"); + + let account_info_iter = &mut accounts.iter(); + let payer_info = next_account_info(account_info_iter)?; + let counter_pda_info = next_account_info(account_info_iter)?; + + let (counter_pda, _) = FlexiCounter::pda(payer_info.key); + assert_keys_equal(counter_pda_info.key, &counter_pda, || { + format!( + "Invalid Counter PDA {}, should be {}", + counter_pda_info.key, counter_pda + ) + })?; + + // Read the sysvar straight from the runtime cache + let high_precision_clock = HighPrecisionClock::get()?; + let millis = high_precision_clock.unix_timestamp_millis; + msg!("Observed unix_timestamp_millis={}", millis); + + // Record the observed value into the counter so the test can assert it is + // reproduced identically after a ledger replay. `count` holds the + // sub-second component in milliseconds (`[0, 1000)`) and `updates` holds the + // whole seconds, together demonstrating the millisecond-accurate timestamp. + let mut counter = + FlexiCounter::try_from_slice(&counter_pda_info.data.borrow())?; + counter.count = millis.rem_euclid(1000) as u64; + counter.updates = millis.div_euclid(1000) as u64; + + let size = counter_pda_info.data_len(); + let counter_data = to_vec(&counter)?; + counter_pda_info.data.borrow_mut()[..size].copy_from_slice(&counter_data); + + Ok(()) +} + fn process_init( program_id: &Pubkey, accounts: &[AccountInfo], diff --git a/test-integration/test-ledger-restore/tests/17_high_precision_clock.rs b/test-integration/test-ledger-restore/tests/17_high_precision_clock.rs new file mode 100644 index 000000000..7e429d29f --- /dev/null +++ b/test-integration/test-ledger-restore/tests/17_high_precision_clock.rs @@ -0,0 +1,133 @@ +use std::{path::Path, process::Child}; + +use cleanass::{assert, assert_eq}; +use integration_test_tools::{ + loaded_accounts::LoadedAccounts, tmpdir::resolve_tmp_dir, + validator::cleanup, +}; +use program_flexi_counter::{ + instruction::create_record_high_precision_clock_ix, state::FlexiCounter, +}; +use solana_sdk::{signature::Keypair, signer::Signer}; +use test_kit::init_logger; +use test_ledger_restore::{ + confirm_tx_with_payer_ephem, fetch_counter_ephem, + init_and_delegate_counter_and_payer, setup_validator_with_local_remote, + wait_for_counter_ephem_state, wait_for_ledger_persist, TMP_DIR_LEDGER, +}; +use tracing::*; + +const COUNTER: &str = "HighPrecisionClock Counter"; + +#[test] +fn test_restore_ledger_reproduces_high_precision_clock() { + init_logger!(); + let (_tmpdir, ledger_path) = resolve_tmp_dir(TMP_DIR_LEDGER); + + let (mut validator, payer, observed) = write(&ledger_path); + test_ledger_restore::kill_validator(&mut validator); + + let mut validator = read(&ledger_path, &payer, &observed); + test_ledger_restore::kill_validator(&mut validator); +} + +fn write(ledger_path: &Path) -> (Child, Keypair, FlexiCounter) { + let (_, mut validator, ctx) = setup_validator_with_local_remote( + ledger_path, + None, + true, + false, + &LoadedAccounts::with_delegation_program_test_authority(), + ); + + let (payer, _counter) = + init_and_delegate_counter_and_payer(&ctx, &mut validator, COUNTER); + + // Record the HighPrecisionClock sysvar value observed during execution. + let ix = create_record_high_precision_clock_ix(payer.pubkey()); + confirm_tx_with_payer_ephem(ix, &payer, &ctx, &mut validator); + + let observed = fetch_counter_ephem(&ctx, &payer.pubkey(), &mut validator); + debug!( + "✅ Recorded high precision clock: nanos={}, unix_timestamp={}", + observed.count, observed.updates + ); + + // Sanity: the sub-second component (stored in `count`) must be a valid + // fraction of a second, confirming a real HighPrecisionClock was read. + assert!( + observed.count < 1_000_000_000, + cleanup(&mut validator), + "nanos out of range: {}", + observed.count + ); + assert!( + observed.updates > 0, + cleanup(&mut validator), + "unix_timestamp should be set: {}", + observed.updates + ); + assert_eq!(observed.label, COUNTER.to_string(), cleanup(&mut validator)); + + let slot = wait_for_ledger_persist(&ctx, &mut validator); + debug!("✅ Ledger persisted at slot {slot}"); + + (validator, payer, observed) +} + +fn read(ledger_path: &Path, payer: &Keypair, expected: &FlexiCounter) -> Child { + let (_, mut validator, ctx) = setup_validator_with_local_remote( + ledger_path, + None, + false, + false, + &LoadedAccounts::with_delegation_program_test_authority(), + ); + + // The replayed transaction must reproduce the exact observed sysvar value. + wait_for_counter_ephem_state( + &ctx, + &mut validator, + &payer.pubkey(), + expected, + ); + + let restored = fetch_counter_ephem(&ctx, &payer.pubkey(), &mut validator); + assert_eq!(restored, *expected, cleanup(&mut validator)); + debug!( + "✅ Verified high precision clock reproduced after replay: nanos={}, unix_timestamp={}", + restored.count, restored.updates + ); + + // Record the sysvar again, now on the freshly restarted validator, to prove + // the HighPrecisionClock keeps advancing (not frozen at the replayed value). + let ix = create_record_high_precision_clock_ix(payer.pubkey()); + confirm_tx_with_payer_ephem(ix, payer, &ctx, &mut validator); + + let observed = fetch_counter_ephem(&ctx, &payer.pubkey(), &mut validator); + debug!( + "✅ Recorded high precision clock after restart: nanos={}, unix_timestamp={}", + observed.count, observed.updates + ); + + // Sanity: the sub-second component (stored in `count`) must be a valid + // fraction of a second, confirming a real HighPrecisionClock was read. + assert!( + observed.count < 1_000_000_000, + cleanup(&mut validator), + "nanos out of range: {}", + observed.count + ); + assert_eq!(observed.label, COUNTER.to_string(), cleanup(&mut validator)); + + assert!( + observed.updates > expected.updates || observed.count > expected.count, + cleanup(&mut validator), + "high precision clock should advance after restart: before={}, after={}", + expected.updates, + observed.updates + ); + debug!("✅ Verified high precision clock advances after restart"); + + validator +}