Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransactionView: summary of transaction effects #4943

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Binary file modified crates/cnidarium/src/gen/proto_descriptor.bin.no_lfs
Binary file not shown.
38 changes: 35 additions & 3 deletions crates/core/asset/src/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use ark_r1cs_std::uint8::UInt8;
use ark_relations::r1cs::SynthesisError;
use penumbra_num::{Amount, AmountVar};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use std::{
collections::{btree_map, BTreeMap},
fmt::{self, Debug, Formatter},
Expand All @@ -29,17 +28,50 @@ use decaf377::{r1cs::ElementVar, Fq, Fr};
use imbalance::Imbalance;

use self::commitment::BalanceCommitmentVar;
use penumbra_proto::{penumbra::core::asset::v1 as pb, DomainType};

/// A `Balance` is a "vector of [`Value`]s", where some values may be required, while others may be
/// provided. For a transaction to be valid, its balance must be zero.
#[serde_as]
#[derive(Clone, Eq, Default, Serialize, Deserialize)]
#[serde(try_from = "pb::Balance", into = "pb::Balance")]
pub struct Balance {
negated: bool,
#[serde_as(as = "Vec<(_, _)>")]
Copy link
Collaborator Author

@TalDerei TalDerei Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: confirmed the previous derived serde policy wasn't used.

balance: BTreeMap<Id, Imbalance<NonZeroU128>>,
}

/* Protobuf impls */
impl DomainType for Balance {
type Proto = pb::Balance;
}

impl TryFrom<pb::Balance> for Balance {
type Error = anyhow::Error;

fn try_from(v: pb::Balance) -> Result<Self, Self::Error> {
let mut balance_map = BTreeMap::new();

for imbalance_value in v.balance.into_iter().map(TryInto::try_into) {
let value: Value = imbalance_value?;
let amount = NonZeroU128::new(value.amount.into())
.ok_or_else(|| anyhow::anyhow!("amount must be non-zero"))?;

let imbalance = Imbalance::Provided(amount); // todo: fix this placeholder
Copy link
Collaborator Author

@TalDerei TalDerei Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: Values cannot be negative, and a negative Balance is formed by negating a balance derived from a value. How do we calculate the Imbalance and differentiate between provided and required balances?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the modeling suggested by henry here: https://github.com/penumbra-zone/penumbra/pull/4943/files#r1855950445 takes care of that

balance_map.insert(value.asset_id, imbalance);
}

Ok(Self {
negated: v.negated,
balance: balance_map,
})
}
}

impl From<Balance> for pb::Balance {
fn from(_v: Balance) -> Self {
todo!() // todo: implement fallible conversion
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: need to implement fallible conversion (proto to domain type)

}
}

impl Debug for Balance {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Balance")
Expand Down
74 changes: 73 additions & 1 deletion crates/core/transaction/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ use anyhow::{Context, Error};
use ark_ff::Zero;
use decaf377::Fr;
use decaf377_rdsa::{Binding, Signature, VerificationKey, VerificationKeyBytes};
use penumbra_asset::Balance;
use penumbra_community_pool::{CommunityPoolDeposit, CommunityPoolOutput, CommunityPoolSpend};
use penumbra_dex::{
lp::action::{PositionClose, PositionOpen},
swap::Swap,
};
Copy link
Collaborator Author

@TalDerei TalDerei Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impl From<&Transaction> for pbt::Transaction {
fn from(msg: &Transaction) -> Self {
msg.into()
}
}

comment: does this lead to infinite recursion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. good catch

use penumbra_governance::{DelegatorVote, ProposalSubmit, ProposalWithdraw, ValidatorVote};
use penumbra_ibc::IbcRelay;
use penumbra_keys::{FullViewingKey, PayloadKey};
use penumbra_keys::{AddressView, FullViewingKey, PayloadKey};
use penumbra_proto::{
core::transaction::v1::{self as pbt},
DomainType, Message,
Expand Down Expand Up @@ -44,6 +45,21 @@ pub struct TransactionBody {
pub memo: Option<MemoCiphertext>,
}

/// Represents a transaction summary containing multiple effects.
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(try_from = "pbt::TransactionSummary", into = "pbt::TransactionSummary")]
pub struct TransactionSummary {
pub effects: Vec<Effects>,
}

/// Represents an individual effect of a transaction.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "pbt::Effects", into = "pbt::Effects")]
pub struct Effects {
pub address: AddressView,
pub balance: Balance,
}

impl EffectingData for TransactionBody {
fn effect_hash(&self) -> EffectHash {
let mut state = blake2b_simd::Params::new()
Expand Down Expand Up @@ -591,6 +607,62 @@ impl Transaction {
}
}

impl DomainType for TransactionSummary {
type Proto = pbt::TransactionSummary;
}

impl From<TransactionSummary> for pbt::TransactionSummary {
fn from(pbt: TransactionSummary) -> Self {
pbt::TransactionSummary {
effects: pbt.effects.into_iter().map(Into::into).collect(),
}
}
}

impl TryFrom<pbt::TransactionSummary> for TransactionSummary {
type Error = anyhow::Error;

fn try_from(pbt: pbt::TransactionSummary) -> Result<Self, Self::Error> {
let effects = pbt
.effects
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?;

Ok(Self { effects })
}
}

impl DomainType for Effects {
type Proto = pbt::Effects;
}

impl From<Effects> for pbt::Effects {
fn from(effect: Effects) -> Self {
pbt::Effects {
address: Some(effect.address.into()),
balance: Some(effect.balance.into()),
}
}
}

impl TryFrom<pbt::Effects> for Effects {
type Error = anyhow::Error;

fn try_from(pbt: pbt::Effects) -> Result<Self, Self::Error> {
Ok(Self {
address: pbt
.address
.ok_or_else(|| anyhow::anyhow!("missing address field"))?
.try_into()?,
balance: pbt
.balance
.ok_or_else(|| anyhow::anyhow!("missing balance field"))?
.try_into()?,
})
}
}

impl DomainType for TransactionBody {
type Proto = pbt::TransactionBody;
}
Expand Down
49 changes: 47 additions & 2 deletions crates/core/transaction/src/view.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use anyhow::Context;
use decaf377_rdsa::{Binding, Signature};
use penumbra_asset::Balance;
use penumbra_keys::AddressView;
use penumbra_proto::{core::transaction::v1 as pbt, DomainType};

use penumbra_shielded_pool::{OutputView, SpendView};
use serde::{Deserialize, Serialize};

pub mod action_view;
Expand All @@ -13,8 +15,9 @@ use penumbra_tct as tct;
pub use transaction_perspective::TransactionPerspective;

use crate::{
memo::MemoCiphertext, Action, DetectionData, Transaction, TransactionBody,
TransactionParameters,
memo::MemoCiphertext,
transaction::{Effects, TransactionSummary},
Action, DetectionData, Transaction, TransactionBody, TransactionParameters,
};

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -94,6 +97,48 @@ impl TransactionView {
pub fn action_views(&self) -> impl Iterator<Item = &ActionView> {
self.body_view.action_views.iter()
}

pub fn summary(&self) -> TransactionSummary {
let mut effects = Vec::new();

for action_view in &self.body_view.action_views {
Copy link
Collaborator Author

@TalDerei TalDerei Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: Iterates through the visible action views and adds the contributing balances to the transaction's effects. Sanity check the required and provided balances are correct.

match action_view {
ActionView::Spend(spend_view) => match spend_view {
SpendView::Visible { spend: _, note } => {
let value = note.value.value();
let balance = Balance::from(value);
let address = AddressView::Opaque {
address: note.address(),
};

effects.push(Effects { address, balance });
}
SpendView::Opaque { spend: _ } => continue,
},
ActionView::Output(output_view) => match output_view {
OutputView::Visible {
output: _,
note,
payload_key: _,
} => {
let value = note.value.value();
let balance = -Balance::from(value);
let address = AddressView::Opaque {
address: note.address(),
};

effects.push(Effects { address, balance });
}
OutputView::Opaque { output: _ } => continue,
},
ActionView::Swap(_) => todo!(),
ActionView::SwapClaim(_) => todo!(),
Comment on lines +134 to +135
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: need to implement other action views

_ => {}
}
}

TransactionSummary { effects }
}
}

impl DomainType for TransactionView {
Expand Down
17 changes: 17 additions & 0 deletions crates/proto/src/gen/penumbra.core.asset.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@ impl ::prost::Name for Value {
::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME)
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Balance {
/// Indicates if the balance is negated.
#[prost(bool, tag = "1")]
pub negated: bool,
/// Represents the vector of 'Values' in the balance.
#[prost(message, repeated, tag = "2")]
pub balance: ::prost::alloc::vec::Vec<Value>,
}
impl ::prost::Name for Balance {
const NAME: &'static str = "Balance";
const PACKAGE: &'static str = "penumbra.core.asset.v1";
fn full_name() -> ::prost::alloc::string::String {
::prost::alloc::format!("penumbra.core.asset.v1.{}", Self::NAME)
}
}
/// Represents a value of a known or unknown denomination.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
Expand Down
112 changes: 112 additions & 0 deletions crates/proto/src/gen/penumbra.core.asset.v1.serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,118 @@ impl<'de> serde::Deserialize<'de> for asset_image::Theme {
deserializer.deserialize_struct("penumbra.core.asset.v1.AssetImage.Theme", FIELDS, GeneratedVisitor)
}
}
impl serde::Serialize for Balance {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut len = 0;
if self.negated {
len += 1;
}
if !self.balance.is_empty() {
len += 1;
}
let mut struct_ser = serializer.serialize_struct("penumbra.core.asset.v1.Balance", len)?;
if self.negated {
struct_ser.serialize_field("negated", &self.negated)?;
}
if !self.balance.is_empty() {
struct_ser.serialize_field("balance", &self.balance)?;
}
struct_ser.end()
}
}
impl<'de> serde::Deserialize<'de> for Balance {
#[allow(deprecated)]
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
const FIELDS: &[&str] = &[
"negated",
"balance",
];

#[allow(clippy::enum_variant_names)]
enum GeneratedField {
Negated,
Balance,
__SkipField__,
}
impl<'de> serde::Deserialize<'de> for GeneratedField {
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
where
D: serde::Deserializer<'de>,
{
struct GeneratedVisitor;

impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = GeneratedField;

fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "expected one of: {:?}", &FIELDS)
}

#[allow(unused_variables)]
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
where
E: serde::de::Error,
{
match value {
"negated" => Ok(GeneratedField::Negated),
"balance" => Ok(GeneratedField::Balance),
_ => Ok(GeneratedField::__SkipField__),
}
}
}
deserializer.deserialize_identifier(GeneratedVisitor)
}
}
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = Balance;

fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("struct penumbra.core.asset.v1.Balance")
}

fn visit_map<V>(self, mut map_: V) -> std::result::Result<Balance, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut negated__ = None;
let mut balance__ = None;
while let Some(k) = map_.next_key()? {
match k {
GeneratedField::Negated => {
if negated__.is_some() {
return Err(serde::de::Error::duplicate_field("negated"));
}
negated__ = Some(map_.next_value()?);
}
GeneratedField::Balance => {
if balance__.is_some() {
return Err(serde::de::Error::duplicate_field("balance"));
}
balance__ = Some(map_.next_value()?);
}
GeneratedField::__SkipField__ => {
let _ = map_.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(Balance {
negated: negated__.unwrap_or_default(),
balance: balance__.unwrap_or_default(),
})
}
}
deserializer.deserialize_struct("penumbra.core.asset.v1.Balance", FIELDS, GeneratedVisitor)
}
}
impl serde::Serialize for BalanceCommitment {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
Expand Down
Loading
Loading