rialo-types 0.12.2

Rialo Types
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use rialo_limits::max_rex_output_serialized_bytes;
use serde::{Deserialize, Serialize};

use crate::{
    rex_info::{RexInfo, TimestampMs},
    websocket_op::WebSocketOperation,
};

/// Configuration for REX duties.
///
/// This struct defines the parameters that control how REX duties are scheduled,
/// executed, and finalized within the system.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RexDutyConfig {
    /// The number of validators assigned to each REX duty.
    pub validators_per_duty: u32,
    /// The time in milliseconds needed for the fulfillment of a REX request.
    pub request_delay_ms: TimestampMs,
    /// Max size of REX output in bytes.
    pub max_output_size: u32,
    /// Whether this duty is ASAP (execute as soon as possible) rather than scheduled.
    pub is_asap: bool,
    /// WebSocket operation information needed to update the WebSocketRegistry with the
    /// on-chain connection state when this duty completes.
    pub websocket_op: Option<WebSocketOperation>,
    /// Marks a synthetic DKG partial-decrypt child duty whose updates
    /// are bank-persisted in `RexDutiesMap` so the combiner can read
    /// them after snapshot reload, but `result_cache` must NOT generate
    /// a Report tx for it (no on-chain RexInfo to report against; the
    /// user-visible result rides the *parent* rex_id).
    /// `#[serde(default)]` lets snapshots written before this field
    /// existed deserialize cleanly with `false`. Removing the default
    /// would break forward-compat on snapshot reload.
    #[serde(default)]
    pub is_synthetic_partial: bool,
}

impl Default for RexDutyConfig {
    fn default() -> Self {
        Self {
            // 3-validator committees gives us 99.996% success rate leveraging TEE guarantees
            validators_per_duty: Self::DEFAULT_VALIDATORS_PER_DUTY,
            // Time to execute a REX task, determined experimentally
            request_delay_ms: Self::DEFAULT_REQUEST_DELAY_MS,
            // Safe size for the default number of validators.
            max_output_size: Self::DEFAULT_MAX_OUTPUT_SIZE,
            is_asap: false,
            websocket_op: None,
            is_synthetic_partial: false,
        }
    }
}

impl RexDutyConfig {
    /// Constructs an instance based on RexInfo.
    pub fn from_rex_info(rex_info: &RexInfo) -> Self {
        Self {
            validators_per_duty: rex_info.validators_per_duty,
            request_delay_ms: rex_info.request_delay_ms,
            max_output_size: max_rex_output_serialized_bytes(rex_info.validators_per_duty) as u32,
            is_asap: rex_info.is_asap(),
            websocket_op: rex_info.websocket_op(),
            is_synthetic_partial: false,
        }
    }

    /// Safe max size limit for the default number of validators.
    pub const DEFAULT_MAX_OUTPUT_SIZE: u32 = 10 * 1024;

    /// A reasonably safe value to make one-off REX requests succeed with >22 bits of security.
    /// This value is based on the example from the Rialo whitepaper, that is N = 130, D = 43.
    pub const ONE_OFF_VALIDATORS_PER_DUTY: u32 = 13;

    /// A small value for REX requests that is reasonably cheap.
    /// Note that by itself it doesn't provide a response reliably enough
    /// and needs retries in case of failure.
    pub const DEFAULT_VALIDATORS_PER_DUTY: u32 = 5;

    /// Most REX requests should complete within a few blocks.
    pub const DEFAULT_REQUEST_DELAY_MS: TimestampMs = 300; // 0.3s

    /// The minimum allowed delay for REX requests, in milliseconds.
    pub const MIN_REX_REQUEST_DELAY: TimestampMs = 100; // 0.1s

    /// The maximum allowed delay for REX requests, in milliseconds.
    pub const MAX_REX_REQUEST_DELAY_MS: TimestampMs = 240_000; // 240s

    /// Default maximum number of ASAP duties to process in a single consensus round
    pub const DEFAULT_MAX_ASAP_DUTIES_PER_COMMIT: usize = 5;

    /// Default timeout for ASAP duties in milliseconds
    pub const DEFAULT_ASAP_TIMEOUT_MS: TimestampMs = 5_000; // 5s

    /// Pipeline overhead budgeted for DKG-encrypted duties, on top of the HTTP
    /// `request_delay_ms`. A plain ASAP duty is collected within
    /// `DEFAULT_ASAP_TIMEOUT_MS` (one validator fetches directly); an encrypted
    /// duty has no result until the committee posts threshold partials and the
    /// combiner runs — a multi-round pipeline that overruns the plain timeout.
    ///
    /// Sizes the encrypted duty's collection window (`DutyRequest::inner`,
    /// `created + overhead + request_delay_ms`) so the combine result lands
    /// inside it instead of being dropped as `OutsideCollectionWindow`. The
    /// on-chain `original_target_timestamp` bound
    /// (`max_asap_original_target_timestamp`) accepts that window plus a further
    /// `overhead` grace for the created→scheduled gap. Independent of the DRA
    /// `deadline_timestamp`, which only gates partial submission.
    ///
    /// The realized margin is exposed as `rex_dkg_combine_window_slack_ms`.
    pub const DKG_ENCRYPTED_PIPELINE_OVERHEAD_MS: TimestampMs = 30_000; // 30s

    /// Sets the REX request delay (in milliseconds).
    ///
    /// # Arguments
    ///
    /// * `delay` - The time in milliseconds to wait before issuing a REX request.
    pub fn set_rex_request_delay_ms(mut self, delay: TimestampMs) -> Self {
        self.request_delay_ms = delay;
        self
    }

    /// Sets the desired number of validators per duty.
    pub fn set_validators_per_duty(mut self, validators_per_duty: u32) -> RexDutyConfig {
        self.validators_per_duty = validators_per_duty;
        self
    }

    /// Marks this duty as a synthetic DKG partial-decrypt child. See the
    /// field doc on [`RexDutyConfig::is_synthetic_partial`] for why such
    /// duties are bank-persisted but excluded from Report-tx emission.
    pub fn set_is_synthetic_partial(mut self, is_synthetic_partial: bool) -> Self {
        self.is_synthetic_partial = is_synthetic_partial;
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rex_info::StartingTimestamp;

    fn rex_info_with(validators_per_duty: u32) -> RexInfo {
        RexInfo {
            validators_per_duty,
            ..RexInfo::default()
        }
    }

    /// `from_rex_info` must size `max_output_size` from the committee size, NOT
    /// reuse the `Default` 10 KiB. A combine duty that kept the default
    /// disagreed with the verifier's committed output bound and produced a
    /// `commitment_mismatch`.
    #[test]
    fn from_rex_info_sizes_output_from_committee_not_default() {
        for validators_per_duty in [1, 5, RexDutyConfig::ONE_OFF_VALIDATORS_PER_DUTY] {
            let config = RexDutyConfig::from_rex_info(&rex_info_with(validators_per_duty));
            assert_eq!(
                config.max_output_size,
                max_rex_output_serialized_bytes(validators_per_duty) as u32,
                "max_output_size must be derived from validators_per_duty={validators_per_duty}"
            );
        }
    }

    /// Guards the exact regression: at the realistic committee size the derived
    /// bound differs from the `Default`, so a silent fall-back to the default
    /// would be observable.
    #[test]
    fn from_rex_info_output_size_differs_from_default() {
        let config = RexDutyConfig::from_rex_info(&rex_info_with(
            RexDutyConfig::DEFAULT_VALIDATORS_PER_DUTY,
        ));
        assert_ne!(
            config.max_output_size,
            RexDutyConfig::DEFAULT_MAX_OUTPUT_SIZE,
            "derived size must not collapse to the Default 10 KiB"
        );
    }

    #[test]
    fn from_rex_info_copies_committee_and_delay() {
        let mut rex_info = rex_info_with(7);
        rex_info.request_delay_ms = 1234;
        let config = RexDutyConfig::from_rex_info(&rex_info);
        assert_eq!(config.validators_per_duty, 7);
        assert_eq!(config.request_delay_ms, 1234);
        // A duty built from on-chain RexInfo is never a synthetic partial.
        assert!(!config.is_synthetic_partial);
    }

    #[test]
    fn from_rex_info_maps_asap_flag() {
        let mut asap = rex_info_with(5);
        asap.starting_timestamp = StartingTimestamp::Asap;
        assert!(RexDutyConfig::from_rex_info(&asap).is_asap);

        let mut scheduled = rex_info_with(5);
        scheduled.starting_timestamp = StartingTimestamp::Timestamp(42);
        assert!(!RexDutyConfig::from_rex_info(&scheduled).is_asap);
    }
}