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: Some(ipb::HashingSchemeVersion::V2 as i32),
94 taps_max_passes: None,
95 estimate_traffic_cost: true,
98 }
99 }
100
101 #[must_use]
105 pub fn add_command(mut self, command: pb::Command) -> Self {
106 self.commands.push(command);
107 self
108 }
109
110 #[must_use]
112 pub fn add_commands(mut self, commands: impl IntoIterator<Item = pb::Command>) -> Self {
113 self.commands.extend(commands);
114 self
115 }
116
117 #[must_use]
124 pub fn with_command_id(mut self, command_id: impl Into<String>) -> Self {
125 self.command_id = Some(command_id.into());
126 self
127 }
128
129 #[must_use]
131 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
132 self.user_id = Some(user_id.into());
133 self
134 }
135
136 #[must_use]
138 pub fn with_read_as(mut self, read_as: Vec<String>) -> Self {
139 self.read_as = read_as;
140 self
141 }
142
143 #[must_use]
145 pub fn with_synchronizer_id(mut self, synchronizer_id: impl Into<String>) -> Self {
146 self.synchronizer_id = Some(synchronizer_id.into());
147 self
148 }
149
150 #[must_use]
153 pub fn with_disclosed_contracts(mut self, contracts: Vec<pb::DisclosedContract>) -> Self {
154 self.disclosed_contracts = contracts;
155 self
156 }
157
158 #[must_use]
160 pub fn with_package_id_selection_preference(mut self, package_ids: Vec<String>) -> Self {
161 self.package_id_selection_preference = package_ids;
162 self
163 }
164
165 #[must_use]
168 pub fn with_prefetch_contract_keys(mut self, keys: Vec<pb::PrefetchContractKey>) -> Self {
169 self.prefetch_contract_keys = keys;
170 self
171 }
172
173 #[must_use]
178 pub fn with_min_ledger_time_abs(mut self, time: prost_types::Timestamp) -> Self {
179 self.min_ledger_time_abs = Some(time);
180 self.min_ledger_time_rel = None;
184 self
185 }
186
187 #[must_use]
192 pub fn with_min_ledger_time_rel(mut self, duration: std::time::Duration) -> Self {
193 self.min_ledger_time_rel = Some(duration);
194 self.min_ledger_time_abs = None;
195 self
196 }
197
198 #[must_use]
204 pub fn with_verbose_hashing(mut self, verbose: bool) -> Self {
205 self.verbose_hashing = verbose;
206 self
207 }
208
209 #[must_use]
216 pub fn with_max_record_time(mut self, time: prost_types::Timestamp) -> Self {
217 self.max_record_time = Some(time);
218 self
219 }
220
221 #[must_use]
227 pub fn with_hashing_scheme_version(mut self, version: ipb::HashingSchemeVersion) -> Self {
228 self.hashing_scheme_version = Some(version as i32);
229 self
230 }
231
232 #[must_use]
235 pub fn with_taps_max_passes(mut self, passes: u32) -> Self {
236 self.taps_max_passes = Some(passes);
237 self
238 }
239
240 #[must_use]
242 pub fn without_traffic_cost_estimation(mut self) -> Self {
243 self.estimate_traffic_cost = false;
244 self
245 }
246
247 #[must_use]
249 pub fn act_as(&self) -> &[String] {
250 &self.act_as
251 }
252
253 #[must_use]
255 pub fn read_as(&self) -> &[String] {
256 &self.read_as
257 }
258
259 #[must_use]
265 pub fn disclosed_contracts(&self) -> &[pb::DisclosedContract] {
266 &self.disclosed_contracts
267 }
268
269 pub(crate) fn requested_hashing_scheme(&self) -> Option<ipb::HashingSchemeVersion> {
280 self.hashing_scheme_version
281 .and_then(|v| ipb::HashingSchemeVersion::try_from(v).ok())
282 }
283
284 pub(crate) fn into_request(self) -> (String, Option<String>, ipb::PrepareSubmissionRequest) {
285 let command_id = self
286 .command_id
287 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
288 let user_id = self.user_id;
289 let min_ledger_time = min_ledger_time(self.min_ledger_time_abs, self.min_ledger_time_rel);
290 let request = ipb::PrepareSubmissionRequest {
291 user_id: user_id.clone().unwrap_or_default(),
292 command_id: command_id.clone(),
293 commands: self.commands,
294 min_ledger_time,
295 act_as: self.act_as,
296 read_as: self.read_as,
297 disclosed_contracts: self.disclosed_contracts,
298 synchronizer_id: self.synchronizer_id.unwrap_or_default(),
299 package_id_selection_preference: self.package_id_selection_preference,
300 verbose_hashing: self.verbose_hashing,
301 prefetch_contract_keys: self.prefetch_contract_keys,
302 max_record_time: self.max_record_time,
303 hashing_scheme_version: self.hashing_scheme_version,
304 taps_max_passes: self.taps_max_passes,
305 estimate_traffic_cost: (!self.estimate_traffic_cost).then_some(
306 ipb::CostEstimationHints {
307 disabled: true,
308 expected_signatures: Vec::new(),
310 },
311 ),
312 };
313 (command_id, user_id, request)
314 }
315}
316
317fn duration(value: std::time::Duration) -> prost_types::Duration {
323 prost_types::Duration {
324 seconds: i64::try_from(value.as_secs()).unwrap_or(i64::MAX),
325 nanos: i32::try_from(value.subsec_nanos()).unwrap_or(0),
326 }
327}
328
329fn min_ledger_time(
335 abs: Option<prost_types::Timestamp>,
336 rel: Option<std::time::Duration>,
337) -> Option<ipb::MinLedgerTime> {
338 use ipb::min_ledger_time::Time;
339 let time = match (abs, rel) {
340 (Some(abs), _) => Time::MinLedgerTimeAbs(abs),
341 (None, Some(rel)) => Time::MinLedgerTimeRel(duration(rel)),
342 (None, None) => return None,
343 };
344 Some(ipb::MinLedgerTime { time: Some(time) })
345}
346
347const PREPARED_HASH_LEN: usize = 32;
370
371#[derive(Clone, Debug)]
374pub struct Prepared {
375 transaction: Option<ipb::PreparedTransaction>,
376 hash: Vec<u8>,
377 hashing_scheme_version: i32,
378 hashing_details: Option<String>,
379 cost_estimation: Option<ipb::CostEstimation>,
380 act_as: Vec<String>,
381 read_as: Vec<String>,
382 command_id: String,
383 user_id: Option<String>,
384}
385
386impl Prepared {
387 pub(crate) fn from_response(
388 response: ipb::PrepareSubmissionResponse,
389 requested_scheme: Option<ipb::HashingSchemeVersion>,
390 act_as: Vec<String>,
391 read_as: Vec<String>,
392 command_id: String,
393 user_id: Option<String>,
394 ) -> Result<Self> {
395 let Some(transaction) = response.prepared_transaction.as_ref() else {
401 return Err(Error::UnexpectedResponse(
402 "prepare returned no prepared transaction, so there is nothing to execute"
403 .to_string(),
404 ));
405 };
406 if response.prepared_transaction_hash.is_empty() {
407 return Err(Error::UnexpectedResponse(
408 "prepare returned no transaction hash".to_string(),
409 ));
410 }
411 let scheme = ipb::HashingSchemeVersion::try_from(response.hashing_scheme_version)
417 .ok()
418 .filter(|s| *s != ipb::HashingSchemeVersion::Unspecified)
419 .ok_or_else(|| {
420 Error::UnexpectedResponse(format!(
421 "prepare answered under an unknown hashing scheme ({})",
422 response.hashing_scheme_version
423 ))
424 })?;
425 if let Some(requested) = requested_scheme.filter(|requested| *requested != scheme) {
426 return Err(Error::UnexpectedResponse(format!(
427 "prepare hashed under {scheme:?} but {requested:?} was requested"
428 )));
429 }
430 if response.prepared_transaction_hash.len() != PREPARED_HASH_LEN {
431 return Err(Error::UnexpectedResponse(format!(
432 "prepare returned a {}-byte transaction hash; {scheme:?} hashes are {PREPARED_HASH_LEN} bytes",
433 response.prepared_transaction_hash.len()
434 )));
435 }
436 if scheme == ipb::HashingSchemeVersion::V2 {
442 let recomputed = crate::hashing::hash_prepared_transaction_v2(transaction)
443 .map_err(|e| {
444 Error::UnexpectedResponse(format!(
445 "the prepared transaction cannot be hashed, so its hash cannot be checked: {e}"
446 ))
447 })?;
448 if recomputed[..] != response.prepared_transaction_hash[..] {
449 return Err(Error::UnexpectedResponse(
450 "the participant's transaction hash is not the hash of the transaction it \
451 returned; refusing to sign it"
452 .to_string(),
453 ));
454 }
455 } else {
456 tracing::warn!(
457 ?scheme,
458 "hashing scheme requested explicitly; the participant's hash is signed as returned"
459 );
460 }
461 Ok(Self {
462 transaction: response.prepared_transaction,
463 hash: response.prepared_transaction_hash,
464 hashing_scheme_version: response.hashing_scheme_version,
465 hashing_details: response.hashing_details,
466 cost_estimation: response.cost_estimation,
467 act_as,
468 read_as,
469 command_id,
470 user_id,
471 })
472 }
473
474 #[must_use]
476 pub fn hash(&self) -> &[u8] {
477 &self.hash
478 }
479
480 #[must_use]
483 pub fn transaction(&self) -> Option<&ipb::PreparedTransaction> {
484 self.transaction.as_ref()
485 }
486
487 #[must_use]
489 pub fn command_id(&self) -> &str {
490 &self.command_id
491 }
492
493 #[must_use]
499 pub fn change_id(&self) -> crate::command::ChangeId {
500 crate::command::ChangeId::new(
501 self.user_id.clone().unwrap_or_default(),
502 self.act_as.clone(),
503 self.command_id.clone(),
504 )
505 }
506
507 #[must_use]
509 pub fn act_as(&self) -> &[String] {
510 &self.act_as
511 }
512
513 #[must_use]
520 pub fn hashing_details(&self) -> Option<&str> {
521 self.hashing_details.as_deref()
522 }
523
524 #[must_use]
527 pub fn cost_estimation(&self) -> Option<&ipb::CostEstimation> {
528 self.cost_estimation.as_ref()
529 }
530
531 #[must_use]
534 pub fn hashing_scheme_version(&self) -> ipb::HashingSchemeVersion {
535 ipb::HashingSchemeVersion::try_from(self.hashing_scheme_version)
536 .unwrap_or(ipb::HashingSchemeVersion::Unspecified)
537 }
538
539 pub async fn sign_with(self, signer: &dyn Signer) -> Result<Executable> {
546 let [party] = self.act_as.as_slice() else {
547 return Err(Error::InvalidRequest(format!(
548 "this submission acts as {} parties, so a signature must name \
549 one: use `sign_as`",
550 self.act_as.len()
551 )));
552 };
553 let party = party.clone();
554 self.sign_as(&party, signer).await
555 }
556
557 pub async fn sign_as(self, party: &str, signer: &dyn Signer) -> Result<Executable> {
564 let signature = self.sign_one(party, signer).await?;
565 Ok(Executable {
566 prepared: self,
567 signatures: vec![signature],
568 submission_id: None,
569 transaction_shape: crate::request::TransactionShape::default(),
570 deduplication: None,
571 })
572 }
573
574 async fn sign_one(
575 &self,
576 party: &str,
577 signer: &dyn Signer,
578 ) -> Result<ipb::SinglePartySignatures> {
579 if !self.act_as.iter().any(|p| p == party) {
580 return Err(Error::InvalidRequest(format!(
581 "`{party}` is not among the parties this submission acts as ({})",
582 self.act_as.join(", ")
583 )));
584 }
585 let signature = signer.sign(&self.hash).await?;
586 Ok(ipb::SinglePartySignatures {
587 party: party.to_string(),
588 signatures: vec![pb::Signature::from(&signature)],
589 })
590 }
591
592 #[must_use]
594 pub fn read_as(&self) -> &[String] {
595 &self.read_as
596 }
597}
598
599#[derive(Clone, Copy, Debug)]
604enum Deduplication {
605 Duration(std::time::Duration),
606 Offset(i64),
607}
608
609#[derive(Clone, Debug)]
611pub struct Executable {
612 prepared: Prepared,
613 signatures: Vec<ipb::SinglePartySignatures>,
614 submission_id: Option<String>,
615 transaction_shape: crate::request::TransactionShape,
616 deduplication: Option<Deduplication>,
617}
618
619impl Executable {
620 pub async fn and_sign_as(mut self, party: &str, signer: &dyn Signer) -> Result<Self> {
626 let signature = self.prepared.sign_one(party, signer).await?;
627 self.add(signature);
628 Ok(self)
629 }
630
631 fn add(&mut self, signature: ipb::SinglePartySignatures) {
639 if let Some(existing) = self
640 .signatures
641 .iter_mut()
642 .find(|entry| entry.party == signature.party)
643 {
644 existing.signatures.extend(signature.signatures);
645 } else {
646 self.signatures.push(signature);
647 }
648 }
649
650 pub fn with_signature(
657 mut self,
658 party: &str,
659 signature: &canton_signer::Signature,
660 ) -> Result<Self> {
661 if !self.prepared.act_as.iter().any(|p| p == party) {
662 return Err(Error::InvalidRequest(format!(
663 "`{party}` is not among the parties this submission acts as ({})",
664 self.prepared.act_as.join(", ")
665 )));
666 }
667 self.add(ipb::SinglePartySignatures {
668 party: party.to_string(),
669 signatures: vec![pb::Signature::from(signature)],
670 });
671 Ok(self)
672 }
673
674 #[must_use]
681 pub fn with_deduplication_duration(mut self, duration: std::time::Duration) -> Self {
682 self.deduplication = Some(Deduplication::Duration(duration));
683 self
684 }
685
686 #[must_use]
689 pub fn with_deduplication_offset(mut self, offset: i64) -> Self {
690 self.deduplication = Some(Deduplication::Offset(offset));
691 self
692 }
693
694 #[must_use]
701 pub fn with_submission_id(mut self, submission_id: impl Into<String>) -> Self {
702 self.submission_id = Some(submission_id.into());
703 self
704 }
705
706 #[must_use]
712 pub fn with_transaction_shape(mut self, shape: crate::request::TransactionShape) -> Self {
713 self.transaction_shape = shape;
714 self
715 }
716
717 #[must_use]
722 pub fn change_id(&self) -> crate::command::ChangeId {
723 self.prepared.change_id()
724 }
725
726 #[must_use]
732 pub fn command_id(&self) -> &str {
733 &self.prepared.command_id
734 }
735
736 #[must_use]
738 pub fn signed_by(&self) -> Vec<&str> {
739 self.signatures.iter().map(|s| s.party.as_str()).collect()
740 }
741
742 #[must_use]
747 pub fn unsigned_parties(&self) -> Vec<&str> {
748 self.prepared
749 .act_as
750 .iter()
751 .filter(|party| !self.signatures.iter().any(|s| &&s.party == party))
752 .map(String::as_str)
753 .collect()
754 }
755
756 fn parts(
758 self,
759 ) -> (
760 Option<ipb::PreparedTransaction>,
761 Option<ipb::PartySignatures>,
762 String,
763 String,
764 i32,
765 crate::request::TransactionShape,
766 Option<Deduplication>,
767 ) {
768 let submission_id = self.submission_id.unwrap_or_default();
774 (
775 self.prepared.transaction,
776 Some(ipb::PartySignatures {
777 signatures: self.signatures,
778 }),
779 submission_id,
780 self.prepared.user_id.unwrap_or_default(),
781 self.prepared.hashing_scheme_version,
782 self.transaction_shape,
783 self.deduplication,
784 )
785 }
786
787 pub(crate) fn into_execute_request(self) -> ipb::ExecuteSubmissionRequest {
788 use ipb::execute_submission_request::DeduplicationPeriod as P;
789 let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
790 self.parts();
791 ipb::ExecuteSubmissionRequest {
792 prepared_transaction: transaction,
793 party_signatures,
794 deduplication_period: dedup.map(|d| match d {
795 Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
796 Deduplication::Offset(o) => P::DeduplicationOffset(o),
797 }),
798 submission_id,
799 user_id,
800 hashing_scheme_version: hashing,
801 min_ledger_time: None,
802 }
803 }
804
805 pub(crate) fn into_execute_and_wait_request(self) -> ipb::ExecuteSubmissionAndWaitRequest {
806 use ipb::execute_submission_and_wait_request::DeduplicationPeriod as P;
807 let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
808 self.parts();
809 ipb::ExecuteSubmissionAndWaitRequest {
810 prepared_transaction: transaction,
811 party_signatures,
812 deduplication_period: dedup.map(|d| match d {
813 Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
814 Deduplication::Offset(o) => P::DeduplicationOffset(o),
815 }),
816 submission_id,
817 user_id,
818 hashing_scheme_version: hashing,
819 min_ledger_time: None,
820 }
821 }
822
823 pub(crate) fn into_execute_and_wait_for_transaction_request(
824 self,
825 event_format: pb::EventFormat,
826 ) -> ipb::ExecuteSubmissionAndWaitForTransactionRequest {
827 use ipb::execute_submission_and_wait_for_transaction_request::DeduplicationPeriod as P;
828 let (transaction, party_signatures, submission_id, user_id, hashing, shape, dedup) =
829 self.parts();
830 ipb::ExecuteSubmissionAndWaitForTransactionRequest {
831 prepared_transaction: transaction,
832 party_signatures,
833 deduplication_period: dedup.map(|d| match d {
834 Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
835 Deduplication::Offset(o) => P::DeduplicationOffset(o),
836 }),
837 submission_id,
838 user_id,
839 hashing_scheme_version: hashing,
840 min_ledger_time: None,
841 transaction_format: Some(pb::TransactionFormat {
842 event_format: Some(event_format),
843 transaction_shape: shape.as_grpc() as i32,
844 }),
845 }
846 }
847
848 pub(crate) fn witnesses(&self) -> Vec<String> {
854 let mut parties = self.prepared.act_as.clone();
855 parties.extend(self.prepared.read_as.iter().cloned());
856 parties.sort_unstable();
857 parties.dedup();
858 parties
859 }
860}
861
862#[cfg(test)]
863#[allow(clippy::unwrap_used, clippy::expect_used)]
864mod tests {
865 use super::*;
866 use canton_signer::Ed25519Key;
867
868 fn signer(fingerprint: &str) -> canton_signer::Ed25519Signer {
869 Ed25519Key::from_seed(&[4; 32])
870 .expect("a valid seed")
871 .into_signer(fingerprint)
872 }
873
874 fn hashable(act_as: &[String], command_id: &str) -> (ipb::PreparedTransaction, Vec<u8>) {
879 let transaction = ipb::PreparedTransaction {
880 transaction: Some(ipb::DamlTransaction {
881 version: "2.1".to_string(),
882 ..Default::default()
883 }),
884 metadata: Some(ipb::Metadata {
885 submitter_info: Some(ipb::metadata::SubmitterInfo {
886 act_as: act_as.to_vec(),
887 command_id: command_id.to_string(),
888 }),
889 ..Default::default()
890 }),
891 };
892 let hash = crate::hashing::hash_prepared_transaction_v2(&transaction)
893 .expect("hashable")
894 .to_vec();
895 (transaction, hash)
896 }
897
898 fn prepared(act_as: &[&str]) -> Prepared {
899 let act_as: Vec<String> = act_as.iter().map(ToString::to_string).collect();
900 let (transaction, hash) = hashable(&act_as, "command-1");
901 Prepared::from_response(
902 ipb::PrepareSubmissionResponse {
903 prepared_transaction: Some(transaction),
904 prepared_transaction_hash: hash,
905 hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
906 hashing_details: None,
907 cost_estimation: None,
908 },
909 Some(ipb::HashingSchemeVersion::V2),
910 act_as,
911 Vec::new(),
912 "command-1".to_string(),
913 Some("user-1".to_string()),
914 )
915 .expect("a response carrying a transaction and a hash")
916 }
917
918 #[test]
921 fn a_prepare_response_without_a_hash_is_refused() {
922 let err = Prepared::from_response(
923 ipb::PrepareSubmissionResponse {
924 prepared_transaction: Some(ipb::PreparedTransaction::default()),
925 prepared_transaction_hash: Vec::new(),
926 hashing_scheme_version: 0,
927 hashing_details: None,
928 cost_estimation: None,
929 },
930 None,
931 vec!["alice".to_string()],
932 Vec::new(),
933 "c".to_string(),
934 None,
935 )
936 .expect_err("no hash");
937 assert!(err.to_string().contains("no transaction hash"), "{err}");
938 }
939
940 #[test]
946 fn a_prepare_response_without_a_transaction_is_refused() {
947 let err = Prepared::from_response(
948 ipb::PrepareSubmissionResponse {
949 prepared_transaction: None,
950 prepared_transaction_hash: vec![1, 2, 3],
951 hashing_scheme_version: 0,
952 hashing_details: None,
953 cost_estimation: None,
954 },
955 None,
956 vec!["alice".to_string()],
957 Vec::new(),
958 "c".to_string(),
959 None,
960 )
961 .expect_err("no transaction");
962 assert!(err.to_string().contains("nothing to execute"), "{err}");
963 }
964
965 fn response_with(hash: Vec<u8>, scheme: i32) -> ipb::PrepareSubmissionResponse {
966 let (transaction, _) = hashable(&["alice".to_string()], "command-1");
967 ipb::PrepareSubmissionResponse {
968 prepared_transaction: Some(transaction),
969 prepared_transaction_hash: hash,
970 hashing_scheme_version: scheme,
971 hashing_details: None,
972 cost_estimation: None,
973 }
974 }
975
976 fn refused(
977 response: ipb::PrepareSubmissionResponse,
978 requested: Option<ipb::HashingSchemeVersion>,
979 ) -> String {
980 let err = Prepared::from_response(
981 response,
982 requested,
983 vec!["alice".to_string()],
984 Vec::new(),
985 "command-1".to_string(),
986 None,
987 )
988 .expect_err("refused");
989 assert!(matches!(err, Error::UnexpectedResponse(_)), "{err:?}");
990 err.to_string()
991 }
992
993 #[test]
994 fn a_hash_under_an_unknown_scheme_is_refused() {
995 let message = refused(response_with(vec![1; 32], 0), None);
996 assert!(message.contains("unknown hashing scheme"), "{message}");
997 let message = refused(response_with(vec![1; 32], 99), None);
998 assert!(message.contains("unknown hashing scheme (99)"), "{message}");
999 }
1000
1001 #[test]
1002 fn a_hash_under_a_scheme_that_was_not_requested_is_refused() {
1003 let message = refused(
1004 response_with(vec![1; 32], ipb::HashingSchemeVersion::V3 as i32),
1005 Some(ipb::HashingSchemeVersion::V2),
1006 );
1007 assert!(
1008 message.contains("V3") && message.contains("V2 was requested"),
1009 "{message}"
1010 );
1011 }
1012
1013 #[test]
1014 fn a_hash_of_the_wrong_length_is_refused() {
1015 for len in [1usize, 31, 33, 64] {
1016 let message = refused(
1017 response_with(vec![7; len], ipb::HashingSchemeVersion::V2 as i32),
1018 Some(ipb::HashingSchemeVersion::V2),
1019 );
1020 assert!(
1021 message.contains(&format!("{len}-byte transaction hash")),
1022 "{message}"
1023 );
1024 }
1025 }
1026
1027 #[test]
1030 fn a_hash_that_is_not_the_transactions_own_is_refused() {
1031 let (_, mut hash) = hashable(&["alice".to_string()], "command-1");
1032 hash[0] ^= 0xff;
1033 let message = refused(
1034 response_with(hash, ipb::HashingSchemeVersion::V2 as i32),
1035 Some(ipb::HashingSchemeVersion::V2),
1036 );
1037 assert!(
1038 message.contains("not the hash of the transaction"),
1039 "{message}"
1040 );
1041 }
1042
1043 #[test]
1044 fn the_scheme_asked_for_defaults_to_v2() {
1045 let prepare = Prepare::new("alice");
1046 assert_eq!(
1047 prepare.requested_hashing_scheme(),
1048 Some(ipb::HashingSchemeVersion::V2)
1049 );
1050 let prepare = prepare.with_hashing_scheme_version(ipb::HashingSchemeVersion::V3);
1051 assert_eq!(
1052 prepare.requested_hashing_scheme(),
1053 Some(ipb::HashingSchemeVersion::V3)
1054 );
1055 }
1056
1057 #[tokio::test]
1058 async fn signing_a_multi_party_submission_must_name_the_party() {
1059 let err = prepared(&["alice", "bob"])
1060 .sign_with(&signer("1220aa"))
1061 .await
1062 .expect_err("ambiguous");
1063 assert!(err.to_string().contains("sign_as"), "{err}");
1064 }
1065
1066 #[tokio::test]
1067 async fn signing_as_a_party_that_is_not_acting_is_refused() {
1068 let err = prepared(&["alice"])
1069 .sign_as("mallory", &signer("1220aa"))
1070 .await
1071 .expect_err("not an acting party");
1072 assert!(err.to_string().contains("mallory"), "{err}");
1073 assert!(err.to_string().contains("alice"), "{err}");
1074 }
1075
1076 #[tokio::test]
1079 async fn a_multi_party_submission_reports_who_has_not_signed() {
1080 let executable = prepared(&["alice", "bob"])
1081 .sign_as("alice", &signer("1220aa"))
1082 .await
1083 .expect("alice signs");
1084 assert_eq!(executable.signed_by(), vec!["alice"]);
1085 assert_eq!(executable.unsigned_parties(), vec!["bob"]);
1086
1087 let executable = executable
1088 .and_sign_as("bob", &signer("1220bb"))
1089 .await
1090 .expect("bob signs");
1091 assert!(executable.unsigned_parties().is_empty());
1092 assert_eq!(executable.signed_by(), vec!["alice", "bob"]);
1093 }
1094
1095 #[tokio::test]
1100 async fn several_signatures_for_one_party_share_its_entry() {
1101 let executable = prepared(&["alice"])
1102 .sign_as("alice", &signer("1220aa"))
1103 .await
1104 .expect("first key")
1105 .and_sign_as("alice", &signer("1220bb"))
1106 .await
1107 .expect("second key");
1108
1109 assert_eq!(executable.signed_by(), vec!["alice"], "one entry");
1110 let wire = executable.into_execute_request();
1111 let signatures = wire.party_signatures.expect("signatures").signatures;
1112 assert_eq!(signatures.len(), 1, "one entry per party");
1113 assert_eq!(signatures[0].signatures.len(), 2, "both keys inside it");
1114 assert_eq!(signatures[0].signatures[0].signed_by, "1220aa");
1115 assert_eq!(signatures[0].signatures[1].signed_by, "1220bb");
1116 }
1117
1118 #[test]
1122 fn the_later_min_ledger_time_wins_in_either_order() {
1123 use ipb::min_ledger_time::Time;
1124 let abs_then_rel = Prepare::new("alice")
1125 .with_min_ledger_time_abs(prost_types::Timestamp {
1126 seconds: 42,
1127 nanos: 0,
1128 })
1129 .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1130 .into_request()
1131 .2
1132 .min_ledger_time
1133 .and_then(|t| t.time);
1134 assert!(
1135 matches!(abs_then_rel, Some(Time::MinLedgerTimeRel(_))),
1136 "rel was set last: {abs_then_rel:?}"
1137 );
1138 }
1139
1140 #[tokio::test]
1144 async fn the_change_id_survives_from_preparation_into_execution() {
1145 let (command_id, user_id, request) = Prepare::new("alice")
1146 .with_command_id("stable-id")
1147 .with_user_id("user-1")
1148 .into_request();
1149 assert_eq!(command_id, "stable-id");
1150 assert_eq!(request.command_id, "stable-id");
1151 assert_eq!(user_id.as_deref(), Some("user-1"));
1152
1153 let executable = prepared(&["alice"])
1154 .sign_as("alice", &signer("1220aa"))
1155 .await
1156 .expect("sign");
1157 let wire = executable.into_execute_request();
1158 assert_eq!(
1159 wire.user_id, "user-1",
1160 "the user id is part of the change ID"
1161 );
1162
1163 let submitter = wire
1168 .prepared_transaction
1169 .expect("the transaction is required")
1170 .metadata
1171 .expect("metadata")
1172 .submitter_info
1173 .expect("submitter info");
1174 assert_eq!(submitter.command_id, "command-1");
1175 assert_eq!(submitter.act_as, ["alice"]);
1176 }
1177
1178 #[test]
1181 fn an_unset_command_id_is_generated_once() {
1182 let (id, _, request) = Prepare::new("alice").into_request();
1183 assert!(!id.is_empty());
1184 assert_eq!(id, request.command_id);
1185 }
1186
1187 #[test]
1190 fn the_two_min_ledger_times_are_mutually_exclusive() {
1191 use ipb::min_ledger_time::Time;
1192
1193 let (_, _, request) = Prepare::new("alice")
1194 .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1195 .into_request();
1196 assert!(matches!(
1197 request.min_ledger_time.and_then(|t| t.time),
1198 Some(Time::MinLedgerTimeRel(_))
1199 ));
1200
1201 let (_, _, request) = Prepare::new("alice")
1202 .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1203 .with_min_ledger_time_abs(prost_types::Timestamp {
1204 seconds: 42,
1205 nanos: 0,
1206 })
1207 .into_request();
1208 let time = request.min_ledger_time.and_then(|t| t.time);
1209 assert!(
1210 matches!(time, Some(Time::MinLedgerTimeAbs(_))),
1211 "one of the two, never both: {time:?}"
1212 );
1213 }
1214
1215 #[test]
1218 fn traffic_cost_estimation_is_only_sent_when_turned_off() {
1219 let (_, _, request) = Prepare::new("alice").into_request();
1220 assert!(request.estimate_traffic_cost.is_none());
1221
1222 let (_, _, request) = Prepare::new("alice")
1223 .without_traffic_cost_estimation()
1224 .into_request();
1225 assert_eq!(
1226 request.estimate_traffic_cost.map(|h| h.disabled),
1227 Some(true)
1228 );
1229 }
1230
1231 #[tokio::test]
1234 async fn a_signature_made_elsewhere_reaches_the_wire() {
1235 let external = canton_signer::Signature::new(
1236 canton_signer::SignatureFormat::Concat,
1237 vec![8; 64],
1238 "1220cc",
1239 canton_signer::SigningAlgorithm::Ed25519,
1240 )
1241 .expect("a well-formed signature");
1242 let executable = prepared(&["alice", "bob"])
1243 .sign_as("alice", &signer("1220aa"))
1244 .await
1245 .expect("alice signs")
1246 .with_signature("bob", &external)
1247 .expect("bob's signature arrives from elsewhere");
1248
1249 let wire = executable.into_execute_and_wait_request();
1250 let signatures = wire.party_signatures.expect("signatures").signatures;
1251 let bob = signatures
1252 .iter()
1253 .find(|s| s.party == "bob")
1254 .expect("bob's entry");
1255 assert_eq!(bob.signatures[0].signature, vec![8; 64]);
1256 assert_eq!(bob.signatures[0].signed_by, "1220cc");
1257 }
1258
1259 #[tokio::test]
1260 async fn a_signature_for_a_party_that_is_not_acting_is_refused() {
1261 let external = canton_signer::Signature::new(
1262 canton_signer::SignatureFormat::Concat,
1263 vec![8; 64],
1264 "1220cc",
1265 canton_signer::SigningAlgorithm::Ed25519,
1266 )
1267 .expect("a well-formed signature");
1268 let err = prepared(&["alice"])
1269 .sign_as("alice", &signer("1220aa"))
1270 .await
1271 .expect("sign")
1272 .with_signature("mallory", &external)
1273 .expect_err("not an acting party");
1274 assert!(err.to_string().contains("mallory"), "{err}");
1275 }
1276
1277 #[tokio::test]
1280 async fn the_hashing_scheme_is_echoed_back_to_the_participant() {
1281 let prepared = prepared(&["alice"]);
1282 assert_eq!(
1283 prepared.hashing_scheme_version(),
1284 ipb::HashingSchemeVersion::V2
1285 );
1286
1287 let wire = prepared
1288 .sign_as("alice", &signer("1220aa"))
1289 .await
1290 .expect("sign")
1291 .into_execute_request();
1292 assert_eq!(
1293 wire.hashing_scheme_version,
1294 ipb::HashingSchemeVersion::V2 as i32
1295 );
1296 }
1297
1298 #[tokio::test]
1300 async fn the_signer_is_handed_the_participants_hash() {
1301 let prepared = prepared(&["alice"]);
1302 let (_, hash) = hashable(&["alice".to_string()], "command-1");
1303 assert_eq!(prepared.hash(), &hash[..]);
1304
1305 let key = Ed25519Key::from_seed(&[4; 32]).expect("seed");
1306 let expected = key.sign_raw(&hash);
1307 let wire = prepared
1308 .sign_as("alice", &signer("1220aa"))
1309 .await
1310 .expect("sign")
1311 .into_execute_request();
1312 let signatures = wire.party_signatures.expect("signatures").signatures;
1313 assert_eq!(signatures[0].signatures[0].signature, expected);
1314 }
1315
1316 #[tokio::test]
1319 async fn the_deduplication_period_reaches_every_execute_variant() {
1320 use ipb::execute_submission_request::DeduplicationPeriod;
1321
1322 let executable = prepared(&["alice"])
1323 .sign_as("alice", &signer("1220aa"))
1324 .await
1325 .expect("sign")
1326 .with_deduplication_duration(std::time::Duration::from_secs(60));
1327
1328 let wire = executable.clone().into_execute_request();
1329 assert!(matches!(
1330 wire.deduplication_period,
1331 Some(DeduplicationPeriod::DeduplicationDuration(d)) if d.seconds == 60
1332 ));
1333
1334 let wire = executable
1335 .with_deduplication_offset(99)
1336 .into_execute_and_wait_request();
1337 assert!(matches!(
1338 wire.deduplication_period,
1339 Some(
1340 ipb::execute_submission_and_wait_request::DeduplicationPeriod::DeduplicationOffset(
1341 99
1342 )
1343 )
1344 ));
1345 }
1346
1347 #[test]
1350 fn prepare_reports_what_it_was_given() {
1351 let disclosed = pb::DisclosedContract {
1352 contract_id: "00d".to_string(),
1353 ..Default::default()
1354 };
1355 let prepare = Prepare::new_multi(vec!["alice".to_string(), "bob".to_string()])
1356 .with_read_as(vec!["carol".to_string()])
1357 .with_disclosed_contracts(vec![disclosed]);
1358 assert_eq!(prepare.act_as(), ["alice".to_string(), "bob".to_string()]);
1359 assert_eq!(prepare.read_as(), ["carol".to_string()]);
1360 assert_eq!(prepare.disclosed_contracts().len(), 1);
1361 assert_eq!(prepare.disclosed_contracts()[0].contract_id, "00d");
1362 }
1363
1364 #[tokio::test]
1365 async fn prepared_and_executable_report_what_the_participant_returned() {
1366 let prepared = Prepared::from_response(
1367 ipb::PrepareSubmissionResponse {
1368 prepared_transaction: Some(hashable(&["alice".to_string()], "command-7").0),
1369 prepared_transaction_hash: hashable(&["alice".to_string()], "command-7").1,
1370 hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
1371 hashing_details: Some("hashed thus".to_string()),
1372 cost_estimation: Some(ipb::CostEstimation {
1373 confirmation_request_traffic_cost_estimation: 1234,
1374 ..Default::default()
1375 }),
1376 },
1377 Some(ipb::HashingSchemeVersion::V2),
1378 vec!["alice".to_string()],
1379 vec!["carol".to_string()],
1380 "command-7".to_string(),
1381 Some("user-1".to_string()),
1382 )
1383 .expect("a full response");
1384
1385 assert_eq!(prepared.command_id(), "command-7");
1386 assert_eq!(prepared.act_as(), ["alice".to_string()]);
1387 assert_eq!(prepared.read_as(), ["carol".to_string()]);
1388 assert_eq!(prepared.hashing_details(), Some("hashed thus"));
1389 assert_eq!(
1390 prepared
1391 .cost_estimation()
1392 .map(|c| c.confirmation_request_traffic_cost_estimation),
1393 Some(1234)
1394 );
1395 let transaction = prepared.transaction().expect("the transaction is kept");
1396 let info = transaction
1397 .metadata
1398 .as_ref()
1399 .and_then(|m| m.submitter_info.as_ref())
1400 .expect("its submitter info");
1401 assert_eq!(info.command_id, "command-7");
1402 assert_eq!(
1403 prepared.change_id(),
1404 crate::command::ChangeId::new(
1405 "user-1".to_string(),
1406 vec!["alice".to_string()],
1407 "command-7".to_string()
1408 )
1409 );
1410
1411 let executable = prepared
1412 .sign_with(&signer("1220aa"))
1413 .await
1414 .expect("one acting party signs without naming itself");
1415 assert_eq!(executable.command_id(), "command-7");
1416 assert_eq!(
1417 executable.change_id(),
1418 crate::command::ChangeId::new(
1419 "user-1".to_string(),
1420 vec!["alice".to_string()],
1421 "command-7".to_string()
1422 )
1423 );
1424 assert_eq!(executable.signed_by(), ["alice"]);
1425 let mut witnesses = executable.witnesses();
1427 witnesses.sort();
1428 assert_eq!(witnesses, ["alice".to_string(), "carol".to_string()]);
1429 }
1430
1431 #[tokio::test]
1435 async fn the_wait_for_transaction_request_carries_the_signature_and_the_format() {
1436 let executable = prepared(&["alice"])
1437 .sign_as("alice", &signer("1220aa"))
1438 .await
1439 .expect("sign")
1440 .with_submission_id("sub-1")
1441 .with_transaction_shape(crate::request::TransactionShape::LedgerEffects);
1442
1443 let wire = executable.into_execute_and_wait_for_transaction_request(pb::EventFormat {
1444 verbose: true,
1445 ..Default::default()
1446 });
1447
1448 assert!(
1449 wire.prepared_transaction.is_some(),
1450 "the transaction travels"
1451 );
1452 let signatures = wire.party_signatures.expect("signatures");
1453 assert_eq!(signatures.signatures.len(), 1);
1454 assert_eq!(signatures.signatures[0].party, "alice");
1455 assert_eq!(wire.submission_id, "sub-1");
1456 assert_eq!(wire.user_id, "user-1");
1457 assert_eq!(
1458 wire.hashing_scheme_version,
1459 ipb::HashingSchemeVersion::V2 as i32
1460 );
1461 let format = wire.transaction_format.expect("a transaction format");
1462 assert!(format.event_format.expect("event format").verbose);
1463 assert_eq!(
1464 format.transaction_shape,
1465 pb::TransactionShape::LedgerEffects as i32
1466 );
1467 }
1468}