Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

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

13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ members = [
"rpc-client-nonce-utils",
"rpc-test",
"runtime",
"runtime-transaction",
"sdk",
"sdk/cargo-build-bpf",
"sdk/cargo-build-sbf",
Expand Down Expand Up @@ -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" }
Expand Down
30 changes: 30 additions & 0 deletions runtime-transaction/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
1 change: 1 addition & 0 deletions runtime-transaction/build.rs
5 changes: 5 additions & 0 deletions runtime-transaction/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
117 changes: 117 additions & 0 deletions runtime-transaction/src/runtime_transaction.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
44 changes: 44 additions & 0 deletions runtime-transaction/src/transaction_meta.rs
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions sdk/src/simple_vote_transaction_checker.rs
Original file line number Diff line number Diff line change
@@ -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)
}
42 changes: 9 additions & 33 deletions sdk/src/transaction/sanitized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
Expand Down Expand Up @@ -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> {
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/transaction/versioned/sanitized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be an impl From<SanitizedVersionedTransaction> for (Vec<Signature>, SanitizedVersionedMessage) if not make the fields pub instead of pub (crate) to allow in place destructuring

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fields on SanitizedVersionedMessage should not be pub. Them being private is intentional, see the previous discussion on it: #33471 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From impl, then. i would assume a method called destruct() to do something... destructive.

(self.signatures, self.message)
}
}

#[cfg(test)]
Expand Down