miden_multisig_client/client/
offline.rs1use std::collections::HashSet;
7
8use guardian_shared::ToJson;
9
10use super::MultisigClient;
11use crate::error::{MultisigError, Result};
12use crate::execution::{SignatureInput, build_final_transaction_request, collect_signature_advice};
13use crate::export::{EXPORT_VERSION, ExportedMetadata, ExportedProposal, ExportedSignature};
14use crate::guardian_endpoint::verify_endpoint_commitment;
15use crate::keystore::proposal_public_key_hex;
16use crate::proposal::TransactionType;
17
18impl MultisigClient {
19 pub async fn create_proposal_offline(
43 &mut self,
44 transaction_type: TransactionType,
45 ) -> Result<ExportedProposal> {
46 self.sync_network_only().await?;
47
48 let account = self.require_account()?.clone();
49 let account_id = account.id();
50 let signatures_required =
51 account.effective_threshold_for_transaction(&transaction_type)? as usize;
52
53 let salt = crate::transaction::generate_salt();
54 let (new_endpoint, new_commitment) = match &transaction_type {
55 TransactionType::SwitchGuardian {
56 new_endpoint,
57 new_commitment,
58 } => {
59 verify_endpoint_commitment(new_endpoint, *new_commitment).await?;
60 (new_endpoint.clone(), *new_commitment)
61 }
62 _ => {
63 return Err(MultisigError::OfflineUnsupportedTransaction(
64 transaction_type.type_name().to_string(),
65 ));
66 }
67 };
68
69 let tx_request = crate::transaction::build_update_guardian_transaction_request(
70 new_commitment,
71 salt,
72 std::iter::empty(),
73 )?;
74 let metadata = ExportedMetadata {
75 proposal_type: "switch_guardian".to_string(),
76 salt_hex: Some(crate::transaction::word_to_hex(&salt)),
77 new_guardian_pubkey_hex: Some(crate::transaction::word_to_hex(&new_commitment)),
78 new_guardian_endpoint: Some(new_endpoint),
79 ..Default::default()
80 };
81
82 let tx_summary =
83 crate::transaction::execute_for_summary(&mut self.miden_client, account_id, tx_request)
84 .await?;
85
86 let tx_commitment = tx_summary.to_commitment();
87 let signature_hex = self.key_manager.sign_word_hex(tx_commitment);
88
89 let id = crate::transaction::word_to_hex(&tx_commitment);
90
91 let exported = ExportedProposal {
92 version: EXPORT_VERSION,
93 account_id: account_id.to_string(),
94 id,
95 nonce: account.nonce() + 1,
96 tx_summary: tx_summary.to_json(),
97 signatures: vec![ExportedSignature {
98 signer_commitment: self.key_manager.commitment_hex(),
99 signature: signature_hex,
100 scheme: self.key_manager.scheme(),
101 public_key_hex: proposal_public_key_hex(self.key_manager.as_ref()),
102 }],
103 signatures_required,
104 metadata,
105 };
106
107 Ok(exported)
108 }
109
110 pub async fn sign_imported_proposal(&mut self, proposal: &mut ExportedProposal) -> Result<()> {
126 let bound_proposal = proposal.to_proposal()?;
127 if !bound_proposal.transaction_type.supports_offline_execution() {
128 return Err(MultisigError::OfflineUnsupportedTransaction(
129 bound_proposal.transaction_type.type_name().to_string(),
130 ));
131 }
132 self.verify_proposal_summary_binding(&bound_proposal)
133 .await?;
134 let account = self.require_account()?;
135 let account_id = account.id();
136 proposal.validate(Some(account_id))?;
137
138 let user_commitment = self.key_manager.commitment();
140 if !account.is_cosigner(&user_commitment) {
141 return Err(MultisigError::NotCosigner);
142 }
143
144 Self::ensure_proposal_account_id(&proposal.account_id, &account_id)?;
145
146 let user_commitment_hex = self.key_manager.commitment_hex();
148 if proposal.signatures.iter().any(|s| {
149 s.signer_commitment
150 .eq_ignore_ascii_case(&user_commitment_hex)
151 }) {
152 return Err(MultisigError::AlreadySigned);
153 }
154 let tx_commitment = bound_proposal.tx_summary.to_commitment();
156 let signature_hex = self.key_manager.sign_word_hex(tx_commitment);
157
158 proposal.add_signature(ExportedSignature {
160 signer_commitment: user_commitment_hex,
161 signature: signature_hex,
162 scheme: self.key_manager.scheme(),
163 public_key_hex: proposal_public_key_hex(self.key_manager.as_ref()),
164 })?;
165
166 Ok(())
167 }
168
169 pub async fn execute_imported_proposal(&mut self, exported: &ExportedProposal) -> Result<()> {
183 self.sync_network_only().await?;
184 let account = self.require_account()?.clone();
185 let account_id = account.id();
186 exported.validate(Some(account_id))?;
187
188 if !exported.is_ready() {
190 return Err(MultisigError::ProposalNotReady {
191 collected: exported.signatures_collected(),
192 required: exported.signatures_required,
193 });
194 }
195
196 let proposal = exported.to_proposal()?;
198 if !proposal.transaction_type.supports_offline_execution() {
199 return Err(MultisigError::OfflineUnsupportedTransaction(
200 proposal.transaction_type.type_name().to_string(),
201 ));
202 }
203 self.verify_proposal_summary_binding(&proposal).await?;
204 let tx_summary = proposal.tx_summary.clone();
205 let tx_summary_commitment = tx_summary.to_commitment();
206
207 let signature_inputs: Vec<SignatureInput> = exported
209 .signatures
210 .iter()
211 .map(|sig| SignatureInput {
212 signer_commitment: sig.signer_commitment.clone(),
213 signature_hex: sig.signature.clone(),
214 scheme: sig.scheme,
215 public_key_hex: sig.public_key_hex.clone(),
216 })
217 .collect();
218
219 let required_commitments: HashSet<String> =
221 account.cosigner_commitments_hex().into_iter().collect();
222 let signature_advice = collect_signature_advice(
223 signature_inputs,
224 &required_commitments,
225 tx_summary_commitment,
226 )?;
227
228 let salt = proposal.metadata.salt()?;
230
231 let final_tx_request = build_final_transaction_request(
232 &self.miden_client,
233 &proposal.transaction_type,
234 account.inner(),
235 salt,
236 signature_advice,
237 None,
238 None,
239 self.key_manager.scheme(),
240 )
241 .await?;
242
243 self.finalize_transaction(account_id, final_tx_request, &proposal.transaction_type)
245 .await
246 }
247}