Skip to content

Commit 18eafc4

Browse files
dev-jodeegithub-actions
andauthored
feat: (PRO-261) add signature verification flag to transaction methods (#208)
* feat: (PRO-261) add signature verification flag to transaction methods - Introduced `sig_verify` flag to `EstimateTransactionFeeRequest`, `SignAndSendTransactionRequest`, `SignTransactionIfPaidRequest`, and `SignTransactionRequest` for controlling signature verification during transaction simulation. - Added `default_sig_verify` function to provide a default value for the `sig_verify` flag. - Updated transaction resolution methods to utilize the `sig_verify` flag when simulating transactions. - Enhanced tests to cover scenarios with the new `sig_verify` functionality. * Add skip coverage PR comment if not main fndn repo * Update coverage badge [skip ci] --------- Co-authored-by: github-actions <github-actions@github.com>
1 parent 2b3b408 commit 18eafc4

File tree

9 files changed

+135
-23
lines changed

9 files changed

+135
-23
lines changed

.github/badges/coverage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"schemaVersion": 1, "label": "coverage", "message": "86.3%", "color": "green"}
1+
{"schemaVersion": 1, "label": "coverage", "message": "86.2%", "color": "green"}

.github/workflows/rust.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ jobs:
171171
retention-days: 30
172172

173173
- name: Update PR description with coverage badge
174-
if: github.event_name == 'pull_request'
174+
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
175175
uses: actions/github-script@v7
176176
with:
177177
script: |

crates/lib/src/rpc_server/method/estimate_transaction_fee.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use utoipa::ToSchema;
44
use crate::{
55
error::KoraError,
66
fee::fee::FeeConfigUtil,
7+
rpc_server::middleware_utils::default_sig_verify,
78
state::{get_config, get_request_signer_with_signer_key},
89
token::token::TokenUtil,
910
transaction::{TransactionUtil, VersionedTransactionResolved},
@@ -22,6 +23,9 @@ pub struct EstimateTransactionFeeRequest {
2223
/// Optional signer signer_key to ensure consistency across related RPC calls
2324
#[serde(default, skip_serializing_if = "Option::is_none")]
2425
pub signer_key: Option<String>,
26+
/// Whether to verify signatures during simulation (defaults to true)
27+
#[serde(default = "default_sig_verify")]
28+
pub sig_verify: bool,
2529
}
2630

2731
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -47,8 +51,12 @@ pub async fn estimate_transaction_fee(
4751
let validation_config = &config.validation;
4852
let fee_payer = signer.solana_pubkey();
4953

50-
let mut resolved_transaction =
51-
VersionedTransactionResolved::from_transaction(&transaction, rpc_client).await?;
54+
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
55+
&transaction,
56+
rpc_client,
57+
request.sig_verify,
58+
)
59+
.await?;
5260

5361
let min_transaction_fee = FeeConfigUtil::estimate_transaction_fee(
5462
rpc_client,
@@ -118,6 +126,7 @@ mod tests {
118126
transaction: "invalid_base64!@#$".to_string(),
119127
fee_token: None,
120128
signer_key: None,
129+
sig_verify: true,
121130
};
122131

123132
let result = estimate_transaction_fee(&rpc_client, request).await;
@@ -136,6 +145,7 @@ mod tests {
136145
transaction: create_mock_encoded_transaction(),
137146
fee_token: None,
138147
signer_key: Some("invalid_pubkey".to_string()),
148+
sig_verify: true,
139149
};
140150

141151
let result = estimate_transaction_fee(&rpc_client, request).await;
@@ -156,6 +166,7 @@ mod tests {
156166
transaction: create_mock_encoded_transaction(),
157167
fee_token: Some("invalid_mint_address".to_string()),
158168
signer_key: None,
169+
sig_verify: true,
159170
};
160171

161172
let result = estimate_transaction_fee(&rpc_client, request).await;

crates/lib/src/rpc_server/method/sign_and_send_transaction.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::rpc_server::middleware_utils::default_sig_verify;
12
use serde::{Deserialize, Serialize};
23
use solana_client::nonblocking::rpc_client::RpcClient;
34
use std::sync::Arc;
@@ -15,6 +16,9 @@ pub struct SignAndSendTransactionRequest {
1516
/// Optional signer signer_key to ensure consistency across related RPC calls
1617
#[serde(default, skip_serializing_if = "Option::is_none")]
1718
pub signer_key: Option<String>,
19+
/// Whether to verify signatures during simulation (defaults to true)
20+
#[serde(default = "default_sig_verify")]
21+
pub sig_verify: bool,
1822
}
1923

2024
#[derive(Debug, Serialize, ToSchema)]
@@ -32,8 +36,12 @@ pub async fn sign_and_send_transaction(
3236
let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
3337
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
3438

35-
let mut resolved_transaction =
36-
VersionedTransactionResolved::from_transaction(&transaction, rpc_client).await?;
39+
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
40+
&transaction,
41+
rpc_client,
42+
request.sig_verify,
43+
)
44+
.await?;
3745

3846
let (signature, signed_transaction) =
3947
resolved_transaction.sign_and_send_transaction(&signer, rpc_client).await?;
@@ -64,6 +72,7 @@ mod tests {
6472
let request = SignAndSendTransactionRequest {
6573
transaction: "invalid_base64!@#$".to_string(),
6674
signer_key: None,
75+
sig_verify: true,
6776
};
6877

6978
let result = sign_and_send_transaction(&rpc_client, request).await;
@@ -81,6 +90,7 @@ mod tests {
8190
let request = SignAndSendTransactionRequest {
8291
transaction: create_mock_encoded_transaction(),
8392
signer_key: Some("invalid_pubkey".to_string()),
93+
sig_verify: true,
8494
};
8595

8696
let result = sign_and_send_transaction(&rpc_client, request).await;

crates/lib/src/rpc_server/method/sign_transaction.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
rpc_server::middleware_utils::default_sig_verify,
23
state::get_request_signer_with_signer_key,
34
transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved},
45
KoraError,
@@ -14,6 +15,9 @@ pub struct SignTransactionRequest {
1415
/// Optional signer signer_key to ensure consistency across related RPC calls
1516
#[serde(default, skip_serializing_if = "Option::is_none")]
1617
pub signer_key: Option<String>,
18+
/// Whether to verify signatures during simulation (defaults to true)
19+
#[serde(default = "default_sig_verify")]
20+
pub sig_verify: bool,
1721
}
1822

1923
#[derive(Debug, Serialize, ToSchema)]
@@ -31,8 +35,12 @@ pub async fn sign_transaction(
3135
let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
3236
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
3337

34-
let mut resolved_transaction =
35-
VersionedTransactionResolved::from_transaction(&transaction, rpc_client).await?;
38+
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
39+
&transaction,
40+
rpc_client,
41+
request.sig_verify,
42+
)
43+
.await?;
3644

3745
let (signed_transaction, _) =
3846
resolved_transaction.sign_transaction(&signer, rpc_client).await?;
@@ -65,6 +73,7 @@ mod tests {
6573
let request = SignTransactionRequest {
6674
transaction: "invalid_base64!@#$".to_string(),
6775
signer_key: None,
76+
sig_verify: true,
6877
};
6978

7079
let result = sign_transaction(&rpc_client, request).await;
@@ -82,6 +91,7 @@ mod tests {
8291
let request = SignTransactionRequest {
8392
transaction: create_mock_encoded_transaction(),
8493
signer_key: Some("invalid_pubkey".to_string()),
94+
sig_verify: true,
8595
};
8696

8797
let result = sign_transaction(&rpc_client, request).await;

crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
rpc_server::middleware_utils::default_sig_verify,
23
state::get_request_signer_with_signer_key,
34
transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved},
45
KoraError,
@@ -14,6 +15,9 @@ pub struct SignTransactionIfPaidRequest {
1415
/// Optional signer signer_key to ensure consistency across related RPC calls
1516
#[serde(default, skip_serializing_if = "Option::is_none")]
1617
pub signer_key: Option<String>,
18+
/// Whether to verify signatures during simulation (defaults to true)
19+
#[serde(default = "default_sig_verify")]
20+
pub sig_verify: bool,
1721
}
1822

1923
#[derive(Debug, Serialize, ToSchema)]
@@ -31,8 +35,12 @@ pub async fn sign_transaction_if_paid(
3135
let transaction_requested = TransactionUtil::decode_b64_transaction(&request.transaction)?;
3236
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
3337

34-
let mut resolved_transaction =
35-
VersionedTransactionResolved::from_transaction(&transaction_requested, rpc_client).await?;
38+
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
39+
&transaction_requested,
40+
rpc_client,
41+
request.sig_verify,
42+
)
43+
.await?;
3644

3745
let (transaction, signed_transaction) = resolved_transaction
3846
.sign_transaction_if_paid(&signer, rpc_client)
@@ -65,6 +73,7 @@ mod tests {
6573
let request = SignTransactionIfPaidRequest {
6674
transaction: "invalid_base64!@#$".to_string(),
6775
signer_key: None,
76+
sig_verify: true,
6877
};
6978

7079
let result = sign_transaction_if_paid(&rpc_client, request).await;
@@ -82,6 +91,7 @@ mod tests {
8291
let request = SignTransactionIfPaidRequest {
8392
transaction: create_mock_encoded_transaction(),
8493
signer_key: Some("invalid_pubkey".to_string()),
94+
sig_verify: true,
8595
};
8696

8797
let result = sign_transaction_if_paid(&rpc_client, request).await;

crates/lib/src/rpc_server/middleware_utils.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ use futures_util::TryStreamExt;
22
use http::Request;
33
use jsonrpsee::server::logger::Body;
44

5+
pub fn default_sig_verify() -> bool {
6+
false
7+
}
8+
59
pub async fn extract_parts_and_body_bytes(
610
request: Request<Body>,
711
) -> (http::request::Parts, Vec<u8>) {

crates/lib/src/transaction/versioned_transaction.rs

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use async_trait::async_trait;
22
use base64::{engine::general_purpose::STANDARD, Engine as _};
3-
use solana_client::nonblocking::rpc_client::RpcClient;
3+
use solana_client::{nonblocking::rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig};
44
use solana_commitment_config::CommitmentConfig;
55
use solana_message::{v0::MessageAddressTableLookup, VersionedMessage};
66
use solana_sdk::{
@@ -79,6 +79,7 @@ impl VersionedTransactionResolved {
7979
pub async fn from_transaction(
8080
transaction: &VersionedTransaction,
8181
rpc_client: &RpcClient,
82+
sig_verify: bool,
8283
) -> Result<Self, KoraError> {
8384
let mut resolved = Self {
8485
transaction: transaction.clone(),
@@ -113,7 +114,7 @@ impl VersionedTransactionResolved {
113114
let outer_instructions =
114115
IxUtils::uncompile_instructions(transaction.message.instructions(), &all_account_keys);
115116

116-
let inner_instructions = resolved.fetch_inner_instructions(rpc_client).await?;
117+
let inner_instructions = resolved.fetch_inner_instructions(rpc_client, sig_verify).await?;
117118

118119
resolved.all_instructions.extend(outer_instructions);
119120
resolved.all_instructions.extend(inner_instructions);
@@ -139,9 +140,17 @@ impl VersionedTransactionResolved {
139140
async fn fetch_inner_instructions(
140141
&mut self,
141142
rpc_client: &RpcClient,
143+
sig_verify: bool,
142144
) -> Result<Vec<Instruction>, KoraError> {
143145
let simulation_result = rpc_client
144-
.simulate_transaction(&self.transaction)
146+
.simulate_transaction_with_config(
147+
&self.transaction,
148+
RpcSimulateTransactionConfig {
149+
commitment: Some(rpc_client.commitment()),
150+
sig_verify,
151+
..Default::default()
152+
},
153+
)
145154
.await
146155
.map_err(|e| KoraError::RpcError(format!("Failed to simulate transaction: {e}")))?;
147156

@@ -669,9 +678,10 @@ mod tests {
669678
);
670679
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
671680

672-
let resolved = VersionedTransactionResolved::from_transaction(&transaction, &rpc_client)
673-
.await
674-
.unwrap();
681+
let resolved =
682+
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client, true)
683+
.await
684+
.unwrap();
675685

676686
assert_eq!(resolved.transaction, transaction);
677687
assert_eq!(resolved.all_account_keys, transaction.message.static_account_keys());
@@ -770,9 +780,10 @@ mod tests {
770780

771781
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
772782

773-
let resolved = VersionedTransactionResolved::from_transaction(&transaction, &rpc_client)
774-
.await
775-
.unwrap();
783+
let resolved =
784+
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client, true)
785+
.await
786+
.unwrap();
776787

777788
assert_eq!(resolved.transaction, transaction);
778789

@@ -815,7 +826,7 @@ mod tests {
815826
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
816827

817828
let result =
818-
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client).await;
829+
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client, true).await;
819830

820831
// The simulation should fail, but the exact error type depends on mock implementation
821832
// We expect either an RpcError (from mock deserialization) or InvalidTransaction (from simulation logic)
@@ -877,7 +888,60 @@ mod tests {
877888
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
878889

879890
let mut resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction);
880-
let inner_instructions = resolved.fetch_inner_instructions(&rpc_client).await.unwrap();
891+
let inner_instructions =
892+
resolved.fetch_inner_instructions(&rpc_client, true).await.unwrap();
893+
894+
assert_eq!(inner_instructions.len(), 1);
895+
assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);
896+
}
897+
898+
#[tokio::test]
899+
async fn test_fetch_inner_instructions_with_sig_verify_false() {
900+
let config = setup_test_config();
901+
let _m = setup_config_mock(config);
902+
903+
let keypair = Keypair::new();
904+
let instruction = Instruction::new_with_bytes(
905+
Pubkey::new_unique(),
906+
&[1, 2, 3],
907+
vec![AccountMeta::new(keypair.pubkey(), true)],
908+
);
909+
let message =
910+
VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
911+
let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
912+
913+
// Mock RPC client with inner instructions
914+
let inner_instruction_data = bs58::encode(&[10, 20, 30]).into_string();
915+
let mut mocks = HashMap::new();
916+
mocks.insert(
917+
RpcRequest::SimulateTransaction,
918+
json!({
919+
"context": { "slot": 1 },
920+
"value": {
921+
"err": null,
922+
"logs": [],
923+
"accounts": null,
924+
"unitsConsumed": 1000,
925+
"innerInstructions": [
926+
{
927+
"index": 0,
928+
"instructions": [
929+
{
930+
"programIdIndex": 1,
931+
"accounts": [0],
932+
"data": inner_instruction_data
933+
}
934+
]
935+
}
936+
]
937+
}
938+
}),
939+
);
940+
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
941+
942+
let mut resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction);
943+
let inner_instructions =
944+
resolved.fetch_inner_instructions(&rpc_client, false).await.unwrap();
881945

882946
assert_eq!(inner_instructions.len(), 1);
883947
assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);

tests/src/common/assertions.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ impl RpcAssertions for Value {
8383

8484
/// Assertions for transaction responses
8585
pub trait TransactionAssertions {
86-
/// Assert the transaction blockhash is valid (44 chars base58)
86+
/// Assert the transaction blockhash is valid (43-44 chars base58)
8787
fn assert_valid_blockhash(&self);
8888
}
8989

@@ -95,7 +95,10 @@ impl TransactionAssertions for Value {
9595
.expect("Response missing blockhash field");
9696

9797
// Solana blockhashes are typically 44 chars in base58
98-
assert_eq!(blockhash.len(), 44, "Invalid blockhash format: {blockhash}");
98+
assert!(
99+
blockhash.len() >= 43 && blockhash.len() <= 44,
100+
"Invalid blockhash format: {blockhash}"
101+
);
99102
}
100103
}
101104

0 commit comments

Comments
 (0)