diff --git a/Cargo.lock b/Cargo.lock index f3911bc7..65a801d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5006,6 +5006,7 @@ dependencies = [ "alloy-primitives", "borsh", "bytes", + "parking_lot", "sov-evm", "sov-modules-api", "sov-rollup-interface", @@ -8305,7 +8306,6 @@ version = "0.3.0" dependencies = [ "anyhow", "borsh", - "bytes", "price-oracle", "serde_json", "sov-address", diff --git a/Cargo.toml b/Cargo.toml index 0be7cda0..8e07c39e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,7 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } tracing-appender = "0.2" tokio = { version = "1", features = ["full"] } lazy_static = "1.5.0" +parking_lot = { version = "0.12", default-features = false } tempfile = "3.20" jsonrpsee = { version = "0.26", features = ["jsonrpsee-types"] } toml = { version = "0.8", default-features = false, features = ["parse"] } diff --git a/crates/precompiles/price-oracle/Cargo.toml b/crates/precompiles/price-oracle/Cargo.toml index 1ceae039..d3f5ee85 100644 --- a/crates/precompiles/price-oracle/Cargo.toml +++ b/crates/precompiles/price-oracle/Cargo.toml @@ -12,6 +12,7 @@ version = "1.0.0" alloy-primitives = { workspace = true, features = ["borsh"] } borsh = { workspace = true } bytes = { workspace = true } +parking_lot = { workspace = true, optional = true } sov-evm = { workspace = true } sov-modules-api = { workspace = true } tracing = { workspace = true, optional = true } @@ -24,4 +25,10 @@ sov-test-utils = { workspace = true } [features] default = [] -native = ["sov-evm/native", "sov-modules-api/native", "dep:tracing"] +native = [ + "sov-evm/native", + "sov-modules-api/native", + "sov-rollup-interface/native", + "dep:tracing", + "dep:parking_lot", +] diff --git a/crates/precompiles/price-oracle/src/lib.rs b/crates/precompiles/price-oracle/src/lib.rs index 752f3e2f..90369c6e 100644 --- a/crates/precompiles/price-oracle/src/lib.rs +++ b/crates/precompiles/price-oracle/src/lib.rs @@ -1,5 +1,7 @@ pub mod precompile; #[cfg(feature = "native")] +pub mod prices; +#[cfg(feature = "native")] pub mod sequencing; pub mod types; diff --git a/crates/precompiles/price-oracle/src/precompile.rs b/crates/precompiles/price-oracle/src/precompile.rs index a8680996..0dc6555d 100644 --- a/crates/precompiles/price-oracle/src/precompile.rs +++ b/crates/precompiles/price-oracle/src/precompile.rs @@ -6,6 +6,8 @@ use sov_evm::precompiles::{ EvmPrecompile, EvmPrecompileEnv, PrecompileError, PrecompileOutput, PrecompileResult, }; use sov_modules_api::{Spec, TxState}; +#[cfg(feature = "native")] +use std::sync::OnceLock; use crate::types::{FeedKey, PriceReports}; @@ -18,7 +20,12 @@ pub const PRICE_ORACLE_PRECOMPILE_BASE_GAS: u64 = 3_000; pub const PRICE_ORACLE_PRECOMPILE_WORD_GAS: u64 = 16; #[derive(Clone, Default)] -pub struct PriceOraclePrecompile(PhantomData); +pub struct PriceOraclePrecompile { + _marker: PhantomData, + // Price data snapshot used during eth_call execution when sov_context is not available. + #[cfg(feature = "native")] + snapshot: OnceLock, +} impl EvmPrecompile for PriceOraclePrecompile { const ADDRESS: Address = PRICE_ORACLE_PRECOMPILE_ADDRESS; @@ -34,35 +41,54 @@ impl EvmPrecompile for PriceOraclePrecompile { } let (provider_id, feed_id) = decode_feed_request(input)?; - - let context = env - .sov_context - .ok_or_else(|| PrecompileError::State("missing transaction context".to_string()))?; - let sequencing_data = context.sequencing_data().as_ref().ok_or_else(|| { - PrecompileError::State("no sequencing data attached to transaction".to_string()) - })?; - let reports = PriceReports::try_from_slice(sequencing_data).map_err(|err| { - PrecompileError::State(format!("could not decode sequencing data: {err}")) - })?; - - let payload = reports - .get(&FeedKey::new(provider_id, feed_id)) - .ok_or_else(|| { - PrecompileError::InvalidInput(format!( - "no price report for provider {provider_id} feed {feed_id}" - )) - })?; - - // Record the feed before the gas check. The payload length affects gas, - // so a feed read here must be kept even if the call then runs out of gas, - // otherwise replay from the DA layer would diverge. - #[cfg(feature = "native")] - crate::sequencing::record_used_feed_key(context, FeedKey::new(provider_id, feed_id)) - .map_err(|err| { - PrecompileError::State(format!("could not record used feed key: {err}")) - })?; - - let words = payload.len().div_ceil(32) as u64; + let feed_key = FeedKey::new(provider_id, feed_id); + + let report = match env.sov_context { + Some(context) => { + let sequencing_data = context.sequencing_data().as_ref().ok_or_else(|| { + PrecompileError::State("no sequencing data attached to transaction".to_string()) + })?; + let reports = PriceReports::try_from_slice(sequencing_data).map_err(|err| { + PrecompileError::State(format!("could not decode sequencing data: {err}")) + })?; + let report = reports + .get(&feed_key) + .ok_or_else(|| { + PrecompileError::InvalidInput(format!( + "no price report for provider {provider_id} feed {feed_id}" + )) + })? + .clone(); + + // Record the feed before the gas check. The payload length affects gas, + // so a feed read here must be kept even if the call then runs out of gas, + // otherwise replay from the DA layer would diverge. + #[cfg(feature = "native")] + crate::sequencing::record_used_feed_key(context, feed_key).map_err(|err| { + PrecompileError::State(format!("could not record used feed key: {err}")) + })?; + + report + } + None => { + #[cfg(not(feature = "native"))] + return Err(PrecompileError::State( + "missing transaction context".to_string(), + )); + #[cfg(feature = "native")] + self.snapshot + .get_or_init(crate::prices::snapshot_prices) + .get(&feed_key) + .ok_or_else(|| { + PrecompileError::InvalidInput(format!( + "no price report for provider {provider_id} feed {feed_id}" + )) + })? + .clone() + } + }; + + let words = report.len().div_ceil(32) as u64; let gas_used = PRICE_ORACLE_PRECOMPILE_BASE_GAS + PRICE_ORACLE_PRECOMPILE_WORD_GAS * words; if gas_used > gas_limit { return Err(PrecompileError::OutOfGas); @@ -70,7 +96,7 @@ impl EvmPrecompile for PriceOraclePrecompile { Ok(PrecompileOutput { gas_used, - bytes: Bytes::from(payload.clone()), + bytes: Bytes::from(report), }) } } diff --git a/crates/stf/src/prices.rs b/crates/precompiles/price-oracle/src/prices.rs similarity index 93% rename from crates/stf/src/prices.rs rename to crates/precompiles/price-oracle/src/prices.rs index 2b9986f5..ff67d040 100644 --- a/crates/stf/src/prices.rs +++ b/crates/precompiles/price-oracle/src/prices.rs @@ -1,11 +1,13 @@ use std::collections::{BTreeMap, BTreeSet}; -use std::sync::{LazyLock, Mutex}; +use std::sync::LazyLock; use bytes::Bytes; -use price_oracle::{FeedKey, PriceReports, B256}; +use parking_lot::RwLock; -static ORACLE_STORE: LazyLock> = - LazyLock::new(|| Mutex::new(OracleStore::default())); +use crate::{FeedKey, PriceReports, B256}; + +static ORACLE_STORE: LazyLock> = + LazyLock::new(|| RwLock::new(OracleStore::default())); #[derive(Debug, PartialEq, Eq)] pub enum InsertOutcome { @@ -137,14 +139,8 @@ impl OracleStore { } } -fn store() -> std::sync::MutexGuard<'static, OracleStore> { - ORACLE_STORE - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) -} - pub fn snapshot_prices() -> PriceReports { - store().snapshot() + ORACLE_STORE.read().snapshot() } pub fn insert_if_newer( @@ -153,15 +149,19 @@ pub fn insert_if_newer( payload: Vec, order_time: u64, ) -> InsertOutcome { - store().insert_if_newer(provider_id, feed_id, payload, order_time) + ORACLE_STORE + .write() + .insert_if_newer(provider_id, feed_id, payload, order_time) } pub fn register_feeds(source_name: &str, provider_id: B256, feeds: Vec) -> RegisterOutcome { - store().register(source_name, provider_id, feeds) + ORACLE_STORE + .write() + .register(source_name, provider_id, feeds) } pub fn remove_source(source_name: &str) -> usize { - store().remove_source(source_name) + ORACLE_STORE.write().remove_source(source_name) } #[cfg(test)] diff --git a/crates/precompiles/price-oracle/tests/execute.rs b/crates/precompiles/price-oracle/tests/execute.rs index 47e2b637..7892674d 100644 --- a/crates/precompiles/price-oracle/tests/execute.rs +++ b/crates/precompiles/price-oracle/tests/execute.rs @@ -138,9 +138,27 @@ fn wrong_length_request_is_invalid_input() { } #[test] -fn missing_context_is_state_error() { - let err = run(None, &request(*PROVIDER_ID, feed_id(1)), GAS_LIMIT).unwrap_err(); - assert!(matches!(err, PrecompileError::State(_))); +fn missing_context_serves_latest_prices() { + let provider = B256::repeat_byte(0xc1); + let feed = B256::repeat_byte(0xf1); + let payload = b"live-store-update".to_vec(); + price_oracle::prices::register_feeds("test-fallback-source", provider, vec![feed]); + assert_eq!( + price_oracle::prices::insert_if_newer(provider, feed, payload.clone(), 1), + price_oracle::prices::InsertOutcome::Inserted + ); + + let output = run(None, &request(provider, feed), GAS_LIMIT).expect("store feed should resolve"); + + assert_eq!(output.bytes.as_ref(), payload.as_slice()); + assert_eq!(output.gas_used, expected_gas(payload.len())); +} + +#[test] +fn missing_context_with_unknown_feed_is_invalid() { + let provider = B256::repeat_byte(0xee); + let err = run(None, &request(provider, feed_id(1)), GAS_LIMIT).unwrap_err(); + assert!(matches!(err, PrecompileError::InvalidInput(_))); } #[test] diff --git a/crates/stf/Cargo.toml b/crates/stf/Cargo.toml index 9e19891d..27c427f3 100644 --- a/crates/stf/Cargo.toml +++ b/crates/stf/Cargo.toml @@ -31,7 +31,6 @@ price-oracle = { workspace = true } anyhow = { workspace = true } borsh = { workspace = true } -bytes = { workspace = true, optional = true } [build-dependencies] anyhow = { workspace = true } @@ -71,7 +70,6 @@ native = [ "sov-state/native", "sov-chain-state/native", "price-oracle/native", - "dep:bytes" ] bench = [ diff --git a/crates/stf/src/lib.rs b/crates/stf/src/lib.rs index ab892b78..7f7227c8 100644 --- a/crates/stf/src/lib.rs +++ b/crates/stf/src/lib.rs @@ -2,10 +2,10 @@ pub mod authentication; mod delegation; -#[cfg(feature = "native")] -pub mod prices; pub mod runtime; +#[cfg(feature = "native")] +pub use price_oracle::prices; pub use runtime::*; use sov_modules_stf_blueprint::StfBlueprint; use sov_rollup_interface::stf::StateTransitionVerifier;