solana/sdk/src/transaction/versioned/mod.rs

381 lines
13 KiB
Rust

//! Defines a transaction which supports multiple versions of messages.
#![cfg(feature = "full")]
use {
crate::{
hash::Hash,
message::VersionedMessage,
sanitize::SanitizeError,
short_vec,
signature::Signature,
signer::SignerError,
signers::Signers,
transaction::{Result, Transaction, TransactionError},
},
serde::Serialize,
std::cmp::Ordering,
};
mod sanitized;
pub use sanitized::*;
use {
crate::program_utils::limited_deserialize,
solana_program::{
nonce::NONCED_TX_MARKER_IX_INDEX, system_instruction::SystemInstruction, system_program,
},
};
/// Type that serializes to the string "legacy"
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Legacy {
Legacy,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", untagged)]
pub enum TransactionVersion {
Legacy(Legacy),
Number(u8),
}
impl TransactionVersion {
pub const LEGACY: Self = Self::Legacy(Legacy::Legacy);
}
// NOTE: Serialization-related changes must be paired with the direct read at sigverify.
/// An atomic transaction
#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)]
pub struct VersionedTransaction {
/// List of signatures
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
/// Message to sign.
pub message: VersionedMessage,
}
impl From<Transaction> for VersionedTransaction {
fn from(transaction: Transaction) -> Self {
Self {
signatures: transaction.signatures,
message: VersionedMessage::Legacy(transaction.message),
}
}
}
impl VersionedTransaction {
/// Signs a versioned message and if successful, returns a signed
/// transaction.
pub fn try_new<T: Signers + ?Sized>(
message: VersionedMessage,
keypairs: &T,
) -> std::result::Result<Self, SignerError> {
let static_account_keys = message.static_account_keys();
if static_account_keys.len() < message.header().num_required_signatures as usize {
return Err(SignerError::InvalidInput("invalid message".to_string()));
}
let signer_keys = keypairs.try_pubkeys()?;
let expected_signer_keys =
&static_account_keys[0..message.header().num_required_signatures as usize];
match signer_keys.len().cmp(&expected_signer_keys.len()) {
Ordering::Greater => Err(SignerError::TooManySigners),
Ordering::Less => Err(SignerError::NotEnoughSigners),
Ordering::Equal => Ok(()),
}?;
let message_data = message.serialize();
let signature_indexes: Vec<usize> = expected_signer_keys
.iter()
.map(|signer_key| {
signer_keys
.iter()
.position(|key| key == signer_key)
.ok_or(SignerError::KeypairPubkeyMismatch)
})
.collect::<std::result::Result<_, SignerError>>()?;
let unordered_signatures = keypairs.try_sign_message(&message_data)?;
let signatures: Vec<Signature> = signature_indexes
.into_iter()
.map(|index| {
unordered_signatures
.get(index)
.copied()
.ok_or_else(|| SignerError::InvalidInput("invalid keypairs".to_string()))
})
.collect::<std::result::Result<_, SignerError>>()?;
Ok(Self {
signatures,
message,
})
}
pub fn sanitize(
&self,
require_static_program_ids: bool,
) -> std::result::Result<(), SanitizeError> {
self.message.sanitize(require_static_program_ids)?;
self.sanitize_signatures()?;
Ok(())
}
pub(crate) fn sanitize_signatures(&self) -> std::result::Result<(), SanitizeError> {
let num_required_signatures = usize::from(self.message.header().num_required_signatures);
match num_required_signatures.cmp(&self.signatures.len()) {
Ordering::Greater => Err(SanitizeError::IndexOutOfBounds),
Ordering::Less => Err(SanitizeError::InvalidValue),
Ordering::Equal => Ok(()),
}?;
// Signatures are verified before message keys are loaded so all signers
// must correspond to static account keys.
if self.signatures.len() > self.message.static_account_keys().len() {
return Err(SanitizeError::IndexOutOfBounds);
}
Ok(())
}
/// Returns the version of the transaction
pub fn version(&self) -> TransactionVersion {
match self.message {
VersionedMessage::Legacy(_) => TransactionVersion::LEGACY,
VersionedMessage::V0(_) => TransactionVersion::Number(0),
}
}
/// Returns a legacy transaction if the transaction message is legacy.
pub fn into_legacy_transaction(self) -> Option<Transaction> {
match self.message {
VersionedMessage::Legacy(message) => Some(Transaction {
signatures: self.signatures,
message,
}),
_ => None,
}
}
/// Verify the transaction and hash its message
pub fn verify_and_hash_message(&self) -> Result<Hash> {
let message_bytes = self.message.serialize();
if !self
._verify_with_results(&message_bytes)
.iter()
.all(|verify_result| *verify_result)
{
Err(TransactionError::SignatureFailure)
} else {
Ok(VersionedMessage::hash_raw_message(&message_bytes))
}
}
/// Verify the transaction and return a list of verification results
pub fn verify_with_results(&self) -> Vec<bool> {
let message_bytes = self.message.serialize();
self._verify_with_results(&message_bytes)
}
fn _verify_with_results(&self, message_bytes: &[u8]) -> Vec<bool> {
self.signatures
.iter()
.zip(self.message.static_account_keys().iter())
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), message_bytes))
.collect()
}
/// Returns true if transaction begins with a valid advance nonce
/// instruction. Since dynamically loaded addresses can't have write locks
/// demoted without loading addresses, this shouldn't be used in the
/// runtime.
pub fn uses_durable_nonce(&self) -> bool {
let message = &self.message;
message
.instructions()
.get(NONCED_TX_MARKER_IX_INDEX as usize)
.filter(|instruction| {
// Is system program
matches!(
message.static_account_keys().get(instruction.program_id_index as usize),
Some(program_id) if system_program::check_id(program_id)
)
// Is a nonce advance instruction
&& matches!(
limited_deserialize(&instruction.data),
Ok(SystemInstruction::AdvanceNonceAccount)
)
// Nonce account is writable
&& matches!(
instruction.accounts.first(),
Some(index) if message.is_maybe_writable(*index as usize)
)
})
.is_some()
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
message::Message as LegacyMessage,
signer::{keypair::Keypair, Signer},
system_instruction, sysvar,
},
solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
},
};
#[test]
fn test_try_new() {
let keypair0 = Keypair::new();
let keypair1 = Keypair::new();
let keypair2 = Keypair::new();
let message = VersionedMessage::Legacy(LegacyMessage::new(
&[Instruction::new_with_bytes(
Pubkey::new_unique(),
&[],
vec![
AccountMeta::new_readonly(keypair1.pubkey(), true),
AccountMeta::new_readonly(keypair2.pubkey(), false),
],
)],
Some(&keypair0.pubkey()),
));
assert_eq!(
VersionedTransaction::try_new(message.clone(), &[&keypair0]),
Err(SignerError::NotEnoughSigners)
);
assert_eq!(
VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair0]),
Err(SignerError::KeypairPubkeyMismatch)
);
assert_eq!(
VersionedTransaction::try_new(message.clone(), &[&keypair1, &keypair2]),
Err(SignerError::KeypairPubkeyMismatch)
);
match VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair1]) {
Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
Err(err) => assert_eq!(Some(err), None),
}
match VersionedTransaction::try_new(message, &[&keypair1, &keypair0]) {
Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
Err(err) => assert_eq!(Some(err), None),
}
}
fn nonced_transfer_tx() -> (Pubkey, Pubkey, VersionedTransaction) {
let from_keypair = Keypair::new();
let from_pubkey = from_keypair.pubkey();
let nonce_keypair = Keypair::new();
let nonce_pubkey = nonce_keypair.pubkey();
let instructions = [
system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
];
let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
(from_pubkey, nonce_pubkey, tx.into())
}
#[test]
fn tx_uses_nonce_ok() {
let (_, _, tx) = nonced_transfer_tx();
assert!(tx.uses_durable_nonce());
}
#[test]
fn tx_uses_nonce_empty_ix_fail() {
assert!(!VersionedTransaction::default().uses_durable_nonce());
}
#[test]
fn tx_uses_nonce_bad_prog_id_idx_fail() {
let (_, _, mut tx) = nonced_transfer_tx();
match &mut tx.message {
VersionedMessage::Legacy(message) => {
message.instructions.get_mut(0).unwrap().program_id_index = 255u8;
}
VersionedMessage::V0(_) => unreachable!(),
};
assert!(!tx.uses_durable_nonce());
}
#[test]
fn tx_uses_nonce_first_prog_id_not_nonce_fail() {
let from_keypair = Keypair::new();
let from_pubkey = from_keypair.pubkey();
let nonce_keypair = Keypair::new();
let nonce_pubkey = nonce_keypair.pubkey();
let instructions = [
system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
];
let message = LegacyMessage::new(&instructions, Some(&from_pubkey));
let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
let tx = VersionedTransaction::from(tx);
assert!(!tx.uses_durable_nonce());
}
#[test]
fn tx_uses_ro_nonce_account() {
let from_keypair = Keypair::new();
let from_pubkey = from_keypair.pubkey();
let nonce_keypair = Keypair::new();
let nonce_pubkey = nonce_keypair.pubkey();
let account_metas = vec![
AccountMeta::new_readonly(nonce_pubkey, false),
#[allow(deprecated)]
AccountMeta::new_readonly(sysvar::recent_blockhashes::id(), false),
AccountMeta::new_readonly(nonce_pubkey, true),
];
let nonce_instruction = Instruction::new_with_bincode(
system_program::id(),
&system_instruction::SystemInstruction::AdvanceNonceAccount,
account_metas,
);
let tx = Transaction::new_signed_with_payer(
&[nonce_instruction],
Some(&from_pubkey),
&[&from_keypair, &nonce_keypair],
Hash::default(),
);
let tx = VersionedTransaction::from(tx);
assert!(!tx.uses_durable_nonce());
}
#[test]
fn tx_uses_nonce_wrong_first_nonce_ix_fail() {
let from_keypair = Keypair::new();
let from_pubkey = from_keypair.pubkey();
let nonce_keypair = Keypair::new();
let nonce_pubkey = nonce_keypair.pubkey();
let instructions = [
system_instruction::withdraw_nonce_account(
&nonce_pubkey,
&nonce_pubkey,
&from_pubkey,
42,
),
system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
];
let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
let tx = VersionedTransaction::from(tx);
assert!(!tx.uses_durable_nonce());
}
}