diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 61d0a174bd..a6eaed27f7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -291,6 +291,7 @@ jobs: --exclude mina-archive-breadcrumb-compare \ --exclude webrtc-sniffer # --exclude mina-tree + # --exclude mina-tx-type # --exclude vrf account-tests: diff --git a/Cargo.lock b/Cargo.lock index 05d3ff856f..fd87ab8078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5335,6 +5335,7 @@ dependencies = [ "mina-p2p-messages", "mina-poseidon", "mina-signer", + "mina-tx-type", "num-bigint", "o1-utils", "once_cell", @@ -5364,6 +5365,22 @@ dependencies = [ "zstd", ] +[[package]] +name = "mina-tx-type" +version = "0.18.1" +dependencies = [ + "ark-ec", + "ark-ff", + "kimchi", + "mina-curves", + "mina-p2p-messages", + "mina-poseidon", + "mina-signer", + "poseidon", + "rand", + "serde", +] + [[package]] name = "minimal-lexical" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 571216bf0d..681c185e77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "ledger", "macros", "mina-p2p-messages", + "mina-tx-type", "node", "node/account", "node/common", @@ -147,6 +148,7 @@ mina-p2p-messages = { path = "mina-p2p-messages" } mina-poseidon = { git = "https://github.com/o1-labs/proof-systems", tag = "0.2.0" } mina-signer = { git = "https://github.com/o1-labs/proof-systems", tag = "0.2.0" } mina-transport = { path = "tools/transport" } +mina-tx-type = { path = "mina-tx-type" } mio = { version = "1.0.4", features = ["os-poll", "net"] } multiaddr = "0.18.1" multihash = { version = "0.18.1", features = ["blake2b"] } diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index 2a6354ea66..7060daf049 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -43,6 +43,7 @@ mina-macros = { workspace = true } mina-p2p-messages = { workspace = true } mina-poseidon = { workspace = true } mina-signer = { workspace = true } +mina-tx-type = { workspace = true } num-bigint = { workspace = true } o1-utils = { workspace = true } once_cell = { workspace = true } diff --git a/ledger/src/account/common.rs b/ledger/src/account/common.rs index 521de1b44b..7df6925817 100644 --- a/ledger/src/account/common.rs +++ b/ledger/src/account/common.rs @@ -4,6 +4,7 @@ use crate::{ numbers::{ currency::{CheckedAmount, CheckedBalance}, nat::{CheckedSlot, CheckedSlotSpan}, + ToCheckedExt, }, to_field_elements::ToFieldElements, }, diff --git a/ledger/src/proofs/block.rs b/ledger/src/proofs/block.rs index 8aafcf2c82..6de7fcf09c 100644 --- a/ledger/src/proofs/block.rs +++ b/ledger/src/proofs/block.rs @@ -21,6 +21,7 @@ use crate::{ numbers::{ currency::CheckedSigned, nat::{CheckedNat, CheckedSlot}, + SignedToCheckedExt, ToCheckedExt, }, step::extract_recursion_challenges, to_field_elements::ToFieldElementsDebug, @@ -621,7 +622,7 @@ mod vrf { scale_non_constant, InnerCurve, }, }, - scan_state::currency::{Amount, Balance}, + scan_state::currency::{Amount, AmountFeeFieldExt, Balance}, sparse_ledger::SparseLedger, AccountIndex, Address, AppendToInputs, }; @@ -752,7 +753,7 @@ mod vrf { use floating_point::*; let top = SnarkyInteger::create(my_stake.to_field::(), balance_upper_bound()); - let bottom = SnarkyInteger::create(total_stake.to_field::(), amount_upper_bound()); + let bottom = SnarkyInteger::create(total_stake.to_field(), amount_upper_bound()); let precision = PARAMS.per_term_precision; let point = floating_point::of_quotient(precision, top, bottom, w); diff --git a/ledger/src/proofs/field.rs b/ledger/src/proofs/field.rs index 14a14c8734..d0a328a057 100644 --- a/ledger/src/proofs/field.rs +++ b/ledger/src/proofs/field.rs @@ -1,3 +1,23 @@ +//! Field-related traits and types for proof circuits. +//! +//! # Refactoring Notes (mina-tx-type extraction) +//! +//! The following types in this module have been duplicated in `mina-tx-type`: +//! - `FieldWitness` trait +//! - `GroupAffine` type alias +//! - `Boolean` enum +//! - `CircuitVar` enum +//! - `ShiftedValue` struct (via `ShiftingValue` trait) +//! - `Shift`, `ShiftFq` types +//! - `FromFpFq`, `IntoGeneric`, `ToBoolean`, `Params` types +//! +//! To complete the migration to `mina-tx-type`: +//! 1. Replace these local type definitions with imports from `mina_tx_type` +//! 2. Update all usages throughout the ledger crate +//! 3. This will then allow replacing `ToFieldElements` trait with import +//! +//! See: mina-tx-type/src/proofs/field.rs for the equivalent types + use ark_ec::{short_weierstrass::Projective, AffineRepr, CurveGroup}; use ark_ff::{BigInteger256, FftField, Field, PrimeField}; use kimchi::curve::KimchiCurve; @@ -19,9 +39,15 @@ use super::{ BACKEND_TICK_ROUNDS_N, BACKEND_TOCK_ROUNDS_N, }; +/// Type alias for affine curve points parameterized by field witness. +/// +/// **Note:** This type is duplicated in `mina_tx_type::GroupAffine`. pub type GroupAffine = ark_ec::short_weierstrass::Affine<::Parameters>; -/// All the generics we need during witness generation +/// All the generics we need during witness generation. +/// +/// **Note:** This trait is duplicated in `mina_tx_type::FieldWitness`. +/// See module-level docs for the migration plan. pub trait FieldWitness where Self: Field diff --git a/ledger/src/proofs/merge.rs b/ledger/src/proofs/merge.rs index de724caec1..823bd7ca92 100644 --- a/ledger/src/proofs/merge.rs +++ b/ledger/src/proofs/merge.rs @@ -10,7 +10,9 @@ use mina_curves::pasta::{Fp, Fq}; use mina_p2p_messages::{bigint::InvalidBigInt, v2}; use crate::{ - proofs::transaction::transaction_snark::assert_equal_local_state, + proofs::{ + numbers::SignedToCheckedExt, transaction::transaction_snark::assert_equal_local_state, + }, scan_state::{ fee_excess::FeeExcess, pending_coinbase, @@ -61,8 +63,8 @@ fn merge_main( ); let _supply_increase = { - let s1 = s1.supply_increase.to_checked::(); - let s2 = s2.supply_increase.to_checked::(); + let s1 = s1.supply_increase.to_checked(); + let s2 = s2.supply_increase.to_checked(); s1.add(&s2, w) }; @@ -82,12 +84,12 @@ fn merge_main( fee_excess_r, .. } = statement.fee_excess; - fee_excess_l.to_checked::().value(w); - fee_excess_r.to_checked::().value(w); + fee_excess_l.to_checked().value(w); + fee_excess_r.to_checked().value(w); // Only `Statement.supply_increase`, not `supply_increase` let supply_increase = statement.supply_increase; - supply_increase.to_checked::().value(w); + supply_increase.to_checked().value(w); } Ok((s1, s2)) diff --git a/ledger/src/proofs/numbers/common.rs b/ledger/src/proofs/numbers/common.rs index bdea14b9ff..ac01ac5de8 100644 --- a/ledger/src/proofs/numbers/common.rs +++ b/ledger/src/proofs/numbers/common.rs @@ -10,8 +10,9 @@ use crate::{ pub trait ForZkappCheck: Magnitude { type CheckedType; + fn zkapp_to_field(&self) -> F; fn to_checked(&self) -> Self::CheckedType { - Self::checked_from_field(self.to_field::()) + Self::checked_from_field(self.zkapp_to_field()) } fn checked_from_field(field: F) -> Self::CheckedType; fn lte(this: &Self::CheckedType, other: &Self::CheckedType, w: &mut Witness) -> Boolean; diff --git a/ledger/src/proofs/numbers/currency.rs b/ledger/src/proofs/numbers/currency.rs index e3710b6019..0d090f68e8 100644 --- a/ledger/src/proofs/numbers/currency.rs +++ b/ledger/src/proofs/numbers/currency.rs @@ -1,6 +1,8 @@ use crate::{ proofs::field::CircuitVar, - scan_state::currency::{self, Amount, Balance, Fee, Magnitude, MinMax, Sgn, Signed}, + scan_state::currency::{ + self, Amount, AmountFeeFieldExt, Balance, Fee, Magnitude, MinMax, Sgn, SgnExt, Signed, + }, }; use std::{cell::Cell, cmp::Ordering::Less}; @@ -261,16 +263,24 @@ pub trait CheckedCurrency: fn to_field(&self) -> F; fn from_field(field: F) -> Self; + /// Convert the inner type to a field element. + /// This is needed because Amount and Fee are now from mina-tx-type + /// and we can't add MagnitudeFieldExt bounds due to different FieldWitness traits. + fn inner_to_field(inner: &Self::Inner) -> F; + + /// Convert from a field element to the inner type. + fn inner_of_field(field: F) -> Self::Inner; + fn zero() -> Self { Self::from_field(F::zero()) } fn to_inner(&self) -> Self::Inner { - Self::Inner::of_field(self.to_field()) + Self::inner_of_field(self.to_field()) } fn from_inner(inner: Self::Inner) -> Self { - Self::from_field(inner.to_field()) + Self::from_field(Self::inner_to_field(&inner)) } fn min() -> Self { @@ -480,25 +490,81 @@ impl CheckedSigned> { } } -macro_rules! impl_currency { - ($({$name:tt, $unchecked:tt}),*) => ($( - impl CheckedCurrency for $name:: { - type Inner = $unchecked; - fn to_field(&self) -> F { - self.0 - } - fn from_field(field: F) -> Self { - Self(field) - } +/// Extension trait for converting currency types to their checked variants. +/// +/// This trait is needed because `Amount` and `Fee` are now defined in `mina-tx-type`, +/// so we can't add inherent methods to them from the ledger crate. +pub trait ToCheckedExt { + type Checked: CheckedCurrency; + fn to_checked(&self) -> Self::Checked; +} + +impl ToCheckedExt for Amount { + type Checked = CheckedAmount; + fn to_checked(&self) -> Self::Checked { + CheckedAmount::from_inner(*self) + } +} + +impl ToCheckedExt for Fee { + type Checked = CheckedFee; + fn to_checked(&self) -> Self::Checked { + CheckedFee::from_inner(*self) + } +} + +impl ToCheckedExt for Balance { + type Checked = CheckedBalance; + fn to_checked(&self) -> Self::Checked { + CheckedBalance::from_inner(*self) + } +} + +/// Extension trait for converting signed currency types to their checked variants. +pub trait SignedToCheckedExt> { + fn to_checked(&self) -> CheckedSigned; +} + +impl SignedToCheckedExt> for Signed { + fn to_checked(&self) -> CheckedSigned> { + CheckedSigned { + magnitude: >::to_checked(&self.magnitude), + sgn: CircuitVar::Var(self.sgn), + value: Cell::new(None), + } + } +} + +impl SignedToCheckedExt> for Signed { + fn to_checked(&self) -> CheckedSigned> { + CheckedSigned { + magnitude: >::to_checked(&self.magnitude), + sgn: CircuitVar::Var(self.sgn), + value: Cell::new(None), + } + } +} + +impl SignedToCheckedExt> for Signed { + fn to_checked(&self) -> CheckedSigned> { + CheckedSigned { + magnitude: >::to_checked(&self.magnitude), + sgn: CircuitVar::Var(self.sgn), + value: Cell::new(None), } + } +} - impl ToFieldElements for $name:: { +/// Macro for shared implementations of CheckedCurrency types. +macro_rules! impl_currency_common { + ($name:tt, $unchecked:tt) => { + impl ToFieldElements for $name { fn to_field_elements(&self, fields: &mut Vec) { self.0.to_field_elements(fields) } } - impl Check for $name:: { + impl Check for $name { fn check(&self, w: &mut Witness) { range_check::(self.0, w); } @@ -509,37 +575,104 @@ macro_rules! impl_currency { self.to_inner().to_inputs(inputs) } } + }; +} - impl $unchecked { - pub fn to_checked(&self) -> $name { - $name::from_inner(*self) - } - } +// Implement CheckedCurrency for Amount (uses AmountFeeFieldExt) +impl CheckedCurrency for CheckedAmount { + type Inner = Amount; + fn to_field(&self) -> F { + self.0 + } + fn from_field(field: F) -> Self { + Self(field) + } + fn inner_to_field(inner: &Self::Inner) -> F { + >::to_field(inner) + } + fn inner_of_field(field: F) -> Self::Inner { + >::of_field(field) + } +} - impl Signed<$unchecked> { - pub fn to_checked(&self) -> CheckedSigned> { - CheckedSigned { - magnitude: self.magnitude.to_checked(), - sgn: CircuitVar::Var(self.sgn), - value: Cell::new(None), - } - } - } +impl ForZkappCheck for Amount { + type CheckedType = CheckedAmount; + fn zkapp_to_field(&self) -> F { + >::to_field(self) + } + fn checked_from_field(field: F) -> Self::CheckedType { + Self::CheckedType::from_field(field) + } + fn lte(this: &Self::CheckedType, other: &Self::CheckedType, w: &mut Witness) -> Boolean { + Self::CheckedType::lte(this, other, w) + } +} - impl ForZkappCheck for $unchecked { - type CheckedType = $name; - fn checked_from_field(field: F) -> Self::CheckedType { - Self::CheckedType::from_field(field) - } - fn lte(this: &Self::CheckedType, other: &Self::CheckedType, w: &mut Witness) -> Boolean { - Self::CheckedType::lte(this, other, w) - } - } - )*) +impl_currency_common!(CheckedAmount, Amount); + +// Implement CheckedCurrency for Fee (uses AmountFeeFieldExt) +impl CheckedCurrency for CheckedFee { + type Inner = Fee; + fn to_field(&self) -> F { + self.0 + } + fn from_field(field: F) -> Self { + Self(field) + } + fn inner_to_field(inner: &Self::Inner) -> F { + >::to_field(inner) + } + fn inner_of_field(field: F) -> Self::Inner { + >::of_field(field) + } +} + +impl ForZkappCheck for Fee { + type CheckedType = CheckedFee; + fn zkapp_to_field(&self) -> F { + >::to_field(self) + } + fn checked_from_field(field: F) -> Self::CheckedType { + Self::CheckedType::from_field(field) + } + fn lte(this: &Self::CheckedType, other: &Self::CheckedType, w: &mut Witness) -> Boolean { + Self::CheckedType::lte(this, other, w) + } +} + +impl_currency_common!(CheckedFee, Fee); + +// Implement CheckedCurrency for Balance (uses inherent to_field method) +impl CheckedCurrency for CheckedBalance { + type Inner = Balance; + fn to_field(&self) -> F { + self.0 + } + fn from_field(field: F) -> Self { + Self(field) + } + fn inner_to_field(inner: &Self::Inner) -> F { + Balance::to_field::(inner) + } + fn inner_of_field(field: F) -> Self::Inner { + use ark_ff::BigInteger256; + let amount: BigInteger256 = field.into(); + let amount: u64 = amount.0[0]; + Balance::from_u64(amount) + } +} + +impl ForZkappCheck for Balance { + type CheckedType = CheckedBalance; + fn zkapp_to_field(&self) -> F { + Balance::to_field::(self) + } + fn checked_from_field(field: F) -> Self::CheckedType { + Self::CheckedType::from_field(field) + } + fn lte(this: &Self::CheckedType, other: &Self::CheckedType, w: &mut Witness) -> Boolean { + Self::CheckedType::lte(this, other, w) + } } -impl_currency!( - {CheckedAmount, Amount}, - {CheckedFee, Fee}, - {CheckedBalance, Balance} -); +impl_currency_common!(CheckedBalance, Balance); diff --git a/ledger/src/proofs/numbers/mod.rs b/ledger/src/proofs/numbers/mod.rs index e0ed94cedf..2d06efe6e7 100644 --- a/ledger/src/proofs/numbers/mod.rs +++ b/ledger/src/proofs/numbers/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod currency; pub mod nat; + +// Re-export extension traits for easy import +pub use currency::{SignedToCheckedExt, ToCheckedExt}; diff --git a/ledger/src/proofs/numbers/nat.rs b/ledger/src/proofs/numbers/nat.rs index e09a83b4c0..9d4ee268ec 100644 --- a/ledger/src/proofs/numbers/nat.rs +++ b/ledger/src/proofs/numbers/nat.rs @@ -25,6 +25,12 @@ pub trait CheckedNat: fn to_field(&self) -> F; fn from_field(field: F) -> Self; + /// Convert the inner type to a field element. + fn inner_to_field(inner: &Self::Inner) -> F; + + /// Convert from a field element to the inner type. + fn inner_of_field(field: F) -> Self::Inner; + fn zero() -> Self { Self::from_field(F::zero()) } @@ -34,11 +40,11 @@ pub trait CheckedNat: } fn to_inner(&self) -> Self::Inner { - Self::Inner::of_field(self.to_field()) + Self::inner_of_field(self.to_field()) } fn from_inner(inner: Self::Inner) -> Self { - Self::from_field(inner.to_field()) + Self::from_field(Self::inner_to_field(&inner)) } /// >= @@ -225,7 +231,8 @@ impl CheckedLength { } } -macro_rules! impl_nat { +/// Macro for 32-bit nat types +macro_rules! impl_nat_32 { ($({$name:tt, $unchecked:tt}),*) => ($( #[derive(Copy, Clone, Debug)] @@ -239,6 +246,15 @@ macro_rules! impl_nat { fn from_field(field: F) -> Self { Self(field) } + fn inner_to_field(inner: &Self::Inner) -> F { + $unchecked::to_field::(inner) + } + fn inner_of_field(field: F) -> Self::Inner { + use ark_ff::BigInteger256; + let bigint: BigInteger256 = field.into(); + let value: u32 = bigint.0[0] as u32; + $unchecked::from_u32(value) + } } impl ToFieldElements for $name:: { @@ -268,6 +284,75 @@ macro_rules! impl_nat { impl ForZkappCheck for $unchecked { type CheckedType = $name; + fn zkapp_to_field(&self) -> F { + $unchecked::to_field::(self) + } + fn checked_from_field(field: F) -> Self::CheckedType { + Self::CheckedType::from_field(field) + } + fn lte(this: &Self::CheckedType, other: &Self::CheckedType, w: &mut Witness) -> Boolean { + Self::CheckedType::lte(this, other, w) + } + } + )*) +} + +/// Macro for 64-bit nat types +macro_rules! impl_nat_64 { + ($({$name:tt, $unchecked:tt}),*) => ($( + + #[derive(Copy, Clone, Debug)] + pub struct $name(F); + + impl CheckedNat for $name:: { + type Inner = $unchecked; + fn to_field(&self) -> F { + self.0 + } + fn from_field(field: F) -> Self { + Self(field) + } + fn inner_to_field(inner: &Self::Inner) -> F { + $unchecked::to_field::(inner) + } + fn inner_of_field(field: F) -> Self::Inner { + use ark_ff::BigInteger256; + let bigint: BigInteger256 = field.into(); + let value: u64 = bigint.0[0]; + $unchecked::from_u64(value) + } + } + + impl ToFieldElements for $name:: { + fn to_field_elements(&self, fields: &mut Vec) { + let Self(this) = self; + this.to_field_elements(fields) + } + } + + impl Check for $name:: { + fn check(&self, w: &mut Witness) { + range_check::(self.0, w); + } + } + + impl ToInputs for $name { + fn to_inputs(&self, inputs: &mut ::poseidon::hash::Inputs) { + self.to_inner().to_inputs(inputs) + } + } + + impl $unchecked { + pub fn to_checked(&self) -> $name { + $name::from_inner(*self) + } + } + + impl ForZkappCheck for $unchecked { + type CheckedType = $name; + fn zkapp_to_field(&self) -> F { + $unchecked::to_field::(self) + } fn checked_from_field(field: F) -> Self::CheckedType { Self::CheckedType::from_field(field) } @@ -278,13 +363,16 @@ macro_rules! impl_nat { )*) } -impl_nat!( +impl_nat_32!( {CheckedTxnVersion, TxnVersion}, {CheckedSlot, Slot}, {CheckedSlotSpan, SlotSpan}, {CheckedLength, Length}, {CheckedNonce, Nonce}, - {CheckedIndex, Index}, + {CheckedIndex, Index} +); + +impl_nat_64!( {CheckedBlockTime, BlockTime}, {CheckedBlockTimeSpan, BlockTimeSpan} ); @@ -301,6 +389,15 @@ impl CheckedNat for CheckedN { fn from_field(field: F) -> Self { Self(field) } + fn inner_to_field(inner: &Self::Inner) -> F { + crate::scan_state::currency::N::to_field::(inner) + } + fn inner_of_field(field: F) -> Self::Inner { + use ark_ff::BigInteger256; + let bigint: BigInteger256 = field.into(); + let value: u64 = bigint.0[0]; + crate::scan_state::currency::N::from_u64(value) + } } impl ToFieldElements for CheckedN { @@ -328,6 +425,15 @@ impl CheckedNat for CheckedN32 { fn from_field(field: F) -> Self { Self(field) } + fn inner_to_field(inner: &Self::Inner) -> F { + crate::scan_state::currency::N::to_field::(inner) + } + fn inner_of_field(field: F) -> Self::Inner { + use ark_ff::BigInteger256; + let bigint: BigInteger256 = field.into(); + let value: u64 = bigint.0[0]; + crate::scan_state::currency::N::from_u64(value) + } } impl ToFieldElements for CheckedN32 { diff --git a/ledger/src/proofs/to_field_elements.rs b/ledger/src/proofs/to_field_elements.rs index 51261e2689..5b35ff620f 100644 --- a/ledger/src/proofs/to_field_elements.rs +++ b/ledger/src/proofs/to_field_elements.rs @@ -1,3 +1,34 @@ +//! Field element conversion trait for proof inputs. +//! +//! # Refactoring Notes (mina-tx-type extraction) +//! +//! The `ToFieldElements` trait and many of its generic implementations have been +//! duplicated in the `mina-tx-type` crate. The goal is to eventually replace this +//! trait definition with an import from `mina-tx-type`. +//! +//! However, this replacement is currently blocked by Rust's orphan rules. To +//! complete the migration, the following steps need to be done first: +//! +//! 1. **Replace local type definitions with imports from mina-tx-type:** +//! - `FieldWitness` trait and impls +//! - `CircuitVar` enum +//! - `Boolean` enum +//! - `GroupAffine` type alias +//! These are defined in `proofs/field.rs` and need to be imported from +//! `mina_tx_type::proofs::field` instead. +//! +//! 2. **Move remaining implementations to mina-tx-type:** +//! Once the types are shared, the following implementations can be moved: +//! - `impl ToFieldElements for OpeningProof` (requires poly-commitment dep) +//! - `impl ToFieldElements for ProverCommitments` (requires kimchi dep) +//! - Various tuple implementations like `(&SetOrKeep, F)` +//! +//! 3. **Replace trait import:** +//! Finally, replace `pub trait ToFieldElements` with: +//! `pub use mina_tx_type::ToFieldElements;` +//! +//! See: mina-tx-type/src/proofs/to_field_elements.rs for the equivalent trait + use std::borrow::Cow; use crate::{ @@ -18,7 +49,7 @@ use crate::{ util::two_u64_to_field, }, scan_state::{ - currency::{self, Sgn}, + currency::{self, Sgn, SgnExt}, fee_excess::FeeExcess, pending_coinbase, scan_state::transaction_snark::{Registers, SokDigest, Statement}, @@ -45,6 +76,13 @@ pub trait ToFieldElementsDebug: ToFieldElements + std::fmt::Debug {} impl + std::fmt::Debug> ToFieldElementsDebug for T {} +/// Trait for converting values to field elements. +/// +/// This trait is used to serialize data structures into vectors of field +/// elements, which are the inputs to Mina's zero-knowledge proof circuits. +/// +/// **Note:** This trait is duplicated in `mina_tx_type::ToFieldElements`. +/// See module-level docs for the migration plan. pub trait ToFieldElements { fn to_field_elements(&self, fields: &mut Vec); diff --git a/ledger/src/proofs/transaction.rs b/ledger/src/proofs/transaction.rs index 01989b624e..feba9c80bc 100644 --- a/ledger/src/proofs/transaction.rs +++ b/ledger/src/proofs/transaction.rs @@ -39,7 +39,7 @@ use crate::{ wrap::{self, WrapParams}, }, scan_state::{ - currency::{self, Sgn}, + currency::{self, Amount, Fee, Sgn, Signed}, fee_excess::FeeExcess, pending_coinbase, scan_state::transaction_snark::{Registers, SokDigest, SokMessage, Statement}, @@ -52,6 +52,7 @@ use crate::{ use super::{ constants::ProofConstants, field::{field, Boolean, CircuitVar, FieldWitness, GroupAffine, ToBoolean}, + numbers::{SignedToCheckedExt, ToCheckedExt}, public_input::messages::{dummy_ipa_step_sg, MessagesForNextWrapProof}, step, step::{InductiveRule, OptFlag, StepProof}, @@ -3598,11 +3599,11 @@ pub mod transaction_snark { t2: &LocalState, w: &mut Witness, ) { - t1.excess.to_checked::().value(w); - t2.excess.to_checked::().value(w); + t1.excess.to_checked().value(w); + t2.excess.to_checked().value(w); - t1.supply_increase.to_checked::().value(w); - t2.supply_increase.to_checked::().value(w); + t1.supply_increase.to_checked().value(w); + t2.supply_increase.to_checked().value(w); } pub fn main( @@ -3666,7 +3667,10 @@ pub mod transaction_snark { // Checked.all_unit { let supply_increase = statement_with_sok.supply_increase; - w.exists_no_check(supply_increase.to_checked::().force_value()); + w.exists_no_check( + as SignedToCheckedExt>::to_checked(&supply_increase) + .force_value(), + ); let FeeExcess { fee_token_l: _, @@ -3675,8 +3679,12 @@ pub mod transaction_snark { fee_excess_r, } = statement_with_sok.fee_excess; - w.exists_no_check(fee_excess_l.to_checked::().force_value()); - w.exists_no_check(fee_excess_r.to_checked::().force_value()); + w.exists_no_check( + as SignedToCheckedExt>::to_checked(&fee_excess_l).force_value(), + ); + w.exists_no_check( + as SignedToCheckedExt>::to_checked(&fee_excess_r).force_value(), + ); } Ok(()) diff --git a/ledger/src/proofs/zkapp.rs b/ledger/src/proofs/zkapp.rs index 55c49daca8..576bcb8d62 100644 --- a/ledger/src/proofs/zkapp.rs +++ b/ledger/src/proofs/zkapp.rs @@ -65,6 +65,7 @@ use super::{ numbers::{ currency::{CheckedAmount, CheckedSigned}, nat::{CheckedIndex, CheckedSlot}, + SignedToCheckedExt, }, provers::devnet_circuit_directory, to_field_elements::ToFieldElements, diff --git a/ledger/src/scan_state/conv.rs b/ledger/src/scan_state/conv.rs index 0dc3724ea6..d5eab4ccd4 100644 --- a/ledger/src/scan_state/conv.rs +++ b/ledger/src/scan_state/conv.rs @@ -13,9 +13,8 @@ use mina_p2p_messages::{ self, BlockTimeTimeStableV1, ConsensusProofOfStakeDataEpochDataNextValueVersionedValueStableV1, ConsensusProofOfStakeDataEpochDataStakingValueVersionedValueStableV1, - CurrencyAmountStableV1, CurrencyBalanceStableV1, CurrencyFeeStableV1, - DataHashLibStateHashStableV1, EpochSeed, LedgerProofProdStableV2, - MinaBaseAccountIdDigestStableV1, MinaBaseAccountIdStableV2, + CurrencyAmountStableV1, CurrencyBalanceStableV1, DataHashLibStateHashStableV1, EpochSeed, + LedgerProofProdStableV2, MinaBaseAccountIdDigestStableV1, MinaBaseAccountIdStableV2, MinaBaseAccountUpdateBodyEventsStableV1, MinaBaseAccountUpdateBodyFeePayerStableV1, MinaBaseAccountUpdateBodyStableV1, MinaBaseAccountUpdateFeePayerStableV1, MinaBaseAccountUpdateMayUseTokenStableV1, MinaBaseAccountUpdatePreconditionsStableV1, @@ -56,8 +55,7 @@ use mina_p2p_messages::{ MinaNumbersGlobalSlotSpanStableV1, MinaStateBlockchainStateValueStableV2LedgerProofStatement, MinaStateBlockchainStateValueStableV2LedgerProofStatementSource, - MinaStateBlockchainStateValueStableV2SignedAmount, MinaStateSnarkedLedgerStateStableV2, - MinaStateSnarkedLedgerStateWithSokStableV2, + MinaStateSnarkedLedgerStateStableV2, MinaStateSnarkedLedgerStateWithSokStableV2, MinaTransactionLogicTransactionAppliedCoinbaseAppliedStableV2, MinaTransactionLogicTransactionAppliedCoinbaseAppliedStableV2Coinbase, MinaTransactionLogicTransactionAppliedCommandAppliedStableV2, @@ -73,7 +71,7 @@ use mina_p2p_messages::{ MinaTransactionLogicTransactionAppliedZkappCommandAppliedStableV1Command, MinaTransactionLogicZkappCommandLogicLocalStateValueStableV1, MinaTransactionTransactionStableV2, ParallelScanJobStatusStableV1, - ParallelScanSequenceNumberStableV1, ParallelScanWeightStableV1, SgnStableV1, SignedAmount, + ParallelScanSequenceNumberStableV1, ParallelScanWeightStableV1, SignedAmount, StagedLedgerDiffDiffDiffStableV2, StagedLedgerDiffDiffFtStableV1, StagedLedgerDiffDiffPreDiffWithAtMostOneCoinbaseStableV2, StagedLedgerDiffDiffPreDiffWithAtMostOneCoinbaseStableV2Coinbase, @@ -116,7 +114,7 @@ use crate::{ }; use super::{ - currency::{Amount, Balance, Fee, Index, Length, Nonce, Sgn, Signed, Slot, SlotSpan}, + currency::{Amount, Balance, Fee, Index, Length, Nonce, Signed, Slot, SlotSpan}, fee_excess::FeeExcess, parallel_scan::{self, JobStatus, ParallelScan, SequenceNumber}, pending_coinbase::{self, PendingCoinbase}, @@ -142,11 +140,7 @@ use super::{ }, }; -impl From for Amount { - fn from(value: CurrencyAmountStableV1) -> Self { - Self(value.as_u64()) - } -} +// Note: From for Amount is now in mina-tx-type impl From for Balance { fn from(value: CurrencyAmountStableV1) -> Self { @@ -154,13 +148,7 @@ impl From for Balance { } } -impl From for CurrencyAmountStableV1 { - fn from(value: Amount) -> Self { - Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( - value.as_u64().into(), - )) - } -} +// Note: From for CurrencyAmountStableV1 is now in mina-tx-type impl From<&Balance> for CurrencyBalanceStableV1 { fn from(value: &Balance) -> Self { @@ -176,51 +164,13 @@ impl From for CurrencyAmountStableV1 { } } -impl From<&SignedAmount> for Signed { - fn from(value: &SignedAmount) -> Self { - Self { - magnitude: Amount(value.magnitude.clone().as_u64()), - sgn: value.sgn.clone().into(), - } - } -} - -impl From<&Amount> for CurrencyAmountStableV1 { - fn from(value: &Amount) -> Self { - CurrencyAmountStableV1(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( - value.as_u64().into(), - )) - } -} - -impl From<&Amount> for CurrencyFeeStableV1 { - fn from(value: &Amount) -> Self { - CurrencyFeeStableV1(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( - value.as_u64().into(), - )) - } -} - -impl From<&Signed> for SignedAmount { - fn from(value: &Signed) -> Self { - Self { - magnitude: (&value.magnitude).into(), - sgn: (&value.sgn).into(), - } - } -} - -impl From<&CurrencyFeeStableV1> for Fee { - fn from(value: &CurrencyFeeStableV1) -> Self { - Self(value.as_u64()) - } -} - -impl From<&CurrencyAmountStableV1> for Fee { - fn from(value: &CurrencyAmountStableV1) -> Self { - Self(value.as_u64()) - } -} +// Note: Multiple Amount/Fee/Signed conversions are now in mina-tx-type: +// - From<&SignedAmount> for Signed +// - From<&Amount> for CurrencyAmountStableV1 +// - From<&Amount> for CurrencyFeeStableV1 +// - From<&Signed> for SignedAmount +// - From<&CurrencyFeeStableV1> for Fee +// - From<&CurrencyAmountStableV1> for Fee impl From<&Nonce> for mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1 { fn from(value: &Nonce) -> Self { @@ -258,57 +208,13 @@ impl From<&Length> for mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1 { } } -impl From for Sgn { - fn from(value: SgnStableV1) -> Self { - match value { - SgnStableV1::Pos => Self::Pos, - SgnStableV1::Neg => Self::Neg, - } - } -} - -impl From<&SignedAmount> for Signed { - fn from(value: &SignedAmount) -> Self { - Self { - magnitude: (&value.magnitude).into(), - sgn: value.sgn.clone().into(), - } - } -} - -impl From<&Sgn> for SgnStableV1 { - fn from(value: &Sgn) -> Self { - match value { - Sgn::Pos => Self::Pos, - Sgn::Neg => Self::Neg, - } - } -} - -impl From<&Fee> for CurrencyFeeStableV1 { - fn from(value: &Fee) -> Self { - Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( - value.as_u64().into(), - )) - } -} - -impl From<&Fee> for CurrencyAmountStableV1 { - fn from(value: &Fee) -> Self { - Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( - value.as_u64().into(), - )) - } -} - -impl From<&Signed> for SignedAmount { - fn from(value: &Signed) -> Self { - Self { - magnitude: (&value.magnitude).into(), - sgn: (&value.sgn).into(), - } - } -} +// Note: Sgn and Fee conversions are now in mina-tx-type: +// - From for Sgn +// - From<&SignedAmount> for Signed +// - From<&Sgn> for SgnStableV1 +// - From<&Fee> for CurrencyFeeStableV1 +// - From<&Fee> for CurrencyAmountStableV1 +// - From<&Signed> for SignedAmount impl TryFrom<&MinaBaseFeeExcessStableV1> for FeeExcess { type Error = InvalidBigInt; @@ -552,27 +458,9 @@ impl TryFrom<&MinaStateBlockchainStateValueStableV2LedgerProofStatementSource> f } } -impl From<&MinaStateBlockchainStateValueStableV2SignedAmount> for Signed { - fn from(value: &MinaStateBlockchainStateValueStableV2SignedAmount) -> Self { - let MinaStateBlockchainStateValueStableV2SignedAmount { magnitude, sgn } = value; - - Self { - magnitude: (magnitude.clone()).into(), - sgn: (sgn.clone()).into(), - } - } -} - -impl From<&Signed> for MinaStateBlockchainStateValueStableV2SignedAmount { - fn from(value: &Signed) -> Self { - let Signed:: { magnitude, sgn } = value; - - Self { - magnitude: (*magnitude).into(), - sgn: sgn.into(), - } - } -} +// Note: From<&MinaStateBlockchainStateValueStableV2SignedAmount> for Signed +// and From<&Signed> for MinaStateBlockchainStateValueStableV2SignedAmount +// are now in mina-tx-type impl TryFrom<&MinaStateBlockchainStateValueStableV2LedgerProofStatement> for Statement<()> { type Error = InvalidBigInt; diff --git a/ledger/src/scan_state/currency.rs b/ledger/src/scan_state/currency.rs index d7c4862e74..80db37e3f3 100644 --- a/ledger/src/scan_state/currency.rs +++ b/ledger/src/scan_state/currency.rs @@ -1,5 +1,3 @@ -use std::cmp::Ordering::{Equal, Greater, Less}; - use ark_ff::{BigInteger256, Field}; use mina_p2p_messages::v2::BlockTimeTimeStableV1; use rand::Rng; @@ -8,233 +6,131 @@ use crate::proofs::{ field::FieldWitness, to_field_elements::ToFieldElements, transaction::Check, witness::Witness, }; -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Sgn { - Pos, - Neg, +// Re-export Amount, Fee, Signed, Magnitude, MinMax, and Sgn from mina-tx-type +pub use mina_tx_type::currency::{Amount, Fee, Magnitude, MagnitudeFieldExt, MinMax, Sgn, Signed}; + +/// Extension trait to provide `to_field` for Amount and Fee in the ledger crate. +/// +/// This is needed because we can't implement `MagnitudeFieldExt` directly due to +/// orphan rules - both the trait and the types are defined in mina-tx-type. +pub trait AmountFeeFieldExt { + fn to_field(&self) -> F; + fn of_field(field: F) -> Self; } -impl Sgn { - pub fn is_pos(&self) -> bool { - match self { - Sgn::Pos => true, - Sgn::Neg => false, - } +impl AmountFeeFieldExt for Amount { + fn to_field(&self) -> F { + let int = self.as_u64(); + F::from(int) } - pub fn negate(&self) -> Self { - match self { - Sgn::Pos => Sgn::Neg, - Sgn::Neg => Sgn::Pos, - } - } - - pub fn to_field(&self) -> F { - match self { - Sgn::Pos => F::one(), - Sgn::Neg => F::one().neg(), - } + fn of_field(field: F) -> Self { + let amount: BigInteger256 = field.into(); + let amount: u64 = amount.0[0]; + Self::from_u64(amount) } } -pub trait Magnitude -where - Self: Sized + PartialOrd + Copy, -{ - const NBITS: usize; - - fn abs_diff(&self, rhs: &Self) -> Self; - fn wrapping_add(&self, rhs: &Self) -> Self; - fn wrapping_mul(&self, rhs: &Self) -> Self; - fn wrapping_sub(&self, rhs: &Self) -> Self; - fn checked_add(&self, rhs: &Self) -> Option; - fn checked_mul(&self, rhs: &Self) -> Option; - fn checked_sub(&self, rhs: &Self) -> Option; - fn checked_div(&self, rhs: &Self) -> Option; - fn checked_rem(&self, rhs: &Self) -> Option; - - fn is_zero(&self) -> bool; - fn zero() -> Self; - - fn add_flagged(&self, rhs: &Self) -> (Self, bool) { - let z = self.wrapping_add(rhs); - (z, z < *self) +impl AmountFeeFieldExt for Fee { + fn to_field(&self) -> F { + let int = self.as_u64(); + F::from(int) } - fn sub_flagged(&self, rhs: &Self) -> (Self, bool) { - (self.wrapping_sub(rhs), self < rhs) + fn of_field(field: F) -> Self { + let amount: BigInteger256 = field.into(); + let amount: u64 = amount.0[0]; + Self::from_u64(amount) } - - fn to_field(&self) -> F; - fn of_field(field: F) -> Self; } -/// Trait used for default values with `ClosedInterval` -pub trait MinMax { - fn min() -> Self; - fn max() -> Self; -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Signed { - pub magnitude: T, - pub sgn: Sgn, -} - -impl Signed -where - T: Magnitude + PartialOrd + Ord + Clone, -{ - const NBITS: usize = T::NBITS; - - pub fn create(magnitude: T, sgn: Sgn) -> Self { - Self { - magnitude, - sgn: if magnitude.is_zero() { Sgn::Pos } else { sgn }, - } +// Ledger-specific trait implementations for Amount and Fee +impl crate::ToInputs for Amount { + fn to_inputs(&self, inputs: &mut poseidon::hash::Inputs) { + inputs.append_u64(self.0); } +} - pub fn of_unsigned(magnitude: T) -> Self { - Self::create(magnitude, Sgn::Pos) +impl crate::ToInputs for Fee { + fn to_inputs(&self, inputs: &mut poseidon::hash::Inputs) { + inputs.append_u64(self.0); } +} - pub fn negate(&self) -> Self { - if self.magnitude.is_zero() { - Self::zero() - } else { - Self { - magnitude: self.magnitude, - sgn: self.sgn.negate(), - } - } +impl ToFieldElements for Amount { + fn to_field_elements(&self, fields: &mut Vec) { + fields.push(>::to_field(self)); } +} - pub fn is_pos(&self) -> bool { - matches!(self.sgn, Sgn::Pos) +impl ToFieldElements for Fee { + fn to_field_elements(&self, fields: &mut Vec) { + fields.push(>::to_field(self)); } +} - /// - pub fn is_non_neg(&self) -> bool { - matches!(self.sgn, Sgn::Pos) - } +impl Check for Amount { + fn check(&self, witnesses: &mut Witness) { + use crate::proofs::transaction::scalar_challenge::to_field_checked_prime; - pub fn is_neg(&self) -> bool { - matches!(self.sgn, Sgn::Neg) - } + const NBITS: usize = u64::BITS as usize; - /// - pub fn zero() -> Self { - Self { - magnitude: T::zero(), - sgn: Sgn::Pos, - } - } + let number: u64 = self.as_u64(); + assert_eq!(NBITS, std::mem::size_of_val(&number) * 8); - pub fn is_zero(&self) -> bool { - self.magnitude.is_zero() //&& matches!(self.sgn, Sgn::Pos) + let number: F = number.into(); + to_field_checked_prime::(number, witnesses); } +} - /// - pub fn add(&self, rhs: &Self) -> Option { - let (magnitude, sgn) = if self.sgn == rhs.sgn { - let magnitude = self.magnitude.checked_add(&rhs.magnitude)?; - let sgn = self.sgn; +impl Check for Fee { + fn check(&self, witnesses: &mut Witness) { + use crate::proofs::transaction::scalar_challenge::to_field_checked_prime; - (magnitude, sgn) - } else { - let sgn = match self.magnitude.cmp(&rhs.magnitude) { - Less => rhs.sgn, - Greater => self.sgn, - Equal => return Some(Self::zero()), - }; - let magnitude = self.magnitude.abs_diff(&rhs.magnitude); - - (magnitude, sgn) - }; + const NBITS: usize = u64::BITS as usize; - Some(Self::create(magnitude, sgn)) - } + let number: u64 = self.as_u64(); + assert_eq!(NBITS, std::mem::size_of_val(&number) * 8); - pub fn add_flagged(&self, rhs: Self) -> (Self, bool) { - match (self.sgn, rhs.sgn) { - (Sgn::Neg, sgn @ Sgn::Neg) | (Sgn::Pos, sgn @ Sgn::Pos) => { - let (magnitude, overflow) = self.magnitude.add_flagged(&rhs.magnitude); - (Self { magnitude, sgn }, overflow) - } - (Sgn::Pos, Sgn::Neg) | (Sgn::Neg, Sgn::Pos) => { - let sgn = match self.magnitude.cmp(&rhs.magnitude) { - Less => rhs.sgn, - Greater => self.sgn, - Equal => Sgn::Pos, - }; - let magnitude = self.magnitude.abs_diff(&rhs.magnitude); - (Self { magnitude, sgn }, false) - } - } + let number: F = number.into(); + to_field_checked_prime::(number, witnesses); } } -impl Signed { - pub fn to_fee(self) -> Signed { - let Self { magnitude, sgn } = self; +// Extension trait for Sgn with ledger-specific field conversion +pub trait SgnExt { + fn to_field(&self) -> F; +} - Signed { - magnitude: Fee(magnitude.0), - sgn, +impl SgnExt for Sgn { + fn to_field(&self) -> F { + match self { + Sgn::Pos => F::one(), + Sgn::Neg => F::one().neg(), } } } -impl Signed +/// Extension trait for Signed with ledger-specific random generation. +pub trait SignedExt { + fn gen() -> Self; +} + +impl SignedExt for Signed where - T: Magnitude + PartialOrd + Ord + Clone, + T: Magnitude, rand::distributions::Standard: rand::distributions::Distribution, { - pub fn gen() -> Self { - let mut rng = rand::thread_rng(); - - let magnitude: T = rng.gen(); - let sgn = if rng.gen::() { - Sgn::Pos - } else { - Sgn::Neg - }; - - Self::create(magnitude, sgn) - } -} - -impl Amount { - /// The number of nanounits in a unit. User for unit transformations. - const UNIT_TO_NANO: u64 = 1_000_000_000; - - pub fn of_fee(fee: &Fee) -> Self { - Self(fee.0) - } - - pub fn add_signed_flagged(&self, rhs: Signed) -> (Self, bool) { - if let Sgn::Pos = rhs.sgn { - self.add_flagged(&rhs.magnitude) - } else { - self.sub_flagged(&rhs.magnitude) + fn gen() -> Self { + Self { + magnitude: rand::random(), + sgn: if rand::random::() { + Sgn::Pos + } else { + Sgn::Neg + }, } } - - pub fn to_nanomina_int(&self) -> Self { - *self - } - - pub fn to_mina_int(&self) -> Self { - Self(self.0.checked_div(Self::UNIT_TO_NANO).unwrap()) - } - - pub fn of_mina_int_exn(int: u64) -> Self { - Self::from_u64(int).scale(Self::UNIT_TO_NANO).unwrap() - } - - pub fn of_nanomina_int_exn(int: u64) -> Self { - Self::from_u64(int) - } } impl Balance { @@ -281,12 +177,6 @@ impl Balance { } } -impl Fee { - pub const fn of_nanomina_int_exn(int: u64) -> Self { - Self::from_u64(int) - } -} - impl Index { // TODO: Not sure if OCaml wraps around here pub fn incr(&self) -> Self { @@ -440,12 +330,14 @@ macro_rules! impl_number { fn abs_diff(&self, rhs: &Self) -> Self { Self(self.0.abs_diff(rhs.0)) } + } - fn to_field(&self) -> F { - self.to_field() + impl MagnitudeFieldExt for $name { + fn to_field(&self) -> F { + <$name>::to_field::(self) } - fn of_field(field: F) -> Self { + fn of_field(field: F) -> Self { let amount: BigInteger256 = field.into(); let amount: $inner = amount.0[0].try_into().unwrap(); @@ -574,5 +466,5 @@ macro_rules! impl_number { impl_number!( 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, Epoch, }, - 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, N, }, + 64: { Balance, BlockTime, BlockTimeSpan, N, }, ); diff --git a/ledger/src/scan_state/fee_excess.rs b/ledger/src/scan_state/fee_excess.rs index cb4756ccb5..fb8885613c 100644 --- a/ledger/src/scan_state/fee_excess.rs +++ b/ledger/src/scan_state/fee_excess.rs @@ -38,7 +38,10 @@ use poseidon::hash::Inputs; use crate::{ proofs::{ field::{field, Boolean, FieldWitness}, - numbers::currency::{CheckedFee, CheckedSigned}, + numbers::{ + currency::{CheckedFee, CheckedSigned}, + SignedToCheckedExt, + }, witness::Witness, }, AppendToInputs, ToInputs, TokenId, @@ -291,10 +294,10 @@ impl FeeExcess { w: &mut Witness, ) -> (TokenId, Signed, TokenId, Signed) { // Represent amounts as field elements. - let fee_excess1_l = fee_excess1_l.to_checked::().value(w); - let fee_excess1_r = fee_excess1_r.to_checked::().value(w); - let fee_excess2_l = fee_excess2_l.to_checked::().value(w); - let fee_excess2_r = fee_excess2_r.to_checked::().value(w); + let fee_excess1_l = fee_excess1_l.to_checked().value(w); + let fee_excess1_r = fee_excess1_r.to_checked().value(w); + let fee_excess2_l = fee_excess2_l.to_checked().value(w); + let fee_excess2_r = fee_excess2_r.to_checked().value(w); let ((fee_token1_l, fee_excess1_l), (fee_token2_l, fee_excess2_l)) = eliminate_fee_excess_checked( @@ -324,10 +327,10 @@ impl FeeExcess { }; let fee_excess_l = w.exists(convert_to_currency(fee_excess_l)); - fee_excess_l.to_checked::().value(w); // Made by `Fee.Signed.Checked.to_field_var` call + fee_excess_l.to_checked().value(w); // Made by `Fee.Signed.Checked.to_field_var` call let fee_excess_r = w.exists(convert_to_currency(fee_excess_r)); - fee_excess_r.to_checked::().value(w); // Made by `Fee.Signed.Checked.to_field_var` call + fee_excess_r.to_checked().value(w); // Made by `Fee.Signed.Checked.to_field_var` call (fee_token_l, fee_excess_l, fee_token_r, fee_excess_r) } @@ -365,8 +368,8 @@ fn eliminate_fee_excess<'a>( } pub fn assert_equal_checked(_t1: &FeeExcess, t2: &FeeExcess, w: &mut Witness) { - t2.fee_excess_l.to_checked::().value(w); - t2.fee_excess_r.to_checked::().value(w); + t2.fee_excess_l.to_checked().value(w); + t2.fee_excess_r.to_checked().value(w); } fn eliminate_fee_excess_checked<'a>( diff --git a/ledger/src/scan_state/pending_coinbase.rs b/ledger/src/scan_state/pending_coinbase.rs index 0f883a9cf3..1ce99e907a 100644 --- a/ledger/src/scan_state/pending_coinbase.rs +++ b/ledger/src/scan_state/pending_coinbase.rs @@ -40,6 +40,7 @@ use crate::{ numbers::{ currency::{CheckedAmount, CheckedCurrency}, nat::{CheckedNat, CheckedSlot}, + ToCheckedExt, }, transaction::transaction_snark::checked_hash, witness::Witness, @@ -754,8 +755,8 @@ impl PendingCoinbase { let superchaged_coinbase = coinbase_amount .scale(constraint_constants().supercharged_coinbase_factor) .unwrap() - .to_checked::(); - let coinbase_amount = coinbase_amount.to_checked::(); + .to_checked(); + let coinbase_amount = coinbase_amount.to_checked(); match supercharge_coinbase { Boolean::True => superchaged_coinbase, diff --git a/ledger/src/scan_state/transaction_logic/local_state.rs b/ledger/src/scan_state/transaction_logic/local_state.rs index dacb137740..d8ed924b06 100644 --- a/ledger/src/scan_state/transaction_logic/local_state.rs +++ b/ledger/src/scan_state/transaction_logic/local_state.rs @@ -8,7 +8,7 @@ use super::{ use crate::{ proofs::{ field::{field, Boolean, ToBoolean}, - numbers::nat::CheckedNat, + numbers::{nat::CheckedNat, SignedToCheckedExt}, to_field_elements::ToFieldElements, witness::Witness, }, @@ -532,16 +532,14 @@ impl LocalState { other.full_transaction_commitment, w, ), - excess - .to_checked::() - .equal(&other.excess.to_checked(), w), + excess.to_checked().equal(&other.excess.to_checked(), w), supply_increase - .to_checked::() + .to_checked() .equal(&other.supply_increase.to_checked(), w), field::equal(*ledger, other.ledger, w), success.to_boolean().equal(&other.success.to_boolean(), w), account_update_index - .to_checked::() + .to_checked() .equal(&other.account_update_index.to_checked(), w), Boolean::True, will_succeed diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs index 66a9b6b78f..d93dcb5963 100644 --- a/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs @@ -14,7 +14,8 @@ use crate::{ }, scan_state::{ currency::{ - Amount, Balance, Fee, Length, Magnitude, MinMax, Nonce, Sgn, Signed, Slot, SlotSpan, + Amount, Balance, Fee, Length, Magnitude, MinMax, Nonce, Sgn, Signed, SignedExt, Slot, + SlotSpan, }, fee_excess::FeeExcess, GenesisConstant, GENESIS_CONSTANT, diff --git a/ledger/src/zkapps/snark.rs b/ledger/src/zkapps/snark.rs index c9c5423e4a..23cb7b6489 100644 --- a/ledger/src/zkapps/snark.rs +++ b/ledger/src/zkapps/snark.rs @@ -19,6 +19,7 @@ use crate::{ numbers::{ currency::{CheckedAmount, CheckedBalance, CheckedCurrency, CheckedSigned}, nat::{CheckedIndex, CheckedNat, CheckedSlot}, + SignedToCheckedExt, ToCheckedExt, }, to_field_elements::ToFieldElements, transaction::{ diff --git a/mina-tx-type/Cargo.toml b/mina-tx-type/Cargo.toml new file mode 100644 index 0000000000..72e9a43eb5 --- /dev/null +++ b/mina-tx-type/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mina-tx-type" +version = "0.18.1" +edition = "2021" +license = "Apache-2.0" +description = "Transaction types for the Mina Protocol" + +[dependencies] +ark-ec = { workspace = true } +ark-ff = { workspace = true } +kimchi = { workspace = true } +mina-curves = { workspace = true } +mina-p2p-messages = { workspace = true } +mina-poseidon = { workspace = true } +mina-signer = { workspace = true } +poseidon = { workspace = true } +rand = { workspace = true } +serde = { workspace = true, features = ["derive"] } diff --git a/mina-tx-type/EXTRACTION_ANALYSIS.md b/mina-tx-type/EXTRACTION_ANALYSIS.md new file mode 100644 index 0000000000..1891456855 --- /dev/null +++ b/mina-tx-type/EXTRACTION_ANALYSIS.md @@ -0,0 +1,207 @@ +# Coinbase Type Extraction Analysis + +This document analyzes the types, traits, and macros that would need to be moved +to extract coinbase transaction types into the `mina-tx-type` crate. + +## Overview + +The coinbase types (`Coinbase`, `CoinbaseFeeTransfer`) depend on currency types +(`Amount`, `Fee`) which are generated by a macro and implement several traits +with deep dependencies on proof-related code. + +## Macros Used for Currency Types + +### `impl_number!` Macro + +Location: `ledger/src/scan_state/currency.rs:382-573` + +This is the **only macro** used to implement the currency types. It's a +two-level macro: + +**Invocation (line 575-578):** + +```rust +impl_number!( + 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, Epoch, }, + 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, N, }, +); +``` + +**What it generates for each type:** + +| Generated Item | Description | +| --------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `struct $name(pub(super) $inner)` | Newtype wrapper around u32 or u64 | +| `impl Debug` | Custom debug formatting | +| `impl Magnitude` | All numeric operations trait | +| `impl MinMax` | Min/max value trait | +| Inherent methods | `as_u64()`, `from_u64()`, `scale()`, `min()`, `max()`, `of_mina_string_exn()`, `to_bits()`, `to_field()` | +| `impl Distribution<$name>` | Random number generation | +| `impl ToInputs` | Poseidon hash inputs | +| `impl ToFieldElements` | Field element conversion | +| `impl Check` | Proof witness checking | + +**Derives (line 388):** + +```rust +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +``` + +## Manually Implemented Types (No Macros) + +### `Sgn` Enum (lines 11-38) + +```rust +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Sgn { Pos, Neg } +``` + +Manual implementations: + +- `is_pos()` - Check if positive +- `negate()` - Flip sign +- `to_field()` - Convert to field element (proof-related) + +### `Magnitude` Trait (lines 40-70) + +Manually defined trait with: + +- **Constants:** `NBITS` +- **Required methods:** `abs_diff`, `wrapping_add`, `wrapping_mul`, + `wrapping_sub`, `checked_add`, `checked_mul`, `checked_sub`, `checked_div`, + `checked_rem`, `is_zero`, `zero`, `to_field`, `of_field` +- **Default methods:** `add_flagged`, `sub_flagged` + +### `MinMax` Trait (lines 72-76) + +Simple trait with `min()` and `max()` methods. + +### `Signed` Struct (lines 78-205) + +```rust +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Signed { + pub magnitude: T, + pub sgn: Sgn, +} +``` + +- Generic over any `Magnitude` type +- Manual implementations: `create`, `of_unsigned`, `negate`, `is_pos`, `is_neg`, + `zero`, `is_zero`, `add`, `add_flagged`, `gen` +- Specialized impl for `Signed::to_fee()` + +### Type-Specific Implementations (lines 207-380) + +Additional methods for specific types (not generated by macro): + +| Type | Additional Methods | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `Amount` | `of_fee()`, `add_signed_flagged()`, `to_nanomina_int()`, `to_mina_int()`, `of_mina_int_exn()`, `of_nanomina_int_exn()` | +| `Balance` | `sub_amount()`, `add_amount()`, `add_signed_flagged()`, `add_signed_amount_flagged()`, `to_amount()`, `from_mina()`, `of_nanomina_int_exn()` | +| `Fee` | `of_nanomina_int_exn()` | +| `Index` | `incr()` | +| `Nonce` | `incr()`, `succ()`, `add_signed_flagged()`, `between()` | +| `BlockTime` | `add()`, `sub()`, `diff()`, `to_span_since_epoch()`, `of_span_since_epoch()`, `From` | +| `BlockTimeSpan` | `of_ms()`, `to_ms()` | +| `Slot` | `incr()`, `add()`, `succ()`, `gen_small()` | + +## External Dependencies in the Macro + +The macro references these external items: + +| Dependency | Used In | From | +| ------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------- | +| `ark_ff::BigInteger256` | `of_field()` | ark-ff crate | +| `ark_ff::Field` | `to_field()` | ark-ff crate | +| `FieldWitness` | `Magnitude::to_field/of_field`, `ToFieldElements`, `Check` | `crate::proofs::field` | +| `ToFieldElements` | trait impl | `crate::proofs::to_field_elements` | +| `Check` | trait impl | `crate::proofs::transaction` | +| `Witness` | `Check::check()` | `crate::proofs::witness` | +| `poseidon::hash::Inputs` | `ToInputs` | poseidon crate | +| `bits_iter` | `to_bits()` | `crate::proofs::transaction::legacy_input` | +| `to_field_checked_prime` | `Check::check()` | `crate::proofs::transaction::scalar_challenge` | +| `rand::Rng`, `rand::distributions::*` | random generation | rand crate | + +## Types Required for Complete Coinbase Extraction + +### Tier 1: Core Types (Minimum Required) + +| Type | Location | Description | +| --------------------- | ---------------------------------- | ------------------------------ | +| `Coinbase` | `transaction_logic/mod.rs:437-541` | Block reward transaction | +| `CoinbaseFeeTransfer` | `transaction_logic/mod.rs:415-432` | Fee transfer to SNARK worker | +| `Amount` | `currency.rs` (via macro) | Currency amount (u64 wrapper) | +| `Fee` | `currency.rs` (via macro) | Transaction fee (u64 wrapper) | +| `CompressedPubKey` | `mina_signer` (external) | Already in `mina-signer` crate | + +### Tier 2: Types Used in Coinbase Methods + +| Type | Location | Used By | +| ------------------- | -------------------------- | --------------------------------------------------------- | +| `AccountId` | `account/account.rs:1022` | `Coinbase::receiver()`, `Coinbase::accounts_referenced()` | +| `TokenId` | `account/account.rs:50` | Part of `AccountId` | +| `FeeExcess` | `fee_excess.rs` | `Coinbase::fee_excess()` | +| `TransactionStatus` | `transaction_logic/mod.rs` | `Coinbase::account_access_statuses()` | +| `AccessedOrNot` | `zkapp_command/mod.rs` | `Coinbase::account_access_statuses()` | + +### Tier 3: Traits and Supporting Types + +| Type/Trait | Location | Description | +| -------------------- | --------------------- | ------------------------------------ | +| `Magnitude` | `currency.rs:40-70` | Core trait for numeric operations | +| `Sgn` | `currency.rs:11-38` | Sign enum (Pos/Neg) | +| `Signed` | `currency.rs:78-205` | Generic signed magnitude type | +| `MinMax` | `currency.rs:72-76` | Min/max value trait | +| `impl_number!` macro | `currency.rs:382-578` | Generates Amount/Fee implementations | + +### Tier 4: Conversion Types (From P2P Messages) + +| Type | Location | Notes | +| -------------------------------- | ------------------- | ------------------------------------- | +| `MinaBaseCoinbaseStableV1` | `mina-p2p-messages` | Keep `From`/`TryFrom` impls in ledger | +| `StagedLedgerDiffDiffFtStableV1` | `mina-p2p-messages` | For CoinbaseFeeTransfer conversions | + +## Extraction Strategy + +For a minimal extraction to `mina-tx-type`, you would need to: + +### Move without proof dependencies + +- `Sgn` enum (simple) +- `Magnitude` trait (simplified, without `to_field`/`of_field`) +- `MinMax` trait (simple) +- `Signed` struct (without `gen()`) +- Simplified `impl_number!` macro (without proof-related impls) +- `Amount` and `Fee` types with basic operations + +### Keep in ledger + +- `ToFieldElements`, `Check`, `ToInputs` implementations +- `to_field()`, `of_field()`, `to_bits()` methods +- All proof-related code +- `From`/`TryFrom` implementations for p2p message types +- Methods returning `AccountId`, `FeeExcess`, `TransactionStatus` + +## Current Implementation + +The current `mina-tx-type` crate provides standalone types with simplified +`Amount` and `Fee` implementations that don't depend on the proof system. The +ledger crate continues to define its own tightly-integrated versions. + +This approach allows external projects to use the transaction types without +pulling in the full proof system dependencies, while the ledger crate maintains +full functionality for block validation and proof generation. + +## Related Issues + +- [#1665](https://github.com/o1-labs/mina-rust/issues/1665) - Extract + transaction types into a crate (parent issue) +- [#1824](https://github.com/o1-labs/mina-rust/issues/1824) - Extract coinbase + transaction types into mina-tx-type crate +- [#1825](https://github.com/o1-labs/mina-rust/issues/1825) - Extract fee + transfer transaction types +- [#1826](https://github.com/o1-labs/mina-rust/issues/1826) - Extract signed + command types +- [#1827](https://github.com/o1-labs/mina-rust/issues/1827) - Extract zkApp + command types diff --git a/mina-tx-type/src/coinbase.rs b/mina-tx-type/src/coinbase.rs new file mode 100644 index 0000000000..b346f5323b --- /dev/null +++ b/mina-tx-type/src/coinbase.rs @@ -0,0 +1,294 @@ +//! Coinbase transaction types for the Mina Protocol +//! +//! This module provides types for coinbase transactions, which are +//! system-generated transactions that reward block producers for successfully +//! producing blocks. +//! +//! # Overview +//! +//! When a block producer creates a new block, they receive a coinbase reward. +//! This reward can optionally include a fee transfer to a SNARK worker who +//! provided proofs for transactions in the block. +//! +//! # Types +//! +//! - [`Coinbase`]: The main coinbase transaction containing the reward +//! - [`CoinbaseFeeTransfer`]: Optional fee transfer to a SNARK worker + +use mina_signer::CompressedPubKey; + +use crate::currency::{Amount, Fee}; + +/// A fee transfer from a coinbase reward to a SNARK worker. +/// +/// When a block contains transactions that were proved by a SNARK worker, +/// the block producer can allocate a portion of the coinbase reward to that +/// worker as compensation for their proof work. +/// +/// # Fields +/// +/// * `receiver_pk` - The public key of the SNARK worker receiving the fee +/// * `fee` - The fee amount being transferred to the SNARK worker +/// +/// # Constraints +/// +/// The fee must not exceed the total coinbase amount. This constraint is +/// enforced when creating a [`Coinbase`] transaction. +/// +/// # OCaml Reference +/// +/// This type corresponds to `Coinbase.Fee_transfer.t` in the OCaml +/// implementation at `src/lib/mina_base/coinbase.ml`. +#[derive(Debug, Clone, PartialEq)] +pub struct CoinbaseFeeTransfer { + /// The public key of the SNARK worker receiving the fee. + /// + /// This is typically the public key that the SNARK worker registered when + /// submitting their proof work to the network. + pub receiver_pk: CompressedPubKey, + + /// The fee amount being transferred to the SNARK worker. + /// + /// This value is in nanomina (1 MINA = 1,000,000,000 nanomina). + /// The fee represents compensation for the computational work of + /// generating zero-knowledge proofs. + pub fee: Fee, +} + +impl CoinbaseFeeTransfer { + /// Creates a new coinbase fee transfer. + /// + /// # Arguments + /// + /// * `receiver_pk` - The public key of the SNARK worker receiving the fee + /// * `fee` - The fee amount to transfer + /// + /// # Examples + /// + /// ```ignore + /// use mina_tx_type::{CoinbaseFeeTransfer, Fee, CompressedPubKey}; + /// + /// let receiver = CompressedPubKey::from_address("B62...").unwrap(); + /// let fee = Fee::from_u64(10_000_000); // 0.01 MINA + /// let transfer = CoinbaseFeeTransfer::create(receiver, fee); + /// ``` + pub fn create(receiver_pk: CompressedPubKey, fee: Fee) -> Self { + Self { receiver_pk, fee } + } +} + +/// A coinbase transaction rewarding a block producer. +/// +/// Coinbase transactions are system-generated transactions that reward the +/// block producer for successfully producing a block. The reward consists of: +/// +/// 1. A fixed block reward (currently 720 MINA on mainnet) +/// 2. Optionally, a fee transfer to compensate a SNARK worker +/// +/// # Fields +/// +/// * `receiver` - The public key of the block producer receiving the reward +/// * `amount` - The total coinbase amount (before any fee transfer deduction) +/// * `fee_transfer` - Optional fee transfer to a SNARK worker +/// +/// # Validation +/// +/// When creating a coinbase transaction: +/// - If a fee transfer is specified, the fee must not exceed the coinbase +/// amount +/// - If the fee transfer receiver is the same as the coinbase receiver, the +/// fee transfer is removed (as the producer receives the full amount anyway) +/// +/// # OCaml Reference +/// +/// This type corresponds to `Coinbase.t` in the OCaml implementation at +/// `src/lib/mina_base/coinbase.ml` lines 17-21. +/// +/// OCaml reference: src/lib/mina_base/coinbase.ml L:17-21 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 +#[derive(Debug, Clone, PartialEq)] +pub struct Coinbase { + /// The public key of the block producer receiving the coinbase reward. + /// + /// This is the public key that the block producer used to stake and + /// was selected to produce the block. + pub receiver: CompressedPubKey, + + /// The total coinbase amount before any fee transfer deduction. + /// + /// This value is in nanomina (1 MINA = 1,000,000,000 nanomina). + /// On mainnet, this is typically 720 MINA for a full coinbase. + pub amount: Amount, + + /// Optional fee transfer to a SNARK worker. + /// + /// If present, a portion of the coinbase is transferred to the SNARK + /// worker who provided proofs for transactions in the block. The fee + /// is deducted from the coinbase amount when computing the producer's + /// actual reward. + /// + /// This field is `None` when: + /// - No SNARK work was included in the block + /// - The SNARK worker is the same as the block producer + pub fee_transfer: Option, +} + +impl Coinbase { + /// Validates that the coinbase transaction is well-formed. + /// + /// A coinbase is valid if: + /// - There is no fee transfer, OR + /// - The fee transfer amount does not exceed the coinbase amount + fn is_valid(&self) -> bool { + match &self.fee_transfer { + None => true, + Some(CoinbaseFeeTransfer { fee, .. }) => Amount::of_fee(fee) <= self.amount, + } + } + + /// Creates a new coinbase transaction. + /// + /// # Arguments + /// + /// * `amount` - The total coinbase amount + /// * `receiver` - The public key of the block producer + /// * `fee_transfer` - Optional fee transfer to a SNARK worker + /// + /// # Returns + /// + /// Returns `Ok(Coinbase)` if the coinbase is valid, or an error message + /// if the fee transfer exceeds the coinbase amount. + /// + /// # Special Cases + /// + /// If the fee transfer receiver is the same as the coinbase receiver, + /// the fee transfer is automatically removed since the producer would + /// receive the full amount anyway. + /// + /// # Examples + /// + /// ```ignore + /// use mina_tx_type::{Coinbase, CoinbaseFeeTransfer, Amount, Fee, CompressedPubKey}; + /// + /// let producer = CompressedPubKey::from_address("B62...").unwrap(); + /// let amount = Amount::from_u64(720_000_000_000); // 720 MINA + /// + /// // Create a coinbase with no fee transfer + /// let cb = Coinbase::create(amount, producer.clone(), None).unwrap(); + /// + /// // Create a coinbase with a fee transfer to a SNARK worker + /// let worker = CompressedPubKey::from_address("B62...").unwrap(); + /// let fee_transfer = CoinbaseFeeTransfer::create(worker, Fee::from_u64(10_000_000)); + /// let cb = Coinbase::create(amount, producer, Some(fee_transfer)).unwrap(); + /// ``` + pub fn create( + amount: Amount, + receiver: CompressedPubKey, + fee_transfer: Option, + ) -> Result { + let mut this = Self { + receiver: receiver.clone(), + amount, + fee_transfer, + }; + + if this.is_valid() { + // If the fee transfer receiver is the same as the coinbase receiver, + // remove the fee transfer since the producer gets everything anyway + let adjusted_fee_transfer = this.fee_transfer.as_ref().and_then(|ft| { + if receiver != ft.receiver_pk { + Some(ft.clone()) + } else { + None + } + }); + this.fee_transfer = adjusted_fee_transfer; + Ok(this) + } else { + Err("Coinbase.create: invalid coinbase".to_string()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper to create a test public key + fn test_pubkey() -> CompressedPubKey { + CompressedPubKey::from_address("B62qiy32p8kAKnny8ZFwoMhYpBppM1DWVCqAPBYNcXnsAHhnfAAuXgg") + .unwrap() + } + + fn test_pubkey_2() -> CompressedPubKey { + CompressedPubKey::from_address("B62qnzbXmRNo9q32n4SNu2mpB8e7FYYLH8NmaX6oFCBYjjQ8SbD7uzV") + .unwrap() + } + + #[test] + fn test_coinbase_fee_transfer_create() { + let receiver = test_pubkey(); + let fee = Fee::from_u64(10_000_000); + let transfer = CoinbaseFeeTransfer::create(receiver.clone(), fee); + + assert_eq!(transfer.receiver_pk, receiver); + assert_eq!(transfer.fee, fee); + } + + #[test] + fn test_coinbase_create_no_fee_transfer() { + let producer = test_pubkey(); + let amount = Amount::from_u64(720_000_000_000); + + let cb = Coinbase::create(amount, producer.clone(), None).unwrap(); + + assert_eq!(cb.receiver, producer); + assert_eq!(cb.amount, amount); + assert!(cb.fee_transfer.is_none()); + } + + #[test] + fn test_coinbase_create_with_fee_transfer() { + let producer = test_pubkey(); + let worker = test_pubkey_2(); + let amount = Amount::from_u64(720_000_000_000); + let fee = Fee::from_u64(10_000_000); + + let fee_transfer = CoinbaseFeeTransfer::create(worker.clone(), fee); + let cb = Coinbase::create(amount, producer.clone(), Some(fee_transfer)).unwrap(); + + assert_eq!(cb.receiver, producer); + assert_eq!(cb.amount, amount); + assert!(cb.fee_transfer.is_some()); + let ft = cb.fee_transfer.unwrap(); + assert_eq!(ft.receiver_pk, worker); + assert_eq!(ft.fee, fee); + } + + #[test] + fn test_coinbase_removes_self_fee_transfer() { + let producer = test_pubkey(); + let amount = Amount::from_u64(720_000_000_000); + let fee = Fee::from_u64(10_000_000); + + // Fee transfer to self should be removed + let fee_transfer = CoinbaseFeeTransfer::create(producer.clone(), fee); + let cb = Coinbase::create(amount, producer.clone(), Some(fee_transfer)).unwrap(); + + assert!(cb.fee_transfer.is_none()); + } + + #[test] + fn test_coinbase_invalid_fee_exceeds_amount() { + let producer = test_pubkey(); + let worker = test_pubkey_2(); + let amount = Amount::from_u64(100); + let fee = Fee::from_u64(200); // Fee exceeds amount + + let fee_transfer = CoinbaseFeeTransfer::create(worker, fee); + let result = Coinbase::create(amount, producer, Some(fee_transfer)); + + assert!(result.is_err()); + } +} diff --git a/mina-tx-type/src/conv.rs b/mina-tx-type/src/conv.rs new file mode 100644 index 0000000000..f535d725b9 --- /dev/null +++ b/mina-tx-type/src/conv.rs @@ -0,0 +1,161 @@ +//! Conversions between currency types and p2p message types. +//! +//! This module provides `From` implementations for converting between the +//! currency types in this crate and the wire format types from `mina-p2p-messages`. + +use mina_p2p_messages::v2::{ + CurrencyAmountStableV1, CurrencyFeeStableV1, SgnStableV1, SignedAmount, + UnsignedExtendedUInt64Int64ForVersionTagsStableV1, +}; + +use crate::currency::{Amount, Fee, Sgn, Signed}; + +// Amount conversions + +impl From for Amount { + fn from(value: CurrencyAmountStableV1) -> Self { + Self(value.as_u64()) + } +} + +impl From for CurrencyAmountStableV1 { + fn from(value: Amount) -> Self { + Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( + value.as_u64().into(), + )) + } +} + +impl From<&Amount> for CurrencyAmountStableV1 { + fn from(value: &Amount) -> Self { + CurrencyAmountStableV1(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( + value.as_u64().into(), + )) + } +} + +impl From<&Amount> for CurrencyFeeStableV1 { + fn from(value: &Amount) -> Self { + CurrencyFeeStableV1(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( + value.as_u64().into(), + )) + } +} + +// Fee conversions + +impl From<&CurrencyFeeStableV1> for Fee { + fn from(value: &CurrencyFeeStableV1) -> Self { + Self(value.as_u64()) + } +} + +impl From<&CurrencyAmountStableV1> for Fee { + fn from(value: &CurrencyAmountStableV1) -> Self { + Self(value.as_u64()) + } +} + +impl From<&Fee> for CurrencyFeeStableV1 { + fn from(value: &Fee) -> Self { + Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( + value.as_u64().into(), + )) + } +} + +impl From<&Fee> for CurrencyAmountStableV1 { + fn from(value: &Fee) -> Self { + Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( + value.as_u64().into(), + )) + } +} + +// Sgn conversions + +impl From for Sgn { + fn from(value: SgnStableV1) -> Self { + match value { + SgnStableV1::Pos => Self::Pos, + SgnStableV1::Neg => Self::Neg, + } + } +} + +impl From<&Sgn> for SgnStableV1 { + fn from(value: &Sgn) -> Self { + match value { + Sgn::Pos => Self::Pos, + Sgn::Neg => Self::Neg, + } + } +} + +// Signed conversions + +impl From<&SignedAmount> for Signed { + fn from(value: &SignedAmount) -> Self { + Self { + magnitude: Amount(value.magnitude.as_u64()), + sgn: value.sgn.clone().into(), + } + } +} + +impl From<&Signed> for SignedAmount { + fn from(value: &Signed) -> Self { + Self { + magnitude: (&value.magnitude).into(), + sgn: (&value.sgn).into(), + } + } +} + +// Signed conversions + +impl From<&SignedAmount> for Signed { + fn from(value: &SignedAmount) -> Self { + Self { + magnitude: (&value.magnitude).into(), + sgn: value.sgn.clone().into(), + } + } +} + +impl From<&Signed> for SignedAmount { + fn from(value: &Signed) -> Self { + Self { + magnitude: (&value.magnitude).into(), + sgn: (&value.sgn).into(), + } + } +} + +// MinaStateBlockchainStateValueStableV2SignedAmount conversions +// (another signed amount type used in blockchain state) + +type BlockchainStateSignedAmount = + mina_p2p_messages::v2::MinaStateBlockchainStateValueStableV2SignedAmount; + +impl From<&BlockchainStateSignedAmount> for Signed { + fn from(value: &BlockchainStateSignedAmount) -> Self { + let BlockchainStateSignedAmount { magnitude, sgn } = value; + + Self { + magnitude: (magnitude.clone()).into(), + sgn: (sgn.clone()).into(), + } + } +} + +impl From<&Signed> for BlockchainStateSignedAmount { + fn from(value: &Signed) -> Self { + let Signed:: { magnitude, sgn } = value; + + Self { + magnitude: (*magnitude).into(), + sgn: sgn.into(), + } + } +} diff --git a/mina-tx-type/src/currency.rs b/mina-tx-type/src/currency.rs new file mode 100644 index 0000000000..bb366830d2 --- /dev/null +++ b/mina-tx-type/src/currency.rs @@ -0,0 +1,703 @@ +//! Currency types for the Mina Protocol +//! +//! This module provides the fundamental currency types used throughout the +//! Mina Protocol for representing monetary values. +//! +//! # Unit System +//! +//! Mina uses a fixed-point representation where values are stored as integers +//! in the smallest unit (nanomina). One MINA equals 1,000,000,000 nanomina. +//! +//! # Types +//! +//! - [`Amount`]: Represents positive currency amounts (e.g., transaction +//! amounts, coinbase rewards) +//! - [`Fee`]: Represents transaction fees paid to block producers +//! - [`Signed`]: A signed wrapper for magnitude types +//! +//! # Traits +//! +//! - [`Magnitude`]: Core trait for numeric operations on currency types +//! - [`MinMax`]: Trait for types with minimum and maximum values + +use std::cmp::Ordering::{Equal, Greater, Less}; + +use ark_ff::BigInteger256; +use serde::{Deserialize, Serialize}; + +use crate::proofs::{ + field::FieldWitness, + to_field_elements::ToFieldElements, + witness::{Check, Witness}, +}; + +/// Sign of a signed value. +/// +/// Used with [`Signed`] to represent positive or negative values. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Sgn { + /// Positive sign + Pos, + /// Negative sign + Neg, +} + +impl Sgn { + /// Returns `true` if the sign is positive. + pub fn is_pos(&self) -> bool { + matches!(self, Sgn::Pos) + } + + /// Returns the negated sign. + pub fn negate(&self) -> Self { + match self { + Sgn::Pos => Sgn::Neg, + Sgn::Neg => Sgn::Pos, + } + } +} + +/// Core trait for numeric magnitude operations. +/// +/// This trait defines the fundamental operations needed for currency types +/// like [`Amount`] and [`Fee`]. It provides checked arithmetic operations +/// that return `None` on overflow/underflow, as well as wrapping operations. +/// +/// # Required Methods +/// +/// Implementors must provide: +/// - Arithmetic operations (add, sub, mul, div, rem) in both checked and +/// wrapping variants +/// - Zero value and zero check +/// - Absolute difference +/// - Bit width constant +pub trait Magnitude +where + Self: Sized + PartialOrd + Copy, +{ + /// The number of bits in this type. + const NBITS: usize; + + /// Returns the absolute difference between `self` and `rhs`. + fn abs_diff(&self, rhs: &Self) -> Self; + + /// Wrapping addition. + fn wrapping_add(&self, rhs: &Self) -> Self; + + /// Wrapping multiplication. + fn wrapping_mul(&self, rhs: &Self) -> Self; + + /// Wrapping subtraction. + fn wrapping_sub(&self, rhs: &Self) -> Self; + + /// Checked addition. Returns `None` if overflow occurred. + fn checked_add(&self, rhs: &Self) -> Option; + + /// Checked multiplication. Returns `None` if overflow occurred. + fn checked_mul(&self, rhs: &Self) -> Option; + + /// Checked subtraction. Returns `None` if underflow occurred. + fn checked_sub(&self, rhs: &Self) -> Option; + + /// Checked division. Returns `None` if `rhs` is zero. + fn checked_div(&self, rhs: &Self) -> Option; + + /// Checked remainder. Returns `None` if `rhs` is zero. + fn checked_rem(&self, rhs: &Self) -> Option; + + /// Returns `true` if the value is zero. + fn is_zero(&self) -> bool; + + /// Returns the zero value. + fn zero() -> Self; + + /// Addition with overflow flag. + /// + /// Returns a tuple of the result and a boolean indicating whether + /// overflow occurred. + fn add_flagged(&self, rhs: &Self) -> (Self, bool) { + let z = self.wrapping_add(rhs); + (z, z < *self) + } + + /// Subtraction with underflow flag. + /// + /// Returns a tuple of the result and a boolean indicating whether + /// underflow occurred. + fn sub_flagged(&self, rhs: &Self) -> (Self, bool) { + (self.wrapping_sub(rhs), self < rhs) + } +} + +/// Extension trait for converting magnitude types to/from field elements. +/// +/// This trait is separate from [`Magnitude`] because the field witness +/// type varies between crates. The ledger crate implements this trait +/// for its own types using its local `FieldWitness` type. +pub trait MagnitudeFieldExt { + /// Convert to a field element. + fn to_field(&self) -> F; + + /// Create from a field element. + fn of_field(field: F) -> Self; +} + +/// Trait for types with minimum and maximum values. +/// +/// Used for defining bounds on numeric types. +pub trait MinMax { + /// Returns the minimum value. + fn min() -> Self; + /// Returns the maximum value. + fn max() -> Self; +} + +/// A signed magnitude value. +/// +/// Combines a magnitude (absolute value) with a sign to represent +/// positive or negative values. Used for balance changes and fee excesses. +/// +/// # Type Parameters +/// +/// * `T` - The magnitude type, must implement [`Magnitude`] +/// +/// # Examples +/// +/// ``` +/// use mina_tx_type::{Signed, Sgn, Fee}; +/// +/// // Create a positive fee +/// let positive = Signed::of_unsigned(Fee::from_u64(100)); +/// assert!(positive.is_pos()); +/// +/// // Create a negative fee +/// let negative = positive.negate(); +/// assert!(negative.is_neg()); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Signed { + /// The absolute value. + pub magnitude: T, + /// The sign (positive or negative). + pub sgn: Sgn, +} + +impl Signed +where + T: Magnitude + PartialOrd + Ord + Clone, +{ + /// The number of bits in the magnitude type. + #[allow(dead_code)] + const NBITS: usize = T::NBITS; + + /// Creates a new signed value with the given magnitude and sign. + /// + /// If the magnitude is zero, the sign is always set to positive. + pub fn create(magnitude: T, sgn: Sgn) -> Self { + Self { + magnitude, + sgn: if magnitude.is_zero() { Sgn::Pos } else { sgn }, + } + } + + /// Creates a positive signed value from an unsigned magnitude. + pub fn of_unsigned(magnitude: T) -> Self { + Self::create(magnitude, Sgn::Pos) + } + + /// Returns the negated value. + pub fn negate(&self) -> Self { + if self.magnitude.is_zero() { + Self::zero() + } else { + Self { + magnitude: self.magnitude, + sgn: self.sgn.negate(), + } + } + } + + /// Returns `true` if the value is positive. + pub fn is_pos(&self) -> bool { + matches!(self.sgn, Sgn::Pos) + } + + /// Returns `true` if the value is non-negative (positive or zero). + pub fn is_non_neg(&self) -> bool { + matches!(self.sgn, Sgn::Pos) + } + + /// Returns `true` if the value is negative. + pub fn is_neg(&self) -> bool { + matches!(self.sgn, Sgn::Neg) + } + + /// Returns the zero value (positive zero). + pub fn zero() -> Self { + Self { + magnitude: T::zero(), + sgn: Sgn::Pos, + } + } + + /// Returns `true` if the value is zero. + pub fn is_zero(&self) -> bool { + self.magnitude.is_zero() + } + + /// Checked addition of two signed values. + /// + /// Returns `None` if overflow would occur. + pub fn add(&self, rhs: &Self) -> Option { + let (magnitude, sgn) = if self.sgn == rhs.sgn { + let magnitude = self.magnitude.checked_add(&rhs.magnitude)?; + let sgn = self.sgn; + (magnitude, sgn) + } else { + let sgn = match self.magnitude.cmp(&rhs.magnitude) { + Less => rhs.sgn, + Greater => self.sgn, + Equal => return Some(Self::zero()), + }; + let magnitude = self.magnitude.abs_diff(&rhs.magnitude); + (magnitude, sgn) + }; + + Some(Self::create(magnitude, sgn)) + } + + /// Addition with overflow flag. + /// + /// Returns a tuple of the result and a boolean indicating whether + /// overflow occurred. + pub fn add_flagged(&self, rhs: Self) -> (Self, bool) { + match (self.sgn, rhs.sgn) { + (Sgn::Neg, sgn @ Sgn::Neg) | (Sgn::Pos, sgn @ Sgn::Pos) => { + let (magnitude, overflow) = self.magnitude.add_flagged(&rhs.magnitude); + (Self { magnitude, sgn }, overflow) + } + (Sgn::Pos, Sgn::Neg) | (Sgn::Neg, Sgn::Pos) => { + let sgn = match self.magnitude.cmp(&rhs.magnitude) { + Less => rhs.sgn, + Greater => self.sgn, + Equal => Sgn::Pos, + }; + let magnitude = self.magnitude.abs_diff(&rhs.magnitude); + (Self { magnitude, sgn }, false) + } + } + } +} + +impl Signed { + /// Converts a signed amount to a signed fee. + pub fn to_fee(self) -> Signed { + let Self { magnitude, sgn } = self; + Signed { + magnitude: Fee(magnitude.0), + sgn, + } + } +} + +/// Macro to implement numeric currency types. +/// +/// This macro generates a newtype wrapper around a primitive integer type +/// with implementations of [`Magnitude`], [`MinMax`], and various utility +/// methods. +/// +/// # Generated Items +/// +/// For each type, the macro generates: +/// - A newtype struct wrapping the inner type +/// - `Debug`, `Serialize`, `Deserialize` implementations +/// - [`Magnitude`] trait implementation +/// - [`MinMax`] trait implementation +/// - Accessor methods (`as_u64`, `from_u64`, etc.) +/// - Utility methods (`scale`, `of_mina_string_exn`) +macro_rules! impl_number { + (32: { $($name32:ident,)* }, 64: { $($name64:ident,)* },) => { + $(impl_number!({$name32, u32, as_u32, from_u32, next_u32},);)* + $(impl_number!({$name64, u64, as_u64, from_u64, next_u64},);)* + }; + ($({ $name:ident, $inner:ty, $as_name:ident, $from_name:ident, $next_name:ident },)*) => ($( + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, + Deserialize, Serialize)] + pub struct $name(pub $inner); + + impl std::fmt::Debug for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}({:?})", stringify!($name), self.0)) + } + } + + impl Magnitude for $name { + const NBITS: usize = Self::NBITS; + + fn zero() -> Self { + Self(0) + } + + fn is_zero(&self) -> bool { + self.0 == 0 + } + + fn wrapping_add(&self, rhs: &Self) -> Self { + Self(self.0.wrapping_add(rhs.0)) + } + + fn wrapping_mul(&self, rhs: &Self) -> Self { + Self(self.0.wrapping_mul(rhs.0)) + } + + fn wrapping_sub(&self, rhs: &Self) -> Self { + Self(self.0.wrapping_sub(rhs.0)) + } + + fn checked_add(&self, rhs: &Self) -> Option { + self.0.checked_add(rhs.0).map(Self) + } + + fn checked_mul(&self, rhs: &Self) -> Option { + self.0.checked_mul(rhs.0).map(Self) + } + + fn checked_sub(&self, rhs: &Self) -> Option { + self.0.checked_sub(rhs.0).map(Self) + } + + fn checked_div(&self, rhs: &Self) -> Option { + self.0.checked_div(rhs.0).map(Self) + } + + fn checked_rem(&self, rhs: &Self) -> Option { + self.0.checked_rem(rhs.0).map(Self) + } + + fn abs_diff(&self, rhs: &Self) -> Self { + Self(self.0.abs_diff(rhs.0)) + } + } + + impl MagnitudeFieldExt for $name { + fn to_field(&self) -> F { + let int = self.0 as u64; + F::from(int) + } + + fn of_field(field: F) -> Self { + let amount: BigInteger256 = field.into(); + let amount: $inner = amount.0[0].try_into().unwrap(); + Self::$from_name(amount) + } + } + + impl MinMax for $name { + fn min() -> Self { Self(0) } + fn max() -> Self { Self(<$inner>::MAX) } + } + + impl ToFieldElements for $name { + fn to_field_elements(&self, fields: &mut Vec) { + fields.push(>::to_field(self)); + } + } + + impl Check for $name { + fn check(&self, w: &mut Witness) { + // Note: Full implementation requires to_field_checked_prime from + // the transaction module. For now, we just add the field element + // to the witness without range checking. + // TODO: Implement proper range checking when snarky-rs is available. + let _ = w.exists_no_check(>::to_field(self)); + } + } + + impl rand::distributions::Distribution<$name> for rand::distributions::Standard { + fn sample(&self, rng: &mut R) -> $name { + $name(rng.$next_name()) + } + } + + impl $name { + /// The number of bits in this type. + pub const NBITS: usize = <$inner>::BITS as usize; + + /// Returns the inner value. + pub fn $as_name(&self) -> $inner { + self.0 + } + + /// Creates a new value from the inner type. + pub const fn $from_name(value: $inner) -> Self { + Self(value) + } + + /// Multiplies by a scalar, returning `None` on overflow. + pub const fn scale(&self, n: $inner) -> Option { + match self.0.checked_mul(n) { + Some(n) => Some(Self(n)), + None => None, + } + } + + /// Returns the minimum value. + pub fn min() -> Self { + ::min() + } + + /// Returns the maximum value. + pub fn max() -> Self { + ::max() + } + + /// Converts the value to a fixed-size bit array (little-endian). + /// + /// Returns an array of booleans representing each bit, with the + /// least significant bit first. + pub fn to_bits(&self) -> [bool; <$inner>::BITS as usize] { + let mut bits = [false; <$inner>::BITS as usize]; + let mut value = self.0; + for bit in bits.iter_mut() { + *bit = (value & 1) != 0; + value >>= 1; + } + bits + } + + /// Parses a MINA amount from a string. + /// + /// The string can be in the format "123" or "123.456789". + /// Values are converted to nanomina (9 decimal places). + /// + /// # Panics + /// + /// Panics if the string is not a valid currency format. + pub fn of_mina_string_exn(input: &str) -> Self { + const PRECISION: usize = 9; + + let mut s = String::with_capacity(input.len() + 9); + + if !input.contains('.') { + let append = "000000000"; + assert_eq!(append.len(), PRECISION); + + s.push_str(input); + s.push_str(append); + } else { + let (whole, decimal) = { + let mut splitted = input.split('.'); + let whole = splitted.next().unwrap(); + let decimal = splitted.next().unwrap(); + assert!( + splitted.next().is_none(), + "Currency.of_mina_string_exn: Invalid currency input" + ); + (whole, decimal) + }; + + let decimal_length = decimal.len(); + + if decimal_length > PRECISION { + s.push_str(whole); + s.push_str(&decimal[0..PRECISION]); + } else { + s.push_str(whole); + s.push_str(decimal); + for _ in 0..PRECISION - decimal_length { + s.push('0'); + } + } + } + + let n = s.parse::<$inner>().unwrap(); + Self(n) + } + } + )*) +} + +// Generate the currency types using the macro +impl_number!( + 32: { }, + 64: { Amount, Fee, }, +); + +// Additional Amount-specific implementations +impl Amount { + /// The number of nanomina in one MINA. + pub const NANOMINA_PER_MINA: u64 = 1_000_000_000; + + /// Converts a [`Fee`] to an [`Amount`]. + /// + /// Since fees and amounts use the same underlying unit (nanomina), + /// this is a direct conversion. + pub const fn of_fee(fee: &Fee) -> Self { + Self(fee.0) + } + + /// Adds a signed amount, returning the result and overflow flag. + pub fn add_signed_flagged(&self, rhs: Signed) -> (Self, bool) { + if let Sgn::Pos = rhs.sgn { + self.add_flagged(&rhs.magnitude) + } else { + self.sub_flagged(&rhs.magnitude) + } + } + + /// Returns the amount in nanomina (identity function). + pub fn to_nanomina_int(&self) -> Self { + *self + } + + /// Converts nanomina to whole MINA (truncates). + pub fn to_mina_int(&self) -> Self { + Self(self.0.checked_div(Self::NANOMINA_PER_MINA).unwrap()) + } + + /// Creates an amount from whole MINA. + /// + /// # Panics + /// + /// Panics if the result would overflow. + pub fn of_mina_int_exn(int: u64) -> Self { + Self::from_u64(int).scale(Self::NANOMINA_PER_MINA).unwrap() + } + + /// Creates an amount from nanomina. + pub fn of_nanomina_int_exn(int: u64) -> Self { + Self::from_u64(int) + } +} + +// Additional Fee-specific implementations +impl Fee { + /// Creates a fee from nanomina. + pub const fn of_nanomina_int_exn(int: u64) -> Self { + Self::from_u64(int) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_amount_from_u64() { + let amount = Amount::from_u64(1_000_000_000); + assert_eq!(amount.as_u64(), 1_000_000_000); + } + + #[test] + fn test_amount_checked_add() { + let a = Amount::from_u64(100); + let b = Amount::from_u64(200); + assert_eq!(a.checked_add(&b), Some(Amount::from_u64(300))); + } + + #[test] + fn test_amount_checked_sub() { + let a = Amount::from_u64(300); + let b = Amount::from_u64(100); + assert_eq!(a.checked_sub(&b), Some(Amount::from_u64(200))); + + let c = Amount::from_u64(50); + assert_eq!(c.checked_sub(&a), None); + } + + #[test] + fn test_fee_operations() { + let fee = Fee::from_u64(10_000_000); + assert!(!fee.is_zero()); + assert_eq!(fee.as_u64(), 10_000_000); + + let zero_fee = Fee::zero(); + assert!(zero_fee.is_zero()); + } + + #[test] + fn test_amount_of_fee() { + let fee = Fee::from_u64(5_000_000); + let amount = Amount::of_fee(&fee); + assert_eq!(amount.as_u64(), 5_000_000); + } + + #[test] + fn test_magnitude_trait() { + let a = Amount::from_u64(100); + let b = Amount::from_u64(50); + + assert_eq!(a.abs_diff(&b), Amount::from_u64(50)); + assert_eq!(b.abs_diff(&a), Amount::from_u64(50)); + + assert_eq!(a.wrapping_add(&b), Amount::from_u64(150)); + assert_eq!(a.wrapping_sub(&b), Amount::from_u64(50)); + } + + #[test] + fn test_signed_operations() { + let pos = Signed::of_unsigned(Amount::from_u64(100)); + assert!(pos.is_pos()); + assert!(!pos.is_neg()); + + let neg = pos.negate(); + assert!(neg.is_neg()); + assert!(!neg.is_pos()); + + // Adding positive and negative of same magnitude gives zero + let sum = pos.add(&neg).unwrap(); + assert!(sum.is_zero()); + } + + #[test] + fn test_signed_add() { + let a = Signed::of_unsigned(Amount::from_u64(100)); + let b = Signed::of_unsigned(Amount::from_u64(50)); + + // Positive + Positive + let sum = a.add(&b).unwrap(); + assert_eq!(sum.magnitude.as_u64(), 150); + assert!(sum.is_pos()); + + // Positive + Negative (result positive) + let neg_b = b.negate(); + let diff = a.add(&neg_b).unwrap(); + assert_eq!(diff.magnitude.as_u64(), 50); + assert!(diff.is_pos()); + + // Positive + Negative (result negative) + let neg_a = a.negate(); + let diff2 = b.add(&neg_a).unwrap(); + assert_eq!(diff2.magnitude.as_u64(), 50); + assert!(diff2.is_neg()); + } + + #[test] + fn test_of_mina_string_exn() { + assert_eq!(Amount::of_mina_string_exn("1").as_u64(), 1_000_000_000); + assert_eq!(Amount::of_mina_string_exn("1.5").as_u64(), 1_500_000_000); + assert_eq!(Amount::of_mina_string_exn("0.000000001").as_u64(), 1); + assert_eq!( + Amount::of_mina_string_exn("123.456789012").as_u64(), + 123_456_789_012 + ); + } + + #[test] + fn test_signed_to_fee() { + let signed_amount = Signed::create(Amount::from_u64(100), Sgn::Neg); + let signed_fee = signed_amount.to_fee(); + + assert_eq!(signed_fee.magnitude.as_u64(), 100); + assert!(signed_fee.is_neg()); + } + + #[test] + fn test_minmax() { + assert_eq!(Amount::min().as_u64(), 0); + assert_eq!(Amount::max().as_u64(), u64::MAX); + + assert_eq!(Fee::min().as_u64(), 0); + assert_eq!(Fee::max().as_u64(), u64::MAX); + } +} diff --git a/mina-tx-type/src/lib.rs b/mina-tx-type/src/lib.rs new file mode 100644 index 0000000000..2ca15aec98 --- /dev/null +++ b/mina-tx-type/src/lib.rs @@ -0,0 +1,37 @@ +//! Transaction types for the Mina Protocol +//! +//! This crate provides standalone data structures representing Mina Protocol +//! transaction types. It can be used by external projects to work with Mina +//! transactions without depending on the full ledger crate. +//! +//! # Overview +//! +//! The Mina Protocol supports several transaction types: +//! +//! - **Coinbase**: Block rewards paid to the block producer +//! - **Fee Transfers**: Distribution of transaction fees to block producers +//! - **Signed Commands**: Payments and stake delegations (TODO) +//! - **zkApp Commands**: Complex multi-account operations using zero-knowledge +//! proofs (TODO) +//! +//! # Documentation References +//! +//! For detailed information about zkApp transaction signing, see: +//! - TODO: Issue #1748 - zkApp transaction signing documentation +//! - + +pub mod coinbase; +mod conv; +pub mod currency; +pub mod proofs; + +pub use coinbase::{Coinbase, CoinbaseFeeTransfer}; +pub use currency::{Amount, Fee, Magnitude, MagnitudeFieldExt, MinMax, Sgn, Signed}; +pub use proofs::{ + field_of_bits, field_to_bits, Boolean, Check, CircuitVar, FieldWitness, FromFpFq, GroupAffine, + IntoGeneric, Params, Shift, ShiftedValue, ShiftingValue, ToBoolean, ToFieldElements, Witness, + BACKEND_TICK_ROUNDS_N, BACKEND_TOCK_ROUNDS_N, +}; + +// Re-export mina-signer types for convenience +pub use mina_signer::CompressedPubKey; diff --git a/mina-tx-type/src/proofs/field.rs b/mina-tx-type/src/proofs/field.rs new file mode 100644 index 0000000000..6ce6a777e2 --- /dev/null +++ b/mina-tx-type/src/proofs/field.rs @@ -0,0 +1,800 @@ +use ark_ec::{short_weierstrass::Projective, AffineRepr, CurveGroup}; +use ark_ff::{BigInteger256, FftField, Field, One, PrimeField}; +use kimchi::curve::KimchiCurve; +use mina_curves::pasta::{ + fields::fft::FpParameters as _, Fp, Fq, PallasParameters, ProjectivePallas, ProjectiveVesta, + VestaParameters, +}; +use mina_poseidon::{constants::PlonkSpongeConstantsKimchi, sponge::DefaultFqSponge}; + +use poseidon::SpongeParamsForField; + +use super::{ + to_field_elements::ToFieldElements, + witness::{Check, Witness}, +}; + +pub const BACKEND_TICK_ROUNDS_N: usize = 16; +pub const BACKEND_TOCK_ROUNDS_N: usize = 17; + +pub type GroupAffine = ark_ec::short_weierstrass::Affine<::Parameters>; + +/// All the generics we need during witness generation +pub trait FieldWitness +where + Self: Field + + Send + + Sync + + Into + + From + + Into + + From + + From + + ToFieldElements + + Check + + FromFpFq + + PrimeField + + FftField + + SpongeParamsForField + + std::fmt::Debug + + 'static, +{ + type Scalar: FieldWitness; + type Affine: AffineRepr< + Group = Self::Projective, + BaseField = Self, + ScalarField = ::Scalar, + > + Into> + + KimchiCurve + + std::fmt::Debug; + type Projective: CurveGroup< + Affine = Self::Affine, + BaseField = Self, + ScalarField = ::Scalar, + > + From> + + std::fmt::Debug; + type Parameters: ark_ec::short_weierstrass::SWCurveConfig< + BaseField = Self, + ScalarField = ::Scalar, + > + Clone + + std::fmt::Debug; + type Shifting: ShiftingValue + Clone + std::fmt::Debug; + type OtherCurve: KimchiCurve::Scalar>; + type FqSponge: Clone + + mina_poseidon::FqSponge<::Scalar, Self::OtherCurve, Self>; + + const PARAMS: Params; + const SIZE: BigInteger256; + const NROUNDS: usize; + const SRS_DEPTH: usize; +} + +pub struct Params { + pub a: F, + pub b: F, +} + +// Implement Check for Fp and Fq +impl Check for Fp { + fn check(&self, _w: &mut Witness) { + // Does not modify the witness + } +} + +impl Check for Fq { + fn check(&self, _w: &mut Witness) { + // Does not modify the witness + } +} + +// Implement Check for arrays +impl Check for [F; N] { + fn check(&self, _w: &mut Witness) { + // Does not modify the witness + } +} + +impl FieldWitness for Fp { + type Scalar = Fq; + type Parameters = PallasParameters; + type Affine = GroupAffine; + type Projective = ProjectivePallas; + type Shifting = ShiftedValue; + type OtherCurve = GroupAffine; + type FqSponge = DefaultFqSponge; + + /// + const PARAMS: Params = Params:: { + a: ark_ff::MontFp!("0"), + b: ark_ff::MontFp!("5"), + }; + const SIZE: BigInteger256 = mina_curves::pasta::fields::FpParameters::MODULUS; + const NROUNDS: usize = BACKEND_TICK_ROUNDS_N; + const SRS_DEPTH: usize = 32768; +} + +impl FieldWitness for Fq { + type Scalar = Fp; + type Parameters = VestaParameters; + type Affine = GroupAffine; + type Projective = ProjectiveVesta; + type Shifting = ShiftedValue; + type OtherCurve = GroupAffine; + type FqSponge = DefaultFqSponge; + + /// + const PARAMS: Params = Params:: { + a: ark_ff::MontFp!("0"), + b: ark_ff::MontFp!("5"), + }; + const SIZE: BigInteger256 = mina_curves::pasta::fields::FqParameters::MODULUS; + const NROUNDS: usize = BACKEND_TOCK_ROUNDS_N; + const SRS_DEPTH: usize = 65536; +} + +/// Trait helping converting generics into concrete types +pub trait FromFpFq { + fn from_fp(fp: Fp) -> Self; + fn from_fq(fp: Fq) -> Self; +} + +impl FromFpFq for Fp { + fn from_fp(fp: Fp) -> Self { + fp + } + fn from_fq(_fq: Fq) -> Self { + // `Fq` cannot be converted into `Fp`. + // Caller must first split `Fq` into 2 parts (high & low bits) + // See `impl ToFieldElements for Fq` + panic!("Attempt to convert a `Fq` into `Fp`") + } +} + +impl FromFpFq for Fq { + fn from_fp(fp: Fp) -> Self { + // `Fp` is smaller than `Fq`, so the conversion is fine + let bigint: BigInteger256 = fp.into(); + Self::from(bigint) + } + fn from_fq(fq: Fq) -> Self { + fq + } +} + +/// Trait helping converting concrete types into generics +pub trait IntoGeneric { + fn into_gen(self) -> F; +} + +impl IntoGeneric for Fp { + fn into_gen(self) -> F { + F::from_fp(self) + } +} + +impl IntoGeneric for Fq { + fn into_gen(self) -> F { + F::from_fq(self) + } +} + +#[allow(clippy::module_inception)] +pub mod field { + use super::*; + + // + pub fn square(field: F, w: &mut Witness) -> F + where + F: FieldWitness, + { + w.exists(field.square()) + // TODO: Rest of the function doesn't modify witness + } + + pub fn mul(x: F, y: F, w: &mut Witness) -> F + where + F: FieldWitness, + { + w.exists(x * y) + } + + pub fn const_mul(x: F, y: F) -> F + where + F: FieldWitness, + { + x * y + } + + pub fn muls(xs: &[F], w: &mut Witness) -> F + where + F: FieldWitness, + { + xs.iter() + .copied() + .reduce(|acc, v| w.exists(acc * v)) + .expect("invalid param") + } + + pub fn div(x: F, y: F, w: &mut Witness) -> F + where + F: FieldWitness, + { + w.exists(x / y) + } + + // TODO: Do we need `div` above ? + pub fn div_by_inv(x: F, y: F, w: &mut Witness) -> F + where + F: FieldWitness, + { + let y_inv = w.exists(y.inverse().unwrap_or_else(F::zero)); + mul(x, y_inv, w) + } + + pub fn sub(x: F, y: F, w: &mut Witness) -> F + where + F: FieldWitness, + { + w.exists(x - y) + } + + pub fn add(x: F, y: F, w: &mut Witness) -> F + where + F: FieldWitness, + { + w.exists(x + y) + } + + pub fn const_add(x: F, y: F) -> F + where + F: FieldWitness, + { + x + y + } + + pub fn equal(x: F, y: F, w: &mut Witness) -> Boolean { + let z = x - y; + + let (boolean, r, inv) = if x == y { + (Boolean::True, F::one(), F::zero()) + } else { + (Boolean::False, F::zero(), z.inverse().unwrap()) + }; + w.exists([r, inv]); + + boolean + } + + // Note: compare and assert_lt functions are not included here as they + // depend on field_to_bits2 from the transaction module. They remain + // in the ledger crate. +} + +pub trait ToBoolean { + fn to_boolean(&self) -> Boolean; +} + +impl ToBoolean for bool { + fn to_boolean(&self) -> Boolean { + if *self { + Boolean::True + } else { + Boolean::False + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Boolean { + True = 1, + False = 0, +} + +impl Boolean { + pub fn of_field(field: F) -> Self { + if field.is_zero() { + Self::False + } else { + Self::True + } + } + + pub fn as_bool(&self) -> bool { + match self { + Boolean::True => true, + Boolean::False => false, + } + } + + pub fn neg(&self) -> Self { + match self { + Boolean::False => Boolean::True, + Boolean::True => Boolean::False, + } + } + + pub fn to_field(self) -> F { + F::from(self.as_bool()) + } + + fn mul(&self, other: &Self, w: &mut Witness) -> Self { + let result: F = self.to_field::() * other.to_field::(); + w.exists(result); + if result.is_zero() { + Self::False + } else { + assert_eq!(result, F::one()); + Self::True + } + } + + pub fn and(&self, other: &Self, w: &mut Witness) -> Self { + self.mul(other, w) + } + + /// Same as `Self::and` but doesn't push values to the witness + pub fn const_and(&self, other: &Self) -> Self { + (self.as_bool() && other.as_bool()).to_boolean() + } + + pub fn or(&self, other: &Self, w: &mut Witness) -> Self { + let both_false = self.neg().and(&other.neg(), w); + both_false.neg() + } + + /// Same as `Self::or` but doesn't push values to the witness + pub fn const_or(&self, other: &Self) -> Self { + (self.as_bool() || other.as_bool()).to_boolean() + } + + pub fn all(x: &[Self], w: &mut Witness) -> Self { + match x { + [] => Self::True, + [b1] => *b1, + [b1, b2] => b1.and(b2, w), + bs => { + let len = F::from(bs.len() as u64); + let sum = bs.iter().fold(0u64, |acc, b| { + acc + match b { + Boolean::True => 1, + Boolean::False => 0, + } + }); + field::equal(len, F::from(sum), w) + } + } + } + + pub fn const_all(x: &[Self]) -> Self { + match x { + [] => Self::True, + [b1] => *b1, + [b1, b2] => b1.const_and(b2), + bs => { + let len = F::from(bs.len() as u64); + let sum = bs.iter().fold(0u64, |acc, b| { + acc + match b { + Boolean::True => 1, + Boolean::False => 0, + } + }); + (len == F::from(sum)).to_boolean() + } + } + } + + // For non-constant + pub fn lxor(&self, other: &Self, w: &mut Witness) -> Self { + let result = (self.as_bool() ^ other.as_bool()).to_boolean(); + w.exists(result.to_field::()); + result + } + + pub fn const_lxor(&self, other: &Self) -> Self { + (self.as_bool() ^ other.as_bool()).to_boolean() + } + + pub fn equal(&self, other: &Self, w: &mut Witness) -> Self { + self.lxor(other, w).neg() + } + + pub fn const_equal(&self, other: &Self) -> Self { + (self.as_bool() == other.as_bool()).to_boolean() + } + + pub fn const_any(x: &[Self]) -> Self { + match x { + [] => Self::False, + [b1] => *b1, + [b1, b2] => b1.const_or(b2), + bs => { + let sum = bs.iter().fold(0u64, |acc, b| { + acc + match b { + Boolean::True => 1, + Boolean::False => 0, + } + }); + (F::from(sum) == F::zero()).to_boolean().neg() + } + } + } + + pub fn any(x: &[Self], w: &mut Witness) -> Self { + match x { + [] => Self::False, + [b1] => *b1, + [b1, b2] => b1.or(b2, w), + bs => { + let sum = bs.iter().fold(0u64, |acc, b| { + acc + match b { + Boolean::True => 1, + Boolean::False => 0, + } + }); + field::equal(F::from(sum), F::zero(), w).neg() + } + } + } + + // Part of utils.inv + pub fn assert_non_zero(v: F, w: &mut Witness) { + if v.is_zero() { + w.exists(v); + } else { + w.exists(v.inverse().unwrap()); + } + } + + pub fn assert_any(bs: &[Self], w: &mut Witness) { + let num_true = bs.iter().fold(0u64, |acc, b| { + acc + match b { + Boolean::True => 1, + Boolean::False => 0, + } + }); + Self::assert_non_zero::(F::from(num_true), w) + } + + pub fn var(&self) -> CircuitVar { + CircuitVar::Var(*self) + } + + pub fn constant(&self) -> CircuitVar { + CircuitVar::Constant(*self) + } +} + +impl Check for Boolean { + fn check(&self, _w: &mut Witness) { + // Does not modify the witness + } +} + +impl Check for CircuitVar { + fn check(&self, _w: &mut Witness) { + // Does not modify the witness + } +} + +/// Our implementation of cvars (compare to OCaml) is incomplete, but sufficient +/// for now (for witness generation). +/// It's also sometimes not used correctly (`CircuitVar>` should be +/// `GroupAffine>` instead) +/// +/// Note that our implementation of `CircuitVar` is complete. +#[derive(Clone, Copy, Debug)] +pub enum CircuitVar { + Var(F), + Constant(F), +} + +impl CircuitVar { + pub fn as_field(&self) -> F { + match self { + CircuitVar::Var(f) => *f, + CircuitVar::Constant(f) => *f, + } + } + + fn scale(x: CircuitVar, s: F) -> CircuitVar { + use CircuitVar::*; + + if s.is_zero() { + Constant(F::zero()) + } else if s.is_one() { + x + } else { + match x { + Constant(x) => Constant(x * s), + Var(x) => Var(x * s), + } + } + } + + fn mul(&self, other: &Self, w: &mut Witness) -> CircuitVar { + use CircuitVar::*; + + let (x, y) = (*self, *other); + match (x, y) { + (Constant(x), Constant(y)) => Constant(x * y), + (Constant(x), _) => Self::scale(y, x), + (_, Constant(y)) => Self::scale(x, y), + (Var(x), Var(y)) => Var(w.exists(x * y)), + } + } + + fn equal(x: &Self, y: &Self, w: &mut Witness) -> CircuitVar { + match (x, y) { + (CircuitVar::Constant(x), CircuitVar::Constant(y)) => { + let eq = if x == y { + Boolean::True + } else { + Boolean::False + }; + CircuitVar::Constant(eq) + } + _ => { + let x = x.as_field(); + let y = y.as_field(); + CircuitVar::Var(field::equal(x, y, w)) + } + } + } +} + +impl CircuitVar { + pub fn is_const(&self) -> bool { + match self { + CircuitVar::Var(_) => false, + CircuitVar::Constant(_) => true, + } + } + + pub fn value(&self) -> &T { + match self { + CircuitVar::Var(v) => v, + CircuitVar::Constant(v) => v, + } + } + + pub fn map(&self, fun: Fun) -> CircuitVar + where + Fun: Fn(&T) -> V, + { + match self { + CircuitVar::Var(v) => CircuitVar::Var(fun(v)), + CircuitVar::Constant(v) => CircuitVar::Constant(fun(v)), + } + } +} + +impl CircuitVar { + pub fn as_boolean(&self) -> Boolean { + match self { + CircuitVar::Var(b) => *b, + CircuitVar::Constant(b) => *b, + } + } + + fn as_cvar(&self) -> CircuitVar { + self.map(|b| b.to_field::()) + } + + pub fn of_cvar(cvar: CircuitVar) -> Self { + cvar.map(|b| { + // TODO: Should we check for `is_one` or `is_zero` here ? To match OCaml behavior + if b.is_one() { + Boolean::True + } else { + Boolean::False + } + }) + } + + pub fn and(&self, other: &Self, w: &mut Witness) -> Self { + self.as_cvar().mul(&other.as_cvar(), w).map(|v| { + // TODO: Should we check for `is_one` or `is_zero` here ? To match OCaml behavior + if v.is_one() { + Boolean::True + } else { + Boolean::False + } + }) + } + + fn boolean_sum(x: &[Self]) -> CircuitVar { + let sum = x.iter().fold(0u64, |acc, b| { + acc + match b.as_boolean() { + Boolean::True => 1, + Boolean::False => 0, + } + }); + if x.iter().all(|x| matches!(x, CircuitVar::Constant(_))) { + CircuitVar::Constant(F::from(sum)) + } else { + CircuitVar::Var(F::from(sum)) + } + } + + pub fn any(x: &[Self], w: &mut Witness) -> CircuitVar { + match x { + [] => CircuitVar::Constant(Boolean::False), + [b1] => *b1, + [b1, b2] => b1.or(b2, w), + bs => { + let sum = Self::boolean_sum(bs); + CircuitVar::equal(&sum, &CircuitVar::Constant(F::zero()), w).map(Boolean::neg) + } + } + } + + pub fn all(x: &[Self], w: &mut Witness) -> CircuitVar { + match x { + [] => CircuitVar::Constant(Boolean::True), + [b1] => *b1, + [b1, b2] => b1.and(b2, w), + bs => { + let sum = Self::boolean_sum(bs); + let len = F::from(bs.len() as u64); + CircuitVar::equal(&CircuitVar::Constant(len), &sum, w) + } + } + } + + pub fn neg(&self) -> Self { + self.map(Boolean::neg) + } + + pub fn or(&self, other: &Self, w: &mut Witness) -> Self { + let both_false = self.neg().and(&other.neg(), w); + both_false.neg() + } + + pub fn lxor(&self, other: &Self, w: &mut Witness) -> Self { + match (self, other) { + (CircuitVar::Var(a), CircuitVar::Var(b)) => CircuitVar::Var(a.lxor(b, w)), + (CircuitVar::Constant(a), CircuitVar::Constant(b)) => { + CircuitVar::Constant(a.const_lxor(b)) + } + (a, b) => CircuitVar::Var(a.as_boolean().const_lxor(&b.as_boolean())), + } + } + + pub fn equal_bool(&self, other: &Self, w: &mut Witness) -> Self { + self.lxor(other, w).neg() + } + + pub fn assert_any(bs: &[Self], w: &mut Witness) { + let num_true = bs.iter().fold(0u64, |acc, b| { + acc + match b.as_boolean() { + Boolean::True => 1, + Boolean::False => 0, + } + }); + Boolean::assert_non_zero::(F::from(num_true), w) + } +} + +// ShiftingValue trait and related types + +pub trait ShiftingValue { + type MyShift; + fn shift() -> Self::MyShift; + fn of_field(field: F) -> Self; + fn shifted_to_field(&self) -> F; + fn shifted_raw(&self) -> F; + fn of_raw(shifted: F) -> Self; +} + +#[derive(Clone, Debug)] +pub struct Shift { + pub c: F, + pub scale: F, +} + +impl Shift +where + F: Field + From, +{ + /// + pub fn create() -> Self { + let c = (0..255).fold(F::one(), |accum, _| accum + accum) + F::one(); + + let scale: F = 2.into(); + let scale = scale.inverse().unwrap(); + + Self { c, scale } // TODO: This can be a constant + } +} + +#[derive(Clone, Debug)] +pub struct ShiftedValue { + pub shifted: F, +} + +impl> ToFieldElements + for ShiftedValue +{ + fn to_field_elements(&self, fields: &mut Vec) { + let Self { shifted } = self; + shifted.to_field_elements(fields); + } +} + +impl ShiftedValue +where + F: Field, +{ + /// Creates without shifting + pub fn new(field: F) -> Self { + Self { shifted: field } + } +} + +#[derive(Clone, Debug)] +pub struct ShiftFq { + pub shift: Fq, +} + +impl ShiftingValue for ShiftedValue { + type MyShift = Shift; + + fn shift() -> Self::MyShift { + let c = (0..255).fold(Fp::one(), |accum, _| accum + accum) + Fp::one(); + + let scale: Fp = 2.into(); + let scale = scale.inverse().unwrap(); + + Shift { c, scale } + } + + fn of_field(field: Fp) -> Self { + let shift = Self::shift(); + Self { + shifted: (field - shift.c) * shift.scale, + } + } + + fn shifted_to_field(&self) -> Fp { + let shift = Self::shift(); + self.shifted + self.shifted + shift.c + } + + fn shifted_raw(&self) -> Fp { + self.shifted + } + + fn of_raw(shifted: Fp) -> Self { + Self { shifted } + } +} + +impl ShiftingValue for ShiftedValue { + type MyShift = ShiftFq; + + fn shift() -> Self::MyShift { + ShiftFq { + shift: (0..255).fold(Fq::one(), |accum, _| accum + accum), + } + } + + fn of_field(field: Fq) -> Self { + let shift = Self::shift(); + Self { + shifted: field - shift.shift, + } + } + + fn shifted_to_field(&self) -> Fq { + let shift = Self::shift(); + self.shifted + shift.shift + } + + fn shifted_raw(&self) -> Fq { + self.shifted + } + + fn of_raw(shifted: Fq) -> Self { + Self { shifted } + } +} diff --git a/mina-tx-type/src/proofs/mod.rs b/mina-tx-type/src/proofs/mod.rs new file mode 100644 index 0000000000..3b4ca98011 --- /dev/null +++ b/mina-tx-type/src/proofs/mod.rs @@ -0,0 +1,28 @@ +//! Proof-related traits and types for the Mina Protocol. +//! +//! This module provides the core traits and types needed for zero-knowledge +//! proof generation and verification in Mina. +//! +//! # Future Migration +//! +//! The types and logic in this module are temporary placeholders. They will be +//! moved to dedicated crates when we reimplement: +//! - **Snarky** in Rust (circuit construction DSL) - see [issue #1762] +//! - **Pickles** in Rust (recursive proof composition system) - see [issue #1763] +//! +//! At that point, this module will re-export types from those crates instead of +//! defining them locally. +//! +//! [issue #1762]: https://github.com/o1-labs/mina-rust/issues/1762 +//! [issue #1763]: https://github.com/o1-labs/mina-rust/issues/1763 + +pub mod field; +pub mod to_field_elements; +pub mod witness; + +pub use field::{ + Boolean, CircuitVar, FieldWitness, FromFpFq, GroupAffine, IntoGeneric, Params, Shift, + ShiftedValue, ShiftingValue, ToBoolean, BACKEND_TICK_ROUNDS_N, BACKEND_TOCK_ROUNDS_N, +}; +pub use to_field_elements::{field_of_bits, field_to_bits, ToFieldElements}; +pub use witness::{Check, Witness}; diff --git a/mina-tx-type/src/proofs/to_field_elements.rs b/mina-tx-type/src/proofs/to_field_elements.rs new file mode 100644 index 0000000000..ba8e033388 --- /dev/null +++ b/mina-tx-type/src/proofs/to_field_elements.rs @@ -0,0 +1,334 @@ +//! Field element conversion trait for proof inputs. +//! +//! This module provides the [`ToFieldElements`] trait used to convert data +//! structures into field elements for use in zero-knowledge proof circuits. + +use std::borrow::Cow; + +use ark_ff::{BigInteger256, Field}; +use kimchi::proof::{PointEvaluations, ProofEvaluations}; +use mina_curves::pasta::{Fp, Fq}; +use mina_p2p_messages::string::ByteString; + +use super::field::{Boolean, CircuitVar, FieldWitness, GroupAffine, IntoGeneric}; + +/// Trait for converting values to field elements. +/// +/// This trait is used to serialize data structures into vectors of field +/// elements, which are the inputs to Mina's zero-knowledge proof circuits. +pub trait ToFieldElements { + /// Appends field elements to the provided vector. + fn to_field_elements(&self, fields: &mut Vec); + + /// Returns a new vector containing the field elements. + fn to_field_elements_owned(&self) -> Vec { + let mut fields = Vec::with_capacity(1024); + self.to_field_elements(&mut fields); + fields + } +} + +impl ToFieldElements for () { + fn to_field_elements(&self, _fields: &mut Vec) {} +} + +impl> ToFieldElements for Vec { + fn to_field_elements(&self, fields: &mut Vec) { + self.iter().for_each(|v| v.to_field_elements(fields)); + } +} + +impl> ToFieldElements for Box<[T]> { + fn to_field_elements(&self, fields: &mut Vec) { + self.iter().for_each(|v| v.to_field_elements(fields)); + } +} + +impl ToFieldElements for Fp { + fn to_field_elements(&self, fields: &mut Vec) { + fields.push(self.into_gen()); + } +} + +impl + Clone> ToFieldElements for Cow<'_, T> { + fn to_field_elements(&self, fields: &mut Vec) { + let this: &T = self.as_ref(); + this.to_field_elements(fields) + } +} + +struct FieldBitsIterator { + index: usize, + bigint: [u64; 4], +} + +impl Iterator for FieldBitsIterator { + type Item = bool; + + fn next(&mut self) -> Option { + let index = self.index; + self.index += 1; + + let limb_index = index / 64; + let bit_index = index % 64; + + let limb = self.bigint.get(limb_index)?; + Some(limb & (1 << bit_index) != 0) + } +} + +fn bigint_to_bits(bigint: BigInteger256) -> [bool; NBITS] { + let mut bits = FieldBitsIterator { + index: 0, + bigint: bigint.0, + } + .take(NBITS); + std::array::from_fn(|_| bits.next().unwrap()) +} + +pub fn field_to_bits(field: F) -> [bool; NBITS] +where + F: Field + Into, +{ + let bigint: BigInteger256 = field.into(); + bigint_to_bits(bigint) +} + +// pack +pub fn field_of_bits(bs: &[bool; N]) -> F { + bs.iter().rev().fold(F::zero(), |acc, b| { + let acc = acc + acc; + if *b { + acc + F::one() + } else { + acc + } + }) +} + +impl ToFieldElements for Fq { + fn to_field_elements(&self, fields: &mut Vec) { + use std::any::TypeId; + + // TODO: Refactor when specialization is stable + if TypeId::of::() == TypeId::of::() { + fields.push(self.into_gen()); + } else { + // `Fq` is larger than `Fp` so we have to split the field (low & high bits) + // See: + // + + let to_high_low = |fq: Fq| { + let [low, high @ ..] = field_to_bits::(fq); + [field_of_bits(&high), F::from(low)] + }; + fields.extend(to_high_low(*self)); + } + } +} + +impl, const N: usize> ToFieldElements for [T; N] { + fn to_field_elements(&self, fields: &mut Vec) { + self.iter().for_each(|v| v.to_field_elements(fields)); + } +} + +impl ToFieldElements for ByteString { + fn to_field_elements(&self, fields: &mut Vec) { + let slice: &[u8] = self; + slice.to_field_elements(fields); + } +} + +impl ToFieldElements for GroupAffine { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + x, y, infinity: _, .. + } = self; + y.to_field_elements(fields); + x.to_field_elements(fields); + } +} + +impl ToFieldElements for &'_ [u8] { + fn to_field_elements(&self, fields: &mut Vec) { + const BITS: [u8; 8] = [1, 2, 4, 8, 16, 32, 64, 128]; + fields.extend( + self.iter() + .flat_map(|byte| BITS.iter().map(|bit| F::from((*byte & bit != 0) as u64))), + ); + } +} + +impl ToFieldElements for &'_ [bool] { + fn to_field_elements(&self, fields: &mut Vec) { + fields.reserve(self.len()); + fields.extend(self.iter().copied().map(F::from)) + } +} + +impl ToFieldElements for bool { + fn to_field_elements(&self, fields: &mut Vec) { + F::from(*self).to_field_elements(fields) + } +} + +impl ToFieldElements for u64 { + fn to_field_elements(&self, fields: &mut Vec) { + F::from(*self).to_field_elements(fields) + } +} + +impl ToFieldElements for u32 { + fn to_field_elements(&self, fields: &mut Vec) { + F::from(*self).to_field_elements(fields) + } +} + +impl> ToFieldElements for PointEvaluations { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { zeta, zeta_omega } = self; + zeta.to_field_elements(fields); + zeta_omega.to_field_elements(fields); + } +} + +impl> ToFieldElements for ProofEvaluations { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + public: _, + w, + z, + s, + coefficients, + generic_selector, + poseidon_selector, + complete_add_selector, + mul_selector, + emul_selector, + endomul_scalar_selector, + range_check0_selector, + range_check1_selector, + foreign_field_add_selector, + foreign_field_mul_selector, + xor_selector, + rot_selector, + lookup_aggregation, + lookup_table, + lookup_sorted, + runtime_lookup_table, + runtime_lookup_table_selector, + xor_lookup_selector, + lookup_gate_lookup_selector, + range_check_lookup_selector, + foreign_field_mul_lookup_selector, + } = self; + + let mut push = |value: &T| { + value.to_field_elements(fields); + }; + + w.iter().for_each(&mut push); + coefficients.iter().for_each(&mut push); + push(z); + s.iter().for_each(&mut push); + push(generic_selector); + push(poseidon_selector); + push(complete_add_selector); + push(mul_selector); + push(emul_selector); + push(endomul_scalar_selector); + range_check0_selector.as_ref().map(&mut push); + range_check1_selector.as_ref().map(&mut push); + foreign_field_add_selector.as_ref().map(&mut push); + foreign_field_mul_selector.as_ref().map(&mut push); + xor_selector.as_ref().map(&mut push); + rot_selector.as_ref().map(&mut push); + lookup_aggregation.as_ref().map(&mut push); + lookup_table.as_ref().map(&mut push); + lookup_sorted.iter().for_each(|v| { + v.as_ref().map(&mut push); + }); + runtime_lookup_table.as_ref().map(&mut push); + runtime_lookup_table_selector.as_ref().map(&mut push); + xor_lookup_selector.as_ref().map(&mut push); + lookup_gate_lookup_selector.as_ref().map(&mut push); + range_check_lookup_selector.as_ref().map(&mut push); + foreign_field_mul_lookup_selector.as_ref().map(&mut push); + } +} + +impl, B: ToFieldElements> ToFieldElements for (A, B) { + fn to_field_elements(&self, fields: &mut Vec) { + let (a, b) = self; + a.to_field_elements(fields); + b.to_field_elements(fields); + } +} + +// Implementation for references +impl> ToFieldElements for &T { + fn to_field_elements(&self, fields: &mut Vec) { + (*self).to_field_elements(fields); + } +} + +impl ToFieldElements for Boolean { + fn to_field_elements(&self, fields: &mut Vec) { + self.to_field::().to_field_elements(fields); + } +} + +impl ToFieldElements for CircuitVar { + fn to_field_elements(&self, fields: &mut Vec) { + self.as_boolean().to_field_elements(fields); + } +} + +impl ToFieldElements for mina_signer::CompressedPubKey { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { x, is_odd } = self; + x.to_field_elements(fields); + is_odd.to_field_elements(fields); + } +} + +impl ToFieldElements for mina_signer::Signature { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { rx, s } = self; + + rx.to_field_elements(fields); + let s_bits = field_to_bits::<_, 255>(*s); + s_bits.to_field_elements(fields); + } +} + +impl ToFieldElements for mina_signer::PubKey { + fn to_field_elements(&self, fields: &mut Vec) { + let GroupAffine:: { x, y, .. } = self.point(); + x.to_field_elements(fields); + y.to_field_elements(fields); + } +} + +impl ToFieldElements + for mina_p2p_messages::v2::MinaBaseProtocolConstantsCheckedValueStableV1 +{ + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + k, + slots_per_epoch, + slots_per_sub_window, + grace_period_slots, + delta, + genesis_state_timestamp, + } = self; + + k.as_u32().to_field_elements(fields); + slots_per_epoch.as_u32().to_field_elements(fields); + slots_per_sub_window.as_u32().to_field_elements(fields); + grace_period_slots.as_u32().to_field_elements(fields); + delta.as_u32().to_field_elements(fields); + genesis_state_timestamp.as_u64().to_field_elements(fields); + } +} diff --git a/mina-tx-type/src/proofs/witness.rs b/mina-tx-type/src/proofs/witness.rs new file mode 100644 index 0000000000..d901cb6975 --- /dev/null +++ b/mina-tx-type/src/proofs/witness.rs @@ -0,0 +1,98 @@ +use super::{field::FieldWitness, to_field_elements::ToFieldElements}; + +/// Trait for checking values in the witness. +pub trait Check { + fn check(&self, w: &mut Witness); +} + +/// Trait for proof constants. +pub trait ProofConstants { + const PRIMARY_LEN: usize; + const AUX_LEN: usize; +} + +#[derive(Debug)] +pub struct Witness { + pub(crate) primary: Vec, + aux: Vec, + // Following fields are used to compare our witness with OCaml + pub ocaml_aux: Vec, + ocaml_aux_index: usize, +} + +impl Witness { + pub fn new() -> Self { + Self { + primary: Vec::with_capacity(C::PRIMARY_LEN), + aux: Vec::with_capacity(C::AUX_LEN), + ocaml_aux: Vec::new(), + ocaml_aux_index: 0, + } + } + + pub fn empty() -> Self { + Self { + primary: Vec::new(), + aux: Vec::new(), + ocaml_aux: Vec::new(), + ocaml_aux_index: 0, + } + } + + pub(crate) fn aux(&self) -> &[F] { + &self.aux + } + + pub fn exists(&mut self, data: T) -> T + where + T: ToFieldElements + Check, + { + let data = self.exists_no_check(data); + data.check(self); + data + } + + /// Same as `Self::exists`, but do not call `Check::check` on `data` + /// We use this wherever `seal` is used in OCaml, or on `if_` conditions + pub fn exists_no_check(&mut self, data: T) -> T + where + T: ToFieldElements, + { + #[cfg(test)] + let start = self.aux.len(); + + data.to_field_elements(&mut self.aux); + + #[cfg(test)] + self.assert_ocaml_aux(start); + + data + } + + /// Compare our witness with OCaml + #[cfg(test)] + fn assert_ocaml_aux(&mut self, start_offset: usize) { + if self.ocaml_aux.is_empty() { + return; + } + + let new_fields = &self.aux[start_offset..]; + let len = new_fields.len(); + let ocaml_fields = &self.ocaml_aux[start_offset..start_offset + len]; + + assert_eq!(start_offset, self.ocaml_aux_index); + assert_eq!(new_fields, ocaml_fields); + + self.ocaml_aux_index += len; + + eprintln!( + "index={:?} w{:?}", + start_offset + self.primary.capacity(), + &self.aux[start_offset..] + ); + } + + // Note: The helper methods that depend on transaction module functions + // (to_field_checked_prime, add_fast) are not included here as they would + // create circular dependencies. They remain in the ledger crate. +}