Skip to content

Commit e8a0a01

Browse files
authored
Merge pull request #178 from pradhankukiran/fix/versioned-smart-tx-signer-order
fix(optimized_transaction): correct v0 signer ordering with separate fee payer
2 parents 1f6792b + a8e7c4e commit e8a0a01

1 file changed

Lines changed: 238 additions & 65 deletions

File tree

src/optimized_transaction.rs

Lines changed: 238 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::types::{
66
SmartTransactionConfig, Timeout,
77
};
88
use crate::Helius;
9+
use std::collections::HashSet;
910
use std::str::FromStr;
1011
use std::sync::Arc;
1112

@@ -64,6 +65,42 @@ const MIN_TIP_LAMPORTS_DUAL: u64 = 200_000; // 0.0002 SOL
6465
/// Minimum tip: 0.000005 SOL (5,000 lamports).
6566
const MIN_TIP_LAMPORTS_SWQOS: u64 = 5_000; // 0.000005 SOL
6667

68+
fn collect_unique_signers(signers: &[Arc<dyn Signer>], fee_payer: Option<&Arc<dyn Signer>>) -> Vec<Arc<dyn Signer>> {
69+
let mut all_signers: Vec<Arc<dyn Signer>> = Vec::with_capacity(signers.len() + usize::from(fee_payer.is_some()));
70+
let mut seen: HashSet<Pubkey> = HashSet::with_capacity(all_signers.capacity());
71+
72+
if let Some(fee_payer) = fee_payer {
73+
if seen.insert(fee_payer.pubkey()) {
74+
all_signers.push(fee_payer.clone());
75+
}
76+
}
77+
78+
for signer in signers {
79+
if seen.insert(signer.pubkey()) {
80+
all_signers.push(signer.clone());
81+
}
82+
}
83+
84+
all_signers
85+
}
86+
87+
fn collect_unique_keypair_refs<'a>(signers: &'a [Keypair], fee_payer: &'a Keypair) -> Vec<&'a Keypair> {
88+
let mut all_signers: Vec<&Keypair> = Vec::with_capacity(signers.len() + 1);
89+
let mut seen: HashSet<Pubkey> = HashSet::with_capacity(all_signers.capacity());
90+
91+
if seen.insert(fee_payer.pubkey()) {
92+
all_signers.push(fee_payer);
93+
}
94+
95+
for signer in signers {
96+
if seen.insert(signer.pubkey()) {
97+
all_signers.push(signer);
98+
}
99+
}
100+
101+
all_signers
102+
}
103+
67104
/// URL to fetch current Jito bundle tip floor prices.
68105
///
69106
/// This endpoint returns the minimum tip amounts required for different
@@ -355,8 +392,8 @@ impl Helius {
355392
{
356393
return Ok(txt_sig);
357394
}
358-
if status.err.is_some() {
359-
return Err(HeliusError::TransactionError(status.err.unwrap()));
395+
if let Some(err) = status.err {
396+
return Err(HeliusError::TransactionError(err));
360397
}
361398
}
362399
None => {
@@ -451,12 +488,13 @@ impl Helius {
451488
final_instructions.push(compute_budget_ix);
452489

453490
// Get the optimal compute units
491+
let all_signers: Vec<Arc<dyn Signer>> = collect_unique_signers(&config.signers, config.fee_payer.as_ref());
454492
let units: Option<u64> = self
455493
.get_compute_units(
456494
updated_instructions,
457495
payer_pubkey,
458496
config.lookup_tables.clone().unwrap_or_default(),
459-
Some(&config.signers),
497+
Some(&all_signers),
460498
)
461499
.await?;
462500

@@ -491,38 +529,17 @@ impl Helius {
491529
let v0_message: v0::Message =
492530
v0::Message::try_compile(&payer_pubkey, &final_instructions, lookup_tables, recent_blockhash)?;
493531
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
494-
495-
let all_signers: Vec<Arc<dyn Signer>> = if let Some(fee_payer) = config.fee_payer.as_ref() {
496-
let mut all_signers = config.signers.clone();
497-
if !all_signers.iter().any(|signer| signer.pubkey() == fee_payer.pubkey()) {
498-
all_signers.push(fee_payer.clone());
499-
}
500-
all_signers
501-
} else {
502-
config.signers.clone()
503-
};
504-
505-
let signatures: Vec<Signature> = all_signers
506-
.iter()
507-
.map(|signer| signer.try_sign_message(versioned_message.serialize().as_slice()))
508-
.collect::<std::result::Result<Vec<_>, _>>()?;
509-
510-
let versioned_transaction = VersionedTransaction {
511-
signatures,
512-
message: versioned_message,
513-
};
532+
let versioned_transaction: VersionedTransaction =
533+
VersionedTransaction::try_new(versioned_message, all_signers.as_slice())
534+
.map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?;
514535

515536
Ok((
516537
SmartTransaction::Versioned(versioned_transaction),
517538
last_valid_block_hash,
518539
))
519540
} else {
520541
let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&payer_pubkey));
521-
tx.try_partial_sign(&config.signers, recent_blockhash)?;
522-
523-
if let Some(fee_payer) = config.fee_payer.as_ref() {
524-
tx.try_partial_sign(&[fee_payer], recent_blockhash)?;
525-
}
542+
tx.try_partial_sign(&all_signers, recent_blockhash)?;
526543

527544
Ok((SmartTransaction::Legacy(tx), last_valid_block_hash))
528545
}
@@ -637,16 +654,8 @@ impl Helius {
637654
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
638655

639656
let transaction: VersionedTransaction = if let Some(keypairs) = keypairs {
640-
let mut tx = VersionedTransaction {
641-
signatures: vec![Signature::default(); keypairs.len()],
642-
message: versioned_message.clone(),
643-
};
644-
645-
for (i, keypair) in keypairs.iter().enumerate() {
646-
tx.signatures[i] = keypair.sign_message(&versioned_message.serialize());
647-
}
648-
649-
tx
657+
VersionedTransaction::try_new(versioned_message, keypairs)
658+
.map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?
650659
} else {
651660
VersionedTransaction {
652661
signatures: vec![],
@@ -759,14 +768,7 @@ impl Helius {
759768
let mut test_instructions: Vec<Instruction> = final_instructions.clone();
760769
test_instructions.extend(create_config.instructions.clone());
761770

762-
let mut all_signers: Vec<&Keypair> = vec![&fee_payer];
763-
let mut seen: std::collections::HashSet<Pubkey> = std::collections::HashSet::new();
764-
seen.insert(fee_payer.pubkey());
765-
for kp in &keypairs {
766-
if seen.insert(kp.pubkey()) {
767-
all_signers.push(kp);
768-
}
769-
}
771+
let all_signers: Vec<&Keypair> = collect_unique_keypair_refs(&keypairs, &fee_payer);
770772

771773
let units: Option<u64> = self
772774
.get_compute_units_thread_safe(
@@ -804,29 +806,13 @@ impl Helius {
804806
)?;
805807

806808
let versioned_message: VersionedMessage = VersionedMessage::V0(message);
807-
808-
let fee_payer_copy: Keypair = fee_payer.insecure_clone();
809-
let mut all_signers: Vec<Keypair> = vec![fee_payer_copy];
810-
all_signers.extend(keypairs.into_iter().filter(|k| k.pubkey() != fee_payer.pubkey()));
811-
812-
let mut tx: VersionedTransaction = VersionedTransaction {
813-
signatures: vec![Signature::default(); all_signers.len()],
814-
message: versioned_message.clone(),
815-
};
816-
817-
// Sign message with all keypairs
818-
for (i, keypair) in all_signers.iter().enumerate() {
819-
tx.signatures[i] = keypair.sign_message(&versioned_message.serialize());
820-
}
809+
let tx: VersionedTransaction = VersionedTransaction::try_new(versioned_message, all_signers.as_slice())
810+
.map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?;
821811

822812
SmartTransaction::Versioned(tx)
823813
} else {
824814
let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&fee_payer.pubkey()));
825-
826-
let mut signers: Vec<&Keypair> = vec![&fee_payer];
827-
signers.extend(keypairs.iter().filter(|k| k.pubkey() != fee_payer.pubkey()));
828-
829-
tx.sign(&signers, recent_blockhash);
815+
tx.sign(&all_signers, recent_blockhash);
830816

831817
SmartTransaction::Legacy(tx)
832818
};
@@ -1210,3 +1196,190 @@ impl Helius {
12101196
}
12111197
}
12121198
}
1199+
1200+
#[cfg(test)]
1201+
mod tests {
1202+
use super::{collect_unique_keypair_refs, collect_unique_signers};
1203+
use solana_sdk::{
1204+
hash::Hash,
1205+
instruction::{AccountMeta, Instruction},
1206+
message::{v0, VersionedMessage},
1207+
pubkey::Pubkey,
1208+
signature::{Keypair, Signature, Signer},
1209+
transaction::{Transaction, VersionedTransaction},
1210+
};
1211+
use std::sync::Arc;
1212+
1213+
fn build_versioned_message(
1214+
payer: &Keypair,
1215+
writable_signer: &Keypair,
1216+
readonly_signer: &Keypair,
1217+
) -> VersionedMessage {
1218+
let instruction = Instruction {
1219+
program_id: Pubkey::new_unique(),
1220+
accounts: vec![
1221+
AccountMeta::new(writable_signer.pubkey(), true),
1222+
AccountMeta::new_readonly(readonly_signer.pubkey(), true),
1223+
],
1224+
data: vec![],
1225+
};
1226+
1227+
VersionedMessage::V0(
1228+
v0::Message::try_compile(&payer.pubkey(), &[instruction], &[], Hash::new_unique()).unwrap(),
1229+
)
1230+
}
1231+
1232+
#[test]
1233+
fn collect_unique_signers_includes_fee_payer_once() {
1234+
let fee_payer: Arc<dyn Signer> = Arc::new(Keypair::new());
1235+
let signer: Arc<dyn Signer> = Arc::new(Keypair::new());
1236+
1237+
let signers: Vec<Arc<dyn Signer>> = vec![signer.clone(), fee_payer.clone(), signer.clone()];
1238+
let all_signers = collect_unique_signers(&signers, Some(&fee_payer));
1239+
1240+
let signer_pubkeys: Vec<Pubkey> = all_signers.iter().map(|signer| signer.pubkey()).collect();
1241+
assert_eq!(signer_pubkeys, vec![fee_payer.pubkey(), signer.pubkey()]);
1242+
}
1243+
1244+
#[test]
1245+
fn collect_unique_keypair_refs_includes_fee_payer_once() {
1246+
let fee_payer = Keypair::new();
1247+
let signer = Keypair::new();
1248+
let signers = vec![
1249+
signer.insecure_clone(),
1250+
fee_payer.insecure_clone(),
1251+
signer.insecure_clone(),
1252+
];
1253+
1254+
let all_signers = collect_unique_keypair_refs(&signers, &fee_payer);
1255+
let signer_pubkeys: Vec<Pubkey> = all_signers.iter().map(|signer| signer.pubkey()).collect();
1256+
1257+
assert_eq!(signer_pubkeys, vec![fee_payer.pubkey(), signer.pubkey()]);
1258+
}
1259+
1260+
#[test]
1261+
fn versioned_try_new_reorders_arc_signers_to_match_message() {
1262+
let fee_payer = Keypair::new();
1263+
let writable_signer = Keypair::new();
1264+
let readonly_signer = Keypair::new();
1265+
let fee_payer_signer: Arc<dyn Signer> = Arc::new(fee_payer.insecure_clone());
1266+
let writable_signer_arc: Arc<dyn Signer> = Arc::new(writable_signer.insecure_clone());
1267+
let readonly_signer_arc: Arc<dyn Signer> = Arc::new(readonly_signer.insecure_clone());
1268+
1269+
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
1270+
let signers: Vec<Arc<dyn Signer>> = vec![readonly_signer_arc.clone(), writable_signer_arc.clone()];
1271+
let all_signers = collect_unique_signers(&signers, Some(&fee_payer_signer));
1272+
let tx = VersionedTransaction::try_new(message.clone(), all_signers.as_slice()).unwrap();
1273+
let message_bytes = message.serialize();
1274+
1275+
assert_eq!(
1276+
tx.signatures,
1277+
vec![
1278+
Signature::from(fee_payer.sign_message(&message_bytes)),
1279+
Signature::from(writable_signer.sign_message(&message_bytes)),
1280+
Signature::from(readonly_signer.sign_message(&message_bytes)),
1281+
]
1282+
);
1283+
}
1284+
1285+
#[test]
1286+
fn manual_fee_payer_appended_signature_order_fails_verification() {
1287+
let fee_payer = Keypair::new();
1288+
let writable_signer = Keypair::new();
1289+
let readonly_signer = Keypair::new();
1290+
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
1291+
let message_bytes = message.serialize();
1292+
1293+
// This mirrors the pre-fix separate fee payer path: caller signers first, fee payer appended last.
1294+
let manual_signatures = [
1295+
readonly_signer.sign_message(&message_bytes),
1296+
writable_signer.sign_message(&message_bytes),
1297+
fee_payer.sign_message(&message_bytes),
1298+
];
1299+
1300+
let verification_results: Vec<bool> = manual_signatures
1301+
.iter()
1302+
.zip(message.static_account_keys().iter())
1303+
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
1304+
.collect();
1305+
1306+
assert_eq!(verification_results, vec![false, true, false]);
1307+
}
1308+
1309+
#[test]
1310+
fn manual_non_payer_caller_order_can_fail_verification() {
1311+
let fee_payer = Keypair::new();
1312+
let writable_signer = Keypair::new();
1313+
let readonly_signer = Keypair::new();
1314+
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
1315+
let message_bytes = message.serialize();
1316+
1317+
// This mirrors the pre-fix seed path: fee payer first, remaining signers left in caller order.
1318+
let manual_signatures = [
1319+
fee_payer.sign_message(&message_bytes),
1320+
readonly_signer.sign_message(&message_bytes),
1321+
writable_signer.sign_message(&message_bytes),
1322+
];
1323+
1324+
let verification_results: Vec<bool> = manual_signatures
1325+
.iter()
1326+
.zip(message.static_account_keys().iter())
1327+
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
1328+
.collect();
1329+
1330+
assert_eq!(verification_results, vec![true, false, false]);
1331+
}
1332+
1333+
#[test]
1334+
fn versioned_try_new_reorders_keypair_signers_to_match_message() {
1335+
let fee_payer = Keypair::new();
1336+
let writable_signer = Keypair::new();
1337+
let readonly_signer = Keypair::new();
1338+
1339+
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
1340+
let signers = vec![readonly_signer.insecure_clone(), writable_signer.insecure_clone()];
1341+
let all_signers = collect_unique_keypair_refs(&signers, &fee_payer);
1342+
let tx = VersionedTransaction::try_new(message.clone(), all_signers.as_slice()).unwrap();
1343+
let message_bytes = message.serialize();
1344+
1345+
assert_eq!(
1346+
tx.signatures,
1347+
vec![
1348+
Signature::from(fee_payer.sign_message(&message_bytes)),
1349+
Signature::from(writable_signer.sign_message(&message_bytes)),
1350+
Signature::from(readonly_signer.sign_message(&message_bytes)),
1351+
]
1352+
);
1353+
}
1354+
1355+
#[test]
1356+
fn legacy_try_partial_sign_reorders_keypairs_to_match_message() {
1357+
let fee_payer = Keypair::new();
1358+
let writable_signer = Keypair::new();
1359+
let readonly_signer = Keypair::new();
1360+
let recent_blockhash = Hash::new_unique();
1361+
let instruction = Instruction {
1362+
program_id: Pubkey::new_unique(),
1363+
accounts: vec![
1364+
AccountMeta::new(writable_signer.pubkey(), true),
1365+
AccountMeta::new_readonly(readonly_signer.pubkey(), true),
1366+
],
1367+
data: vec![],
1368+
};
1369+
let mut tx = Transaction::new_with_payer(&[instruction], Some(&fee_payer.pubkey()));
1370+
let signers = vec![readonly_signer.insecure_clone(), writable_signer.insecure_clone()];
1371+
let all_signers = collect_unique_keypair_refs(&signers, &fee_payer);
1372+
1373+
tx.try_partial_sign(&all_signers, recent_blockhash).unwrap();
1374+
1375+
let message_bytes = tx.message_data();
1376+
assert_eq!(
1377+
tx.signatures,
1378+
vec![
1379+
fee_payer.sign_message(&message_bytes),
1380+
writable_signer.sign_message(&message_bytes),
1381+
readonly_signer.sign_message(&message_bytes),
1382+
]
1383+
);
1384+
}
1385+
}

0 commit comments

Comments
 (0)