rialo_oracle_processor_interface/
instruction.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Instructions for the Oracle Processor program.
5//!
6//! This module defines the instructions for communicating resulting data from Oracles.
7
8use rialo_s_instruction::{AccountMeta, Instruction};
9use rialo_s_program::system_program;
10use rialo_s_pubkey::Pubkey;
11use rialo_types::{Nonce, OracleId};
12use serde::{Deserialize, Serialize};
13
14pub use crate::state::OracleUpdate;
15
16/// Static seed for oracle update PDA derivation
17pub const ORACLE_REPORT_SEED: &[u8] = b"oracle_report";
18
19/// Static seed for payer PDA derivation
20pub const ORACLE_REPORT_PAYER_SEED: &[u8] = b"report_payer";
21
22/// Default payer for oracle updates - synchronized with `svm-execution/src/bank.rs`
23pub const ORACLE_UPDATE_PAYER: Pubkey =
24    Pubkey::from_str_const("Qrac1eUpdatePayer11111111111111111111111111");
25
26/// Instructions supported by the Oracle Processor program. Made to report oracle updates from
27/// validators.
28#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
29pub enum OracleProcessorInstruction {
30    /// Report oracle updates from validators
31    Report {
32        /// Unique identifier for the oracle
33        oracle_id: OracleId,
34        /// Updates from each validator
35        updates: Vec<OracleUpdate>,
36    },
37}
38
39impl OracleProcessorInstruction {
40    /// Creates instructions for oracle reporting.
41    ///
42    /// This function creates an instruction that will report an oracle update.
43    ///
44    /// # Arguments
45    ///
46    /// * `payer` - The public key of the payer
47    /// * `aggregators` - A vector of tuples containing the topic and the aggregator program ID.
48    ///   Note: If `updates` is empty, no aggregation instructions will be created regardless
49    ///   of the aggregators provided, as there is no data to aggregate.
50    /// * `oracle_id` - The ID of the oracle
51    /// * `updates` - A vector of oracle updates. If empty, no aggregation instructions
52    ///   will be generated.
53    ///
54    /// # Returns
55    ///
56    /// A vector of instructions to include in the transaction:
57    /// - First instruction: Report instruction that stores oracle data in a PDA
58    /// - Subsequent instructions: One aggregation instruction for each aggregator
59    ///   (only if `updates` is not empty)
60    ///
61    /// # Behavior
62    ///
63    /// When `updates` is empty, the function will:
64    /// - Still create the report instruction (to maintain consistency)
65    /// - Skip all aggregation instructions, effectively ignoring the `aggregators` parameter
66    /// - Return a vector containing only the report instruction
67    ///
68    /// This behavior prevents triggering aggregators when there is no oracle data to process.
69    pub fn report(
70        payer: Pubkey,
71        aggregators: Vec<(String, Pubkey)>,
72        oracle_id: OracleId,
73        updates: Vec<OracleUpdate>,
74    ) -> Vec<Instruction> {
75        // Derive the PDA for the oracle account
76        let (report_key, _bump) = derive_report_address(&oracle_id.creator, oracle_id.nonce);
77
78        let aggregators = if updates.is_empty() {
79            // Don't trigger the aggregator if there are no updates
80            vec![]
81        } else {
82            aggregators
83        };
84
85        // Create accounts vector using iterators and collect
86        // Start with a vector of capacity 3 (payer, report, and system program) + 2 for each aggregator
87        let mut itxs = Vec::with_capacity(1 + aggregators.len());
88
89        itxs.push(Instruction::new_with_bincode(
90            crate::id(),
91            &OracleProcessorInstruction::Report { oracle_id, updates },
92            vec![
93                AccountMeta::new(payer, true),
94                AccountMeta::new(report_key, false),
95                AccountMeta::new_readonly(system_program::id(), false),
96            ],
97        ));
98
99        // Add aggregators instructions
100        for (topic, aggregator) in aggregators {
101            // Push aggregator instruction
102            itxs.push(
103                rialo_aggregator_interface::instruction::AggregatorInstruction::aggregate(
104                    &aggregator,
105                    &payer,
106                    &report_key,
107                    topic,
108                ),
109            );
110        }
111
112        itxs
113    }
114}
115
116/// Derive the PDA for an oracle report account
117///
118/// # Arguments
119///
120/// * `creator` - The creator of the oracle
121/// * `nonce` - The nonce of the oracle
122///
123/// # Returns
124///
125/// A tuple containing the PDA and the bump seed
126pub fn derive_report_address<NONCE: Into<Nonce>>(creator: &Pubkey, nonce: NONCE) -> (Pubkey, u8) {
127    let nonce: Nonce = nonce.into();
128    Pubkey::find_program_address(
129        &[ORACLE_REPORT_SEED, &creator.to_bytes(), nonce.as_bytes()],
130        &crate::id(),
131    )
132}
133
134#[cfg(test)]
135mod tests {
136    use rialo_events_core::RialoEvent;
137    use rialo_s_pubkey::Pubkey;
138
139    use super::*;
140
141    #[derive(Default, RialoEvent)]
142    struct TestEvent {
143        name: String,
144        value: u64,
145    }
146
147    #[test]
148    fn test_report_oracle_updates() {
149        let aggregators = vec![
150            ("aggregator1".to_string(), Pubkey::new_unique()),
151            ("aggregator2".to_string(), Pubkey::new_unique()),
152        ];
153        let validator = Pubkey::new_unique();
154        let payer = Pubkey::new_unique();
155        let data = vec![0, 1, 2, 3];
156        let updates = vec![OracleUpdate { data, validator }];
157        let nonce = Nonce::from(b"test-oracle-42");
158        let oracle_id = OracleId::new(payer, nonce);
159
160        // Create the instruction
161        let instruction = OracleProcessorInstruction::report(
162            payer,
163            aggregators.clone(),
164            oracle_id,
165            updates.clone(),
166        );
167        // Check that the instruction is correct
168        assert_eq!(instruction.len(), 1 + aggregators.len()); // Report instruction + one for each aggregator
169
170        // Check the report instruction (first instruction in the vector)
171        let report_instruction = &instruction[0];
172        let expected_accounts = vec![
173            AccountMeta::new(payer, true),
174            AccountMeta::new(
175                derive_report_address(&oracle_id.creator, oracle_id.nonce).0,
176                false,
177            ),
178            AccountMeta::new_readonly(system_program::id(), false),
179        ];
180
181        // Verify the instruction data
182        let expected_data = bincode::serialize(&OracleProcessorInstruction::Report {
183            oracle_id,
184            updates: updates.clone(),
185        })
186        .unwrap();
187
188        assert_eq!(report_instruction.data, expected_data);
189        assert_eq!(report_instruction.accounts, expected_accounts);
190
191        // Check the aggregator instructions
192        for (i, (topic, aggregator)) in aggregators.into_iter().enumerate() {
193            let agg_instruction = &instruction[i + 1];
194
195            // Create the expected instruction to compare with
196            let expected_agg_instruction =
197                rialo_aggregator_interface::instruction::AggregatorInstruction::aggregate(
198                    &aggregator,
199                    &payer,
200                    &derive_report_address(&oracle_id.creator, oracle_id.nonce).0,
201                    topic,
202                );
203
204            assert_eq!(agg_instruction, &expected_agg_instruction);
205        }
206    }
207
208    #[test]
209    fn test_report_address_derivation() {
210        let oracle_creator = Pubkey::new_unique();
211        let (report_pubkey1, _) = derive_report_address(&oracle_creator, b"test-oracle-a42");
212        let (report_pubkey2, _) = derive_report_address(&oracle_creator, b"test-oracle-43");
213        assert_ne!(report_pubkey1, report_pubkey2);
214    }
215}