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 trust_participant_hash: bool,
63}
64
65impl Prepare {
66 pub fn new(act_as: impl Into<String>) -> Self {
68 Self::new_multi(vec![act_as.into()])
69 }
70
71 #[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 hashing_scheme_version: Some(ipb::HashingSchemeVersion::V2 as i32),
95 taps_max_passes: None,
96 estimate_traffic_cost: true,
99 trust_participant_hash: false,
100 }
101 }
102
103 #[must_use]
107 pub fn add_command(mut self, command: pb::Command) -> Self {
108 self.commands.push(command);
109 self
110 }
111
112 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 self.min_ledger_time_rel = None;
186 self
187 }
188
189 #[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 #[must_use]
206 pub fn with_verbose_hashing(mut self, verbose: bool) -> Self {
207 self.verbose_hashing = verbose;
208 self
209 }
210
211 #[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 #[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 #[must_use]
248 pub fn trust_participant_hash(mut self) -> Self {
249 self.trust_participant_hash = true;
250 self
251 }
252
253 #[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 #[must_use]
263 pub fn without_traffic_cost_estimation(mut self) -> Self {
264 self.estimate_traffic_cost = false;
265 self
266 }
267
268 #[must_use]
270 pub fn act_as(&self) -> &[String] {
271 &self.act_as
272 }
273
274 #[must_use]
276 pub fn read_as(&self) -> &[String] {
277 &self.read_as
278 }
279
280 #[must_use]
286 pub fn disclosed_contracts(&self) -> &[pb::DisclosedContract] {
287 &self.disclosed_contracts
288 }
289
290 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 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 expected_signatures: Vec::new(),
337 },
338 ),
339 };
340 (command_id, user_id, request)
341 }
342}
343
344fn 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
356fn 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
374const PREPARED_HASH_LEN: usize = 32;
397
398#[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 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 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 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 #[must_use]
512 pub fn hash(&self) -> &[u8] {
513 &self.hash
514 }
515
516 #[must_use]
519 pub fn transaction(&self) -> Option<&ipb::PreparedTransaction> {
520 self.transaction.as_ref()
521 }
522
523 #[must_use]
525 pub fn command_id(&self) -> &str {
526 &self.command_id
527 }
528
529 #[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 #[must_use]
545 pub fn act_as(&self) -> &[String] {
546 &self.act_as
547 }
548
549 #[must_use]
556 pub fn hashing_details(&self) -> Option<&str> {
557 self.hashing_details.as_deref()
558 }
559
560 #[must_use]
563 pub fn cost_estimation(&self) -> Option<&ipb::CostEstimation> {
564 self.cost_estimation.as_ref()
565 }
566
567 #[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 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 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 #[must_use]
630 pub fn read_as(&self) -> &[String] {
631 &self.read_as
632 }
633}
634
635#[derive(Clone, Copy, Debug)]
640enum Deduplication {
641 Duration(std::time::Duration),
642 Offset(i64),
643}
644
645#[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 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 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 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 #[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 #[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 #[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 #[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 #[must_use]
758 pub fn change_id(&self) -> crate::command::ChangeId {
759 self.prepared.change_id()
760 }
761
762 #[must_use]
768 pub fn command_id(&self) -> &str {
769 &self.prepared.command_id
770 }
771
772 #[must_use]
774 pub fn signed_by(&self) -> Vec<&str> {
775 self.signatures.iter().map(|s| s.party.as_str()).collect()
776 }
777
778 #[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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 let mut witnesses = executable.witnesses();
1548 witnesses.sort();
1549 assert_eq!(witnesses, ["alice".to_string(), "carol".to_string()]);
1550 }
1551
1552 #[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}