use std::{collections::BTreeMap, convert::Infallible, fmt, ops::Deref, str::FromStr, sync::Arc};
use borsh::{BorshDeserialize, BorshSerialize};
#[cfg(feature = "non-pdk")]
use clap::Subcommand;
use rialo_cli_representable::Representable;
use rialo_limits::{max_oracle_output_serialized_bytes, MIN_VIABLE_LIMIT_OF_ORACLE_OUTPUT_SIZE};
use rialo_s_compute_budget::compute_budget_limits::{MAX_COMPUTE_UNIT_LIMIT, MAX_HEAP_FRAME_BYTES};
use rialo_s_pubkey::Pubkey;
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
#[cfg(feature = "non-pdk")]
use url::Url;
use crate::{AttestationReport, Headers, HttpFilter, Nonce, OracleDutyConfig, RetryConfig};
#[derive(
Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct OracleId {
pub nonce: Nonce,
pub creator: Pubkey,
}
impl OracleId {
pub fn new(creator: Pubkey, nonce: impl Into<Nonce>) -> Self {
Self {
nonce: nonce.into(),
creator,
}
}
}
impl fmt::Display for OracleId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", &self.nonce, &self.creator)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Representable)]
#[representable(human_readable = "oracle_info_human_readable")]
pub struct OracleInfo {
pub id: OracleId,
pub description: String,
pub update_frequency: UpdateFrequency,
pub target_oracles: Vec<TargetOracle>,
pub starting_round: StartingRound,
pub is_active: bool,
pub created_at: i64,
pub aggregators: Vec<(String, Pubkey)>,
#[serde(default = "default_validators_per_duty")]
pub validators_per_duty: u32,
pub retry_config: RetryConfig,
#[serde(default = "default_oracle_request_delay")]
pub oracle_request_delay: u32,
pub compute_units_limit: Option<u32>,
pub heap_size_limit: Option<u32>,
}
fn oracle_info_human_readable(info: &OracleInfo) -> String {
let mut out = String::new();
out.push_str(&format!("Oracle ID: {}\n", info.id));
out.push_str(&format!("Description: {}\n", info.description));
out.push_str(&format!("Active: {}\n", info.is_active));
out.push_str(&format!("Starting Round: {:?}\n", info.starting_round));
out.push_str(&format!("Update Frequency: {:?}\n", info.update_frequency));
out.push_str(&format!("Created At: {}\n", info.created_at));
out.push_str(&format!(
"Validators Per Duty: {}\n",
info.validators_per_duty
));
out.push_str(&format!(
"Oracle Request Delay: {}\n",
info.oracle_request_delay
));
out.push_str("\nRetry Config:\n");
out.push_str(&format!(
" - Retry Delay: {}\n",
info.retry_config.retry_delay()
));
out.push_str(&format!(
" - Max Retries: {:?}\n",
info.retry_config.num_retries()
));
if !info.target_oracles.is_empty() {
out.push_str(&format!(
"\nTarget Oracles ({}):\n",
info.target_oracles.len()
));
for (i, target) in info.target_oracles.iter().enumerate() {
out.push_str(&format!(" {}. {:?}\n", i + 1, target));
}
}
if !info.aggregators.is_empty() {
out.push_str(&format!("\nAggregators ({}):\n", info.aggregators.len()));
for (topic, agg) in &info.aggregators {
out.push_str(&format!(" - {} -> {}\n", topic, agg));
}
}
if let Some(compute_units) = info.compute_units_limit {
out.push_str(&format!("\nCompute Units Limit: {}\n", compute_units));
}
if let Some(heap_size) = info.heap_size_limit {
out.push_str(&format!("Heap Size Limit: {}\n", heap_size));
}
out
}
impl Default for OracleInfo {
fn default() -> Self {
Self {
id: OracleId::default(),
description: String::new(),
update_frequency: UpdateFrequency::default(),
target_oracles: Vec::new(),
starting_round: StartingRound::default(),
is_active: false,
created_at: 0,
aggregators: Vec::new(),
validators_per_duty: default_validators_per_duty(),
retry_config: RetryConfig::default(),
oracle_request_delay: default_oracle_request_delay(),
compute_units_limit: None,
heap_size_limit: None,
}
}
}
impl OracleInfo {
pub fn is_asap(&self) -> bool {
matches!(self.starting_round, StartingRound::Asap)
}
pub fn target_commit(&self) -> Option<u64> {
match self.starting_round {
StartingRound::Round(round) => Some(round as u64),
StartingRound::Asap => None,
}
}
pub fn validate(&self) -> Result<(), String> {
match self.starting_round {
StartingRound::Asap => {
if !matches!(self.update_frequency, UpdateFrequency::OneShot) {
return Err("ASAP oracles cannot be periodic".to_string());
}
}
StartingRound::Round(starting_round) => {
match self.update_frequency {
UpdateFrequency::OneShot => {}
UpdateFrequency::Periodic(period)
| UpdateFrequency::LimitedPeriodic(period, _) => {
if period == 0 {
return Err("update frequency cannot be zero".to_string());
}
if let UpdateFrequency::LimitedPeriodic(_, end_round) =
self.update_frequency
{
if starting_round >= end_round {
return Err("end_round of a LimitedPeriodic oracle should be above starting_round".to_string());
}
}
}
}
}
}
if self.target_oracles.is_empty() {
return Err("OracleTargets cannot be empty".to_string());
}
if self.retry_config.retry_delay() == 0 {
return Err("Retry delay cannot be zero".to_string());
}
if self.oracle_request_delay < OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY {
return Err(format!(
"oracle_request_delay cannot be below {}",
OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY
));
}
if self.oracle_request_delay > OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY {
return Err(format!(
"oracle_request_delay cannot be above {}",
OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY
));
}
if self.validators_per_duty == 0 {
return Err("validators_per_duty cannot be 0".to_string());
}
let max_oracle_output_size = max_oracle_output_serialized_bytes(self.validators_per_duty);
if max_oracle_output_size < MIN_VIABLE_LIMIT_OF_ORACLE_OUTPUT_SIZE {
return Err(format!("validators_per_duty is too high, results in max size of oracle updates that is too low: {max_oracle_output_size} vs {MIN_VIABLE_LIMIT_OF_ORACLE_OUTPUT_SIZE}"));
}
if let Some(compute_units_limit) = self.compute_units_limit {
if compute_units_limit == 0 {
return Err("compute_usage_limit cannot be Some(0)".to_string());
}
if compute_units_limit > MAX_COMPUTE_UNIT_LIMIT {
return Err(format!("compute_usage_limit cannot be above MAX_COMPUTE_UNIT_LIMIT={MAX_COMPUTE_UNIT_LIMIT}"));
}
}
if let Some(heap_size_limit) = self.heap_size_limit {
if heap_size_limit == 0 {
return Err("heap_size_limit cannot be Some(0)".to_string());
}
if heap_size_limit > MAX_HEAP_FRAME_BYTES {
return Err(format!(
"heap_size_limit cannot be above MAX_HEAP_FRAME_BYTES={MAX_HEAP_FRAME_BYTES}"
));
}
}
Ok(())
}
}
fn default_validators_per_duty() -> u32 {
OracleDutyConfig::DEFAULT_VALIDATORS_PER_DUTY
}
fn default_oracle_request_delay() -> u32 {
OracleDutyConfig::DEFAULT_ORACLE_REQUEST_DELAY
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct OracleEntry {
oracle_info: Arc<OracleInfo>,
data_hash: [u8; OracleEntry::HASH_LENGTH],
last_modified_round: u64,
}
impl OracleEntry {
const HASH_LENGTH: usize = 32;
pub fn new(
oracle_info: OracleInfo,
data_hash: [u8; Self::HASH_LENGTH],
last_modified_round: u64,
) -> Self {
Self {
oracle_info: Arc::new(oracle_info),
data_hash,
last_modified_round,
}
}
pub fn oracle_info(&self) -> Arc<OracleInfo> {
self.oracle_info.clone()
}
pub fn last_modified_round(&self) -> u64 {
self.last_modified_round
}
pub fn data_hash(&self) -> &[u8; Self::HASH_LENGTH] {
&self.data_hash
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum OracleValue {
Plain(String),
Encrypted(String),
}
impl Default for OracleValue {
fn default() -> Self {
OracleValue::Plain(String::new())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OracleUrl(OracleValue);
impl fmt::Display for OracleUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
OracleValue::Plain(ref s) => write!(f, "{}", s),
OracleValue::Encrypted(ref s) => write!(f, "enc://{}", s),
}
}
}
impl Deref for OracleUrl {
type Target = OracleValue;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(feature = "non-pdk")]
impl From<Url> for OracleUrl {
fn from(url: Url) -> Self {
url.to_string().into()
}
}
#[cfg(feature = "non-pdk")]
impl From<&Url> for OracleUrl {
fn from(url: &Url) -> Self {
Self(OracleValue::Plain(url.to_string()))
}
}
impl From<String> for OracleUrl {
fn from(url: String) -> Self {
url.as_str().into()
}
}
impl From<&str> for OracleUrl {
fn from(s: &str) -> Self {
if let Some(encrypted) = s.strip_prefix("enc://") {
Self(OracleValue::Encrypted(encrypted.into()))
} else {
Self(OracleValue::Plain(s.into()))
}
}
}
impl FromStr for OracleUrl {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum OracleValueBody {
Plain(Vec<u8>),
Encrypted(Vec<u8>),
}
impl Default for OracleValueBody {
fn default() -> Self {
OracleValueBody::Plain(vec![])
}
}
impl FromStr for OracleValueBody {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(OracleValueBody::Plain(s.as_bytes().to_vec()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, strum_macros::AsRefStr)]
#[cfg_attr(feature = "non-pdk", derive(Subcommand))]
pub enum TargetOracle {
HttpGet {
#[cfg_attr(feature = "non-pdk", clap(long = "target-url", value_parser = clap::value_parser!(OracleUrl)))]
url: OracleUrl,
#[cfg_attr(feature = "non-pdk", clap(long, default_value = None))]
filter: Option<Vec<HttpFilter>>,
#[cfg_attr(feature = "non-pdk", clap(long, default_value_t = Headers::default()))]
headers: Headers,
},
HttpPost {
#[cfg_attr(feature = "non-pdk", clap(long = "target-url", value_parser = clap::value_parser!(OracleUrl)))]
url: OracleUrl,
#[cfg_attr(feature = "non-pdk", clap(long))]
filter: Option<Vec<HttpFilter>>,
#[cfg_attr(feature = "non-pdk", clap(long, value_parser = clap::value_parser!(OracleValueBody)))]
body: OracleValueBody,
#[cfg_attr(feature = "non-pdk", clap(long))]
content_type: String,
#[cfg_attr(feature = "non-pdk", clap(long, default_value_t = Headers::default()))]
headers: Headers,
},
Time,
PriceReactor,
Number,
SecretKeyGeneration {
#[cfg_attr(feature = "non-pdk", clap(long))]
committee_id: String,
#[cfg_attr(feature = "non-pdk", clap(long))]
committee_members: Vec<String>,
},
SecretKeyEncryption {
#[cfg_attr(feature = "non-pdk", clap(long))]
target_tee_id: String,
#[cfg_attr(feature = "non-pdk", clap(long))]
secret_data: Vec<u8>,
#[cfg_attr(feature = "non-pdk", clap(long))]
committee_id: String,
},
SecretKeyDecryption {
#[cfg_attr(feature = "non-pdk", clap(long))]
encrypted_data: Vec<u8>,
#[cfg_attr(feature = "non-pdk", clap(long))]
source_committee_id: String,
},
Stonks,
}
impl FromStr for TargetOracle {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "Time" {
Ok(TargetOracle::Time)
} else if s == "PriceReactor" {
Ok(TargetOracle::PriceReactor)
} else if s == "SecretKeyGeneration" {
Err("SecretKeyGeneration oracle requires committee_id and committee_members parameters. Use the appropriate API to create this oracle type.".to_string())
} else if s == "SecretKeyEncryption" {
Err("SecretKeyEncryption oracle requires target_tee_id, secret_data, and committee_id parameters. Use the appropriate API to create this oracle type.".to_string())
} else if s == "SecretKeyDecryption" {
Err("SecretKeyDecryption oracle requires encrypted_data and source_committee_id parameters. Use the appropriate API to create this oracle type.".to_string())
} else if s == "number" {
Err("The 'number' oracle is only for testing purposes and should not be used in production.".to_string())
} else {
if let Some(rest) = s.strip_prefix("HttpGet:") {
let parts: Vec<&str> = rest.splitn(2, '|').collect();
if parts.is_empty() {
return Err(
"Invalid HttpGet format. Use 'HttpGet:<url>[|<filter>]'.".to_string()
);
}
let url = parts[0].to_string();
let filter = if parts.len() > 1 && !parts[1].is_empty() {
Some(vec![HttpFilter::from_str(parts[1])?])
} else {
None
};
#[cfg(feature = "non-pdk")]
if Url::parse(&url).is_err() {
return Err(format!("Invalid URL: {url}"));
}
return Ok(TargetOracle::HttpGet {
url: url.into(),
filter,
headers: Headers::default(),
});
}
Err(format!("Unknown TargetOracle type: {s}"))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OracleUpdateResult {
pub oracle_id: OracleId,
pub target_commit: u64,
#[serde(with = "BigArray")]
pub response_hash: [u8; 32],
#[serde(with = "BigArray")]
pub signature: [u8; 64],
pub oracle_result: Vec<u8>,
pub attestation_report: Option<AttestationReport>,
}
impl OracleUpdateResult {
pub fn new(
oracle_id: OracleId,
target_commit: u64,
oracle_result: Vec<u8>,
signature: [u8; 64],
attestation_report: Option<AttestationReport>,
) -> Result<Self, &'static str> {
let hash = blake3::hash(&oracle_result);
#[cfg(feature = "non-pdk")]
let oracle_result = if oracle_result.len() > rialo_limits::MAX_TRANSACTION_SIZE as usize {
tracing::error!(
"Oracle result size {} exceeds maximum size {}, dropping the result.",
oracle_result.len(),
rialo_limits::MAX_TRANSACTION_SIZE as usize
);
return Err("Oracle result exceeds maximum size");
} else {
oracle_result
};
#[cfg(not(feature = "non-pdk"))]
let oracle_result = oracle_result;
Ok(Self {
oracle_id,
target_commit,
response_hash: *hash.as_bytes(),
signature,
oracle_result,
attestation_report,
})
}
}
impl Default for OracleUpdateResult {
fn default() -> Self {
Self {
oracle_id: OracleId::default(),
target_commit: 0,
response_hash: [0; 32],
signature: [0; 64],
oracle_result: vec![],
attestation_report: None,
}
}
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct OracleRequest {
pub params: BTreeMap<String, String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UpdateFrequency {
#[default]
OneShot,
Periodic(u32),
LimitedPeriodic(u32, u32),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
pub enum StartingRound {
Round(u32),
Asap,
}
impl Default for StartingRound {
fn default() -> Self {
StartingRound::Round(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base_valid_oracle_info() -> OracleInfo {
OracleInfo {
description: "test".to_string(),
target_oracles: vec![TargetOracle::Time],
update_frequency: UpdateFrequency::OneShot,
starting_round: StartingRound::Round(0),
retry_config: RetryConfig::default(),
..OracleInfo::default()
}
}
#[test]
fn test_is_asap_true_and_false() {
let mut info = base_valid_oracle_info();
assert!(!info.is_asap());
info.starting_round = StartingRound::Asap;
assert!(info.is_asap());
}
#[test]
fn test_validate_success_minimal() {
let info = base_valid_oracle_info();
assert!(info.validate().is_ok());
}
#[test]
fn test_asap_cannot_be_periodic() {
let mut info = base_valid_oracle_info();
info.starting_round = StartingRound::Asap;
info.update_frequency = UpdateFrequency::Periodic(10);
let err = info.validate().unwrap_err();
assert!(err.contains("ASAP oracles cannot be periodic"));
}
#[test]
fn test_asap_cannot_be_limited_periodic() {
let mut info = base_valid_oracle_info();
info.starting_round = StartingRound::Asap;
info.update_frequency = UpdateFrequency::LimitedPeriodic(5, 100);
let err = info.validate().unwrap_err();
assert!(err.contains("ASAP oracles cannot be periodic"));
}
#[test]
fn test_periodic_with_zero_period_is_invalid() {
let mut info = base_valid_oracle_info();
info.starting_round = StartingRound::Round(1);
info.update_frequency = UpdateFrequency::Periodic(0);
let err = info.validate().unwrap_err();
assert!(err.contains("update frequency cannot be zero"));
}
#[test]
fn test_limited_periodic_with_zero_period_is_invalid() {
let mut info = base_valid_oracle_info();
info.starting_round = StartingRound::Round(1);
info.update_frequency = UpdateFrequency::LimitedPeriodic(0, 100);
let err = info.validate().unwrap_err();
assert!(err.contains("update frequency cannot be zero"));
}
#[test]
fn test_limited_periodic_end_round_must_be_above_starting_round() {
let mut info = base_valid_oracle_info();
info.starting_round = StartingRound::Round(5);
info.update_frequency = UpdateFrequency::LimitedPeriodic(3, 5);
let err = info.validate().unwrap_err();
assert!(
err.contains("end_round of a LimitedPeriodic oracle should be above starting_round")
);
}
#[test]
fn test_limited_periodic_end_round_below_starting_round_is_invalid() {
let mut info = base_valid_oracle_info();
info.starting_round = StartingRound::Round(10);
info.update_frequency = UpdateFrequency::LimitedPeriodic(3, 9);
let err = info.validate().unwrap_err();
assert!(
err.contains("end_round of a LimitedPeriodic oracle should be above starting_round")
);
}
#[test]
fn test_target_oracles_cannot_be_empty() {
let mut info = base_valid_oracle_info();
info.target_oracles.clear();
let err = info.validate().unwrap_err();
assert!(err.contains("OracleTargets cannot be empty"));
}
#[test]
fn test_retry_delay_cannot_be_zero() {
let mut info = base_valid_oracle_info();
info.retry_config = RetryConfig::new(0, 0);
let err = info.validate().unwrap_err();
assert!(err.contains("Retry delay cannot be zero"));
}
#[test]
fn test_oracle_request_delay_bounds() {
let mut info = base_valid_oracle_info();
info.oracle_request_delay = OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY - 1;
let err = info.validate().unwrap_err();
assert!(err.contains(&format!(
"oracle_request_delay cannot be below {}",
OracleDutyConfig::MIN_ORACLE_REQUEST_DELAY
)));
let mut info = base_valid_oracle_info();
info.oracle_request_delay = OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY + 1;
let err = info.validate().unwrap_err();
assert!(err.contains(&format!(
"oracle_request_delay cannot be above {}",
OracleDutyConfig::MAX_ORACLE_REQUEST_DELAY
)));
}
#[test]
fn test_validators_per_duty_cannot_be_zero() {
let mut info = base_valid_oracle_info();
info.validators_per_duty = 0;
let err = info.validate().unwrap_err();
assert!(err.contains("validators_per_duty cannot be 0"));
}
#[test]
fn test_validators_per_duty_too_high_results_in_too_low_output_size() {
let mut info = base_valid_oracle_info();
info.validators_per_duty = 1_000_000; let err = info.validate().unwrap_err();
assert!(err.contains("validators_per_duty is too high"));
}
#[test]
fn test_compute_units_limit_checks() {
let mut info = base_valid_oracle_info();
info.compute_units_limit = Some(0);
let err = info.validate().unwrap_err();
assert!(err.contains("compute_usage_limit cannot be Some(0)"));
let mut info = base_valid_oracle_info();
info.compute_units_limit =
Some(rialo_s_compute_budget::compute_budget_limits::MAX_COMPUTE_UNIT_LIMIT + 1);
let err = info.validate().unwrap_err();
assert!(err.contains(&format!(
"compute_usage_limit cannot be above MAX_COMPUTE_UNIT_LIMIT={}",
rialo_s_compute_budget::compute_budget_limits::MAX_COMPUTE_UNIT_LIMIT
)));
}
#[test]
fn test_heap_size_limit_checks() {
let mut info = base_valid_oracle_info();
info.heap_size_limit = Some(0);
let err = info.validate().unwrap_err();
assert!(err.contains("heap_size_limit cannot be Some(0)"));
let mut info = base_valid_oracle_info();
info.heap_size_limit =
Some(rialo_s_compute_budget::compute_budget_limits::MAX_HEAP_FRAME_BYTES + 1);
let err = info.validate().unwrap_err();
assert!(err.contains(&format!(
"heap_size_limit cannot be above MAX_HEAP_FRAME_BYTES={}",
rialo_s_compute_budget::compute_budget_limits::MAX_HEAP_FRAME_BYTES
)));
}
}