use std::{
collections::BTreeMap,
convert::Infallible,
fmt,
ops::Deref,
str::FromStr,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use borsh::{BorshDeserialize, BorshSerialize};
#[cfg(feature = "non-pdk")]
use clap::Subcommand;
#[cfg(feature = "non-pdk")]
use fastcrypto::encoding::{Base64, Encoding};
use rialo_cli_representable::Representable;
use rialo_limits::{max_rex_output_serialized_bytes, MIN_VIABLE_LIMIT_OF_REX_OUTPUT_SIZE};
use rialo_s_pubkey::Pubkey;
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
#[cfg(feature = "non-pdk")]
use url::Url;
use crate::{
websocket_op::WebSocketOperation, AttestationReport, AuthorityKeyBytes, Headers, HttpFilter,
Nonce, RexDutyConfig,
};
pub type TimestampMs = u64;
const MIN_UPDATE_PERIOD_MS: TimestampMs = 50;
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
BorshSerialize,
BorshDeserialize,
)]
pub struct RexId {
pub nonce: Nonce,
pub creator: Pubkey,
}
impl RexId {
pub fn new(creator: Pubkey, nonce: impl Into<Nonce>) -> Self {
Self {
nonce: nonce.into(),
creator,
}
}
}
impl fmt::Display for RexId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", &self.nonce, &self.creator)
}
}
impl FromStr for RexId {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s).map_err(|e| format!("Failed to parse RexId: {}", e))
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Representable)]
#[representable(human_readable = "rex_info_human_readable")]
pub struct RexInfo {
pub id: RexId,
pub description: String,
pub update_frequency: UpdateFrequency,
pub target_rex_programs: Vec<TargetRexProgram>,
pub starting_timestamp: StartingTimestamp,
pub is_active: bool,
pub created_at_ms: i64,
#[serde(default = "default_validators_per_duty")]
pub validators_per_duty: u32,
#[serde(default = "default_rex_request_delay_ms")]
pub request_delay_ms: TimestampMs,
}
fn rex_info_human_readable(info: &RexInfo) -> String {
let mut out = String::new();
out.push_str(&format!("REX 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 Timestamp: {:?}\n",
info.starting_timestamp
));
out.push_str(&format!("Update Frequency: {:?}\n", info.update_frequency));
out.push_str(&format!("Created At: {}\n", info.created_at_ms));
out.push_str(&format!(
"Validators Per Duty: {}\n",
info.validators_per_duty
));
out.push_str(&format!("REX Request Delay: {}\n", info.request_delay_ms));
if !info.target_rex_programs.is_empty() {
out.push_str(&format!(
"\nTarget REX operations ({}):\n",
info.target_rex_programs.len()
));
for (i, target) in info.target_rex_programs.iter().enumerate() {
out.push_str(&format!(" {}. {:?}\n", i + 1, target));
}
}
out
}
impl Default for RexInfo {
fn default() -> Self {
Self {
id: RexId::default(),
description: String::new(),
update_frequency: UpdateFrequency::default(),
target_rex_programs: Vec::new(),
starting_timestamp: StartingTimestamp::default(),
is_active: false,
created_at_ms: 0,
validators_per_duty: default_validators_per_duty(),
request_delay_ms: default_rex_request_delay_ms(),
}
}
}
impl RexInfo {
pub fn is_asap(&self) -> bool {
matches!(self.starting_timestamp, StartingTimestamp::Asap)
}
pub fn target_timestamp(&self) -> Option<TimestampMs> {
match self.starting_timestamp {
StartingTimestamp::Timestamp(timestamp) => Some(timestamp),
StartingTimestamp::Asap => None,
}
}
pub fn requires_dkg_decryption(&self) -> bool {
self.target_rex_programs
.iter()
.any(TargetRexProgram::is_dkg_encrypted)
}
pub fn validate(&self) -> Result<(), String> {
match self.starting_timestamp {
StartingTimestamp::Asap => {
if !matches!(self.update_frequency, UpdateFrequency::OneShot) {
return Err("ASAP REX requests cannot be periodic".to_string());
}
}
StartingTimestamp::Timestamp(starting_timestamp) => {
match self.update_frequency {
UpdateFrequency::OneShot => {}
UpdateFrequency::Periodic(period)
| UpdateFrequency::LimitedPeriodic(period, _) => {
validate_periodic_frequency(period)?;
if let UpdateFrequency::LimitedPeriodic(_, end_timestamp) =
self.update_frequency
{
if starting_timestamp >= end_timestamp {
return Err("end_timestamp of a LimitedPeriodic REX should be above starting_timestamp".to_string());
}
}
}
}
}
}
if self.target_rex_programs.is_empty() {
return Err("RexTargets cannot be empty".to_string());
}
if self.request_delay_ms < RexDutyConfig::MIN_REX_REQUEST_DELAY {
return Err(format!(
"rex_request_delay cannot be below {}",
RexDutyConfig::MIN_REX_REQUEST_DELAY
));
}
if self.request_delay_ms > RexDutyConfig::MAX_REX_REQUEST_DELAY_MS {
return Err(format!(
"rex_request_delay cannot be above {}",
RexDutyConfig::MAX_REX_REQUEST_DELAY_MS
));
}
if self.validators_per_duty == 0 {
return Err("validators_per_duty cannot be 0".to_string());
}
let max_rex_output_size = max_rex_output_serialized_bytes(self.validators_per_duty);
if max_rex_output_size < MIN_VIABLE_LIMIT_OF_REX_OUTPUT_SIZE {
return Err(format!("validators_per_duty is too high, results in max size of REX updates that is too low: {max_rex_output_size} vs {MIN_VIABLE_LIMIT_OF_REX_OUTPUT_SIZE}"));
}
Ok(())
}
pub fn websocket_op(&self) -> Option<WebSocketOperation> {
self.target_rex_programs
.first()
.and_then(|target| target.websocket_op())
}
}
fn validate_periodic_frequency(period_ms: TimestampMs) -> Result<(), String> {
if period_ms == 0 {
return Err("update frequency cannot be zero".to_string());
}
if period_ms < MIN_UPDATE_PERIOD_MS {
return Err(format!(
"update frequency {period_ms} cannot be below {MIN_UPDATE_PERIOD_MS}"
));
}
Ok(())
}
impl TargetRexProgram {
pub fn websocket_op(&self) -> Option<WebSocketOperation> {
if let TargetRexProgram::WebSocket(ws_op) = self {
Some(ws_op.clone())
} else {
None
}
}
pub fn encrypted_payloads(&self) -> Box<dyn Iterator<Item = &[u8]> + '_> {
fn enc(v: &RexValue) -> Option<&[u8]> {
v.is_encrypted().then(|| v.as_bytes())
}
match self {
TargetRexProgram::HttpGet { url, .. } if url.is_encrypted() => {
Box::new(std::iter::once(url.as_bytes()))
}
TargetRexProgram::HttpGet { .. } => Box::new(std::iter::empty()),
TargetRexProgram::HttpPost { body, .. } => Box::new(enc(body).into_iter()),
TargetRexProgram::WebSocket(WebSocketOperation::Send { messages, .. }) => {
Box::new(messages.iter().filter_map(enc))
}
TargetRexProgram::WebSocket(_) => Box::new(std::iter::empty()),
TargetRexProgram::Wasm { input, .. } => Box::new(input.iter().filter_map(enc)),
TargetRexProgram::Time
| TargetRexProgram::Number
| TargetRexProgram::SecretKeyGeneration { .. }
| TargetRexProgram::SecretKeyEncryption { .. }
| TargetRexProgram::SecretKeyDecryption { .. } => Box::new(std::iter::empty()),
}
}
pub fn is_dkg_encrypted(&self) -> bool {
self.encrypted_payloads().next().is_some()
}
}
fn default_validators_per_duty() -> u32 {
RexDutyConfig::DEFAULT_VALIDATORS_PER_DUTY
}
fn default_rex_request_delay_ms() -> TimestampMs {
RexDutyConfig::DEFAULT_REQUEST_DELAY_MS
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct RexEntry {
rex_info: Arc<RexInfo>,
data_hash: [u8; RexEntry::HASH_LENGTH],
last_modified_timestamp: u64,
}
impl RexEntry {
const HASH_LENGTH: usize = 32;
pub fn new(
rex_info: RexInfo,
data_hash: [u8; Self::HASH_LENGTH],
last_modified_round: u64,
) -> Self {
Self {
rex_info: Arc::new(rex_info),
data_hash,
last_modified_timestamp: last_modified_round,
}
}
pub fn rex_info(&self) -> Arc<RexInfo> {
self.rex_info.clone()
}
pub fn last_modified_timestamp(&self) -> u64 {
self.last_modified_timestamp
}
pub fn data_hash(&self) -> &[u8; Self::HASH_LENGTH] {
&self.data_hash
}
}
fn deserialize_bytes_or_string<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct BytesOrString;
impl<'de> serde::de::Visitor<'de> for BytesOrString {
type Value = Vec<u8>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a byte array, byte slice, or a string")
}
fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Vec<u8>, E> {
Ok(bytes.to_vec())
}
fn visit_byte_buf<E: serde::de::Error>(self, bytes: Vec<u8>) -> Result<Vec<u8>, E> {
Ok(bytes)
}
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Vec<u8>, E> {
Ok(s.as_bytes().to_vec())
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<u8>, A::Error> {
let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(b) = seq.next_element()? {
bytes.push(b);
}
Ok(bytes)
}
}
deserializer.deserialize_bytes(BytesOrString)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum RexValue {
Plain(#[serde(deserialize_with = "deserialize_bytes_or_string")] Vec<u8>),
Encrypted(#[serde(deserialize_with = "deserialize_bytes_or_string")] Vec<u8>),
}
impl RexValue {
pub fn plain(data: Vec<u8>) -> Self {
RexValue::Plain(data)
}
pub fn plain_string(s: impl Into<String>) -> Self {
RexValue::Plain(s.into().into_bytes())
}
pub fn encrypted(ciphertext: Vec<u8>) -> Self {
RexValue::Encrypted(ciphertext)
}
pub fn as_string(&self) -> Option<&str> {
match self {
RexValue::Plain(bytes) => std::str::from_utf8(bytes).ok(),
RexValue::Encrypted(_) => None,
}
}
pub fn as_bytes(&self) -> &[u8] {
match self {
RexValue::Plain(bytes) | RexValue::Encrypted(bytes) => bytes,
}
}
pub fn is_plain(&self) -> bool {
matches!(self, RexValue::Plain(_))
}
pub fn is_encrypted(&self) -> bool {
matches!(self, RexValue::Encrypted(_))
}
}
impl Default for RexValue {
fn default() -> Self {
RexValue::Plain(vec![])
}
}
pub trait IntoRexValueFor<T> {
fn into_rex_value_for(self) -> RexValue;
}
impl<T: BorshSerialize> IntoRexValueFor<T> for T {
fn into_rex_value_for(self) -> RexValue {
RexValue::Plain(borsh::to_vec(&self).expect("borsh serialize failed"))
}
}
impl<T> IntoRexValueFor<T> for EncryptedInput<T> {
fn into_rex_value_for(self) -> RexValue {
RexValue::Encrypted(self.into_bytes())
}
}
impl FromStr for RexValue {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(RexValue::Plain(s.as_bytes().to_vec()))
}
}
#[deprecated(since = "0.2.0", note = "Use RexValue instead")]
pub type RexValueBody = RexValue;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct RexUrl(RexValue);
impl fmt::Display for RexUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
RexValue::Plain(bytes) => {
match std::str::from_utf8(bytes) {
Ok(s) => write!(f, "{}", s),
#[cfg(feature = "non-pdk")]
Err(_) => write!(f, "<binary:{}>", Base64::encode(bytes)),
#[cfg(not(feature = "non-pdk"))]
Err(_) => write!(f, "<binary:{}>", hex::encode(bytes)),
}
}
RexValue::Encrypted(bytes) => {
match std::str::from_utf8(bytes) {
Ok(s) => write!(f, "enc://{}", s),
#[cfg(feature = "non-pdk")]
Err(_) => write!(f, "enc://<binary:{}>", Base64::encode(bytes)),
#[cfg(not(feature = "non-pdk"))]
Err(_) => write!(f, "enc://<binary:{}>", hex::encode(bytes)),
}
}
}
}
}
impl Deref for RexUrl {
type Target = RexValue;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(feature = "non-pdk")]
impl From<Url> for RexUrl {
fn from(url: Url) -> Self {
url.to_string().into()
}
}
#[cfg(feature = "non-pdk")]
impl From<&Url> for RexUrl {
fn from(url: &Url) -> Self {
Self(RexValue::Plain(url.to_string().into_bytes()))
}
}
impl From<String> for RexUrl {
fn from(url: String) -> Self {
url.as_str().into()
}
}
impl From<&str> for RexUrl {
fn from(s: &str) -> Self {
if let Some(encrypted) = s.strip_prefix("enc://") {
Self(RexValue::Encrypted(encrypted.into()))
} else {
Self(RexValue::Plain(s.into()))
}
}
}
impl FromStr for RexUrl {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.into())
}
}
impl From<RexValue> for RexUrl {
fn from(value: RexValue) -> Self {
Self(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, strum_macros::AsRefStr)]
#[cfg_attr(feature = "non-pdk", derive(Subcommand))]
#[repr(u16)]
#[non_exhaustive]
pub enum TargetRexProgram {
HttpGet {
#[cfg_attr(feature = "non-pdk", clap(
long = "target-url",
value_parser = clap::value_parser!(RexUrl)
))]
url: RexUrl,
#[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(),
action = clap::ArgAction::Append
))]
headers: Headers,
} = 0,
HttpPost {
#[cfg_attr(feature = "non-pdk", clap(
long = "target-url",
value_parser = clap::value_parser!(RexUrl)
))]
url: RexUrl,
#[cfg_attr(feature = "non-pdk", clap(long))]
filter: Option<Vec<HttpFilter>>,
#[cfg_attr(feature = "non-pdk", clap(
long,
value_parser = clap::value_parser!(RexValue)
))]
body: RexValue,
#[cfg_attr(feature = "non-pdk", clap(long))]
content_type: String,
#[cfg_attr(feature = "non-pdk", clap(
long,
default_value_t = Headers::default(),
action = clap::ArgAction::Append
))]
headers: Headers,
} = 1,
Time = 2,
Number = 3,
SecretKeyGeneration {
#[cfg_attr(feature = "non-pdk", clap(long))]
committee_id: String,
#[cfg_attr(feature = "non-pdk", clap(long))]
committee_members: Vec<String>,
} = 4,
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,
} = 5,
SecretKeyDecryption {
#[cfg_attr(feature = "non-pdk", clap(long))]
encrypted_data: Vec<u8>,
#[cfg_attr(feature = "non-pdk", clap(long))]
source_committee_id: String,
} = 6,
#[cfg_attr(feature = "non-pdk", clap(subcommand))]
WebSocket(WebSocketOperation) = 7,
Wasm {
#[cfg_attr(feature = "non-pdk", clap(long))]
bytecode_account: Pubkey,
#[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_base64_rex_value))]
input: Vec<RexValue>,
#[cfg_attr(feature = "non-pdk", clap(long))]
program_index: Option<u32>,
} = 8,
}
pub struct EncryptedInput<T = ()> {
ciphertext: Vec<u8>,
_phantom: std::marker::PhantomData<T>,
}
impl<T> Clone for EncryptedInput<T> {
fn clone(&self) -> Self {
Self {
ciphertext: self.ciphertext.clone(),
_phantom: std::marker::PhantomData,
}
}
}
impl<T> Default for EncryptedInput<T> {
fn default() -> Self {
Self {
ciphertext: Vec::new(),
_phantom: std::marker::PhantomData,
}
}
}
impl<T> std::fmt::Debug for EncryptedInput<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("EncryptedInput")
.field(&format!("[{} bytes]", self.ciphertext.len()))
.finish()
}
}
impl<T> PartialEq for EncryptedInput<T> {
fn eq(&self, other: &Self) -> bool {
self.ciphertext == other.ciphertext
}
}
impl<T> Eq for EncryptedInput<T> {}
impl<T> BorshSerialize for EncryptedInput<T> {
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
borsh::BorshSerialize::serialize(&self.ciphertext, writer)
}
}
impl<T> BorshDeserialize for EncryptedInput<T> {
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
Ok(Self {
ciphertext: borsh::BorshDeserialize::deserialize_reader(reader)?,
_phantom: std::marker::PhantomData,
})
}
}
impl<T> Serialize for EncryptedInput<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serde::Serialize::serialize(&self.ciphertext, serializer)
}
}
impl<'de, T> Deserialize<'de> for EncryptedInput<T> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self {
ciphertext: <Vec<u8> as serde::Deserialize>::deserialize(deserializer)?,
_phantom: std::marker::PhantomData,
})
}
}
impl<T> EncryptedInput<T> {
pub fn new(ciphertext: Vec<u8>) -> Self {
Self {
ciphertext,
_phantom: std::marker::PhantomData,
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.ciphertext
}
pub fn into_bytes(self) -> Vec<u8> {
self.ciphertext
}
}
impl<T> From<Vec<u8>> for EncryptedInput<T> {
fn from(v: Vec<u8>) -> Self {
Self::new(v)
}
}
#[cfg(feature = "non-pdk")]
fn parse_base64_rex_value(s: &str) -> Result<RexValue, String> {
use fastcrypto::encoding::{Base64, Encoding};
Base64::decode(s)
.map(RexValue::Plain)
.map_err(|e| format!("Invalid base64 input: {e}"))
}
impl TargetRexProgram {
pub fn is_websocket(&self) -> bool {
matches!(self, TargetRexProgram::WebSocket(_))
}
}
impl FromStr for TargetRexProgram {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "Time" {
Ok(TargetRexProgram::Time)
} else if s == "SecretKeyGeneration" {
Err("SecretKeyGeneration REX requires committee_id and committee_members parameters. Use the appropriate API to create this REX type.".to_string())
} else if s == "SecretKeyEncryption" {
Err("SecretKeyEncryption REX requires target_tee_id, secret_data, and committee_id parameters. Use the appropriate API to create this REX type.".to_string())
} else if s == "SecretKeyDecryption" {
Err("SecretKeyDecryption REX requires encrypted_data and source_committee_id parameters. Use the appropriate API to create this REX type.".to_string())
} else if s == "number" {
Err("The 'number' REX 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(TargetRexProgram::HttpGet {
url: url.into(),
filter,
headers: Headers::default(),
});
}
Err(format!("Unknown TargetRexProgram type: {s}"))
}
}
}
pub type InputCommitmentBytes = [u8; 32];
pub type SignatureBytes = [u8; 64];
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RexUpdateResult {
pub rex_id: RexId,
pub target_timestamp: TimestampMs,
#[serde(with = "BigArray")]
pub response_hash: [u8; 32],
#[serde(with = "BigArray")]
pub input_commitment: InputCommitmentBytes,
#[serde(with = "BigArray")]
pub signature: SignatureBytes,
pub rex_result: Vec<u8>,
pub attestation_report: Option<AttestationReport>,
#[serde(with = "BigArray")]
pub authority_key: AuthorityKeyBytes,
}
impl RexUpdateResult {
pub fn new(
rex_id: RexId,
target_timestamp: TimestampMs,
rex_result: Vec<u8>,
input_commitment: InputCommitmentBytes,
signature: SignatureBytes,
attestation_report: Option<AttestationReport>,
authority_key: AuthorityKeyBytes,
) -> Result<Self, &'static str> {
let hash = blake3::hash(&rex_result);
#[cfg(feature = "non-pdk")]
let rex_result = if rex_result.len() > rialo_limits::MAX_TRANSACTION_SIZE as usize {
tracing::error!(
"REX result size {} exceeds maximum size {}, dropping the result.",
rex_result.len(),
rialo_limits::MAX_TRANSACTION_SIZE
);
return Err("REX result exceeds maximum size");
} else {
rex_result
};
Ok(Self {
rex_id,
target_timestamp,
response_hash: *hash.as_bytes(),
input_commitment,
signature,
rex_result,
attestation_report,
authority_key,
})
}
}
impl Default for RexUpdateResult {
fn default() -> Self {
Self {
rex_id: RexId::default(),
target_timestamp: 0,
response_hash: [0; 32],
input_commitment: [0xee; 32],
signature: [0; 64],
rex_result: vec![],
attestation_report: None,
authority_key: [0xff; 96],
}
}
}
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)]
pub struct RexRequest {
pub rex_id: Option<RexId>,
pub target_timestamp: Option<TimestampMs>,
pub authority_key: AuthorityKeyBytes,
pub include_attestation: bool,
pub max_output_size: u32,
pub params: BTreeMap<String, String>,
}
impl Default for RexRequest {
fn default() -> Self {
Self {
rex_id: None,
target_timestamp: None,
authority_key: [0; 96],
include_attestation: true,
max_output_size: 0,
params: BTreeMap::default(),
}
}
}
impl RexRequest {
pub fn input_commitment(&self) -> Result<blake3::Hash, &'static str> {
let request_bytes = borsh::to_vec(self).map_err(|_| "Failed to serialize RexRequest")?;
Ok(blake3::hash(&request_bytes))
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UpdateFrequency {
#[default]
OneShot,
Periodic(TimestampMs),
LimitedPeriodic(TimestampMs, TimestampMs),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
pub enum StartingTimestamp {
Timestamp(TimestampMs),
Asap,
}
impl Default for StartingTimestamp {
fn default() -> Self {
StartingTimestamp::Timestamp(0)
}
}
impl UpdateFrequency {
pub fn periodic(duration: Duration) -> Self {
Self::Periodic(duration.as_millis() as TimestampMs)
}
}
impl StartingTimestamp {
pub fn start_offset(offset: Duration) -> Self {
let timestamp = SystemTime::now() + offset;
Self::Timestamp(timestamp.duration_since(UNIX_EPOCH).unwrap().as_millis() as TimestampMs)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn http_post(body: RexValue) -> TargetRexProgram {
TargetRexProgram::HttpPost {
url: "https://example.com".into(),
filter: None,
body,
content_type: "application/json".to_string(),
headers: Headers::default(),
}
}
fn ws_send(messages: Vec<RexValue>) -> TargetRexProgram {
TargetRexProgram::WebSocket(WebSocketOperation::Send {
connection_rex_id: RexId::default(),
messages,
})
}
#[test]
fn test_is_dkg_encrypted_false_for_plain_carriers() {
assert!(!TargetRexProgram::Time.is_dkg_encrypted());
assert!(!TargetRexProgram::HttpGet {
url: "https://example.com".into(),
filter: None,
headers: Headers::default(),
}
.is_dkg_encrypted());
assert!(!http_post(RexValue::plain(vec![1, 2, 3])).is_dkg_encrypted());
assert!(!ws_send(vec![RexValue::plain(vec![1])]).is_dkg_encrypted());
assert!(!TargetRexProgram::Wasm {
bytecode_account: Pubkey::new_unique(),
input: vec![RexValue::plain(vec![1]), RexValue::plain(vec![2])],
program_index: None,
}
.is_dkg_encrypted());
}
#[test]
fn test_encrypted_payloads_yields_ciphertext_from_every_carrier() {
let get = TargetRexProgram::HttpGet {
url: RexValue::Encrypted(vec![0x02, 1, 2]).into(),
filter: None,
headers: Headers::default(),
};
assert_eq!(
get.encrypted_payloads().collect::<Vec<_>>(),
vec![&[0x02, 1, 2][..]]
);
assert!(get.is_dkg_encrypted());
let post = http_post(RexValue::Encrypted(vec![0x02, 7]));
assert_eq!(
post.encrypted_payloads().collect::<Vec<_>>(),
vec![&[0x02, 7][..]]
);
assert!(post.is_dkg_encrypted());
let ws = ws_send(vec![
RexValue::plain(vec![9]),
RexValue::Encrypted(vec![0x02, 8]),
RexValue::Encrypted(vec![0x02, 5]),
]);
assert_eq!(
ws.encrypted_payloads().collect::<Vec<_>>(),
vec![&[0x02, 8][..], &[0x02, 5][..]]
);
assert!(ws.is_dkg_encrypted());
let wasm = TargetRexProgram::Wasm {
bytecode_account: Pubkey::new_unique(),
input: vec![RexValue::plain(vec![1]), RexValue::Encrypted(vec![0x02, 9])],
program_index: None,
};
assert_eq!(
wasm.encrypted_payloads().collect::<Vec<_>>(),
vec![&[0x02, 9][..]]
);
assert!(wasm.is_dkg_encrypted());
assert!(TargetRexProgram::Time.encrypted_payloads().next().is_none());
}
#[test]
fn test_requires_dkg_decryption_rolls_up_across_programs() {
let mut info = RexInfo {
target_rex_programs: vec![TargetRexProgram::Time],
..RexInfo::default()
};
assert!(!info.requires_dkg_decryption());
info.target_rex_programs
.push(http_post(RexValue::Encrypted(vec![0x02, 1])));
assert!(info.requires_dkg_decryption());
}
fn base_valid_rex_info() -> RexInfo {
RexInfo {
description: "test".to_string(),
target_rex_programs: vec![TargetRexProgram::Time],
update_frequency: UpdateFrequency::OneShot,
starting_timestamp: StartingTimestamp::Timestamp(0),
..RexInfo::default()
}
}
#[test]
fn test_is_asap_true_and_false() {
let mut info = base_valid_rex_info();
assert!(!info.is_asap());
info.starting_timestamp = StartingTimestamp::Asap;
assert!(info.is_asap());
}
#[test]
fn test_validate_success_minimal() {
let info = base_valid_rex_info();
assert!(info.validate().is_ok());
}
#[test]
fn test_asap_cannot_be_periodic() {
let mut info = base_valid_rex_info();
info.starting_timestamp = StartingTimestamp::Asap;
info.update_frequency = UpdateFrequency::Periodic(10);
let err = info.validate().unwrap_err();
assert!(err.contains("ASAP REX requests cannot be periodic"));
}
#[test]
fn test_asap_cannot_be_limited_periodic() {
let mut info = base_valid_rex_info();
info.starting_timestamp = StartingTimestamp::Asap;
info.update_frequency = UpdateFrequency::LimitedPeriodic(5, 100);
let err = info.validate().unwrap_err();
assert!(err.contains("ASAP REX requests cannot be periodic"));
}
#[test]
fn test_periodic_with_zero_period_is_invalid() {
let mut info = base_valid_rex_info();
info.starting_timestamp = StartingTimestamp::Timestamp(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_rex_info();
info.starting_timestamp = StartingTimestamp::Timestamp(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_timestamp_must_be_above_starting_timestamp() {
let mut info = base_valid_rex_info();
info.starting_timestamp = StartingTimestamp::Timestamp(500);
info.update_frequency = UpdateFrequency::LimitedPeriodic(300, 500);
let err = info.validate().unwrap_err();
assert!(err
.contains("end_timestamp of a LimitedPeriodic REX should be above starting_timestamp"));
}
#[test]
fn test_limited_periodic_end_timestamp_below_starting_timestamp_is_invalid() {
let mut info = base_valid_rex_info();
info.starting_timestamp = StartingTimestamp::Timestamp(1000);
info.update_frequency = UpdateFrequency::LimitedPeriodic(300, 900);
let err = info.validate().unwrap_err();
assert!(err
.contains("end_timestamp of a LimitedPeriodic REX should be above starting_timestamp"));
}
#[test]
fn test_target_rex_programs_cannot_be_empty() {
let mut info = base_valid_rex_info();
info.target_rex_programs.clear();
let err = info.validate().unwrap_err();
assert!(err.contains("RexTargets cannot be empty"));
}
#[test]
fn test_rex_request_delay_bounds() {
let mut info = base_valid_rex_info();
info.request_delay_ms = RexDutyConfig::MIN_REX_REQUEST_DELAY - 1;
let err = info.validate().unwrap_err();
assert!(err.contains(&format!(
"rex_request_delay cannot be below {}",
RexDutyConfig::MIN_REX_REQUEST_DELAY
)));
let mut info = base_valid_rex_info();
info.request_delay_ms = RexDutyConfig::MAX_REX_REQUEST_DELAY_MS + 1;
let err = info.validate().unwrap_err();
assert!(err.contains(&format!(
"rex_request_delay cannot be above {}",
RexDutyConfig::MAX_REX_REQUEST_DELAY_MS
)));
}
#[test]
fn test_validators_per_duty_cannot_be_zero() {
let mut info = base_valid_rex_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_rex_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_rex_value_plain_deserialize_from_byte_array() {
let json = r#"{"Plain":[104,101,108,108,111]}"#;
let value: RexValue = serde_json::from_str(json).unwrap();
assert_eq!(value, RexValue::Plain(b"hello".to_vec()));
}
#[test]
fn test_rex_value_plain_deserialize_from_string() {
let json = r#"{"Plain":"hello"}"#;
let value: RexValue = serde_json::from_str(json).unwrap();
assert_eq!(value, RexValue::Plain(b"hello".to_vec()));
}
#[test]
fn test_rex_value_encrypted_deserialize_from_string() {
let json = r#"{"Encrypted":"base64data"}"#;
let value: RexValue = serde_json::from_str(json).unwrap();
assert_eq!(value, RexValue::Encrypted(b"base64data".to_vec()));
}
#[test]
fn test_rex_value_encrypted_deserialize_from_byte_array() {
let json = r#"{"Encrypted":[65,66,67]}"#;
let value: RexValue = serde_json::from_str(json).unwrap();
assert_eq!(value, RexValue::Encrypted(b"ABC".to_vec()));
}
#[test]
fn test_into_rex_value_for_plain() {
let out = <u64 as IntoRexValueFor<u64>>::into_rex_value_for(1_000_000u64);
assert_eq!(out, RexValue::Plain(borsh::to_vec(&1_000_000u64).unwrap()));
}
#[test]
fn test_into_rex_value_for_encrypted() {
let ct = vec![0xAAu8; 56];
let wrapped = EncryptedInput::<u64>::new(ct.clone());
let out = <EncryptedInput<u64> as IntoRexValueFor<u64>>::into_rex_value_for(wrapped);
assert_eq!(out, RexValue::Encrypted(ct));
}
}