diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 25e8b1f5c..f265cd8ce 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -666,6 +666,15 @@ dependencies = [ "solana-program", ] +[[package]] +name = "chainlink_solana_data_streams" +version = "1.0.0" +dependencies = [ + "borsh 0.10.3", + "solana-program", + "solana-sdk", +] + [[package]] name = "chrono" version = "0.4.34" diff --git a/contracts/crates/chainlink-solana-data-streams/Cargo.toml b/contracts/crates/chainlink-solana-data-streams/Cargo.toml new file mode 100644 index 000000000..1834cb37a --- /dev/null +++ b/contracts/crates/chainlink-solana-data-streams/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "chainlink_solana_data_streams" +description = "Chainlink Data Streams Uility for Solana. Can be used on-chain/off-chain to get `verify` transaction instructions." +version = "1.0.0" +edition = "2018" +license = "MIT" + +[lib] +crate-type = ["cdylib", "lib"] +name = "chainlink_solana_data_streams" + +[features] +default = [] + +[dependencies] +borsh = "0.10.3" + +[target.'cfg(target_os = "solana")'.dependencies] +solana-program = { version = ">=1.17" } + +[target.'cfg(not(target_os = "solana"))'.dependencies] +solana-sdk = { version = ">=1.17" } + +[dev-dependencies] +solana-sdk = ">=1.17" \ No newline at end of file diff --git a/contracts/crates/chainlink-solana-data-streams/LICENSE b/contracts/crates/chainlink-solana-data-streams/LICENSE new file mode 100644 index 000000000..812debd8e --- /dev/null +++ b/contracts/crates/chainlink-solana-data-streams/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 SmartContract ChainLink, Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/contracts/crates/chainlink-solana-data-streams/README.md b/contracts/crates/chainlink-solana-data-streams/README.md new file mode 100644 index 000000000..9216b522c --- /dev/null +++ b/contracts/crates/chainlink-solana-data-streams/README.md @@ -0,0 +1,3 @@ +# chainlink-solana-data-streams + +This tool is provided under an MIT license and is for convenience and illustration purposes only. diff --git a/contracts/crates/chainlink-solana-data-streams/src/lib.rs b/contracts/crates/chainlink-solana-data-streams/src/lib.rs new file mode 100644 index 000000000..78d9805b1 --- /dev/null +++ b/contracts/crates/chainlink-solana-data-streams/src/lib.rs @@ -0,0 +1,121 @@ +//! Chainlink Data Streams Client for Solana + +mod solana { + #[cfg(not(target_os = "solana"))] + pub use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }; + + #[cfg(target_os = "solana")] + pub use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }; +} + +use crate::solana::{AccountMeta, Instruction, Pubkey}; +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Program function name discriminators +pub mod discriminator { + pub const VERIFY: [u8; 8] = [133, 161, 141, 48, 120, 198, 88, 150]; +} + +#[derive(BorshSerialize, BorshDeserialize)] +struct VerifyParams { + signed_report: Vec, +} + +/// A helper struct for creating Verifier program instructions +pub struct VerifierInstructions; + +impl VerifierInstructions { + /// Creates a verify instruction. + /// + /// # Parameters: + /// + /// * `program_id` - The public key of the verifier program. + /// * `verifier_account` - The public key of the verifier account. The function [`Self::get_verifier_config_pda`] can be used to calculate this. + /// * `access_controller_account` - The public key of the access controller account. + /// * `user` - The public key of the user - this account must be a signer + /// * `report_config_account` - The public key of the report configuration account. The function [`Self::get_config_pda`] can be used to calculate this. + /// * `signed_report` - The signed report data as a vector of bytes. Returned from data streams API/WS + /// + /// # Returns + /// + /// Returns an `Instruction` object that can be sent to the Solana runtime. + pub fn verify( + program_id: &Pubkey, + verifier_account: &Pubkey, + access_controller_account: &Pubkey, + user: &Pubkey, + report_config_account: &Pubkey, + signed_report: Vec, + ) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*verifier_account, false), + AccountMeta::new_readonly(*access_controller_account, false), + AccountMeta::new_readonly(*user, true), + AccountMeta::new_readonly(*report_config_account, false), + ]; + + // 8 bytes for discriminator + // 4 bytes size of the length prefix for the signed_report vector + let mut instruction_data = Vec::with_capacity(8 + 4 + signed_report.len()); + instruction_data.extend_from_slice(&discriminator::VERIFY); + + let params = VerifyParams { signed_report }; + let param_data = params.try_to_vec().unwrap(); + instruction_data.extend_from_slice(¶m_data); + + Instruction { + program_id: *program_id, + accounts, + data: instruction_data, + } + } + + /// Helper to compute the verifier config PDA account. + pub fn get_verifier_config_pda(program_id: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&[b"verifier"], program_id).0 + } + + /// Helper to compute the report config PDA account. This uses the first 32 bytes of the + /// uncompressed report as the seed. This is validated within the verifier program + pub fn get_config_pda(report: &[u8], program_id: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&[&report[..32]], program_id).0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_verify_instruction() { + let program_id = Pubkey::new_unique(); + let verifier = Pubkey::new_unique(); + let controller = Pubkey::new_unique(); + let user = Pubkey::new_unique(); + let report = vec![1u8; 64]; + + // Calculate expected PDA before moving report + let expected_config = VerifierInstructions::get_config_pda(&report, &program_id); + + let ix = VerifierInstructions::verify( + &program_id, + &verifier, + &controller, + &user, + &expected_config, + report, + ); + + assert!(ix.data.starts_with(&discriminator::VERIFY)); + assert_eq!(ix.program_id, program_id); + assert_eq!(ix.accounts.len(), 4); + assert!(ix.accounts[2].is_signer); + assert_eq!(ix.accounts[3].pubkey, expected_config); + } +}