miden_multisig_client/client/
account.rs1use std::collections::HashSet;
7
8use base64::Engine;
9use guardian_client::{
10 AuthConfig, ClientError as GuardianClientError, MidenEcdsaAuth, MidenFalconRpoAuth,
11 TryIntoTxSummary, auth_config::AuthType,
12};
13use guardian_shared::SignatureScheme;
14use miden_client::account::Account;
15use miden_client::{Deserializable, Serializable};
16use miden_confidential_contracts::multisig_guardian::{
17 MultisigGuardianBuilder, MultisigGuardianConfig,
18};
19use miden_protocol::Word;
20use miden_protocol::account::AccountId;
21
22use super::{MultisigClient, StateVerificationResult};
23use crate::account::MultisigAccount;
24use crate::error::{MultisigError, Result};
25use crate::keystore::word_from_hex;
26use crate::procedures::ProcedureThreshold;
27use crate::transaction::word_to_hex;
28
29impl MultisigClient {
30 fn ensure_unique_signer_commitments(signer_commitments: &[Word]) -> Result<()> {
31 let mut seen = HashSet::new();
32
33 for commitment in signer_commitments {
34 let commitment_hex = word_to_hex(commitment);
35 if !seen.insert(commitment_hex.clone()) {
36 return Err(MultisigError::InvalidConfig(format!(
37 "duplicate signer commitment: {}",
38 commitment_hex
39 )));
40 }
41 }
42
43 Ok(())
44 }
45
46 pub async fn create_account(
54 &mut self,
55 threshold: u32,
56 signer_commitments: Vec<Word>,
57 ) -> Result<&MultisigAccount> {
58 self.create_account_with_proc_thresholds(threshold, signer_commitments, Vec::new())
59 .await
60 }
61
62 pub async fn create_account_with_proc_thresholds(
86 &mut self,
87 threshold: u32,
88 signer_commitments: Vec<Word>,
89 proc_threshold_overrides: Vec<ProcedureThreshold>,
90 ) -> Result<&MultisigAccount> {
91 Self::ensure_unique_signer_commitments(&signer_commitments)?;
92 let signature_scheme = self.key_manager.scheme();
93
94 let mut guardian_client = self.create_guardian_client().await?;
96 let (guardian_commitment_hex, _raw_pubkey) = guardian_client
97 .get_pubkey(Some(&signature_scheme.to_string()))
98 .await
99 .map_err(|e| {
100 MultisigError::GuardianServer(format!("failed to get GUARDIAN pubkey: {}", e))
101 })?;
102
103 let guardian_commitment =
104 word_from_hex(&guardian_commitment_hex).map_err(MultisigError::HexDecode)?;
105
106 let overrides: Vec<(Word, u32)> = proc_threshold_overrides
108 .iter()
109 .map(|pt| (pt.procedure_root(), pt.threshold))
110 .collect();
111
112 let guardian_config =
114 MultisigGuardianConfig::new(threshold, signer_commitments, guardian_commitment)
115 .with_signature_scheme(signature_scheme)
116 .with_proc_threshold_overrides(overrides);
117
118 let mut seed = [0u8; 32];
120 rand::Rng::fill(&mut rand::rng(), &mut seed);
121
122 let account = MultisigGuardianBuilder::new(guardian_config)
123 .with_seed(seed)
124 .build()
125 .map_err(|e| MultisigError::MidenClient(format!("failed to build account: {}", e)))?;
126
127 self.add_or_update_account(&account, false).await?;
129
130 let multisig_account = MultisigAccount::new(account);
132 self.account = Some(multisig_account);
133
134 Ok(self.account.as_ref().unwrap())
135 }
136
137 pub async fn pull_account(&mut self, account_id: AccountId) -> Result<&MultisigAccount> {
141 let mut guardian_client = self.create_authenticated_guardian_client().await?;
142
143 let state_response = guardian_client
144 .get_state(&account_id)
145 .await
146 .map_err(|e| MultisigError::GuardianServer(format!("failed to get state: {}", e)))?;
147
148 let state_obj = state_response.state.ok_or_else(|| {
149 MultisigError::GuardianServer("no state returned from GUARDIAN".to_string())
150 })?;
151
152 let state_value: serde_json::Value = serde_json::from_str(&state_obj.state_json)?;
153
154 let account_base64 = state_value["data"].as_str().ok_or_else(|| {
155 MultisigError::GuardianServer("missing 'data' field in state".to_string())
156 })?;
157
158 let account_bytes = base64::engine::general_purpose::STANDARD
159 .decode(account_base64)
160 .map_err(|e| MultisigError::MidenClient(format!("failed to decode account: {}", e)))?;
161
162 let account = Account::read_from_bytes(&account_bytes).map_err(|e| {
163 MultisigError::MidenClient(format!("failed to deserialize account: {}", e))
164 })?;
165
166 self.add_or_update_account(&account, true).await?;
167
168 let multisig_account = MultisigAccount::new(account);
169 self.account = Some(multisig_account);
170
171 Ok(self.account.as_ref().unwrap())
172 }
173
174 pub async fn push_account(&mut self) -> Result<()> {
176 let account = self
177 .account
178 .as_ref()
179 .ok_or_else(|| MultisigError::MissingConfig("no account loaded".to_string()))?;
180
181 let mut guardian_client = self.create_authenticated_guardian_client().await?;
182
183 let account_bytes = account.inner().to_bytes();
184 let account_base64 = base64::engine::general_purpose::STANDARD.encode(&account_bytes);
185
186 let initial_state = serde_json::json!({
187 "data": account_base64,
188 "account_id": account.id().to_string(),
189 });
190
191 let cosigner_commitments = account.cosigner_commitments_hex();
192 let auth_config = AuthConfig {
193 auth_type: Some(match self.key_manager.scheme() {
194 SignatureScheme::Falcon => AuthType::MidenFalconRpo(MidenFalconRpoAuth {
195 cosigner_commitments,
196 }),
197 SignatureScheme::Ecdsa => AuthType::MidenEcdsa(MidenEcdsaAuth {
198 cosigner_commitments,
199 }),
200 }),
201 };
202
203 let account_id = account.id();
204
205 guardian_client
207 .configure(&account_id, auth_config, initial_state)
208 .await
209 .map_err(|e| {
210 MultisigError::GuardianServer(format!("failed to configure account: {}", e))
211 })?;
212
213 Ok(())
214 }
215
216 pub async fn sync(&mut self) -> Result<()> {
218 self.sync_network_state().await?;
219
220 let account_updated = self.sync_from_guardian_internal().await?;
221
222 if account_updated {
223 self.sync_network_state().await?;
224 }
225
226 self.refresh_cached_account_from_store().await
227 }
228
229 pub async fn sync_network_only(&mut self) -> Result<()> {
231 self.sync_network_state().await?;
232 self.refresh_cached_account_from_store().await
233 }
234
235 pub async fn sync_from_guardian(&mut self) -> Result<()> {
237 self.sync_from_guardian_internal().await?;
238 Ok(())
239 }
240
241 async fn sync_network_state(&mut self) -> Result<()> {
242 self.miden_client
243 .sync_state()
244 .await
245 .map_err(|e| MultisigError::MidenClient(format!("failed to sync state: {:#?}", e)))?;
246 Ok(())
247 }
248
249 async fn refresh_cached_account_from_store(&mut self) -> Result<()> {
250 if let Some(current) = self.account.take() {
251 let account_id = current.id();
252 let account_record = self
253 .miden_client
254 .get_account(account_id)
255 .await
256 .map_err(|e| {
257 MultisigError::MidenClient(format!("failed to get updated account: {}", e))
258 })?
259 .ok_or_else(|| {
260 MultisigError::MissingConfig("account not found after sync".to_string())
261 })?;
262 let account: Account = account_record;
263 let refreshed = MultisigAccount::new(account);
264 self.account = Some(refreshed);
265 }
266
267 Ok(())
268 }
269
270 pub async fn verify_state_commitment(&self) -> Result<StateVerificationResult> {
272 let account = self.require_account()?;
273 let account_id = account.id();
274 let local_commitment = account.commitment();
275 let on_chain_commitment = self.get_on_chain_account_commitment(account_id).await?;
276
277 if local_commitment != on_chain_commitment {
278 return Err(MultisigError::InvalidConfig(format!(
279 "local account commitment does not match on-chain commitment for account {}: local={}, on_chain={}",
280 account_id,
281 word_to_hex(&local_commitment),
282 word_to_hex(&on_chain_commitment)
283 )));
284 }
285
286 Ok(StateVerificationResult {
287 account_id,
288 local_commitment_hex: word_to_hex(&local_commitment),
289 on_chain_commitment_hex: word_to_hex(&on_chain_commitment),
290 })
291 }
292
293 async fn ensure_safe_to_overwrite_local_state(
294 &self,
295 account_id: AccountId,
296 incoming_commitment: Word,
297 ) -> Result<()> {
298 match self.try_get_on_chain_account_commitment(account_id).await? {
299 None => Ok(()),
300 Some(on_chain_commitment) if on_chain_commitment == incoming_commitment => Ok(()),
301 Some(on_chain_commitment) => Err(MultisigError::InvalidConfig(format!(
302 "refusing to overwrite local state: incoming commitment does not match on-chain commitment for account {}: incoming={}, on_chain={}",
303 account_id,
304 word_to_hex(&incoming_commitment),
305 word_to_hex(&on_chain_commitment)
306 ))),
307 }
308 }
309 async fn sync_from_guardian_internal(&mut self) -> Result<bool> {
311 let account = self.require_account()?;
312 let account_id = account.id();
313 let local_commitment = account.inner().to_commitment();
314 let local_nonce = account.nonce();
315
316 let mut guardian_client = self.create_authenticated_guardian_client().await?;
318 let state_response = guardian_client.get_state(&account_id).await.map_err(|e| {
319 MultisigError::GuardianServer(format!("failed to get state from GUARDIAN: {}", e))
320 })?;
321
322 let state_obj = state_response.state.ok_or_else(|| {
323 MultisigError::GuardianServer("no state returned from GUARDIAN".to_string())
324 })?;
325
326 let guardian_commitment_hex = &state_obj.commitment;
328 let guardian_commitment =
329 word_from_hex(guardian_commitment_hex).map_err(MultisigError::HexDecode)?;
330
331 if local_commitment == guardian_commitment {
333 return Ok(false);
334 }
335
336 let state_value: serde_json::Value = serde_json::from_str(&state_obj.state_json)?;
338
339 let account_base64 = state_value["data"].as_str().ok_or_else(|| {
340 MultisigError::GuardianServer("missing 'data' field in state".to_string())
341 })?;
342
343 let account_bytes = base64::engine::general_purpose::STANDARD
344 .decode(account_base64)
345 .map_err(|e| MultisigError::MidenClient(format!("failed to decode account: {}", e)))?;
346
347 let fresh_account = Account::read_from_bytes(&account_bytes).map_err(|e| {
348 MultisigError::MidenClient(format!("failed to deserialize account: {}", e))
349 })?;
350
351 let guardian_nonce = fresh_account.nonce().as_canonical_u64();
354 if local_nonce >= guardian_nonce {
355 return Ok(false);
357 }
358
359 self.ensure_safe_to_overwrite_local_state(account_id, fresh_account.to_commitment())
360 .await?;
361
362 match self.add_or_update_account(&fresh_account, true).await {
365 Ok(()) => {}
366 Err(e)
367 if e.to_string()
368 .contains("doesn't match the imported account commitment") =>
369 {
370 self.reset_miden_client().await?;
372 self.add_or_update_account(&fresh_account, true).await?;
373 }
374 Err(e) => return Err(e),
375 }
376
377 let multisig_account = MultisigAccount::new(fresh_account);
378 self.account = Some(multisig_account);
379
380 Ok(true)
381 }
382
383 pub async fn get_deltas(&mut self) -> Result<()> {
385 let account = self.require_account()?.clone();
386 let account_id = account.id();
387 let current_nonce = account.nonce();
388 let from_nonce = current_nonce.saturating_add(1);
389
390 let mut guardian_client = self.create_authenticated_guardian_client().await?;
391 let response = match guardian_client
392 .get_delta_since(&account_id, from_nonce)
393 .await
394 {
395 Ok(resp) => resp,
396 Err(GuardianClientError::ServerError(msg)) if msg.contains("not found") => {
397 return Ok(());
399 }
400 Err(e) => {
401 return Err(MultisigError::GuardianServer(format!(
402 "failed to pull deltas from GUARDIAN: {}",
403 e
404 )));
405 }
406 };
407
408 let merged_delta = response.merged_delta.ok_or_else(|| {
409 MultisigError::GuardianServer("no merged_delta in response".to_string())
410 })?;
411
412 let expected_prev_commitment = if merged_delta.prev_commitment.is_empty() {
413 None
414 } else {
415 Some(word_from_hex(&merged_delta.prev_commitment).map_err(MultisigError::HexDecode)?)
416 };
417
418 if let Some(prev_commitment) = expected_prev_commitment
419 && account.commitment() != prev_commitment
420 {
421 return Ok(());
422 }
423
424 let tx_summary = merged_delta.try_into_tx_summary().map_err(|e| {
425 MultisigError::MidenClient(format!("failed to parse delta payload: {}", e))
426 })?;
427
428 let account_delta = tx_summary.account_delta();
429
430 let updated_account: Account = if account_delta.is_full_state() {
431 Account::try_from(account_delta).map_err(|e| {
432 MultisigError::MidenClient(format!(
433 "failed to convert full state delta to account: {}",
434 e
435 ))
436 })?
437 } else {
438 let mut acc: Account = account.into_inner();
439 acc.apply_delta(account_delta).map_err(|e| {
440 MultisigError::MidenClient(format!("failed to apply delta to account: {}", e))
441 })?;
442 acc
443 };
444
445 self.ensure_safe_to_overwrite_local_state(account_id, updated_account.to_commitment())
446 .await?;
447
448 match self.add_or_update_account(&updated_account, true).await {
451 Ok(()) => {
452 let multisig_account = MultisigAccount::new(updated_account);
453 self.account = Some(multisig_account);
454 Ok(())
455 }
456 Err(e)
457 if e.to_string()
458 .contains("doesn't match the imported account commitment") =>
459 {
460 self.reset_miden_client().await?;
463
464 let mut guardian_client = self.create_authenticated_guardian_client().await?;
466 let state_response = guardian_client.get_state(&account_id).await.map_err(|e| {
467 MultisigError::GuardianServer(format!("failed to get state: {}", e))
468 })?;
469
470 let state_obj = state_response.state.ok_or_else(|| {
471 MultisigError::GuardianServer("no state returned from GUARDIAN".to_string())
472 })?;
473
474 let state_value: serde_json::Value = serde_json::from_str(&state_obj.state_json)?;
475
476 let account_base64 = state_value["data"].as_str().ok_or_else(|| {
477 MultisigError::GuardianServer("missing 'data' field in state".to_string())
478 })?;
479
480 let account_bytes = base64::engine::general_purpose::STANDARD
481 .decode(account_base64)
482 .map_err(|e| {
483 MultisigError::MidenClient(format!("failed to decode account: {}", e))
484 })?;
485
486 let fresh_account = Account::read_from_bytes(&account_bytes).map_err(|e| {
487 MultisigError::MidenClient(format!("failed to deserialize account: {}", e))
488 })?;
489
490 self.ensure_safe_to_overwrite_local_state(
491 account_id,
492 fresh_account.to_commitment(),
493 )
494 .await?;
495
496 self.add_or_update_account(&fresh_account, true).await?;
497
498 let multisig_account = MultisigAccount::new(fresh_account);
499 self.account = Some(multisig_account);
500 Ok(())
501 }
502 Err(e) => Err(e),
503 }
504 }
505
506 pub async fn register_on_guardian(&mut self) -> Result<()> {
516 self.push_account().await
517 }
518
519 pub async fn set_guardian_endpoint(
533 &mut self,
534 new_endpoint: &str,
535 register: bool,
536 ) -> Result<()> {
537 self.guardian_endpoint = new_endpoint.to_string();
538
539 if register {
540 self.register_on_guardian().await?;
541 }
542
543 Ok(())
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 fn word(value: u32) -> Word {
552 Word::from([value, 0, 0, 0])
553 }
554
555 #[test]
556 fn ensure_unique_signer_commitments_rejects_duplicates() {
557 let result = MultisigClient::ensure_unique_signer_commitments(&[word(1), word(2), word(1)]);
558 assert!(result.is_err());
559 assert!(
560 result
561 .unwrap_err()
562 .to_string()
563 .contains("duplicate signer commitment")
564 );
565 }
566}