From 99aae5c77a54a49848e386e4bcc1cc35008b5373 Mon Sep 17 00:00:00 2001 From: "app-token-issuer-infra-releng[bot]" <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:36:06 -0700 Subject: [PATCH 1/4] [automated] bump solana dependencies (#983) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- integration-tests/testconfig/default.toml | 2 +- scripts/install-solana-ci.sh | 2 +- scripts/setup-localnet/localnet.sh | 2 +- solana.nix | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/integration-tests/testconfig/default.toml b/integration-tests/testconfig/default.toml index 55711bd86..191bda38f 100644 --- a/integration-tests/testconfig/default.toml +++ b/integration-tests/testconfig/default.toml @@ -38,7 +38,7 @@ inside_k8 = false network = "localnet" user = "default" stateful_db = false -devnet_image = "anzaxyz/agave:v2.0.18" +devnet_image = "anzaxyz/agave:v2.0.20" [OCR2] node_count = 6 diff --git a/scripts/install-solana-ci.sh b/scripts/install-solana-ci.sh index 762d3f37c..30dd83c1f 100755 --- a/scripts/install-solana-ci.sh +++ b/scripts/install-solana-ci.sh @@ -2,5 +2,5 @@ set -euxo pipefail -sh -c "$(curl -sSfL https://release.anza.xyz/v2.0.18/install)" +sh -c "$(curl -sSfL https://release.anza.xyz/v2.0.20/install)" echo "PATH=$HOME/.local/share/solana/install/active_release/bin:$PATH" >> $GITHUB_ENV diff --git a/scripts/setup-localnet/localnet.sh b/scripts/setup-localnet/localnet.sh index ac83d0d8e..ed0ef1429 100755 --- a/scripts/setup-localnet/localnet.sh +++ b/scripts/setup-localnet/localnet.sh @@ -6,7 +6,7 @@ cpu_struct="linux"; # Clean up first bash "$(dirname -- "$0";)/localnet.down.sh" -container_version=v2.0.18 +container_version=v2.0.20 container_name="chainlink-solana.test-validator" echo "Starting $container_name@$container_version" diff --git a/solana.nix b/solana.nix index 50ab2229f..6a94a7619 100644 --- a/solana.nix +++ b/solana.nix @@ -5,7 +5,7 @@ # Solana integration let - version = "v2.0.18"; + version = "v2.0.20"; getBinDerivation = { name, @@ -37,14 +37,14 @@ let name = "solana-cli-x86_64-linux"; filename = "solana-release-x86_64-unknown-linux-gnu.tar.bz2"; ### BEGIN_LINUX_SHA256 ### - sha256 = "sha256-3FW6IMZeDtyU4GTsRIwT9BFLNzLPEuP+oiQdur7P13s="; + sha256 = "sha256-6w624hoeRZM/HaM43I1eUeyf6oTbRfrVcb85m0Oww4s="; ### END_LINUX_SHA256 ### }; aarch64-apple-darwin = getBinDerivation { name = "solana-cli-aarch64-apple-darwin"; filename = "solana-release-aarch64-apple-darwin.tar.bz2"; ### BEGIN_DARWIN_SHA256 ### - sha256 = "sha256-6VjycYU0NU0evXoqtGAZMYGHQEKijofnFQnBJNVsb6Q="; + sha256 = "sha256-9jJtes7CHb5XR+Zklh3asDiyC+intzzDd6Bb69rhHRk="; ### END_DARWIN_SHA256 ### }; }; From 573ea3fec5483f55154bbacb48f9b04f1d351428 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Dec 2024 09:39:37 -0700 Subject: [PATCH 2/4] DS utility (#982) * Add utility for generating data streams transaction instructions * description * format --- contracts/Cargo.lock | 9 ++ .../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 | 121 ++++++++++++++++++ 5 files changed, 179 insertions(+) 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..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); + } +} From 1be704a3d7cf85540817640145eb1dfde262ef41 Mon Sep 17 00:00:00 2001 From: Farber98 Date: Tue, 17 Dec 2024 17:18:18 -0300 Subject: [PATCH 3/4] add enq iface comm to help callers --- pkg/solana/relay.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/solana/relay.go b/pkg/solana/relay.go index 1df61b99d..bd6407cca 100644 --- a/pkg/solana/relay.go +++ b/pkg/solana/relay.go @@ -24,6 +24,15 @@ import ( var _ TxManager = (*txm.Txm)(nil) type TxManager interface { + // Enqueue adds a tx to the txm queue for processing and submitting to the Solana network. + // An error is returned if the txm is not ready, if the tx is invalid, or if the queue is full. + // + // Important Notes: + // - The tx must contain at least one account key. The first account will be used to sign the tx (fee payer's public key). + // - txCfgs can be used to set custom tx configurations. + // - If an txID is provided, it will be used to identify the tx. Otherwise, a random UUID will be generated. + // - The caller needs to set the tx.Message.RecentBlockhash and provide the corresponding lastValidBlockHeight. + // These values are obtained from the GetLatestBlockhash RPC call. Enqueue(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, lastValidBlockHeight uint64, txCfgs ...txm.SetTxConfig) error } From 9ee89773227d19bc4aa9ae3f4e2593e856ac8f2d Mon Sep 17 00:00:00 2001 From: Farber98 Date: Tue, 17 Dec 2024 17:54:07 -0300 Subject: [PATCH 4/4] address feedback --- pkg/solana/relay.go | 5 ++--- pkg/solana/txm/txm.go | 26 +++++++++++++------------- pkg/solana/txm/txm_integration_test.go | 15 ++++++++------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/solana/relay.go b/pkg/solana/relay.go index bd6407cca..fca61ba9f 100644 --- a/pkg/solana/relay.go +++ b/pkg/solana/relay.go @@ -30,9 +30,8 @@ type TxManager interface { // Important Notes: // - The tx must contain at least one account key. The first account will be used to sign the tx (fee payer's public key). // - txCfgs can be used to set custom tx configurations. - // - If an txID is provided, it will be used to identify the tx. Otherwise, a random UUID will be generated. - // - The caller needs to set the tx.Message.RecentBlockhash and provide the corresponding lastValidBlockHeight. - // These values are obtained from the GetLatestBlockhash RPC call. + // - If a txID is provided, it will be used to identify the tx. Otherwise, a random UUID will be generated. + // - The caller needs to set the tx.Message.RecentBlockhash and provide the corresponding lastValidBlockHeight. These values are obtained from the GetLatestBlockhash RPC call. Enqueue(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, lastValidBlockHeight uint64, txCfgs ...txm.SetTxConfig) error } diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index 187904c3b..3e169d88a 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -566,28 +566,28 @@ func (txm *Txm) rebroadcastExpiredTxs(ctx context.Context, client client.ReaderW return } - // Request new blockhash and loop through all expired txes overwriting with new blockhash and rebroadcasting + blockhash, err := client.LatestBlockhash(ctx) + if err != nil { + txm.lggr.Errorw("failed to getLatestBlockhash for rebroadcast", "error", err) + return + } + if blockhash == nil || blockhash.Value == nil { + txm.lggr.Errorw("nil pointer returned from getLatestBlockhash for rebroadcast") + return + } + + // rebroadcast each expired tx after updating blockhash, lastValidBlockHeight and compute unit price (priority fee) for _, tx := range expiredBroadcastedTxes { txm.lggr.Debugw("transaction expired, rebroadcasting", "id", tx.id, "signature", tx.signatures, "lastValidBlockHeight", tx.lastValidBlockHeight, "currentBlockHeight", *currBlock.BlockHeight) - // Removes all signatures associated to tx and cancels context. + // Removes all signatures associated to prior tx and cancels context. _, err := txm.txs.Remove(tx.id) if err != nil { txm.lggr.Errorw("failed to remove expired transaction", "id", tx.id, "error", err) continue } - blockhash, err := client.LatestBlockhash(ctx) - if err != nil { - txm.lggr.Errorw("failed to get latest blockhash for rebroadcast", "error", err) - return - } - if blockhash == nil || blockhash.Value == nil { - txm.lggr.Errorw("nil pointer returned from LatestBlockhash for rebroadcast") - return - } - tx.tx.Message.RecentBlockhash = blockhash.Value.Blockhash - tx.cfg.BaseComputeUnitPrice = txm.fee.BaseComputeUnitPrice() // update compute unit price (priority fee) for rebroadcast + tx.cfg.BaseComputeUnitPrice = txm.fee.BaseComputeUnitPrice() rebroadcastTx := pendingTx{ tx: tx.tx, cfg: tx.cfg, diff --git a/pkg/solana/txm/txm_integration_test.go b/pkg/solana/txm/txm_integration_test.go index 1beeafe2b..154a42f6a 100644 --- a/pkg/solana/txm/txm_integration_test.go +++ b/pkg/solana/txm/txm_integration_test.go @@ -18,6 +18,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/utils" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" relayconfig "github.com/smartcontractkit/chainlink-common/pkg/config" @@ -103,21 +104,21 @@ func TestTxm_Integration_ExpirationRebroadcast(t *testing.T) { } // Verify rebroadcast logs - rebroadcastLogs := observer.FilterMessageSnippet("rebroadcast transaction sent").All() - rebroadcastLogs2 := observer.FilterMessageSnippet("transaction expired, rebroadcasting").All() + rebroadcastLogs := observer.FilterMessageSnippet("rebroadcast transaction sent").Len() + rebroadcastLogs2 := observer.FilterMessageSnippet("transaction expired, rebroadcasting").Len() if tc.expectRebroadcast { - require.NotEmpty(t, rebroadcastLogs, "Expected rebroadcast log message not found") - require.NotEmpty(t, rebroadcastLogs2, "Expected rebroadcast log message not found") + require.Equal(t, 1, rebroadcastLogs, "Expected rebroadcast log message not found") + require.Equal(t, 1, rebroadcastLogs2, "Expected rebroadcast log message not found") } else { - require.Empty(t, rebroadcastLogs, "Rebroadcast should not occur") - require.Empty(t, rebroadcastLogs2, "Rebroadcast should not occur") + require.Equal(t, 0, rebroadcastLogs, "Rebroadcast should not occur") + require.Equal(t, 0, rebroadcastLogs2, "Rebroadcast should not occur") } }) } } func setup(t *testing.T, url string, txExpirationRebroadcast bool) (context.Context, *solanaClient.Client, *txm.Txm, solana.PublicKey, solana.PublicKey, *observer.ObservedLogs) { - ctx := context.Background() + ctx := tests.Context(t) // Generate sender and receiver keys and fund sender account senderKey, err := solana.NewRandomPrivateKey()