solana_cost_model/
transaction_cost.rs

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