Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
9 changes: 8 additions & 1 deletion crates/precompiles/price-oracle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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",
]
2 changes: 2 additions & 0 deletions crates/precompiles/price-oracle/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod precompile;
#[cfg(feature = "native")]
pub mod prices;
#[cfg(feature = "native")]
pub mod sequencing;
pub mod types;

Expand Down
88 changes: 57 additions & 31 deletions crates/precompiles/price-oracle/src/precompile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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<S>(PhantomData<S>);
pub struct PriceOraclePrecompile<S> {
_marker: PhantomData<S>,
// Price data snapshot used during eth_call execution when sov_context is not available.
#[cfg(feature = "native")]
snapshot: OnceLock<PriceReports>,
}

impl<S: Spec> EvmPrecompile<S> for PriceOraclePrecompile<S> {
const ADDRESS: Address = PRICE_ORACLE_PRECOMPILE_ADDRESS;
Expand All @@ -34,43 +41,62 @@ impl<S: Spec> EvmPrecompile<S> for PriceOraclePrecompile<S> {
}

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);
}

Ok(PrecompileOutput {
gas_used,
bytes: Bytes::from(payload.clone()),
bytes: Bytes::from(report),
})
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Mutex<OracleStore>> =
LazyLock::new(|| Mutex::new(OracleStore::default()));
use crate::{FeedKey, PriceReports, B256};

static ORACLE_STORE: LazyLock<RwLock<OracleStore>> =
LazyLock::new(|| RwLock::new(OracleStore::default()));

#[derive(Debug, PartialEq, Eq)]
pub enum InsertOutcome {
Expand Down Expand Up @@ -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(
Expand All @@ -153,15 +149,19 @@ pub fn insert_if_newer(
payload: Vec<u8>,
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<B256>) -> 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)]
Expand Down
24 changes: 21 additions & 3 deletions crates/precompiles/price-oracle/tests/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 0 additions & 2 deletions crates/stf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ price-oracle = { workspace = true }

anyhow = { workspace = true }
borsh = { workspace = true }
bytes = { workspace = true, optional = true }

[build-dependencies]
anyhow = { workspace = true }
Expand Down Expand Up @@ -71,7 +70,6 @@ native = [
"sov-state/native",
"sov-chain-state/native",
"price-oracle/native",
"dep:bytes"
]

bench = [
Expand Down
4 changes: 2 additions & 2 deletions crates/stf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading