solana_cost_model/
transaction_cost.rs

1#[cfg(feature = "dev-context-only-utils")]
2use solana_compute_budget_instruction::compute_budget_instruction_details::ComputeBudgetInstructionDetails;
3use {
4    crate::block_cost_limits, solana_pubkey::Pubkey,
5    solana_runtime_transaction::transaction_meta::StaticMeta,
6    solana_svm_transaction::svm_message::SVMMessage,
7};
8
9/// TransactionCost is used to represent resources required to process
10/// a transaction, denominated in CU (eg. Compute Units).
11/// Resources required to process a regular transaction often include
12/// an array of variables, such as execution cost, loaded bytes, write
13/// lock and read lock etc.
14/// SimpleVote has a simpler and pre-determined format: it has 1 or 2 signatures,
15/// 2 write locks, a vote instruction and less than 32k (page size) accounts to load.
16/// It's cost therefore can be static #33269.
17const SIMPLE_VOTE_USAGE_COST: u64 = 3428;
18
19#[derive(Debug)]
20pub enum TransactionCost<'a, Tx> {
21    SimpleVote { transaction: &'a Tx },
22    Transaction(UsageCostDetails<'a, Tx>),
23}
24
25impl<Tx> TransactionCost<'_, Tx> {
26    pub fn sum(&self) -> u64 {
27        #![allow(clippy::assertions_on_constants)]
28        match self {
29            Self::SimpleVote { .. } => {
30                const _: () = assert!(
31                    SIMPLE_VOTE_USAGE_COST
32                        == solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS
33                            + block_cost_limits::SIGNATURE_COST
34                            + 2 * block_cost_limits::WRITE_LOCK_UNITS
35                            + 8
36                );
37
38                SIMPLE_VOTE_USAGE_COST
39            }
40            Self::Transaction(usage_cost) => usage_cost.sum(),
41        }
42    }
43
44    pub fn programs_execution_cost(&self) -> u64 {
45        match self {
46            Self::SimpleVote { .. } => solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS,
47            Self::Transaction(usage_cost) => usage_cost.programs_execution_cost,
48        }
49    }
50
51    pub fn is_simple_vote(&self) -> bool {
52        match self {
53            Self::SimpleVote { .. } => true,
54            Self::Transaction(_) => false,
55        }
56    }
57
58    pub fn data_bytes_cost(&self) -> u64 {
59        match self {
60            Self::SimpleVote { .. } => 0,
61            Self::Transaction(usage_cost) => usage_cost.data_bytes_cost,
62        }
63    }
64
65    pub fn allocated_accounts_data_size(&self) -> u64 {
66        match self {
67            Self::SimpleVote { .. } => 0,
68            Self::Transaction(usage_cost) => usage_cost.allocated_accounts_data_size,
69        }
70    }
71
72    pub fn loaded_accounts_data_size_cost(&self) -> u64 {
73        match self {
74            Self::SimpleVote { .. } => 8, // simple-vote loads less than 32K account data,
75            // the cost round up to be one page (32K) cost: 8CU
76            Self::Transaction(usage_cost) => usage_cost.loaded_accounts_data_size_cost,
77        }
78    }
79
80    pub fn signature_cost(&self) -> u64 {
81        match self {
82            Self::SimpleVote { .. } => block_cost_limits::SIGNATURE_COST,
83            Self::Transaction(usage_cost) => usage_cost.signature_cost,
84        }
85    }
86
87    pub fn write_lock_cost(&self) -> u64 {
88        match self {
89            Self::SimpleVote { .. } => block_cost_limits::WRITE_LOCK_UNITS.saturating_mul(2),
90            Self::Transaction(usage_cost) => usage_cost.write_lock_cost,
91        }
92    }
93}
94
95impl<Tx: SVMMessage> TransactionCost<'_, Tx> {
96    pub fn writable_accounts(&self) -> impl Iterator<Item = &Pubkey> {
97        let transaction = match self {
98            Self::SimpleVote { transaction } => transaction,
99            Self::Transaction(usage_cost) => usage_cost.transaction,
100        };
101        transaction
102            .account_keys()
103            .iter()
104            .enumerate()
105            .filter_map(|(index, key)| transaction.is_writable(index).then_some(key))
106    }
107}
108
109impl<Tx: StaticMeta> TransactionCost<'_, Tx> {
110    pub fn num_transaction_signatures(&self) -> u64 {
111        match self {
112            Self::SimpleVote { .. } => 1,
113            Self::Transaction(usage_cost) => usage_cost
114                .transaction
115                .signature_details()
116                .num_transaction_signatures(),
117        }
118    }
119
120    pub fn num_secp256k1_instruction_signatures(&self) -> u64 {
121        match self {
122            Self::SimpleVote { .. } => 0,
123            Self::Transaction(usage_cost) => usage_cost
124                .transaction
125                .signature_details()
126                .num_secp256k1_instruction_signatures(),
127        }
128    }
129
130    pub fn num_ed25519_instruction_signatures(&self) -> u64 {
131        match self {
132            Self::SimpleVote { .. } => 0,
133            Self::Transaction(usage_cost) => usage_cost
134                .transaction
135                .signature_details()
136                .num_ed25519_instruction_signatures(),
137        }
138    }
139
140    pub fn num_secp256r1_instruction_signatures(&self) -> u64 {
141        match self {
142            Self::SimpleVote { .. } => 0,
143            Self::Transaction(usage_cost) => usage_cost
144                .transaction
145                .signature_details()
146                .num_secp256r1_instruction_signatures(),
147        }
148    }
149}
150
151// costs are stored in number of 'compute unit's
152#[derive(Debug)]
153pub struct UsageCostDetails<'a, Tx> {
154    pub transaction: &'a Tx,
155    pub signature_cost: u64,
156    pub write_lock_cost: u64,
157    pub data_bytes_cost: u64,
158    pub programs_execution_cost: u64,
159    pub loaded_accounts_data_size_cost: u64,
160    pub allocated_accounts_data_size: u64,
161}
162
163impl<Tx> UsageCostDetails<'_, Tx> {
164    pub fn sum(&self) -> u64 {
165        self.signature_cost
166            .saturating_add(self.write_lock_cost)
167            .saturating_add(self.data_bytes_cost)
168            .saturating_add(self.programs_execution_cost)
169            .saturating_add(self.loaded_accounts_data_size_cost)
170    }
171}
172
173#[cfg(feature = "dev-context-only-utils")]
174#[derive(Debug)]
175pub struct WritableKeysTransaction(pub Vec<Pubkey>);
176
177#[cfg(feature = "dev-context-only-utils")]
178impl solana_svm_transaction::svm_message::SVMMessage for WritableKeysTransaction {
179    fn num_transaction_signatures(&self) -> u64 {
180        unimplemented!("WritableKeysTransaction::num_transaction_signatures")
181    }
182
183    fn num_write_locks(&self) -> u64 {
184        unimplemented!("WritableKeysTransaction::num_write_locks")
185    }
186
187    fn recent_blockhash(&self) -> &solana_hash::Hash {
188        unimplemented!("WritableKeysTransaction::recent_blockhash")
189    }
190
191    fn num_instructions(&self) -> usize {
192        unimplemented!("WritableKeysTransaction::num_instructions")
193    }
194
195    fn instructions_iter(
196        &self,
197    ) -> impl Iterator<Item = solana_svm_transaction::instruction::SVMInstruction> {
198        core::iter::empty()
199    }
200
201    fn program_instructions_iter(
202        &self,
203    ) -> impl Iterator<Item = (&Pubkey, solana_svm_transaction::instruction::SVMInstruction)> + Clone
204    {
205        core::iter::empty()
206    }
207
208    fn account_keys(&self) -> solana_message::AccountKeys {
209        solana_message::AccountKeys::new(&self.0, None)
210    }
211
212    fn fee_payer(&self) -> &Pubkey {
213        unimplemented!("WritableKeysTransaction::fee_payer")
214    }
215
216    fn is_writable(&self, _index: usize) -> bool {
217        true
218    }
219
220    fn is_signer(&self, _index: usize) -> bool {
221        unimplemented!("WritableKeysTransaction::is_signer")
222    }
223
224    fn is_invoked(&self, _key_index: usize) -> bool {
225        unimplemented!("WritableKeysTransaction::is_invoked")
226    }
227
228    fn num_lookup_tables(&self) -> usize {
229        unimplemented!("WritableKeysTransaction::num_lookup_tables")
230    }
231
232    fn message_address_table_lookups(
233        &self,
234    ) -> impl Iterator<
235        Item = solana_svm_transaction::message_address_table_lookup::SVMMessageAddressTableLookup,
236    > {
237        core::iter::empty()
238    }
239}
240
241#[cfg(feature = "dev-context-only-utils")]
242impl solana_svm_transaction::svm_transaction::SVMTransaction for WritableKeysTransaction {
243    fn signature(&self) -> &solana_signature::Signature {
244        unimplemented!("WritableKeysTransaction::signature")
245    }
246
247    fn signatures(&self) -> &[solana_signature::Signature] {
248        unimplemented!("WritableKeysTransaction::signatures")
249    }
250}
251
252#[cfg(feature = "dev-context-only-utils")]
253impl solana_runtime_transaction::transaction_meta::StaticMeta for WritableKeysTransaction {
254    fn message_hash(&self) -> &solana_hash::Hash {
255        unimplemented!("WritableKeysTransaction::message_hash")
256    }
257
258    fn is_simple_vote_transaction(&self) -> bool {
259        unimplemented!("WritableKeysTransaction::is_simple_vote_transaction")
260    }
261
262    fn signature_details(&self) -> &solana_message::TransactionSignatureDetails {
263        const DUMMY: solana_message::TransactionSignatureDetails =
264            solana_message::TransactionSignatureDetails::new(0, 0, 0, 0);
265        &DUMMY
266    }
267
268    fn compute_budget_instruction_details(&self) -> &ComputeBudgetInstructionDetails {
269        unimplemented!("WritableKeysTransaction::compute_budget_instruction_details")
270    }
271}
272
273#[cfg(feature = "dev-context-only-utils")]
274impl solana_runtime_transaction::transaction_with_meta::TransactionWithMeta
275    for WritableKeysTransaction
276{
277    #[allow(refining_impl_trait)]
278    fn as_sanitized_transaction(
279        &self,
280    ) -> std::borrow::Cow<solana_transaction::sanitized::SanitizedTransaction> {
281        unimplemented!("WritableKeysTransaction::as_sanitized_transaction");
282    }
283
284    fn to_versioned_transaction(&self) -> solana_transaction::versioned::VersionedTransaction {
285        unimplemented!("WritableKeysTransaction::to_versioned_transaction")
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use {
292        super::*,
293        crate::cost_model::CostModel,
294        solana_feature_set::FeatureSet,
295        solana_hash::Hash,
296        solana_keypair::Keypair,
297        solana_message::SimpleAddressLoader,
298        solana_reserved_account_keys::ReservedAccountKeys,
299        solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
300        solana_transaction::{sanitized::MessageHash, versioned::VersionedTransaction},
301        solana_vote_program::{vote_state::TowerSync, vote_transaction},
302    };
303
304    #[test]
305    fn test_vote_transaction_cost() {
306        solana_logger::setup();
307        let node_keypair = Keypair::new();
308        let vote_keypair = Keypair::new();
309        let auth_keypair = Keypair::new();
310        let transaction = vote_transaction::new_tower_sync_transaction(
311            TowerSync::default(),
312            Hash::default(),
313            &node_keypair,
314            &vote_keypair,
315            &auth_keypair,
316            None,
317        );
318
319        // create a sanitized vote transaction
320        let vote_transaction = RuntimeTransaction::try_create(
321            VersionedTransaction::from(transaction.clone()),
322            MessageHash::Compute,
323            Some(true),
324            SimpleAddressLoader::Disabled,
325            &ReservedAccountKeys::empty_key_set(),
326        )
327        .unwrap();
328
329        // create a identical sanitized transaction, but identified as non-vote
330        let none_vote_transaction = RuntimeTransaction::try_create(
331            VersionedTransaction::from(transaction),
332            MessageHash::Compute,
333            Some(false),
334            SimpleAddressLoader::Disabled,
335            &ReservedAccountKeys::empty_key_set(),
336        )
337        .unwrap();
338
339        // expected vote tx cost: 2 write locks, 1 sig, 1 vote ix, 8cu of loaded accounts size,
340        let expected_vote_cost = SIMPLE_VOTE_USAGE_COST;
341        // expected non-vote tx cost would include default loaded accounts size cost (16384) additionally, and 3_000 for instruction
342        let expected_none_vote_cost = 21443;
343
344        let vote_cost = CostModel::calculate_cost(&vote_transaction, &FeatureSet::all_enabled());
345        let none_vote_cost =
346            CostModel::calculate_cost(&none_vote_transaction, &FeatureSet::all_enabled());
347
348        assert_eq!(expected_vote_cost, vote_cost.sum());
349        assert_eq!(expected_none_vote_cost, none_vote_cost.sum());
350    }
351}