1use std::{
2 collections::BTreeMap,
3 convert::{TryFrom, TryInto},
4};
5
6use anyhow::{Context, Error};
7use ark_ff::Zero;
8use decaf377::Fr;
9use decaf377_rdsa::{Binding, Signature, VerificationKey, VerificationKeyBytes};
10use penumbra_sdk_asset::Balance;
11use penumbra_sdk_community_pool::{CommunityPoolDeposit, CommunityPoolOutput, CommunityPoolSpend};
12use penumbra_sdk_dex::{
13 lp::action::{PositionClose, PositionOpen},
14 swap::Swap,
15};
16use penumbra_sdk_governance::{DelegatorVote, ProposalSubmit, ProposalWithdraw, ValidatorVote};
17use penumbra_sdk_ibc::IbcRelay;
18use penumbra_sdk_keys::{AddressView, FullViewingKey, PayloadKey};
19use penumbra_sdk_proto::{
20 core::transaction::v1::{self as pbt},
21 DomainType, Message,
22};
23use penumbra_sdk_sct::Nullifier;
24use penumbra_sdk_shielded_pool::{Note, Output, Spend};
25use penumbra_sdk_stake::{Delegate, Undelegate, UndelegateClaim};
26use penumbra_sdk_tct as tct;
27use penumbra_sdk_tct::StateCommitment;
28use penumbra_sdk_txhash::{
29 AuthHash, AuthorizingData, EffectHash, EffectingData, TransactionContext, TransactionId,
30};
31use serde::{Deserialize, Serialize};
32
33use crate::{
34 memo::{MemoCiphertext, MemoPlaintext},
35 view::{action_view::OutputView, MemoView, TransactionBodyView},
36 Action, ActionView, DetectionData, IsAction, MemoPlaintextView, TransactionParameters,
37 TransactionPerspective, TransactionView,
38};
39
40#[derive(Clone, Debug, Default)]
41pub struct TransactionBody {
42 pub actions: Vec<Action>,
43 pub transaction_parameters: TransactionParameters,
44 pub detection_data: Option<DetectionData>,
45 pub memo: Option<MemoCiphertext>,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50#[serde(try_from = "pbt::TransactionSummary", into = "pbt::TransactionSummary")]
51pub struct TransactionSummary {
52 pub effects: Vec<TransactionEffect>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct TransactionEffect {
58 pub address: AddressView,
59 pub balance: Balance,
60}
61
62impl EffectingData for TransactionBody {
63 fn effect_hash(&self) -> EffectHash {
64 let mut state = blake2b_simd::Params::new()
65 .personal(b"PenumbraEfHs")
66 .to_state();
67
68 let parameters_hash = self.transaction_parameters.effect_hash();
69 let memo_hash = self
70 .memo
71 .as_ref()
72 .map(|memo| memo.effect_hash())
73 .unwrap_or_default();
76 let detection_data_hash = self
77 .detection_data
78 .as_ref()
79 .map(|detection_data| detection_data.effect_hash())
80 .unwrap_or_default();
83
84 state.update(parameters_hash.as_bytes());
86 state.update(memo_hash.as_bytes());
87 state.update(detection_data_hash.as_bytes());
88
89 let num_actions = self.actions.len() as u32;
91 state.update(&num_actions.to_le_bytes());
92 for action in &self.actions {
93 state.update(action.effect_hash().as_bytes());
94 }
95
96 EffectHash(state.finalize().as_array().clone())
97 }
98}
99
100impl EffectingData for Transaction {
101 fn effect_hash(&self) -> EffectHash {
102 self.transaction_body.effect_hash()
103 }
104}
105
106impl AuthorizingData for TransactionBody {
107 fn auth_hash(&self) -> AuthHash {
108 AuthHash(
109 blake2b_simd::Params::default()
110 .hash(&self.encode_to_vec())
111 .as_bytes()[0..32]
112 .try_into()
113 .expect("blake2b output is always 32 bytes long"),
114 )
115 }
116}
117
118impl AuthorizingData for Transaction {
119 fn auth_hash(&self) -> AuthHash {
120 self.transaction_body.auth_hash()
121 }
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize)]
125#[serde(try_from = "pbt::Transaction", into = "pbt::Transaction")]
126pub struct Transaction {
127 pub transaction_body: TransactionBody,
128 pub binding_sig: Signature<Binding>,
129 pub anchor: tct::Root,
130}
131
132impl Default for Transaction {
133 fn default() -> Self {
134 Transaction {
135 transaction_body: Default::default(),
136 binding_sig: [0u8; 64].into(),
137 anchor: tct::Tree::new().root(),
138 }
139 }
140}
141
142impl Transaction {
143 pub fn context(&self) -> TransactionContext {
144 TransactionContext {
145 anchor: self.anchor,
146 effect_hash: self.effect_hash(),
147 }
148 }
149
150 pub fn num_proofs(&self) -> usize {
151 self.transaction_body
152 .actions
153 .iter()
154 .map(|action| match action {
155 Action::Spend(_) => 1,
156 Action::Output(_) => 1,
157 Action::Swap(_) => 1,
158 Action::SwapClaim(_) => 1,
159 Action::UndelegateClaim(_) => 1,
160 Action::DelegatorVote(_) => 1,
161 _ => 0,
162 })
163 .sum()
164 }
165
166 pub fn decrypt_memo(&self, fvk: &FullViewingKey) -> anyhow::Result<MemoPlaintext> {
170 if self.transaction_body().memo.is_none() {
172 return Err(anyhow::anyhow!("no memo"));
173 }
174
175 if let Some(output) = self.outputs().next() {
177 let ovk_wrapped_key = output.body.ovk_wrapped_key.clone();
179 let shared_secret = Note::decrypt_key(
180 ovk_wrapped_key,
181 output.body.note_payload.note_commitment,
182 output.body.balance_commitment,
183 fvk.outgoing(),
184 &output.body.note_payload.ephemeral_key,
185 );
186
187 let wrapped_memo_key = &output.body.wrapped_memo_key;
188 let memo_key: PayloadKey = match shared_secret {
189 Ok(shared_secret) => {
190 let payload_key =
191 PayloadKey::derive(&shared_secret, &output.body.note_payload.ephemeral_key);
192 wrapped_memo_key.decrypt_outgoing(&payload_key)?
193 }
194 Err(_) => wrapped_memo_key
195 .decrypt(output.body.note_payload.ephemeral_key, fvk.incoming())?,
196 };
197
198 let tx_body = self.transaction_body();
200 let memo_ciphertext = tx_body
201 .memo
202 .as_ref()
203 .expect("memo field exists on this transaction");
204 let decrypted_memo = MemoCiphertext::decrypt(&memo_key, memo_ciphertext.clone())?;
205
206 return Ok(decrypted_memo);
208 }
209
210 Err(anyhow::anyhow!("unable to decrypt memo"))
212 }
213
214 pub fn payload_keys(
215 &self,
216 fvk: &FullViewingKey,
217 ) -> anyhow::Result<BTreeMap<StateCommitment, PayloadKey>> {
218 let mut result = BTreeMap::new();
219
220 for action in self.actions() {
221 match action {
222 Action::Swap(swap) => {
223 let commitment = swap.body.payload.commitment;
224 let payload_key = PayloadKey::derive_swap(fvk.outgoing(), commitment);
225
226 result.insert(commitment, payload_key);
227 }
228 Action::Output(output) => {
229 let ovk_wrapped_key = output.body.ovk_wrapped_key.clone();
233 let commitment = output.body.note_payload.note_commitment;
234 let epk = &output.body.note_payload.ephemeral_key;
235 let cv = output.body.balance_commitment;
236 let ovk = fvk.outgoing();
237 let shared_secret =
238 Note::decrypt_key(ovk_wrapped_key, commitment, cv, ovk, epk);
239
240 match shared_secret {
241 Ok(shared_secret) => {
242 let payload_key = PayloadKey::derive(&shared_secret, epk);
244 result.insert(commitment, payload_key);
245 }
246 Err(_) => {
247 let shared_secret = fvk.incoming().key_agreement_with(epk)?;
249 let payload_key = PayloadKey::derive(&shared_secret, epk);
250
251 result.insert(commitment, payload_key);
252 }
253 }
254 }
255 Action::SwapClaim(_)
258 | Action::Spend(_)
259 | Action::Delegate(_)
260 | Action::Undelegate(_)
261 | Action::UndelegateClaim(_)
262 | Action::ValidatorDefinition(_)
263 | Action::IbcRelay(_)
264 | Action::ProposalSubmit(_)
265 | Action::ProposalWithdraw(_)
266 | Action::ValidatorVote(_)
267 | Action::DelegatorVote(_)
268 | Action::ProposalDepositClaim(_)
269 | Action::PositionOpen(_)
270 | Action::PositionClose(_)
271 | Action::PositionWithdraw(_)
272 | Action::Ics20Withdrawal(_)
273 | Action::CommunityPoolSpend(_)
274 | Action::CommunityPoolOutput(_)
275 | Action::CommunityPoolDeposit(_)
276 | Action::ActionDutchAuctionSchedule(_)
277 | Action::ActionDutchAuctionEnd(_)
278 | Action::ActionDutchAuctionWithdraw(_)
279 | Action::ActionLiquidityTournamentVote(_) => {}
280 }
281 }
282
283 Ok(result)
284 }
285
286 pub fn view_from_perspective(&self, txp: &TransactionPerspective) -> TransactionView {
287 let mut action_views = Vec::new();
288
289 let mut memo_plaintext: Option<MemoPlaintext> = None;
290 let mut memo_ciphertext: Option<MemoCiphertext> = None;
291
292 for action in self.actions() {
293 let action_view = action.view_from_perspective(txp);
294
295 if let ActionView::Output(output) = &action_view {
297 if memo_plaintext.is_none() {
298 memo_plaintext = match self.transaction_body().memo {
299 Some(ciphertext) => {
300 memo_ciphertext = Some(ciphertext.clone());
301 match output {
302 OutputView::Visible {
303 output: _,
304 note: _,
305 payload_key: decrypted_memo_key,
306 } => MemoCiphertext::decrypt(decrypted_memo_key, ciphertext).ok(),
307 OutputView::Opaque { output: _ } => None,
308 }
309 }
310 None => None,
311 }
312 }
313 }
314
315 action_views.push(action_view);
316 }
317
318 let memo_view = match memo_ciphertext {
319 Some(ciphertext) => match memo_plaintext {
320 Some(plaintext) => {
321 let plaintext_view: MemoPlaintextView = MemoPlaintextView {
322 return_address: txp.view_address(plaintext.return_address()),
323 text: plaintext.text().to_owned(),
324 };
325 Some(MemoView::Visible {
326 plaintext: plaintext_view,
327 ciphertext,
328 })
329 }
330 None => Some(MemoView::Opaque { ciphertext }),
331 },
332 None => None,
333 };
334
335 let detection_data =
336 self.transaction_body()
337 .detection_data
338 .as_ref()
339 .map(|detection_data| DetectionData {
340 fmd_clues: detection_data.fmd_clues.clone(),
341 });
342
343 TransactionView {
344 body_view: TransactionBodyView {
345 action_views,
346 transaction_parameters: self.transaction_parameters(),
347 detection_data,
348 memo_view,
349 },
350 binding_sig: self.binding_sig,
351 anchor: self.anchor,
352 }
353 }
354
355 pub fn actions(&self) -> impl Iterator<Item = &Action> {
356 self.transaction_body.actions.iter()
357 }
358
359 pub fn delegations(&self) -> impl Iterator<Item = &Delegate> {
360 self.actions().filter_map(|action| {
361 if let Action::Delegate(d) = action {
362 Some(d)
363 } else {
364 None
365 }
366 })
367 }
368
369 pub fn undelegations(&self) -> impl Iterator<Item = &Undelegate> {
370 self.actions().filter_map(|action| {
371 if let Action::Undelegate(d) = action {
372 Some(d)
373 } else {
374 None
375 }
376 })
377 }
378
379 pub fn undelegate_claims(&self) -> impl Iterator<Item = &UndelegateClaim> {
380 self.actions().filter_map(|action| {
381 if let Action::UndelegateClaim(d) = action {
382 Some(d)
383 } else {
384 None
385 }
386 })
387 }
388
389 pub fn proposal_submits(&self) -> impl Iterator<Item = &ProposalSubmit> {
390 self.actions().filter_map(|action| {
391 if let Action::ProposalSubmit(s) = action {
392 Some(s)
393 } else {
394 None
395 }
396 })
397 }
398
399 pub fn proposal_withdraws(&self) -> impl Iterator<Item = &ProposalWithdraw> {
400 self.actions().filter_map(|action| {
401 if let Action::ProposalWithdraw(w) = action {
402 Some(w)
403 } else {
404 None
405 }
406 })
407 }
408
409 pub fn validator_votes(&self) -> impl Iterator<Item = &ValidatorVote> {
410 self.actions().filter_map(|action| {
411 if let Action::ValidatorVote(v) = action {
412 Some(v)
413 } else {
414 None
415 }
416 })
417 }
418
419 pub fn delegator_votes(&self) -> impl Iterator<Item = &DelegatorVote> {
420 self.actions().filter_map(|action| {
421 if let Action::DelegatorVote(v) = action {
422 Some(v)
423 } else {
424 None
425 }
426 })
427 }
428
429 pub fn ibc_actions(&self) -> impl Iterator<Item = &IbcRelay> {
430 self.actions().filter_map(|action| {
431 if let Action::IbcRelay(ibc_action) = action {
432 Some(ibc_action)
433 } else {
434 None
435 }
436 })
437 }
438
439 pub fn validator_definitions(
440 &self,
441 ) -> impl Iterator<Item = &penumbra_sdk_stake::validator::Definition> {
442 self.actions().filter_map(|action| {
443 if let Action::ValidatorDefinition(d) = action {
444 Some(d)
445 } else {
446 None
447 }
448 })
449 }
450
451 pub fn outputs(&self) -> impl Iterator<Item = &Output> {
452 self.actions().filter_map(|action| {
453 if let Action::Output(d) = action {
454 Some(d)
455 } else {
456 None
457 }
458 })
459 }
460
461 pub fn swaps(&self) -> impl Iterator<Item = &Swap> {
462 self.actions().filter_map(|action| {
463 if let Action::Swap(s) = action {
464 Some(s)
465 } else {
466 None
467 }
468 })
469 }
470
471 pub fn spent_nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
472 self.actions().filter_map(|action| {
473 match action {
476 Action::Spend(spend) => Some(spend.body.nullifier),
477 Action::SwapClaim(swap_claim) => Some(swap_claim.body.nullifier),
478 _ => None,
479 }
480 })
481 }
482
483 pub fn state_commitments(&self) -> impl Iterator<Item = StateCommitment> + '_ {
484 self.actions()
485 .flat_map(|action| {
486 match action {
489 Action::Output(output) => {
490 [Some(output.body.note_payload.note_commitment), None]
491 }
492 Action::Swap(swap) => [Some(swap.body.payload.commitment), None],
493 Action::SwapClaim(claim) => [
494 Some(claim.body.output_1_commitment),
495 Some(claim.body.output_2_commitment),
496 ],
497 _ => [None, None],
498 }
499 })
500 .filter_map(|x| x)
501 }
502
503 pub fn community_pool_deposits(&self) -> impl Iterator<Item = &CommunityPoolDeposit> {
504 self.actions().filter_map(|action| {
505 if let Action::CommunityPoolDeposit(d) = action {
506 Some(d)
507 } else {
508 None
509 }
510 })
511 }
512
513 pub fn community_pool_spends(&self) -> impl Iterator<Item = &CommunityPoolSpend> {
514 self.actions().filter_map(|action| {
515 if let Action::CommunityPoolSpend(s) = action {
516 Some(s)
517 } else {
518 None
519 }
520 })
521 }
522
523 pub fn spends(&self) -> impl Iterator<Item = &Spend> {
524 self.actions().filter_map(|action| {
525 if let Action::Spend(s) = action {
526 Some(s)
527 } else {
528 None
529 }
530 })
531 }
532
533 pub fn community_pool_outputs(&self) -> impl Iterator<Item = &CommunityPoolOutput> {
534 self.actions().filter_map(|action| {
535 if let Action::CommunityPoolOutput(o) = action {
536 Some(o)
537 } else {
538 None
539 }
540 })
541 }
542
543 pub fn position_openings(&self) -> impl Iterator<Item = &PositionOpen> {
544 self.actions().filter_map(|action| {
545 if let Action::PositionOpen(d) = action {
546 Some(d)
547 } else {
548 None
549 }
550 })
551 }
552
553 pub fn position_closings(&self) -> impl Iterator<Item = &PositionClose> {
554 self.actions().filter_map(|action| {
555 if let Action::PositionClose(d) = action {
556 Some(d)
557 } else {
558 None
559 }
560 })
561 }
562
563 pub fn transaction_body(&self) -> TransactionBody {
564 self.transaction_body.clone()
565 }
566
567 pub fn transaction_parameters(&self) -> TransactionParameters {
568 self.transaction_body.transaction_parameters.clone()
569 }
570
571 pub fn binding_sig(&self) -> &Signature<Binding> {
572 &self.binding_sig
573 }
574
575 pub fn id(&self) -> TransactionId {
576 use sha2::{Digest, Sha256};
577
578 let tx_bytes: Vec<u8> = self.clone().try_into().expect("can serialize transaction");
579 let mut id_bytes = [0; 32];
580 id_bytes[..].copy_from_slice(Sha256::digest(&tx_bytes).as_slice());
581
582 TransactionId(id_bytes)
583 }
584
585 pub fn binding_verification_key(&self) -> VerificationKey<Binding> {
587 let mut balance_commitments = decaf377::Element::default();
588 for action in &self.transaction_body.actions {
589 balance_commitments += action.balance_commitment().0;
590 }
591
592 let fee_v_blinding = Fr::zero();
594 let fee_value_commitment = self
595 .transaction_body
596 .transaction_parameters
597 .fee
598 .commit(fee_v_blinding);
599 balance_commitments += fee_value_commitment.0;
600
601 let binding_verification_key_bytes: VerificationKeyBytes<Binding> =
602 balance_commitments.vartime_compress().0.into();
603
604 binding_verification_key_bytes
605 .try_into()
606 .expect("verification key is valid")
607 }
608}
609
610impl DomainType for TransactionSummary {
611 type Proto = pbt::TransactionSummary;
612}
613
614impl From<TransactionSummary> for pbt::TransactionSummary {
615 fn from(summary: TransactionSummary) -> Self {
616 pbt::TransactionSummary {
617 effects: summary
618 .effects
619 .into_iter()
620 .map(|effect| pbt::transaction_summary::Effects {
621 address: Some(effect.address.into()),
622 balance: Some(effect.balance.into()),
623 })
624 .collect(),
625 }
626 }
627}
628
629impl TryFrom<pbt::TransactionSummary> for TransactionSummary {
630 type Error = anyhow::Error;
631
632 fn try_from(pbt: pbt::TransactionSummary) -> Result<Self, Self::Error> {
633 let effects = pbt
634 .effects
635 .into_iter()
636 .map(|effect| {
637 Ok(TransactionEffect {
638 address: effect
639 .address
640 .ok_or_else(|| anyhow::anyhow!("missing address field"))?
641 .try_into()?,
642 balance: effect
643 .balance
644 .ok_or_else(|| anyhow::anyhow!("missing balance field"))?
645 .try_into()?,
646 })
647 })
648 .collect::<Result<Vec<TransactionEffect>, anyhow::Error>>()?;
649
650 Ok(Self { effects })
651 }
652}
653
654impl DomainType for TransactionBody {
655 type Proto = pbt::TransactionBody;
656}
657
658impl From<TransactionBody> for pbt::TransactionBody {
659 fn from(msg: TransactionBody) -> Self {
660 pbt::TransactionBody {
661 actions: msg.actions.into_iter().map(|x| x.into()).collect(),
662 transaction_parameters: Some(msg.transaction_parameters.into()),
663 detection_data: msg.detection_data.map(|x| x.into()),
664 memo: msg.memo.map(Into::into),
665 }
666 }
667}
668
669impl TryFrom<pbt::TransactionBody> for TransactionBody {
670 type Error = Error;
671
672 fn try_from(proto: pbt::TransactionBody) -> anyhow::Result<Self, Self::Error> {
673 let mut actions = Vec::<Action>::new();
674 for action in proto.actions {
675 actions.push(
676 action
677 .try_into()
678 .context("action malformed while parsing transaction body")?,
679 );
680 }
681
682 let memo = proto
683 .memo
684 .map(TryFrom::try_from)
685 .transpose()
686 .context("encrypted memo malformed while parsing transaction body")?;
687
688 let detection_data = proto
689 .detection_data
690 .map(TryFrom::try_from)
691 .transpose()
692 .context("detection data malformed while parsing transaction body")?;
693
694 let transaction_parameters = proto
695 .transaction_parameters
696 .ok_or_else(|| anyhow::anyhow!("transaction body missing transaction parameters"))?
697 .try_into()
698 .context("transaction parameters malformed")?;
699
700 Ok(TransactionBody {
701 actions,
702 transaction_parameters,
703 detection_data,
704 memo,
705 })
706 }
707}
708
709impl DomainType for Transaction {
710 type Proto = pbt::Transaction;
711}
712
713impl From<Transaction> for pbt::Transaction {
714 fn from(msg: Transaction) -> Self {
715 pbt::Transaction {
716 body: Some(msg.transaction_body.into()),
717 anchor: Some(msg.anchor.into()),
718 binding_sig: Some(msg.binding_sig.into()),
719 }
720 }
721}
722
723impl From<&Transaction> for pbt::Transaction {
724 fn from(msg: &Transaction) -> Self {
725 Transaction {
726 transaction_body: msg.transaction_body.clone(),
727 anchor: msg.anchor.clone(),
728 binding_sig: msg.binding_sig.clone(),
729 }
730 .into()
731 }
732}
733
734impl TryFrom<pbt::Transaction> for Transaction {
735 type Error = Error;
736
737 fn try_from(proto: pbt::Transaction) -> anyhow::Result<Self, Self::Error> {
738 let transaction_body = proto
739 .body
740 .ok_or_else(|| anyhow::anyhow!("transaction missing body"))?
741 .try_into()
742 .context("transaction body malformed")?;
743
744 let binding_sig = proto
745 .binding_sig
746 .ok_or_else(|| anyhow::anyhow!("transaction missing binding signature"))?
747 .try_into()
748 .context("transaction binding signature malformed")?;
749
750 let anchor = proto
751 .anchor
752 .ok_or_else(|| anyhow::anyhow!("transaction missing anchor"))?
753 .try_into()
754 .context("transaction anchor malformed")?;
755
756 Ok(Transaction {
757 transaction_body,
758 binding_sig,
759 anchor,
760 })
761 }
762}
763
764impl TryFrom<&[u8]> for Transaction {
765 type Error = Error;
766
767 fn try_from(bytes: &[u8]) -> Result<Transaction, Self::Error> {
768 pbt::Transaction::decode(bytes)?.try_into()
769 }
770}
771
772impl TryFrom<Vec<u8>> for Transaction {
773 type Error = Error;
774
775 fn try_from(bytes: Vec<u8>) -> Result<Transaction, Self::Error> {
776 Self::try_from(&bytes[..])
777 }
778}
779
780impl From<Transaction> for Vec<u8> {
781 fn from(transaction: Transaction) -> Vec<u8> {
782 let protobuf_serialized: pbt::Transaction = transaction.into();
783 protobuf_serialized.encode_to_vec()
784 }
785}
786
787impl From<&Transaction> for Vec<u8> {
788 fn from(transaction: &Transaction) -> Vec<u8> {
789 let protobuf_serialized: pbt::Transaction = transaction.into();
790 protobuf_serialized.encode_to_vec()
791 }
792}