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 }
280 }
281
282 Ok(result)
283 }
284
285 pub fn view_from_perspective(&self, txp: &TransactionPerspective) -> TransactionView {
286 let mut action_views = Vec::new();
287
288 let mut memo_plaintext: Option<MemoPlaintext> = None;
289 let mut memo_ciphertext: Option<MemoCiphertext> = None;
290
291 for action in self.actions() {
292 let action_view = action.view_from_perspective(txp);
293
294 if let ActionView::Output(output) = &action_view {
296 if memo_plaintext.is_none() {
297 memo_plaintext = match self.transaction_body().memo {
298 Some(ciphertext) => {
299 memo_ciphertext = Some(ciphertext.clone());
300 match output {
301 OutputView::Visible {
302 output: _,
303 note: _,
304 payload_key: decrypted_memo_key,
305 } => MemoCiphertext::decrypt(decrypted_memo_key, ciphertext).ok(),
306 OutputView::Opaque { output: _ } => None,
307 }
308 }
309 None => None,
310 }
311 }
312 }
313
314 action_views.push(action_view);
315 }
316
317 let memo_view = match memo_ciphertext {
318 Some(ciphertext) => match memo_plaintext {
319 Some(plaintext) => {
320 let plaintext_view: MemoPlaintextView = MemoPlaintextView {
321 return_address: txp.view_address(plaintext.return_address()),
322 text: plaintext.text().to_owned(),
323 };
324 Some(MemoView::Visible {
325 plaintext: plaintext_view,
326 ciphertext,
327 })
328 }
329 None => Some(MemoView::Opaque { ciphertext }),
330 },
331 None => None,
332 };
333
334 let detection_data =
335 self.transaction_body()
336 .detection_data
337 .as_ref()
338 .map(|detection_data| DetectionData {
339 fmd_clues: detection_data.fmd_clues.clone(),
340 });
341
342 TransactionView {
343 body_view: TransactionBodyView {
344 action_views,
345 transaction_parameters: self.transaction_parameters(),
346 detection_data,
347 memo_view,
348 },
349 binding_sig: self.binding_sig,
350 anchor: self.anchor,
351 }
352 }
353
354 pub fn actions(&self) -> impl Iterator<Item = &Action> {
355 self.transaction_body.actions.iter()
356 }
357
358 pub fn delegations(&self) -> impl Iterator<Item = &Delegate> {
359 self.actions().filter_map(|action| {
360 if let Action::Delegate(d) = action {
361 Some(d)
362 } else {
363 None
364 }
365 })
366 }
367
368 pub fn undelegations(&self) -> impl Iterator<Item = &Undelegate> {
369 self.actions().filter_map(|action| {
370 if let Action::Undelegate(d) = action {
371 Some(d)
372 } else {
373 None
374 }
375 })
376 }
377
378 pub fn undelegate_claims(&self) -> impl Iterator<Item = &UndelegateClaim> {
379 self.actions().filter_map(|action| {
380 if let Action::UndelegateClaim(d) = action {
381 Some(d)
382 } else {
383 None
384 }
385 })
386 }
387
388 pub fn proposal_submits(&self) -> impl Iterator<Item = &ProposalSubmit> {
389 self.actions().filter_map(|action| {
390 if let Action::ProposalSubmit(s) = action {
391 Some(s)
392 } else {
393 None
394 }
395 })
396 }
397
398 pub fn proposal_withdraws(&self) -> impl Iterator<Item = &ProposalWithdraw> {
399 self.actions().filter_map(|action| {
400 if let Action::ProposalWithdraw(w) = action {
401 Some(w)
402 } else {
403 None
404 }
405 })
406 }
407
408 pub fn validator_votes(&self) -> impl Iterator<Item = &ValidatorVote> {
409 self.actions().filter_map(|action| {
410 if let Action::ValidatorVote(v) = action {
411 Some(v)
412 } else {
413 None
414 }
415 })
416 }
417
418 pub fn delegator_votes(&self) -> impl Iterator<Item = &DelegatorVote> {
419 self.actions().filter_map(|action| {
420 if let Action::DelegatorVote(v) = action {
421 Some(v)
422 } else {
423 None
424 }
425 })
426 }
427
428 pub fn ibc_actions(&self) -> impl Iterator<Item = &IbcRelay> {
429 self.actions().filter_map(|action| {
430 if let Action::IbcRelay(ibc_action) = action {
431 Some(ibc_action)
432 } else {
433 None
434 }
435 })
436 }
437
438 pub fn validator_definitions(
439 &self,
440 ) -> impl Iterator<Item = &penumbra_sdk_stake::validator::Definition> {
441 self.actions().filter_map(|action| {
442 if let Action::ValidatorDefinition(d) = action {
443 Some(d)
444 } else {
445 None
446 }
447 })
448 }
449
450 pub fn outputs(&self) -> impl Iterator<Item = &Output> {
451 self.actions().filter_map(|action| {
452 if let Action::Output(d) = action {
453 Some(d)
454 } else {
455 None
456 }
457 })
458 }
459
460 pub fn swaps(&self) -> impl Iterator<Item = &Swap> {
461 self.actions().filter_map(|action| {
462 if let Action::Swap(s) = action {
463 Some(s)
464 } else {
465 None
466 }
467 })
468 }
469
470 pub fn spent_nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
471 self.actions().filter_map(|action| {
472 match action {
475 Action::Spend(spend) => Some(spend.body.nullifier),
476 Action::SwapClaim(swap_claim) => Some(swap_claim.body.nullifier),
477 _ => None,
478 }
479 })
480 }
481
482 pub fn state_commitments(&self) -> impl Iterator<Item = StateCommitment> + '_ {
483 self.actions()
484 .flat_map(|action| {
485 match action {
488 Action::Output(output) => {
489 [Some(output.body.note_payload.note_commitment), None]
490 }
491 Action::Swap(swap) => [Some(swap.body.payload.commitment), None],
492 Action::SwapClaim(claim) => [
493 Some(claim.body.output_1_commitment),
494 Some(claim.body.output_2_commitment),
495 ],
496 _ => [None, None],
497 }
498 })
499 .filter_map(|x| x)
500 }
501
502 pub fn community_pool_deposits(&self) -> impl Iterator<Item = &CommunityPoolDeposit> {
503 self.actions().filter_map(|action| {
504 if let Action::CommunityPoolDeposit(d) = action {
505 Some(d)
506 } else {
507 None
508 }
509 })
510 }
511
512 pub fn community_pool_spends(&self) -> impl Iterator<Item = &CommunityPoolSpend> {
513 self.actions().filter_map(|action| {
514 if let Action::CommunityPoolSpend(s) = action {
515 Some(s)
516 } else {
517 None
518 }
519 })
520 }
521
522 pub fn spends(&self) -> impl Iterator<Item = &Spend> {
523 self.actions().filter_map(|action| {
524 if let Action::Spend(s) = action {
525 Some(s)
526 } else {
527 None
528 }
529 })
530 }
531
532 pub fn community_pool_outputs(&self) -> impl Iterator<Item = &CommunityPoolOutput> {
533 self.actions().filter_map(|action| {
534 if let Action::CommunityPoolOutput(o) = action {
535 Some(o)
536 } else {
537 None
538 }
539 })
540 }
541
542 pub fn position_openings(&self) -> impl Iterator<Item = &PositionOpen> {
543 self.actions().filter_map(|action| {
544 if let Action::PositionOpen(d) = action {
545 Some(d)
546 } else {
547 None
548 }
549 })
550 }
551
552 pub fn position_closings(&self) -> impl Iterator<Item = &PositionClose> {
553 self.actions().filter_map(|action| {
554 if let Action::PositionClose(d) = action {
555 Some(d)
556 } else {
557 None
558 }
559 })
560 }
561
562 pub fn transaction_body(&self) -> TransactionBody {
563 self.transaction_body.clone()
564 }
565
566 pub fn transaction_parameters(&self) -> TransactionParameters {
567 self.transaction_body.transaction_parameters.clone()
568 }
569
570 pub fn binding_sig(&self) -> &Signature<Binding> {
571 &self.binding_sig
572 }
573
574 pub fn id(&self) -> TransactionId {
575 use sha2::{Digest, Sha256};
576
577 let tx_bytes: Vec<u8> = self.clone().try_into().expect("can serialize transaction");
578 let mut id_bytes = [0; 32];
579 id_bytes[..].copy_from_slice(Sha256::digest(&tx_bytes).as_slice());
580
581 TransactionId(id_bytes)
582 }
583
584 pub fn binding_verification_key(&self) -> VerificationKey<Binding> {
586 let mut balance_commitments = decaf377::Element::default();
587 for action in &self.transaction_body.actions {
588 balance_commitments += action.balance_commitment().0;
589 }
590
591 let fee_v_blinding = Fr::zero();
593 let fee_value_commitment = self
594 .transaction_body
595 .transaction_parameters
596 .fee
597 .commit(fee_v_blinding);
598 balance_commitments += fee_value_commitment.0;
599
600 let binding_verification_key_bytes: VerificationKeyBytes<Binding> =
601 balance_commitments.vartime_compress().0.into();
602
603 binding_verification_key_bytes
604 .try_into()
605 .expect("verification key is valid")
606 }
607}
608
609impl DomainType for TransactionSummary {
610 type Proto = pbt::TransactionSummary;
611}
612
613impl From<TransactionSummary> for pbt::TransactionSummary {
614 fn from(summary: TransactionSummary) -> Self {
615 pbt::TransactionSummary {
616 effects: summary
617 .effects
618 .into_iter()
619 .map(|effect| pbt::transaction_summary::Effects {
620 address: Some(effect.address.into()),
621 balance: Some(effect.balance.into()),
622 })
623 .collect(),
624 }
625 }
626}
627
628impl TryFrom<pbt::TransactionSummary> for TransactionSummary {
629 type Error = anyhow::Error;
630
631 fn try_from(pbt: pbt::TransactionSummary) -> Result<Self, Self::Error> {
632 let effects = pbt
633 .effects
634 .into_iter()
635 .map(|effect| {
636 Ok(TransactionEffect {
637 address: effect
638 .address
639 .ok_or_else(|| anyhow::anyhow!("missing address field"))?
640 .try_into()?,
641 balance: effect
642 .balance
643 .ok_or_else(|| anyhow::anyhow!("missing balance field"))?
644 .try_into()?,
645 })
646 })
647 .collect::<Result<Vec<TransactionEffect>, anyhow::Error>>()?;
648
649 Ok(Self { effects })
650 }
651}
652
653impl DomainType for TransactionBody {
654 type Proto = pbt::TransactionBody;
655}
656
657impl From<TransactionBody> for pbt::TransactionBody {
658 fn from(msg: TransactionBody) -> Self {
659 pbt::TransactionBody {
660 actions: msg.actions.into_iter().map(|x| x.into()).collect(),
661 transaction_parameters: Some(msg.transaction_parameters.into()),
662 detection_data: msg.detection_data.map(|x| x.into()),
663 memo: msg.memo.map(Into::into),
664 }
665 }
666}
667
668impl TryFrom<pbt::TransactionBody> for TransactionBody {
669 type Error = Error;
670
671 fn try_from(proto: pbt::TransactionBody) -> anyhow::Result<Self, Self::Error> {
672 let mut actions = Vec::<Action>::new();
673 for action in proto.actions {
674 actions.push(
675 action
676 .try_into()
677 .context("action malformed while parsing transaction body")?,
678 );
679 }
680
681 let memo = proto
682 .memo
683 .map(TryFrom::try_from)
684 .transpose()
685 .context("encrypted memo malformed while parsing transaction body")?;
686
687 let detection_data = proto
688 .detection_data
689 .map(TryFrom::try_from)
690 .transpose()
691 .context("detection data malformed while parsing transaction body")?;
692
693 let transaction_parameters = proto
694 .transaction_parameters
695 .ok_or_else(|| anyhow::anyhow!("transaction body missing transaction parameters"))?
696 .try_into()
697 .context("transaction parameters malformed")?;
698
699 Ok(TransactionBody {
700 actions,
701 transaction_parameters,
702 detection_data,
703 memo,
704 })
705 }
706}
707
708impl DomainType for Transaction {
709 type Proto = pbt::Transaction;
710}
711
712impl From<Transaction> for pbt::Transaction {
713 fn from(msg: Transaction) -> Self {
714 pbt::Transaction {
715 body: Some(msg.transaction_body.into()),
716 anchor: Some(msg.anchor.into()),
717 binding_sig: Some(msg.binding_sig.into()),
718 }
719 }
720}
721
722impl From<&Transaction> for pbt::Transaction {
723 fn from(msg: &Transaction) -> Self {
724 Transaction {
725 transaction_body: msg.transaction_body.clone(),
726 anchor: msg.anchor.clone(),
727 binding_sig: msg.binding_sig.clone(),
728 }
729 .into()
730 }
731}
732
733impl TryFrom<pbt::Transaction> for Transaction {
734 type Error = Error;
735
736 fn try_from(proto: pbt::Transaction) -> anyhow::Result<Self, Self::Error> {
737 let transaction_body = proto
738 .body
739 .ok_or_else(|| anyhow::anyhow!("transaction missing body"))?
740 .try_into()
741 .context("transaction body malformed")?;
742
743 let binding_sig = proto
744 .binding_sig
745 .ok_or_else(|| anyhow::anyhow!("transaction missing binding signature"))?
746 .try_into()
747 .context("transaction binding signature malformed")?;
748
749 let anchor = proto
750 .anchor
751 .ok_or_else(|| anyhow::anyhow!("transaction missing anchor"))?
752 .try_into()
753 .context("transaction anchor malformed")?;
754
755 Ok(Transaction {
756 transaction_body,
757 binding_sig,
758 anchor,
759 })
760 }
761}
762
763impl TryFrom<&[u8]> for Transaction {
764 type Error = Error;
765
766 fn try_from(bytes: &[u8]) -> Result<Transaction, Self::Error> {
767 pbt::Transaction::decode(bytes)?.try_into()
768 }
769}
770
771impl TryFrom<Vec<u8>> for Transaction {
772 type Error = Error;
773
774 fn try_from(bytes: Vec<u8>) -> Result<Transaction, Self::Error> {
775 Self::try_from(&bytes[..])
776 }
777}
778
779impl From<Transaction> for Vec<u8> {
780 fn from(transaction: Transaction) -> Vec<u8> {
781 let protobuf_serialized: pbt::Transaction = transaction.into();
782 protobuf_serialized.encode_to_vec()
783 }
784}
785
786impl From<&Transaction> for Vec<u8> {
787 fn from(transaction: &Transaction) -> Vec<u8> {
788 let protobuf_serialized: pbt::Transaction = transaction.into();
789 protobuf_serialized.encode_to_vec()
790 }
791}