Skip to main content

canton_ledger/
interactive.rs

1//! Interactive submission: prepare, sign elsewhere, execute.
2//!
3//! An ordinary submission asks the participant to interpret *and* authorize a
4//! command, which means the participant holds the party's key. Interactive
5//! submission splits that: the participant interprets and returns the
6//! transaction with its hash, the key signs the hash wherever it lives, and the
7//! signature comes back to be executed.
8//!
9//! The three types here are the three states of that flow, and each one only
10//! offers what is possible in it:
11//!
12//! * [`Prepare`] — what to interpret. Goes to
13//!   [`prepare_submission`](crate::CantonClient::prepare_submission).
14//! * [`Prepared`] — the interpreted transaction and its hash. Sign it.
15//! * [`Executable`] — signed, and therefore submittable.
16//!
17//! # The window
18//!
19//! A prepared transaction does not stay executable indefinitely. If it depends
20//! on time, it must be executed within the ledger-time tolerance of the ledger
21//! time assigned during preparation — usually about a minute. If it does not,
22//! the bound is the much larger preparation-time skew, in the order of hours.
23//! Holding a `Prepared` across a long signing ceremony is a design decision to
24//! make deliberately, not an accident to discover in production.
25
26use canton_core::{Error, Result};
27use canton_proto::com::daml::ledger::api::v2 as pb;
28// The interactive submission service has a package of its own; `pb` stays
29// the root, where `Command`, `Signature` and the transaction types live.
30use canton_proto::com::daml::ledger::api::v2::interactive as ipb;
31use canton_signer::Signer;
32
33/// A submission to be interpreted but not authorized: the *prepare* half of
34/// interactive submission.
35///
36/// This carries what `PrepareSubmissionRequest` carries, and nothing else.
37/// [`Submit`](crate::Submit) is deliberately not reused: it has a
38/// `deduplication` period and a `workflow_id` that preparation has no field
39/// for, and accepting them here would silently drop them. The de-duplication
40/// period, the transaction shape and the submission id belong to *execution*
41/// and are set on [`Executable`]. In the other direction, preparation has four
42/// fields a plain submission does not: `verbose_hashing`, `max_record_time`,
43/// the hashing scheme, and traffic-cost estimation.
44#[derive(Clone, Debug)]
45pub struct Prepare {
46    act_as: Vec<String>,
47    commands: Vec<pb::Command>,
48    command_id: Option<String>,
49    user_id: Option<String>,
50    read_as: Vec<String>,
51    synchronizer_id: Option<String>,
52    disclosed_contracts: Vec<pb::DisclosedContract>,
53    package_id_selection_preference: Vec<String>,
54    prefetch_contract_keys: Vec<pb::PrefetchContractKey>,
55    min_ledger_time_abs: Option<prost_types::Timestamp>,
56    min_ledger_time_rel: Option<std::time::Duration>,
57    verbose_hashing: bool,
58    max_record_time: Option<prost_types::Timestamp>,
59    hashing_scheme_version: Option<i32>,
60    taps_max_passes: Option<u32>,
61    estimate_traffic_cost: bool,
62}
63
64impl Prepare {
65    /// Prepare a submission acting as a single party.
66    pub fn new(act_as: impl Into<String>) -> Self {
67        Self::new_multi(vec![act_as.into()])
68    }
69
70    /// Prepare a submission acting as several parties.
71    ///
72    /// Each of them signs: the participant requires a signature per party in
73    /// `act_as`, so a transaction prepared for two parties needs two.
74    #[must_use]
75    pub fn new_multi(act_as: Vec<String>) -> Self {
76        Self {
77            act_as,
78            commands: Vec::new(),
79            command_id: None,
80            user_id: None,
81            read_as: Vec::new(),
82            synchronizer_id: None,
83            disclosed_contracts: Vec::new(),
84            package_id_selection_preference: Vec::new(),
85            prefetch_contract_keys: Vec::new(),
86            min_ledger_time_abs: None,
87            min_ledger_time_rel: None,
88            verbose_hashing: false,
89            max_record_time: None,
90            // Asked for explicitly rather than left to the participant: the
91            // scheme decides what the signature covers, and `Prepared` refuses
92            // a response hashed under one the caller did not ask for.
93            hashing_scheme_version: Some(ipb::HashingSchemeVersion::V2 as i32),
94            taps_max_passes: None,
95            // The participant estimates by default; only an explicit opt-out
96            // is sent, so leaving this alone changes nothing.
97            estimate_traffic_cost: true,
98        }
99    }
100
101    /// Add a command — build them with
102    /// [`create`](crate::create) / [`exercise`](crate::exercise), or from
103    /// generated bindings.
104    #[must_use]
105    pub fn add_command(mut self, command: pb::Command) -> Self {
106        self.commands.push(command);
107        self
108    }
109
110    /// Add several commands, executed atomically.
111    #[must_use]
112    pub fn add_commands(mut self, commands: impl IntoIterator<Item = pb::Command>) -> Self {
113        self.commands.extend(commands);
114        self
115    }
116
117    /// Set the command id, one component of the change ID the Ledger API
118    /// de-duplicates on (`user_id`, `act_as`, `command_id`).
119    ///
120    /// Left unset, a fresh UUID is generated at prepare time and *carried
121    /// through execution*, so re-executing the same [`Executable`] is
122    /// de-duplicated rather than committed twice.
123    #[must_use]
124    pub fn with_command_id(mut self, command_id: impl Into<String>) -> Self {
125        self.command_id = Some(command_id.into());
126        self
127    }
128
129    /// Set the user id. Defaults to the one derived from the bearer token.
130    #[must_use]
131    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
132        self.user_id = Some(user_id.into());
133        self
134    }
135
136    /// Parties to read as, in addition to those acting.
137    #[must_use]
138    pub fn with_read_as(mut self, read_as: Vec<String>) -> Self {
139        self.read_as = read_as;
140        self
141    }
142
143    /// Pin the submission to a synchronizer.
144    #[must_use]
145    pub fn with_synchronizer_id(mut self, synchronizer_id: impl Into<String>) -> Self {
146        self.synchronizer_id = Some(synchronizer_id.into());
147        self
148    }
149
150    /// Attach explicitly disclosed contracts — the `created_event_blob` path
151    /// for contracts the submitting party cannot see on its own.
152    #[must_use]
153    pub fn with_disclosed_contracts(mut self, contracts: Vec<pb::DisclosedContract>) -> Self {
154        self.disclosed_contracts = contracts;
155        self
156    }
157
158    /// Prefer these package ids when several satisfy a package-name reference.
159    #[must_use]
160    pub fn with_package_id_selection_preference(mut self, package_ids: Vec<String>) -> Self {
161        self.package_id_selection_preference = package_ids;
162        self
163    }
164
165    /// Fetch these contract keys into the participant's cache before
166    /// interpretation.
167    #[must_use]
168    pub fn with_prefetch_contract_keys(mut self, keys: Vec<pb::PrefetchContractKey>) -> Self {
169        self.prefetch_contract_keys = keys;
170        self
171    }
172
173    /// Lower bound for the transaction's ledger time, absolute.
174    ///
175    /// Mutually exclusive with [`with_min_ledger_time_rel`](Self::with_min_ledger_time_rel);
176    /// the participant rejects both at once.
177    #[must_use]
178    pub fn with_min_ledger_time_abs(mut self, time: prost_types::Timestamp) -> Self {
179        self.min_ledger_time_abs = Some(time);
180        // Clearing the other one is what makes "the later call wins" true. The
181        // wire has a `oneof`, so keeping both would mean silently dropping
182        // whichever this code happened to prefer.
183        self.min_ledger_time_rel = None;
184        self
185    }
186
187    /// Lower bound for the transaction's ledger time, relative to when the
188    /// participant receives the request.
189    ///
190    /// Mutually exclusive with [`with_min_ledger_time_abs`](Self::with_min_ledger_time_abs).
191    #[must_use]
192    pub fn with_min_ledger_time_rel(mut self, duration: std::time::Duration) -> Self {
193        self.min_ledger_time_rel = Some(duration);
194        self.min_ledger_time_abs = None;
195        self
196    }
197
198    /// Ask the participant to return the hashing details it used.
199    ///
200    /// For debugging a hash mismatch. The participant may refuse — it is
201    /// configurable server-side — and what comes back describes internal
202    /// structure, so it is off by default.
203    #[must_use]
204    pub fn with_verbose_hashing(mut self, verbose: bool) -> Self {
205        self.verbose_hashing = verbose;
206        self
207    }
208
209    /// Bound how long the prepared transaction can be executed for.
210    ///
211    /// Past this record time the participant will certainly refuse it, which is
212    /// what makes preparing a fresh transaction for the same intent safe again:
213    /// without a bound, "it might still commit" and "prepare another" are both
214    /// true at once.
215    #[must_use]
216    pub fn with_max_record_time(mut self, time: prost_types::Timestamp) -> Self {
217        self.max_record_time = Some(time);
218        self
219    }
220
221    /// Choose the hashing scheme (defaults to the participant's, `V2`).
222    ///
223    /// The scheme decides what the hash covers, so it is echoed in the response
224    /// and sent back with the signature — the participant must verify under the
225    /// same scheme it hashed with.
226    #[must_use]
227    pub fn with_hashing_scheme_version(mut self, version: ipb::HashingSchemeVersion) -> Self {
228        self.hashing_scheme_version = Some(version as i32);
229        self
230    }
231
232    /// Cap the topology-aware package selection passes (defaults to the
233    /// participant's configured value).
234    #[must_use]
235    pub fn with_taps_max_passes(mut self, passes: u32) -> Self {
236        self.taps_max_passes = Some(passes);
237        self
238    }
239
240    /// Turn off the traffic-cost estimate the participant returns by default.
241    #[must_use]
242    pub fn without_traffic_cost_estimation(mut self) -> Self {
243        self.estimate_traffic_cost = false;
244        self
245    }
246
247    /// The parties this will act as.
248    #[must_use]
249    pub fn act_as(&self) -> &[String] {
250        &self.act_as
251    }
252
253    /// The parties this will read as, beyond those acting.
254    #[must_use]
255    pub fn read_as(&self) -> &[String] {
256        &self.read_as
257    }
258
259    /// The contracts this will disclose.
260    ///
261    /// A token-standard choice is meaningless without the registry's
262    /// disclosures, so a caller assembling one wants to see them before
263    /// sending, not after a rejection.
264    #[must_use]
265    pub fn disclosed_contracts(&self) -> &[pb::DisclosedContract] {
266        &self.disclosed_contracts
267    }
268
269    /// Build the wire request, returning the command id that went into it.
270    ///
271    /// The id is returned rather than read back off the request so a caller can
272    /// still name the change ID after preparation — to recover a completion, or
273    /// to prepare the same command again deliberately. Execution does not carry
274    /// it as a field: the participant reads it from the prepared transaction's
275    /// own `submitter_info`, which is what makes a repeated execution a
276    /// duplicate rather than a second transaction.
277    /// The hashing scheme this request asks for, if any, so the response can
278    /// be held to it.
279    pub(crate) fn requested_hashing_scheme(&self) -> Option<ipb::HashingSchemeVersion> {
280        self.hashing_scheme_version
281            .and_then(|v| ipb::HashingSchemeVersion::try_from(v).ok())
282    }
283
284    pub(crate) fn into_request(self) -> (String, Option<String>, ipb::PrepareSubmissionRequest) {
285        let command_id = self
286            .command_id
287            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
288        let user_id = self.user_id;
289        let min_ledger_time = min_ledger_time(self.min_ledger_time_abs, self.min_ledger_time_rel);
290        let request = ipb::PrepareSubmissionRequest {
291            user_id: user_id.clone().unwrap_or_default(),
292            command_id: command_id.clone(),
293            commands: self.commands,
294            min_ledger_time,
295            act_as: self.act_as,
296            read_as: self.read_as,
297            disclosed_contracts: self.disclosed_contracts,
298            synchronizer_id: self.synchronizer_id.unwrap_or_default(),
299            package_id_selection_preference: self.package_id_selection_preference,
300            verbose_hashing: self.verbose_hashing,
301            prefetch_contract_keys: self.prefetch_contract_keys,
302            max_record_time: self.max_record_time,
303            hashing_scheme_version: self.hashing_scheme_version,
304            taps_max_passes: self.taps_max_passes,
305            estimate_traffic_cost: (!self.estimate_traffic_cost).then_some(
306                ipb::CostEstimationHints {
307                    disabled: true,
308                    // Only meaningful when estimation is on, and it is off here.
309                    expected_signatures: Vec::new(),
310                },
311            ),
312        };
313        (command_id, user_id, request)
314    }
315}
316
317/// A `std::time::Duration` as protobuf carries one.
318///
319/// Saturating rather than wrapping: a duration past `i64::MAX` seconds is a
320/// caller mistake, and the participant refusing a very long period is a better
321/// outcome than a negative one it accepts.
322fn duration(value: std::time::Duration) -> prost_types::Duration {
323    prost_types::Duration {
324        seconds: i64::try_from(value.as_secs()).unwrap_or(i64::MAX),
325        nanos: i32::try_from(value.subsec_nanos()).unwrap_or(0),
326    }
327}
328
329/// `MinLedgerTime` is a `oneof`, so at most one of the two can be set.
330///
331/// Both set is rejected by the participant; expressing it as a `oneof` here
332/// means the later call simply wins rather than producing a request that
333/// cannot succeed.
334fn min_ledger_time(
335    abs: Option<prost_types::Timestamp>,
336    rel: Option<std::time::Duration>,
337) -> Option<ipb::MinLedgerTime> {
338    use ipb::min_ledger_time::Time;
339    let time = match (abs, rel) {
340        (Some(abs), _) => Time::MinLedgerTimeAbs(abs),
341        (None, Some(rel)) => Time::MinLedgerTimeRel(duration(rel)),
342        (None, None) => return None,
343    };
344    Some(ipb::MinLedgerTime { time: Some(time) })
345}
346
347/// An interpreted transaction, waiting for signatures.
348///
349/// [`hash`](Self::hash) is what gets signed — it is the participant's hash of
350/// the transaction, not a message to hash again.
351///
352/// # What signing this does and does not prove
353///
354/// The hash is the **preparing participant's word**. This crate does not
355/// recompute it from [`transaction`](Self::transaction), so signing a
356/// `Prepared` authorizes whatever that participant hashed, which is not
357/// necessarily what [`transaction`](Self::transaction) shows.
358///
359/// The Ledger API is explicit about the two obligations that follow, and both
360/// are the caller's:
361///
362/// * show the transaction to whoever authorizes it, before signing;
363/// * **recompute the hash from the transaction if the participant is not
364///   trusted** — the API calls the hash "provided for convenience" for exactly
365///   this reason.
366///
367/// Every hashing scheme the participant offers is a SHA-256, so its hash is
368/// 32 bytes; anything else is not a hash this SDK knows how to sign for.
369const PREPARED_HASH_LEN: usize = 32;
370
371/// Interactive submission keeps the key away from the participant. It does not,
372/// on its own, make a dishonest participant harmless.
373#[derive(Clone, Debug)]
374pub struct Prepared {
375    transaction: Option<ipb::PreparedTransaction>,
376    hash: Vec<u8>,
377    hashing_scheme_version: i32,
378    hashing_details: Option<String>,
379    cost_estimation: Option<ipb::CostEstimation>,
380    act_as: Vec<String>,
381    read_as: Vec<String>,
382    command_id: String,
383    user_id: Option<String>,
384}
385
386impl Prepared {
387    pub(crate) fn from_response(
388        response: ipb::PrepareSubmissionResponse,
389        requested_scheme: Option<ipb::HashingSchemeVersion>,
390        act_as: Vec<String>,
391        read_as: Vec<String>,
392        command_id: String,
393        user_id: Option<String>,
394    ) -> Result<Self> {
395        // The transaction is what the proto marks `Required`, and it is where
396        // the change ID lives — without it an execute request is invalid and
397        // de-duplicates against nothing. The hash beside it is documented as
398        // "provided for convenience" and "may be removed in future versions",
399        // so checking only that one guards the wrong field.
400        let Some(transaction) = response.prepared_transaction.as_ref() else {
401            return Err(Error::UnexpectedResponse(
402                "prepare returned no prepared transaction, so there is nothing to execute"
403                    .to_string(),
404            ));
405        };
406        if response.prepared_transaction_hash.is_empty() {
407            return Err(Error::UnexpectedResponse(
408                "prepare returned no transaction hash".to_string(),
409            ));
410        }
411        // What the signer is handed is trusted from the participant (see the
412        // struct docs), so the little that can be checked here is checked:
413        // the hash is one the SDK knows the shape of, under the scheme that
414        // was asked for. A participant answering under another scheme, or with
415        // a hash of another length, is not one to sign for.
416        let scheme = ipb::HashingSchemeVersion::try_from(response.hashing_scheme_version)
417            .ok()
418            .filter(|s| *s != ipb::HashingSchemeVersion::Unspecified)
419            .ok_or_else(|| {
420                Error::UnexpectedResponse(format!(
421                    "prepare answered under an unknown hashing scheme ({})",
422                    response.hashing_scheme_version
423                ))
424            })?;
425        if let Some(requested) = requested_scheme.filter(|requested| *requested != scheme) {
426            return Err(Error::UnexpectedResponse(format!(
427                "prepare hashed under {scheme:?} but {requested:?} was requested"
428            )));
429        }
430        if response.prepared_transaction_hash.len() != PREPARED_HASH_LEN {
431            return Err(Error::UnexpectedResponse(format!(
432                "prepare returned a {}-byte transaction hash; {scheme:?} hashes are {PREPARED_HASH_LEN} bytes",
433                response.prepared_transaction_hash.len()
434            )));
435        }
436        // The hash is what gets signed, so it is not taken on trust: under
437        // the scheme this SDK implements it is recomputed from the transaction
438        // the caller can read, and a participant whose hash is of something
439        // else is refused. A caller that asked for another scheme explicitly
440        // has accepted the participant's hash as it is.
441        if scheme == ipb::HashingSchemeVersion::V2 {
442            let recomputed = crate::hashing::hash_prepared_transaction_v2(transaction)
443                .map_err(|e| {
444                    Error::UnexpectedResponse(format!(
445                        "the prepared transaction cannot be hashed, so its hash cannot be checked: {e}"
446                    ))
447                })?;
448            if recomputed[..] != response.prepared_transaction_hash[..] {
449                return Err(Error::UnexpectedResponse(
450                    "the participant's transaction hash is not the hash of the transaction it \
451                     returned; refusing to sign it"
452                        .to_string(),
453                ));
454            }
455        } else {
456            tracing::warn!(
457                ?scheme,
458                "hashing scheme requested explicitly; the participant's hash is signed as returned"
459            );
460        }
461        Ok(Self {
462            transaction: response.prepared_transaction,
463            hash: response.prepared_transaction_hash,
464            hashing_scheme_version: response.hashing_scheme_version,
465            hashing_details: response.hashing_details,
466            cost_estimation: response.cost_estimation,
467            act_as,
468            read_as,
469            command_id,
470            user_id,
471        })
472    }
473
474    /// The hash to sign.
475    #[must_use]
476    pub fn hash(&self) -> &[u8] {
477        &self.hash
478    }
479
480    /// The interpreted transaction, for a caller that wants to inspect what it
481    /// is about to authorize before signing it.
482    #[must_use]
483    pub fn transaction(&self) -> Option<&ipb::PreparedTransaction> {
484        self.transaction.as_ref()
485    }
486
487    /// The change ID's command component, carried from preparation.
488    #[must_use]
489    pub fn command_id(&self) -> &str {
490        &self.command_id
491    }
492
493    /// The identity the participant de-duplicates this submission by —
494    /// (`user_id`, `act_as`, `command_id`). Keep it: it is what
495    /// [`CantonClient::await_completion`](crate::CantonClient::await_completion)
496    /// recovers an ambiguous execute by, and `Executable` is consumed on
497    /// execute.
498    #[must_use]
499    pub fn change_id(&self) -> crate::command::ChangeId {
500        crate::command::ChangeId::new(
501            self.user_id.clone().unwrap_or_default(),
502            self.act_as.clone(),
503            self.command_id.clone(),
504        )
505    }
506
507    /// The parties that must sign.
508    #[must_use]
509    pub fn act_as(&self) -> &[String] {
510        &self.act_as
511    }
512
513    /// What the participant says about how it hashed, when preparation asked
514    /// for it with [`Prepare::with_verbose_hashing`].
515    ///
516    /// For troubleshooting a hash mismatch and nothing else: the proto is
517    /// explicit that neither the format nor the content is stable, so this is
518    /// a string to read, never one to parse.
519    #[must_use]
520    pub fn hashing_details(&self) -> Option<&str> {
521        self.hashing_details.as_deref()
522    }
523
524    /// The participant's estimate of what executing this will cost in traffic,
525    /// unless preparation turned the estimate off.
526    #[must_use]
527    pub fn cost_estimation(&self) -> Option<&ipb::CostEstimation> {
528        self.cost_estimation.as_ref()
529    }
530
531    /// The hashing scheme the participant hashed under, echoed back with the
532    /// signature so it verifies under the same one.
533    #[must_use]
534    pub fn hashing_scheme_version(&self) -> ipb::HashingSchemeVersion {
535        ipb::HashingSchemeVersion::try_from(self.hashing_scheme_version)
536            .unwrap_or(ipb::HashingSchemeVersion::Unspecified)
537    }
538
539    /// Sign as the single acting party.
540    ///
541    /// # Errors
542    /// [`Error::InvalidRequest`] if the submission acts as more than one party
543    /// — then it is ambiguous which one this signature is for, and
544    /// [`sign_as`](Self::sign_as) says so explicitly.
545    pub async fn sign_with(self, signer: &dyn Signer) -> Result<Executable> {
546        let [party] = self.act_as.as_slice() else {
547            return Err(Error::InvalidRequest(format!(
548                "this submission acts as {} parties, so a signature must name \
549                 one: use `sign_as`",
550                self.act_as.len()
551            )));
552        };
553        let party = party.clone();
554        self.sign_as(&party, signer).await
555    }
556
557    /// Sign on behalf of `party`.
558    ///
559    /// # Errors
560    /// [`Error::InvalidRequest`] if `party` is not among the acting parties —
561    /// the participant would reject the execution, and catching it here says
562    /// which party was meant. Otherwise, whatever the signer returns.
563    pub async fn sign_as(self, party: &str, signer: &dyn Signer) -> Result<Executable> {
564        let signature = self.sign_one(party, signer).await?;
565        Ok(Executable {
566            prepared: self,
567            signatures: vec![signature],
568            submission_id: None,
569            transaction_shape: crate::request::TransactionShape::default(),
570            deduplication: None,
571        })
572    }
573
574    async fn sign_one(
575        &self,
576        party: &str,
577        signer: &dyn Signer,
578    ) -> Result<ipb::SinglePartySignatures> {
579        if !self.act_as.iter().any(|p| p == party) {
580            return Err(Error::InvalidRequest(format!(
581                "`{party}` is not among the parties this submission acts as ({})",
582                self.act_as.join(", ")
583            )));
584        }
585        let signature = signer.sign(&self.hash).await?;
586        Ok(ipb::SinglePartySignatures {
587            party: party.to_string(),
588            signatures: vec![pb::Signature::from(&signature)],
589        })
590    }
591
592    /// Every party this reads as, beyond those acting.
593    #[must_use]
594    pub fn read_as(&self) -> &[String] {
595        &self.read_as
596    }
597}
598
599/// How long a change ID stays a duplicate.
600///
601/// On the wire this is a `oneof` per execute variant, so it is held in one
602/// neutral form here and converted at the point of sending.
603#[derive(Clone, Copy, Debug)]
604enum Deduplication {
605    Duration(std::time::Duration),
606    Offset(i64),
607}
608
609/// A prepared transaction with the signatures that authorize it.
610#[derive(Clone, Debug)]
611pub struct Executable {
612    prepared: Prepared,
613    signatures: Vec<ipb::SinglePartySignatures>,
614    submission_id: Option<String>,
615    transaction_shape: crate::request::TransactionShape,
616    deduplication: Option<Deduplication>,
617}
618
619impl Executable {
620    /// Add another party's signature — for a multi-party submission, where the
621    /// participant requires one per acting party.
622    ///
623    /// # Errors
624    /// As [`Prepared::sign_as`].
625    pub async fn and_sign_as(mut self, party: &str, signer: &dyn Signer) -> Result<Self> {
626        let signature = self.prepared.sign_one(party, signer).await?;
627        self.add(signature);
628        Ok(self)
629    }
630
631    /// Fold a signature into the entry for its party.
632    ///
633    /// The wire has one entry per party carrying *several* signatures — which
634    /// is how a party whose topology sets a threshold above one authorizes at
635    /// all. Pushing a second entry for the same party would send a shape the
636    /// standard does not describe, and would make the threshold case
637    /// unreachable.
638    fn add(&mut self, signature: ipb::SinglePartySignatures) {
639        if let Some(existing) = self
640            .signatures
641            .iter_mut()
642            .find(|entry| entry.party == signature.party)
643        {
644            existing.signatures.extend(signature.signatures);
645        } else {
646            self.signatures.push(signature);
647        }
648    }
649
650    /// Attach a signature produced elsewhere — an offline ceremony, or a
651    /// service that returns signatures rather than implementing
652    /// [`Signer`].
653    ///
654    /// # Errors
655    /// [`Error::InvalidRequest`] if `party` is not among the acting parties.
656    pub fn with_signature(
657        mut self,
658        party: &str,
659        signature: &canton_signer::Signature,
660    ) -> Result<Self> {
661        if !self.prepared.act_as.iter().any(|p| p == party) {
662            return Err(Error::InvalidRequest(format!(
663                "`{party}` is not among the parties this submission acts as ({})",
664                self.prepared.act_as.join(", ")
665            )));
666        }
667        self.add(ipb::SinglePartySignatures {
668            party: party.to_string(),
669            signatures: vec![pb::Signature::from(signature)],
670        });
671        Ok(self)
672    }
673
674    /// Set the de-duplication period as a wall-clock duration: an execution
675    /// with the same change ID within this window is rejected as a duplicate.
676    ///
677    /// The change ID comes from preparation (`user_id`, `act_as`,
678    /// `command_id`), so this de-duplicates against ordinary submissions of the
679    /// same command too, not only against other executions.
680    #[must_use]
681    pub fn with_deduplication_duration(mut self, duration: std::time::Duration) -> Self {
682        self.deduplication = Some(Deduplication::Duration(duration));
683        self
684    }
685
686    /// Set the de-duplication period as a ledger offset: executions with the
687    /// same change ID since that offset are rejected as duplicates.
688    #[must_use]
689    pub fn with_deduplication_offset(mut self, offset: i64) -> Self {
690        self.deduplication = Some(Deduplication::Offset(offset));
691        self
692    }
693
694    /// Set the submission id — the identifier that distinguishes completions
695    /// for different submissions of the same change ID.
696    ///
697    /// Left unset, a fresh UUID is generated **per attempt**, which is what the
698    /// Ledger API expects of a retried submission. Setting one fixes it across
699    /// retries, which is a deliberate choice rather than a default.
700    #[must_use]
701    pub fn with_submission_id(mut self, submission_id: impl Into<String>) -> Self {
702        self.submission_id = Some(submission_id.into());
703        self
704    }
705
706    /// Which events the returned transaction carries — only meaningful for
707    /// [`execute_submission_and_wait_for_transaction`].
708    ///
709    /// [`execute_submission_and_wait_for_transaction`]:
710    ///     crate::CantonClient::execute_submission_and_wait_for_transaction
711    #[must_use]
712    pub fn with_transaction_shape(mut self, shape: crate::request::TransactionShape) -> Self {
713        self.transaction_shape = shape;
714        self
715    }
716
717    /// The command id, carried from preparation.
718    ///
719    /// The change id the participant will de-duplicate by; see
720    /// [`Prepared::change_id`].
721    #[must_use]
722    pub fn change_id(&self) -> crate::command::ChangeId {
723        self.prepared.change_id()
724    }
725
726    /// One component of the change ID (`user_id`, `act_as`, `command_id`), and
727    /// what [`await_completion`](crate::CantonClient::await_completion) matches
728    /// on — so a caller using the fire-and-forget
729    /// [`execute_submission`](crate::CantonClient::execute_submission) needs it
730    /// to recover the outcome.
731    #[must_use]
732    pub fn command_id(&self) -> &str {
733        &self.prepared.command_id
734    }
735
736    /// The parties that have signed so far.
737    #[must_use]
738    pub fn signed_by(&self) -> Vec<&str> {
739        self.signatures.iter().map(|s| s.party.as_str()).collect()
740    }
741
742    /// Every acting party that has not signed yet.
743    ///
744    /// The participant requires a signature per acting party; this is that
745    /// check, before the round trip that would report it as a rejection.
746    #[must_use]
747    pub fn unsigned_parties(&self) -> Vec<&str> {
748        self.prepared
749            .act_as
750            .iter()
751            .filter(|party| !self.signatures.iter().any(|s| &&s.party == party))
752            .map(String::as_str)
753            .collect()
754    }
755
756    /// The parts every execute variant shares.
757    fn parts(
758        self,
759    ) -> (
760        Option<ipb::PreparedTransaction>,
761        Option<ipb::PartySignatures>,
762        String,
763        String,
764        i32,
765        crate::request::TransactionShape,
766        Option<Deduplication>,
767    ) {
768        // Left as the caller set it — possibly empty. The client fills a fresh
769        // UUID per *attempt*, because the proto says "applications are expected
770        // to use a different UUID for each retry of a submission with the same
771        // change ID". Baking one in here would give every retry the same id and
772        // defeat the field's only purpose: telling those completions apart.
773        let submission_id = self.submission_id.unwrap_or_default();
774        (
775            self.prepared.transaction,
776            Some(ipb::PartySignatures {
777                signatures: self.signatures,
778            }),
779            submission_id,
780            self.prepared.user_id.unwrap_or_default(),
781            self.prepared.hashing_scheme_version,
782            self.transaction_shape,
783            self.deduplication,
784        )
785    }
786
787    pub(crate) fn into_execute_request(self) -> ipb::ExecuteSubmissionRequest {
788        use ipb::execute_submission_request::DeduplicationPeriod as P;
789        let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
790            self.parts();
791        ipb::ExecuteSubmissionRequest {
792            prepared_transaction: transaction,
793            party_signatures,
794            deduplication_period: dedup.map(|d| match d {
795                Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
796                Deduplication::Offset(o) => P::DeduplicationOffset(o),
797            }),
798            submission_id,
799            user_id,
800            hashing_scheme_version: hashing,
801            min_ledger_time: None,
802        }
803    }
804
805    pub(crate) fn into_execute_and_wait_request(self) -> ipb::ExecuteSubmissionAndWaitRequest {
806        use ipb::execute_submission_and_wait_request::DeduplicationPeriod as P;
807        let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
808            self.parts();
809        ipb::ExecuteSubmissionAndWaitRequest {
810            prepared_transaction: transaction,
811            party_signatures,
812            deduplication_period: dedup.map(|d| match d {
813                Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
814                Deduplication::Offset(o) => P::DeduplicationOffset(o),
815            }),
816            submission_id,
817            user_id,
818            hashing_scheme_version: hashing,
819            min_ledger_time: None,
820        }
821    }
822
823    pub(crate) fn into_execute_and_wait_for_transaction_request(
824        self,
825        event_format: pb::EventFormat,
826    ) -> ipb::ExecuteSubmissionAndWaitForTransactionRequest {
827        use ipb::execute_submission_and_wait_for_transaction_request::DeduplicationPeriod as P;
828        let (transaction, party_signatures, submission_id, user_id, hashing, shape, dedup) =
829            self.parts();
830        ipb::ExecuteSubmissionAndWaitForTransactionRequest {
831            prepared_transaction: transaction,
832            party_signatures,
833            deduplication_period: dedup.map(|d| match d {
834                Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
835                Deduplication::Offset(o) => P::DeduplicationOffset(o),
836            }),
837            submission_id,
838            user_id,
839            hashing_scheme_version: hashing,
840            min_ledger_time: None,
841            transaction_format: Some(pb::TransactionFormat {
842                event_format: Some(event_format),
843                transaction_shape: shape.as_grpc() as i32,
844            }),
845        }
846    }
847
848    /// Every party the returned transaction should be filtered for.
849    ///
850    /// `act_as` *and* `read_as`: the Ledger API documents its default event
851    /// format as covering both, so filtering on the acting parties alone would
852    /// drop events only a reading party witnesses.
853    pub(crate) fn witnesses(&self) -> Vec<String> {
854        let mut parties = self.prepared.act_as.clone();
855        parties.extend(self.prepared.read_as.iter().cloned());
856        parties.sort_unstable();
857        parties.dedup();
858        parties
859    }
860}
861
862#[cfg(test)]
863#[allow(clippy::unwrap_used, clippy::expect_used)]
864mod tests {
865    use super::*;
866    use canton_signer::Ed25519Key;
867
868    fn signer(fingerprint: &str) -> canton_signer::Ed25519Signer {
869        Ed25519Key::from_seed(&[4; 32])
870            .expect("a valid seed")
871            .into_signer(fingerprint)
872    }
873
874    /// The smallest prepared transaction the hasher accepts: no nodes, and
875    /// metadata naming the acting parties and command. What a participant
876    /// returns for it is its V2 hash, computed here the way `from_response`
877    /// will recompute it.
878    fn hashable(act_as: &[String], command_id: &str) -> (ipb::PreparedTransaction, Vec<u8>) {
879        let transaction = ipb::PreparedTransaction {
880            transaction: Some(ipb::DamlTransaction {
881                version: "2.1".to_string(),
882                ..Default::default()
883            }),
884            metadata: Some(ipb::Metadata {
885                submitter_info: Some(ipb::metadata::SubmitterInfo {
886                    act_as: act_as.to_vec(),
887                    command_id: command_id.to_string(),
888                }),
889                ..Default::default()
890            }),
891        };
892        let hash = crate::hashing::hash_prepared_transaction_v2(&transaction)
893            .expect("hashable")
894            .to_vec();
895        (transaction, hash)
896    }
897
898    fn prepared(act_as: &[&str]) -> Prepared {
899        let act_as: Vec<String> = act_as.iter().map(ToString::to_string).collect();
900        let (transaction, hash) = hashable(&act_as, "command-1");
901        Prepared::from_response(
902            ipb::PrepareSubmissionResponse {
903                prepared_transaction: Some(transaction),
904                prepared_transaction_hash: hash,
905                hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
906                hashing_details: None,
907                cost_estimation: None,
908            },
909            Some(ipb::HashingSchemeVersion::V2),
910            act_as,
911            Vec::new(),
912            "command-1".to_string(),
913            Some("user-1".to_string()),
914        )
915        .expect("a response carrying a transaction and a hash")
916    }
917
918    /// A response with no hash leaves nothing to sign, so it is an error here
919    /// rather than an empty signature the participant rejects later.
920    #[test]
921    fn a_prepare_response_without_a_hash_is_refused() {
922        let err = Prepared::from_response(
923            ipb::PrepareSubmissionResponse {
924                prepared_transaction: Some(ipb::PreparedTransaction::default()),
925                prepared_transaction_hash: Vec::new(),
926                hashing_scheme_version: 0,
927                hashing_details: None,
928                cost_estimation: None,
929            },
930            None,
931            vec!["alice".to_string()],
932            Vec::new(),
933            "c".to_string(),
934            None,
935        )
936        .expect_err("no hash");
937        assert!(err.to_string().contains("no transaction hash"), "{err}");
938    }
939
940    /// And a response with no *transaction* is refused too — that is the field
941    /// the API marks required, and it carries the change ID. Checking only the
942    /// hash guarded the field the API calls "provided for convenience" and
943    /// documents as removable, while letting the required one through as an
944    /// execute request that de-duplicates against nothing.
945    #[test]
946    fn a_prepare_response_without_a_transaction_is_refused() {
947        let err = Prepared::from_response(
948            ipb::PrepareSubmissionResponse {
949                prepared_transaction: None,
950                prepared_transaction_hash: vec![1, 2, 3],
951                hashing_scheme_version: 0,
952                hashing_details: None,
953                cost_estimation: None,
954            },
955            None,
956            vec!["alice".to_string()],
957            Vec::new(),
958            "c".to_string(),
959            None,
960        )
961        .expect_err("no transaction");
962        assert!(err.to_string().contains("nothing to execute"), "{err}");
963    }
964
965    fn response_with(hash: Vec<u8>, scheme: i32) -> ipb::PrepareSubmissionResponse {
966        let (transaction, _) = hashable(&["alice".to_string()], "command-1");
967        ipb::PrepareSubmissionResponse {
968            prepared_transaction: Some(transaction),
969            prepared_transaction_hash: hash,
970            hashing_scheme_version: scheme,
971            hashing_details: None,
972            cost_estimation: None,
973        }
974    }
975
976    fn refused(
977        response: ipb::PrepareSubmissionResponse,
978        requested: Option<ipb::HashingSchemeVersion>,
979    ) -> String {
980        let err = Prepared::from_response(
981            response,
982            requested,
983            vec!["alice".to_string()],
984            Vec::new(),
985            "command-1".to_string(),
986            None,
987        )
988        .expect_err("refused");
989        assert!(matches!(err, Error::UnexpectedResponse(_)), "{err:?}");
990        err.to_string()
991    }
992
993    #[test]
994    fn a_hash_under_an_unknown_scheme_is_refused() {
995        let message = refused(response_with(vec![1; 32], 0), None);
996        assert!(message.contains("unknown hashing scheme"), "{message}");
997        let message = refused(response_with(vec![1; 32], 99), None);
998        assert!(message.contains("unknown hashing scheme (99)"), "{message}");
999    }
1000
1001    #[test]
1002    fn a_hash_under_a_scheme_that_was_not_requested_is_refused() {
1003        let message = refused(
1004            response_with(vec![1; 32], ipb::HashingSchemeVersion::V3 as i32),
1005            Some(ipb::HashingSchemeVersion::V2),
1006        );
1007        assert!(
1008            message.contains("V3") && message.contains("V2 was requested"),
1009            "{message}"
1010        );
1011    }
1012
1013    #[test]
1014    fn a_hash_of_the_wrong_length_is_refused() {
1015        for len in [1usize, 31, 33, 64] {
1016            let message = refused(
1017                response_with(vec![7; len], ipb::HashingSchemeVersion::V2 as i32),
1018                Some(ipb::HashingSchemeVersion::V2),
1019            );
1020            assert!(
1021                message.contains(&format!("{len}-byte transaction hash")),
1022                "{message}"
1023            );
1024        }
1025    }
1026
1027    /// The one that matters: a hash of something other than the returned
1028    /// transaction is refused, however well-formed it is.
1029    #[test]
1030    fn a_hash_that_is_not_the_transactions_own_is_refused() {
1031        let (_, mut hash) = hashable(&["alice".to_string()], "command-1");
1032        hash[0] ^= 0xff;
1033        let message = refused(
1034            response_with(hash, ipb::HashingSchemeVersion::V2 as i32),
1035            Some(ipb::HashingSchemeVersion::V2),
1036        );
1037        assert!(
1038            message.contains("not the hash of the transaction"),
1039            "{message}"
1040        );
1041    }
1042
1043    #[test]
1044    fn the_scheme_asked_for_defaults_to_v2() {
1045        let prepare = Prepare::new("alice");
1046        assert_eq!(
1047            prepare.requested_hashing_scheme(),
1048            Some(ipb::HashingSchemeVersion::V2)
1049        );
1050        let prepare = prepare.with_hashing_scheme_version(ipb::HashingSchemeVersion::V3);
1051        assert_eq!(
1052            prepare.requested_hashing_scheme(),
1053            Some(ipb::HashingSchemeVersion::V3)
1054        );
1055    }
1056
1057    #[tokio::test]
1058    async fn signing_a_multi_party_submission_must_name_the_party() {
1059        let err = prepared(&["alice", "bob"])
1060            .sign_with(&signer("1220aa"))
1061            .await
1062            .expect_err("ambiguous");
1063        assert!(err.to_string().contains("sign_as"), "{err}");
1064    }
1065
1066    #[tokio::test]
1067    async fn signing_as_a_party_that_is_not_acting_is_refused() {
1068        let err = prepared(&["alice"])
1069            .sign_as("mallory", &signer("1220aa"))
1070            .await
1071            .expect_err("not an acting party");
1072        assert!(err.to_string().contains("mallory"), "{err}");
1073        assert!(err.to_string().contains("alice"), "{err}");
1074    }
1075
1076    /// The participant requires a signature per acting party. Asking locally
1077    /// costs nothing; finding out from a rejection costs a round trip.
1078    #[tokio::test]
1079    async fn a_multi_party_submission_reports_who_has_not_signed() {
1080        let executable = prepared(&["alice", "bob"])
1081            .sign_as("alice", &signer("1220aa"))
1082            .await
1083            .expect("alice signs");
1084        assert_eq!(executable.signed_by(), vec!["alice"]);
1085        assert_eq!(executable.unsigned_parties(), vec!["bob"]);
1086
1087        let executable = executable
1088            .and_sign_as("bob", &signer("1220bb"))
1089            .await
1090            .expect("bob signs");
1091        assert!(executable.unsigned_parties().is_empty());
1092        assert_eq!(executable.signed_by(), vec!["alice", "bob"]);
1093    }
1094
1095    /// A party with a signing threshold above one provides several signatures,
1096    /// and the wire carries them inside that party's single entry. Pushing a
1097    /// second entry for the same party sent a shape the API does not describe —
1098    /// and left the threshold case unreachable.
1099    #[tokio::test]
1100    async fn several_signatures_for_one_party_share_its_entry() {
1101        let executable = prepared(&["alice"])
1102            .sign_as("alice", &signer("1220aa"))
1103            .await
1104            .expect("first key")
1105            .and_sign_as("alice", &signer("1220bb"))
1106            .await
1107            .expect("second key");
1108
1109        assert_eq!(executable.signed_by(), vec!["alice"], "one entry");
1110        let wire = executable.into_execute_request();
1111        let signatures = wire.party_signatures.expect("signatures").signatures;
1112        assert_eq!(signatures.len(), 1, "one entry per party");
1113        assert_eq!(signatures[0].signatures.len(), 2, "both keys inside it");
1114        assert_eq!(signatures[0].signatures[0].signed_by, "1220aa");
1115        assert_eq!(signatures[0].signatures[1].signed_by, "1220bb");
1116    }
1117
1118    /// Setting one bound clears the other, so "the later call wins" is true
1119    /// rather than aspirational — the wire has a `oneof` and one of the two had
1120    /// to be dropped.
1121    #[test]
1122    fn the_later_min_ledger_time_wins_in_either_order() {
1123        use ipb::min_ledger_time::Time;
1124        let abs_then_rel = Prepare::new("alice")
1125            .with_min_ledger_time_abs(prost_types::Timestamp {
1126                seconds: 42,
1127                nanos: 0,
1128            })
1129            .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1130            .into_request()
1131            .2
1132            .min_ledger_time
1133            .and_then(|t| t.time);
1134        assert!(
1135            matches!(abs_then_rel, Some(Time::MinLedgerTimeRel(_))),
1136            "rel was set last: {abs_then_rel:?}"
1137        );
1138    }
1139
1140    /// The change ID is `(user_id, act_as, command_id)`. If the command id did
1141    /// not survive preparation, executing twice would commit twice instead of
1142    /// being de-duplicated — the failure mode the id exists to prevent.
1143    #[tokio::test]
1144    async fn the_change_id_survives_from_preparation_into_execution() {
1145        let (command_id, user_id, request) = Prepare::new("alice")
1146            .with_command_id("stable-id")
1147            .with_user_id("user-1")
1148            .into_request();
1149        assert_eq!(command_id, "stable-id");
1150        assert_eq!(request.command_id, "stable-id");
1151        assert_eq!(user_id.as_deref(), Some("user-1"));
1152
1153        let executable = prepared(&["alice"])
1154            .sign_as("alice", &signer("1220aa"))
1155            .await
1156            .expect("sign");
1157        let wire = executable.into_execute_request();
1158        assert_eq!(
1159            wire.user_id, "user-1",
1160            "the user id is part of the change ID"
1161        );
1162
1163        // And the rest of the change ID travels inside the prepared
1164        // transaction, which is where the participant reads it from — not in a
1165        // field of the execute request. Asserting on the user id alone let this
1166        // test pass with `Prepared::command_id` deleted entirely.
1167        let submitter = wire
1168            .prepared_transaction
1169            .expect("the transaction is required")
1170            .metadata
1171            .expect("metadata")
1172            .submitter_info
1173            .expect("submitter info");
1174        assert_eq!(submitter.command_id, "command-1");
1175        assert_eq!(submitter.act_as, ["alice"]);
1176    }
1177
1178    /// Left unset it is still stable across a retry, because it is generated
1179    /// once at build time rather than per attempt.
1180    #[test]
1181    fn an_unset_command_id_is_generated_once() {
1182        let (id, _, request) = Prepare::new("alice").into_request();
1183        assert!(!id.is_empty());
1184        assert_eq!(id, request.command_id);
1185    }
1186
1187    /// `MinLedgerTime` is a `oneof`: the participant rejects both at once, so
1188    /// the builder cannot produce both.
1189    #[test]
1190    fn the_two_min_ledger_times_are_mutually_exclusive() {
1191        use ipb::min_ledger_time::Time;
1192
1193        let (_, _, request) = Prepare::new("alice")
1194            .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1195            .into_request();
1196        assert!(matches!(
1197            request.min_ledger_time.and_then(|t| t.time),
1198            Some(Time::MinLedgerTimeRel(_))
1199        ));
1200
1201        let (_, _, request) = Prepare::new("alice")
1202            .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1203            .with_min_ledger_time_abs(prost_types::Timestamp {
1204                seconds: 42,
1205                nanos: 0,
1206            })
1207            .into_request();
1208        let time = request.min_ledger_time.and_then(|t| t.time);
1209        assert!(
1210            matches!(time, Some(Time::MinLedgerTimeAbs(_))),
1211            "one of the two, never both: {time:?}"
1212        );
1213    }
1214
1215    /// The estimate is on by default and only an opt-out is sent, so an
1216    /// untouched builder must not carry the hints at all.
1217    #[test]
1218    fn traffic_cost_estimation_is_only_sent_when_turned_off() {
1219        let (_, _, request) = Prepare::new("alice").into_request();
1220        assert!(request.estimate_traffic_cost.is_none());
1221
1222        let (_, _, request) = Prepare::new("alice")
1223            .without_traffic_cost_estimation()
1224            .into_request();
1225        assert_eq!(
1226            request.estimate_traffic_cost.map(|h| h.disabled),
1227            Some(true)
1228        );
1229    }
1230
1231    /// A signature produced somewhere this crate cannot reach — an offline
1232    /// ceremony — reaches the wire under the party it was made for.
1233    #[tokio::test]
1234    async fn a_signature_made_elsewhere_reaches_the_wire() {
1235        let external = canton_signer::Signature::new(
1236            canton_signer::SignatureFormat::Concat,
1237            vec![8; 64],
1238            "1220cc",
1239            canton_signer::SigningAlgorithm::Ed25519,
1240        )
1241        .expect("a well-formed signature");
1242        let executable = prepared(&["alice", "bob"])
1243            .sign_as("alice", &signer("1220aa"))
1244            .await
1245            .expect("alice signs")
1246            .with_signature("bob", &external)
1247            .expect("bob's signature arrives from elsewhere");
1248
1249        let wire = executable.into_execute_and_wait_request();
1250        let signatures = wire.party_signatures.expect("signatures").signatures;
1251        let bob = signatures
1252            .iter()
1253            .find(|s| s.party == "bob")
1254            .expect("bob's entry");
1255        assert_eq!(bob.signatures[0].signature, vec![8; 64]);
1256        assert_eq!(bob.signatures[0].signed_by, "1220cc");
1257    }
1258
1259    #[tokio::test]
1260    async fn a_signature_for_a_party_that_is_not_acting_is_refused() {
1261        let external = canton_signer::Signature::new(
1262            canton_signer::SignatureFormat::Concat,
1263            vec![8; 64],
1264            "1220cc",
1265            canton_signer::SigningAlgorithm::Ed25519,
1266        )
1267        .expect("a well-formed signature");
1268        let err = prepared(&["alice"])
1269            .sign_as("alice", &signer("1220aa"))
1270            .await
1271            .expect("sign")
1272            .with_signature("mallory", &external)
1273            .expect_err("not an acting party");
1274        assert!(err.to_string().contains("mallory"), "{err}");
1275    }
1276
1277    /// The scheme the participant hashed under has to come back with the
1278    /// signature: verifying under a different one fails.
1279    #[tokio::test]
1280    async fn the_hashing_scheme_is_echoed_back_to_the_participant() {
1281        let prepared = prepared(&["alice"]);
1282        assert_eq!(
1283            prepared.hashing_scheme_version(),
1284            ipb::HashingSchemeVersion::V2
1285        );
1286
1287        let wire = prepared
1288            .sign_as("alice", &signer("1220aa"))
1289            .await
1290            .expect("sign")
1291            .into_execute_request();
1292        assert_eq!(
1293            wire.hashing_scheme_version,
1294            ipb::HashingSchemeVersion::V2 as i32
1295        );
1296    }
1297
1298    /// The hash the signer sees is the participant's, unmodified.
1299    #[tokio::test]
1300    async fn the_signer_is_handed_the_participants_hash() {
1301        let prepared = prepared(&["alice"]);
1302        let (_, hash) = hashable(&["alice".to_string()], "command-1");
1303        assert_eq!(prepared.hash(), &hash[..]);
1304
1305        let key = Ed25519Key::from_seed(&[4; 32]).expect("seed");
1306        let expected = key.sign_raw(&hash);
1307        let wire = prepared
1308            .sign_as("alice", &signer("1220aa"))
1309            .await
1310            .expect("sign")
1311            .into_execute_request();
1312        let signatures = wire.party_signatures.expect("signatures").signatures;
1313        assert_eq!(signatures[0].signatures[0].signature, expected);
1314    }
1315
1316    /// De-duplication lives on execution, not preparation — the wire has it
1317    /// there, and a period set before signing would be silently lost.
1318    #[tokio::test]
1319    async fn the_deduplication_period_reaches_every_execute_variant() {
1320        use ipb::execute_submission_request::DeduplicationPeriod;
1321
1322        let executable = prepared(&["alice"])
1323            .sign_as("alice", &signer("1220aa"))
1324            .await
1325            .expect("sign")
1326            .with_deduplication_duration(std::time::Duration::from_secs(60));
1327
1328        let wire = executable.clone().into_execute_request();
1329        assert!(matches!(
1330            wire.deduplication_period,
1331            Some(DeduplicationPeriod::DeduplicationDuration(d)) if d.seconds == 60
1332        ));
1333
1334        let wire = executable
1335            .with_deduplication_offset(99)
1336            .into_execute_and_wait_request();
1337        assert!(matches!(
1338            wire.deduplication_period,
1339            Some(
1340                ipb::execute_submission_and_wait_request::DeduplicationPeriod::DeduplicationOffset(
1341                    99
1342                )
1343            )
1344        ));
1345    }
1346
1347    /// What a caller can read off each state, and that each reader returns the
1348    /// value that was put in — not a default the type happens to have.
1349    #[test]
1350    fn prepare_reports_what_it_was_given() {
1351        let disclosed = pb::DisclosedContract {
1352            contract_id: "00d".to_string(),
1353            ..Default::default()
1354        };
1355        let prepare = Prepare::new_multi(vec!["alice".to_string(), "bob".to_string()])
1356            .with_read_as(vec!["carol".to_string()])
1357            .with_disclosed_contracts(vec![disclosed]);
1358        assert_eq!(prepare.act_as(), ["alice".to_string(), "bob".to_string()]);
1359        assert_eq!(prepare.read_as(), ["carol".to_string()]);
1360        assert_eq!(prepare.disclosed_contracts().len(), 1);
1361        assert_eq!(prepare.disclosed_contracts()[0].contract_id, "00d");
1362    }
1363
1364    #[tokio::test]
1365    async fn prepared_and_executable_report_what_the_participant_returned() {
1366        let prepared = Prepared::from_response(
1367            ipb::PrepareSubmissionResponse {
1368                prepared_transaction: Some(hashable(&["alice".to_string()], "command-7").0),
1369                prepared_transaction_hash: hashable(&["alice".to_string()], "command-7").1,
1370                hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
1371                hashing_details: Some("hashed thus".to_string()),
1372                cost_estimation: Some(ipb::CostEstimation {
1373                    confirmation_request_traffic_cost_estimation: 1234,
1374                    ..Default::default()
1375                }),
1376            },
1377            Some(ipb::HashingSchemeVersion::V2),
1378            vec!["alice".to_string()],
1379            vec!["carol".to_string()],
1380            "command-7".to_string(),
1381            Some("user-1".to_string()),
1382        )
1383        .expect("a full response");
1384
1385        assert_eq!(prepared.command_id(), "command-7");
1386        assert_eq!(prepared.act_as(), ["alice".to_string()]);
1387        assert_eq!(prepared.read_as(), ["carol".to_string()]);
1388        assert_eq!(prepared.hashing_details(), Some("hashed thus"));
1389        assert_eq!(
1390            prepared
1391                .cost_estimation()
1392                .map(|c| c.confirmation_request_traffic_cost_estimation),
1393            Some(1234)
1394        );
1395        let transaction = prepared.transaction().expect("the transaction is kept");
1396        let info = transaction
1397            .metadata
1398            .as_ref()
1399            .and_then(|m| m.submitter_info.as_ref())
1400            .expect("its submitter info");
1401        assert_eq!(info.command_id, "command-7");
1402        assert_eq!(
1403            prepared.change_id(),
1404            crate::command::ChangeId::new(
1405                "user-1".to_string(),
1406                vec!["alice".to_string()],
1407                "command-7".to_string()
1408            )
1409        );
1410
1411        let executable = prepared
1412            .sign_with(&signer("1220aa"))
1413            .await
1414            .expect("one acting party signs without naming itself");
1415        assert_eq!(executable.command_id(), "command-7");
1416        assert_eq!(
1417            executable.change_id(),
1418            crate::command::ChangeId::new(
1419                "user-1".to_string(),
1420                vec!["alice".to_string()],
1421                "command-7".to_string()
1422            )
1423        );
1424        assert_eq!(executable.signed_by(), ["alice"]);
1425        // Both acting and reading parties witness the returned transaction.
1426        let mut witnesses = executable.witnesses();
1427        witnesses.sort();
1428        assert_eq!(witnesses, ["alice".to_string(), "carol".to_string()]);
1429    }
1430
1431    /// The wait-for-transaction request carries everything the other two do,
1432    /// plus the format that decides which events come back — for every party
1433    /// that witnesses the transaction, in the shape the caller chose.
1434    #[tokio::test]
1435    async fn the_wait_for_transaction_request_carries_the_signature_and_the_format() {
1436        let executable = prepared(&["alice"])
1437            .sign_as("alice", &signer("1220aa"))
1438            .await
1439            .expect("sign")
1440            .with_submission_id("sub-1")
1441            .with_transaction_shape(crate::request::TransactionShape::LedgerEffects);
1442
1443        let wire = executable.into_execute_and_wait_for_transaction_request(pb::EventFormat {
1444            verbose: true,
1445            ..Default::default()
1446        });
1447
1448        assert!(
1449            wire.prepared_transaction.is_some(),
1450            "the transaction travels"
1451        );
1452        let signatures = wire.party_signatures.expect("signatures");
1453        assert_eq!(signatures.signatures.len(), 1);
1454        assert_eq!(signatures.signatures[0].party, "alice");
1455        assert_eq!(wire.submission_id, "sub-1");
1456        assert_eq!(wire.user_id, "user-1");
1457        assert_eq!(
1458            wire.hashing_scheme_version,
1459            ipb::HashingSchemeVersion::V2 as i32
1460        );
1461        let format = wire.transaction_format.expect("a transaction format");
1462        assert!(format.event_format.expect("event format").verbose);
1463        assert_eq!(
1464            format.transaction_shape,
1465            pb::TransactionShape::LedgerEffects as i32
1466        );
1467    }
1468}