diff --git a/Cargo.toml b/Cargo.toml index d5985011..18be369a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,13 @@ [workspace] members = [ + "nucleus", + "programs/magic-root-interface", + "programs/v42-calculator-interface", "solana/account", "solana/program-runtime", "solana/svm", "solana/transaction-context", - "solana/transaction-view", + "solana/transaction-view" ] resolver = "3" @@ -20,6 +23,8 @@ version = "0.1.0" [workspace.dependencies] magic-root-interface = { path = "programs/magic-root-interface" } magic-root-program = { path = "programs/magic-root-program" } +nucleus = { path = "nucleus", package = "magicblock-engine-nucleus" } +v42-calculator-interface = { path = "programs/v42-calculator-interface", default-features = false } solana-account = { path = "solana/account" } solana-program-runtime = { path = "solana/program-runtime" } solana-svm = { path = "solana/svm" } @@ -36,7 +41,11 @@ cfg-if = "1.0.4" criterion = "0.8.2" derive_more = "2.1.1" env_logger = "0.11.8" +futures = { version = "0.3.32", default-features = false } +heed = { version = "0.22.1", default-features = false } itertools = "0.13.0" +oneshot = "0.2.1" +prometheus = { version = "0.14.0", default-features = false } qualifier_attr = "0.2.2" rand = "0.9.2" rustix = { version = "1.1.4" } @@ -48,6 +57,9 @@ snedfile = "0.1" tar = "0.4.45" tempfile = "3" thiserror = "2.0.17" +tokio = "1.52.1" +tokio-util = "0.7.18" +tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] } wincode = "0.5.1" zstd = { version = "0.13.3", default-features = false } diff --git a/nucleus/Cargo.toml b/nucleus/Cargo.toml new file mode 100644 index 00000000..aeae2e35 --- /dev/null +++ b/nucleus/Cargo.toml @@ -0,0 +1,86 @@ +[package] +name = "magicblock-engine-nucleus" + +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lib] +name = "nucleus" + +[features] +config = [ + "dep:serde", + "dep:serde_with", + "dep:solana-keypair", + "dep:solana-pubkey", + "dep:solana-signer" +] +default = [] +heed = ["dep:heed"] +ledger = ["solana-hash/copy", "solana-hash/wincode", "wincode/derive"] +metrics = ["dep:prometheus", "dep:tracing"] +notifier = ["tokio/sync"] +runtime = [ + "agave-transaction-view/agave-unstable-api", + "dep:derive_more", + "dep:oneshot", + "dep:solana-svm", + "dep:solana-transaction-error", + "ledger", + "service", + "tls", + "tokio/sync" +] +service = ["metrics", "shutdown"] +shutdown = ["dep:futures", "dep:oneshot", "dep:tokio", "dep:tokio-util", "dep:tracing"] +testkit = [ + "dep:solana-instruction", + "dep:solana-keypair", + "dep:solana-message", + "dep:solana-pubkey", + "dep:solana-signature", + "dep:solana-signer", + "dep:tempfile", + "dep:tracing-subscriber", + "dep:v42-calculator-interface", + "runtime", + "solana-transaction/wincode" +] +tls = ["dep:solana-instruction-error", "dep:solana-pubkey", "dep:wincode"] + +[dependencies] +derive_more = { workspace = true, optional = true, features = ["deref", "from"] } +futures = { workspace = true, features = ["alloc"], optional = true } +heed = { workspace = true, optional = true } +oneshot = { workspace = true, features = ["async"], optional = true } +prometheus = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"], optional = true } +serde_with = { workspace = true, optional = true } +solana-hash = { workspace = true, optional = true } +solana-instruction-error = { workspace = true, optional = true } +tempfile = { workspace = true, optional = true } +tokio = { workspace = true, features = ["macros", "signal", "time"], optional = true } +tokio-util = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"], optional = true } +wincode = { workspace = true, optional = true } + +agave-transaction-view = { workspace = true, optional = true } +solana-instruction = { workspace = true, optional = true } +solana-keypair = { workspace = true, optional = true } +solana-message = { workspace = true, optional = true } +solana-pubkey = { workspace = true, optional = true } +solana-signature = { workspace = true, optional = true } +solana-signer = { workspace = true, optional = true } +solana-svm = { workspace = true, optional = true } +solana-transaction = { workspace = true, optional = true } +solana-transaction-error = { workspace = true, optional = true } +v42-calculator-interface = { workspace = true, optional = true, features = ["builder"] } + +[lints] +workspace = true diff --git a/nucleus/README.md b/nucleus/README.md new file mode 100644 index 00000000..a463b5b6 --- /dev/null +++ b/nucleus/README.md @@ -0,0 +1,33 @@ +# `magicblock-engine-nucleus` + +Nucleus contains shared engine types that do not own storage or execution +policy. It also exposes byte-size constants and a Unix-time helper that returns +zero when the system clock predates the epoch. Its default feature set is empty. + +## Features + +- `config`: serializable authority, accountsdb, blockstore, and ledger + configuration types. Authority serialization includes the complete local + keypair; consumers must redact it before exposing serialized output. +- `heed`: LMDB transaction aliases, safe environment-bound transaction reuse + helpers, and the shared `DatabaseIndex` trait. +- `shutdown`: ordered cancellation, service handles, and termination reporting. + The pacemaker quiesces execution and terminally syncs the ledger before the + sequencer and appender tier; remaining backing services stop afterward. + Dropping the manager cancels every tier without waiting for services to stop. +- `notifier`: the one-shot, non-resetting `EventNotifier` latch. +- `ledger`: shared block-boundary metadata, including each block's locally + computed hash and parent, plus snapshot checksum/transaction seals and + blockstore positions. +- `metrics`: Prometheus metric construction, `engine_`-namespaced registration, + labels, and timers. +- `service`: the `metrics` and `shutdown` feature bundle. +- `runtime`: transaction views, execution messages, sequencer handles, and the + quiescence barrier; it also enables `ledger`, `service`, and `tls`. +- `tls`: thread-local MagicRoot authority and encoded service-message state. +- `testkit`: engine-independent fixtures, temporary directories, Legacy/V0/V1 + transaction encoding, v42 instructions, transaction views, and tracing setup + used by downstream test targets. It enables `runtime` because `signed_view` + returns the runtime transaction view. + +Keeper-specific harnesses remain in `keeper::testkit`. diff --git a/nucleus/src/config.rs b/nucleus/src/config.rs new file mode 100644 index 00000000..efc26313 --- /dev/null +++ b/nucleus/src/config.rs @@ -0,0 +1,92 @@ +//! Shared engine configuration types. + +use std::{num::NonZeroU64, path::PathBuf, sync::Arc, time::Duration}; + +use serde::{Deserialize, Serialize}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Local signing identity and optional authority override represented by a replica. +/// +/// Serialization includes the complete local keypair as a base58 string. +/// Consumers must redact the `local` field before exposing serialized output. +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Authority { + /// Signer used for locally produced messages and transactions. + #[serde(with = "keypair")] + pub local: Arc, + /// Immediate upstream identity exposed as the engine authority when set. + #[serde(default, with = "serde_with::As::>")] + pub remote: Option, +} + +impl Authority { + /// Returns the remote authority when configured, otherwise the local identity. + pub fn pubkey(&self) -> Pubkey { + self.remote.unwrap_or(self.local.pubkey()) + } +} + +impl>> From for Authority { + fn from(local: K) -> Self { + let local = local.into(); + Self { local, remote: None } + } +} + +/// Account storage and recent-load cache parameters. +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct AccountsDBParams { + /// Accounts database root directory. + pub directory: PathBuf, + /// Requested maximum number of resolved account pubkeys retained for + /// recency tracking and eviction notifications. + /// + /// The cache uses at least 256 slots, rounds larger capacities up to a + /// power of two, and may evict earlier under bucket pressure. + pub lru_capacity: usize, +} + +/// Block production timing used by the engine and keeper caches. +#[derive(Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct BlockstoreParams { + /// Expected wall-clock interval between produced slots. + pub blocktime: Duration, + /// Number of blocks included into each superblock. + pub superblock: NonZeroU64, +} + +/// Ledger storage parameters. +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct LedgerParams { + /// Ledger root directory. + pub directory: PathBuf, + /// Maximum used bytes allowed on the ledger filesystem before eviction runs. + pub size_limit: u64, +} + +mod keypair { + use super::*; + use serde::{Deserializer, Serializer, de::Error as _}; + + pub(super) fn serialize( + keypair: &Arc, + serializer: S, + ) -> Result { + serializer.serialize_str(&keypair.to_base58_string()) + } + + pub(super) fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + let encoded = String::deserialize(deserializer)?; + Keypair::try_from_base58_string(&encoded) + .map(Arc::new) + .map_err(D::Error::custom) + } +} diff --git a/nucleus/src/heed.rs b/nucleus/src/heed.rs new file mode 100644 index 00000000..fe95adcf --- /dev/null +++ b/nucleus/src/heed.rs @@ -0,0 +1,37 @@ +//! Shared heed index plumbing. + +use ::heed::{Env, Result, RoTxn, RwTxn, WithTls}; + +/// Read-only transaction using heed thread-local storage. +pub type RoTxnTls<'e> = RoTxn<'e, WithTls>; +/// Optional write transaction used by batched updates. +pub type OptRwTxn<'t, 'e> = &'t mut Option>; +/// Optional read transaction used by batched reads. +pub type OptRoTxn<'t, 'e> = &'t mut Option>; + +/// Common access for heed-backed indexes. +pub trait DatabaseIndex { + /// Returns the owning heed environment. + fn env(&self) -> &Env; + + /// Flushes the index databases to durable storage. + fn flush(&self) -> Result<()> { + self.env().force_sync() + } +} + +/// Uses the supplied write transaction or opens one against `env` on demand. +pub fn write_txn<'t, 'e>(env: &'e Env, txn: OptRwTxn<'t, 'e>) -> Result<&'t mut RwTxn<'e>> { + if let Some(txn) = txn { + return Ok(txn); + } + Ok(txn.insert(env.write_txn()?)) +} + +/// Uses the supplied read transaction or opens one against `env` on demand. +pub fn read_txn<'t, 'e>(env: &'e Env, txn: OptRoTxn<'t, 'e>) -> Result<&'t RoTxnTls<'e>> { + if let Some(txn) = txn { + return Ok(txn); + } + Ok(txn.insert(env.read_txn()?)) +} diff --git a/nucleus/src/ledger.rs b/nucleus/src/ledger.rs new file mode 100644 index 00000000..82b7ed5d --- /dev/null +++ b/nucleus/src/ledger.rs @@ -0,0 +1,51 @@ +//! Ledger block-boundary schema shared by storage-adjacent crates. + +use solana_hash::Hash; +use wincode::{SchemaRead, SchemaWrite}; + +use crate::Slot; + +/// File name of the archived accountsdb snapshot tarball inside a superblock directory. +pub const ACCOUNTSDB_SNAPSHOT_FILE: &str = "accountsdb.tar.zst"; + +/// A byte cursor into the ledger blockstore stream, used by the replication path +/// to mark how far a follower has consumed. Ordering is lexicographic over +/// `(superblock, offset)`, matching the on-disk append order across rotations. +#[derive(Clone, Copy, SchemaRead, SchemaWrite, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct BlockstorePosition { + /// Superblock whose blockstore file the offset indexes into. + pub superblock: u64, + /// Byte offset of the write cursor within that superblock's blockstore file. + pub offset: u64, +} + +/// Block boundary entry stored after all transactions in the block. +#[derive(SchemaRead, SchemaWrite, Clone, Copy, Default, PartialEq, Eq, Debug)] +pub struct Block { + /// Slot that produced the block. + pub slot: Slot, + /// Block hash for `slot`. + pub hash: Hash, + /// Block timestamp in the producer's time base. + pub time: i64, + /// Hash of the preceding block. + pub parent: Hash, +} + +impl Block { + /// Creates a block boundary whose hash-chain metadata is not yet known. + pub fn new(slot: Slot, time: i64) -> Self { + Self { slot, time, ..Default::default() } + } +} + +/// Superblock boundary entry stored at the end of the blockstore stream. +#[derive(SchemaRead, SchemaWrite, Clone, Copy, Debug, PartialEq, Eq)] +pub struct SuperblockSeal { + /// Id of the superblock this seal closes. + pub id: u64, + /// Checksum of accountsdb at the moment the superblock was sealed. + pub checksum: u64, + /// Total committed transactions represented by the sealed accountsdb snapshot. + pub transactions: u64, +} diff --git a/nucleus/src/lib.rs b/nucleus/src/lib.rs new file mode 100644 index 00000000..61b41626 --- /dev/null +++ b/nucleus/src/lib.rs @@ -0,0 +1,44 @@ +#![doc = include_str!("../README.md")] + +use std::time::{Duration, UNIX_EPOCH}; + +#[cfg(feature = "config")] +pub mod config; + +#[cfg(feature = "heed")] +pub mod heed; + +#[cfg(feature = "shutdown")] +pub mod shutdown; + +#[cfg(feature = "notifier")] +pub mod notifier; + +#[cfg(feature = "ledger")] +pub mod ledger; + +#[cfg(feature = "metrics")] +pub mod metrics; + +#[cfg(feature = "runtime")] +pub mod runtime; + +#[cfg(feature = "testkit")] +pub mod testkit; + +#[cfg(feature = "tls")] +pub mod tls; + +/// Ledger slot number. +pub type Slot = u64; +/// One kibibyte in bytes. +pub const KB: usize = 1024; +/// One mebibyte in bytes. +pub const MB: usize = 1024 * KB; +/// One gibibyte in bytes. +pub const GB: usize = 1024 * MB; + +/// Returns the duration since the Unix epoch, or zero if the clock predates it. +pub fn unix_time() -> Duration { + UNIX_EPOCH.elapsed().unwrap_or_default() +} diff --git a/nucleus/src/metrics.rs b/nucleus/src/metrics.rs new file mode 100644 index 00000000..05692189 --- /dev/null +++ b/nucleus/src/metrics.rs @@ -0,0 +1,186 @@ +//! Prometheus metric helpers shared by engine crates. + +use std::{fmt::Display, sync::OnceLock, time::Instant}; + +use prometheus::{HistogramOpts, HistogramVec, Opts, default_registry}; +pub use prometheus::{IntCounter, IntCounterVec, IntGauge, IntGaugeVec}; +use tracing::{info, warn}; + +/// Prometheus namespace shared by all engine metrics. +const NAMESPACE: &str = "engine"; + +/// Duration logger for the time elapsed between events and their total duration. +pub struct EventTimer { + sequence: &'static str, + start: Instant, + interval: Instant, +} + +impl EventTimer { + /// Initialize a new timer for the given sequence of events + pub fn new(sequence: &'static str) -> Self { + let start = Instant::now(); + let interval = start; + Self { sequence, start, interval } + } + /// Logs the supplied event and time since construction or the previous event, + /// then starts a new interval. + pub fn record(&mut self, event: impl Display) { + let elapsed = self.interval.elapsed(); + self.interval = Instant::now(); + info!(?elapsed, "{}: {event}", self.sequence); + } +} + +impl Drop for EventTimer { + fn drop(&mut self) { + let elapsed = self.start.elapsed(); + info!(?elapsed, "{} is complete", self.sequence); + } +} + +/// Metric name and help text kept together so collector definitions stay grepable. +#[derive(Clone, Copy)] +pub struct MetricSpec { + /// Prometheus collector name. + pub name: &'static str, + /// Prometheus help text. + pub help: &'static str, +} + +/// Creates and registers a counter after applying its initial value. +pub fn counter(spec: MetricSpec, initial: u64) -> IntCounter { + let counter = validate(IntCounter::with_opts(opts(spec))); + counter.inc_by(initial); + register(spec, counter.clone()); + counter +} + +/// Creates and registers a labeled counter. +pub fn counter_vec(spec: MetricSpec, labels: &[&'static str]) -> IntCounterVec { + let counter = validate(IntCounterVec::new(opts(spec), labels)); + register(spec, counter.clone()); + counter +} + +/// Creates and registers a gauge after applying its initial value. +pub fn gauge(spec: MetricSpec, initial: i64) -> IntGauge { + let gauge = validate(IntGauge::with_opts(opts(spec))); + gauge.set(initial); + register(spec, gauge.clone()); + gauge +} + +/// Creates and registers a labeled gauge. +pub fn gauge_vec(spec: MetricSpec, labels: &[&'static str]) -> IntGaugeVec { + let gauge = validate(IntGaugeVec::new(opts(spec), labels)); + register(spec, gauge.clone()); + gauge +} + +/// Converts an unsigned metric value to a saturating Prometheus gauge value. +pub fn gauge_value(value: T) -> i64 +where + i64: TryFrom, +{ + i64::try_from(value).unwrap_or(i64::MAX) +} + +/// Applies `f` when metrics have been initialized; early calls are intentionally no-ops. +pub fn with_metrics(metrics: &OnceLock, f: impl FnOnce(&T)) { + if let Some(metrics) = metrics.get() { + f(metrics); + } +} + +/// Low-cardinality operation label used by duration histograms. +pub trait MetricOperation: Copy { + /// Returns the Prometheus label value for this operation. + fn label(self) -> &'static str; + + /// Starts a timer against `counters`, or a no-op timer before metrics are initialized. + fn time(self, counters: Option<&OperationCounters>) -> OperationTimer<'_> { + counters.map(|c| c.time(self)).unwrap_or_else(|| OperationTimer::noop(self)) + } +} + +/// Runtime operation duration histogram sharing one `op` label. +pub struct OperationCounters(HistogramVec); + +/// Operation duration histogram buckets, in microseconds. +const OPERATION_BUCKETS_MICROS: [f64; 8] = + [50.0, 200.0, 800.0, 3_200.0, 12_800.0, 51_200.0, 204_800.0, 1_000_000.0]; + +impl OperationCounters { + /// Builds the duration histogram collector. + pub fn new(micros: MetricSpec) -> Self { + let opts = HistogramOpts::new(micros.name, micros.help) + .namespace(NAMESPACE) + .buckets(OPERATION_BUCKETS_MICROS.to_vec()); + let counters = Self(validate(HistogramVec::new(opts, &["op"]))); + register(micros, counters.0.clone()); + counters + } + + /// Starts an operation timer that records latency when the returned guard drops. + pub fn time(&self, op: impl MetricOperation) -> OperationTimer<'_> { + OperationTimer { + counters: Some(self), + op: op.label(), + started: Instant::now(), + } + } +} + +/// Drop guard that records elapsed operation time in the metrics registry. +pub struct OperationTimer<'a> { + /// Operation counters to update on drop. + counters: Option<&'a OperationCounters>, + /// Operation label recorded with the duration observation. + op: &'static str, + /// Monotonic start instant captured when the guard is created. + started: Instant, +} + +impl OperationTimer<'static> { + /// Returns a timer that intentionally records nothing. + pub fn noop(op: impl MetricOperation) -> Self { + Self { + counters: None, + op: op.label(), + started: Instant::now(), + } + } +} + +impl Drop for OperationTimer<'_> { + /// Records elapsed microseconds when the timer leaves scope. + fn drop(&mut self) { + let Some(counters) = self.counters else { + return; + }; + let elapsed = self.started.elapsed().as_micros() as f64; + counters.0.with_label_values(&[self.op]).observe(elapsed); + } +} + +/// Builds namespaced Prometheus options for an engine metric. +fn opts(spec: MetricSpec) -> Opts { + Opts::new(spec.name, spec.help).namespace(NAMESPACE) +} + +/// Registers `collector`, logging registry errors without aborting startup. +fn register(spec: MetricSpec, collector: C) +where + C: prometheus::core::Collector + 'static, +{ + if let Err(error) = default_registry().register(Box::new(collector)) { + warn!(metric = spec.name, ?error, "failed to register metric"); + } +} + +/// Unwraps construction of static metric definitions. +#[allow(clippy::expect_used)] +fn validate(result: prometheus::Result) -> T { + result.expect("prometheus metric registration should succeed") +} diff --git a/nucleus/src/notifier.rs b/nucleus/src/notifier.rs new file mode 100644 index 00000000..d51b401a --- /dev/null +++ b/nucleus/src/notifier.rs @@ -0,0 +1,42 @@ +//! One-shot async event notification. + +use std::sync::atomic::{AtomicBool, Ordering::*}; + +use tokio::sync::Notify; + +/// A one-shot latch that wakes every waiter once notified. +/// +/// Waiters that arrive after notification return immediately. The event cannot +/// be reset. +#[derive(Default)] +pub struct EventNotifier { + /// Set after notification so future waiters can return immediately. + done: AtomicBool, + /// Outcome published before `done` makes the event observable. + success: AtomicBool, + /// Wakes tasks that registered before notification. + notify: Notify, +} + +impl EventNotifier { + /// Completes the event with `success` and wakes all current waiters. + pub fn notify(&self, success: bool) { + self.success.store(success, Release); + self.done.store(true, Release); + self.notify.notify_waiters(); + } + + /// Waits until completion and returns the published outcome. + pub async fn notified(&self) -> bool { + loop { + let notified = self.notify.notified(); + // The waiter is created before the second load, so a concurrent + // notify cannot land between observing `false` and registering. + if self.done.load(Acquire) { + return self.success.load(Acquire); + } + + notified.await; + } + } +} diff --git a/nucleus/src/runtime.rs b/nucleus/src/runtime.rs new file mode 100644 index 00000000..06735410 --- /dev/null +++ b/nucleus/src/runtime.rs @@ -0,0 +1,129 @@ +//! Runtime-facing shared types: transaction views, execution I/O, scheduling +//! messages, and the schema of the engine's built-in MagicRoot program (its +//! address, instruction set, and authority). The MagicRoot execution logic +//! lives in the `magic-root-program` crate, which depends on these shared +//! definitions. + +use std::sync::Arc; + +use agave_transaction_view::{ + resolved_transaction_view::ResolvedTransactionView, transaction_view::SanitizedTransactionView, +}; +use derive_more::{Deref, From}; +use solana_svm::{ + transaction_balances::BalanceCollector, + transaction_processing_result::TransactionProcessingResult, +}; +use solana_transaction_error::TransactionResult; +use tokio::sync::mpsc::Sender; + +use crate::{Slot, ledger::Block}; + +/// Sanitized transaction view backed by a shared, immutable payload buffer. +pub type TransactionView = SanitizedTransactionView>>; +/// Sanitized transaction view with its account keys already resolved. +pub type ResolvedTransaction = ResolvedTransactionView>>; +/// Dropping this handle releases a sequencer quiescence barrier. +pub type BarrierHandle = oneshot::Sender<()>; + +/// Cloneable submission handle into the sequencer's execution and simulation +/// channels. +#[derive(Clone, Deref)] +pub struct SequencerHandle { + /// Channel for submitting transactions to be executed and committed. + #[deref] + pub execution: Sender, + /// Channel for submitting transactions to be simulated without committing. + pub simulation: Sender, +} + +/// Work item handed to the transaction sequencer. +#[derive(From)] +pub enum SequencerMessage { + /// A transaction to schedule and execute. + Transaction(TransactionView), + /// A block boundary to seal before scheduling further transactions. + Block(Block), + /// Quiesce the sequencer and all its executors until released — used to take + /// a consistent snapshot at superblock boundaries (see ledger replay and + /// `finalize_superblock`). + Barrier(BarrierGuard), +} + +/// Quiescence barrier handed to a running service. +/// +/// On receipt the service drains its in-flight work, signals `acknowledged`, +/// then blocks on `released` before resuming. Paired with a [`BarrierController`] +/// via [`barrier`]. +pub struct BarrierGuard { + /// Signals the controller that the service is now idle. + pub acknowledged: BarrierHandle, + /// Resolves when the controller permits the service to resume; a dropped + /// controller also releases it. + pub released: oneshot::Receiver<()>, +} + +/// Controller side of a quiescence barrier, held by the caller that raised it. +/// +/// Awaits `acknowledged` to learn the service is quiesced, then sends `released` +/// (or drops) to let it resume. +pub struct BarrierController { + /// Resolves once the service reports it has gone idle. + pub acknowledged: oneshot::Receiver<()>, + /// Releases the service to resume operation. + pub released: BarrierHandle, +} + +/// Constructs a paired [`BarrierController`] and [`BarrierGuard`] over two +/// oneshot channels. +pub fn barrier() -> (BarrierController, BarrierGuard) { + let (acknowledged_tx, acknowledged_rx) = oneshot::channel(); + let (released_tx, released_rx) = oneshot::channel(); + let controller = BarrierController { + acknowledged: acknowledged_rx, + released: released_tx, + }; + let guard = BarrierGuard { + acknowledged: acknowledged_tx, + released: released_rx, + }; + (controller, guard) +} + +/// Work item handed to the transaction simulator. +#[derive(From)] +pub enum SimulatorMessage { + /// A transaction to simulate against the current state. + Transaction(Simulation), + /// A block boundary advancing the simulator's environment. + Block(Block), + /// Quiesce the simulator until released — keeps it idle while a consistent + /// snapshot is taken at superblock boundaries. + Barrier(BarrierGuard), +} + +/// A single simulation request and the channel to deliver its outcome. +pub struct Simulation { + /// The transaction to simulate. + pub transaction: TransactionView, + /// Where the simulation result is returned to the caller. + pub response: oneshot::Sender>, +} + +/// Transaction payload paired with the SVM output needed to finalize state. +pub struct FullTransaction { + /// Sanitized transaction bytes and signatures accepted by the ledger. + pub transaction: TransactionView, + /// SVM execution output for the transaction. + pub execution: ExecutionRecord, +} + +/// SVM output produced by executing a transaction. +pub struct ExecutionRecord { + /// SVM processing result produced for this transaction. + pub result: TransactionProcessingResult, + /// Native pre/post balances collected during execution. + pub balances: Option, + /// Slot assigned to the execution result. + pub slot: Slot, +} diff --git a/nucleus/src/shutdown.rs b/nucleus/src/shutdown.rs new file mode 100644 index 00000000..3dd2396d --- /dev/null +++ b/nucleus/src/shutdown.rs @@ -0,0 +1,257 @@ +//! Cooperative shutdown for engine services. +//! +//! A [`ShutdownManager`] owns ordered cancellation tokens and a set of +//! registered service handles. Each service receives a [`ShutdownHandle`], +//! observes its tier token while running, and reports a [`ShutdownReason`] when +//! it exits. The manager waits for an OS shutdown signal, internal cancellation, +//! or service termination, then cancels each tier in order and gives it a +//! bounded window to stop before moving on. + +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; + +use futures::{StreamExt, future::BoxFuture, stream::FuturesUnordered}; +use oneshot::Sender; +use tokio::time::timeout; +use tracing::{error, info, warn}; + +pub use tokio_util::sync::CancellationToken; + +const TIMEOUT: Duration = Duration::from_secs(4); + +type HandleFuture = BoxFuture<'static, (Service, ShutdownTier, ShutdownReason)>; + +/// Background service tracked by the shutdown manager. +#[derive(Clone, Copy, Debug)] +pub enum Service { + /// Ledger append worker. + LedgerAppender, + /// Ledger read worker. + LedgerReader, + /// Ledger replay worker. + LedgerReplayer, + /// Transaction scheduler service. + Sequencer, + /// Transaction executor worker with its worker index. + TransactionExecutor(u32), + /// Transaction simulation worker. + TransactionSimulator, + /// Subscription map cleanup worker. + SubscriptionsCleanup, + /// Block pacing task. + PaceMaker, + /// Leader-side service streaming blockstore bytes to followers. + ReplicationDispatcher, + /// Follower-side service pulling replicated state from the leader. + ReplicationClient, +} + +impl Service { + /// Shutdown tier for this service; lower tiers are stopped first. + fn tier(&self) -> ShutdownTier { + use Service::*; + match self { + ReplicationClient => ShutdownTier::One, + PaceMaker => ShutdownTier::Two, + // The pacemaker drains the sequencer and sends the appender's final + // sync before either service reaches this tier. + Sequencer | LedgerAppender => ShutdownTier::Three, + _ => ShutdownTier::Four, + } + } +} + +#[derive(Clone, Copy, Debug)] +enum ShutdownTier { + One, + Two, + Three, + Four, +} + +impl ShutdownTier { + const COUNT: usize = 4; + const ORDER: [Self; Self::COUNT] = [Self::One, Self::Two, Self::Three, Self::Four]; +} + +/// Coordinates graceful shutdown across engine services. +#[derive(Default)] +pub struct ShutdownManager { + /// Service cancellation tokens, one per ordered shutdown tier. + tokens: [CancellationToken; ShutdownTier::COUNT], + /// Registered service termination reports. + handles: FuturesUnordered, + /// Number of services that have not reported termination, by tier. + pending: [isize; ShutdownTier::COUNT], +} + +impl ShutdownManager { + /// Wait for an OS shutdown signal, internal cancellation, or service failure. + pub async fn wait(&mut self) -> ShutdownReason { + tokio::select! { + result = graceful_shutdown() => { + match result { + Ok(()) => { + info!("graceful shutdown has been requested"); + ShutdownReason::Signalled + } + Err(error) => ShutdownReason::Error(Box::new(error)), + } + } + Some((service, tier, reason)) = self.handles.next(), if !self.handles.is_empty() => { + self.pending(tier, -1); + error!(?service, ?reason, "terminated prematurely"); + reason + } + } + } + + /// Cancels services one tier at a time and drains their termination reports. + /// + /// Each tier gets `TIMEOUT` to report before the next tier is + /// cancelled. Already terminated services are skipped by their tier. + pub async fn terminate(&mut self) { + info!("initiating graceful shutdown of the engine"); + let start = Instant::now(); + let mut timers = [start; ShutdownTier::COUNT]; + for tier in ShutdownTier::ORDER { + timers[tier as usize] = Instant::now(); + self.tokens[tier as usize].cancel(); + if self.pending[tier as usize] == 0 { + continue; + } + if timeout(TIMEOUT, self.drain(tier, &timers)).await.is_err() { + let remaining = self.pending[tier as usize]; + let elapsed = timers[tier as usize].elapsed(); + warn!(?tier, remaining, ?elapsed, "shutdown tier timed out"); + } + } + info!(elapsed = ?start.elapsed(), "engine shutdown complete"); + } + + /// Register a service and return its cancellation handle. + pub fn handle(&mut self, service: Service) -> ShutdownHandle { + let tier = service.tier(); + let (tx, rx) = oneshot::channel(); + let fut = async move { + let reason = rx.await.unwrap_or_default(); + (service, tier, reason) + }; + self.handles.push(Box::pin(fut)); + self.pending(tier, 1); + ShutdownHandle { + cancel: self.tokens[tier as usize].child_token(), + reason: Some(tx), + } + } + + async fn drain(&mut self, tier: ShutdownTier, timers: &[Instant]) { + while self.pending[tier as usize] != 0 { + let Some((service, tier, reason)) = self.handles.next().await else { + return; + }; + // Another tier may finish while this one drains; debit its own pending count. + self.pending(tier, -1); + let elapsed = timers[tier as usize].elapsed(); + Self::log(service, reason, elapsed); + } + } + + fn pending(&mut self, tier: ShutdownTier, op: isize) { + self.pending[tier as usize] += op; + } + + fn log(service: Service, reason: ShutdownReason, elapsed: Duration) { + match reason { + ShutdownReason::Unexpected => { + warn!(?service, ?elapsed, "terminated unexpectedly") + } + ShutdownReason::Signalled => { + info!(?service, ?elapsed, "terminated gracefully") + } + ShutdownReason::RestartRequired => { + warn!(?service, ?elapsed, "requested a restart") + } + ShutdownReason::Error(error) => { + error!(?service, ?error, ?elapsed, "terminated with error") + } + } + } +} + +impl Drop for ShutdownManager { + fn drop(&mut self) { + for token in &self.tokens { + token.cancel(); + } + } +} + +/// Waits for SIGTERM or Ctrl-C. +async fn graceful_shutdown() -> io::Result<()> { + use tokio::signal::unix::{SignalKind, signal}; + let mut term = signal(SignalKind::terminate())?; + tokio::select! { + signal = term.recv() => signal + .ok_or_else(|| io::Error::other("SIGTERM listener closed")), + result = tokio::signal::ctrl_c() => result, + } +} + +/// Cancellation handle held by a running service. +pub struct ShutdownHandle { + /// Token observed by the running service. + cancel: CancellationToken, + /// One-shot report consumed by the manager when the service exits. + reason: Option>, +} + +/// Reason reported when a service terminates. +#[derive(Debug, Default)] +pub enum ShutdownReason { + /// Service handle was dropped without reporting a reason. + #[default] + Unexpected, + /// Service stopped after being signalled. + Signalled, + /// Service stopped because of an error. + Error(Box), + /// Service staged state that must be installed by restarting the engine. + RestartRequired, +} + +impl ShutdownHandle { + /// Request engine shutdown and report this service's termination reason. + pub fn terminate(&mut self, reason: ShutdownReason) { + self.cancel.cancel(); + if let Some(tx) = self.reason.take() { + let _ = tx.send(reason); + }; + } + + /// Wait until this service's shutdown tier is cancelled. + pub async fn signalled(&self) { + self.cancel.cancelled().await + } + + /// Returns whether this service's shutdown tier has been cancelled. + pub fn requested(&self) -> bool { + self.cancel.is_cancelled() + } + + /// Creates a cancellation token for work owned by this service. + pub fn child(&self) -> CancellationToken { + self.cancel.child_token() + } +} + +impl Drop for ShutdownHandle { + fn drop(&mut self) { + if let Some(tx) = self.reason.take() { + let _ = tx.send(ShutdownReason::Unexpected); + }; + } +} diff --git a/nucleus/src/testkit.rs b/nucleus/src/testkit.rs new file mode 100644 index 00000000..8f05a58b --- /dev/null +++ b/nucleus/src/testkit.rs @@ -0,0 +1,156 @@ +//! Engine-agnostic test fixtures shared across crate test suites. +//! +//! Only the primitives that depend on nothing above nucleus live here — +//! wincode-serialized transactions, block boundaries, and throwaway directories. +//! Keeper-level harness code (building a `Keeper`, loading the v42 ELF) lives in +//! `keeper::testkit`. Compiled only under the `testkit` feature, so it never +//! reaches release builds. +// Test-support code: a panic here fails the test that caused it, which is the +// intended reporting path. Kept out of release builds by the `testkit` feature. +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use std::sync::{Arc, Once}; + +use solana_hash::Hash; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_message::{Message, VersionedMessage, v0, v1}; +use solana_pubkey::Pubkey; +use solana_signature::Signature; +use solana_signer::Signer; +use solana_transaction::versioned::VersionedTransaction; +pub use tempfile::TempDir; +use tracing_subscriber::{EnvFilter, fmt}; +use v42_calculator_interface::builder::Expr as E; + +pub use v42_calculator_interface::ID as V42_ID; + +use crate::{Slot, ledger::Block, runtime::TransactionView}; + +static TRACING: Once = Once::new(); + +/// Standard transaction wire format produced by client SDKs. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WireVersion { + /// Unversioned legacy message. + Legacy, + /// Version 0 message without address lookup tables. + V0, + /// Version 1 message. + V1, +} + +/// Installs a libtest-aware tracing subscriber for test processes. +/// +/// The default filter is intentionally quiet. Set `RUST_LOG` and run tests with +/// `-- --nocapture` to see lower-level spans and events while debugging. +pub fn init_tracing() { + TRACING.call_once(|| { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")); + let _ = fmt().with_env_filter(filter).with_test_writer().try_init(); + }); +} + +/// A throwaway on-disk directory; the returned guard must outlive the store +/// opened over it, since these stores keep their files open/mmapped. +pub fn tempdir() -> TempDir { + TempDir::new().unwrap() +} + +/// A block boundary with a distinct hash and time derived from `slot`. +pub fn block(slot: Slot) -> Block { + let mut hash = [0; 32]; + let bytes = slot.to_le_bytes(); + hash[..bytes.len()].copy_from_slice(&bytes); + Block { + slot, + hash: Hash::new_from_array(hash), + time: slot as i64, + parent: Hash::default(), + } +} + +/// Signs `instructions` in a standard client wire format. +pub fn sign_versioned_instructions( + payer: &Keypair, + version: WireVersion, + instructions: impl AsRef<[Instruction]>, + blockhash: Hash, +) -> (Signature, Vec) { + let instructions = instructions.as_ref(); + let message = match version { + WireVersion::Legacy => VersionedMessage::Legacy(Message::new_with_blockhash( + instructions, + Some(&payer.pubkey()), + &blockhash, + )), + WireVersion::V0 => VersionedMessage::V0( + v0::Message::try_compile(&payer.pubkey(), instructions, &[], blockhash).unwrap(), + ), + WireVersion::V1 => VersionedMessage::V1( + v1::Message::try_compile(&payer.pubkey(), instructions, blockhash).unwrap(), + ), + }; + let transaction = VersionedTransaction::try_new(message, &[payer]).unwrap(); + let signature = transaction.signatures[0]; + (signature, wincode::serialize(&transaction).unwrap()) +} + +/// Signs `instructions` from `payer` against `blockhash`, returning the first +/// signature and the wincode-serialized transaction bytes. +pub fn sign_instructions( + payer: &Keypair, + instructions: impl AsRef<[Instruction]>, + blockhash: Hash, +) -> (Signature, Arc>) { + let (signature, transaction) = + sign_versioned_instructions(payer, WireVersion::Legacy, instructions, blockhash); + (signature, Arc::new(transaction)) +} + +/// Builds one v42 instruction that sums every supplied operand into `output`. +pub fn v42_sum(output: Pubkey, operands: &[Pubkey]) -> Instruction { + assert!( + !operands.is_empty(), + "v42 sum requires at least one operand" + ); + let expression = (2..=operands.len()).fold(E::acc(1), |expr, index| expr + E::acc(index as u8)); + expression.compose(output, operands) +} + +/// Builds a v42 instruction retaining `value` while adding evaluator work. +pub fn v42_padded_value(output: Pubkey, value: i64, terms: usize) -> Instruction { + let expression = (1..terms).fold(E::lit(value), |expr, _| expr + E::lit(0)); + expression.compose(output, &[]) +} + +/// Returns deterministic non-uniform bytes for detecting damaged large payloads. +pub fn patterned_bytes(len: usize, seed: u8) -> Vec { + (0..len).map(|index| seed.wrapping_add((index % 251) as u8)).collect() +} + +/// Signs `instructions` and returns the sanitized transaction view consumed by +/// the runtime. +pub fn signed_view( + payer: &Keypair, + instructions: impl AsRef<[Instruction]>, + blockhash: Hash, +) -> (Signature, TransactionView) { + let (signature, bytes) = sign_instructions(payer, instructions, blockhash); + let view = TransactionView::try_new_sanitized(bytes, true).unwrap(); + (signature, view) +} + +/// A signed, wincode-serialized v42 transaction plus its first signature. +/// +/// The instruction references `accounts` as read-only keys so they land in the +/// transaction's static account keys (and thus any account index), and targets +/// the v42 program so the transaction is executable, not just well-formed. A +/// fresh random payer per call keeps signatures unique without varying the +/// blockhash. +pub fn transaction(accounts: &[Pubkey]) -> (Signature, Arc>) { + let payer = Keypair::new(); + let metas = accounts.iter().map(|k| AccountMeta::new_readonly(*k, false)).collect(); + let ix = Instruction::new_with_bytes(V42_ID, &[], metas); + sign_instructions(&payer, [ix], Hash::default()) +} diff --git a/nucleus/src/tls.rs b/nucleus/src/tls.rs new file mode 100644 index 00000000..35968321 --- /dev/null +++ b/nucleus/src/tls.rs @@ -0,0 +1,46 @@ +//! Thread-local execution state shared with runtime-adjacent code. + +use std::{ + cell::{Cell, RefCell}, + collections::VecDeque, +}; + +use solana_instruction_error::InstructionError; +use solana_pubkey::Pubkey; +use wincode::{SchemaWrite, config::Configuration}; + +/// Wincode-encoded message buffered for later handling on the same thread. +pub type EncodedMessage = Vec; + +thread_local! { + /// Per-thread queue for messages emitted while a transaction executes. + pub static TLS: RefCell = RefCell::new(Default::default()); + /// Signer authorized to invoke the MagicRoot program on the current thread. + pub static AUTHORITY: Cell = Cell::new(Default::default()); +} + +/// FIFO queue of encoded messages scoped to the current thread. +#[derive(Default)] +pub struct TlsManager(VecDeque); + +impl TlsManager { + /// Encodes `msg` and appends it to the current thread's queue. + pub fn enqueue(msg: &T) -> Result<(), InstructionError> + where + T: SchemaWrite, + { + let encoded = wincode::serialize(msg).map_err(|_| InstructionError::Custom(u32::MAX))?; + TLS.with_borrow_mut(|tls| tls.0.push_back(encoded)); + Ok(()) + } + + /// Removes the oldest encoded message from the current thread's queue. + pub fn dequeue() -> Option { + TLS.with_borrow_mut(|tls| tls.0.pop_front()) + } + + /// Drops every queued message for the current thread. + pub fn clear() { + TLS.with_borrow_mut(|tls| tls.0.clear()) + } +} diff --git a/programs/v42-calculator-interface/Cargo.toml b/programs/v42-calculator-interface/Cargo.toml new file mode 100644 index 00000000..c0ffd3c5 --- /dev/null +++ b/programs/v42-calculator-interface/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "v42-calculator-interface" + +authors.workspace = true +# Compiled to BPF as the program's opcode dependency, so it stays within the +# SBF toolchain's rustc: edition 2021 and no elevated workspace MSRV. +edition = "2021" +homepage.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +solana-instruction = { workspace = true, optional = true } +solana-pubkey = { workspace = true } + +[features] +builder = ["dep:solana-instruction"] +default = ["builder"] + +[lints] +workspace = true diff --git a/programs/v42-calculator-interface/README.md b/programs/v42-calculator-interface/README.md new file mode 100644 index 00000000..3359c7b3 --- /dev/null +++ b/programs/v42-calculator-interface/README.md @@ -0,0 +1,20 @@ +# `v42-calculator-interface` + +This crate defines the v42 calculator program id, instruction wire constants, +and optional off-chain builders. The SBF program depends on the wire definitions +with the default `builder` feature disabled. + +`Expr` produces postfix instruction data from signed `i64` literals, account +operands, the `Clock` sysvar, arithmetic operators, and recursive self-CPI +subexpressions. Expression composition concatenates existing postfix byte +streams. + +`Expr::compose` builds an instruction with the writable output at account zero, +read-only operands after it, and the calculator program id last for recursive +CPI. `Expr::acc` indexes the full instruction account list, so operand indexes +start at one and remain stable across nested calls. + +`builder::transfer` applies a signed delta between distinct writable v42 accounts +at indexes 0 and 1. Its data is `TRANSFER` followed by a little-endian `i64`; +positive values move lamports and calculator value from account 0 to account 1, +while negative values reverse the direction. diff --git a/programs/v42-calculator-interface/src/builder.rs b/programs/v42-calculator-interface/src/builder.rs new file mode 100644 index 00000000..5ffb0127 --- /dev/null +++ b/programs/v42-calculator-interface/src/builder.rs @@ -0,0 +1,117 @@ +//! Off-chain expression builder. + +use core::ops::{Add, Div, Mul, Sub}; + +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +use crate::{opcodes::*, ID, TRANSFER}; + +/// Build a transfer between distinct v42-owned accounts. A positive `delta` +/// moves value from `from` to `to`; a negative delta reverses the direction. +/// Neither account is required to sign, but both are writable. +pub fn transfer(from: Pubkey, to: Pubkey, delta: i64) -> Instruction { + let mut data = Vec::with_capacity(9); + data.push(TRANSFER); + data.extend_from_slice(&delta.to_le_bytes()); + Instruction { + program_id: ID, + accounts: vec![AccountMeta::new(from, false), AccountMeta::new(to, false)], + data, + } +} + +/// A calculator expression, built the way you'd write the math and lowered to +/// the postfix byte stream the program evaluates. Construct leaves with the +/// associated functions, combine them with `+`, `-`, `*`, `/`, and wrap any +/// subtree in [`Expr::cpi`] to force it through a recursive self-CPI (its result +/// then travels back through return data instead of being computed inline). +/// +/// ``` +/// use solana_pubkey::Pubkey; +/// use v42_calculator_interface::builder::Expr as E; +/// +/// // (42 + (31 * 4) - 56) * 2, with the product evaluated via CPI. +/// let program = (E::lit(42) + (E::lit(31) * E::lit(4)).cpi() - E::lit(56)) * E::lit(2); +/// let ix = program.compose(Pubkey::default(), &[]); +/// assert_eq!(ix.program_id, v42_calculator_interface::ID); +/// ``` +/// +/// The wrapped bytes are already in postfix order, so combining expressions is +/// just concatenation — there is no intermediate tree to walk. +#[derive(Clone, Debug)] +pub struct Expr(Vec); + +impl Expr { + /// An immediate signed literal operand. + pub fn lit(value: i64) -> Self { + let mut bytes = Vec::with_capacity(9); + bytes.push(PUSH_LIT); + bytes.extend_from_slice(&value.to_le_bytes()); + Self(bytes) + } + + /// The `i64` LE stored in instruction account `index`. Account 0 is the + /// output account; the operands passed to [`Expr::compose`] start at 1. + pub fn acc(index: u8) -> Self { + Self(vec![PUSH_ACC, index]) + } + + /// The current clock unix timestamp. + pub fn clock() -> Self { + Self(vec![PUSH_CLOCK]) + } + + /// Evaluate this subexpression through a recursive self-CPI rather than + /// inline. All instruction accounts are forwarded to the callee, so + /// [`Expr::acc`] indices are unchanged at any depth. + pub fn cpi(self) -> Self { + let mut bytes = Vec::with_capacity(3 + self.0.len()); + bytes.push(CALL); + bytes.extend_from_slice(&(self.0.len() as u16).to_le_bytes()); + bytes.extend_from_slice(&self.0); + Self(bytes) + } + + /// The raw postfix byte stream — i.e. the instruction data. + pub fn program(&self) -> Vec { + self.0.clone() + } + + /// Build the instruction: account 0 is the writable `output` the final + /// result is written to, followed by the read-only `operands` referenced by + /// [`Expr::acc`], followed by this program id for recursive CPI. + pub fn compose(&self, output: Pubkey, operands: &[Pubkey]) -> Instruction { + let mut accounts = Vec::with_capacity(2 + operands.len()); + accounts.push(AccountMeta::new(output, false)); + accounts.extend(operands.iter().map(|key| AccountMeta::new_readonly(*key, false))); + accounts.push(AccountMeta::new_readonly(ID, false)); + Instruction { + program_id: ID, + accounts, + data: self.0.clone(), + } + } + + fn binary(mut self, rhs: Expr, op: u8) -> Self { + self.0.extend_from_slice(&rhs.0); + self.0.push(op); + self + } +} + +macro_rules! bin_op { + ($trait:ident, $method:ident, $op:expr) => { + impl $trait for Expr { + type Output = Expr; + fn $method(self, rhs: Expr) -> Expr { + self.binary(rhs, $op) + } + } + }; +} + +bin_op!(Add, add, ADD); +bin_op!(Sub, sub, SUB); +bin_op!(Mul, mul, MUL); +bin_op!(Div, div, DIV); diff --git a/programs/v42-calculator-interface/src/lib.rs b/programs/v42-calculator-interface/src/lib.rs new file mode 100644 index 00000000..216fef44 --- /dev/null +++ b/programs/v42-calculator-interface/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!("../README.md")] + +use solana_pubkey::declare_id; + +pub mod opcodes; + +#[cfg(feature = "builder")] +pub mod builder; + +declare_id!("V42CaLcu1atormagicb1ock11111111111111111111"); + +/// Transfer lamports and calculator value between instruction accounts 0 and +/// 1; an exact little-endian `i64` delta follows. +pub const TRANSFER: u8 = 0x30; diff --git a/programs/v42-calculator-interface/src/opcodes.rs b/programs/v42-calculator-interface/src/opcodes.rs new file mode 100644 index 00000000..219fc1f4 --- /dev/null +++ b/programs/v42-calculator-interface/src/opcodes.rs @@ -0,0 +1,30 @@ +//! Opcodes for the v42-calculator RPN byte stream — the single source of truth +//! for the wire format, shared by the off-chain `Expr` builder and the on-chain +//! evaluator so the two can never drift. Adding an operation is one constant +//! here plus one match arm in the program. +//! +//! A program is a flat sequence of tokens evaluated left-to-right against a +//! `i64` stack: `PUSH_*` tokens push one value, the arithmetic tokens pop two +//! and push one, and `CALL` evaluates a nested program through a self-CPI and +//! pushes its result. Exactly one value must remain when the stream ends. + +/// Push an immediate `i64`; 8 little-endian bytes follow. +pub const PUSH_LIT: u8 = 0x00; +/// Push the `i64` LE held in the first 8 data bytes of an instruction account; +/// a 1-byte account index follows. +pub const PUSH_ACC: u8 = 0x01; +/// Push the current clock unix timestamp. +pub const PUSH_CLOCK: u8 = 0x03; + +/// Pop `b`, pop `a`, push `a + b` (checked). +pub const ADD: u8 = 0x10; +/// Pop `b`, pop `a`, push `a - b` (checked). +pub const SUB: u8 = 0x11; +/// Pop `b`, pop `a`, push `a * b` (checked). +pub const MUL: u8 = 0x12; +/// Pop `b`, pop `a`, push checked `a / b` (`b == 0` is an error). +pub const DIV: u8 = 0x13; + +/// Evaluate a nested program via self-CPI and push its return-data `i64`. A +/// `u16` LE byte length follows, then that many bytes of nested program. +pub const CALL: u8 = 0x20;