lyquor_primitives/
oracle.rs

1use super::*;
2
3#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug, Hash)]
4pub enum OracleServiceTarget {
5    // Lyquor network fn
6    LVM(LyquidID),
7    // EVM-based sequence backend
8    EVM {
9        /// Final destination contract that the sequencing contract calls into.
10        target: Address,
11        /// Sequencing contract on the destination backend that must receive the cert.
12        eth_contract: Address,
13    },
14}
15
16#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug, Hash)]
17pub struct OracleTarget {
18    /// Service destination of the certified call.
19    pub target: OracleServiceTarget,
20    /// Sequence backend this target belongs to.
21    pub seq_id: SequenceBackendID,
22}
23
24impl OracleTarget {
25    pub fn cipher(&self) -> Cipher {
26        match self.target {
27            OracleServiceTarget::EVM { .. } => Cipher::Secp256k1,
28            OracleServiceTarget::LVM(_) => Cipher::Ed25519,
29        }
30    }
31}
32
33/// Contains all fields needed to define a call other than the call parameters.
34#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)]
35pub struct OracleHeader {
36    /// Node that proposed the call for certification.
37    pub proposer: NodeID,
38    /// Destination of the call (where it will be finally executed).
39    pub target: OracleTarget,
40    /// Oracle config digest.
41    pub config_hash: HashBytes,
42    /// Epoch number used by OracleDest.
43    pub epoch: u32,
44    /// Random nonce that uniquely identifies the certified call within an epoch.
45    pub nonce: HashBytes,
46}
47
48#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)]
49pub struct OracleEpochInfo {
50    pub epoch: u32,
51    pub config_hash: HashBytes,
52}
53
54pub type SignerID = u32;
55
56#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
57pub struct OracleSigner {
58    pub id: SignerID,
59    pub key: Bytes,
60}
61
62#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
63pub struct OracleConfig {
64    pub committee: Vec<OracleSigner>,
65    pub threshold: u16,
66}
67
68impl OracleConfig {
69    pub fn to_hash(&self) -> Hash {
70        blake3::hash(&encode_object(self))
71    }
72}
73
74#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
75pub struct OracleConfigDelta {
76    pub upsert: Vec<OracleSigner>,
77    pub remove: Vec<SignerID>,
78    pub threshold: Option<u16>,
79}
80
81#[derive(Serialize, Deserialize)]
82pub struct ValidatePreimage {
83    pub header: OracleHeader,
84    pub params: CallParams,
85    pub approval: bool,
86}
87
88impl ValidatePreimage {
89    const PREFIX: &'static [u8] = b"lyquor_validate_preimage_v1\0";
90
91    pub fn to_preimage(&self) -> Vec<u8> {
92        encode_object_with_prefix(Self::PREFIX, self)
93    }
94
95    pub fn to_hash(&self) -> Hash {
96        blake3::hash(&self.to_preimage())
97    }
98}
99
100/// Oracle certificate that could be sequenced.
101#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
102pub struct OracleCert {
103    pub header: OracleHeader,
104    /// Signers for the signatures in order.
105    pub signers: Vec<SignerID>,
106    /// Vote signatures.
107    pub signatures: Vec<Bytes>,
108}
109
110/// Oracle call groups are represented as `<topic>[::<suffix...>]`.
111#[inline]
112pub fn topic_from_group(group: &str) -> &str {
113    group.split_once("::").map(|(topic, _)| topic).unwrap_or(group)
114}
115
116/// Transport prefix used by the sequencing contract when forwarding certified calls.
117pub const ORACLE_CERTIFIED_GROUP_PREFIX: &str = "oracle::certified::";
118
119/// Convert a dispatch-time certified call group into its topic key.
120///
121/// The input may be either:
122/// - a raw oracle group (`<topic>[::<suffix...>]`), or
123/// - a sequencer-dispatched group (`oracle::certified::<topic>[::<suffix...>]`).
124#[inline]
125pub fn topic_from_dispatch_group(group: &str) -> &str {
126    let group = group.strip_prefix(ORACLE_CERTIFIED_GROUP_PREFIX).unwrap_or(group);
127    topic_from_group(group)
128}
129
130/// Build a full oracle group string from topic and optional suffix.
131#[inline]
132pub fn group_with_topic_suffix(topic: &str, suffix: Option<&str>) -> String {
133    match suffix {
134        Some(s) if !s.is_empty() => format!("{topic}::{s}"),
135        _ => topic.to_string(),
136    }
137}
138
139pub mod eth {
140    use alloy_sol_types::{SolType, sol};
141    sol! {
142        struct OracleHeader {
143            bytes32 topic;
144            bytes32 group;
145            bytes32 proposer;
146            address target;
147            bytes32 seqId;
148            address ethContract;
149            bytes32 configHash;
150            uint32 epoch;
151            bytes32 nonce;
152        }
153
154        struct OracleSigner {
155            uint32 id;
156            address key;
157        }
158
159        struct OracleConfig {
160            OracleSigner[] committee;
161            uint16 threshold;
162        }
163
164        struct OracleConfigDelta {
165            OracleSigner[] upsert;
166            uint32[] remove;
167            bool thresholdChanged;
168            uint16 threshold;
169        }
170
171        enum ABI {
172            Lyquor,
173            Eth
174        }
175
176        struct CallParams {
177            address origin;
178            address caller;
179            string group;
180            string method;
181            bytes input;
182            ABI abi_;
183        }
184
185        struct ValidatePreimage {
186            OracleHeader header;
187            CallParams params;
188            bool approval; // Should always be true signed by multi-sigs that make up the final cert.
189        }
190    }
191
192    impl OracleConfig {
193        pub fn to_hash(&self) -> super::Hash {
194            alloy_primitives::keccak256(&OracleConfig::abi_encode(self)).0.into()
195        }
196    }
197
198    impl Default for OracleConfig {
199        fn default() -> Self {
200            Self {
201                committee: Default::default(),
202                threshold: Default::default(),
203            }
204        }
205    }
206
207    impl Default for OracleConfigDelta {
208        fn default() -> Self {
209            Self {
210                upsert: Default::default(),
211                remove: Default::default(),
212                thresholdChanged: false,
213                threshold: 0,
214            }
215        }
216    }
217
218    impl ValidatePreimage {
219        const PREFIX: &'static [u8] = b"lyquor_validate_preimage_v1\0";
220
221        pub fn to_preimage(&self) -> Vec<u8> {
222            let mut buf = Vec::from(Self::PREFIX);
223            buf.extend_from_slice(&Self::abi_encode(self));
224            buf
225        }
226
227        pub fn to_hash(&self) -> super::Hash {
228            alloy_primitives::keccak256(&self.to_preimage()).0.into()
229        }
230    }
231
232    impl From<OracleHeader> for super::OracleHeader {
233        fn from(oh: OracleHeader) -> Self {
234            Self {
235                proposer: <[u8; 32]>::from(oh.proposer).into(),
236                target: super::OracleTarget {
237                    target: super::OracleServiceTarget::EVM {
238                        target: oh.target,
239                        eth_contract: oh.ethContract,
240                    },
241                    seq_id: <[u8; 32]>::from(oh.seqId).into(),
242                },
243                config_hash: <[u8; 32]>::from(oh.configHash).into(),
244                epoch: oh.epoch,
245                nonce: <[u8; 32]>::from(oh.nonce).into(),
246            }
247        }
248    }
249
250    impl From<super::OracleConfig> for OracleConfig {
251        fn from(oc: super::OracleConfig) -> Self {
252            Self {
253                committee: oc
254                    .committee
255                    .into_iter()
256                    .map(|s| OracleSigner {
257                        id: s.id,
258                        key: s.key.as_ref().try_into().unwrap(),
259                    })
260                    .collect(),
261                threshold: oc.threshold as u16,
262            }
263        }
264    }
265
266    impl TryFrom<super::ValidatePreimage> for ValidatePreimage {
267        type Error = ();
268        fn try_from(om: super::ValidatePreimage) -> Result<Self, ()> {
269            let params = om.params;
270            let topic = super::topic_from_group(params.group.as_str());
271            let topic_hash = alloy_primitives::keccak256(topic.as_bytes());
272            let group_hash = alloy_primitives::keccak256(params.group.as_bytes());
273            let (target, eth_contract) = match om.header.target.target {
274                super::OracleServiceTarget::LVM(_) => return Err(()),
275                super::OracleServiceTarget::EVM { target, eth_contract } => (target, eth_contract),
276            };
277            let params = CallParams {
278                origin: params.origin,
279                caller: params.caller,
280                group: params.group,
281                method: params.method,
282                input: params.input.into(),
283                abi_: match params.abi {
284                    super::InputABI::Lyquor => ABI::Lyquor,
285                    super::InputABI::Eth => ABI::Eth,
286                },
287            };
288            let header = OracleHeader {
289                topic: topic_hash.into(),
290                group: group_hash.into(),
291                proposer: <[u8; 32]>::from(om.header.proposer).into(),
292                target,
293                seqId: <[u8; 32]>::from(om.header.target.seq_id).into(),
294                ethContract: eth_contract,
295                configHash: <[u8; 32]>::from(om.header.config_hash).into(),
296                epoch: om.header.epoch,
297                nonce: <[u8; 32]>::from(om.header.nonce).into(),
298            };
299
300            Ok(Self {
301                header,
302                params,
303                approval: om.approval,
304            })
305        }
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn topic_from_group_uses_first_segment() {
315        assert_eq!(topic_from_group("price_feed"), "price_feed");
316        assert_eq!(topic_from_group("price_feed::two_phase"), "price_feed");
317    }
318
319    #[test]
320    fn topic_from_dispatch_group_accepts_both_forms() {
321        assert_eq!(topic_from_dispatch_group("price_feed"), "price_feed");
322        assert_eq!(topic_from_dispatch_group("price_feed::two_phase"), "price_feed");
323        assert_eq!(topic_from_dispatch_group("oracle::certified::price_feed"), "price_feed");
324        assert_eq!(
325            topic_from_dispatch_group("oracle::certified::price_feed::two_phase"),
326            "price_feed"
327        );
328    }
329}