Create runtime-transaction crate to build transaction types with state for runtime use (#33471)

* Create runtime-transaction crate to host transaction type for runtime that comes with metadata

* create separate structs for runtime in different state, add corresponding traits for transaction_meta

* share simple-vote checking code

* not to expose private fields to outside of sdk

---------

Co-authored-by: Andrew Fitzgerald <apfitzge@gmail.com>
This commit is contained in:
Tao Zhu 2023-11-15 22:29:37 -06:00 committed by GitHub
parent f9fba7e08b
commit e790f098b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 255 additions and 33 deletions

13
Cargo.lock generated
View File

@ -7065,6 +7065,19 @@ dependencies = [
"zstd",
]
[[package]]
name = "solana-runtime-transaction"
version = "1.18.0"
dependencies = [
"bincode",
"log",
"rand 0.8.5",
"rustc_version 0.4.0",
"solana-program-runtime",
"solana-sdk",
"thiserror",
]
[[package]]
name = "solana-sdk"
version = "1.18.0"

View File

@ -85,6 +85,7 @@ members = [
"rpc-client-nonce-utils",
"rpc-test",
"runtime",
"runtime-transaction",
"sdk",
"sdk/cargo-build-bpf",
"sdk/cargo-build-sbf",
@ -361,6 +362,7 @@ solana-rpc-client = { path = "rpc-client", version = "=1.18.0", default-features
solana-rpc-client-api = { path = "rpc-client-api", version = "=1.18.0" }
solana-rpc-client-nonce-utils = { path = "rpc-client-nonce-utils", version = "=1.18.0" }
solana-runtime = { path = "runtime", version = "=1.18.0" }
solana-runtime-transaction = { path = "runtime-transaction", version = "=1.18.0" }
solana-sdk = { path = "sdk", version = "=1.18.0" }
solana-sdk-macro = { path = "sdk/macro", version = "=1.18.0" }
solana-send-transaction-service = { path = "send-transaction-service", version = "=1.18.0" }

View File

@ -0,0 +1,30 @@
[package]
name = "solana-runtime-transaction"
description = "Solana runtime-transaction"
documentation = "https://docs.rs/solana-runtime-transaction"
version = { workspace = true }
authors = { workspace = true }
repository = { workspace = true }
homepage = { workspace = true }
license = { workspace = true }
edition = { workspace = true }
[dependencies]
log = { workspace = true }
solana-program-runtime = { workspace = true }
solana-sdk = { workspace = true }
thiserror = { workspace = true }
[lib]
crate-type = ["lib"]
name = "solana_runtime_transaction"
[dev-dependencies]
bincode = { workspace = true }
rand = { workspace = true }
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[build-dependencies]
rustc_version = { workspace = true }

View File

@ -0,0 +1 @@
../frozen-abi/build.rs

View File

@ -0,0 +1,5 @@
#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))]
#![allow(clippy::arithmetic_side_effects)]
pub mod runtime_transaction;
pub mod transaction_meta;

View File

@ -0,0 +1,117 @@
/// RuntimeTransaction is `runtime` facing representation of transaction, while
/// solana_sdk::SanitizedTransaction is client facing representation.
///
/// It has two states:
/// 1. Statically Loaded: after receiving `packet` from sigverify and deserializing
/// it into `solana_sdk::VersionedTransaction`, then sanitizing into
/// `solana_sdk::SanitizedVersionedTransaction`, `RuntimeTransactionStatic`
/// can be created from it with static transaction metadata extracted.
/// 2. Dynamically Loaded: after successfully loaded account addresses from onchain
/// ALT, RuntimeTransaction transits into Dynamically Loaded state, with
/// its dynamic metadata loaded.
use {
crate::transaction_meta::{DynamicMeta, StaticMeta, TransactionMeta},
solana_sdk::{
hash::Hash,
message::{AddressLoader, SanitizedMessage, SanitizedVersionedMessage},
signature::Signature,
simple_vote_transaction_checker::is_simple_vote_transaction,
transaction::{Result, SanitizedVersionedTransaction},
},
};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RuntimeTransactionStatic {
// sanitized signatures
signatures: Vec<Signature>,
// sanitized message
message: SanitizedVersionedMessage,
// transaction meta is a collection of fields, it is updated
// during message state transition
meta: TransactionMeta,
}
impl StaticMeta for RuntimeTransactionStatic {
fn message_hash(&self) -> &Hash {
&self.meta.message_hash
}
fn is_simple_vote_tx(&self) -> bool {
self.meta.is_simple_vote_tx
}
}
impl RuntimeTransactionStatic {
pub fn try_from(
sanitized_versioned_tx: SanitizedVersionedTransaction,
message_hash: Option<Hash>,
is_simple_vote_tx: Option<bool>,
) -> Result<Self> {
let mut meta = TransactionMeta::default();
meta.set_is_simple_vote_tx(
is_simple_vote_tx
.unwrap_or_else(|| is_simple_vote_transaction(&sanitized_versioned_tx)),
);
let (signatures, message) = sanitized_versioned_tx.destruct();
meta.set_message_hash(message_hash.unwrap_or_else(|| message.message.hash()));
Ok(Self {
signatures,
message,
meta,
})
}
}
/// Statically Loaded transaction can transit to Dynamically Loaded with supplied
/// address_loader, to load accounts from on-chain ALT, then resolve dynamic metadata
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RuntimeTransactionDynamic {
// sanitized signatures
signatures: Vec<Signature>,
// sanitized message
message: SanitizedMessage,
// transaction meta is a collection of fields, it is updated
// during message state transition
meta: TransactionMeta,
}
impl DynamicMeta for RuntimeTransactionDynamic {}
impl StaticMeta for RuntimeTransactionDynamic {
fn message_hash(&self) -> &Hash {
&self.meta.message_hash
}
fn is_simple_vote_tx(&self) -> bool {
self.meta.is_simple_vote_tx
}
}
impl RuntimeTransactionDynamic {
pub fn try_from(
statically_loaded_runtime_tx: RuntimeTransactionStatic,
address_loader: impl AddressLoader,
) -> Result<Self> {
let mut tx = Self {
signatures: statically_loaded_runtime_tx.signatures,
message: SanitizedMessage::try_new(
statically_loaded_runtime_tx.message,
address_loader,
)?,
meta: statically_loaded_runtime_tx.meta,
};
tx.load_dynamic_metadata()?;
Ok(tx)
}
// private helpers
fn load_dynamic_metadata(&mut self) -> Result<()> {
Ok(())
}
}

View File

@ -0,0 +1,44 @@
//! Transaction Meta contains data that follows a transaction through the
//! execution pipeline in runtime. Examples of metadata could be limits
//! specified by compute-budget instructions, simple-vote flag, transaction
//! costs, durable nonce account etc;
//!
//! The premise is if anything qualifies as metadata, then it must be valid
//! and available as long as the transaction itself is valid and available.
//! Hence they are not Option<T> type. Their visibility at different states
//! are defined in traits.
//!
//! The StaticMeta and DynamicMeta traits are accessor traits on the
//! RuntimeTransaction types, not the TransactionMeta itself.
//!
use solana_sdk::hash::Hash;
/// metadata can be extracted statically from sanitized transaction,
/// for example: message hash, simple-vote-tx flag, compute budget limits,
pub trait StaticMeta {
fn message_hash(&self) -> &Hash;
fn is_simple_vote_tx(&self) -> bool;
}
/// Statically loaded meta is a supertrait of Dynamically loaded meta, when
/// transaction transited successfully into dynamically loaded, it should
/// have both meta data populated and available.
/// Dynamic metadata available after accounts addresses are loaded from
/// on-chain ALT, examples are: transaction usage costs, nonce account.
pub trait DynamicMeta: StaticMeta {}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TransactionMeta {
pub(crate) message_hash: Hash,
pub(crate) is_simple_vote_tx: bool,
}
impl TransactionMeta {
pub fn set_message_hash(&mut self, message_hash: Hash) {
self.message_hash = message_hash;
}
pub fn set_is_simple_vote_tx(&mut self, is_simple_vote_tx: bool) {
self.is_simple_vote_tx = is_simple_vote_tx;
}
}

View File

@ -95,6 +95,7 @@ pub mod secp256k1_instruction;
pub mod shred_version;
pub mod signature;
pub mod signer;
pub mod simple_vote_transaction_checker;
pub mod system_transaction;
pub mod timing;
pub mod transaction;

View File

@ -0,0 +1,28 @@
#![cfg(feature = "full")]
use crate::{message::VersionedMessage, transaction::SanitizedVersionedTransaction};
/// Simple vote transaction meets these conditions:
/// 1. has 1 or 2 signatures;
/// 2. is legacy message;
/// 3. has only one instruction;
/// 4. which must be Vote instruction;
pub fn is_simple_vote_transaction(
sanitized_versioned_transaction: &SanitizedVersionedTransaction,
) -> bool {
let signatures_count = sanitized_versioned_transaction.signatures.len();
let is_legacy_message = matches!(
sanitized_versioned_transaction.message.message,
VersionedMessage::Legacy(_)
);
let mut instructions = sanitized_versioned_transaction
.message
.program_instructions_iter();
signatures_count < 3
&& is_legacy_message
&& instructions
.next()
.xor(instructions.next())
.map(|(program_id, _ix)| program_id == &solana_sdk::vote::program::id())
.unwrap_or(false)
}

View File

@ -14,6 +14,7 @@ use {
pubkey::Pubkey,
sanitize::Sanitize,
signature::Signature,
simple_vote_transaction_checker::is_simple_vote_transaction,
solana_sdk::feature_set,
transaction::{Result, Transaction, TransactionError, VersionedTransaction},
},
@ -96,44 +97,19 @@ impl SanitizedTransaction {
is_simple_vote_tx: Option<bool>,
address_loader: impl AddressLoader,
) -> Result<Self> {
tx.sanitize()?;
let sanitized_versioned_tx = SanitizedVersionedTransaction::try_from(tx)?;
let is_simple_vote_tx = is_simple_vote_tx
.unwrap_or_else(|| is_simple_vote_transaction(&sanitized_versioned_tx));
let message_hash = match message_hash.into() {
MessageHash::Compute => tx.message.hash(),
MessageHash::Compute => sanitized_versioned_tx.message.message.hash(),
MessageHash::Precomputed(hash) => hash,
};
let signatures = tx.signatures;
let message = match tx.message {
VersionedMessage::Legacy(message) => {
SanitizedMessage::Legacy(LegacyMessage::new(message))
}
VersionedMessage::V0(message) => {
let loaded_addresses =
address_loader.load_addresses(&message.address_table_lookups)?;
SanitizedMessage::V0(v0::LoadedMessage::new(message, loaded_addresses))
}
};
let is_simple_vote_tx = is_simple_vote_tx.unwrap_or_else(|| {
if signatures.len() < 3
&& message.instructions().len() == 1
&& matches!(message, SanitizedMessage::Legacy(_))
{
let mut ix_iter = message.program_instructions_iter();
ix_iter.next().map(|(program_id, _ix)| program_id)
== Some(&crate::vote::program::id())
} else {
false
}
});
Ok(Self {
message,
Self::try_new(
sanitized_versioned_tx,
message_hash,
is_simple_vote_tx,
signatures,
})
address_loader,
)
}
pub fn try_from_legacy_transaction(tx: Transaction) -> Result<Self> {

View File

@ -32,6 +32,11 @@ impl SanitizedVersionedTransaction {
pub fn get_message(&self) -> &SanitizedVersionedMessage {
&self.message
}
/// Consumes the SanitizedVersionedTransaction, returning the fields individually.
pub fn destruct(self) -> (Vec<Signature>, SanitizedVersionedMessage) {
(self.signatures, self.message)
}
}
#[cfg(test)]