Skip to main content

solana_nonce/
versions.rs

1//! State for durable transaction nonces.
2
3#[cfg(feature = "wincode")]
4use wincode::{SchemaRead, SchemaWrite};
5use {
6    crate::state::{Data, DurableNonce, State},
7    solana_hash::Hash,
8    solana_pubkey::Pubkey,
9    std::collections::HashSet,
10};
11
12#[cfg_attr(
13    feature = "serde",
14    derive(serde_derive::Deserialize, serde_derive::Serialize)
15)]
16#[cfg_attr(feature = "wincode", derive(SchemaWrite, SchemaRead))]
17#[derive(Debug, PartialEq, Eq, Clone)]
18pub enum Versions {
19    Legacy(Box<State>),
20    /// Current variants have durable nonce and blockhash domains separated.
21    Current(Box<State>),
22}
23
24#[derive(Debug, Eq, PartialEq)]
25pub enum AuthorizeNonceError {
26    MissingRequiredSignature(/*account authority:*/ Pubkey),
27    Uninitialized,
28}
29
30impl Versions {
31    pub fn new(state: State) -> Self {
32        Self::Current(Box::new(state))
33    }
34
35    pub fn state(&self) -> &State {
36        match self {
37            Self::Legacy(state) => state,
38            Self::Current(state) => state,
39        }
40    }
41
42    /// Checks if the recent_blockhash field in Transaction verifies, and
43    /// returns nonce account data if so.
44    pub fn verify_recent_blockhash(
45        &self,
46        recent_blockhash: &Hash, // Transaction.message.recent_blockhash
47    ) -> Option<&Data> {
48        match self {
49            // Legacy durable nonces are invalid and should not
50            // allow durable transactions.
51            Self::Legacy(_) => None,
52            Self::Current(state) => match **state {
53                State::Uninitialized => None,
54                State::Initialized(ref data) => {
55                    (recent_blockhash == &data.blockhash()).then_some(data)
56                }
57            },
58        }
59    }
60
61    /// Upgrades legacy nonces out of chain blockhash domains.
62    pub fn upgrade(self) -> Option<Self> {
63        match self {
64            Self::Legacy(mut state) => {
65                match *state {
66                    // An Uninitialized legacy nonce cannot verify a durable
67                    // transaction. The nonce will be upgraded to Current
68                    // version when initialized. Therefore there is no need to
69                    // upgrade Uninitialized legacy nonces.
70                    State::Uninitialized => None,
71                    State::Initialized(ref mut data) => {
72                        data.durable_nonce = DurableNonce::from_blockhash(&data.blockhash());
73                        Some(Self::Current(state))
74                    }
75                }
76            }
77            Self::Current(_) => None,
78        }
79    }
80
81    /// Updates the authority pubkey on the nonce account.
82    pub fn authorize(
83        self,
84        signers: &HashSet<Pubkey>,
85        authority: Pubkey,
86    ) -> Result<Self, AuthorizeNonceError> {
87        let data = match self.state() {
88            State::Uninitialized => return Err(AuthorizeNonceError::Uninitialized),
89            State::Initialized(data) => data,
90        };
91        if !signers.contains(&data.authority) {
92            return Err(AuthorizeNonceError::MissingRequiredSignature(
93                data.authority,
94            ));
95        }
96        let data = Data::new(
97            authority,
98            data.durable_nonce,
99            data.get_lamports_per_signature(),
100        );
101        let state = Box::new(State::Initialized(data));
102        // Preserve Version variant since cannot
103        // change durable_nonce field here.
104        Ok(match self {
105            Self::Legacy(_) => Self::Legacy,
106            Self::Current(_) => Self::Current,
107        }(state))
108    }
109}
110
111impl From<Versions> for State {
112    fn from(versions: Versions) -> Self {
113        match versions {
114            Versions::Legacy(state) => *state,
115            Versions::Current(state) => *state,
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use {
123        super::*, solana_fee_calculator::FeeCalculator, solana_pubkey::Pubkey,
124        std::iter::repeat_with,
125    };
126
127    #[test]
128    fn test_verify_recent_blockhash() {
129        let blockhash = Hash::from([171; 32]);
130        let versions = Versions::Legacy(Box::new(State::Uninitialized));
131        assert_eq!(versions.verify_recent_blockhash(&blockhash), None);
132        assert_eq!(versions.verify_recent_blockhash(&Hash::default()), None);
133        let versions = Versions::Current(Box::new(State::Uninitialized));
134        assert_eq!(versions.verify_recent_blockhash(&blockhash), None);
135        assert_eq!(versions.verify_recent_blockhash(&Hash::default()), None);
136        let durable_nonce = DurableNonce::from_blockhash(&blockhash);
137        let data = Data {
138            authority: Pubkey::new_unique(),
139            durable_nonce,
140            fee_calculator: FeeCalculator {
141                lamports_per_signature: 2718,
142            },
143        };
144        let versions = Versions::Legacy(Box::new(State::Initialized(data.clone())));
145        assert_eq!(versions.verify_recent_blockhash(&Hash::default()), None);
146        assert_eq!(versions.verify_recent_blockhash(&blockhash), None);
147        assert_eq!(versions.verify_recent_blockhash(&data.blockhash()), None);
148        assert_eq!(
149            versions.verify_recent_blockhash(durable_nonce.as_hash()),
150            None
151        );
152        let durable_nonce = DurableNonce::from_blockhash(durable_nonce.as_hash());
153        assert_ne!(data.durable_nonce, durable_nonce);
154        let data = Data {
155            durable_nonce,
156            ..data
157        };
158        let versions = Versions::Current(Box::new(State::Initialized(data.clone())));
159        assert_eq!(versions.verify_recent_blockhash(&blockhash), None);
160        assert_eq!(versions.verify_recent_blockhash(&Hash::default()), None);
161        assert_eq!(
162            versions.verify_recent_blockhash(&data.blockhash()),
163            Some(&data)
164        );
165        assert_eq!(
166            versions.verify_recent_blockhash(durable_nonce.as_hash()),
167            Some(&data)
168        );
169    }
170
171    #[test]
172    fn test_nonce_versions_upgrade() {
173        // Uninitialized
174        let versions = Versions::Legacy(Box::new(State::Uninitialized));
175        assert_eq!(versions.upgrade(), None);
176        // Initialized
177        let blockhash = Hash::from([171; 32]);
178        let durable_nonce = DurableNonce::from_blockhash(&blockhash);
179        let data = Data {
180            authority: Pubkey::new_unique(),
181            durable_nonce,
182            fee_calculator: FeeCalculator {
183                lamports_per_signature: 2718,
184            },
185        };
186        let versions = Versions::Legacy(Box::new(State::Initialized(data.clone())));
187        let durable_nonce = DurableNonce::from_blockhash(durable_nonce.as_hash());
188        assert_ne!(data.durable_nonce, durable_nonce);
189        let data = Data {
190            durable_nonce,
191            ..data
192        };
193        let versions = versions.upgrade().unwrap();
194        assert_eq!(
195            versions,
196            Versions::Current(Box::new(State::Initialized(data)))
197        );
198        assert_eq!(versions.upgrade(), None);
199    }
200
201    #[test]
202    fn test_nonce_versions_authorize() {
203        // Uninitialized
204        let mut signers = repeat_with(Pubkey::new_unique).take(16).collect();
205        let versions = Versions::Legacy(Box::new(State::Uninitialized));
206        assert_eq!(
207            versions.authorize(&signers, Pubkey::new_unique()),
208            Err(AuthorizeNonceError::Uninitialized)
209        );
210        let versions = Versions::Current(Box::new(State::Uninitialized));
211        assert_eq!(
212            versions.authorize(&signers, Pubkey::new_unique()),
213            Err(AuthorizeNonceError::Uninitialized)
214        );
215        // Initialized, Legacy
216        let blockhash = Hash::from([171; 32]);
217        let durable_nonce = DurableNonce::from_blockhash(&blockhash);
218        let data = Data {
219            authority: Pubkey::new_unique(),
220            durable_nonce,
221            fee_calculator: FeeCalculator {
222                lamports_per_signature: 2718,
223            },
224        };
225        let account_authority = data.authority;
226        let versions = Versions::Legacy(Box::new(State::Initialized(data.clone())));
227        let authority = Pubkey::new_unique();
228        assert_ne!(authority, account_authority);
229        let data = Data { authority, ..data };
230        assert_eq!(
231            versions.clone().authorize(&signers, authority),
232            Err(AuthorizeNonceError::MissingRequiredSignature(
233                account_authority
234            )),
235        );
236        assert!(signers.insert(account_authority));
237        assert_eq!(
238            versions.authorize(&signers, authority),
239            Ok(Versions::Legacy(Box::new(State::Initialized(data.clone()))))
240        );
241        // Initialized, Current
242        let account_authority = data.authority;
243        let versions = Versions::Current(Box::new(State::Initialized(data.clone())));
244        let authority = Pubkey::new_unique();
245        assert_ne!(authority, account_authority);
246        let data = Data { authority, ..data };
247        assert_eq!(
248            versions.clone().authorize(&signers, authority),
249            Err(AuthorizeNonceError::MissingRequiredSignature(
250                account_authority
251            )),
252        );
253        assert!(signers.insert(account_authority));
254        assert_eq!(
255            versions.authorize(&signers, authority),
256            Ok(Versions::Current(Box::new(State::Initialized(data))))
257        );
258    }
259}