1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// 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);
}
}