1use canton_core::{Error, Result};
27use canton_proto::com::daml::ledger::api::v2 as pb;
28use canton_proto::com::daml::ledger::api::v2::interactive as ipb;
31use canton_signer::Signer;
32
33#[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 pub fn new(act_as: impl Into<String>) -> Self {
67 Self::new_multi(vec![act_as.into()])
68 }
69
70 #[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 hashing_scheme_version: None,
91 taps_max_passes: None,
92 estimate_traffic_cost: true,
95 }
96 }
97
98 #[must_use]
102 pub fn add_command(mut self, command: pb::Command) -> Self {
103 self.commands.push(command);
104 self
105 }
106
107 #[must_use]
109 pub fn add_commands(mut self, commands: impl IntoIterator<Item = pb::Command>) -> Self {
110 self.commands.extend(commands);
111 self
112 }
113
114 #[must_use]
121 pub fn with_command_id(mut self, command_id: impl Into<String>) -> Self {
122 self.command_id = Some(command_id.into());
123 self
124 }
125
126 #[must_use]
128 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
129 self.user_id = Some(user_id.into());
130 self
131 }
132
133 #[must_use]
135 pub fn with_read_as(mut self, read_as: Vec<String>) -> Self {
136 self.read_as = read_as;
137 self
138 }
139
140 #[must_use]
142 pub fn with_synchronizer_id(mut self, synchronizer_id: impl Into<String>) -> Self {
143 self.synchronizer_id = Some(synchronizer_id.into());
144 self
145 }
146
147 #[must_use]
150 pub fn with_disclosed_contracts(mut self, contracts: Vec<pb::DisclosedContract>) -> Self {
151 self.disclosed_contracts = contracts;
152 self
153 }
154
155 #[must_use]
157 pub fn with_package_id_selection_preference(mut self, package_ids: Vec<String>) -> Self {
158 self.package_id_selection_preference = package_ids;
159 self
160 }
161
162 #[must_use]
165 pub fn with_prefetch_contract_keys(mut self, keys: Vec<pb::PrefetchContractKey>) -> Self {
166 self.prefetch_contract_keys = keys;
167 self
168 }
169
170 #[must_use]
175 pub fn with_min_ledger_time_abs(mut self, time: prost_types::Timestamp) -> Self {
176 self.min_ledger_time_abs = Some(time);
177 self.min_ledger_time_rel = None;
181 self
182 }
183
184 #[must_use]
189 pub fn with_min_ledger_time_rel(mut self, duration: std::time::Duration) -> Self {
190 self.min_ledger_time_rel = Some(duration);
191 self.min_ledger_time_abs = None;
192 self
193 }
194
195 #[must_use]
201 pub fn with_verbose_hashing(mut self, verbose: bool) -> Self {
202 self.verbose_hashing = verbose;
203 self
204 }
205
206 #[must_use]
213 pub fn with_max_record_time(mut self, time: prost_types::Timestamp) -> Self {
214 self.max_record_time = Some(time);
215 self
216 }
217
218 #[must_use]
224 pub fn with_hashing_scheme_version(mut self, version: ipb::HashingSchemeVersion) -> Self {
225 self.hashing_scheme_version = Some(version as i32);
226 self
227 }
228
229 #[must_use]
232 pub fn with_taps_max_passes(mut self, passes: u32) -> Self {
233 self.taps_max_passes = Some(passes);
234 self
235 }
236
237 #[must_use]
239 pub fn without_traffic_cost_estimation(mut self) -> Self {
240 self.estimate_traffic_cost = false;
241 self
242 }
243
244 #[must_use]
246 pub fn act_as(&self) -> &[String] {
247 &self.act_as
248 }
249
250 #[must_use]
252 pub fn read_as(&self) -> &[String] {
253 &self.read_as
254 }
255
256 #[must_use]
262 pub fn disclosed_contracts(&self) -> &[pb::DisclosedContract] {
263 &self.disclosed_contracts
264 }
265
266 pub(crate) fn into_request(self) -> (String, Option<String>, ipb::PrepareSubmissionRequest) {
275 let command_id = self
276 .command_id
277 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
278 let user_id = self.user_id;
279 let min_ledger_time = min_ledger_time(self.min_ledger_time_abs, self.min_ledger_time_rel);
280 let request = ipb::PrepareSubmissionRequest {
281 user_id: user_id.clone().unwrap_or_default(),
282 command_id: command_id.clone(),
283 commands: self.commands,
284 min_ledger_time,
285 act_as: self.act_as,
286 read_as: self.read_as,
287 disclosed_contracts: self.disclosed_contracts,
288 synchronizer_id: self.synchronizer_id.unwrap_or_default(),
289 package_id_selection_preference: self.package_id_selection_preference,
290 verbose_hashing: self.verbose_hashing,
291 prefetch_contract_keys: self.prefetch_contract_keys,
292 max_record_time: self.max_record_time,
293 hashing_scheme_version: self.hashing_scheme_version,
294 taps_max_passes: self.taps_max_passes,
295 estimate_traffic_cost: (!self.estimate_traffic_cost).then_some(
296 ipb::CostEstimationHints {
297 disabled: true,
298 expected_signatures: Vec::new(),
300 },
301 ),
302 };
303 (command_id, user_id, request)
304 }
305}
306
307fn duration(value: std::time::Duration) -> prost_types::Duration {
313 prost_types::Duration {
314 seconds: i64::try_from(value.as_secs()).unwrap_or(i64::MAX),
315 nanos: i32::try_from(value.subsec_nanos()).unwrap_or(0),
316 }
317}
318
319fn min_ledger_time(
325 abs: Option<prost_types::Timestamp>,
326 rel: Option<std::time::Duration>,
327) -> Option<ipb::MinLedgerTime> {
328 use ipb::min_ledger_time::Time;
329 let time = match (abs, rel) {
330 (Some(abs), _) => Time::MinLedgerTimeAbs(abs),
331 (None, Some(rel)) => Time::MinLedgerTimeRel(duration(rel)),
332 (None, None) => return None,
333 };
334 Some(ipb::MinLedgerTime { time: Some(time) })
335}
336
337#[derive(Clone, Debug)]
360pub struct Prepared {
361 transaction: Option<ipb::PreparedTransaction>,
362 hash: Vec<u8>,
363 hashing_scheme_version: i32,
364 hashing_details: Option<String>,
365 cost_estimation: Option<ipb::CostEstimation>,
366 act_as: Vec<String>,
367 read_as: Vec<String>,
368 command_id: String,
369 user_id: Option<String>,
370}
371
372impl Prepared {
373 pub(crate) fn from_response(
374 response: ipb::PrepareSubmissionResponse,
375 act_as: Vec<String>,
376 read_as: Vec<String>,
377 command_id: String,
378 user_id: Option<String>,
379 ) -> Result<Self> {
380 if response.prepared_transaction.is_none() {
386 return Err(Error::UnexpectedResponse(
387 "prepare returned no prepared transaction, so there is nothing to execute"
388 .to_string(),
389 ));
390 }
391 if response.prepared_transaction_hash.is_empty() {
392 return Err(Error::UnexpectedResponse(
393 "prepare returned no transaction hash".to_string(),
394 ));
395 }
396 Ok(Self {
397 transaction: response.prepared_transaction,
398 hash: response.prepared_transaction_hash,
399 hashing_scheme_version: response.hashing_scheme_version,
400 hashing_details: response.hashing_details,
401 cost_estimation: response.cost_estimation,
402 act_as,
403 read_as,
404 command_id,
405 user_id,
406 })
407 }
408
409 #[must_use]
411 pub fn hash(&self) -> &[u8] {
412 &self.hash
413 }
414
415 #[must_use]
418 pub fn transaction(&self) -> Option<&ipb::PreparedTransaction> {
419 self.transaction.as_ref()
420 }
421
422 #[must_use]
424 pub fn command_id(&self) -> &str {
425 &self.command_id
426 }
427
428 #[must_use]
434 pub fn change_id(&self) -> crate::command::ChangeId {
435 crate::command::ChangeId::new(
436 self.user_id.clone().unwrap_or_default(),
437 self.act_as.clone(),
438 self.command_id.clone(),
439 )
440 }
441
442 #[must_use]
444 pub fn act_as(&self) -> &[String] {
445 &self.act_as
446 }
447
448 #[must_use]
455 pub fn hashing_details(&self) -> Option<&str> {
456 self.hashing_details.as_deref()
457 }
458
459 #[must_use]
462 pub fn cost_estimation(&self) -> Option<&ipb::CostEstimation> {
463 self.cost_estimation.as_ref()
464 }
465
466 #[must_use]
469 pub fn hashing_scheme_version(&self) -> ipb::HashingSchemeVersion {
470 ipb::HashingSchemeVersion::try_from(self.hashing_scheme_version)
471 .unwrap_or(ipb::HashingSchemeVersion::Unspecified)
472 }
473
474 pub async fn sign_with(self, signer: &dyn Signer) -> Result<Executable> {
481 let [party] = self.act_as.as_slice() else {
482 return Err(Error::InvalidRequest(format!(
483 "this submission acts as {} parties, so a signature must name \
484 one: use `sign_as`",
485 self.act_as.len()
486 )));
487 };
488 let party = party.clone();
489 self.sign_as(&party, signer).await
490 }
491
492 pub async fn sign_as(self, party: &str, signer: &dyn Signer) -> Result<Executable> {
499 let signature = self.sign_one(party, signer).await?;
500 Ok(Executable {
501 prepared: self,
502 signatures: vec![signature],
503 submission_id: None,
504 transaction_shape: crate::request::TransactionShape::default(),
505 deduplication: None,
506 })
507 }
508
509 async fn sign_one(
510 &self,
511 party: &str,
512 signer: &dyn Signer,
513 ) -> Result<ipb::SinglePartySignatures> {
514 if !self.act_as.iter().any(|p| p == party) {
515 return Err(Error::InvalidRequest(format!(
516 "`{party}` is not among the parties this submission acts as ({})",
517 self.act_as.join(", ")
518 )));
519 }
520 let signature = signer.sign(&self.hash).await?;
521 Ok(ipb::SinglePartySignatures {
522 party: party.to_string(),
523 signatures: vec![pb::Signature::from(&signature)],
524 })
525 }
526
527 #[must_use]
529 pub fn read_as(&self) -> &[String] {
530 &self.read_as
531 }
532}
533
534#[derive(Clone, Copy, Debug)]
539enum Deduplication {
540 Duration(std::time::Duration),
541 Offset(i64),
542}
543
544#[derive(Clone, Debug)]
546pub struct Executable {
547 prepared: Prepared,
548 signatures: Vec<ipb::SinglePartySignatures>,
549 submission_id: Option<String>,
550 transaction_shape: crate::request::TransactionShape,
551 deduplication: Option<Deduplication>,
552}
553
554impl Executable {
555 pub async fn and_sign_as(mut self, party: &str, signer: &dyn Signer) -> Result<Self> {
561 let signature = self.prepared.sign_one(party, signer).await?;
562 self.add(signature);
563 Ok(self)
564 }
565
566 fn add(&mut self, signature: ipb::SinglePartySignatures) {
574 if let Some(existing) = self
575 .signatures
576 .iter_mut()
577 .find(|entry| entry.party == signature.party)
578 {
579 existing.signatures.extend(signature.signatures);
580 } else {
581 self.signatures.push(signature);
582 }
583 }
584
585 pub fn with_signature(
592 mut self,
593 party: &str,
594 signature: &canton_signer::Signature,
595 ) -> Result<Self> {
596 if !self.prepared.act_as.iter().any(|p| p == party) {
597 return Err(Error::InvalidRequest(format!(
598 "`{party}` is not among the parties this submission acts as ({})",
599 self.prepared.act_as.join(", ")
600 )));
601 }
602 self.add(ipb::SinglePartySignatures {
603 party: party.to_string(),
604 signatures: vec![pb::Signature::from(signature)],
605 });
606 Ok(self)
607 }
608
609 #[must_use]
616 pub fn with_deduplication_duration(mut self, duration: std::time::Duration) -> Self {
617 self.deduplication = Some(Deduplication::Duration(duration));
618 self
619 }
620
621 #[must_use]
624 pub fn with_deduplication_offset(mut self, offset: i64) -> Self {
625 self.deduplication = Some(Deduplication::Offset(offset));
626 self
627 }
628
629 #[must_use]
636 pub fn with_submission_id(mut self, submission_id: impl Into<String>) -> Self {
637 self.submission_id = Some(submission_id.into());
638 self
639 }
640
641 #[must_use]
647 pub fn with_transaction_shape(mut self, shape: crate::request::TransactionShape) -> Self {
648 self.transaction_shape = shape;
649 self
650 }
651
652 #[must_use]
657 pub fn change_id(&self) -> crate::command::ChangeId {
658 self.prepared.change_id()
659 }
660
661 #[must_use]
667 pub fn command_id(&self) -> &str {
668 &self.prepared.command_id
669 }
670
671 #[must_use]
673 pub fn signed_by(&self) -> Vec<&str> {
674 self.signatures.iter().map(|s| s.party.as_str()).collect()
675 }
676
677 #[must_use]
682 pub fn unsigned_parties(&self) -> Vec<&str> {
683 self.prepared
684 .act_as
685 .iter()
686 .filter(|party| !self.signatures.iter().any(|s| &&s.party == party))
687 .map(String::as_str)
688 .collect()
689 }
690
691 fn parts(
693 self,
694 ) -> (
695 Option<ipb::PreparedTransaction>,
696 Option<ipb::PartySignatures>,
697 String,
698 String,
699 i32,
700 crate::request::TransactionShape,
701 Option<Deduplication>,
702 ) {
703 let submission_id = self.submission_id.unwrap_or_default();
709 (
710 self.prepared.transaction,
711 Some(ipb::PartySignatures {
712 signatures: self.signatures,
713 }),
714 submission_id,
715 self.prepared.user_id.unwrap_or_default(),
716 self.prepared.hashing_scheme_version,
717 self.transaction_shape,
718 self.deduplication,
719 )
720 }
721
722 pub(crate) fn into_execute_request(self) -> ipb::ExecuteSubmissionRequest {
723 use ipb::execute_submission_request::DeduplicationPeriod as P;
724 let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
725 self.parts();
726 ipb::ExecuteSubmissionRequest {
727 prepared_transaction: transaction,
728 party_signatures,
729 deduplication_period: dedup.map(|d| match d {
730 Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
731 Deduplication::Offset(o) => P::DeduplicationOffset(o),
732 }),
733 submission_id,
734 user_id,
735 hashing_scheme_version: hashing,
736 min_ledger_time: None,
737 }
738 }
739
740 pub(crate) fn into_execute_and_wait_request(self) -> ipb::ExecuteSubmissionAndWaitRequest {
741 use ipb::execute_submission_and_wait_request::DeduplicationPeriod as P;
742 let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
743 self.parts();
744 ipb::ExecuteSubmissionAndWaitRequest {
745 prepared_transaction: transaction,
746 party_signatures,
747 deduplication_period: dedup.map(|d| match d {
748 Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
749 Deduplication::Offset(o) => P::DeduplicationOffset(o),
750 }),
751 submission_id,
752 user_id,
753 hashing_scheme_version: hashing,
754 min_ledger_time: None,
755 }
756 }
757
758 pub(crate) fn into_execute_and_wait_for_transaction_request(
759 self,
760 event_format: pb::EventFormat,
761 ) -> ipb::ExecuteSubmissionAndWaitForTransactionRequest {
762 use ipb::execute_submission_and_wait_for_transaction_request::DeduplicationPeriod as P;
763 let (transaction, party_signatures, submission_id, user_id, hashing, shape, dedup) =
764 self.parts();
765 ipb::ExecuteSubmissionAndWaitForTransactionRequest {
766 prepared_transaction: transaction,
767 party_signatures,
768 deduplication_period: dedup.map(|d| match d {
769 Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
770 Deduplication::Offset(o) => P::DeduplicationOffset(o),
771 }),
772 submission_id,
773 user_id,
774 hashing_scheme_version: hashing,
775 min_ledger_time: None,
776 transaction_format: Some(pb::TransactionFormat {
777 event_format: Some(event_format),
778 transaction_shape: shape.as_grpc() as i32,
779 }),
780 }
781 }
782
783 pub(crate) fn witnesses(&self) -> Vec<String> {
789 let mut parties = self.prepared.act_as.clone();
790 parties.extend(self.prepared.read_as.iter().cloned());
791 parties.sort_unstable();
792 parties.dedup();
793 parties
794 }
795}
796
797#[cfg(test)]
798#[allow(clippy::unwrap_used, clippy::expect_used)]
799mod tests {
800 use super::*;
801 use canton_signer::Ed25519Key;
802
803 fn signer(fingerprint: &str) -> canton_signer::Ed25519Signer {
804 Ed25519Key::from_seed(&[4; 32])
805 .expect("a valid seed")
806 .into_signer(fingerprint)
807 }
808
809 fn prepared(act_as: &[&str]) -> Prepared {
810 let act_as: Vec<String> = act_as.iter().map(ToString::to_string).collect();
811 Prepared::from_response(
812 ipb::PrepareSubmissionResponse {
813 prepared_transaction: Some(ipb::PreparedTransaction {
817 transaction: None,
818 metadata: Some(ipb::Metadata {
819 submitter_info: Some(ipb::metadata::SubmitterInfo {
820 act_as: act_as.clone(),
821 command_id: "command-1".to_string(),
822 }),
823 ..Default::default()
824 }),
825 }),
826 prepared_transaction_hash: vec![1, 2, 3],
827 hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
828 hashing_details: None,
829 cost_estimation: None,
830 },
831 act_as,
832 Vec::new(),
833 "command-1".to_string(),
834 Some("user-1".to_string()),
835 )
836 .expect("a response carrying a transaction and a hash")
837 }
838
839 #[test]
842 fn a_prepare_response_without_a_hash_is_refused() {
843 let err = Prepared::from_response(
844 ipb::PrepareSubmissionResponse {
845 prepared_transaction: Some(ipb::PreparedTransaction::default()),
846 prepared_transaction_hash: Vec::new(),
847 hashing_scheme_version: 0,
848 hashing_details: None,
849 cost_estimation: None,
850 },
851 vec!["alice".to_string()],
852 Vec::new(),
853 "c".to_string(),
854 None,
855 )
856 .expect_err("no hash");
857 assert!(err.to_string().contains("no transaction hash"), "{err}");
858 }
859
860 #[test]
866 fn a_prepare_response_without_a_transaction_is_refused() {
867 let err = Prepared::from_response(
868 ipb::PrepareSubmissionResponse {
869 prepared_transaction: None,
870 prepared_transaction_hash: vec![1, 2, 3],
871 hashing_scheme_version: 0,
872 hashing_details: None,
873 cost_estimation: None,
874 },
875 vec!["alice".to_string()],
876 Vec::new(),
877 "c".to_string(),
878 None,
879 )
880 .expect_err("no transaction");
881 assert!(err.to_string().contains("nothing to execute"), "{err}");
882 }
883
884 #[tokio::test]
885 async fn signing_a_multi_party_submission_must_name_the_party() {
886 let err = prepared(&["alice", "bob"])
887 .sign_with(&signer("1220aa"))
888 .await
889 .expect_err("ambiguous");
890 assert!(err.to_string().contains("sign_as"), "{err}");
891 }
892
893 #[tokio::test]
894 async fn signing_as_a_party_that_is_not_acting_is_refused() {
895 let err = prepared(&["alice"])
896 .sign_as("mallory", &signer("1220aa"))
897 .await
898 .expect_err("not an acting party");
899 assert!(err.to_string().contains("mallory"), "{err}");
900 assert!(err.to_string().contains("alice"), "{err}");
901 }
902
903 #[tokio::test]
906 async fn a_multi_party_submission_reports_who_has_not_signed() {
907 let executable = prepared(&["alice", "bob"])
908 .sign_as("alice", &signer("1220aa"))
909 .await
910 .expect("alice signs");
911 assert_eq!(executable.signed_by(), vec!["alice"]);
912 assert_eq!(executable.unsigned_parties(), vec!["bob"]);
913
914 let executable = executable
915 .and_sign_as("bob", &signer("1220bb"))
916 .await
917 .expect("bob signs");
918 assert!(executable.unsigned_parties().is_empty());
919 assert_eq!(executable.signed_by(), vec!["alice", "bob"]);
920 }
921
922 #[tokio::test]
927 async fn several_signatures_for_one_party_share_its_entry() {
928 let executable = prepared(&["alice"])
929 .sign_as("alice", &signer("1220aa"))
930 .await
931 .expect("first key")
932 .and_sign_as("alice", &signer("1220bb"))
933 .await
934 .expect("second key");
935
936 assert_eq!(executable.signed_by(), vec!["alice"], "one entry");
937 let wire = executable.into_execute_request();
938 let signatures = wire.party_signatures.expect("signatures").signatures;
939 assert_eq!(signatures.len(), 1, "one entry per party");
940 assert_eq!(signatures[0].signatures.len(), 2, "both keys inside it");
941 assert_eq!(signatures[0].signatures[0].signed_by, "1220aa");
942 assert_eq!(signatures[0].signatures[1].signed_by, "1220bb");
943 }
944
945 #[test]
949 fn the_later_min_ledger_time_wins_in_either_order() {
950 use ipb::min_ledger_time::Time;
951 let abs_then_rel = Prepare::new("alice")
952 .with_min_ledger_time_abs(prost_types::Timestamp {
953 seconds: 42,
954 nanos: 0,
955 })
956 .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
957 .into_request()
958 .2
959 .min_ledger_time
960 .and_then(|t| t.time);
961 assert!(
962 matches!(abs_then_rel, Some(Time::MinLedgerTimeRel(_))),
963 "rel was set last: {abs_then_rel:?}"
964 );
965 }
966
967 #[tokio::test]
971 async fn the_change_id_survives_from_preparation_into_execution() {
972 let (command_id, user_id, request) = Prepare::new("alice")
973 .with_command_id("stable-id")
974 .with_user_id("user-1")
975 .into_request();
976 assert_eq!(command_id, "stable-id");
977 assert_eq!(request.command_id, "stable-id");
978 assert_eq!(user_id.as_deref(), Some("user-1"));
979
980 let executable = prepared(&["alice"])
981 .sign_as("alice", &signer("1220aa"))
982 .await
983 .expect("sign");
984 let wire = executable.into_execute_request();
985 assert_eq!(
986 wire.user_id, "user-1",
987 "the user id is part of the change ID"
988 );
989
990 let submitter = wire
995 .prepared_transaction
996 .expect("the transaction is required")
997 .metadata
998 .expect("metadata")
999 .submitter_info
1000 .expect("submitter info");
1001 assert_eq!(submitter.command_id, "command-1");
1002 assert_eq!(submitter.act_as, ["alice"]);
1003 }
1004
1005 #[test]
1008 fn an_unset_command_id_is_generated_once() {
1009 let (id, _, request) = Prepare::new("alice").into_request();
1010 assert!(!id.is_empty());
1011 assert_eq!(id, request.command_id);
1012 }
1013
1014 #[test]
1017 fn the_two_min_ledger_times_are_mutually_exclusive() {
1018 use ipb::min_ledger_time::Time;
1019
1020 let (_, _, request) = Prepare::new("alice")
1021 .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1022 .into_request();
1023 assert!(matches!(
1024 request.min_ledger_time.and_then(|t| t.time),
1025 Some(Time::MinLedgerTimeRel(_))
1026 ));
1027
1028 let (_, _, request) = Prepare::new("alice")
1029 .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1030 .with_min_ledger_time_abs(prost_types::Timestamp {
1031 seconds: 42,
1032 nanos: 0,
1033 })
1034 .into_request();
1035 let time = request.min_ledger_time.and_then(|t| t.time);
1036 assert!(
1037 matches!(time, Some(Time::MinLedgerTimeAbs(_))),
1038 "one of the two, never both: {time:?}"
1039 );
1040 }
1041
1042 #[test]
1045 fn traffic_cost_estimation_is_only_sent_when_turned_off() {
1046 let (_, _, request) = Prepare::new("alice").into_request();
1047 assert!(request.estimate_traffic_cost.is_none());
1048
1049 let (_, _, request) = Prepare::new("alice")
1050 .without_traffic_cost_estimation()
1051 .into_request();
1052 assert_eq!(
1053 request.estimate_traffic_cost.map(|h| h.disabled),
1054 Some(true)
1055 );
1056 }
1057
1058 #[tokio::test]
1061 async fn a_signature_made_elsewhere_reaches_the_wire() {
1062 let external = canton_signer::Signature::new(
1063 canton_signer::SignatureFormat::Concat,
1064 vec![8; 64],
1065 "1220cc",
1066 canton_signer::SigningAlgorithm::Ed25519,
1067 )
1068 .expect("a well-formed signature");
1069 let executable = prepared(&["alice", "bob"])
1070 .sign_as("alice", &signer("1220aa"))
1071 .await
1072 .expect("alice signs")
1073 .with_signature("bob", &external)
1074 .expect("bob's signature arrives from elsewhere");
1075
1076 let wire = executable.into_execute_and_wait_request();
1077 let signatures = wire.party_signatures.expect("signatures").signatures;
1078 let bob = signatures
1079 .iter()
1080 .find(|s| s.party == "bob")
1081 .expect("bob's entry");
1082 assert_eq!(bob.signatures[0].signature, vec![8; 64]);
1083 assert_eq!(bob.signatures[0].signed_by, "1220cc");
1084 }
1085
1086 #[tokio::test]
1087 async fn a_signature_for_a_party_that_is_not_acting_is_refused() {
1088 let external = canton_signer::Signature::new(
1089 canton_signer::SignatureFormat::Concat,
1090 vec![8; 64],
1091 "1220cc",
1092 canton_signer::SigningAlgorithm::Ed25519,
1093 )
1094 .expect("a well-formed signature");
1095 let err = prepared(&["alice"])
1096 .sign_as("alice", &signer("1220aa"))
1097 .await
1098 .expect("sign")
1099 .with_signature("mallory", &external)
1100 .expect_err("not an acting party");
1101 assert!(err.to_string().contains("mallory"), "{err}");
1102 }
1103
1104 #[tokio::test]
1107 async fn the_hashing_scheme_is_echoed_back_to_the_participant() {
1108 let prepared = prepared(&["alice"]);
1109 assert_eq!(
1110 prepared.hashing_scheme_version(),
1111 ipb::HashingSchemeVersion::V2
1112 );
1113
1114 let wire = prepared
1115 .sign_as("alice", &signer("1220aa"))
1116 .await
1117 .expect("sign")
1118 .into_execute_request();
1119 assert_eq!(
1120 wire.hashing_scheme_version,
1121 ipb::HashingSchemeVersion::V2 as i32
1122 );
1123 }
1124
1125 #[tokio::test]
1127 async fn the_signer_is_handed_the_participants_hash() {
1128 let prepared = prepared(&["alice"]);
1129 assert_eq!(prepared.hash(), &[1, 2, 3]);
1130
1131 let key = Ed25519Key::from_seed(&[4; 32]).expect("seed");
1132 let expected = key.sign_raw(&[1, 2, 3]);
1133 let wire = prepared
1134 .sign_as("alice", &signer("1220aa"))
1135 .await
1136 .expect("sign")
1137 .into_execute_request();
1138 let signatures = wire.party_signatures.expect("signatures").signatures;
1139 assert_eq!(signatures[0].signatures[0].signature, expected);
1140 }
1141
1142 #[tokio::test]
1145 async fn the_deduplication_period_reaches_every_execute_variant() {
1146 use ipb::execute_submission_request::DeduplicationPeriod;
1147
1148 let executable = prepared(&["alice"])
1149 .sign_as("alice", &signer("1220aa"))
1150 .await
1151 .expect("sign")
1152 .with_deduplication_duration(std::time::Duration::from_secs(60));
1153
1154 let wire = executable.clone().into_execute_request();
1155 assert!(matches!(
1156 wire.deduplication_period,
1157 Some(DeduplicationPeriod::DeduplicationDuration(d)) if d.seconds == 60
1158 ));
1159
1160 let wire = executable
1161 .with_deduplication_offset(99)
1162 .into_execute_and_wait_request();
1163 assert!(matches!(
1164 wire.deduplication_period,
1165 Some(
1166 ipb::execute_submission_and_wait_request::DeduplicationPeriod::DeduplicationOffset(
1167 99
1168 )
1169 )
1170 ));
1171 }
1172
1173 #[test]
1176 fn prepare_reports_what_it_was_given() {
1177 let disclosed = pb::DisclosedContract {
1178 contract_id: "00d".to_string(),
1179 ..Default::default()
1180 };
1181 let prepare = Prepare::new_multi(vec!["alice".to_string(), "bob".to_string()])
1182 .with_read_as(vec!["carol".to_string()])
1183 .with_disclosed_contracts(vec![disclosed]);
1184 assert_eq!(prepare.act_as(), ["alice".to_string(), "bob".to_string()]);
1185 assert_eq!(prepare.read_as(), ["carol".to_string()]);
1186 assert_eq!(prepare.disclosed_contracts().len(), 1);
1187 assert_eq!(prepare.disclosed_contracts()[0].contract_id, "00d");
1188 }
1189
1190 #[tokio::test]
1191 async fn prepared_and_executable_report_what_the_participant_returned() {
1192 let prepared = Prepared::from_response(
1193 ipb::PrepareSubmissionResponse {
1194 prepared_transaction: Some(ipb::PreparedTransaction {
1195 transaction: None,
1196 metadata: Some(ipb::Metadata {
1197 submitter_info: Some(ipb::metadata::SubmitterInfo {
1198 act_as: vec!["alice".to_string()],
1199 command_id: "command-7".to_string(),
1200 }),
1201 ..Default::default()
1202 }),
1203 }),
1204 prepared_transaction_hash: vec![9, 9, 9],
1205 hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
1206 hashing_details: Some("hashed thus".to_string()),
1207 cost_estimation: Some(ipb::CostEstimation {
1208 confirmation_request_traffic_cost_estimation: 1234,
1209 ..Default::default()
1210 }),
1211 },
1212 vec!["alice".to_string()],
1213 vec!["carol".to_string()],
1214 "command-7".to_string(),
1215 Some("user-1".to_string()),
1216 )
1217 .expect("a full response");
1218
1219 assert_eq!(prepared.command_id(), "command-7");
1220 assert_eq!(prepared.act_as(), ["alice".to_string()]);
1221 assert_eq!(prepared.read_as(), ["carol".to_string()]);
1222 assert_eq!(prepared.hashing_details(), Some("hashed thus"));
1223 assert_eq!(
1224 prepared
1225 .cost_estimation()
1226 .map(|c| c.confirmation_request_traffic_cost_estimation),
1227 Some(1234)
1228 );
1229 let transaction = prepared.transaction().expect("the transaction is kept");
1230 let info = transaction
1231 .metadata
1232 .as_ref()
1233 .and_then(|m| m.submitter_info.as_ref())
1234 .expect("its submitter info");
1235 assert_eq!(info.command_id, "command-7");
1236 assert_eq!(
1237 prepared.change_id(),
1238 crate::command::ChangeId::new(
1239 "user-1".to_string(),
1240 vec!["alice".to_string()],
1241 "command-7".to_string()
1242 )
1243 );
1244
1245 let executable = prepared
1246 .sign_with(&signer("1220aa"))
1247 .await
1248 .expect("one acting party signs without naming itself");
1249 assert_eq!(executable.command_id(), "command-7");
1250 assert_eq!(
1251 executable.change_id(),
1252 crate::command::ChangeId::new(
1253 "user-1".to_string(),
1254 vec!["alice".to_string()],
1255 "command-7".to_string()
1256 )
1257 );
1258 assert_eq!(executable.signed_by(), ["alice"]);
1259 let mut witnesses = executable.witnesses();
1261 witnesses.sort();
1262 assert_eq!(witnesses, ["alice".to_string(), "carol".to_string()]);
1263 }
1264
1265 #[tokio::test]
1269 async fn the_wait_for_transaction_request_carries_the_signature_and_the_format() {
1270 let executable = prepared(&["alice"])
1271 .sign_as("alice", &signer("1220aa"))
1272 .await
1273 .expect("sign")
1274 .with_submission_id("sub-1")
1275 .with_transaction_shape(crate::request::TransactionShape::LedgerEffects);
1276
1277 let wire = executable.into_execute_and_wait_for_transaction_request(pb::EventFormat {
1278 verbose: true,
1279 ..Default::default()
1280 });
1281
1282 assert!(
1283 wire.prepared_transaction.is_some(),
1284 "the transaction travels"
1285 );
1286 let signatures = wire.party_signatures.expect("signatures");
1287 assert_eq!(signatures.signatures.len(), 1);
1288 assert_eq!(signatures.signatures[0].party, "alice");
1289 assert_eq!(wire.submission_id, "sub-1");
1290 assert_eq!(wire.user_id, "user-1");
1291 assert_eq!(
1292 wire.hashing_scheme_version,
1293 ipb::HashingSchemeVersion::V2 as i32
1294 );
1295 let format = wire.transaction_format.expect("a transaction format");
1296 assert!(format.event_format.expect("event format").verbose);
1297 assert_eq!(
1298 format.transaction_shape,
1299 pb::TransactionShape::LedgerEffects as i32
1300 );
1301 }
1302}