Skip to main content

miden_protocol/account/patch/
update_details.rs

1use crate::account::{Account, AccountId, AccountPatch};
2use crate::errors::{
3    AccountPatchError,
4    AccountUpdateDetailsValidationError,
5    AccountUpdateSizeValidationError,
6    NewPublicAccountValidationError,
7};
8use crate::utils::serde::{
9    ByteReader,
10    ByteWriter,
11    Deserializable,
12    DeserializationError,
13    Serializable,
14};
15use crate::{ACCOUNT_UPDATE_MAX_SIZE, Word};
16
17// ACCOUNT UPDATE DETAILS
18// ================================================================================================
19
20/// Describes the update of an account at any aggregation level: as the result of a single
21/// transaction, of all transactions in a batch, or of all batches in a block. The representation is
22/// the same in all cases, which lets transaction-, batch-, and block-level updates merge into one
23/// another.
24///
25/// For a public account, the update is represented as an [`AccountPatch`] describing the new
26/// absolute state of the changed components of the account. For a private account, the actual state
27/// change is not publicly visible and only the state commitments are; this is captured by the
28/// [`AccountUpdateDetails::Private`] variant.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum AccountUpdateDetails {
31    /// The state update of a private account is not publicly accessible.
32    Private,
33
34    /// The state update of a public account, represented as an [`AccountPatch`] describing the new
35    /// absolute state of the changed components of the account.
36    Public(AccountPatch),
37}
38
39impl AccountUpdateDetails {
40    const PRIVATE_TAG: u8 = 0;
41    const PUBLIC_TAG: u8 = 1;
42
43    /// Returns `true` if the account update details are for a private account, `false` otherwise.
44    pub fn is_private(&self) -> bool {
45        matches!(self, Self::Private)
46    }
47
48    /// Returns `true` if the account update details are for a public account, `false` otherwise.
49    pub fn is_public(&self) -> bool {
50        matches!(self, Self::Public(_))
51    }
52
53    /// Validates that the serialized size of these details does not exceed the maximum account
54    /// update size.
55    pub(crate) fn validate_size(
56        &self,
57        account_id: AccountId,
58    ) -> Result<(), AccountUpdateSizeValidationError> {
59        let update_size = self.get_size_hint();
60        if update_size > ACCOUNT_UPDATE_MAX_SIZE as usize {
61            return Err(AccountUpdateSizeValidationError { account_id, update_size });
62        }
63
64        Ok(())
65    }
66
67    /// Validates that these details are compatible with `account_id`.
68    ///
69    /// Returns the account patch for a valid public account update and `None` for a valid private
70    /// account update.
71    pub(crate) fn validate_for_account(
72        &self,
73        account_id: AccountId,
74    ) -> Result<Option<&AccountPatch>, AccountUpdateDetailsValidationError> {
75        match (self, account_id.is_private()) {
76            (Self::Private, true) => Ok(None),
77            (Self::Public(_), true) => {
78                Err(AccountUpdateDetailsValidationError::PrivateAccountWithDetails(account_id))
79            },
80            (Self::Private, false) => Err(
81                AccountUpdateDetailsValidationError::PublicStateAccountMissingDetails(account_id),
82            ),
83            (Self::Public(patch), false) if patch.id() != account_id => {
84                Err(AccountUpdateDetailsValidationError::AccountIdMismatch {
85                    account_id,
86                    patch_account_id: patch.id(),
87                })
88            },
89            (Self::Public(patch), false) => Ok(Some(patch)),
90        }
91    }
92
93    /// Merges the `other` update into this one.
94    ///
95    /// This account update (`self`) must come before `other`, i.e. `self.nonce + 1` must be equal
96    /// to `other.nonce`.
97    pub fn merge(self, other: AccountUpdateDetails) -> Result<Self, AccountPatchError> {
98        let merged_update = match (self, other) {
99            (AccountUpdateDetails::Private, AccountUpdateDetails::Private) => {
100                AccountUpdateDetails::Private
101            },
102            (AccountUpdateDetails::Public(mut patch), AccountUpdateDetails::Public(new_patch)) => {
103                patch.merge(new_patch)?;
104                AccountUpdateDetails::Public(patch)
105            },
106            (left, right) => {
107                return Err(AccountPatchError::IncompatibleAccountUpdates {
108                    left_update_type: left.as_tag_str(),
109                    right_update_type: right.as_tag_str(),
110                });
111            },
112        };
113
114        Ok(merged_update)
115    }
116
117    /// Returns the tag of the [`AccountUpdateDetails`] as a string for inclusion in error messages.
118    pub(crate) const fn as_tag_str(&self) -> &'static str {
119        match self {
120            AccountUpdateDetails::Private => "private",
121            AccountUpdateDetails::Public(_) => "public",
122        }
123    }
124}
125
126/// Validates that `patch` contains the complete state of a new public account and reconstructs to
127/// `final_state_commitment`.
128pub(crate) fn validate_new_public_account(
129    patch: &AccountPatch,
130    final_state_commitment: Word,
131) -> Result<(), NewPublicAccountValidationError> {
132    let account = Account::try_from(patch).map_err(|source| {
133        NewPublicAccountValidationError::RequiresFullStatePatch { id: patch.id(), source }
134    })?;
135    let account_commitment = account.to_commitment();
136    if account_commitment != final_state_commitment {
137        return Err(NewPublicAccountValidationError::FinalCommitmentMismatch {
138            final_state_commitment,
139            account_commitment,
140        });
141    }
142
143    Ok(())
144}
145
146// SERIALIZATION
147// ================================================================================================
148
149impl Serializable for AccountUpdateDetails {
150    fn write_into<W: ByteWriter>(&self, target: &mut W) {
151        match self {
152            AccountUpdateDetails::Private => {
153                Self::PRIVATE_TAG.write_into(target);
154            },
155            AccountUpdateDetails::Public(public) => {
156                Self::PUBLIC_TAG.write_into(target);
157                public.write_into(target);
158            },
159        }
160    }
161
162    fn get_size_hint(&self) -> usize {
163        // Size of the serialized enum tag.
164        let u8_size = 0u8.get_size_hint();
165
166        match self {
167            AccountUpdateDetails::Private => u8_size,
168            AccountUpdateDetails::Public(public) => u8_size + public.get_size_hint(),
169        }
170    }
171}
172
173impl Deserializable for AccountUpdateDetails {
174    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
175        match u8::read_from(source)? {
176            Self::PRIVATE_TAG => Ok(Self::Private),
177            Self::PUBLIC_TAG => Ok(Self::Public(AccountPatch::read_from(source)?)),
178            variant => Err(DeserializationError::InvalidValue(format!(
179                "Unknown variant {variant} for AccountUpdateDetails"
180            ))),
181        }
182    }
183}
184
185// TESTS
186// ================================================================================================
187
188#[cfg(test)]
189mod tests {
190    use super::AccountUpdateDetails;
191    use crate::account::{
192        AccountCode,
193        AccountId,
194        AccountPatch,
195        AccountStoragePatch,
196        AccountVaultPatch,
197        StorageMapKey,
198        StorageSlotName,
199    };
200    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
201    use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
202    use crate::utils::serde::Serializable;
203    use crate::{ONE, Word};
204
205    #[test]
206    fn account_update_details_size_hint() -> anyhow::Result<()> {
207        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
208
209        // A full state patch may only create slots, so build it with create ops.
210        let storage_patch = AccountStoragePatch::builder()
211            .create_value(StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32]))
212            .create_value(StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32]))
213            .create_map(
214                StorageSlotName::mock(4),
215                [(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
216            )
217            .build();
218
219        let non_fungible: Asset = NonFungibleAsset::mock(&[6]);
220        let fungible: Asset = FungibleAsset::mock(42);
221        let vault_patch = AccountVaultPatch::with_assets([non_fungible, fungible]);
222
223        let account_patch = AccountPatch::new(
224            account_id,
225            storage_patch,
226            vault_patch,
227            Some(AccountCode::mock()),
228            Some(ONE),
229        )?;
230
231        let update_details_private = AccountUpdateDetails::Private;
232        assert_eq!(update_details_private.to_bytes().len(), update_details_private.get_size_hint());
233
234        let update_details_patch = AccountUpdateDetails::Public(account_patch);
235        assert_eq!(update_details_patch.to_bytes().len(), update_details_patch.get_size_hint());
236
237        // Verify the `Public` flavor differs from `Private` in size, just to confirm both branches
238        // are exercised.
239        assert!(update_details_patch.get_size_hint() > update_details_private.get_size_hint());
240
241        Ok(())
242    }
243}