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