miden_protocol/block/
block_account_update.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct BlockAccountUpdate {
21 account_id: AccountId,
23
24 final_state_commitment: Word,
26
27 details: AccountUpdateDetails,
31}
32
33impl BlockAccountUpdate {
34 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 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 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 pub fn account_id(&self) -> AccountId {
76 self.account_id
77 }
78
79 pub fn final_state_commitment(&self) -> Word {
81 self.final_state_commitment
82 }
83
84 pub fn details(&self) -> &AccountUpdateDetails {
88 &self.details
89 }
90
91 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#[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}