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

Merged
merged 30 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9935b89
proto(balance): define Balance message
TalDerei Nov 24, 2024
30138f8
regenerate protos for Balance message
TalDerei Nov 24, 2024
7174118
proto(TransactionSummary): define TransactionSummary message
TalDerei Nov 24, 2024
06bf641
regenerate protos for TransactionSummary message
TalDerei Nov 24, 2024
9add44d
add corresponding domain types to Balance
TalDerei Nov 24, 2024
abd9a29
modify message and regenerate protos for Balance
TalDerei Nov 25, 2024
8c12d8e
stub fallible conversion (proto message to domain type)
TalDerei Nov 25, 2024
8382e6a
add corresponding domain types to TransactionSummary and Effects
TalDerei Nov 25, 2024
ff86c9a
stub summary method in TransactionView
TalDerei Nov 25, 2024
425e549
proto(TransactionSummary): combine into a sub-message
TalDerei Nov 30, 2024
95aacc0
domain type (TransactionSummary): combine into a sub-message
TalDerei Nov 30, 2024
78741f8
proto(Balance): encapsulate list of value pairs
TalDerei Nov 30, 2024
36e9e5e
domain type (Balance): refactor conversions
TalDerei Nov 30, 2024
e434fa4
fix infinite recursion
TalDerei Nov 30, 2024
7b5670c
fill other action views
TalDerei Nov 30, 2024
f812330
proto(Balance): modify serialization and add unit testing
TalDerei Dec 8, 2024
f2142d8
proto(Balance): fix negation flag
TalDerei Dec 9, 2024
746d981
TransactionSummary: add unit testing
TalDerei Dec 9, 2024
86ede41
cargo clippy
TalDerei Dec 9, 2024
0ef739a
change visibility to private
TalDerei Dec 9, 2024
ef70e9c
simplify accumulate_summary method
TalDerei Dec 11, 2024
6b59390
swap unit test and dummy data
TalDerei Dec 11, 2024
2544232
remove log statements
TalDerei Dec 11, 2024
ee48a48
simplify accumulate effects helper
TalDerei Dec 13, 2024
d59033e
proto(Balance): handle zero cases in fallible conversion
TalDerei Dec 13, 2024
02b18d0
serialization: explicitely define ordering
TalDerei Dec 13, 2024
a37c046
testing: add proptest and filter zero amounts in fallible conversion
TalDerei Dec 13, 2024
f34601f
Simplify balance conversion
cronokirby Dec 13, 2024
74aa056
Simplify manual PartialOrd for AdressView
cronokirby Dec 13, 2024
28c8e70
core(balance): fix typo
erwanor Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<(_, _)>")]
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> {
Copy link
Member

Choose a reason for hiding this comment

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

PR crux, reviewers should ACK.

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 determine 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

Copy link
Collaborator Author

@TalDerei TalDerei Nov 30, 2024

Choose a reason for hiding this comment

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

the negated flag for each individual SignedValue pair now determines the imbalance

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

@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: need to implement infallible conversion (domain type to proto)

Copy link
Collaborator Author

@TalDerei TalDerei Nov 30, 2024

Choose a reason for hiding this comment

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

filled in the implementation. Sanity check the Amount was computed properly, references from https://github.com/penumbra-zone/penumbra/blob/main/crates/core/asset/src/balance.rs#L296-L301

}
}

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,
};
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 {
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!(),
_ => {}
}
}

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