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