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