From d0f6cdb7e2966eae0345365c5d2f6db287104843 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 2 Jul 2026 15:18:12 +0200 Subject: [PATCH 1/5] feat: high precision sysvar --- Cargo.lock | 10 +- Cargo.toml | 26 ++-- magicblock-core/src/link/replication.rs | 4 + .../src/blockstore_processor/mod.rs | 13 +- .../src/database/cf_descriptors.rs | 1 + magicblock-ledger/src/database/columns.rs | 25 ++++ magicblock-ledger/src/ledger_truncator.rs | 20 ++- magicblock-ledger/src/lib.rs | 17 +++ magicblock-ledger/src/store/api.rs | 75 +++++++++- magicblock-magic-program-api/Cargo.toml | 11 +- magicblock-magic-program-api/src/lib.rs | 1 + magicblock-magic-program-api/src/sysvar.rs | 87 ++++++++++++ magicblock-processor/src/executor/mod.rs | 5 + magicblock-processor/src/scheduler/mod.rs | 44 ++++-- magicblock-processor/src/scheduler/state.rs | 6 + .../tests/high_precision_clock.rs | 56 ++++++++ .../tests/replica_ordering.rs | 2 + test-integration/Cargo.lock | 10 +- test-integration/Cargo.toml | 15 +- .../programs/flexi-counter/src/instruction.rs | 27 ++++ .../programs/flexi-counter/src/processor.rs | 43 ++++++ .../tests/17_high_precision_clock.rs | 133 ++++++++++++++++++ 22 files changed, 590 insertions(+), 41 deletions(-) create mode 100644 magicblock-magic-program-api/src/sysvar.rs create mode 100644 magicblock-processor/tests/high_precision_clock.rs create mode 100644 test-integration/test-ledger-restore/tests/17_high_precision_clock.rs diff --git a/Cargo.lock b/Cargo.lock index af6285d2f..570baf979 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" dependencies = [ "bincode", "qualifier_attr", diff --git a/Cargo.toml b/Cargo.toml index 010b6ccd0..6a80fdd6d 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 = "b275f82", features = [ +solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28", features = [ "dev-context-only-utils", ] } solana-transaction-error = { version = "3.0" } @@ -194,7 +195,9 @@ 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 = "b275f82" } +# TODO: Update to the merged version when ready + +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } solana-account-decoder = { version = "4.0" } solana-account-decoder-client-types = { version = "4.0" } solana-account-info = { version = "3.1" } @@ -246,7 +249,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 = "b275f82" } +# TODO: Update to the merged version when ready +solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } solana-pubkey = { version = "4.1" } solana-pubsub-client = { version = "4.0" } solana-rent = { version = "3.0" } @@ -270,22 +274,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 = "b275f82" +rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" [patch.crates-io] -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b275f82" } -solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b275f82" } +# TODO: Update to the merged version when ready +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +# TODO: Update to the merged version when ready +solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } solana-storage-proto = { path = "storage-proto" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b275f82" } -solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b275f82" } +# TODO: Update to the merged version when ready +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +# TODO: Update to the merged version when ready +solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } # 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..1b7f41b0b 100644 --- a/magicblock-core/src/link/replication.rs +++ b/magicblock-core/src/link/replication.rs @@ -79,6 +79,10 @@ pub struct Block { pub hash: Hash, /// Unix timestamp (seconds). pub timestamp: i64, + /// Sub-second component of the timestamp, in nanoseconds + /// (`[0, 1_000_000_000)`), preserving the precision discarded when + /// rounding down to whole seconds for `timestamp`. + pub nanos: u32, } /// 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..7e0a54abd 100644 --- a/magicblock-ledger/src/blockstore_processor/mod.rs +++ b/magicblock-ledger/src/blockstore_processor/mod.rs @@ -116,9 +116,16 @@ async fn replay_blocks( ))); }; { - let block = - LatestBlockInner::new(block.slot, block.blockhash, timestamp); - ledger.latest_block().store(block); + // Restore the sub-second precision so the HighPrecisionClock sysvar + // is reproduced exactly during replay. Absent (0) for slots written + // before high-precision time was persisted. + let latest = LatestBlockInner::new_with_nanos( + block.slot, + block.blockhash, + timestamp, + ledger.get_block_time_nanos(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/cf_descriptors.rs b/magicblock-ledger/src/database/cf_descriptors.rs index 7dc63a5bb..38b9ad6a8 100644 --- a/magicblock-ledger/src/database/cf_descriptors.rs +++ b/magicblock-ledger/src/database/cf_descriptors.rs @@ -38,6 +38,7 @@ pub fn cf_descriptors( new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), + new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), diff --git a/magicblock-ledger/src/database/columns.rs b/magicblock-ledger/src/database/columns.rs index df32ce691..637a25e2f 100644 --- a/magicblock-ledger/src/database/columns.rs +++ b/magicblock-ledger/src/database/columns.rs @@ -15,6 +15,8 @@ const ADDRESS_SIGNATURES_CF: &str = "address_signatures"; const SLOT_SIGNATURES_CF: &str = "slot_signatures"; /// Column family for Blocktime const BLOCKTIME_CF: &str = "blocktime"; +/// Column family for the sub-second (nanosecond) component of the block time +const BLOCKTIME_NANOS_CF: &str = "blocktime_nanos"; /// Column family for Blockhash const BLOCKHASH_CF: &str = "blockhash"; /// Column family for Confirmed Transaction @@ -60,6 +62,17 @@ pub struct SlotSignatures; /// * value type: [`UnixTimestamp`] pub struct Blocktime; +/// The sub-second component of the block time, in nanoseconds. +/// +/// Stored separately from [`Blocktime`] (which holds whole seconds) so that the +/// high-precision timestamp exposed via the `HighPrecisionClock` sysvar can be +/// reproduced deterministically during ledger replay. Absent for slots written +/// by older validators, in which case it is treated as `0`. +/// +/// * index type: `u64` (see [`SlotColumn`]) +/// * value type: `u32` +pub struct BlocktimeNanos; + /// The block hash column /// /// * index type: `u64` (see [`SlotColumn`]) @@ -102,6 +115,7 @@ pub fn columns() -> Vec<&'static str> { AddressSignatures::NAME, SlotSignatures::NAME, Blocktime::NAME, + BlocktimeNanos::NAME, Blockhash::NAME, Transaction::NAME, TransactionMemos::NAME, @@ -467,6 +481,17 @@ impl TypedColumn for Blocktime { type Type = solana_clock::UnixTimestamp; } +// ----------------- +// BlocktimeNanos +// ----------------- +impl SlotColumn for BlocktimeNanos {} +impl ColumnName for BlocktimeNanos { + const NAME: &'static str = BLOCKTIME_NANOS_CF; +} +impl TypedColumn for BlocktimeNanos { + type Type = u32; +} + // ----------------- // Blockhash // ----------------- diff --git a/magicblock-ledger/src/ledger_truncator.rs b/magicblock-ledger/src/ledger_truncator.rs index 471af1e28..4852189e4 100644 --- a/magicblock-ledger/src/ledger_truncator.rs +++ b/magicblock-ledger/src/ledger_truncator.rs @@ -18,8 +18,9 @@ use crate::{ database::{ columns, columns::{ - AddressSignatures, Blockhash, Blocktime, PerfSamples, - SlotSignatures, Transaction, TransactionMemos, TransactionStatus, + AddressSignatures, Blockhash, Blocktime, BlocktimeNanos, + PerfSamples, SlotSignatures, Transaction, TransactionMemos, + TransactionStatus, }, }, errors::LedgerResult, @@ -181,6 +182,14 @@ impl LedgerTrunctationWorker { }, ); + ledger.delete_range_cf::(start, end)?; + >::with_column( + ledger.as_ref(), + |column| { + column.try_decrease_entry_counter(end - start); + }, + ); + ledger.delete_range_cf::(start, end)?; >::with_column( ledger.as_ref(), @@ -345,6 +354,13 @@ impl LedgerTrunctationWorker { end, Blocktime ); + compact_cf_or_return!( + ledger, + cancellation_token, + start, + end, + BlocktimeNanos + ); compact_cf_or_return!( ledger, cancellation_token, diff --git a/magicblock-ledger/src/lib.rs b/magicblock-ledger/src/lib.rs index 2828c0fa2..730114112 100644 --- a/magicblock-ledger/src/lib.rs +++ b/magicblock-ledger/src/lib.rs @@ -15,6 +15,11 @@ pub struct LatestBlockInner { pub slot: u64, pub blockhash: Hash, pub clock: Clock, + /// Sub-second component of the block timestamp, in nanoseconds + /// (`[0, 1_000_000_000)`). Together with `clock.unix_timestamp` this forms + /// the high-precision timestamp; `clock.unix_timestamp` is that value + /// rounded down to the whole second. + pub nanos: u32, } /// Atomically updated, shared, latest block information @@ -45,8 +50,20 @@ impl LatestBlockInner { slot, blockhash, clock, + nanos: 0, } } + + pub fn new_with_nanos( + slot: u64, + blockhash: Hash, + timestamp: i64, + nanos: u32, + ) -> Self { + let mut inner = Self::new(slot, blockhash, timestamp); + inner.nanos = nanos; + inner + } } impl Default for LatestBlock { diff --git a/magicblock-ledger/src/store/api.rs b/magicblock-ledger/src/store/api.rs index 7809cbd23..759f772f3 100644 --- a/magicblock-ledger/src/store/api.rs +++ b/magicblock-ledger/src/store/api.rs @@ -57,6 +57,7 @@ pub struct Ledger { db: Arc, blocktime_cf: LedgerColumn, + blocktimenanos_cf: LedgerColumn, blockhash_cf: LedgerColumn, slot_signatures_cf: LedgerColumn, address_signatures_cf: LedgerColumn, @@ -133,6 +134,7 @@ impl Ledger { let address_signatures_cf = db.column(); let slot_signatures_cf = db.column(); let blocktime_cf = db.column(); + let blocktimenanos_cf = db.column(); let blockhash_cf = db.column(); let transaction_cf = db.column(); let transaction_memos_cf = db.column(); @@ -154,6 +156,7 @@ impl Ledger { address_signatures_cf, slot_signatures_cf, blocktime_cf, + blocktimenanos_cf, blockhash_cf, transaction_cf, transaction_memos_cf, @@ -168,7 +171,13 @@ impl Ledger { }; let (slot, blockhash) = ledger.get_max_blockhash()?; let time = ledger.get_block_time(slot)?.unwrap_or_default(); - let block = LatestBlockInner::new(slot, blockhash, time); + // Absent for ledgers written before high-precision time was persisted. + let block = LatestBlockInner::new_with_nanos( + slot, + blockhash, + time, + ledger.get_block_time_nanos(slot)?.unwrap_or_default(), + ); ledger.latest_block.store(block); ledger.initialize_lowest_cleanup_slot()?; @@ -184,6 +193,7 @@ impl Ledger { self.address_signatures_cf.submit_rocksdb_cf_metrics(); self.slot_signatures_cf.submit_rocksdb_cf_metrics(); self.blocktime_cf.submit_rocksdb_cf_metrics(); + self.blocktimenanos_cf.submit_rocksdb_cf_metrics(); self.blockhash_cf.submit_rocksdb_cf_metrics(); self.transaction_cf.submit_rocksdb_cf_metrics(); self.transaction_memos_cf.submit_rocksdb_cf_metrics(); @@ -283,6 +293,16 @@ impl Ledger { self.blocktime_cf.get(slot) } + /// Returns the sub-second (nanosecond) component of the block time for + /// `slot`, or `None` for slots written before it was persisted. + pub(crate) fn get_block_time_nanos( + &self, + slot: Slot, + ) -> LedgerResult> { + let _lock = self.check_lowest_cleanup_slot(slot)?; + self.blocktimenanos_cf.get(slot) + } + pub fn count_block_times(&self) -> LedgerResult { self.blocktime_cf.count_column_using_cache() } @@ -372,6 +392,9 @@ impl Ledger { .put(block.slot, &block.clock.unix_timestamp)?; self.blocktime_cf.try_increase_entry_counter(1); + self.blocktimenanos_cf.put(block.slot, &block.nanos)?; + self.blocktimenanos_cf.try_increase_entry_counter(1); + self.blockhash_cf.put(block.slot, &block.blockhash)?; self.blockhash_cf.try_increase_entry_counter(1); self.latest_block.store(block); @@ -1336,6 +1359,7 @@ impl Ledger { self.address_signatures_cf.handle(), self.slot_signatures_cf.handle(), self.blocktime_cf.handle(), + self.blocktimenanos_cf.handle(), self.blockhash_cf.handle(), self.transaction_cf.handle(), self.transaction_memos_cf.handle(), @@ -1407,6 +1431,7 @@ impl_has_column!(TransactionStatus, transaction_status_cf); impl_has_column!(AddressSignatures, address_signatures_cf); impl_has_column!(SlotSignatures, slot_signatures_cf); impl_has_column!(Blocktime, blocktime_cf); +impl_has_column!(BlocktimeNanos, blocktimenanos_cf); impl_has_column!(Blockhash, blockhash_cf); impl_has_column!(Transaction, transaction_cf); impl_has_column!(TransactionMemos, transaction_memos_cf); @@ -2784,4 +2809,52 @@ mod tests { let result = store.verify_transaction_signature(&real_sig).unwrap(); assert_eq!(result, None); } + + #[test] + fn test_block_time_nanos_persist_and_reload() { + init_logger!(); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + + let slot = 7; + let secs = 1_700_000_000; + let nanos = 123_456_789u32; + + { + let store = Ledger::open(ledger_path.path()).unwrap(); + let block = LatestBlockInner::new_with_nanos( + slot, + Hash::new_unique(), + secs, + nanos, + ); + store.write_block(block).unwrap(); + + assert_eq!(store.get_block_time(slot).unwrap(), Some(secs)); + assert_eq!(store.get_block_time_nanos(slot).unwrap(), Some(nanos)); + assert_eq!(store.latest_block().load().nanos, nanos); + } + + // Reopening reconstructs the sub-second precision for the tip block. + { + let store = Ledger::open(ledger_path.path()).unwrap(); + assert_eq!(store.latest_block().load().nanos, nanos); + assert_eq!(store.latest_block().load().clock.unix_timestamp, secs); + } + } + + #[test] + fn test_block_time_nanos_absent_defaults_to_zero() { + init_logger!(); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let store = Ledger::open(ledger_path.path()).unwrap(); + + // Simulate a slot written by an older validator: only the whole-second + // block time is present, with no entry in the nanos column. + let slot = 3; + store.blocktime_cf.put(slot, &42).unwrap(); + store.blockhash_cf.put(slot, &Hash::new_unique()).unwrap(); + + assert_eq!(store.get_block_time(slot).unwrap(), Some(42)); + assert_eq!(store.get_block_time_nanos(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..533191f50 --- /dev/null +++ b/magicblock-magic-program-api/src/sysvar.rs @@ -0,0 +1,87 @@ +//! 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 validator samples wall-clock time with sub-second resolution but the +/// standard `Clock` can only carry whole seconds (`unix_timestamp: i64`), so +/// that value is the high-precision timestamp rounded *down* to the second. +/// This sysvar preserves the discarded precision: `unix_timestamp` is identical +/// to `Clock::unix_timestamp` and `nanos` carries the sub-second remainder. +/// +/// The full timestamp is therefore +/// `unix_timestamp as i128 * 1_000_000_000 + nanos as i128` nanoseconds. +/// +/// 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 nanos = HighPrecisionClock::get()?.nanos; +/// ``` +#[derive( + Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, +)] +pub struct HighPrecisionClock { + /// Whole seconds since the Unix epoch. Identical to `Clock::unix_timestamp`. + pub unix_timestamp: i64, + /// Sub-second component in nanoseconds, in the range `[0, 1_000_000_000)`. + pub nanos: u32, +} + +impl HighPrecisionClock { + /// Size in bytes of the bincode-serialized sysvar + /// (`i64` timestamp + `u32` nanos). + pub const SIZE: usize = 12; +} + +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 = i64::from_le_bytes( + data[0..8] + .try_into() + .map_err(|_| ProgramError::InvalidAccountData)?, + ); + let nanos = u32::from_le_bytes( + data[8..12] + .try_into() + .map_err(|_| ProgramError::InvalidAccountData)?, + ); + Ok(Self { + unix_timestamp, + nanos, + }) + } +} + +impl SysvarSerialize for HighPrecisionClock {} diff --git a/magicblock-processor/src/executor/mod.rs b/magicblock-processor/src/executor/mod.rs index 0a2edab46..bdf45fbd1 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,10 @@ 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: block.clock.unix_timestamp, + nanos: block.nanos, + }); 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..9e9bdd8a4 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,12 @@ 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_nanos( + block.slot, + block.hash, + block.timestamp, + block.nanos, + ); self.verify_block_as_replica(&block); self.ledger .write_block(block.clone()) @@ -578,17 +583,21 @@ 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(); + // NOTE: since we can tick very frequently, a lot + // of blocks might have identical timestamps + let timestamp = now.as_secs() as i64; + // The discarded sub-second precision is preserved separately and + // exposed via the HighPrecisionClock sysvar. + let nanos = now.subsec_nanos(); + let block = LatestBlockInner::new_with_nanos( + self.slot, blockhash, timestamp, nanos, + ); let msg = Message::Block(replication::Block { slot: block.slot, hash: block.blockhash, timestamp: block.clock.unix_timestamp, + nanos: block.nanos, }); self.send_replication(msg).await; block @@ -639,6 +648,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 +669,22 @@ 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: block.clock.unix_timestamp, + nanos: block.nanos, + }; + 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..94ddbb6eb 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,11 @@ impl TransactionSchedulerState { // Mutable sysvars (updated on each slot transition) self.ensure_sysvar(&sysvar::clock::ID, &block.clock); + let high_precision_clock = HighPrecisionClock { + unix_timestamp: block.clock.unix_timestamp, + nanos: block.nanos, + }; + 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..ccc3caee7 --- /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); + // Sub-second remainder must always be a valid fraction of a second. + assert!( + hpc.nanos < 1_000_000_000, + "nanos out of range: {}", + hpc.nanos + ); + + // Read a consistent (same-block) snapshot of both sysvars. A slot boundary + // may land between the two reads, so retry until they agree. + let mut consistent = false; + for _ in 0..50 { + let clock = read_clock(&env); + hpc = read_high_precision_clock(&env); + if hpc.unix_timestamp == clock.unix_timestamp { + consistent = true; + break; + } + tokio::time::sleep(Duration::from_millis(5)).await; + } + assert!( + consistent, + "HighPrecisionClock.unix_timestamp never matched Clock.unix_timestamp" + ); + assert!(hpc.nanos < 1_000_000_000); +} diff --git a/magicblock-processor/tests/replica_ordering.rs b/magicblock-processor/tests/replica_ordering.rs index de72f249d..d07f33512 100644 --- a/magicblock-processor/tests/replica_ordering.rs +++ b/magicblock-processor/tests/replica_ordering.rs @@ -221,6 +221,7 @@ async fn test_replay_block_waits_for_queued_transactions() { slot, hash, timestamp: slot as i64, + nanos: 0, }) .await .expect("failed to apply replayed block"); @@ -251,6 +252,7 @@ async fn test_replica_replayed_superblock_takes_snapshot_without_publishing() { slot, hash, timestamp: slot as i64, + nanos: 0, }) .await .expect("failed to apply replayed superblock boundary"); diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 2d6c2aa7c..6527c4845 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" 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=b275f82#b275f825fdf2c33139b99a9ef9a6db109ef5306a" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" dependencies = [ "bincode", "qualifier_attr", diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index b27717add..da3258634 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 = "b275f82" } +# TODO: Update to the merged version when ready +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } 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 = "b275f82" } -solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b275f82" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b275f82" } -solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b275f82" } +# TODO: Update to the merged version when ready +solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +# TODO: Update to the merged version when ready +solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +# TODO: Update to the merged version when ready +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +# TODO: Update to the merged version when ready +solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } # 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..0c0a90868 100644 --- a/test-integration/programs/flexi-counter/src/instruction.rs +++ b/test-integration/programs/flexi-counter/src/instruction.rs @@ -267,6 +267,17 @@ 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: `count` receives the sub-second `nanos` + /// and `updates` receives the whole-second `unix_timestamp`. + /// + /// 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 +331,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..c0bed0081 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,52 @@ 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()?; + msg!( + "Observed unix_timestamp={} nanos={}", + high_precision_clock.unix_timestamp, + high_precision_clock.nanos + ); + + // Record the observed value into the counter so the test can assert it is + // reproduced identically after a ledger replay. + let mut counter = + FlexiCounter::try_from_slice(&counter_pda_info.data.borrow())?; + counter.count = high_precision_clock.nanos as u64; + counter.updates = high_precision_clock.unix_timestamp 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..e124c9955 --- /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 +} From 85eb44c9207b52a1b890cb2fa06f6179c48dbda0 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 2 Jul 2026 15:36:26 +0200 Subject: [PATCH 2/5] style: lint --- .../test-ledger-restore/tests/17_high_precision_clock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index e124c9955..7e429d29f 100644 --- a/test-integration/test-ledger-restore/tests/17_high_precision_clock.rs +++ b/test-integration/test-ledger-restore/tests/17_high_precision_clock.rs @@ -102,7 +102,7 @@ fn read(ledger_path: &Path, payer: &Keypair, expected: &FlexiCounter) -> Child { // 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); + confirm_tx_with_payer_ephem(ix, payer, &ctx, &mut validator); let observed = fetch_counter_ephem(&ctx, &payer.pubkey(), &mut validator); debug!( From f36170be5b23374b505273ab79e244b719f938f0 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 7 Jul 2026 12:30:15 +0200 Subject: [PATCH 3/5] feat: milliseconds clock --- magicblock-core/src/link/replication.rs | 10 ++- .../src/blockstore_processor/mod.rs | 16 ++--- .../src/database/cf_descriptors.rs | 1 - magicblock-ledger/src/database/columns.rs | 33 ++------- magicblock-ledger/src/ledger_truncator.rs | 20 +----- magicblock-ledger/src/lib.rs | 33 ++++----- magicblock-ledger/src/store/api.rs | 71 +++++++++---------- magicblock-magic-program-api/src/sysvar.rs | 39 ++++------ magicblock-processor/src/executor/mod.rs | 3 +- magicblock-processor/src/scheduler/mod.rs | 27 ++++--- magicblock-processor/src/scheduler/state.rs | 3 +- .../tests/high_precision_clock.rs | 16 ++--- .../tests/replica_ordering.rs | 6 +- .../programs/flexi-counter/src/instruction.rs | 5 +- .../programs/flexi-counter/src/processor.rs | 15 ++-- 15 files changed, 115 insertions(+), 183 deletions(-) diff --git a/magicblock-core/src/link/replication.rs b/magicblock-core/src/link/replication.rs index 1b7f41b0b..41d3fc948 100644 --- a/magicblock-core/src/link/replication.rs +++ b/magicblock-core/src/link/replication.rs @@ -77,12 +77,10 @@ pub struct Block { pub slot: Slot, /// Blockhash for this slot. pub hash: Hash, - /// Unix timestamp (seconds). - pub timestamp: i64, - /// Sub-second component of the timestamp, in nanoseconds - /// (`[0, 1_000_000_000)`), preserving the precision discarded when - /// rounding down to whole seconds for `timestamp`. - pub nanos: u32, + /// 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 7e0a54abd..ec2c807e7 100644 --- a/magicblock-ledger/src/blockstore_processor/mod.rs +++ b/magicblock-ledger/src/blockstore_processor/mod.rs @@ -110,20 +110,20 @@ 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:?}", ))); - }; + } { - // Restore the sub-second precision so the HighPrecisionClock sysvar - // is reproduced exactly during replay. Absent (0) for slots written - // before high-precision time was persisted. - let latest = LatestBlockInner::new_with_nanos( + // Restore the millisecond precision so the HighPrecisionClock + // sysvar is reproduced exactly during replay. + let latest = LatestBlockInner::new_with_millis( block.slot, block.blockhash, - timestamp, - ledger.get_block_time_nanos(block.slot)?.unwrap_or_default(), + ledger + .get_block_time_millis(block.slot)? + .unwrap_or_default(), ); ledger.latest_block().store(latest); } diff --git a/magicblock-ledger/src/database/cf_descriptors.rs b/magicblock-ledger/src/database/cf_descriptors.rs index 38b9ad6a8..7dc63a5bb 100644 --- a/magicblock-ledger/src/database/cf_descriptors.rs +++ b/magicblock-ledger/src/database/cf_descriptors.rs @@ -38,7 +38,6 @@ pub fn cf_descriptors( new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), - new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), diff --git a/magicblock-ledger/src/database/columns.rs b/magicblock-ledger/src/database/columns.rs index 637a25e2f..f9cb52599 100644 --- a/magicblock-ledger/src/database/columns.rs +++ b/magicblock-ledger/src/database/columns.rs @@ -15,8 +15,6 @@ const ADDRESS_SIGNATURES_CF: &str = "address_signatures"; const SLOT_SIGNATURES_CF: &str = "slot_signatures"; /// Column family for Blocktime const BLOCKTIME_CF: &str = "blocktime"; -/// Column family for the sub-second (nanosecond) component of the block time -const BLOCKTIME_NANOS_CF: &str = "blocktime_nanos"; /// Column family for Blockhash const BLOCKHASH_CF: &str = "blockhash"; /// Column family for Confirmed Transaction @@ -58,20 +56,15 @@ pub struct SlotSignatures; /// The block time column /// -/// * index type: `u64` (see [`SlotColumn`]) -/// * value type: [`UnixTimestamp`] -pub struct Blocktime; - -/// The sub-second component of the block time, in nanoseconds. -/// -/// Stored separately from [`Blocktime`] (which holds whole seconds) so that the -/// high-precision timestamp exposed via the `HighPrecisionClock` sysvar can be -/// reproduced deterministically during ledger replay. Absent for slots written -/// by older validators, in which case it is treated as `0`. +/// 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: `u32` -pub struct BlocktimeNanos; +/// * value type: [`UnixTimestamp`] (milliseconds, despite the type name) +pub struct Blocktime; /// The block hash column /// @@ -115,7 +108,6 @@ pub fn columns() -> Vec<&'static str> { AddressSignatures::NAME, SlotSignatures::NAME, Blocktime::NAME, - BlocktimeNanos::NAME, Blockhash::NAME, Transaction::NAME, TransactionMemos::NAME, @@ -481,17 +473,6 @@ impl TypedColumn for Blocktime { type Type = solana_clock::UnixTimestamp; } -// ----------------- -// BlocktimeNanos -// ----------------- -impl SlotColumn for BlocktimeNanos {} -impl ColumnName for BlocktimeNanos { - const NAME: &'static str = BLOCKTIME_NANOS_CF; -} -impl TypedColumn for BlocktimeNanos { - type Type = u32; -} - // ----------------- // Blockhash // ----------------- diff --git a/magicblock-ledger/src/ledger_truncator.rs b/magicblock-ledger/src/ledger_truncator.rs index 4852189e4..471af1e28 100644 --- a/magicblock-ledger/src/ledger_truncator.rs +++ b/magicblock-ledger/src/ledger_truncator.rs @@ -18,9 +18,8 @@ use crate::{ database::{ columns, columns::{ - AddressSignatures, Blockhash, Blocktime, BlocktimeNanos, - PerfSamples, SlotSignatures, Transaction, TransactionMemos, - TransactionStatus, + AddressSignatures, Blockhash, Blocktime, PerfSamples, + SlotSignatures, Transaction, TransactionMemos, TransactionStatus, }, }, errors::LedgerResult, @@ -182,14 +181,6 @@ impl LedgerTrunctationWorker { }, ); - ledger.delete_range_cf::(start, end)?; - >::with_column( - ledger.as_ref(), - |column| { - column.try_decrease_entry_counter(end - start); - }, - ); - ledger.delete_range_cf::(start, end)?; >::with_column( ledger.as_ref(), @@ -354,13 +345,6 @@ impl LedgerTrunctationWorker { end, Blocktime ); - compact_cf_or_return!( - ledger, - cancellation_token, - start, - end, - BlocktimeNanos - ); compact_cf_or_return!( ledger, cancellation_token, diff --git a/magicblock-ledger/src/lib.rs b/magicblock-ledger/src/lib.rs index 730114112..3c39a5dfa 100644 --- a/magicblock-ledger/src/lib.rs +++ b/magicblock-ledger/src/lib.rs @@ -15,11 +15,7 @@ pub struct LatestBlockInner { pub slot: u64, pub blockhash: Hash, pub clock: Clock, - /// Sub-second component of the block timestamp, in nanoseconds - /// (`[0, 1_000_000_000)`). Together with `clock.unix_timestamp` this forms - /// the high-precision timestamp; `clock.unix_timestamp` is that value - /// rounded down to the whole second. - pub nanos: u32, + pub timestamp_millis: i64, } /// Atomically updated, shared, latest block information @@ -40,30 +36,31 @@ 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, - nanos: 0, + timestamp_millis, } } - - pub fn new_with_nanos( - slot: u64, - blockhash: Hash, - timestamp: i64, - nanos: u32, - ) -> Self { - let mut inner = Self::new(slot, blockhash, timestamp); - inner.nanos = nanos; - inner - } } impl Default for LatestBlock { diff --git a/magicblock-ledger/src/store/api.rs b/magicblock-ledger/src/store/api.rs index 759f772f3..e831becb5 100644 --- a/magicblock-ledger/src/store/api.rs +++ b/magicblock-ledger/src/store/api.rs @@ -57,7 +57,6 @@ pub struct Ledger { db: Arc, blocktime_cf: LedgerColumn, - blocktimenanos_cf: LedgerColumn, blockhash_cf: LedgerColumn, slot_signatures_cf: LedgerColumn, address_signatures_cf: LedgerColumn, @@ -134,7 +133,6 @@ impl Ledger { let address_signatures_cf = db.column(); let slot_signatures_cf = db.column(); let blocktime_cf = db.column(); - let blocktimenanos_cf = db.column(); let blockhash_cf = db.column(); let transaction_cf = db.column(); let transaction_memos_cf = db.column(); @@ -156,7 +154,6 @@ impl Ledger { address_signatures_cf, slot_signatures_cf, blocktime_cf, - blocktimenanos_cf, blockhash_cf, transaction_cf, transaction_memos_cf, @@ -170,13 +167,12 @@ impl Ledger { latest_block, }; let (slot, blockhash) = ledger.get_max_blockhash()?; - let time = ledger.get_block_time(slot)?.unwrap_or_default(); - // Absent for ledgers written before high-precision time was persisted. - let block = LatestBlockInner::new_with_nanos( + let timestamp_millis = + ledger.get_block_time_millis(slot)?.unwrap_or_default(); + let block = LatestBlockInner::new_with_millis( slot, blockhash, - time, - ledger.get_block_time_nanos(slot)?.unwrap_or_default(), + timestamp_millis, ); ledger.latest_block.store(block); ledger.initialize_lowest_cleanup_slot()?; @@ -193,7 +189,6 @@ impl Ledger { self.address_signatures_cf.submit_rocksdb_cf_metrics(); self.slot_signatures_cf.submit_rocksdb_cf_metrics(); self.blocktime_cf.submit_rocksdb_cf_metrics(); - self.blocktimenanos_cf.submit_rocksdb_cf_metrics(); self.blockhash_cf.submit_rocksdb_cf_metrics(); self.transaction_cf.submit_rocksdb_cf_metrics(); self.transaction_memos_cf.submit_rocksdb_cf_metrics(); @@ -290,17 +285,20 @@ impl Ledger { slot: Slot, ) -> LedgerResult> { let _lock = self.check_lowest_cleanup_slot(slot)?; - self.blocktime_cf.get(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))) } - /// Returns the sub-second (nanosecond) component of the block time for - /// `slot`, or `None` for slots written before it was persisted. - pub(crate) fn get_block_time_nanos( + pub(crate) fn get_block_time_millis( &self, slot: Slot, - ) -> LedgerResult> { + ) -> LedgerResult> { let _lock = self.check_lowest_cleanup_slot(slot)?; - self.blocktimenanos_cf.get(slot) + self.blocktime_cf.get(slot) } pub fn count_block_times(&self) -> LedgerResult { @@ -388,13 +386,9 @@ 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.blocktimenanos_cf.put(block.slot, &block.nanos)?; - self.blocktimenanos_cf.try_increase_entry_counter(1); - self.blockhash_cf.put(block.slot, &block.blockhash)?; self.blockhash_cf.try_increase_entry_counter(1); self.latest_block.store(block); @@ -1359,7 +1353,6 @@ impl Ledger { self.address_signatures_cf.handle(), self.slot_signatures_cf.handle(), self.blocktime_cf.handle(), - self.blocktimenanos_cf.handle(), self.blockhash_cf.handle(), self.transaction_cf.handle(), self.transaction_memos_cf.handle(), @@ -1431,7 +1424,6 @@ impl_has_column!(TransactionStatus, transaction_status_cf); impl_has_column!(AddressSignatures, address_signatures_cf); impl_has_column!(SlotSignatures, slot_signatures_cf); impl_has_column!(Blocktime, blocktime_cf); -impl_has_column!(BlocktimeNanos, blocktimenanos_cf); impl_has_column!(Blockhash, blockhash_cf); impl_has_column!(Transaction, transaction_cf); impl_has_column!(TransactionMemos, transaction_memos_cf); @@ -2811,50 +2803,51 @@ mod tests { } #[test] - fn test_block_time_nanos_persist_and_reload() { + 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 nanos = 123_456_789u32; + let millis = secs * 1000 + 123; { let store = Ledger::open(ledger_path.path()).unwrap(); - let block = LatestBlockInner::new_with_nanos( + let block = LatestBlockInner::new_with_millis( slot, Hash::new_unique(), - secs, - nanos, + 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.get_block_time_nanos(slot).unwrap(), Some(nanos)); - assert_eq!(store.latest_block().load().nanos, nanos); + assert_eq!(store.latest_block().load().timestamp_millis, millis); + assert_eq!(store.latest_block().load().clock.unix_timestamp, secs); } - // Reopening reconstructs the sub-second precision for the tip block. + // Reopening reconstructs the millisecond precision for the tip block. { let store = Ledger::open(ledger_path.path()).unwrap(); - assert_eq!(store.latest_block().load().nanos, nanos); + assert_eq!(store.latest_block().load().timestamp_millis, millis); assert_eq!(store.latest_block().load().clock.unix_timestamp, secs); } } #[test] - fn test_block_time_nanos_absent_defaults_to_zero() { + 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(); - // Simulate a slot written by an older validator: only the whole-second - // block time is present, with no entry in the nanos column. + // A slot with no recorded block time reports no time in either form. let slot = 3; - store.blocktime_cf.put(slot, &42).unwrap(); - store.blockhash_cf.put(slot, &Hash::new_unique()).unwrap(); - - assert_eq!(store.get_block_time(slot).unwrap(), Some(42)); - assert_eq!(store.get_block_time_nanos(slot).unwrap(), None); + 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/src/sysvar.rs b/magicblock-magic-program-api/src/sysvar.rs index 533191f50..b639d8fcf 100644 --- a/magicblock-magic-program-api/src/sysvar.rs +++ b/magicblock-magic-program-api/src/sysvar.rs @@ -17,14 +17,11 @@ pub const HIGH_PRECISION_CLOCK_ID: Pubkey = /// A high-precision companion to the [`Clock`](solana_program::clock::Clock) /// sysvar. /// -/// The validator samples wall-clock time with sub-second resolution but the -/// standard `Clock` can only carry whole seconds (`unix_timestamp: i64`), so -/// that value is the high-precision timestamp rounded *down* to the second. -/// This sysvar preserves the discarded precision: `unix_timestamp` is identical -/// to `Clock::unix_timestamp` and `nanos` carries the sub-second remainder. -/// -/// The full timestamp is therefore -/// `unix_timestamp as i128 * 1_000_000_000 + nanos as i128` nanoseconds. +/// 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: @@ -33,22 +30,19 @@ pub const HIGH_PRECISION_CLOCK_ID: Pubkey = /// use magicblock_magic_program_api::sysvar::HighPrecisionClock; /// use solana_program::sysvar::Sysvar; /// -/// let nanos = HighPrecisionClock::get()?.nanos; +/// let millis = HighPrecisionClock::get()?.unix_timestamp_millis; /// ``` #[derive( Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, )] pub struct HighPrecisionClock { - /// Whole seconds since the Unix epoch. Identical to `Clock::unix_timestamp`. - pub unix_timestamp: i64, - /// Sub-second component in nanoseconds, in the range `[0, 1_000_000_000)`. - pub nanos: u32, + /// Milliseconds since the Unix epoch. + pub unix_timestamp_millis: i64, } impl HighPrecisionClock { - /// Size in bytes of the bincode-serialized sysvar - /// (`i64` timestamp + `u32` nanos). - pub const SIZE: usize = 12; + /// Size in bytes of the bincode-serialized sysvar (a single `i64`). + pub const SIZE: usize = 8; } impl SysvarId for HighPrecisionClock { @@ -67,19 +61,12 @@ impl Sysvar for HighPrecisionClock { 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 = i64::from_le_bytes( - data[0..8] - .try_into() - .map_err(|_| ProgramError::InvalidAccountData)?, - ); - let nanos = u32::from_le_bytes( - data[8..12] - .try_into() + let unix_timestamp_millis = i64::from_le_bytes( + data.try_into() .map_err(|_| ProgramError::InvalidAccountData)?, ); Ok(Self { - unix_timestamp, - nanos, + unix_timestamp_millis, }) } } diff --git a/magicblock-processor/src/executor/mod.rs b/magicblock-processor/src/executor/mod.rs index bdf45fbd1..97016cb6c 100644 --- a/magicblock-processor/src/executor/mod.rs +++ b/magicblock-processor/src/executor/mod.rs @@ -254,8 +254,7 @@ impl TransactionExecutor { 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: block.clock.unix_timestamp, - nanos: block.nanos, + unix_timestamp_millis: block.timestamp_millis, }); if let Ok(hashes) = cache.get_slot_hashes() { diff --git a/magicblock-processor/src/scheduler/mod.rs b/magicblock-processor/src/scheduler/mod.rs index 9e9bdd8a4..3df7cb2ee 100644 --- a/magicblock-processor/src/scheduler/mod.rs +++ b/magicblock-processor/src/scheduler/mod.rs @@ -392,11 +392,10 @@ impl TransactionScheduler { .await .map_err(|_| "scheduler semaphore closed".to_string())?; - let block = LatestBlockInner::new_with_nanos( + let block = LatestBlockInner::new_with_millis( block.slot, block.hash, - block.timestamp, - block.nanos, + block.timestamp_millis, ); self.verify_block_as_replica(&block); self.ledger @@ -584,20 +583,19 @@ impl TransactionScheduler { // As we have a single node network, we have no // option but to use the time from host machine let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - // NOTE: since we can tick very frequently, a lot - // of blocks might have identical timestamps - let timestamp = now.as_secs() as i64; - // The discarded sub-second precision is preserved separately and - // exposed via the HighPrecisionClock sysvar. - let nanos = now.subsec_nanos(); - let block = LatestBlockInner::new_with_nanos( - self.slot, blockhash, timestamp, nanos, + // 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, - nanos: block.nanos, + timestamp_millis: block.timestamp_millis, }); self.send_replication(msg).await; block @@ -675,8 +673,7 @@ impl TransactionScheduler { self.accountsdb.get_account(&HIGH_PRECISION_CLOCK_ID) { let high_precision_clock = HighPrecisionClock { - unix_timestamp: block.clock.unix_timestamp, - nanos: block.nanos, + unix_timestamp_millis: block.timestamp_millis, }; let _ = account.serialize_data(&high_precision_clock); let _ = self diff --git a/magicblock-processor/src/scheduler/state.rs b/magicblock-processor/src/scheduler/state.rs index 94ddbb6eb..1abab51dd 100644 --- a/magicblock-processor/src/scheduler/state.rs +++ b/magicblock-processor/src/scheduler/state.rs @@ -94,8 +94,7 @@ impl TransactionSchedulerState { // Mutable sysvars (updated on each slot transition) self.ensure_sysvar(&sysvar::clock::ID, &block.clock); let high_precision_clock = HighPrecisionClock { - unix_timestamp: block.clock.unix_timestamp, - nanos: block.nanos, + unix_timestamp_millis: block.timestamp_millis, }; self.ensure_sysvar(&HIGH_PRECISION_CLOCK_ID, &high_precision_clock); let slot_hashes = diff --git a/magicblock-processor/tests/high_precision_clock.rs b/magicblock-processor/tests/high_precision_clock.rs index ccc3caee7..0353d7f60 100644 --- a/magicblock-processor/tests/high_precision_clock.rs +++ b/magicblock-processor/tests/high_precision_clock.rs @@ -29,20 +29,21 @@ async fn test_high_precision_clock_tracks_clock() { tokio::time::sleep(Duration::from_millis(200)).await; let mut hpc = read_high_precision_clock(&env); - // Sub-second remainder must always be a valid fraction of a second. + // A real timestamp must have been sampled from wall-clock time. assert!( - hpc.nanos < 1_000_000_000, - "nanos out of range: {}", - hpc.nanos + 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. + // 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 == clock.unix_timestamp { + if hpc.unix_timestamp_millis.div_euclid(1000) == clock.unix_timestamp { consistent = true; break; } @@ -50,7 +51,6 @@ async fn test_high_precision_clock_tracks_clock() { } assert!( consistent, - "HighPrecisionClock.unix_timestamp never matched Clock.unix_timestamp" + "HighPrecisionClock timestamp never matched Clock.unix_timestamp" ); - assert!(hpc.nanos < 1_000_000_000); } diff --git a/magicblock-processor/tests/replica_ordering.rs b/magicblock-processor/tests/replica_ordering.rs index d07f33512..a8b93050b 100644 --- a/magicblock-processor/tests/replica_ordering.rs +++ b/magicblock-processor/tests/replica_ordering.rs @@ -220,8 +220,7 @@ async fn test_replay_block_waits_for_queued_transactions() { .replay_block(Block { slot, hash, - timestamp: slot as i64, - nanos: 0, + timestamp_millis: slot as i64 * 1000, }) .await .expect("failed to apply replayed block"); @@ -251,8 +250,7 @@ async fn test_replica_replayed_superblock_takes_snapshot_without_publishing() { .replay_block(Block { slot, hash, - timestamp: slot as i64, - nanos: 0, + timestamp_millis: slot as i64 * 1000, }) .await .expect("failed to apply replayed superblock boundary"); diff --git a/test-integration/programs/flexi-counter/src/instruction.rs b/test-integration/programs/flexi-counter/src/instruction.rs index 0c0a90868..235223521 100644 --- a/test-integration/programs/flexi-counter/src/instruction.rs +++ b/test-integration/programs/flexi-counter/src/instruction.rs @@ -271,8 +271,9 @@ pub enum FlexiCounterInstruction { /// 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: `count` receives the sub-second `nanos` - /// and `updates` receives the whole-second `unix_timestamp`. + /// 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. diff --git a/test-integration/programs/flexi-counter/src/processor.rs b/test-integration/programs/flexi-counter/src/processor.rs index c0bed0081..6483c1811 100644 --- a/test-integration/programs/flexi-counter/src/processor.rs +++ b/test-integration/programs/flexi-counter/src/processor.rs @@ -175,18 +175,17 @@ fn process_record_high_precision_clock( // Read the sysvar straight from the runtime cache let high_precision_clock = HighPrecisionClock::get()?; - msg!( - "Observed unix_timestamp={} nanos={}", - high_precision_clock.unix_timestamp, - high_precision_clock.nanos - ); + 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. + // 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 = high_precision_clock.nanos as u64; - counter.updates = high_precision_clock.unix_timestamp as u64; + 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)?; From ef4f235d6510298bda1ec7697d40cf52b2e16615 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 7 Jul 2026 12:31:37 +0200 Subject: [PATCH 4/5] style: lint --- magicblock-magic-program-api/src/sysvar.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/magicblock-magic-program-api/src/sysvar.rs b/magicblock-magic-program-api/src/sysvar.rs index b639d8fcf..fecdb9be6 100644 --- a/magicblock-magic-program-api/src/sysvar.rs +++ b/magicblock-magic-program-api/src/sysvar.rs @@ -61,10 +61,7 @@ impl Sysvar for HighPrecisionClock { 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.try_into() - .map_err(|_| ProgramError::InvalidAccountData)?, - ); + let unix_timestamp_millis = i64::from_le_bytes(data); Ok(Self { unix_timestamp_millis, }) From 7a8463865cf669925704312f3299979699d8dbee Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 7 Jul 2026 16:17:24 +0200 Subject: [PATCH 5/5] feat: update svm --- Cargo.lock | 8 ++++---- Cargo.toml | 16 ++++++++-------- test-integration/Cargo.lock | 8 ++++---- test-integration/Cargo.toml | 10 +++++----- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 731b59a17..02ec99e95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6268,7 +6268,7 @@ dependencies = [ [[package]] name = "solana-account" version = "3.4.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "bincode", "qualifier_attr", @@ -7848,7 +7848,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "base64 0.22.1", "bincode", @@ -8544,7 +8544,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "ahash 0.8.12", "log", @@ -8860,7 +8860,7 @@ dependencies = [ [[package]] name = "solana-transaction-context" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +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 564b0fee0..1682d3c6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ prometheus = "0.13.4" # TODO: Update to the merged version when ready -solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28", 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" } @@ -196,7 +196,7 @@ serde_with = "3.16" serial_test = "3.2" sha3 = "0.10.8" # TODO: Update to the merged version when ready -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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" } @@ -249,7 +249,7 @@ solana-program-error = { version = "3.0" } solana-program-option = { version = "3.0" } solana-program-pack = { version = "3.0" } # TODO: Update to the merged version when ready -solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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" } @@ -283,18 +283,18 @@ solana-transaction = { version = "3.0" } [workspace.dependencies.solana-svm] features = ["dev-context-only-utils"] git = "https://github.com/magicblock-labs/magicblock-svm.git" -rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" +rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" [patch.crates-io] # TODO: Update to the merged version when ready -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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 = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +solana-program-runtime = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "ab02436a458bf83aade6a8b9276f6384077c78fc" } solana-storage-proto = { path = "storage-proto" } # TODO: Update to the merged version when ready -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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 = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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/test-integration/Cargo.lock b/test-integration/Cargo.lock index 8be9456e9..5caae099f 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -6444,7 +6444,7 @@ dependencies = [ [[package]] name = "solana-account" version = "3.4.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "bincode", "qualifier_attr", @@ -8024,7 +8024,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "base64 0.22.1", "bincode", @@ -8708,7 +8708,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=ab02436a458bf83aade6a8b9276f6384077c78fc#ab02436a458bf83aade6a8b9276f6384077c78fc" dependencies = [ "ahash 0.8.12", "log", @@ -9024,7 +9024,7 @@ dependencies = [ [[package]] name = "solana-transaction-context" version = "4.0.0" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bccdeeb1e773352cf0afef2087f4e752efe69a28#bccdeeb1e773352cf0afef2087f4e752efe69a28" +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 da3258634..3fac22868 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -81,7 +81,7 @@ serde = "1.0.217" serial_test = "3.2.0" shlex = "1.3.0" # TODO: Update to the merged version when ready -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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" @@ -123,12 +123,12 @@ solana-storage-proto = { path = "../storage-proto" } # same reason as above rocksdb = { git = "https://github.com/magicblock-labs/rust-rocksdb.git", rev = "6d975197" } # TODO: Update to the merged version when ready -solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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 = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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 = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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 = "bccdeeb1e773352cf0afef2087f4e752efe69a28" } +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" }