solana_svm/
rollback_accounts.rs

1use {
2    crate::nonce_info::NonceInfo,
3    solana_account::{AccountSharedData, ReadableAccount, WritableAccount},
4    solana_clock::Epoch,
5    solana_pubkey::Pubkey,
6};
7
8/// Captured account state used to rollback account state for nonce and fee
9/// payer accounts after a failed executed transaction.
10#[derive(PartialEq, Eq, Debug, Clone)]
11pub enum RollbackAccounts {
12    FeePayerOnly {
13        fee_payer_account: AccountSharedData,
14    },
15    SameNonceAndFeePayer {
16        nonce: NonceInfo,
17    },
18    SeparateNonceAndFeePayer {
19        nonce: NonceInfo,
20        fee_payer_account: AccountSharedData,
21    },
22}
23
24#[cfg(feature = "dev-context-only-utils")]
25impl Default for RollbackAccounts {
26    fn default() -> Self {
27        Self::FeePayerOnly {
28            fee_payer_account: AccountSharedData::default(),
29        }
30    }
31}
32
33impl RollbackAccounts {
34    pub(crate) fn new(
35        nonce: Option<NonceInfo>,
36        fee_payer_address: Pubkey,
37        mut fee_payer_account: AccountSharedData,
38        fee_payer_rent_debit: u64,
39        fee_payer_loaded_rent_epoch: Epoch,
40    ) -> Self {
41        // When the fee payer account is rolled back due to transaction failure,
42        // rent should not be charged so credit the previously debited rent
43        // amount.
44        fee_payer_account.set_lamports(
45            fee_payer_account
46                .lamports()
47                .saturating_add(fee_payer_rent_debit),
48        );
49
50        if let Some(nonce) = nonce {
51            if &fee_payer_address == nonce.address() {
52                // `nonce` contains an AccountSharedData which has already been advanced to the current DurableNonce
53                // `fee_payer_account` is an AccountSharedData as it currently exists on-chain
54                // thus if the nonce account is being used as the fee payer, we need to update that data here
55                // so we capture both the data change for the nonce and the lamports/rent epoch change for the fee payer
56                fee_payer_account.set_data_from_slice(nonce.account().data());
57
58                RollbackAccounts::SameNonceAndFeePayer {
59                    nonce: NonceInfo::new(fee_payer_address, fee_payer_account),
60                }
61            } else {
62                RollbackAccounts::SeparateNonceAndFeePayer {
63                    nonce,
64                    fee_payer_account,
65                }
66            }
67        } else {
68            // When rolling back failed transactions which don't use nonces, the
69            // runtime should not update the fee payer's rent epoch so reset the
70            // rollback fee payer account's rent epoch to its originally loaded
71            // rent epoch value. In the future, a feature gate could be used to
72            // alter this behavior such that rent epoch updates are handled the
73            // same for both nonce and non-nonce failed transactions.
74            fee_payer_account.set_rent_epoch(fee_payer_loaded_rent_epoch);
75            RollbackAccounts::FeePayerOnly { fee_payer_account }
76        }
77    }
78
79    /// Number of accounts tracked for rollback
80    pub fn count(&self) -> usize {
81        match self {
82            Self::FeePayerOnly { .. } | Self::SameNonceAndFeePayer { .. } => 1,
83            Self::SeparateNonceAndFeePayer { .. } => 2,
84        }
85    }
86
87    /// Size of accounts tracked for rollback, used when calculating the actual
88    /// cost of transaction processing in the cost model.
89    pub fn data_size(&self) -> usize {
90        match self {
91            Self::FeePayerOnly { fee_payer_account } => fee_payer_account.data().len(),
92            Self::SameNonceAndFeePayer { nonce } => nonce.account().data().len(),
93            Self::SeparateNonceAndFeePayer {
94                nonce,
95                fee_payer_account,
96            } => fee_payer_account
97                .data()
98                .len()
99                .saturating_add(nonce.account().data().len()),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use {
107        super::*,
108        solana_account::{ReadableAccount, WritableAccount},
109        solana_hash::Hash,
110        solana_nonce::{
111            state::{Data as NonceData, DurableNonce, State as NonceState},
112            versions::Versions as NonceVersions,
113        },
114        solana_sdk_ids::system_program,
115    };
116
117    #[test]
118    fn test_new_fee_payer_only() {
119        let fee_payer_address = Pubkey::new_unique();
120        let fee_payer_account = AccountSharedData::new(100, 0, &Pubkey::default());
121        let fee_payer_rent_epoch = fee_payer_account.rent_epoch();
122
123        const TEST_RENT_DEBIT: u64 = 1;
124        let rent_collected_fee_payer_account = {
125            let mut account = fee_payer_account.clone();
126            account.set_lamports(fee_payer_account.lamports() - TEST_RENT_DEBIT);
127            account.set_rent_epoch(fee_payer_rent_epoch + 1);
128            account
129        };
130
131        let rollback_accounts = RollbackAccounts::new(
132            None,
133            fee_payer_address,
134            rent_collected_fee_payer_account,
135            TEST_RENT_DEBIT,
136            fee_payer_rent_epoch,
137        );
138
139        let expected_fee_payer_account = fee_payer_account;
140        match rollback_accounts {
141            RollbackAccounts::FeePayerOnly { fee_payer_account } => {
142                assert_eq!(expected_fee_payer_account, fee_payer_account);
143            }
144            _ => panic!("Expected FeePayerOnly variant"),
145        }
146    }
147
148    #[test]
149    fn test_new_same_nonce_and_fee_payer() {
150        let nonce_address = Pubkey::new_unique();
151        let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique());
152        let lamports_per_signature = 42;
153        let nonce_account = AccountSharedData::new_data(
154            43,
155            &NonceVersions::new(NonceState::Initialized(NonceData::new(
156                Pubkey::default(),
157                durable_nonce,
158                lamports_per_signature,
159            ))),
160            &system_program::id(),
161        )
162        .unwrap();
163
164        const TEST_RENT_DEBIT: u64 = 1;
165        let rent_collected_nonce_account = {
166            let mut account = nonce_account.clone();
167            account.set_lamports(nonce_account.lamports() - TEST_RENT_DEBIT);
168            account
169        };
170
171        let nonce = NonceInfo::new(nonce_address, rent_collected_nonce_account.clone());
172        let rollback_accounts = RollbackAccounts::new(
173            Some(nonce),
174            nonce_address,
175            rent_collected_nonce_account,
176            TEST_RENT_DEBIT,
177            u64::MAX, // ignored
178        );
179
180        match rollback_accounts {
181            RollbackAccounts::SameNonceAndFeePayer { nonce } => {
182                assert_eq!(nonce.address(), &nonce_address);
183                assert_eq!(nonce.account(), &nonce_account);
184            }
185            _ => panic!("Expected SameNonceAndFeePayer variant"),
186        }
187    }
188
189    #[test]
190    fn test_separate_nonce_and_fee_payer() {
191        let nonce_address = Pubkey::new_unique();
192        let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique());
193        let lamports_per_signature = 42;
194        let nonce_account = AccountSharedData::new_data(
195            43,
196            &NonceVersions::new(NonceState::Initialized(NonceData::new(
197                Pubkey::default(),
198                durable_nonce,
199                lamports_per_signature,
200            ))),
201            &system_program::id(),
202        )
203        .unwrap();
204
205        let fee_payer_address = Pubkey::new_unique();
206        let fee_payer_account = AccountSharedData::new(44, 0, &Pubkey::default());
207
208        const TEST_RENT_DEBIT: u64 = 1;
209        let rent_collected_fee_payer_account = {
210            let mut account = fee_payer_account.clone();
211            account.set_lamports(fee_payer_account.lamports() - TEST_RENT_DEBIT);
212            account
213        };
214
215        let nonce = NonceInfo::new(nonce_address, nonce_account.clone());
216        let rollback_accounts = RollbackAccounts::new(
217            Some(nonce),
218            fee_payer_address,
219            rent_collected_fee_payer_account.clone(),
220            TEST_RENT_DEBIT,
221            u64::MAX, // ignored
222        );
223
224        let expected_fee_payer_account = fee_payer_account;
225        match rollback_accounts {
226            RollbackAccounts::SeparateNonceAndFeePayer {
227                nonce,
228                fee_payer_account,
229            } => {
230                assert_eq!(nonce.address(), &nonce_address);
231                assert_eq!(nonce.account(), &nonce_account);
232                assert_eq!(expected_fee_payer_account, fee_payer_account);
233            }
234            _ => panic!("Expected SeparateNonceAndFeePayer variant"),
235        }
236    }
237}