Store versioned transactions in the ledger, disabled by default (#19139)

* Add support for versioned transactions, but disable by default

* merge conflicts

* trent's feedback

* bump Cargo.lock

* Fix transaction error encoding

* Rename legacy_transaction method

* cargo clippy

* Clean up casts, int arithmetic, and unused methods

* Check for duplicates in sanitized message conversion

* fix clippy

* fix new test

* Fix bpf conditional compilation for message module
This commit is contained in:
Justin Starry
2021-08-17 15:17:56 -07:00
committed by GitHub
parent 098e2b2de3
commit c50b01cb60
47 changed files with 2373 additions and 1049 deletions

View File

@ -1,10 +1,12 @@
#![cfg(feature = "full")]
use crate::transaction::{Transaction, TransactionError};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_sdk::{
borsh::try_from_slice_unchecked,
instruction::{Instruction, InstructionError},
use {
crate::{
borsh::try_from_slice_unchecked,
instruction::{Instruction, InstructionError},
transaction::{SanitizedTransaction, TransactionError},
},
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
};
crate::declare_id!("ComputeBudget111111111111111111111111111111");
@ -96,11 +98,14 @@ impl ComputeBudget {
heap_size: None,
}
}
pub fn process_transaction(&mut self, tx: &Transaction) -> Result<(), TransactionError> {
pub fn process_transaction(
&mut self,
tx: &SanitizedTransaction,
) -> Result<(), TransactionError> {
let error = TransactionError::InstructionError(0, InstructionError::InvalidInstructionData);
// Compute budget instruction must be in 1st or 2nd instruction (avoid nonce marker)
for instruction in tx.message().instructions.iter().take(2) {
if check_id(instruction.program_id(&tx.message().account_keys)) {
for (program_id, instruction) in tx.message().program_instructions_iter().take(2) {
if check_id(program_id) {
let ComputeBudgetInstruction::RequestUnits(units) =
try_from_slice_unchecked::<ComputeBudgetInstruction>(&instruction.data)
.map_err(|_| error.clone())?;
@ -117,22 +122,30 @@ impl ComputeBudget {
#[cfg(test)]
mod tests {
use super::*;
use crate::{hash::Hash, message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer};
use crate::{
hash::Hash, message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer,
transaction::Transaction,
};
use std::convert::TryInto;
fn sanitize_tx(tx: Transaction) -> SanitizedTransaction {
tx.try_into().unwrap()
}
#[test]
fn test_process_transaction() {
let payer_keypair = Keypair::new();
let mut compute_budget = ComputeBudget::default();
let tx = Transaction::new(
let tx = sanitize_tx(Transaction::new(
&[&payer_keypair],
Message::new(&[], Some(&payer_keypair.pubkey())),
Hash::default(),
);
));
compute_budget.process_transaction(&tx).unwrap();
assert_eq!(compute_budget, ComputeBudget::default());
let tx = Transaction::new(
let tx = sanitize_tx(Transaction::new(
&[&payer_keypair],
Message::new(
&[
@ -142,7 +155,7 @@ mod tests {
Some(&payer_keypair.pubkey()),
),
Hash::default(),
);
));
compute_budget.process_transaction(&tx).unwrap();
assert_eq!(
compute_budget,
@ -152,7 +165,7 @@ mod tests {
}
);
let tx = Transaction::new(
let tx = sanitize_tx(Transaction::new(
&[&payer_keypair],
Message::new(
&[
@ -162,7 +175,7 @@ mod tests {
Some(&payer_keypair.pubkey()),
),
Hash::default(),
);
));
let result = compute_budget.process_transaction(&tx);
assert_eq!(
result,
@ -172,7 +185,7 @@ mod tests {
))
);
let tx = Transaction::new(
let tx = sanitize_tx(Transaction::new(
&[&payer_keypair],
Message::new(
&[
@ -182,7 +195,7 @@ mod tests {
Some(&payer_keypair.pubkey()),
),
Hash::default(),
);
));
compute_budget.process_transaction(&tx).unwrap();
assert_eq!(
compute_budget,

View File

@ -195,6 +195,10 @@ pub mod mem_overlap_fix {
solana_sdk::declare_id!("vXDCFK7gphrEmyf5VnKgLmqbdJ4UxD2eZH1qbdouYKF");
}
pub mod versioned_tx_message_enabled {
solana_sdk::declare_id!("3KZZ6Ks1885aGBQ45fwRcPXVBCtzUvxhUTkwKMR41Tca");
}
lazy_static! {
/// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
@ -238,6 +242,7 @@ lazy_static! {
(stake_merge_with_unmatched_credits_observed::id(), "allow merging active stakes with unmatched credits_observed #18985"),
(gate_large_block::id(), "validator checks block cost against max limit in realtime, reject if exceeds."),
(mem_overlap_fix::id(), "Memory overlap fix"),
(versioned_tx_message_enabled::id(), "enable versioned transaction message processing"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()

View File

@ -40,7 +40,6 @@ pub mod program_utils;
pub mod pubkey;
pub mod recent_blockhashes_account;
pub mod rpc_port;
pub mod sanitized_transaction;
pub mod secp256k1_instruction;
pub mod shred_version;
pub mod signature;

View File

@ -1,87 +0,0 @@
#![cfg(feature = "full")]
use crate::{
hash::Hash,
sanitize::Sanitize,
transaction::{Result, Transaction, TransactionError},
};
use std::{borrow::Cow, convert::TryFrom, ops::Deref};
/// Sanitized transaction and the hash of its message
#[derive(Debug, Clone)]
pub struct SanitizedTransaction<'a> {
transaction: Cow<'a, Transaction>,
pub message_hash: Hash,
}
impl<'a> SanitizedTransaction<'a> {
pub fn try_create(transaction: Cow<'a, Transaction>, message_hash: Hash) -> Result<Self> {
transaction.sanitize()?;
if Self::has_duplicates(&transaction.message.account_keys) {
return Err(TransactionError::AccountLoadedTwice);
}
Ok(Self {
transaction,
message_hash,
})
}
/// Return true if the slice has any duplicate elements
pub fn has_duplicates<T: PartialEq>(xs: &[T]) -> bool {
// Note: This is an O(n^2) algorithm, but requires no heap allocations. The benchmark
// `bench_has_duplicates` in benches/message_processor.rs shows that this implementation is
// ~50 times faster than using HashSet for very short slices.
for i in 1..xs.len() {
#[allow(clippy::integer_arithmetic)]
if xs[i..].contains(&xs[i - 1]) {
return true;
}
}
false
}
}
impl Deref for SanitizedTransaction<'_> {
type Target = Transaction;
fn deref(&self) -> &Self::Target {
&self.transaction
}
}
impl<'a> TryFrom<Transaction> for SanitizedTransaction<'_> {
type Error = TransactionError;
fn try_from(transaction: Transaction) -> Result<Self> {
let message_hash = transaction.message().hash();
Self::try_create(Cow::Owned(transaction), message_hash)
}
}
impl<'a> TryFrom<&'a Transaction> for SanitizedTransaction<'a> {
type Error = TransactionError;
fn try_from(transaction: &'a Transaction) -> Result<Self> {
let message_hash = transaction.message().hash();
Self::try_create(Cow::Borrowed(transaction), message_hash)
}
}
pub trait SanitizedTransactionSlice<'a> {
fn as_transactions_iter(&'a self) -> Box<dyn Iterator<Item = &'a Transaction> + '_>;
}
impl<'a> SanitizedTransactionSlice<'a> for [SanitizedTransaction<'a>] {
fn as_transactions_iter(&'a self) -> Box<dyn Iterator<Item = &'a Transaction> + '_> {
Box::new(self.iter().map(Deref::deref))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_duplicates() {
assert!(!SanitizedTransaction::has_duplicates(&[1, 2]));
assert!(SanitizedTransaction::has_duplicates(&[1, 2, 1]));
}
}

View File

@ -2,23 +2,31 @@
#![cfg(feature = "full")]
use crate::sanitize::{Sanitize, SanitizeError};
use crate::secp256k1_instruction::verify_eth_addresses;
use crate::{
hash::Hash,
instruction::{CompiledInstruction, Instruction, InstructionError},
message::Message,
nonce::NONCED_TX_MARKER_IX_INDEX,
program_utils::limited_deserialize,
pubkey::Pubkey,
short_vec,
signature::{Signature, SignerError},
signers::Signers,
system_instruction::SystemInstruction,
system_program,
use {
crate::{
hash::Hash,
instruction::{CompiledInstruction, Instruction, InstructionError},
message::{Message, SanitizeMessageError},
nonce::NONCED_TX_MARKER_IX_INDEX,
program_utils::limited_deserialize,
pubkey::Pubkey,
sanitize::{Sanitize, SanitizeError},
secp256k1_instruction::verify_eth_addresses,
short_vec,
signature::{Signature, SignerError},
signers::Signers,
},
serde::Serialize,
solana_program::{system_instruction::SystemInstruction, system_program},
std::result,
thiserror::Error,
};
use std::result;
use thiserror::Error;
mod sanitized;
mod versioned;
pub use sanitized::*;
pub use versioned::*;
/// Reasons a transaction might be rejected.
#[derive(
@ -104,6 +112,10 @@ pub enum TransactionError {
"Transaction could not fit into current block without exceeding the Max Block Cost Limit"
)]
WouldExceedMaxBlockCostLimit,
/// Transaction version is unsupported
#[error("Transaction version is unsupported")]
UnsupportedVersion,
}
pub type Result<T> = result::Result<T, TransactionError>;
@ -114,6 +126,17 @@ impl From<SanitizeError> for TransactionError {
}
}
impl From<SanitizeMessageError> for TransactionError {
fn from(err: SanitizeMessageError) -> Self {
match err {
SanitizeMessageError::IndexOutOfBounds
| SanitizeMessageError::ValueOutOfBounds
| SanitizeMessageError::InvalidValue => Self::SanitizeFailure,
SanitizeMessageError::DuplicateAccountKey => Self::AccountLoadedTwice,
}
}
}
/// An atomic transaction
#[frozen_abi(digest = "FZtncnS1Xk8ghHfKiXE5oGiUbw2wJhmfXQuNgQR3K6Mc")]
#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)]
@ -230,10 +253,12 @@ impl Transaction {
.and_then(|instruction| instruction.accounts.get(accounts_index))
.map(|&account_keys_index| account_keys_index as usize)
}
pub fn key(&self, instruction_index: usize, accounts_index: usize) -> Option<&Pubkey> {
self.key_index(instruction_index, accounts_index)
.and_then(|account_keys_index| self.message.account_keys.get(account_keys_index))
}
pub fn signer_key(&self, instruction_index: usize, accounts_index: usize) -> Option<&Pubkey> {
match self.key_index(instruction_index, accounts_index) {
None => None,
@ -484,6 +509,7 @@ pub fn uses_durable_nonce(tx: &Transaction) -> Option<&CompiledInstruction> {
)
}
#[deprecated]
pub fn get_nonce_pubkey_from_instruction<'a>(
ix: &CompiledInstruction,
tx: &'a Transaction,
@ -496,6 +522,8 @@ pub fn get_nonce_pubkey_from_instruction<'a>(
#[cfg(test)]
mod tests {
#![allow(deprecated)]
use super::*;
use crate::{
hash::hash,
@ -553,6 +581,7 @@ mod tests {
assert_eq!(*get_program_id(&tx, 0), prog1);
assert_eq!(*get_program_id(&tx, 1), prog2);
}
#[test]
fn test_refs_invalid_program_id() {
let key = Keypair::new();

View File

@ -0,0 +1,226 @@
#![cfg(feature = "full")]
use {
crate::{
hash::Hash,
message::{v0, MappedAddresses, MappedMessage, SanitizedMessage, VersionedMessage},
nonce::NONCED_TX_MARKER_IX_INDEX,
program_utils::limited_deserialize,
pubkey::Pubkey,
sanitize::Sanitize,
secp256k1_instruction::verify_eth_addresses,
secp256k1_program,
signature::Signature,
transaction::{Result, Transaction, TransactionError, VersionedTransaction},
},
solana_program::{system_instruction::SystemInstruction, system_program},
std::convert::TryFrom,
};
/// Sanitized transaction and the hash of its message
#[derive(Debug, Clone)]
pub struct SanitizedTransaction {
message: SanitizedMessage,
message_hash: Hash,
signatures: Vec<Signature>,
}
/// Set of accounts that must be locked for safe transaction processing
#[derive(Debug, Clone, Default)]
pub struct TransactionAccountLocks<'a> {
/// List of readonly account key locks
pub readonly: Vec<&'a Pubkey>,
/// List of writable account key locks
pub writable: Vec<&'a Pubkey>,
}
impl TryFrom<Transaction> for SanitizedTransaction {
type Error = TransactionError;
fn try_from(tx: Transaction) -> Result<Self> {
tx.sanitize()?;
if tx.message.has_duplicates() {
return Err(TransactionError::AccountLoadedTwice);
}
Ok(Self {
message_hash: tx.message.hash(),
message: SanitizedMessage::Legacy(tx.message),
signatures: tx.signatures,
})
}
}
impl SanitizedTransaction {
/// Create a sanitized transaction from an unsanitized transaction.
/// If the input transaction uses address maps, attempt to map the
/// transaction keys to full addresses.
pub fn try_create(
tx: VersionedTransaction,
message_hash: Hash,
address_mapper: impl Fn(&v0::Message) -> Result<MappedAddresses>,
) -> Result<Self> {
tx.sanitize()?;
let signatures = tx.signatures;
let message = match tx.message {
VersionedMessage::Legacy(message) => SanitizedMessage::Legacy(message),
VersionedMessage::V0(message) => SanitizedMessage::V0(MappedMessage {
mapped_addresses: address_mapper(&message)?,
message,
}),
};
if message.has_duplicates() {
return Err(TransactionError::AccountLoadedTwice);
}
Ok(Self {
message,
message_hash,
signatures,
})
}
/// Return the first signature for this transaction.
///
/// Notes:
///
/// Sanitized transactions must have at least one signature because the
/// number of signatures must be greater than or equal to the message header
/// value `num_required_signatures` which must be greater than 0 itself.
pub fn signature(&self) -> &Signature {
&self.signatures[0]
}
/// Return the list of signatures for this transaction
pub fn signatures(&self) -> &[Signature] {
&self.signatures
}
/// Return the signed message
pub fn message(&self) -> &SanitizedMessage {
&self.message
}
/// Return the hash of the signed message
pub fn message_hash(&self) -> &Hash {
&self.message_hash
}
/// Convert this sanitized transaction into a versioned transaction for
/// recording in the ledger.
pub fn to_versioned_transaction(&self) -> VersionedTransaction {
let signatures = self.signatures.clone();
match &self.message {
SanitizedMessage::V0(mapped_msg) => VersionedTransaction {
signatures,
message: VersionedMessage::V0(mapped_msg.message.clone()),
},
SanitizedMessage::Legacy(message) => VersionedTransaction {
signatures,
message: VersionedMessage::Legacy(message.clone()),
},
}
}
/// Return the list of accounts that must be locked during processing this transaction.
pub fn get_account_locks(&self) -> TransactionAccountLocks {
let message = &self.message;
let num_readonly_accounts = message.num_readonly_accounts();
let num_writable_accounts = message
.account_keys_len()
.saturating_sub(num_readonly_accounts);
let mut account_locks = TransactionAccountLocks {
writable: Vec::with_capacity(num_writable_accounts),
readonly: Vec::with_capacity(num_readonly_accounts),
};
for (i, key) in message.account_keys_iter().enumerate() {
if message.is_writable(i) {
account_locks.writable.push(key);
} else {
account_locks.readonly.push(key);
}
}
account_locks
}
/// If the transaction uses a durable nonce, return the pubkey of the nonce account
pub fn get_durable_nonce(&self) -> Option<&Pubkey> {
self.message
.instructions()
.get(NONCED_TX_MARKER_IX_INDEX as usize)
.filter(
|ix| match self.message.get_account_key(ix.program_id_index as usize) {
Some(program_id) => system_program::check_id(program_id),
_ => false,
},
)
.filter(|ix| {
matches!(
limited_deserialize(&ix.data),
Ok(SystemInstruction::AdvanceNonceAccount)
)
})
.and_then(|ix| {
ix.accounts.get(0).and_then(|idx| {
let idx = *idx as usize;
self.message.get_account_key(idx)
})
})
}
/// Return the serialized message data to sign.
fn message_data(&self) -> Vec<u8> {
match &self.message {
SanitizedMessage::Legacy(message) => message.serialize(),
SanitizedMessage::V0(mapped_msg) => mapped_msg.message.serialize(),
}
}
/// Verify the length of signatures matches the value in the message header
pub fn verify_signatures_len(&self) -> bool {
self.signatures.len() == self.message.header().num_required_signatures as usize
}
/// Verify the transaction signatures
pub fn verify(&self) -> Result<()> {
let message_bytes = self.message_data();
if self
.signatures
.iter()
.zip(self.message.account_keys_iter())
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
.any(|verified| !verified)
{
Err(TransactionError::SignatureFailure)
} else {
Ok(())
}
}
/// Verify the encoded secp256k1 signatures in this transaction
pub fn verify_precompiles(&self, libsecp256k1_0_5_upgrade_enabled: bool) -> Result<()> {
for (program_id, instruction) in self.message.program_instructions_iter() {
if secp256k1_program::check_id(program_id) {
let instruction_datas: Vec<_> = self
.message
.instructions()
.iter()
.map(|instruction| instruction.data.as_ref())
.collect();
let data = &instruction.data;
let e = verify_eth_addresses(
data,
&instruction_datas,
libsecp256k1_0_5_upgrade_enabled,
);
e.map_err(|_| TransactionError::InvalidAccountIndex)?;
}
}
Ok(())
}
}

View File

@ -0,0 +1,84 @@
//! Defines a transaction which supports multiple versions of messages.
#![cfg(feature = "full")]
use {
crate::{
hash::Hash,
message::VersionedMessage,
sanitize::{Sanitize, SanitizeError},
short_vec,
signature::Signature,
transaction::{Result, Transaction, TransactionError},
},
serde::Serialize,
};
// 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 Sanitize for VersionedTransaction {
fn sanitize(&self) -> std::result::Result<(), SanitizeError> {
self.message.sanitize()?;
// Once the "verify_tx_signatures_len" feature is enabled, this may be
// updated to an equality check.
if usize::from(self.message.header().num_required_signatures) > self.signatures.len() {
return Err(SanitizeError::IndexOutOfBounds);
}
// Signatures are verified before message keys are mapped so all signers
// must correspond to unmapped keys.
if self.signatures.len() > self.message.unmapped_keys_len() {
return Err(SanitizeError::IndexOutOfBounds);
}
Ok(())
}
}
impl From<Transaction> for VersionedTransaction {
fn from(transaction: Transaction) -> Self {
Self {
signatures: transaction.signatures,
message: VersionedMessage::Legacy(transaction.message),
}
}
}
impl VersionedTransaction {
/// 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
.signatures
.iter()
.zip(self.message.unmapped_keys_iter())
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
.any(|verified| !verified)
{
Err(TransactionError::SignatureFailure)
} else {
Ok(VersionedMessage::hash_raw_message(&message_bytes))
}
}
}