Skip to main content

rialo_types/
rex_info.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! # REX Module
5//!
6//! This module contains all core REX-related types and structures.
7//!
8//! ## REX Identification
9//! - [`RexId`] - Uniquely identifies a REX instance using a nonce and creator
10//!
11//! ## REX Configuration & State
12//! - [`RexInfo`] - Complete REX definition and configuration
13//! - [`RexEntry`] - REX registry entry with metadata
14//!
15//! ## REX Values
16//! - [`RexValue`] - String value (plain or encrypted)
17//! - [`RexValueBody`] - Binary value (plain or encrypted)
18//!
19//! ## REX Targets & Updates
20//! - [`TargetRexProgram`] - Defines what the REX system should query (HTTP, Time, etc.)
21//! - [`RexUpdateResult`] - Result of a REX update with signature
22//!
23//! ## REX Requests & Scheduling
24//! - [`RexRequest`] - Request parameters for REX execution
25//! - [`UpdateFrequency`] - How often the REX system should run
26//! - [`StartingTimestamp`] - When the REX system should start
27
28use std::{
29    collections::BTreeMap,
30    convert::Infallible,
31    fmt,
32    ops::Deref,
33    str::FromStr,
34    sync::Arc,
35    time::{Duration, SystemTime, UNIX_EPOCH},
36};
37
38use borsh::{BorshDeserialize, BorshSerialize};
39#[cfg(feature = "non-pdk")]
40use clap::Subcommand;
41#[cfg(feature = "non-pdk")]
42use fastcrypto::encoding::{Base64, Encoding};
43use rialo_cli_representable::Representable;
44use rialo_limits::{max_rex_output_serialized_bytes, MIN_VIABLE_LIMIT_OF_REX_OUTPUT_SIZE};
45use rialo_s_pubkey::Pubkey;
46use serde::{Deserialize, Serialize};
47use serde_big_array::BigArray;
48#[cfg(feature = "non-pdk")]
49use url::Url;
50
51use crate::{
52    time::{DurationMs, TimestampMs},
53    websocket_op::WebSocketOperation,
54    AttestationReport, AuthorityKeyBytes, Headers, HttpFilter, Nonce, RexDutyConfig,
55};
56
57/// Lowest allowed update period for periodic REX requests, in milliseconds.
58///
59/// This is a pragmatic lower bound to avoid excessive scheduling / load.
60const MIN_UPDATE_PERIOD_MS: DurationMs = DurationMs::new(50);
61
62// ============================================================================
63// REX Identification
64// ============================================================================
65
66/// REX identifier that uniquely identifies a REX instance using a nonce and creator.
67///
68/// # String Parsing
69///
70/// `RexId` implements `FromStr` which expects a JSON format:
71/// ```json
72/// {"nonce":"<nonce_value>","creator":"<base58_pubkey>"}
73/// ```
74///
75/// This JSON format is used for CLI parsing and other string-based inputs.
76#[derive(
77    Debug,
78    Default,
79    Clone,
80    Copy,
81    PartialEq,
82    Eq,
83    Hash,
84    PartialOrd,
85    Ord,
86    Serialize,
87    Deserialize,
88    BorshSerialize,
89    BorshDeserialize,
90)]
91pub struct RexId {
92    pub nonce: Nonce,
93    pub creator: Pubkey,
94}
95
96impl RexId {
97    /// Create a new RexId from a nonce and creator
98    pub fn new(creator: Pubkey, nonce: impl Into<Nonce>) -> Self {
99        Self {
100            nonce: nonce.into(),
101            creator,
102        }
103    }
104}
105
106impl fmt::Display for RexId {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{}:{}", &self.nonce, &self.creator)
109    }
110}
111
112impl FromStr for RexId {
113    type Err = String;
114
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        // Parse JSON format: {"nonce":"...","creator":"..."}
117        serde_json::from_str(s).map_err(|e| format!("Failed to parse RexId: {}", e))
118    }
119}
120
121// ============================================================================
122// REX Configuration & State
123// ============================================================================
124
125/// Represents a REX definition and configuration.
126#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Representable)]
127#[representable(human_readable = "rex_info_human_readable")]
128pub struct RexInfo {
129    /// Unique identifier
130    pub id: RexId,
131    /// Description for the REX instance.
132    pub description: String,
133    /// When the REX instance should bring back data.
134    pub update_frequency: UpdateFrequency,
135    /// URLs for the REX instance.
136    pub target_rex_programs: Vec<TargetRexProgram>,
137    /// Timestamp in which the REX instance will start.
138    pub starting_timestamp: StartingTimestamp,
139    /// Whether the REX instance is active.
140    pub is_active: bool,
141    /// When the REX instance was created.
142    pub created_at_ms: i64,
143    /// Number of validators to assign per REX request.
144    #[serde(default = "default_validators_per_duty")]
145    pub validators_per_duty: u32,
146    /// Delay sufficient for the REX task to complete, in milliseconds.
147    #[serde(default = "default_rex_request_delay_ms")]
148    pub request_delay_ms: DurationMs,
149}
150
151fn rex_info_human_readable(info: &RexInfo) -> String {
152    let mut out = String::new();
153
154    out.push_str(&format!("REX ID: {}\n", info.id));
155    out.push_str(&format!("Description: {}\n", info.description));
156    out.push_str(&format!("Active: {}\n", info.is_active));
157    out.push_str(&format!(
158        "Starting Timestamp: {:?}\n",
159        info.starting_timestamp
160    ));
161    out.push_str(&format!("Update Frequency: {:?}\n", info.update_frequency));
162    out.push_str(&format!("Created At: {}\n", info.created_at_ms));
163    out.push_str(&format!(
164        "Validators Per Duty: {}\n",
165        info.validators_per_duty
166    ));
167    out.push_str(&format!("REX Request Delay: {}\n", info.request_delay_ms));
168
169    if !info.target_rex_programs.is_empty() {
170        out.push_str(&format!(
171            "\nTarget REX operations ({}):\n",
172            info.target_rex_programs.len()
173        ));
174        for (i, target) in info.target_rex_programs.iter().enumerate() {
175            out.push_str(&format!("  {}. {:?}\n", i + 1, target));
176        }
177    }
178
179    out
180}
181
182impl Default for RexInfo {
183    fn default() -> Self {
184        Self {
185            id: RexId::default(),
186            description: String::new(),
187            update_frequency: UpdateFrequency::default(),
188            target_rex_programs: Vec::new(),
189            starting_timestamp: StartingTimestamp::default(),
190            is_active: false,
191            created_at_ms: 0,
192            validators_per_duty: default_validators_per_duty(),
193            request_delay_ms: default_rex_request_delay_ms(),
194        }
195    }
196}
197
198impl RexInfo {
199    /// Returns true if the REX should start as soon as possible (ASAP),
200    /// rather than at a specific timestamp.
201    pub fn is_asap(&self) -> bool {
202        matches!(self.starting_timestamp, StartingTimestamp::Asap)
203    }
204
205    pub fn target_timestamp(&self) -> Option<TimestampMs> {
206        match self.starting_timestamp {
207            StartingTimestamp::Timestamp(timestamp) => Some(timestamp),
208            StartingTimestamp::Asap => None,
209        }
210    }
211
212    /// True if any target program carries a DKG-encrypted payload, so this REX
213    /// needs the threshold-decryption committee + combine pipeline before any
214    /// result can be produced. Such duties get an extended collection window
215    /// (`DKG_ENCRYPTED_PIPELINE_OVERHEAD_MS + request_delay_ms`) rather than the
216    /// plain ASAP timeout. See [`TargetRexProgram::is_dkg_encrypted`].
217    pub fn requires_dkg_decryption(&self) -> bool {
218        self.target_rex_programs
219            .iter()
220            .any(TargetRexProgram::is_dkg_encrypted)
221    }
222
223    /// Validates all REX configuration fields and their consistency.
224    pub fn validate(&self) -> Result<(), String> {
225        match self.starting_timestamp {
226            StartingTimestamp::Asap => {
227                if !matches!(self.update_frequency, UpdateFrequency::OneShot) {
228                    return Err("ASAP REX requests cannot be periodic".to_string());
229                }
230            }
231            StartingTimestamp::Timestamp(starting_timestamp) => {
232                match self.update_frequency {
233                    UpdateFrequency::OneShot => {}
234                    UpdateFrequency::Periodic(period)
235                    | UpdateFrequency::LimitedPeriodic(period, _) => {
236                        validate_periodic_frequency(period)?;
237
238                        // Additional validation for LimitedPeriodic
239                        if let UpdateFrequency::LimitedPeriodic(_, end_timestamp) =
240                            self.update_frequency
241                        {
242                            if starting_timestamp >= end_timestamp {
243                                return Err("end_timestamp of a LimitedPeriodic REX should be above starting_timestamp".to_string());
244                            }
245                        }
246                    }
247                }
248            }
249        }
250
251        // Validate `target_rex_programs`.
252        if self.target_rex_programs.is_empty() {
253            return Err("RexTargets cannot be empty".to_string());
254        }
255
256        // Validate `rex_request_delay`.
257        if self.request_delay_ms < RexDutyConfig::MIN_REX_REQUEST_DELAY {
258            return Err(format!(
259                "rex_request_delay cannot be below {}",
260                RexDutyConfig::MIN_REX_REQUEST_DELAY
261            ));
262        }
263        if self.request_delay_ms > RexDutyConfig::MAX_REX_REQUEST_DELAY_MS {
264            return Err(format!(
265                "rex_request_delay cannot be above {}",
266                RexDutyConfig::MAX_REX_REQUEST_DELAY_MS
267            ));
268        }
269
270        // Validate `validators_per_duty`.
271        if self.validators_per_duty == 0 {
272            return Err("validators_per_duty cannot be 0".to_string());
273        }
274        let max_rex_output_size = max_rex_output_serialized_bytes(self.validators_per_duty);
275        if max_rex_output_size < MIN_VIABLE_LIMIT_OF_REX_OUTPUT_SIZE {
276            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}"));
277        }
278
279        Ok(())
280    }
281
282    /// Extracts the WebSocket operation from the REX targets, if any.
283    pub fn websocket_op(&self) -> Option<WebSocketOperation> {
284        self.target_rex_programs
285            .first()
286            .and_then(|target| target.websocket_op())
287    }
288}
289
290fn validate_periodic_frequency(period_ms: DurationMs) -> Result<(), String> {
291    if period_ms == DurationMs::ZERO {
292        return Err("update frequency cannot be zero".to_string());
293    }
294
295    if period_ms < MIN_UPDATE_PERIOD_MS {
296        return Err(format!(
297            "update frequency {period_ms} cannot be below {MIN_UPDATE_PERIOD_MS}"
298        ));
299    }
300
301    Ok(())
302}
303
304impl TargetRexProgram {
305    /// Extracts the WebSocket operation if this is a WebSocket target.
306    pub fn websocket_op(&self) -> Option<WebSocketOperation> {
307        if let TargetRexProgram::WebSocket(ws_op) = self {
308            Some(ws_op.clone())
309        } else {
310            None
311        }
312    }
313
314    /// Ciphertext bytes of every field on this program that can carry an
315    /// encrypted payload: the encrypted URL, body, WebSocket `Send` message(s),
316    /// or WASM input(s). Single source of truth for *which fields are ciphertext
317    /// carriers*; the version-gated routing (`Router::dkg_payload_bytes`) and
318    /// the on-chain ciphertext-match check (`encrypted_payload_iter`) build on
319    /// this and additionally require the `DKG_PAYLOAD_VERSION` byte (a constant
320    /// that lives above this crate, so it is applied by those callers).
321    pub fn encrypted_payloads(&self) -> Box<dyn Iterator<Item = &[u8]> + '_> {
322        fn enc(v: &RexValue) -> Option<&[u8]> {
323            v.is_encrypted().then(|| v.as_bytes())
324        }
325        // Matched exhaustively (no `_`) on purpose: this is the single source
326        // for which program fields can carry ciphertext, so a newly-added
327        // `TargetRexProgram` variant must fail to compile here until it is
328        // classified — otherwise it would silently bypass DKG detection in both
329        // the off-chain router and the on-chain ciphertext-match check.
330        // (`#[non_exhaustive]` only forces a wildcard in *other* crates; this is
331        // the defining crate, so the guardrail holds.)
332        match self {
333            TargetRexProgram::HttpGet { url, .. } if url.is_encrypted() => {
334                Box::new(std::iter::once(url.as_bytes()))
335            }
336            TargetRexProgram::HttpGet { .. } => Box::new(std::iter::empty()),
337            TargetRexProgram::HttpPost { body, .. } => Box::new(enc(body).into_iter()),
338            TargetRexProgram::WebSocket(WebSocketOperation::Send { messages, .. }) => {
339                Box::new(messages.iter().filter_map(enc))
340            }
341            TargetRexProgram::WebSocket(_) => Box::new(std::iter::empty()),
342            TargetRexProgram::Wasm { input, .. } => Box::new(input.iter().filter_map(enc)),
343            TargetRexProgram::Time
344            | TargetRexProgram::Number
345            | TargetRexProgram::SecretKeyGeneration { .. }
346            | TargetRexProgram::SecretKeyEncryption { .. }
347            | TargetRexProgram::SecretKeyDecryption { .. } => Box::new(std::iter::empty()),
348        }
349    }
350
351    /// True if this program carries any encrypted payload, meaning the duty
352    /// cannot be served by a direct fetch and must go through the
353    /// threshold-decryption committee + combine pipeline before any result can
354    /// exist. Used to size the encrypted duty's collection window
355    /// (`DutyRequest::inner`); does not inspect `DKG_PAYLOAD_VERSION` —
356    /// encryption alone is sufficient (and conservative) for that purpose.
357    pub fn is_dkg_encrypted(&self) -> bool {
358        self.encrypted_payloads().next().is_some()
359    }
360}
361
362fn default_validators_per_duty() -> u32 {
363    RexDutyConfig::DEFAULT_VALIDATORS_PER_DUTY
364}
365
366fn default_rex_request_delay_ms() -> DurationMs {
367    RexDutyConfig::DEFAULT_REQUEST_DELAY_MS
368}
369
370/// Represents an REX registry entry containing REX information and metadata.
371///
372/// This struct stores REX data along with a hash of the data for change detection
373/// and tracking information about when the entry was last modified.
374#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
375pub struct RexEntry {
376    rex_info: Arc<RexInfo>,
377    data_hash: [u8; RexEntry::HASH_LENGTH],
378    last_modified_timestamp: u64,
379}
380
381impl RexEntry {
382    const HASH_LENGTH: usize = 32;
383
384    /// Creates a new RexEntry instance.
385    ///
386    /// # Arguments
387    ///
388    /// * `data`: The account data as a byte vector.
389    /// * `data_hash`: The hash of the account data.
390    /// * `last_modified_timestamp`: The round in which the account was last modified.
391    pub fn new(
392        rex_info: RexInfo,
393        data_hash: [u8; Self::HASH_LENGTH],
394        last_modified_round: u64,
395    ) -> Self {
396        Self {
397            rex_info: Arc::new(rex_info),
398            data_hash,
399            last_modified_timestamp: last_modified_round,
400        }
401    }
402
403    /// Retrieves the account data.
404    ///
405    /// # Returns
406    ///
407    /// The account data as an RexInfo.
408    pub fn rex_info(&self) -> Arc<RexInfo> {
409        self.rex_info.clone()
410    }
411
412    /// Retrieves the last modified round.
413    ///
414    /// # Returns
415    ///
416    /// The last modified round as a `u64`.
417    pub fn last_modified_timestamp(&self) -> u64 {
418        self.last_modified_timestamp
419    }
420
421    /// Retrieves the hash of the account data.
422    ///
423    /// # Returns
424    ///
425    /// The hash of the account data as a byte array.
426    pub fn data_hash(&self) -> &[u8; Self::HASH_LENGTH] {
427        &self.data_hash
428    }
429}
430
431// ============================================================================
432// REX Values
433// ============================================================================
434
435/// Deserializes a `Vec<u8>` from either a JSON string or a JSON byte array.
436///
437/// This allows CLI users and scripts to pass `{"Plain": "hello"}` instead of
438/// the canonical `{"Plain": [104, 101, 108, 108, 111]}` byte-array format.
439/// Both formats are accepted; strings are converted via UTF-8 `.as_bytes()`.
440///
441/// Uses `deserialize_bytes` rather than `deserialize_any` for compatibility
442/// with non-self-describing binary formats (e.g., bincode used for on-chain data).
443/// For JSON, `serde_json`'s `deserialize_bytes` handles both strings (via
444/// `visit_str`) and arrays (via `visit_seq`) directly. For bincode, it reads
445/// raw bytes via `visit_bytes`.
446fn deserialize_bytes_or_string<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
447where
448    D: serde::Deserializer<'de>,
449{
450    struct BytesOrString;
451
452    impl<'de> serde::de::Visitor<'de> for BytesOrString {
453        type Value = Vec<u8>;
454
455        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456            f.write_str("a byte array, byte slice, or a string")
457        }
458
459        fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Vec<u8>, E> {
460            Ok(bytes.to_vec())
461        }
462
463        fn visit_byte_buf<E: serde::de::Error>(self, bytes: Vec<u8>) -> Result<Vec<u8>, E> {
464            Ok(bytes)
465        }
466
467        fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Vec<u8>, E> {
468            Ok(s.as_bytes().to_vec())
469        }
470
471        fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<u8>, A::Error> {
472            let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0));
473            while let Some(b) = seq.next_element()? {
474                bytes.push(b);
475            }
476            Ok(bytes)
477        }
478    }
479
480    deserializer.deserialize_bytes(BytesOrString)
481}
482
483/// Represents a value that can be either plain text or encrypted.
484///
485/// This enum is used to handle REX data that may contain sensitive information
486/// that needs to be encrypted when transmitted or stored, while also supporting
487/// plain text values for non-sensitive data.
488///
489/// Both variants accept JSON strings or byte arrays during deserialization.
490/// Serialization always outputs the canonical byte-array format.
491///
492/// # Variants
493/// * `Plain(Vec<u8>)` - A plain text value that is not encrypted.
494///   Accepts a JSON string (e.g., `{"Plain": "hello"}`) or byte array.
495/// * `Encrypted(Vec<u8>)` - Encrypted ciphertext. When passed as a JSON string,
496///   the value should be base64-encoded ciphertext for TEE decryption.
497///   No base64 validation is performed at deserialization time.
498#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
499pub enum RexValue {
500    Plain(#[serde(deserialize_with = "deserialize_bytes_or_string")] Vec<u8>),
501    Encrypted(#[serde(deserialize_with = "deserialize_bytes_or_string")] Vec<u8>),
502}
503
504impl RexValue {
505    /// Create a plain value from raw bytes.
506    pub fn plain(data: Vec<u8>) -> Self {
507        RexValue::Plain(data)
508    }
509
510    /// Create a plain value from a string (convenience method).
511    ///
512    /// This is the preferred way to create an `RexValue` from string data
513    /// after the migration from string-based `RexValue`.
514    pub fn plain_string(s: impl Into<String>) -> Self {
515        RexValue::Plain(s.into().into_bytes())
516    }
517
518    /// Create an encrypted value from raw ciphertext bytes.
519    pub fn encrypted(ciphertext: Vec<u8>) -> Self {
520        RexValue::Encrypted(ciphertext)
521    }
522
523    /// Get the inner data as a string slice, if it's valid UTF-8.
524    ///
525    /// Returns `Some(&str)` if the data is valid UTF-8, `None` otherwise.
526    /// Only works on `Plain` variants; `Encrypted` always returns `None`.
527    pub fn as_string(&self) -> Option<&str> {
528        match self {
529            RexValue::Plain(bytes) => std::str::from_utf8(bytes).ok(),
530            RexValue::Encrypted(_) => None,
531        }
532    }
533
534    /// Get the inner data as a byte slice.
535    ///
536    /// Works on both `Plain` and `Encrypted` variants.
537    pub fn as_bytes(&self) -> &[u8] {
538        match self {
539            RexValue::Plain(bytes) | RexValue::Encrypted(bytes) => bytes,
540        }
541    }
542
543    /// Returns `true` if this is a `Plain` variant.
544    pub fn is_plain(&self) -> bool {
545        matches!(self, RexValue::Plain(_))
546    }
547
548    /// Returns `true` if this is an `Encrypted` variant.
549    pub fn is_encrypted(&self) -> bool {
550        matches!(self, RexValue::Encrypted(_))
551    }
552}
553
554impl Default for RexValue {
555    fn default() -> Self {
556        RexValue::Plain(vec![])
557    }
558}
559
560/// Convert an argument into a [`RexValue`] bound to the WASM rex function's
561/// expected parameter type `T`.
562///
563/// Parameterizing the trait by the *target* type lets two non-overlapping
564/// impls do compile-time dispatch:
565///
566/// - `impl<T: BorshSerialize> IntoRexValueFor<T> for T` — plain values are
567///   borsh-serialized and emitted as `RexValue::Plain`.
568/// - `impl<T> IntoRexValueFor<T> for EncryptedInput<T>` — encrypted values
569///   forward their ciphertext as `RexValue::Encrypted`.
570///
571/// The two impls cover distinct `(Self, T)` pairs (`(u64, u64)` vs
572/// `(EncryptedInput<u64>, u64)`), so they don't overlap and no specialization
573/// is required. Venus codegen emits `IntoRexValueFor::<#param_ty>` for each
574/// positional argument, so type mismatches (e.g. `EncryptedInput<String>`
575/// passed to a `u64` rex parameter) surface as ordinary `rustc` errors from
576/// the trait resolver rather than requiring the macro to run its own
577/// precheck.
578pub trait IntoRexValueFor<T> {
579    fn into_rex_value_for(self) -> RexValue;
580}
581
582impl<T: BorshSerialize> IntoRexValueFor<T> for T {
583    fn into_rex_value_for(self) -> RexValue {
584        RexValue::Plain(borsh::to_vec(&self).expect("borsh serialize failed"))
585    }
586}
587
588impl<T> IntoRexValueFor<T> for EncryptedInput<T> {
589    fn into_rex_value_for(self) -> RexValue {
590        RexValue::Encrypted(self.into_bytes())
591    }
592}
593
594impl FromStr for RexValue {
595    type Err = Infallible;
596    fn from_str(s: &str) -> Result<Self, Self::Err> {
597        Ok(RexValue::Plain(s.as_bytes().to_vec()))
598    }
599}
600
601/// Type alias for backward compatibility.
602///
603/// `RexValueBody` has been consolidated into `RexValue`.
604/// This alias is provided for migration but will be removed in a future version.
605#[deprecated(since = "0.2.0", note = "Use RexValue instead")]
606pub type RexValueBody = RexValue;
607
608/// A wrapper type around `RexValue` specifically for handling URLs in REX configurations.
609///
610/// This type provides a convenient way to handle both plain text and encrypted URLs:
611/// - Plain text URLs are stored directly as strings
612/// - Encrypted URLs are stored as base64-encoded encrypted strings prefixed with "enc://"
613///
614/// The type implements common traits like Display and FromStr for easy conversion and
615/// formatting, and integrates with the url crate when the "non-pdk" feature is enabled.
616///
617/// # Examples
618///
619/// ```
620/// use std::str::FromStr;
621/// use rialo_types::RexUrl;
622///
623/// // Create from plain text URL
624/// let plain_url = RexUrl::from("https://example.com");
625///
626/// // Create from encrypted URL
627/// let encrypted_url = RexUrl::from("enc://encrypted_data");
628/// ```
629#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
630pub struct RexUrl(RexValue);
631
632impl fmt::Display for RexUrl {
633    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
634        match &self.0 {
635            RexValue::Plain(bytes) => {
636                // Try to display as UTF-8 string, fall back to hex format
637                match std::str::from_utf8(bytes) {
638                    Ok(s) => write!(f, "{}", s),
639                    #[cfg(feature = "non-pdk")]
640                    Err(_) => write!(f, "<binary:{}>", Base64::encode(bytes)),
641                    #[cfg(not(feature = "non-pdk"))]
642                    Err(_) => write!(f, "<binary:{}>", hex::encode(bytes)),
643                }
644            }
645            RexValue::Encrypted(bytes) => {
646                // Try to display as UTF-8 string, fall back to hex format
647                match std::str::from_utf8(bytes) {
648                    Ok(s) => write!(f, "enc://{}", s),
649                    #[cfg(feature = "non-pdk")]
650                    Err(_) => write!(f, "enc://<binary:{}>", Base64::encode(bytes)),
651                    #[cfg(not(feature = "non-pdk"))]
652                    Err(_) => write!(f, "enc://<binary:{}>", hex::encode(bytes)),
653                }
654            }
655        }
656    }
657}
658
659impl Deref for RexUrl {
660    type Target = RexValue;
661
662    fn deref(&self) -> &Self::Target {
663        &self.0
664    }
665}
666
667#[cfg(feature = "non-pdk")]
668impl From<Url> for RexUrl {
669    fn from(url: Url) -> Self {
670        url.to_string().into()
671    }
672}
673
674#[cfg(feature = "non-pdk")]
675impl From<&Url> for RexUrl {
676    fn from(url: &Url) -> Self {
677        Self(RexValue::Plain(url.to_string().into_bytes()))
678    }
679}
680
681impl From<String> for RexUrl {
682    fn from(url: String) -> Self {
683        url.as_str().into()
684    }
685}
686
687impl From<&str> for RexUrl {
688    fn from(s: &str) -> Self {
689        if let Some(encrypted) = s.strip_prefix("enc://") {
690            Self(RexValue::Encrypted(encrypted.into()))
691        } else {
692            // If it isn't prefixed with `enc://`, treat it as plain text
693            Self(RexValue::Plain(s.into()))
694        }
695    }
696}
697
698impl FromStr for RexUrl {
699    type Err = Infallible;
700    fn from_str(s: &str) -> Result<Self, Self::Err> {
701        Ok(s.into())
702    }
703}
704
705impl From<RexValue> for RexUrl {
706    fn from(value: RexValue) -> Self {
707        Self(value)
708    }
709}
710
711// ============================================================================
712// REX Targets
713// ============================================================================
714
715/// Enum that represents the target REX Program of the REX request.
716///
717/// Discriminant values are fixed to ensure stable serialization across builds
718/// with different feature flags.
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, strum_macros::AsRefStr)]
720#[cfg_attr(feature = "non-pdk", derive(Subcommand))]
721#[repr(u16)]
722#[non_exhaustive]
723pub enum TargetRexProgram {
724    /// HTTP GET request to the REX. The URL is specified in the RexDutyRequest.
725    /// The URL must be an HTTPS URL.
726    /// The filter is optional, and if provided, it is used to filter the response.
727    HttpGet {
728        #[cfg_attr(feature = "non-pdk", clap(
729            long = "target-url",
730            value_parser = clap::value_parser!(RexUrl)
731        ))]
732        url: RexUrl,
733        #[cfg_attr(feature = "non-pdk", clap(long, default_value = None))]
734        filter: Option<Vec<HttpFilter>>,
735        #[cfg_attr(feature = "non-pdk", clap(
736            long,
737            default_value_t = Headers::default(),
738            action = clap::ArgAction::Append
739        ))]
740        headers: Headers,
741    } = 0,
742    /// HTTP POST request to the REX. The URL is specified in the RexDutyRequest.
743    /// The URL must be an HTTPS URL. This REX type is restricted to single-validator assignment
744    /// to prevent duplicate operations on non-idempotent endpoints.
745    HttpPost {
746        #[cfg_attr(feature = "non-pdk", clap(
747            long = "target-url",
748            value_parser = clap::value_parser!(RexUrl)
749        ))]
750        url: RexUrl,
751        #[cfg_attr(feature = "non-pdk", clap(long))]
752        filter: Option<Vec<HttpFilter>>,
753        #[cfg_attr(feature = "non-pdk", clap(
754            long,
755            value_parser = clap::value_parser!(RexValue)
756        ))]
757        body: RexValue,
758        #[cfg_attr(feature = "non-pdk", clap(long))]
759        content_type: String,
760        #[cfg_attr(feature = "non-pdk", clap(
761            long,
762            default_value_t = Headers::default(),
763            action = clap::ArgAction::Append
764        ))]
765        headers: Headers,
766    } = 1,
767    /// Get the current time from several NIST servers.
768    Time = 2,
769    /// Only used for testing purposes, to simulate an REX that always returns a fixed value.
770    /// TODO: remove this variant with a cfg testing flag.
771    Number = 3,
772    /// Generate a shared secret key within a committee of TEEs.
773    /// The manager TEE generates the key and distributes it to all committee members.
774    SecretKeyGeneration {
775        #[cfg_attr(feature = "non-pdk", clap(long))]
776        committee_id: String,
777        #[cfg_attr(feature = "non-pdk", clap(long))]
778        committee_members: Vec<String>,
779    } = 4,
780    /// Encrypt a secret key for a target TEE using their public key.
781    /// This is used to share keys with TEEs outside the original committee.
782    SecretKeyEncryption {
783        #[cfg_attr(feature = "non-pdk", clap(long))]
784        target_tee_id: String,
785        #[cfg_attr(feature = "non-pdk", clap(long))]
786        secret_data: Vec<u8>,
787        #[cfg_attr(feature = "non-pdk", clap(long))]
788        committee_id: String,
789    } = 5,
790    /// Decrypt a secret key that was encrypted for this TEE.
791    /// This is used by TEEs to access keys shared with them.
792    SecretKeyDecryption {
793        #[cfg_attr(feature = "non-pdk", clap(long))]
794        encrypted_data: Vec<u8>,
795        #[cfg_attr(feature = "non-pdk", clap(long))]
796        source_committee_id: String,
797    } = 6,
798    /// WebSocket REX operations for persistent connections
799    #[cfg_attr(feature = "non-pdk", clap(subcommand))]
800    WebSocket(WebSocketOperation) = 7,
801    /// Execute WASM bytecode inside the TEE.
802    /// The bytecode must be a WASI-compatible component deployed to an on-chain account.
803    Wasm {
804        /// Account pubkey containing the deployed WASM component(s)
805        #[cfg_attr(feature = "non-pdk", clap(long))]
806        bytecode_account: Pubkey,
807        /// Per-argument input data for the WASM module.
808        /// Each entry is one argument, independently Plain (borsh-serialized)
809        /// or Encrypted (HPKE ciphertext the TEE decrypts). The TEE decrypts
810        /// each Encrypted entry and concatenates all bytes into a borsh tuple.
811        /// From CLI: pass base64-encoded bytes (single plain argument).
812        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_base64_rex_value))]
813        input: Vec<RexValue>,
814        /// Index into the program table within the account (defaults to 0)
815        #[cfg_attr(feature = "non-pdk", clap(long))]
816        program_index: Option<u32>,
817    } = 8,
818}
819
820/// Pre-encrypted input bytes for TEE-decrypted REX arguments.
821///
822/// On-chain this is opaque ciphertext (`Vec<u8>`). The TEE decrypts it
823/// transparently before passing the plaintext to the REX handler (e.g.,
824/// a WASM component). Venus codegen recognizes this type and emits
825/// `RexValue::Encrypted(...)` instead of borsh-serializing individual args.
826///
827/// The type parameter `T` is a phantom type that Venus codegen uses to
828/// verify the encrypted value matches the expected function parameter type
829/// at compile time. It has no effect on serialization — `EncryptedInput<u64>`
830/// and `EncryptedInput<String>` serialize identically as opaque bytes.
831///
832/// Create with `rialo_cdk::encrypt_input` on the client side.
833///
834/// # Examples
835///
836/// ```ignore
837/// // Typed: Venus verifies EncryptedInput<u64> matches `amount: u64`
838/// state { encrypted_amount: EncryptedInput<u64> }
839///
840/// // Untyped: no compile-time check (T defaults to ())
841/// state { encrypted_blob: EncryptedInput }
842/// ```
843pub struct EncryptedInput<T = ()> {
844    ciphertext: Vec<u8>,
845    _phantom: std::marker::PhantomData<T>,
846}
847
848// Manual derives because #[derive] adds bounds on T that we don't want.
849// EncryptedInput<T> should be Clone/Default/etc regardless of T.
850
851impl<T> Clone for EncryptedInput<T> {
852    fn clone(&self) -> Self {
853        Self {
854            ciphertext: self.ciphertext.clone(),
855            _phantom: std::marker::PhantomData,
856        }
857    }
858}
859
860impl<T> Default for EncryptedInput<T> {
861    fn default() -> Self {
862        Self {
863            ciphertext: Vec::new(),
864            _phantom: std::marker::PhantomData,
865        }
866    }
867}
868
869impl<T> std::fmt::Debug for EncryptedInput<T> {
870    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
871        f.debug_tuple("EncryptedInput")
872            .field(&format!("[{} bytes]", self.ciphertext.len()))
873            .finish()
874    }
875}
876
877impl<T> PartialEq for EncryptedInput<T> {
878    fn eq(&self, other: &Self) -> bool {
879        self.ciphertext == other.ciphertext
880    }
881}
882
883impl<T> Eq for EncryptedInput<T> {}
884
885impl<T> BorshSerialize for EncryptedInput<T> {
886    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
887        borsh::BorshSerialize::serialize(&self.ciphertext, writer)
888    }
889}
890
891impl<T> BorshDeserialize for EncryptedInput<T> {
892    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
893        Ok(Self {
894            ciphertext: borsh::BorshDeserialize::deserialize_reader(reader)?,
895            _phantom: std::marker::PhantomData,
896        })
897    }
898}
899
900impl<T> Serialize for EncryptedInput<T> {
901    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
902        serde::Serialize::serialize(&self.ciphertext, serializer)
903    }
904}
905
906impl<'de, T> Deserialize<'de> for EncryptedInput<T> {
907    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
908        Ok(Self {
909            ciphertext: <Vec<u8> as serde::Deserialize>::deserialize(deserializer)?,
910            _phantom: std::marker::PhantomData,
911        })
912    }
913}
914
915impl<T> EncryptedInput<T> {
916    pub fn new(ciphertext: Vec<u8>) -> Self {
917        Self {
918            ciphertext,
919            _phantom: std::marker::PhantomData,
920        }
921    }
922
923    pub fn as_bytes(&self) -> &[u8] {
924        &self.ciphertext
925    }
926
927    pub fn into_bytes(self) -> Vec<u8> {
928        self.ciphertext
929    }
930}
931
932impl<T> From<Vec<u8>> for EncryptedInput<T> {
933    fn from(v: Vec<u8>) -> Self {
934        Self::new(v)
935    }
936}
937
938/// Parse CLI input as base64-encoded bytes into a plain RexValue for the WASM `--input` flag.
939#[cfg(feature = "non-pdk")]
940fn parse_base64_rex_value(s: &str) -> Result<RexValue, String> {
941    use fastcrypto::encoding::{Base64, Encoding};
942    Base64::decode(s)
943        .map(RexValue::Plain)
944        .map_err(|e| format!("Invalid base64 input: {e}"))
945}
946
947impl TargetRexProgram {
948    /// Returns true if this is a WebSocket operation.
949    pub fn is_websocket(&self) -> bool {
950        matches!(self, TargetRexProgram::WebSocket(_))
951    }
952}
953
954impl FromStr for TargetRexProgram {
955    type Err = String;
956
957    fn from_str(s: &str) -> Result<Self, Self::Err> {
958        if s == "Time" {
959            Ok(TargetRexProgram::Time)
960        } else if s == "SecretKeyGeneration" {
961            Err("SecretKeyGeneration REX requires committee_id and committee_members parameters. Use the appropriate API to create this REX type.".to_string())
962        } else if s == "SecretKeyEncryption" {
963            Err("SecretKeyEncryption REX requires target_tee_id, secret_data, and committee_id parameters. Use the appropriate API to create this REX type.".to_string())
964        } else if s == "SecretKeyDecryption" {
965            Err("SecretKeyDecryption REX requires encrypted_data and source_committee_id parameters. Use the appropriate API to create this REX type.".to_string())
966        } else if s == "number" {
967            Err("The 'number' REX is only for testing purposes and should not be used in production.".to_string())
968        } else {
969            if let Some(rest) = s.strip_prefix("HttpGet:") {
970                let parts: Vec<&str> = rest.splitn(2, '|').collect();
971                if parts.is_empty() {
972                    return Err(
973                        "Invalid HttpGet format. Use 'HttpGet:<url>[|<filter>]'.".to_string()
974                    );
975                }
976
977                let url = parts[0].to_string();
978                let filter = if parts.len() > 1 && !parts[1].is_empty() {
979                    Some(vec![HttpFilter::from_str(parts[1])?])
980                } else {
981                    None
982                };
983
984                // Validate URL (only when url crate is available)
985                #[cfg(feature = "non-pdk")]
986                if Url::parse(&url).is_err() {
987                    return Err(format!("Invalid URL: {url}"));
988                }
989
990                return Ok(TargetRexProgram::HttpGet {
991                    url: url.into(),
992                    filter,
993                    headers: Headers::default(),
994                });
995            }
996
997            Err(format!("Unknown TargetRexProgram type: {s}"))
998        }
999    }
1000}
1001
1002// ============================================================================
1003// REX Updates
1004// ============================================================================
1005
1006/// 32‑byte Blake3 hash of the raw request payload observed by the REX service.
1007///
1008/// This binds a response to the exact input it was computed from and is included
1009/// in the signature transcript as `input_commitment || blake3(response_value)`.
1010pub type InputCommitmentBytes = [u8; 32];
1011
1012/// Raw 64‑byte Ed25519 signature over the REX response transcript.
1013///
1014/// The transcript signed by the REX is `input_commitment || blake3(response_value)`.
1015pub type SignatureBytes = [u8; 64];
1016
1017/// A structure representing the result of an REX update
1018/// This structure is designed to fit within Solana's transaction size limit
1019/// TODO: <https://linear.app/subzero-labs/issue/SUB-449/audit-transaction-sizes-in-the-REX-subsystem>
1020#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1021pub struct RexUpdateResult {
1022    /// The identifier of the REX (should be kept under 100 bytes)
1023    pub rex_id: RexId,
1024
1025    /// The round in which this result should be proposed
1026    pub target_timestamp: TimestampMs,
1027
1028    /// Blake3 hash of `rex_result` (32 bytes)
1029    #[serde(with = "BigArray")]
1030    pub response_hash: [u8; 32],
1031
1032    /// Blake3 hash commitment of the original request input bytes (32 bytes)
1033    /// This is included in the signature transcript to bind the response to
1034    /// the exact request payload observed by the REX service.
1035    #[serde(with = "BigArray")]
1036    pub input_commitment: InputCommitmentBytes,
1037
1038    /// Signature of the hash of the REX response, base64 encoded.
1039    #[serde(with = "BigArray")]
1040    pub signature: SignatureBytes,
1041
1042    /// Response data (up to MAX_TRANSACTION_SIZE bytes in size)
1043    pub rex_result: Vec<u8>,
1044
1045    /// Optional attestation report, if available
1046    /// This can be used to provide additional context or verification of the REX result.
1047    pub attestation_report: Option<AttestationReport>,
1048
1049    /// Validator's protocol public key bytes of the validator that executed the edge rquest.
1050    #[serde(with = "BigArray")]
1051    pub authority_key: AuthorityKeyBytes,
1052}
1053
1054impl RexUpdateResult {
1055    /// Create a RexUpdateResult
1056    pub fn new(
1057        rex_id: RexId,
1058        target_timestamp: TimestampMs,
1059        rex_result: Vec<u8>,
1060        input_commitment: InputCommitmentBytes,
1061        signature: SignatureBytes,
1062        attestation_report: Option<AttestationReport>,
1063        authority_key: AuthorityKeyBytes,
1064    ) -> Result<Self, &'static str> {
1065        let hash = blake3::hash(&rex_result);
1066
1067        // This is a temporary solution to avoid having to deal with the size of the REX result
1068        #[cfg(feature = "non-pdk")]
1069        let rex_result = if rex_result.len() > rialo_limits::MAX_TRANSACTION_SIZE as usize {
1070            tracing::error!(
1071                "REX result size {} exceeds maximum size {}, dropping the result.",
1072                rex_result.len(),
1073                rialo_limits::MAX_TRANSACTION_SIZE
1074            );
1075            return Err("REX result exceeds maximum size");
1076        } else {
1077            // Use the REX result as-is
1078            rex_result
1079        };
1080
1081        Ok(Self {
1082            rex_id,
1083            target_timestamp,
1084            response_hash: *hash.as_bytes(),
1085            input_commitment,
1086            signature,
1087            rex_result,
1088            attestation_report,
1089            authority_key,
1090        })
1091    }
1092}
1093
1094impl Default for RexUpdateResult {
1095    fn default() -> Self {
1096        Self {
1097            rex_id: RexId::default(),
1098            target_timestamp: TimestampMs::ZERO,
1099            response_hash: [0; 32],
1100            input_commitment: [0xee; 32],
1101            signature: [0; 64],
1102            rex_result: vec![],
1103            attestation_report: None,
1104            authority_key: [0xff; 96],
1105        }
1106    }
1107}
1108
1109// ============================================================================
1110// REX Requests & Scheduling
1111// ============================================================================
1112
1113/// Represents a request to an REX with parameters that can be used to specify the query.
1114/// The parameters are stored as a BTreeMap to allow for flexible key-value pairs.
1115/// Contains fields that uniquely identify and configure the request.
1116#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)]
1117pub struct RexRequest {
1118    /// Structured fields of the request.
1119    pub rex_id: Option<RexId>,
1120    pub target_timestamp: Option<TimestampMs>,
1121    pub authority_key: AuthorityKeyBytes,
1122    pub include_attestation: bool,
1123    pub max_output_size: u32,
1124
1125    /// Request-specific extra data for the request.
1126    pub params: BTreeMap<String, String>,
1127}
1128
1129impl Default for RexRequest {
1130    fn default() -> Self {
1131        Self {
1132            rex_id: None,
1133            target_timestamp: None,
1134            authority_key: [0; 96],
1135            include_attestation: true,
1136            max_output_size: 0,
1137            params: BTreeMap::default(),
1138        }
1139    }
1140}
1141
1142impl RexRequest {
1143    pub fn input_commitment(&self) -> Result<blake3::Hash, &'static str> {
1144        let request_bytes = borsh::to_vec(self).map_err(|_| "Failed to serialize RexRequest")?;
1145        Ok(blake3::hash(&request_bytes))
1146    }
1147}
1148
1149/// The frequency of the REX update.
1150#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
1151pub enum UpdateFrequency {
1152    /// The REX will update once.
1153    #[default]
1154    OneShot,
1155    /// The REX will update every N milliseconds.
1156    Periodic(DurationMs),
1157    /// The REX will update every N milliseconds, but only up to end_timestamp_ms.
1158    /// First parameter is the frequency in ms, second parameter is the end timestamp in ms.
1159    LimitedPeriodic(DurationMs, TimestampMs),
1160}
1161
1162/// Starting timestamp configuration for REX requests
1163#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
1164pub enum StartingTimestamp {
1165    /// Start at a specific timestamp in milliseconds
1166    Timestamp(TimestampMs),
1167    /// Start as soon as possible
1168    Asap,
1169}
1170
1171impl Default for StartingTimestamp {
1172    fn default() -> Self {
1173        StartingTimestamp::Timestamp(TimestampMs::ZERO)
1174    }
1175}
1176
1177impl UpdateFrequency {
1178    /// Creates a periodic update frequency from a [`Duration`].
1179    pub fn periodic(duration: Duration) -> Self {
1180        Self::Periodic(DurationMs::new(duration.as_millis() as u64))
1181    }
1182}
1183
1184impl StartingTimestamp {
1185    pub fn start_offset(offset: Duration) -> Self {
1186        let timestamp = SystemTime::now() + offset;
1187        Self::Timestamp(TimestampMs::new(
1188            timestamp.duration_since(UNIX_EPOCH).unwrap().as_millis() as u64,
1189        ))
1190    }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195    use super::*;
1196
1197    fn http_post(body: RexValue) -> TargetRexProgram {
1198        TargetRexProgram::HttpPost {
1199            url: "https://example.com".into(),
1200            filter: None,
1201            body,
1202            content_type: "application/json".to_string(),
1203            headers: Headers::default(),
1204        }
1205    }
1206
1207    fn ws_send(messages: Vec<RexValue>) -> TargetRexProgram {
1208        TargetRexProgram::WebSocket(WebSocketOperation::Send {
1209            connection_rex_id: RexId::default(),
1210            messages,
1211        })
1212    }
1213
1214    #[test]
1215    fn test_is_dkg_encrypted_false_for_plain_carriers() {
1216        assert!(!TargetRexProgram::Time.is_dkg_encrypted());
1217        assert!(!TargetRexProgram::HttpGet {
1218            url: "https://example.com".into(),
1219            filter: None,
1220            headers: Headers::default(),
1221        }
1222        .is_dkg_encrypted());
1223        assert!(!http_post(RexValue::plain(vec![1, 2, 3])).is_dkg_encrypted());
1224        assert!(!ws_send(vec![RexValue::plain(vec![1])]).is_dkg_encrypted());
1225        assert!(!TargetRexProgram::Wasm {
1226            bytecode_account: Pubkey::new_unique(),
1227            input: vec![RexValue::plain(vec![1]), RexValue::plain(vec![2])],
1228            program_index: None,
1229        }
1230        .is_dkg_encrypted());
1231    }
1232
1233    /// `encrypted_payloads` is the single source the router and the on-chain
1234    /// ciphertext-match check both build on, so it must surface the right bytes
1235    /// from every carrier — not merely report a bool.
1236    #[test]
1237    fn test_encrypted_payloads_yields_ciphertext_from_every_carrier() {
1238        let get = TargetRexProgram::HttpGet {
1239            url: RexValue::Encrypted(vec![0x02, 1, 2]).into(),
1240            filter: None,
1241            headers: Headers::default(),
1242        };
1243        assert_eq!(
1244            get.encrypted_payloads().collect::<Vec<_>>(),
1245            vec![&[0x02, 1, 2][..]]
1246        );
1247        assert!(get.is_dkg_encrypted());
1248
1249        let post = http_post(RexValue::Encrypted(vec![0x02, 7]));
1250        assert_eq!(
1251            post.encrypted_payloads().collect::<Vec<_>>(),
1252            vec![&[0x02, 7][..]]
1253        );
1254        assert!(post.is_dkg_encrypted());
1255
1256        // WebSocket Send: only the encrypted message(s), in order.
1257        let ws = ws_send(vec![
1258            RexValue::plain(vec![9]),
1259            RexValue::Encrypted(vec![0x02, 8]),
1260            RexValue::Encrypted(vec![0x02, 5]),
1261        ]);
1262        assert_eq!(
1263            ws.encrypted_payloads().collect::<Vec<_>>(),
1264            vec![&[0x02, 8][..], &[0x02, 5][..]]
1265        );
1266        assert!(ws.is_dkg_encrypted());
1267
1268        // WASM: a non-first encrypted arg is still surfaced (preserves order).
1269        let wasm = TargetRexProgram::Wasm {
1270            bytecode_account: Pubkey::new_unique(),
1271            input: vec![RexValue::plain(vec![1]), RexValue::Encrypted(vec![0x02, 9])],
1272            program_index: None,
1273        };
1274        assert_eq!(
1275            wasm.encrypted_payloads().collect::<Vec<_>>(),
1276            vec![&[0x02, 9][..]]
1277        );
1278        assert!(wasm.is_dkg_encrypted());
1279
1280        // Plain program yields nothing.
1281        assert!(TargetRexProgram::Time.encrypted_payloads().next().is_none());
1282    }
1283
1284    #[test]
1285    fn test_requires_dkg_decryption_rolls_up_across_programs() {
1286        let mut info = RexInfo {
1287            target_rex_programs: vec![TargetRexProgram::Time],
1288            ..RexInfo::default()
1289        };
1290        assert!(!info.requires_dkg_decryption());
1291        info.target_rex_programs
1292            .push(http_post(RexValue::Encrypted(vec![0x02, 1])));
1293        assert!(info.requires_dkg_decryption());
1294    }
1295
1296    fn base_valid_rex_info() -> RexInfo {
1297        RexInfo {
1298            description: "test".to_string(),
1299            target_rex_programs: vec![TargetRexProgram::Time],
1300            // Default is OneShot, which is allowed for both Asap and Timestamp
1301            update_frequency: UpdateFrequency::OneShot,
1302            // Start at timestamp 0 by default
1303            starting_timestamp: StartingTimestamp::Timestamp(TimestampMs::ZERO),
1304            // Keep other fields as default
1305            ..RexInfo::default()
1306        }
1307    }
1308
1309    #[test]
1310    fn test_is_asap_true_and_false() {
1311        let mut info = base_valid_rex_info();
1312        assert!(!info.is_asap());
1313        info.starting_timestamp = StartingTimestamp::Asap;
1314        assert!(info.is_asap());
1315    }
1316
1317    #[test]
1318    fn test_validate_success_minimal() {
1319        let info = base_valid_rex_info();
1320        assert!(info.validate().is_ok());
1321    }
1322
1323    #[test]
1324    fn test_asap_cannot_be_periodic() {
1325        let mut info = base_valid_rex_info();
1326        info.starting_timestamp = StartingTimestamp::Asap;
1327        info.update_frequency = UpdateFrequency::Periodic(DurationMs::new(10));
1328        let err = info.validate().unwrap_err();
1329        assert!(err.contains("ASAP REX requests cannot be periodic"));
1330    }
1331
1332    #[test]
1333    fn test_asap_cannot_be_limited_periodic() {
1334        let mut info = base_valid_rex_info();
1335        info.starting_timestamp = StartingTimestamp::Asap;
1336        info.update_frequency =
1337            UpdateFrequency::LimitedPeriodic(DurationMs::new(5), TimestampMs::new(100));
1338        let err = info.validate().unwrap_err();
1339        assert!(err.contains("ASAP REX requests cannot be periodic"));
1340    }
1341
1342    #[test]
1343    fn test_periodic_with_zero_period_is_invalid() {
1344        let mut info = base_valid_rex_info();
1345        info.starting_timestamp = StartingTimestamp::Timestamp(TimestampMs::new(1));
1346        info.update_frequency = UpdateFrequency::Periodic(DurationMs::ZERO);
1347        let err = info.validate().unwrap_err();
1348        assert!(err.contains("update frequency cannot be zero"));
1349    }
1350
1351    #[test]
1352    fn test_limited_periodic_with_zero_period_is_invalid() {
1353        let mut info = base_valid_rex_info();
1354        info.starting_timestamp = StartingTimestamp::Timestamp(TimestampMs::new(1));
1355        info.update_frequency =
1356            UpdateFrequency::LimitedPeriodic(DurationMs::ZERO, TimestampMs::new(100));
1357        let err = info.validate().unwrap_err();
1358        assert!(err.contains("update frequency cannot be zero"));
1359    }
1360
1361    #[test]
1362    fn test_limited_periodic_end_timestamp_must_be_above_starting_timestamp() {
1363        let mut info = base_valid_rex_info();
1364        info.starting_timestamp = StartingTimestamp::Timestamp(TimestampMs::new(500));
1365        info.update_frequency =
1366            UpdateFrequency::LimitedPeriodic(DurationMs::new(300), TimestampMs::new(500));
1367        let err = info.validate().unwrap_err();
1368        assert!(err
1369            .contains("end_timestamp of a LimitedPeriodic REX should be above starting_timestamp"));
1370    }
1371
1372    #[test]
1373    fn test_limited_periodic_end_timestamp_below_starting_timestamp_is_invalid() {
1374        let mut info = base_valid_rex_info();
1375        info.starting_timestamp = StartingTimestamp::Timestamp(TimestampMs::new(1000));
1376        info.update_frequency =
1377            UpdateFrequency::LimitedPeriodic(DurationMs::new(300), TimestampMs::new(900));
1378        let err = info.validate().unwrap_err();
1379        assert!(err
1380            .contains("end_timestamp of a LimitedPeriodic REX should be above starting_timestamp"));
1381    }
1382
1383    #[test]
1384    fn test_target_rex_programs_cannot_be_empty() {
1385        let mut info = base_valid_rex_info();
1386        info.target_rex_programs.clear();
1387        let err = info.validate().unwrap_err();
1388        assert!(err.contains("RexTargets cannot be empty"));
1389    }
1390
1391    #[test]
1392    fn test_rex_request_delay_bounds() {
1393        // Below minimum
1394        let mut info = base_valid_rex_info();
1395        info.request_delay_ms = RexDutyConfig::MIN_REX_REQUEST_DELAY - DurationMs::new(1);
1396        let err = info.validate().unwrap_err();
1397        assert!(err.contains(&format!(
1398            "rex_request_delay cannot be below {}",
1399            RexDutyConfig::MIN_REX_REQUEST_DELAY
1400        )));
1401
1402        // Above maximum
1403        let mut info = base_valid_rex_info();
1404        info.request_delay_ms = RexDutyConfig::MAX_REX_REQUEST_DELAY_MS + DurationMs::new(1);
1405        let err = info.validate().unwrap_err();
1406        assert!(err.contains(&format!(
1407            "rex_request_delay cannot be above {}",
1408            RexDutyConfig::MAX_REX_REQUEST_DELAY_MS
1409        )));
1410    }
1411
1412    #[test]
1413    fn test_validators_per_duty_cannot_be_zero() {
1414        let mut info = base_valid_rex_info();
1415        info.validators_per_duty = 0;
1416        let err = info.validate().unwrap_err();
1417        assert!(err.contains("validators_per_duty cannot be 0"));
1418    }
1419
1420    #[test]
1421    fn test_validators_per_duty_too_high_results_in_too_low_output_size() {
1422        let mut info = base_valid_rex_info();
1423        info.validators_per_duty = 1_000_000; // ridiculously high
1424        let err = info.validate().unwrap_err();
1425        assert!(err.contains("validators_per_duty is too high"));
1426    }
1427
1428    #[test]
1429    fn test_rex_value_plain_deserialize_from_byte_array() {
1430        let json = r#"{"Plain":[104,101,108,108,111]}"#;
1431        let value: RexValue = serde_json::from_str(json).unwrap();
1432        assert_eq!(value, RexValue::Plain(b"hello".to_vec()));
1433    }
1434
1435    #[test]
1436    fn test_rex_value_plain_deserialize_from_string() {
1437        let json = r#"{"Plain":"hello"}"#;
1438        let value: RexValue = serde_json::from_str(json).unwrap();
1439        assert_eq!(value, RexValue::Plain(b"hello".to_vec()));
1440    }
1441
1442    #[test]
1443    fn test_rex_value_encrypted_deserialize_from_string() {
1444        let json = r#"{"Encrypted":"base64data"}"#;
1445        let value: RexValue = serde_json::from_str(json).unwrap();
1446        assert_eq!(value, RexValue::Encrypted(b"base64data".to_vec()));
1447    }
1448
1449    #[test]
1450    fn test_rex_value_encrypted_deserialize_from_byte_array() {
1451        let json = r#"{"Encrypted":[65,66,67]}"#;
1452        let value: RexValue = serde_json::from_str(json).unwrap();
1453        assert_eq!(value, RexValue::Encrypted(b"ABC".to_vec()));
1454    }
1455
1456    /// Plain borsh-serializable arg → `RexValue::Plain(borsh_bytes)`.
1457    #[test]
1458    fn test_into_rex_value_for_plain() {
1459        let out = <u64 as IntoRexValueFor<u64>>::into_rex_value_for(1_000_000u64);
1460        assert_eq!(out, RexValue::Plain(borsh::to_vec(&1_000_000u64).unwrap()));
1461    }
1462
1463    /// `EncryptedInput<T>` → `RexValue::Encrypted(ciphertext)` with `T`
1464    /// as the target rex-fn param type.
1465    #[test]
1466    fn test_into_rex_value_for_encrypted() {
1467        let ct = vec![0xAAu8; 56];
1468        let wrapped = EncryptedInput::<u64>::new(ct.clone());
1469        let out = <EncryptedInput<u64> as IntoRexValueFor<u64>>::into_rex_value_for(wrapped);
1470        assert_eq!(out, RexValue::Encrypted(ct));
1471    }
1472}