Skip to main content

miden_protocol/block/
block_account_update.rs

1use alloc::string::ToString;
2
3use crate::Word;
4use crate::account::{AccountId, AccountUpdateDetails, validate_new_public_account};
5use crate::errors::BlockAccountUpdateError;
6use crate::utils::serde::{
7    ByteReader,
8    ByteWriter,
9    Deserializable,
10    DeserializationError,
11    Serializable,
12};
13
14// BLOCK ACCOUNT UPDATE
15// ================================================================================================
16
17/// Describes the changes made to an account state resulting from executing transactions contained
18/// in a block.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct BlockAccountUpdate {
21    /// ID of the updated account.
22    account_id: AccountId,
23
24    /// Final commitment to the new state of the account after this update.
25    final_state_commitment: Word,
26
27    /// A set of changes which can be applied to the previous account state (i.e., the state as of
28    /// the last block) to get the new account state. For private accounts, this is set to
29    /// [AccountUpdateDetails::Private].
30    details: AccountUpdateDetails,
31}
32
33impl BlockAccountUpdate {
34    /// Returns a new validated [`BlockAccountUpdate`].
35    pub fn new(
36        account_id: AccountId,
37        final_state_commitment: Word,
38        details: AccountUpdateDetails,
39    ) -> Result<Self, BlockAccountUpdateError> {
40        let update = Self::new_unchecked(account_id, final_state_commitment, details);
41        update.validate()?;
42        Ok(update)
43    }
44
45    /// Returns a new [`BlockAccountUpdate`] without validating its invariants.
46    ///
47    /// Callers must ensure that the update details are compatible with the account ID and that a
48    /// full-state public account update matches the final state commitment.
49    pub(crate) const fn new_unchecked(
50        account_id: AccountId,
51        final_state_commitment: Word,
52        details: AccountUpdateDetails,
53    ) -> Self {
54        Self {
55            account_id,
56            final_state_commitment,
57            details,
58        }
59    }
60
61    /// Validates that this account update's details are compatible with its account ID.
62    pub(crate) fn validate(&self) -> Result<(), BlockAccountUpdateError> {
63        let Some(patch) = self.details.validate_for_account(self.account_id)? else {
64            return Ok(());
65        };
66
67        if patch.is_full_state() {
68            validate_new_public_account(patch, self.final_state_commitment)?;
69        }
70
71        Ok(())
72    }
73
74    /// Returns the ID of the updated account.
75    pub fn account_id(&self) -> AccountId {
76        self.account_id
77    }
78
79    /// Returns the state commitment of the account after this update.
80    pub fn final_state_commitment(&self) -> Word {
81        self.final_state_commitment
82    }
83
84    /// Returns the account update details for this account update.
85    ///
86    /// These details can be used to build the new account state from the previous account state.
87    pub fn details(&self) -> &AccountUpdateDetails {
88        &self.details
89    }
90
91    /// Returns `true` if the account update details are for private account.
92    pub fn is_private(&self) -> bool {
93        self.details.is_private()
94    }
95}
96
97impl Serializable for BlockAccountUpdate {
98    fn write_into<W: ByteWriter>(&self, target: &mut W) {
99        self.account_id.write_into(target);
100        self.final_state_commitment.write_into(target);
101        self.details.write_into(target);
102    }
103}
104
105impl Deserializable for BlockAccountUpdate {
106    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
107        Self::new(
108            AccountId::read_from(source)?,
109            Word::read_from(source)?,
110            AccountUpdateDetails::read_from(source)?,
111        )
112        .map_err(|error| DeserializationError::InvalidValue(error.to_string()))
113    }
114}
115
116// TESTS
117// ================================================================================================
118
119#[cfg(test)]
120mod tests {
121    use assert_matches::assert_matches;
122
123    use super::BlockAccountUpdate;
124    use crate::Word;
125    use crate::account::{Account, AccountPatch, AccountType, AccountUpdateDetails};
126    use crate::errors::BlockAccountUpdateError;
127    use crate::testing::add_component::AddComponent;
128    use crate::testing::noop_auth_component::NoopAuthComponent;
129    use crate::utils::serde::{Deserializable, DeserializationError, Serializable};
130
131    fn public_account_and_full_patch() -> (Account, AccountPatch) {
132        let account = Account::builder([9; 32])
133            .account_type(AccountType::Public)
134            .with_component(NoopAuthComponent)
135            .with_component(AddComponent)
136            .build_existing()
137            .unwrap();
138        let patch = AccountPatch::try_from(account.clone()).unwrap();
139        assert!(patch.is_full_state());
140
141        (account, patch)
142    }
143
144    #[test]
145    fn accepts_full_state_patch_matching_final_commitment() {
146        let (account, patch) = public_account_and_full_patch();
147
148        BlockAccountUpdate::new(
149            account.id(),
150            account.to_commitment(),
151            AccountUpdateDetails::Public(patch),
152        )
153        .unwrap();
154    }
155
156    #[test]
157    fn rejects_full_state_patch_not_matching_final_commitment() {
158        let (account, patch) = public_account_and_full_patch();
159        let final_state_commitment = Word::empty();
160        let account_commitment = account.to_commitment();
161        assert_ne!(final_state_commitment, account_commitment);
162
163        let error = BlockAccountUpdate::new(
164            account.id(),
165            final_state_commitment,
166            AccountUpdateDetails::Public(patch),
167        )
168        .unwrap_err();
169
170        assert_matches!(
171            error,
172            BlockAccountUpdateError::AccountFinalCommitmentMismatch {
173                final_state_commitment: actual_final_state_commitment,
174                account_commitment: actual_account_commitment,
175            } if actual_final_state_commitment == final_state_commitment
176                && actual_account_commitment == account_commitment
177        );
178    }
179
180    #[test]
181    fn deserialization_rejects_full_state_patch_not_matching_final_commitment() {
182        let (account, patch) = public_account_and_full_patch();
183        let final_state_commitment = Word::empty();
184        let account_commitment = account.to_commitment();
185        assert_ne!(final_state_commitment, account_commitment);
186        let invalid_update = BlockAccountUpdate::new_unchecked(
187            account.id(),
188            final_state_commitment,
189            AccountUpdateDetails::Public(patch),
190        );
191
192        let error = BlockAccountUpdate::read_from_bytes(&invalid_update.to_bytes()).unwrap_err();
193
194        assert_matches!(
195            error,
196            DeserializationError::InvalidValue(message)
197                if message == format!(
198                    "block account update's final commitment {final_state_commitment} and reconstructed account commitment {account_commitment} must match"
199                )
200        );
201    }
202
203    #[test]
204    fn accepts_partial_public_account_patch() {
205        let (account, _) = public_account_and_full_patch();
206
207        BlockAccountUpdate::new(
208            account.id(),
209            Word::empty(),
210            AccountUpdateDetails::Public(AccountPatch::empty(account.id())),
211        )
212        .unwrap();
213    }
214}