From d907cc5e943e59796e985225166db3cf02df2022 Mon Sep 17 00:00:00 2001 From: Alex Kuznicki Date: Mon, 16 Dec 2024 14:39:32 -0700 Subject: [PATCH] Add utility for generating data streams transaction instructions --- contracts/Cargo.lock | 27 ++-- .../chainlink-solana-data-streams/Cargo.toml | 25 ++++ .../chainlink-solana-data-streams/LICENSE | 21 +++ .../chainlink-solana-data-streams/README.md | 3 + .../chainlink-solana-data-streams/src/lib.rs | 122 ++++++++++++++++++ 5 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 contracts/crates/chainlink-solana-data-streams/Cargo.toml create mode 100644 contracts/crates/chainlink-solana-data-streams/LICENSE create mode 100644 contracts/crates/chainlink-solana-data-streams/README.md create mode 100644 contracts/crates/chainlink-solana-data-streams/src/lib.rs diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 25e8b1f5c..d0d4b5ed7 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "access-controller" @@ -204,7 +204,7 @@ dependencies = [ "arrayref", "base64 0.13.1", "bincode", - "borsh 0.10.3", + "borsh 0.9.3", "bytemuck", "getrandom 0.2.12", "solana-program", @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "borsh" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" dependencies = [ "borsh-derive 0.10.3", "hashbrown 0.13.2", @@ -666,6 +666,15 @@ dependencies = [ "solana-program", ] +[[package]] +name = "chainlink_solana_data_streams" +version = "1.0.0" +dependencies = [ + "borsh 0.10.4", + "solana-program", + "solana-sdk", +] + [[package]] name = "chrono" version = "0.4.34" @@ -1895,7 +1904,7 @@ dependencies = [ "bincode", "bitflags 2.4.2", "blake3", - "borsh 0.10.3", + "borsh 0.10.4", "borsh 0.9.3", "bs58 0.4.0", "bv", @@ -1945,7 +1954,7 @@ dependencies = [ "base64 0.21.7", "bincode", "bitflags 2.4.2", - "borsh 0.10.3", + "borsh 0.10.4", "bs58 0.4.0", "bytemuck", "byteorder", @@ -2044,7 +2053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4414117bead33f2a5cf059cefac0685592bdd36f31f3caac49b89bff7f6bbf32" dependencies = [ "assert_matches", - "borsh 0.10.3", + "borsh 0.10.4", "num-derive 0.4.2", "num-traits", "solana-program", @@ -2103,7 +2112,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85a5db7e4efb1107b0b8e52a13f035437cdcb36ef99c58f6d467f089d9b2915a" dependencies = [ - "borsh 0.10.3", + "borsh 0.10.4", "bytemuck", "solana-program", "solana-zk-token-sdk", @@ -2243,7 +2252,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16aa8f64b6e0eaab3f5034e84d867c8435d8216497b4543a4978a31f4b6e8a8" dependencies = [ - "borsh 0.10.3", + "borsh 0.10.4", "solana-program", "spl-discriminator", "spl-pod", 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..9eb6191ef --- /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 Client for Solana" +version = "1.0.0" +edition = "2018" +license = "MIT" + +[lib] +crate-type = ["cdylib", "lib"] +name = "chainlink_solana_data_streams" + +[features] +default = [] + +[dependencies] +borsh = "0.10.4" + +[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..8fbb999fe --- /dev/null +++ b/contracts/crates/chainlink-solana-data-streams/src/lib.rs @@ -0,0 +1,122 @@ +//! 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); + } +} \ No newline at end of file