diff --git a/Cargo.lock b/Cargo.lock index f9fa798ff6..9bb0889bbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5317,6 +5317,7 @@ dependencies = [ "mina-p2p-messages", "mina-poseidon", "mina-signer", + "mina-tx-type", "num-bigint", "o1-utils", "once_cell", @@ -5346,6 +5347,18 @@ dependencies = [ "zstd", ] +[[package]] +name = "mina-tx-type" +version = "0.18.1" +dependencies = [ + "ark-ff", + "mina-curves", + "mina-p2p-messages", + "mina-signer", + "rand", + "serde", +] + [[package]] name = "minimal-lexical" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 3a6f198198..1ab1247d89 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/frontend/Dockerfile b/frontend/Dockerfile index ec03780915..d2bb7b38bc 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -69,6 +69,7 @@ COPY genesis_ledgers ./genesis_ledgers COPY ledger ./ledger COPY macros ./macros COPY mina-p2p-messages ./mina-p2p-messages +COPY mina-tx-type ./mina-tx-type COPY node ./node COPY p2p ./p2p COPY poseidon ./poseidon 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..6b6999c597 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}, + AmountToChecked, BalanceToChecked, SlotSpanToChecked, SlotToChecked, }, to_field_elements::ToFieldElements, }, diff --git a/ledger/src/hash.rs b/ledger/src/hash.rs index 6a6f68a14f..a0d71fd5b1 100644 --- a/ledger/src/hash.rs +++ b/ledger/src/hash.rs @@ -1,24 +1,39 @@ +//! Hash computation types and traits for the ledger. +//! +//! This module provides the [`ToInputs`] trait for converting types to hash +//! inputs, enabling Poseidon hash computation for transactions and other +//! protocol data structures. + use mina_curves::pasta::Fp; use mina_signer::CompressedPubKey; use crate::{proofs::witness::Witness, scan_state::currency}; use poseidon::hash::{hash_with_kimchi, Inputs, LazyParam}; +/// Trait for types that can be converted to hash inputs. +/// +/// This trait is the foundation for computing Poseidon hashes of protocol +/// data structures. Types implementing this trait can be hashed using +/// the Mina-compatible Kimchi hash function. pub trait ToInputs { + /// Appends the hash inputs for this value to the given input buffer. fn to_inputs(&self, inputs: &mut Inputs); + /// Creates a new input buffer containing this value's hash inputs. fn to_inputs_owned(&self) -> Inputs { let mut inputs = Inputs::new(); self.to_inputs(&mut inputs); inputs } + /// Computes the Poseidon hash of this value with the given parameter. fn hash_with_param(&self, param: &LazyParam) -> Fp { let mut inputs = Inputs::new(); self.to_inputs(&mut inputs); hash_with_kimchi(param, &inputs.to_fields()) } + /// Computes the Poseidon hash with circuit witness generation. fn checked_hash_with_param(&self, param: &LazyParam, w: &mut Witness) -> Fp { use crate::proofs::transaction::transaction_snark::checked_hash; @@ -73,7 +88,9 @@ where } } +/// Extension trait for appending [`ToInputs`] values to an input buffer. pub trait AppendToInputs { + /// Appends a value implementing [`ToInputs`] to this input buffer. fn append(&mut self, value: &T) where T: ToInputs; @@ -88,36 +105,37 @@ impl AppendToInputs for Inputs { } } -#[cfg(test)] -mod tests { - use o1_utils::FieldHelpers; - - use poseidon::hash::param_to_field; - #[cfg(target_family = "wasm")] - use wasm_bindgen_test::wasm_bindgen_test as test; +// ============================================================================ +// ToInputs implementations for currency types +// ============================================================================ + +macro_rules! impl_to_inputs { + (32: { $($name32:ident,)* }, 64: { $($name64:ident,)* },) => { + $( + impl ToInputs for currency::$name32 { + fn to_inputs(&self, inputs: &mut Inputs) { + inputs.append_u32(self.as_u32()); + } + } + )* + $( + impl ToInputs for currency::$name64 { + fn to_inputs(&self, inputs: &mut Inputs) { + inputs.append_u64(self.as_u64()); + } + } + )* + }; +} - use super::*; +impl_to_inputs!( + 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, Epoch, }, + 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, N, }, +); - #[test] - fn test_param() { - for (s, hex) in [ - ( - "", - "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000000000000000", - ), - ( - "hello", - "68656c6c6f2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000000000000000", - ), - ( - "aaaaaaaaaaaaaaaaaaaa", - "6161616161616161616161616161616161616161000000000000000000000000", - ), - ] { - let field = param_to_field(s); - assert_eq!(field.to_hex(), hex); - } - } +#[cfg(test)] +mod tests { + use poseidon::hash::Inputs; #[test] fn test_inputs() { @@ -132,31 +150,5 @@ mod tests { elog!("INPUTS={:?}", inputs); elog!("FIELDS={:?}", inputs.to_fields()); - - // // Self::timing - // match self.timing { - // Timing::Untimed => { - // roi.append_bool(false); - // roi.append_u64(0); // initial_minimum_balance - // roi.append_u32(0); // cliff_time - // roi.append_u64(0); // cliff_amount - // roi.append_u32(1); // vesting_period - // roi.append_u64(0); // vesting_increment - // } - // Timing::Timed { - // initial_minimum_balance, - // cliff_time, - // cliff_amount, - // vesting_period, - // vesting_increment, - // } => { - // roi.append_bool(true); - // roi.append_u64(initial_minimum_balance); - // roi.append_u32(cliff_time); - // roi.append_u64(cliff_amount); - // roi.append_u32(vesting_period); - // roi.append_u64(vesting_increment); - // } - // } } } diff --git a/ledger/src/proofs/block.rs b/ledger/src/proofs/block.rs index 8aafcf2c82..77dae8e404 100644 --- a/ledger/src/proofs/block.rs +++ b/ledger/src/proofs/block.rs @@ -28,7 +28,7 @@ use crate::{ wrap::{wrap, WrapParams}, }, scan_state::{ - currency, + currency::{self, Magnitude}, fee_excess::{self, FeeExcess}, pending_coinbase::{PendingCoinbase, PendingCoinbaseWitness, Stack}, protocol_state::MinaHash, @@ -47,6 +47,7 @@ use super::{ numbers::{ currency::CheckedAmount, nat::{CheckedBlockTime, CheckedBlockTimeSpan, CheckedLength}, + AmountToChecked, LengthToChecked, SignedAmountToChecked, }, step::{step, InductiveRule, OptFlag, PreviousProofStatement, StepParams, StepProof}, to_field_elements::ToFieldElements, diff --git a/ledger/src/proofs/merge.rs b/ledger/src/proofs/merge.rs index de724caec1..601986ff51 100644 --- a/ledger/src/proofs/merge.rs +++ b/ledger/src/proofs/merge.rs @@ -23,6 +23,7 @@ use crate::{ use super::{ constants::WrapMergeProof, field::{Boolean, CircuitVar, FieldWitness}, + numbers::{SignedAmountToChecked, SignedFeeToChecked}, public_input::plonk_checks::PlonkMinimal, step::{ extract_recursion_challenges, InductiveRule, OptFlag, PreviousProofStatement, StepProof, diff --git a/ledger/src/proofs/numbers/currency.rs b/ledger/src/proofs/numbers/currency.rs index e3710b6019..b5202c7afe 100644 --- a/ledger/src/proofs/numbers/currency.rs +++ b/ledger/src/proofs/numbers/currency.rs @@ -510,22 +510,6 @@ macro_rules! impl_currency { } } - impl $unchecked { - pub fn to_checked(&self) -> $name { - $name::from_inner(*self) - } - } - - 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 $unchecked { type CheckedType = $name; fn checked_from_field(field: F) -> Self::CheckedType { @@ -543,3 +527,76 @@ impl_currency!( {CheckedFee, Fee}, {CheckedBalance, Balance} ); + +// Extension traits for to_checked conversion +pub trait AmountToChecked { + fn to_checked(&self) -> CheckedAmount; +} + +impl AmountToChecked for Amount { + fn to_checked(&self) -> CheckedAmount { + CheckedAmount::from_inner(*self) + } +} + +pub trait FeeToChecked { + fn to_checked(&self) -> CheckedFee; +} + +impl FeeToChecked for Fee { + fn to_checked(&self) -> CheckedFee { + CheckedFee::from_inner(*self) + } +} + +pub trait BalanceToChecked { + fn to_checked(&self) -> CheckedBalance; +} + +impl BalanceToChecked for Balance { + fn to_checked(&self) -> CheckedBalance { + CheckedBalance::from_inner(*self) + } +} + +pub trait SignedAmountToChecked { + fn to_checked(&self) -> CheckedSigned>; +} + +impl SignedAmountToChecked for Signed { + fn to_checked(&self) -> CheckedSigned> { + CheckedSigned { + magnitude: AmountToChecked::to_checked::(&self.magnitude), + sgn: CircuitVar::Var(self.sgn), + value: Cell::new(None), + } + } +} + +pub trait SignedFeeToChecked { + fn to_checked(&self) -> CheckedSigned>; +} + +impl SignedFeeToChecked for Signed { + fn to_checked(&self) -> CheckedSigned> { + CheckedSigned { + magnitude: FeeToChecked::to_checked::(&self.magnitude), + sgn: CircuitVar::Var(self.sgn), + value: Cell::new(None), + } + } +} + +pub trait SignedBalanceToChecked { + fn to_checked(&self) -> CheckedSigned>; +} + +impl SignedBalanceToChecked for Signed { + fn to_checked(&self) -> CheckedSigned> { + CheckedSigned { + magnitude: BalanceToChecked::to_checked::(&self.magnitude), + sgn: CircuitVar::Var(self.sgn), + value: Cell::new(None), + } + } +} diff --git a/ledger/src/proofs/numbers/mod.rs b/ledger/src/proofs/numbers/mod.rs index e0ed94cedf..c535491434 100644 --- a/ledger/src/proofs/numbers/mod.rs +++ b/ledger/src/proofs/numbers/mod.rs @@ -1,3 +1,13 @@ pub mod common; pub mod currency; pub mod nat; + +// Re-export extension traits for to_checked conversion +pub use currency::{ + AmountToChecked, BalanceToChecked, FeeToChecked, SignedAmountToChecked, SignedBalanceToChecked, + SignedFeeToChecked, +}; +pub use nat::{ + BlockTimeSpanToChecked, BlockTimeToChecked, IndexToChecked, LengthToChecked, NonceToChecked, + SlotSpanToChecked, SlotToChecked, TxnVersionToChecked, +}; diff --git a/ledger/src/proofs/numbers/nat.rs b/ledger/src/proofs/numbers/nat.rs index e09a83b4c0..4e3c53ff62 100644 --- a/ledger/src/proofs/numbers/nat.rs +++ b/ledger/src/proofs/numbers/nat.rs @@ -204,7 +204,7 @@ impl CheckedSlot { w: &mut Witness, ) -> Boolean { // constant - let c = |n: u32| Length::from_u32(n).to_checked(); + let c = |n: u32| LengthToChecked::to_checked::(&Length::from_u32(n)); let third_epoch = { let (q, _r) = constants.slots_per_epoch.div_mod(&c(3), w); q @@ -260,12 +260,6 @@ macro_rules! impl_nat { } } - impl $unchecked { - pub fn to_checked(&self) -> $name { - $name::from_inner(*self) - } - } - impl ForZkappCheck for $unchecked { type CheckedType = $name; fn checked_from_field(field: F) -> Self::CheckedType { @@ -349,3 +343,85 @@ impl CheckedN32 { Self::from_field(n.into()) } } + +// Extension traits for to_checked conversion + +pub trait TxnVersionToChecked { + fn to_checked(&self) -> CheckedTxnVersion; +} + +impl TxnVersionToChecked for TxnVersion { + fn to_checked(&self) -> CheckedTxnVersion { + CheckedTxnVersion::from_inner(*self) + } +} + +pub trait SlotToChecked { + fn to_checked(&self) -> CheckedSlot; +} + +impl SlotToChecked for Slot { + fn to_checked(&self) -> CheckedSlot { + CheckedSlot::from_inner(*self) + } +} + +pub trait SlotSpanToChecked { + fn to_checked(&self) -> CheckedSlotSpan; +} + +impl SlotSpanToChecked for SlotSpan { + fn to_checked(&self) -> CheckedSlotSpan { + CheckedSlotSpan::from_inner(*self) + } +} + +pub trait LengthToChecked { + fn to_checked(&self) -> CheckedLength; +} + +impl LengthToChecked for Length { + fn to_checked(&self) -> CheckedLength { + CheckedLength::from_inner(*self) + } +} + +pub trait NonceToChecked { + fn to_checked(&self) -> CheckedNonce; +} + +impl NonceToChecked for Nonce { + fn to_checked(&self) -> CheckedNonce { + CheckedNonce::from_inner(*self) + } +} + +pub trait IndexToChecked { + fn to_checked(&self) -> CheckedIndex; +} + +impl IndexToChecked for Index { + fn to_checked(&self) -> CheckedIndex { + CheckedIndex::from_inner(*self) + } +} + +pub trait BlockTimeToChecked { + fn to_checked(&self) -> CheckedBlockTime; +} + +impl BlockTimeToChecked for BlockTime { + fn to_checked(&self) -> CheckedBlockTime { + CheckedBlockTime::from_inner(*self) + } +} + +pub trait BlockTimeSpanToChecked { + fn to_checked(&self) -> CheckedBlockTimeSpan; +} + +impl BlockTimeSpanToChecked for BlockTimeSpan { + fn to_checked(&self) -> CheckedBlockTimeSpan { + CheckedBlockTimeSpan::from_inner(*self) + } +} diff --git a/ledger/src/proofs/transaction.rs b/ledger/src/proofs/transaction.rs index 01989b624e..91b31d8445 100644 --- a/ledger/src/proofs/transaction.rs +++ b/ledger/src/proofs/transaction.rs @@ -52,6 +52,10 @@ use crate::{ use super::{ constants::ProofConstants, field::{field, Boolean, CircuitVar, FieldWitness, GroupAffine, ToBoolean}, + numbers::{ + AmountToChecked, BalanceToChecked, FeeToChecked, NonceToChecked, SignedAmountToChecked, + SignedFeeToChecked, SlotToChecked, + }, public_input::messages::{dummy_ipa_step_sg, MessagesForNextWrapProof}, step, step::{InductiveRule, OptFlag, StepProof}, diff --git a/ledger/src/proofs/zkapp.rs b/ledger/src/proofs/zkapp.rs index 55c49daca8..d12028baea 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}, + IndexToChecked, SignedAmountToChecked, SlotToChecked, }, 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..3d3af5793b 100644 --- a/ledger/src/scan_state/conv.rs +++ b/ledger/src/scan_state/conv.rs @@ -10,11 +10,9 @@ use mina_p2p_messages::{ pseq::PaddedSeq, string::CharString, v2::{ - self, BlockTimeTimeStableV1, - ConsensusProofOfStakeDataEpochDataNextValueVersionedValueStableV1, + self, ConsensusProofOfStakeDataEpochDataNextValueVersionedValueStableV1, ConsensusProofOfStakeDataEpochDataStakingValueVersionedValueStableV1, - CurrencyAmountStableV1, CurrencyBalanceStableV1, CurrencyFeeStableV1, - DataHashLibStateHashStableV1, EpochSeed, LedgerProofProdStableV2, + CurrencyBalanceStableV1, DataHashLibStateHashStableV1, EpochSeed, LedgerProofProdStableV2, MinaBaseAccountIdDigestStableV1, MinaBaseAccountIdStableV2, MinaBaseAccountUpdateBodyEventsStableV1, MinaBaseAccountUpdateBodyFeePayerStableV1, MinaBaseAccountUpdateBodyStableV1, MinaBaseAccountUpdateFeePayerStableV1, @@ -52,12 +50,9 @@ use mina_p2p_messages::{ MinaBaseZkappPreconditionProtocolStateStableV1GlobalSlotA, MinaBaseZkappPreconditionProtocolStateStableV1Length, MinaBaseZkappPreconditionProtocolStateStableV1LengthA, - MinaNumbersGlobalSlotSinceGenesisMStableV1, MinaNumbersGlobalSlotSinceHardForkMStableV1, - MinaNumbersGlobalSlotSpanStableV1, MinaStateBlockchainStateValueStableV2LedgerProofStatement, MinaStateBlockchainStateValueStableV2LedgerProofStatementSource, - MinaStateBlockchainStateValueStableV2SignedAmount, MinaStateSnarkedLedgerStateStableV2, - MinaStateSnarkedLedgerStateWithSokStableV2, + MinaStateSnarkedLedgerStateStableV2, MinaStateSnarkedLedgerStateWithSokStableV2, MinaTransactionLogicTransactionAppliedCoinbaseAppliedStableV2, MinaTransactionLogicTransactionAppliedCoinbaseAppliedStableV2Coinbase, MinaTransactionLogicTransactionAppliedCommandAppliedStableV2, @@ -73,7 +68,7 @@ use mina_p2p_messages::{ MinaTransactionLogicTransactionAppliedZkappCommandAppliedStableV1Command, MinaTransactionLogicZkappCommandLogicLocalStateValueStableV1, MinaTransactionTransactionStableV2, ParallelScanJobStatusStableV1, - ParallelScanSequenceNumberStableV1, ParallelScanWeightStableV1, SgnStableV1, SignedAmount, + ParallelScanSequenceNumberStableV1, ParallelScanWeightStableV1, SignedAmount, StagedLedgerDiffDiffDiffStableV2, StagedLedgerDiffDiffFtStableV1, StagedLedgerDiffDiffPreDiffWithAtMostOneCoinbaseStableV2, StagedLedgerDiffDiffPreDiffWithAtMostOneCoinbaseStableV2Coinbase, @@ -92,7 +87,7 @@ use mina_p2p_messages::{ TransactionSnarkScanStateStableV2TreesAMerge, TransactionSnarkScanStateTransactionWithWitnessStableV2, TransactionSnarkStableV2, TransactionSnarkWorkTStableV2, TransactionSnarkWorkTStableV2Proofs, - UnsignedExtendedUInt32StableV1, UnsignedExtendedUInt64Int64ForVersionTagsStableV1, + UnsignedExtendedUInt32StableV1, }, }; use mina_signer::Signature; @@ -101,7 +96,6 @@ use crate::{ array_into_with, proofs::field::FieldWitness, scan_state::{ - currency::BlockTime, pending_coinbase::{Stack, StackHasher}, scan_state::BorderBlockContinuedInTheNextTree, transaction_logic::{ @@ -116,7 +110,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,173 +136,7 @@ use super::{ }, }; -impl From for Amount { - fn from(value: CurrencyAmountStableV1) -> Self { - Self(value.as_u64()) - } -} - -impl From for Balance { - 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<&Balance> for CurrencyBalanceStableV1 { - fn from(value: &Balance) -> Self { - Self((*value).into()) - } -} - -impl From for CurrencyAmountStableV1 { - fn from(value: Balance) -> Self { - Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( - value.as_u64().into(), - )) - } -} - -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()) - } -} - -impl From<&Nonce> for mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1 { - fn from(value: &Nonce) -> Self { - Self(value.as_u32().into()) - } -} - -impl From<&mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1> for Nonce { - fn from(value: &mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1) -> Self { - Self::from_u32(value.as_u32()) - } -} - -impl From<&mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1> for Slot { - fn from(value: &mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1) -> Self { - Self::from_u32(value.as_u32()) - } -} - -impl From<&Slot> for mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1 { - fn from(value: &Slot) -> Self { - Self(value.as_u32().into()) - } -} - -impl From<&mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1> for Length { - fn from(value: &mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1) -> Self { - Self::from_u32(value.0.as_u32()) - } -} - -impl From<&Length> for mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1 { - fn from(value: &Length) -> Self { - Self(value.as_u32().into()) - } -} - -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(), - } - } -} +// Currency type conversions are implemented in mina-tx-type impl TryFrom<&MinaBaseFeeExcessStableV1> for FeeExcess { type Error = InvalidBigInt; @@ -552,28 +380,6 @@ 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(), - } - } -} - impl TryFrom<&MinaStateBlockchainStateValueStableV2LedgerProofStatement> for Statement<()> { type Error = InvalidBigInt; @@ -1018,14 +824,6 @@ impl TryFrom<&MinaBaseAccountUpdatePreconditionsStableV1> for zkapp_command::Pre } } -impl From<&BlockTime> for BlockTimeTimeStableV1 { - fn from(value: &BlockTime) -> Self { - Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( - value.as_u64().into(), - )) - } -} - impl From<&zkapp_command::Preconditions> for MinaBaseAccountUpdatePreconditionsStableV1 { fn from(value: &zkapp_command::Preconditions) -> Self { use mina_p2p_messages::v2::{ @@ -3527,45 +3325,6 @@ impl From<&crate::staged_ledger::diff::with_valid_signatures_and_proofs::Diff> } } -impl From<&MinaNumbersGlobalSlotSinceGenesisMStableV1> for Slot { - fn from(value: &MinaNumbersGlobalSlotSinceGenesisMStableV1) -> Self { - let MinaNumbersGlobalSlotSinceGenesisMStableV1::SinceGenesis(slot) = value; - Self(slot.as_u32()) - } -} - -impl From<&MinaNumbersGlobalSlotSinceHardForkMStableV1> for Slot { - fn from(value: &MinaNumbersGlobalSlotSinceHardForkMStableV1) -> Self { - let MinaNumbersGlobalSlotSinceHardForkMStableV1::SinceHardFork(slot) = value; - Self(slot.as_u32()) - } -} - -impl From<&MinaNumbersGlobalSlotSpanStableV1> for SlotSpan { - fn from(value: &MinaNumbersGlobalSlotSpanStableV1) -> Self { - let MinaNumbersGlobalSlotSpanStableV1::GlobalSlotSpan(slot) = value; - Self(slot.as_u32()) - } -} - -impl From<&Slot> for MinaNumbersGlobalSlotSinceGenesisMStableV1 { - fn from(value: &Slot) -> Self { - Self::SinceGenesis(value.as_u32().into()) - } -} - -impl From<&Slot> for MinaNumbersGlobalSlotSinceHardForkMStableV1 { - fn from(value: &Slot) -> Self { - Self::SinceHardFork(value.as_u32().into()) - } -} - -impl From<&SlotSpan> for MinaNumbersGlobalSlotSpanStableV1 { - fn from(value: &SlotSpan) -> Self { - Self::GlobalSlotSpan(value.as_u32().into()) - } -} - impl From<&ZkappStatement> for v2::MinaBaseZkappStatementStableV2 { fn from(value: &ZkappStatement) -> Self { use transaction_logic::zkapp_statement::TransactionCommitment; diff --git a/ledger/src/scan_state/currency.rs b/ledger/src/scan_state/currency.rs index d7c4862e74..5d34b4502a 100644 --- a/ledger/src/scan_state/currency.rs +++ b/ledger/src/scan_state/currency.rs @@ -1,554 +1,53 @@ -use std::cmp::Ordering::{Equal, Greater, Less}; - -use ark_ff::{BigInteger256, Field}; -use mina_p2p_messages::v2::BlockTimeTimeStableV1; -use rand::Rng; +//! Currency types with ledger-specific implementations. +//! +//! Re-exports types from [`mina_tx_type::currency`] and adds implementations +//! for ledger-specific traits like [`ToFieldElements`] and [`Check`]. +//! +//! Note: [`ToInputs`] implementations are provided by mina-tx-type. use crate::proofs::{ field::FieldWitness, to_field_elements::ToFieldElements, transaction::Check, witness::Witness, }; -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Sgn { - Pos, - Neg, -} - -impl Sgn { - pub fn is_pos(&self) -> bool { - match self { - Sgn::Pos => true, - Sgn::Neg => false, - } - } - - 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(), - } - } -} - -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) - } - - fn sub_flagged(&self, rhs: &Self) -> (Self, bool) { - (self.wrapping_sub(rhs), self < rhs) - } - - 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 }, - } - } - - pub fn of_unsigned(magnitude: T) -> Self { - Self::create(magnitude, Sgn::Pos) - } - - pub fn negate(&self) -> Self { - if self.magnitude.is_zero() { - Self::zero() - } else { - Self { - magnitude: self.magnitude, - sgn: self.sgn.negate(), - } - } - } - - pub fn is_pos(&self) -> bool { - matches!(self.sgn, Sgn::Pos) - } - - /// - pub fn is_non_neg(&self) -> bool { - matches!(self.sgn, Sgn::Pos) - } - - pub fn is_neg(&self) -> bool { - matches!(self.sgn, Sgn::Neg) - } - - /// - pub fn zero() -> Self { - Self { - magnitude: T::zero(), - sgn: Sgn::Pos, - } - } - - pub fn is_zero(&self) -> bool { - self.magnitude.is_zero() //&& matches!(self.sgn, Sgn::Pos) - } - - /// - 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)) - } - - 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 { - pub fn to_fee(self) -> Signed { - let Self { magnitude, sgn } = self; - - Signed { - magnitude: Fee(magnitude.0), - sgn, - } - } -} - -impl Signed -where - T: Magnitude + PartialOrd + Ord + Clone, - 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) - } - } - - 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 { - pub fn sub_amount(&self, amount: Amount) -> Option { - self.0.checked_sub(amount.0).map(Self) - } - - pub fn add_amount(&self, amount: Amount) -> Option { - self.0.checked_add(amount.0).map(Self) - } - - 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) - } - } - - pub fn add_signed_amount_flagged(&self, rhs: Signed) -> (Self, bool) { - let rhs = Signed { - magnitude: Balance::from_u64(rhs.magnitude.0), - sgn: rhs.sgn, - }; - - if let Sgn::Pos = rhs.sgn { - self.add_flagged(&rhs.magnitude) - } else { - self.sub_flagged(&rhs.magnitude) - } - } - - pub fn to_amount(self) -> Amount { - Amount(self.0) - } - - /// Passed amount gets multiplied by 1 billion to convert to nanomina. - pub fn from_mina(amount: u64) -> Option { - amount.checked_mul(1_000_000_000).map(Self::from_u64) - } - - pub fn of_nanomina_int_exn(int: u64) -> Self { - Self::from_u64(int) - } -} - -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 { - Self(self.0.wrapping_add(1)) - } -} - -impl Nonce { - // TODO: Not sure if OCaml wraps around here - pub fn incr(&self) -> Self { - Self(self.0.wrapping_add(1)) - } - - pub fn succ(&self) -> Self { - self.incr() - } - - 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) - } - } - - /// low <= self <= high - pub fn between(&self, low: &Self, high: &Self) -> bool { - low <= self && self <= high - } -} - -impl BlockTime { - pub fn add(&self, span: BlockTimeSpan) -> Self { - Self(self.0.checked_add(span.0).unwrap()) - } - - pub fn sub(&self, span: BlockTimeSpan) -> Self { - Self(self.0.checked_sub(span.0).unwrap()) - } - - pub fn diff(&self, other: Self) -> BlockTimeSpan { - BlockTimeSpan(self.0 - other.0) - } +// Re-export all types from mina-tx-type +pub use mina_tx_type::currency::{ + Amount, Balance, BlockTime, BlockTimeSpan, Epoch, Fee, FieldLike, Index, Length, Magnitude, + MinMax, Nonce, Sgn, Signed, SignedRandExt, Slot, SlotRandExt, SlotSpan, ToChecked, TxnVersion, + N, +}; - pub fn to_span_since_epoch(&self) -> BlockTimeSpan { - let Self(ms) = self; - BlockTimeSpan(*ms) - } +// ============================================================================ +// Ledger-specific trait implementations for N +// ============================================================================ - pub fn of_span_since_epoch(span: BlockTimeSpan) -> Self { - let BlockTimeSpan(ms) = span; - Self(ms) +impl ToFieldElements for N { + fn to_field_elements(&self, fields: &mut Vec) { + fields.push(self.to_field()); } } -impl From for BlockTime { - fn from(bt: BlockTimeTimeStableV1) -> Self { - Self(bt.0 .0 .0) +impl Check for N { + fn check(&self, witnesses: &mut Witness) { + use crate::proofs::transaction::scalar_challenge::to_field_checked_prime; + const NBITS: usize = 64; + let number: u64 = self.as_u64(); + let number: F = number.into(); + to_field_checked_prime::(number, witnesses); } } -impl BlockTimeSpan { - pub fn of_ms(ms: u64) -> Self { - Self(ms) - } - pub fn to_ms(&self) -> u64 { - let Self(ms) = self; - *ms - } -} +// ============================================================================ +// Macro for implementing ledger-specific traits on currency types +// ============================================================================ -impl Slot { - // TODO: Not sure if OCaml wraps around here - pub fn incr(&self) -> Self { - Self(self.0.wrapping_add(1)) - } - - pub fn add(&self, other: SlotSpan) -> Self { - let SlotSpan(other) = other; - Self(self.0.checked_add(other).unwrap()) - } - - pub fn succ(&self) -> Self { - self.incr() - } - - pub fn gen_small() -> Self { - let mut rng = rand::thread_rng(); - Self(rng.gen::() % 10_000) - } -} - -macro_rules! impl_number { +/// Macro to implement ToFieldElements and Check for currency types. +/// ToInputs implementations are provided by mina-tx-type. +macro_rules! impl_ledger_traits { (32: { $($name32:ident,)* }, 64: { $($name64:ident,)* },) => { - $(impl_number!({$name32, u32, as_u32, from_u32, next_u32, append_u32},);)+ - $(impl_number!({$name64, u64, as_u64, from_u64, next_u64, append_u64},);)+ + $(impl_ledger_traits!({$name32, u32, as_u32},);)* + $(impl_ledger_traits!({$name64, u64, as_u64},);)* }; - ($({ $name:ident, $inner:ty, $as_name:ident, $from_name:ident, $next_name:ident, $append_name:ident },)*) => ($( - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] - pub struct $name(pub(super) $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)) - } - - fn to_field(&self) -> F { - self.to_field() - } - - 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 $name { - pub const NBITS: usize = <$inner>::BITS as usize; - - pub fn $as_name(&self) -> $inner { - self.0 - } - - pub const fn $from_name(value: $inner) -> Self { - Self(value) - } - - /// - pub const fn scale(&self, n: $inner) -> Option { - match self.0.checked_mul(n) { - Some(n) => Some(Self(n)), - None => None - } - } - - pub fn min() -> Self { - ::min() - } - - pub fn max() -> Self { - ::max() - } - - /// - 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) - } - - pub fn to_bits(&self) -> [bool; <$inner>::BITS as usize] { - use crate::proofs::transaction::legacy_input::bits_iter; - - let mut iter = bits_iter::<$inner, { <$inner>::BITS as usize }>(self.0); - std::array::from_fn(|_| iter.next().unwrap()) - } - - pub fn to_field>(&self) -> F { - let int = self.0 as u64; - F::from(int) - } - } - - impl rand::distributions::Distribution<$name> for rand::distributions::Standard { - fn sample(&self, rng: &mut R) -> $name { - $name(rng.$next_name()) - } - } - - impl crate::ToInputs for $name { - fn to_inputs(&self, inputs: &mut poseidon::hash::Inputs) { - inputs.$append_name(self.0); - } - } - + ($({ $name:ident, $inner:ty, $as_name:ident },)*) => ($( impl ToFieldElements for $name { fn to_field_elements(&self, fields: &mut Vec) { fields.push(self.to_field()); @@ -568,11 +67,10 @@ macro_rules! impl_number { to_field_checked_prime::(number, witnesses); } } - - )+) + )*) } -impl_number!( +impl_ledger_traits!( 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, Epoch, }, - 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, N, }, + 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, }, ); diff --git a/ledger/src/scan_state/fee_excess.rs b/ledger/src/scan_state/fee_excess.rs index cb4756ccb5..6b590a2c5b 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}, + SignedFeeToChecked, + }, witness::Witness, }, AppendToInputs, ToInputs, TokenId, diff --git a/ledger/src/scan_state/pending_coinbase.rs b/ledger/src/scan_state/pending_coinbase.rs index 0f883a9cf3..7a2863eb23 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}, + AmountToChecked, }, transaction::transaction_snark::checked_hash, witness::Witness, diff --git a/ledger/src/scan_state/transaction_logic/local_state.rs b/ledger/src/scan_state/transaction_logic/local_state.rs index dacb137740..0b6b39fdc1 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, IndexToChecked, SignedAmountToChecked}, to_field_elements::ToFieldElements, witness::Witness, }, 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..167bd11d15 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, SignedRandExt, + Slot, SlotSpan, }, fee_excess::FeeExcess, GenesisConstant, GENESIS_CONSTANT, diff --git a/ledger/src/staged_ledger/staged_ledger.rs b/ledger/src/staged_ledger/staged_ledger.rs index cc0e9465b6..d7218c7876 100644 --- a/ledger/src/staged_ledger/staged_ledger.rs +++ b/ledger/src/staged_ledger/staged_ledger.rs @@ -2025,7 +2025,7 @@ mod tests_ocaml { user_command::sequence_zkapp_command_with_ledger, zkapp_command_builder, Failure, }, scan_state::{ - currency::{Balance, Fee, Nonce, SlotSpan}, + currency::{Balance, Fee, Nonce, SlotRandExt, SlotSpan}, scan_state::transaction_snark::SokDigest, transaction_logic::{ apply_transactions, diff --git a/ledger/src/transaction_pool.rs b/ledger/src/transaction_pool.rs index f6eb38d16f..b2e9e51e7c 100644 --- a/ledger/src/transaction_pool.rs +++ b/ledger/src/transaction_pool.rs @@ -117,9 +117,18 @@ mod consensus { } } - // Consensus epoch - impl Epoch { - fn of_time_exn(constants: &Constants, time: BlockTime) -> Result { + // Extension trait for consensus epoch methods + pub trait EpochConsensusExt { + fn of_time_exn(constants: &Constants, time: BlockTime) -> Result; + fn start_time(constants: &Constants, epoch: Epoch) -> BlockTime; + fn epoch_and_slot_of_time_exn( + constants: &Constants, + time: BlockTime, + ) -> Result<(Epoch, Slot), String>; + } + + impl EpochConsensusExt for Epoch { + fn of_time_exn(constants: &Constants, time: BlockTime) -> Result { if time < constants.genesis_state_timestamp { return Err( "Epoch.of_time: time is earlier than genesis block timestamp".to_string(), @@ -130,10 +139,10 @@ mod consensus { let epoch = time_since_genesis.to_ms() / constants.epoch_duration.to_ms(); let epoch: u32 = epoch.try_into().unwrap(); - Ok(Self::from_u32(epoch)) + Ok(Epoch::from_u32(epoch)) } - fn start_time(constants: &Constants, epoch: Self) -> BlockTime { + fn start_time(constants: &Constants, epoch: Epoch) -> BlockTime { let ms = constants .genesis_state_timestamp .to_span_since_epoch() @@ -142,12 +151,13 @@ mod consensus { BlockTime::of_span_since_epoch(BlockTimeSpan::of_ms(ms)) } - pub fn epoch_and_slot_of_time_exn( + fn epoch_and_slot_of_time_exn( constants: &Constants, time: BlockTime, - ) -> Result<(Self, Slot), String> { - let epoch = Self::of_time_exn(constants, time)?; - let time_since_epoch = time.diff(Self::start_time(constants, epoch)); + ) -> Result<(Epoch, Slot), String> { + let epoch = ::of_time_exn(constants, time)?; + let time_since_epoch = + time.diff(::start_time(constants, epoch)); let slot: u64 = time_since_epoch.to_ms() / constants.slot_duration_ms.to_ms(); let slot = Slot::from_u32(slot.try_into().unwrap()); diff --git a/ledger/src/zkapps/snark.rs b/ledger/src/zkapps/snark.rs index c9c5423e4a..00f0c09232 100644 --- a/ledger/src/zkapps/snark.rs +++ b/ledger/src/zkapps/snark.rs @@ -19,6 +19,8 @@ use crate::{ numbers::{ currency::{CheckedAmount, CheckedBalance, CheckedCurrency, CheckedSigned}, nat::{CheckedIndex, CheckedNat, CheckedSlot}, + AmountToChecked, BalanceToChecked, NonceToChecked, SignedAmountToChecked, + SlotSpanToChecked, SlotToChecked, TxnVersionToChecked, }, 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..9ffaf388ee --- /dev/null +++ b/mina-tx-type/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mina-tx-type" +version = "0.18.1" +edition = "2021" +description = "Transaction types for the Mina Protocol" +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/o1-labs/mina-rust" +keywords = ["mina", "blockchain", "cryptocurrency", "zkapp", "transaction"] +categories = ["cryptography::cryptocurrencies"] + +[dependencies] +ark-ff = { workspace = true } +mina-curves = { workspace = true } +mina-p2p-messages = { workspace = true } +mina-signer = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } diff --git a/mina-tx-type/src/currency.rs b/mina-tx-type/src/currency.rs new file mode 100644 index 0000000000..901c1fec8d --- /dev/null +++ b/mina-tx-type/src/currency.rs @@ -0,0 +1,1262 @@ +//! Currency and numeric types for the Mina Protocol +//! +//! This module defines the fundamental numeric types used throughout Mina +//! transactions, including amounts, balances, fees, nonces, and slots. +//! +//! # Type Overview +//! +//! - [`Amount`]: Token amount in nanomina (1 MINA = 10^9 nanomina) +//! - [`Balance`]: Account balance in nanomina +//! - [`Fee`]: Transaction fee in nanomina +//! - [`Nonce`]: Account transaction sequence number +//! - [`Slot`]: Global slot number since genesis +//! - [`Length`]: Blockchain length (block height) +//! +//! # Signed Values +//! +//! The [`Signed`] type wraps any magnitude type to represent positive or +//! negative values, commonly used for balance changes in transactions. + +use core::cmp::Ordering::{Equal, Greater, Less}; + +use ark_ff::{BigInteger256, Field}; +use serde::{Deserialize, Serialize}; + +/// Trait bound for field types that support conversion to/from BigInteger256. +/// +/// This is satisfied by `Fp` (Pallas base field) and any type implementing +/// the ledger's `FieldWitness` trait. +pub trait FieldLike: Field + From + Into {} + +impl FieldLike for F where F: Field + From + Into {} + +/// Sign of a value (positive or negative). +/// +/// # References +/// +/// OCaml: `src/lib/currency/currency.ml` (Sgn) +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Sgn { + /// Positive value. + #[default] + Pos, + /// Negative value. + Neg, +} + +impl Sgn { + /// Returns `true` if positive. + pub fn is_pos(&self) -> bool { + matches!(self, Sgn::Pos) + } + + /// Returns `true` if negative. + pub fn is_neg(&self) -> bool { + matches!(self, Sgn::Neg) + } + + /// Returns the negated sign. + pub fn negate(&self) -> Self { + match self { + Sgn::Pos => Sgn::Neg, + Sgn::Neg => Sgn::Pos, + } + } + + /// Converts to a field element (+1 or -1). + pub fn to_field(&self) -> F { + match self { + Sgn::Pos => F::one(), + Sgn::Neg => F::one().neg(), + } + } +} + +/// Trait for numeric magnitude types. +/// +/// This trait provides arithmetic operations for currency types, +/// including field conversion methods for zero-knowledge proof operations. +pub trait Magnitude: Sized + PartialOrd + Copy { + /// Number of bits in this type. + const NBITS: usize; + + /// Returns the absolute difference between two values. + 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. + fn checked_add(&self, rhs: &Self) -> Option; + + /// Checked multiplication. + fn checked_mul(&self, rhs: &Self) -> Option; + + /// Checked subtraction. + fn checked_sub(&self, rhs: &Self) -> Option; + + /// Checked division. + fn checked_div(&self, rhs: &Self) -> Option; + + /// Checked remainder. + 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; + + /// Add with overflow flag. + fn add_flagged(&self, rhs: &Self) -> (Self, bool) { + let z = self.wrapping_add(rhs); + (z, z < *self) + } + + /// Subtract with underflow flag. + fn sub_flagged(&self, rhs: &Self) -> (Self, bool) { + (self.wrapping_sub(rhs), self < rhs) + } + + /// Converts to a field element. + fn to_field(&self) -> F; + + /// Creates from a field element. + fn of_field(field: F) -> Self; +} + +/// Trait for types with minimum and maximum values. +/// +/// Used for defining valid ranges in preconditions. +pub trait MinMax { + /// Returns the minimum value. + fn min() -> Self; + /// Returns the maximum value. + fn max() -> Self; +} + +/// Trait for converting currency types to their checked (circuit) representation. +/// +/// This trait enables conversion from unchecked currency types (like [`Amount`], +/// [`Fee`], [`Balance`]) to their checked equivalents used in zero-knowledge +/// proof circuits. +/// +/// The checked types are parameterized by a field type `F` that represents the +/// field used in the proof system (typically `Fp` for the Pallas curve). +/// +/// # Type Parameters +/// +/// - `F`: The field type used in the proof circuit (must implement `FieldLike`) +/// - `Checked`: The checked type returned by the conversion +/// +/// # Example +/// +/// ```ignore +/// use mina_tx_type::currency::{Amount, ToChecked}; +/// +/// let amount = Amount::of_mina(10).unwrap(); +/// let checked: CheckedAmount = amount.to_checked(); +/// ``` +pub trait ToChecked { + /// The checked type that this converts to. + type Checked; + + /// Converts to the checked representation for use in proof circuits. + fn to_checked(&self) -> Self::Checked; +} + +/// A signed value with magnitude and sign. +/// +/// Used to represent balance changes in transactions where the value +/// can be positive (receiving) or negative (sending). +/// +/// # References +/// +/// OCaml: `src/lib/currency/currency.ml` (Signed) +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, 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, +{ + /// Number of bits in the magnitude type. + pub const NBITS: usize = T::NBITS; + + /// Creates a signed value, normalizing zero to positive. + pub fn create(magnitude: T, sgn: Sgn) -> Self { + Self { + magnitude, + sgn: if magnitude.is_zero() { Sgn::Pos } else { sgn }, + } + } + + /// Creates a positive (unsigned) value. + 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 positive. + pub fn is_pos(&self) -> bool { + matches!(self.sgn, Sgn::Pos) + } + + /// Returns `true` if non-negative (positive or zero). + pub fn is_non_neg(&self) -> bool { + matches!(self.sgn, Sgn::Pos) + } + + /// Returns `true` if 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() + } + + /// Adds two signed values, returning `None` on overflow. + 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)) + } + + /// Adds two signed values with overflow flag. + 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 Default for Signed { + fn default() -> Self { + Self { + magnitude: T::zero(), + sgn: Sgn::Pos, + } + } +} + +// ============================================================================ +// Macro for generating numeric types +// ============================================================================ + +/// Macro for generating numeric currency types. +macro_rules! impl_number { + (32: { $($name32:ident,)* }, 64: { $($name64:ident,)* },) => { + $(impl_number!({$name32, u32, as_u32, from_u32},);)* + $(impl_number!({$name64, u64, as_u64, from_u64},);)* + }; + ($({ $name:ident, $inner:ty, $as_name:ident, $from_name:ident },)*) => ($( + #[doc = concat!("A ", stringify!($name), " value.")] + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] + pub struct $name(pub $inner); + + impl core::fmt::Debug for $name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}({:?})", stringify!($name), self.0) + } + } + + impl Magnitude for $name { + const NBITS: usize = <$inner>::BITS as usize; + + 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)) + } + + fn to_field(&self) -> F { + let int = self.0 as u64; + F::from(int) + } + + fn of_field(field: F) -> Self { + let bigint: BigInteger256 = field.into(); + let value: $inner = bigint.0[0].try_into().unwrap(); + Self(value) + } + } + + impl MinMax for $name { + fn min() -> Self { Self(0) } + fn max() -> Self { Self(<$inner>::MAX) } + } + + impl $name { + /// Number of bits in this type. + pub const NBITS: usize = <$inner>::BITS as usize; + + /// Returns the inner value. + #[inline] + pub fn as_inner(&self) -> $inner { + self.0 + } + + /// Creates from the inner type. + #[inline] + pub const fn from_inner(value: $inner) -> Self { + Self(value) + } + + #[doc = concat!("Returns the value as ", stringify!($inner), ".")] + #[inline] + pub fn $as_name(&self) -> $inner { + self.0 + } + + #[doc = concat!("Creates from a ", stringify!($inner), " value.")] + #[inline] + pub const fn $from_name(value: $inner) -> Self { + Self(value) + } + + /// Scales by a factor, 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() + } + + /// Parses from a MINA string format (e.g., "1.5" for 1.5 MINA). + /// + /// # Panics + /// + /// Panics if the input is not a valid currency string. + 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) + } + + /// Converts to bit array representation. + pub fn to_bits(&self) -> [bool; <$inner>::BITS as usize] { + let value = self.0; + let mut bits = [false; <$inner>::BITS as usize]; + for i in 0..<$inner>::BITS as usize { + bits[i] = (value >> i) & 1 == 1; + } + bits + } + } + + impl From<$inner> for $name { + fn from(value: $inner) -> Self { + Self(value) + } + } + + impl From<$name> for $inner { + fn from(value: $name) -> Self { + value.0 + } + } + )*) +} + +impl_number!( + 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, Epoch, }, + 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, }, +); + +// ============================================================================ +// Type-specific implementations +// ============================================================================ + +impl Amount { + /// The number of nanounits in a unit (1 MINA = 10^9 nanomina). + pub const UNIT_TO_NANO: u64 = 1_000_000_000; + + /// Creates from a fee. + pub fn of_fee(fee: &Fee) -> Self { + Self(fee.0) + } + + /// Add a signed amount with 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. + pub fn to_nanomina(&self) -> u64 { + self.0 + } + + /// Converts to MINA (integer part only). + pub fn to_mina(&self) -> u64 { + self.0 / Self::UNIT_TO_NANO + } + + /// Creates from MINA amount. + pub fn of_mina(mina: u64) -> Option { + mina.checked_mul(Self::UNIT_TO_NANO).map(Self) + } + + /// Creates from nanomina amount. + pub fn of_nanomina(nanomina: u64) -> Self { + Self(nanomina) + } + + /// Returns self (for compatibility). + pub fn to_nanomina_int(&self) -> Self { + *self + } + + /// Converts to MINA units (integer division). + pub fn to_mina_int(&self) -> Self { + Self(self.0.checked_div(Self::UNIT_TO_NANO).unwrap()) + } + + /// Creates from MINA int (panics on overflow). + pub fn of_mina_int_exn(int: u64) -> Self { + Self::from_u64(int).scale(Self::UNIT_TO_NANO).unwrap() + } + + /// Creates from nanomina int. + pub fn of_nanomina_int_exn(int: u64) -> Self { + Self::from_u64(int) + } +} + +impl Balance { + /// Subtracts an amount, returning `None` on underflow. + pub fn sub_amount(&self, amount: Amount) -> Option { + self.0.checked_sub(amount.0).map(Self) + } + + /// Adds an amount, returning `None` on overflow. + pub fn add_amount(&self, amount: Amount) -> Option { + self.0.checked_add(amount.0).map(Self) + } + + /// Adds a signed balance with 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) + } + } + + /// Adds a signed amount with overflow flag. + pub fn add_signed_amount_flagged(&self, rhs: Signed) -> (Self, bool) { + let rhs = Signed { + magnitude: Balance::from_u64(rhs.magnitude.0), + sgn: rhs.sgn, + }; + + if let Sgn::Pos = rhs.sgn { + self.add_flagged(&rhs.magnitude) + } else { + self.sub_flagged(&rhs.magnitude) + } + } + + /// Converts to amount. + pub fn to_amount(self) -> Amount { + Amount(self.0) + } + + /// Creates from MINA amount. + pub fn from_mina(mina: u64) -> Option { + mina.checked_mul(Amount::UNIT_TO_NANO).map(Self) + } + + /// Creates from nanomina amount. + pub fn of_nanomina(nanomina: u64) -> Self { + Self(nanomina) + } + + /// Creates from nanomina int (for compatibility). + pub fn of_nanomina_int_exn(int: u64) -> Self { + Self::from_u64(int) + } +} + +impl Fee { + /// Creates from nanomina amount. + pub const fn of_nanomina(nanomina: u64) -> Self { + Self(nanomina) + } + + /// Creates from nanomina int (for compatibility). + pub const fn of_nanomina_int_exn(int: u64) -> Self { + Self(int) + } + + /// Returns the fee in nanomina. + pub fn to_nanomina(&self) -> u64 { + self.0 + } +} + +impl Index { + /// Increments the index. + pub fn incr(&self) -> Self { + Self(self.0.wrapping_add(1)) + } +} + +impl Nonce { + /// Increments the nonce. + pub fn incr(&self) -> Self { + Self(self.0.wrapping_add(1)) + } + + /// Returns the successor nonce. + pub fn succ(&self) -> Self { + self.incr() + } + + /// Add a signed nonce with 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 `true` if `low <= self <= high`. + pub fn between(&self, low: &Self, high: &Self) -> bool { + low <= self && self <= high + } +} + +impl Slot { + /// Increments the slot. + pub fn incr(&self) -> Self { + Self(self.0.wrapping_add(1)) + } + + /// Returns the successor slot. + pub fn succ(&self) -> Self { + self.incr() + } + + /// Adds a slot span. + pub fn add(&self, span: SlotSpan) -> Self { + Self(self.0.checked_add(span.0).unwrap()) + } +} + +impl SlotSpan { + /// Creates from a number of slots. + pub const fn of_slots(slots: u32) -> Self { + Self(slots) + } + + /// Returns the number of slots. + pub fn to_slots(&self) -> u32 { + self.0 + } +} + +impl Length { + /// Increments the length. + pub fn incr(&self) -> Self { + Self(self.0.wrapping_add(1)) + } + + /// Returns the successor length. + pub fn succ(&self) -> Self { + self.incr() + } +} + +impl BlockTime { + /// Adds a time span. + pub fn add(&self, span: BlockTimeSpan) -> Self { + Self(self.0.checked_add(span.0).unwrap()) + } + + /// Subtracts a time span. + pub fn sub(&self, span: BlockTimeSpan) -> Self { + Self(self.0.checked_sub(span.0).unwrap()) + } + + /// Returns the difference as a time span. + pub fn diff(&self, other: Self) -> BlockTimeSpan { + BlockTimeSpan(self.0 - other.0) + } + + /// Converts to milliseconds since epoch. + pub fn to_ms(&self) -> u64 { + self.0 + } + + /// Creates from milliseconds since epoch. + pub fn of_ms(ms: u64) -> Self { + Self(ms) + } + + /// Converts to span since epoch. + pub fn to_span_since_epoch(&self) -> BlockTimeSpan { + BlockTimeSpan(self.0) + } + + /// Creates from span since epoch. + pub fn of_span_since_epoch(span: BlockTimeSpan) -> Self { + Self(span.0) + } +} + +impl BlockTimeSpan { + /// Creates from milliseconds. + pub fn of_ms(ms: u64) -> Self { + Self(ms) + } + + /// Returns the span in milliseconds. + pub fn to_ms(&self) -> u64 { + self.0 + } +} + +impl Signed { + /// Converts to a signed fee. + pub fn to_fee(self) -> Signed { + Signed { + magnitude: Fee(self.magnitude.0), + sgn: self.sgn, + } + } +} + +// ============================================================================ +// Conversions with mina-p2p-messages types +// ============================================================================ + +use mina_p2p_messages::v2::{ + BlockTimeTimeStableV1, CurrencyAmountStableV1, CurrencyBalanceStableV1, CurrencyFeeStableV1, + MinaNumbersGlobalSlotSinceGenesisMStableV1, MinaNumbersGlobalSlotSinceHardForkMStableV1, + MinaNumbersGlobalSlotSpanStableV1, MinaStateBlockchainStateValueStableV2SignedAmount, + SgnStableV1, SignedAmount, UnsignedExtendedUInt32StableV1, + UnsignedExtendedUInt64Int64ForVersionTagsStableV1, +}; + +impl From for Amount { + fn from(value: CurrencyAmountStableV1) -> Self { + Self(value.as_u64()) + } +} + +impl From for Balance { + 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<&Balance> for CurrencyBalanceStableV1 { + fn from(value: &Balance) -> Self { + Self((*value).into()) + } +} + +impl From for CurrencyAmountStableV1 { + fn from(value: Balance) -> Self { + Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( + value.as_u64().into(), + )) + } +} + +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()) + } +} + +impl From<&Nonce> for UnsignedExtendedUInt32StableV1 { + fn from(value: &Nonce) -> Self { + Self(value.as_u32().into()) + } +} + +impl From<&UnsignedExtendedUInt32StableV1> for Nonce { + fn from(value: &UnsignedExtendedUInt32StableV1) -> Self { + Self::from_u32(value.as_u32()) + } +} + +impl From<&UnsignedExtendedUInt32StableV1> for Slot { + fn from(value: &UnsignedExtendedUInt32StableV1) -> Self { + Self::from_u32(value.as_u32()) + } +} + +impl From<&Slot> for UnsignedExtendedUInt32StableV1 { + fn from(value: &Slot) -> Self { + Self(value.as_u32().into()) + } +} + +impl From<&UnsignedExtendedUInt32StableV1> for Length { + fn from(value: &UnsignedExtendedUInt32StableV1) -> Self { + Self::from_u32(value.0.as_u32()) + } +} + +impl From<&Length> for UnsignedExtendedUInt32StableV1 { + fn from(value: &Length) -> Self { + Self(value.as_u32().into()) + } +} + +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(), + } + } +} + +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(), + } + } +} + +impl From<&BlockTime> for BlockTimeTimeStableV1 { + fn from(value: &BlockTime) -> Self { + Self(UnsignedExtendedUInt64Int64ForVersionTagsStableV1( + value.as_u64().into(), + )) + } +} + +impl From<&MinaNumbersGlobalSlotSinceGenesisMStableV1> for Slot { + fn from(value: &MinaNumbersGlobalSlotSinceGenesisMStableV1) -> Self { + let MinaNumbersGlobalSlotSinceGenesisMStableV1::SinceGenesis(slot) = value; + Self(slot.as_u32()) + } +} + +impl From<&MinaNumbersGlobalSlotSinceHardForkMStableV1> for Slot { + fn from(value: &MinaNumbersGlobalSlotSinceHardForkMStableV1) -> Self { + let MinaNumbersGlobalSlotSinceHardForkMStableV1::SinceHardFork(slot) = value; + Self(slot.as_u32()) + } +} + +impl From<&MinaNumbersGlobalSlotSpanStableV1> for SlotSpan { + fn from(value: &MinaNumbersGlobalSlotSpanStableV1) -> Self { + let MinaNumbersGlobalSlotSpanStableV1::GlobalSlotSpan(slot) = value; + Self(slot.as_u32()) + } +} + +impl From<&Slot> for MinaNumbersGlobalSlotSinceGenesisMStableV1 { + fn from(value: &Slot) -> Self { + Self::SinceGenesis(value.as_u32().into()) + } +} + +impl From<&Slot> for MinaNumbersGlobalSlotSinceHardForkMStableV1 { + fn from(value: &Slot) -> Self { + Self::SinceHardFork(value.as_u32().into()) + } +} + +impl From<&SlotSpan> for MinaNumbersGlobalSlotSpanStableV1 { + fn from(value: &SlotSpan) -> Self { + Self::GlobalSlotSpan(value.as_u32().into()) + } +} + +impl From for BlockTime { + fn from(bt: BlockTimeTimeStableV1) -> Self { + BlockTime::from_u64(bt.0 .0 .0) + } +} + +// ============================================================================ +// Generic number type N +// ============================================================================ + +/// A generic 64-bit number type for proof computations. +/// +/// This type is used in various proof-related computations where a generic +/// 64-bit unsigned integer is needed. Unlike the specialized currency types, +/// `N` doesn't represent any specific unit. +#[derive( + Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, +)] +pub struct N(pub(crate) u64); + +impl std::fmt::Debug for N { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("N({:?})", self.0)) + } +} + +impl Magnitude for N { + const NBITS: usize = 64; + + 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)) + } + + fn to_field(&self) -> F { + F::from(self.0) + } + + fn of_field(field: F) -> Self { + use ark_ff::BigInteger256; + let bigint: BigInteger256 = field.into(); + Self(bigint.0[0]) + } +} + +impl MinMax for N { + fn min() -> Self { + Self(0) + } + fn max() -> Self { + Self(u64::MAX) + } +} + +impl N { + /// Number of bits in this type. + pub const NBITS: usize = 64; + + /// Returns the inner value as u64. + pub fn as_u64(&self) -> u64 { + self.0 + } + + /// Creates from a u64 value. + pub const fn from_u64(value: u64) -> Self { + Self(value) + } + + /// Multiplies by a scalar, returning None on overflow. + pub const fn scale(&self, n: u64) -> Option { + match self.0.checked_mul(n) { + Some(n) => Some(Self(n)), + None => None, + } + } +} + +impl rand::distributions::Distribution for rand::distributions::Standard { + fn sample(&self, rng: &mut R) -> N { + N(rng.next_u64()) + } +} + +// ============================================================================ +// Random generation implementations +// ============================================================================ + +/// Extension trait for random generation of [`Signed`] values. +/// +/// This trait provides a convenient method for generating random signed values +/// for testing purposes. +pub trait SignedRandExt { + /// Generates a random signed value. + fn gen() -> Signed; +} + +impl SignedRandExt for Signed +where + T: Magnitude + PartialOrd + Ord + Clone, + rand::distributions::Standard: rand::distributions::Distribution, +{ + fn gen() -> Signed { + use rand::Rng; + let mut rng = rand::thread_rng(); + + let magnitude: T = rng.gen(); + let sgn = if rng.gen::() { + Sgn::Pos + } else { + Sgn::Neg + }; + + Signed::create(magnitude, sgn) + } +} + +/// Extension trait for generating small random [`Slot`] values. +/// +/// This trait provides a method for generating random slot values within +/// a small range, useful for testing scenarios. +pub trait SlotRandExt { + /// Generates a random slot value in the range [0, 10000). + fn gen_small() -> Slot; +} + +impl SlotRandExt for Slot { + fn gen_small() -> Slot { + use rand::Rng; + let mut rng = rand::thread_rng(); + Slot::from_u32(rng.gen::() % 10_000) + } +} + +macro_rules! impl_rand_distribution { + (32: { $($name32:ident,)* }, 64: { $($name64:ident,)* },) => { + $( + impl rand::distributions::Distribution<$name32> for rand::distributions::Standard { + fn sample(&self, rng: &mut R) -> $name32 { + $name32::from_u32(rng.next_u32()) + } + } + )* + $( + impl rand::distributions::Distribution<$name64> for rand::distributions::Standard { + fn sample(&self, rng: &mut R) -> $name64 { + $name64::from_u64(rng.next_u64()) + } + } + )* + }; +} + +impl_rand_distribution!( + 32: { Length, Slot, Nonce, Index, SlotSpan, TxnVersion, Epoch, }, + 64: { Amount, Balance, Fee, BlockTime, BlockTimeSpan, }, +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_amount_mina_conversion() { + let amount = Amount::of_mina(1).unwrap(); + assert_eq!(amount.0, 1_000_000_000); + assert_eq!(amount.to_mina(), 1); + } + + #[test] + fn test_signed_add() { + let a = Signed::of_unsigned(Amount(100)); + let b = Signed::create(Amount(30), Sgn::Neg); + let result = a.add(&b).unwrap(); + assert_eq!(result.magnitude.0, 70); + assert!(result.is_pos()); + } + + #[test] + fn test_nonce_incr() { + let n = Nonce(5); + assert_eq!(n.incr().0, 6); + } + + #[test] + fn test_balance_arithmetic() { + let balance = Balance(1000); + let new_balance = balance.add_amount(Amount(500)).unwrap(); + assert_eq!(new_balance.0, 1500); + + let reduced = new_balance.sub_amount(Amount(200)).unwrap(); + assert_eq!(reduced.0, 1300); + } + + #[test] + fn test_to_bits() { + let n = Nonce(5); + let bits = n.to_bits(); + assert!(bits[0]); // 1 + assert!(!bits[1]); // 0 + assert!(bits[2]); // 1 + for bit in &bits[3..32] { + assert!(!bit); + } + } +} diff --git a/mina-tx-type/src/lib.rs b/mina-tx-type/src/lib.rs new file mode 100644 index 0000000000..45824b6dfe --- /dev/null +++ b/mina-tx-type/src/lib.rs @@ -0,0 +1,43 @@ +//! 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 two primary user-initiated transaction types: +//! +//! - **Signed Commands**: Traditional payments and stake delegations authorized +//! by a cryptographic signature. +//! - **zkApp Commands**: Complex multi-account operations using zero-knowledge +//! proofs for authorization and state updates. +//! +//! # zkApp Transaction Structure +//! +//! A zkApp command consists of: +//! - A **fee payer** account that pays the transaction fee +//! - A tree of **account updates** that modify account state +//! - A **memo** field for user-defined metadata +//! +//! Each account update can specify: +//! - State updates (app state, delegate, permissions, etc.) +//! - Balance changes +//! - Preconditions that must be satisfied +//! - Authorization method (signature, proof, or none) +//! +//! # Documentation References +//! +//! For detailed information about zkApp transaction signing, see: +//! - TODO: Issue #1748 - zkApp transaction signing documentation +//! - + +pub mod currency; +pub mod zkapp; + +pub use currency::*; +pub use zkapp::*; + +// Re-export the field type for convenience +pub use mina_curves::pasta::Fp; +pub use mina_signer::{CompressedPubKey, Signature}; diff --git a/mina-tx-type/src/zkapp.rs b/mina-tx-type/src/zkapp.rs new file mode 100644 index 0000000000..6a78196590 --- /dev/null +++ b/mina-tx-type/src/zkapp.rs @@ -0,0 +1,904 @@ +//! zkApp command types +//! +//! This module defines the data structures for zkApp commands, which are +//! complex multi-account transactions that use zero-knowledge proofs for +//! authorization. +//! +//! # Structure Overview +//! +//! A [`ZkAppCommand`] contains: +//! - [`FeePayer`]: The account paying transaction fees +//! - [`CallForest`]: A tree of account updates +//! - [`Memo`]: User-defined transaction metadata +//! +//! # Authorization +//! +//! zkApp commands support three authorization methods: +//! - **Proof**: Verified against the account's verification key +//! - **Signature**: Signed by the account's private key +//! - **None**: No authorization (for certain permitted operations) +//! +//! # Preconditions +//! +//! Account updates can specify preconditions that must be satisfied: +//! - Network state (blockchain length, global slot, etc.) +//! - Account state (balance, nonce, app state, etc.) +//! - Time validity windows + +use crate::currency::{Amount, Balance, Fee, Length, Nonce, Signed, Slot, SlotSpan}; +use mina_curves::pasta::Fp; +use mina_signer::{CompressedPubKey, Signature}; + +/// A zkApp command representing a complex multi-account transaction. +/// +/// zkApp commands enable sophisticated transaction logic through zero-knowledge +/// proofs. Unlike signed commands, they can update multiple accounts atomically +/// and enforce complex preconditions. +/// +/// # Fields +/// +/// - `fee_payer`: The account responsible for paying the transaction fee. This +/// account must authorize the fee payment with a signature. +/// - `account_updates`: A forest (tree) of account updates to apply. Updates +/// are processed in a specific order and can have parent-child relationships. +/// - `memo`: Optional user-defined metadata (up to 32 bytes of user data). +/// +/// # Example Structure +/// +/// ```text +/// ZkAppCommand +/// +-- fee_payer: FeePayer (pays fees, always signed) +/// +-- account_updates: CallForest +/// | +-- AccountUpdate (can be proof/signature/none authorized) +/// | | +-- children: CallForest (nested updates) +/// | +-- AccountUpdate +/// | +-- children: CallForest +/// +-- memo: "user memo" +/// ``` +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_command.ml` +#[derive(Debug, Clone, PartialEq)] +pub struct ZkAppCommand { + /// The account paying the transaction fee. + /// + /// The fee payer is always authorized by a signature and pays for all + /// computation and storage costs of the transaction. The fee payer's + /// nonce is incremented to prevent replay attacks. + pub fee_payer: FeePayer, + + /// A tree of account updates to apply atomically. + /// + /// Account updates are organized in a forest structure where updates can + /// have child updates. This enables complex transaction patterns like + /// token transfers that require updates to multiple accounts. + pub account_updates: CallForest, + + /// User-defined transaction memo. + /// + /// A 34-byte field where the first 2 bytes encode the length and format, + /// and up to 32 bytes contain user data. Commonly used for transaction + /// descriptions or external reference IDs. + pub memo: Memo, +} + +/// The fee payer for a zkApp command. +/// +/// The fee payer is a special account update that: +/// - Always uses the default token (MINA) +/// - Must be authorized by a signature +/// - Pays the transaction fee +/// - Has its nonce incremented +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (FeePayer module) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FeePayer { + /// The fee payer's account details and fee information. + pub body: FeePayerBody, + + /// The signature authorizing the fee payment. + /// + /// This signature covers the entire zkApp command, including all account + /// updates, ensuring the fee payer consents to the full transaction. + pub authorization: Signature, +} + +/// The body of a fee payer, containing account and fee details. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (FeePayerBody) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FeePayerBody { + /// The public key of the fee payer account. + /// + /// This identifies which account will pay the transaction fee and have + /// its nonce incremented. + pub public_key: CompressedPubKey, + + /// The fee to pay for the transaction. + /// + /// The fee compensates block producers for including the transaction and + /// must be sufficient for the transaction's computational cost. + pub fee: Fee, + + /// Optional slot until which the transaction is valid. + /// + /// If `Some(slot)`, the transaction will fail if applied after this slot. + /// If `None`, the transaction has no expiration. + pub valid_until: Option, + + /// The fee payer's account nonce. + /// + /// Must match the current nonce of the fee payer's account. The nonce is + /// incremented after the transaction is applied to prevent replay attacks. + pub nonce: Nonce, +} + +/// A forest of account updates with cryptographic commitments. +/// +/// The call forest represents a tree structure where each account update can +/// have child updates. This enables complex transaction patterns where one +/// account's update depends on or triggers updates to other accounts. +/// +/// # Structure +/// +/// Each node in the forest contains: +/// - An account update +/// - A cryptographic hash for efficient commitment +/// - Potentially nested child updates +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_command.ml` (CallForest) +#[derive(Debug, Clone, PartialEq)] +pub struct CallForest(pub Vec>); + +impl Default for CallForest { + fn default() -> Self { + Self(Vec::new()) + } +} + +/// An account update wrapped with its stack hash for cryptographic commitment. +/// +/// The stack hash enables efficient verification of the account update tree +/// without examining every node. +#[derive(Debug, Clone, PartialEq)] +pub struct WithStackHash { + /// The account update and its children. + pub elt: Tree, + + /// The cryptographic hash of this subtree. + /// + /// This hash commits to this account update and all its descendants, + /// enabling efficient Merkle-style proofs. + pub stack_hash: Fp, +} + +/// A tree node containing an account update and its children. +#[derive(Debug, Clone, PartialEq)] +pub struct Tree { + /// The account update at this node. + pub account_update: AccUpdate, + + /// The stack hash of just this account update (without children). + pub account_update_digest: Fp, + + /// Child account updates that depend on this update. + pub calls: CallForest, +} + +/// An account update specifying changes to an account's state. +/// +/// Account updates are the core building blocks of zkApp commands. Each update +/// specifies: +/// - Which account to modify (by public key and token) +/// - What changes to make (balance, app state, permissions, etc.) +/// - What preconditions must be satisfied +/// - How the update is authorized +/// +/// # Authorization +/// +/// Updates can be authorized in three ways: +/// - **Proof**: A zero-knowledge proof verified against the account's +/// verification key +/// - **Signature**: A cryptographic signature from the account's private key +/// - **None**: No authorization (only valid for certain operations) +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` +#[derive(Debug, Clone, PartialEq)] +pub struct AccountUpdate { + /// The body containing all update details and preconditions. + pub body: AccountUpdateBody, + + /// The authorization for this update. + /// + /// Must match the `authorization_kind` specified in the body. + pub authorization: Control, +} + +/// The body of an account update containing all modification details. +/// +/// This structure specifies exactly what changes should be made to an account +/// and under what conditions those changes are permitted. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (Body) +#[derive(Debug, Clone, PartialEq)] +pub struct AccountUpdateBody { + /// The public key of the account to update. + pub public_key: CompressedPubKey, + + /// The token ID for this account. + /// + /// Combined with the public key, this uniquely identifies an account. + /// The default token ID represents MINA. + pub token_id: TokenId, + + /// The updates to apply to the account's state. + /// + /// Each field can be `Set` to a new value or `Keep` the existing value. + pub update: Update, + + /// The change to the account's balance. + /// + /// Positive values increase the balance (receiving), negative values + /// decrease it (sending). The magnitude and sign are specified separately. + pub balance_change: Signed, + + /// Whether to increment the account's nonce. + /// + /// Should be `true` for updates that should only be applied once, + /// preventing replay attacks. + pub increment_nonce: bool, + + /// Events emitted by this account update. + /// + /// Events are arbitrary field elements that are included in the + /// transaction but don't affect account state. They can be used for + /// off-chain indexing and logging. + pub events: Events, + + /// Actions (sequenced events) emitted by this account update. + /// + /// Unlike events, actions are accumulated in the account's action state + /// and can be processed by subsequent zkApp transactions. + pub actions: Actions, + + /// Arbitrary field element for zkApp-specific data. + /// + /// This field is included in the account update hash and can be used + /// to pass data to the zkApp's verification logic. + pub call_data: Fp, + + /// Preconditions that must be satisfied for this update to succeed. + /// + /// Includes network state preconditions (blockchain length, slot, etc.) + /// and account state preconditions (balance, nonce, app state, etc.). + pub preconditions: Preconditions, + + /// Whether to use the full transaction commitment for signing. + /// + /// When `true`, signatures cover all account updates in the command. + /// When `false`, signatures only cover this update and its descendants. + pub use_full_commitment: bool, + + /// Whether to implicitly pay the account creation fee. + /// + /// When `true` and this update creates a new account, the creation fee + /// is automatically deducted from the balance change. + pub implicit_account_creation_fee: bool, + + /// Token permission inheritance configuration. + /// + /// Controls whether this update can use custom tokens and under what + /// conditions. + pub may_use_token: MayUseToken, + + /// The kind of authorization used for this update. + /// + /// Must be consistent with the `authorization` field in the parent + /// `AccountUpdate` structure. + pub authorization_kind: AuthorizationKind, +} + +/// Updates to apply to an account's state. +/// +/// Each field uses [`SetOrKeep`] to either set a new value or keep the +/// existing value unchanged. This allows partial updates where only specific +/// fields are modified. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (Update) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Update { + /// Updates to the 8 app state field elements. + /// + /// zkApp accounts have 8 field elements of arbitrary state that can be + /// used by the zkApp's smart contract logic. + pub app_state: [SetOrKeep; 8], + + /// Update to the account's delegate. + /// + /// The delegate receives staking rewards on behalf of this account. + /// Only applicable for accounts using the default MINA token. + pub delegate: SetOrKeep, + + /// Update to the account's verification key. + /// + /// The verification key is used to verify zero-knowledge proofs for + /// account updates authorized by proof. + pub verification_key: SetOrKeep, + + /// Update to the account's permissions. + /// + /// Permissions control which operations require which types of + /// authorization (proof, signature, or impossible). + pub permissions: SetOrKeep, + + /// Update to the zkApp URI. + /// + /// A URI pointing to off-chain resources related to this zkApp, + /// such as documentation or a web interface. + pub zkapp_uri: SetOrKeep, + + /// Update to the token symbol. + /// + /// A human-readable symbol for custom tokens (e.g., "USDC", "WETH"). + pub token_symbol: SetOrKeep, + + /// Update to the account's timing (vesting schedule). + /// + /// Timing controls when tokens become available for spending, + /// used for vesting schedules and time-locked tokens. + pub timing: SetOrKeep, + + /// Update to the voting-for field. + /// + /// Indicates which proposal or election this account is voting for + /// in on-chain governance. + pub voting_for: SetOrKeep, +} + +/// A value that can be set to a new value or kept unchanged. +/// +/// Used throughout account updates to allow partial modifications where +/// only specific fields are changed. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_basic.ml` (SetOrKeep) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SetOrKeep { + /// Set the field to a new value. + Set(T), + + /// Keep the existing value unchanged. + Keep, +} + +impl SetOrKeep { + /// Returns `true` if this is `Keep`. + pub fn is_keep(&self) -> bool { + matches!(self, Self::Keep) + } + + /// Returns `true` if this is `Set`. + pub fn is_set(&self) -> bool { + !self.is_keep() + } +} + +impl SetOrKeep { + /// Returns the set value or the provided default. + pub fn set_or_keep(&self, default: T) -> T { + match self { + Self::Set(v) => v.clone(), + Self::Keep => default, + } + } +} + +/// Preconditions that must be satisfied for an account update to succeed. +/// +/// Preconditions enable conditional transaction execution, where updates +/// only apply if the blockchain and account state match expected values. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (Preconditions) +#[derive(Debug, Clone, PartialEq)] +pub struct Preconditions { + /// Network state preconditions (blockchain length, slot, etc.). + pub network: NetworkPreconditions, + + /// Account state preconditions (balance, nonce, app state, etc.). + pub account: AccountPreconditions, + + /// Slot range during which this update is valid. + /// + /// The update will fail if applied outside this slot range. + pub valid_while: Numeric, +} + +/// Network state preconditions. +/// +/// These preconditions check the global blockchain state at the time +/// the transaction is applied. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (Protocol_state) +#[derive(Debug, Clone, PartialEq)] +pub struct NetworkPreconditions { + /// Expected hash of the snarked ledger. + pub snarked_ledger_hash: OrIgnore, + + /// Expected blockchain length (block height). + pub blockchain_length: Numeric, + + /// Expected minimum window density. + pub min_window_density: Numeric, + + /// Expected total currency in circulation. + pub total_currency: Numeric, + + /// Expected global slot since genesis. + pub global_slot_since_genesis: Numeric, + + /// Preconditions on the current staking epoch. + pub staking_epoch_data: EpochData, + + /// Preconditions on the next staking epoch. + pub next_epoch_data: EpochData, +} + +/// Epoch data preconditions. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (EpochData) +#[derive(Debug, Clone, PartialEq)] +pub struct EpochData { + /// Expected epoch ledger hash. + pub ledger: EpochLedger, + + /// Expected epoch seed. + pub seed: OrIgnore, + + /// Expected start checkpoint. + pub start_checkpoint: OrIgnore, + + /// Expected lock checkpoint. + pub lock_checkpoint: OrIgnore, + + /// Expected epoch length. + pub epoch_length: Numeric, +} + +/// Epoch ledger preconditions. +#[derive(Debug, Clone, PartialEq)] +pub struct EpochLedger { + /// Expected ledger hash. + pub hash: OrIgnore, + + /// Expected total currency in the epoch ledger. + pub total_currency: Numeric, +} + +/// Account state preconditions. +/// +/// These preconditions check the state of the target account at the time +/// the update is applied. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (Account) +#[derive(Debug, Clone, PartialEq)] +pub struct AccountPreconditions { + /// Expected account balance range. + pub balance: Numeric, + + /// Expected account nonce range. + pub nonce: Numeric, + + /// Expected receipt chain hash. + pub receipt_chain_hash: OrIgnore, + + /// Expected delegate public key. + pub delegate: OrIgnore, + + /// Expected app state values (8 field elements). + pub state: [OrIgnore; 8], + + /// Expected action state (for sequenced events). + pub action_state: OrIgnore, + + /// Expected proved state flag. + pub proved_state: OrIgnore, + + /// Expected "is new account" flag. + pub is_new: OrIgnore, +} + +/// A precondition that either checks a value or ignores it. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_basic.ml` (OrIgnore) +#[derive(Debug, Clone, PartialEq)] +pub enum OrIgnore { + /// Check that the value matches. + Check(T), + + /// Ignore this precondition (always passes). + Ignore, +} + +/// A numeric precondition specifying a valid range. +/// +/// The precondition passes if the actual value falls within +/// `[lower, upper]` (inclusive). +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (Numeric) +pub type Numeric = OrIgnore>; + +/// A closed interval `[lower, upper]`. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (ClosedInterval) +#[derive(Debug, Clone, PartialEq)] +pub struct ClosedInterval { + /// The lower bound (inclusive). + pub lower: T, + + /// The upper bound (inclusive). + pub upper: T, +} + +/// The kind of authorization used for an account update. +/// +/// This must match the actual authorization provided in the `AccountUpdate`. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (AuthorizationKind) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuthorizationKind { + /// No authorization provided. + /// + /// Only valid for operations that don't require authorization + /// based on the account's permissions. + NoneGiven, + + /// Authorized by a cryptographic signature. + Signature, + + /// Authorized by a zero-knowledge proof. + /// + /// The field element is the hash of the verification key that + /// will verify the proof. + Proof(Fp), +} + +/// Token permission configuration for account updates. +/// +/// Controls whether and how an account update can interact with custom tokens. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (MayUseToken) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MayUseToken { + /// Cannot use custom tokens. + No, + + /// Can inherit token permissions from parent updates. + ParentsOwnToken, + + /// Can use tokens from any parent in the update tree. + InheritFromParent, +} + +/// Authorization methods for zkApp account updates. +/// +/// Defines how an account update is authorized to modify an account's state. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/control.ml` +#[derive(Debug, Clone, PartialEq)] +pub enum Control { + /// Verified by a zero-knowledge proof against the account's verification + /// key. + Proof(SideLoadedProof), + + /// Signed by the account's private key. + Signature(Signature), + + /// No authorization (only valid for certain operations). + NoneGiven, +} + +/// A side-loaded proof for zkApp authorization. +/// +/// This is a placeholder type - the actual proof structure is complex +/// and defined in the proof-systems crate. +pub type SideLoadedProof = Vec; + +/// Events emitted by an account update. +/// +/// Events are arbitrary data included in the transaction for off-chain +/// indexing. They don't affect on-chain state. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (Events) +#[derive(Debug, Clone, PartialEq)] +pub struct Events(pub Vec); + +impl Events { + /// Create an empty events list. + pub fn empty() -> Self { + Self(Vec::new()) + } + + /// Check if the events list is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// A single event, consisting of field elements. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (Event) +#[derive(Debug, Clone, PartialEq)] +pub struct Event(pub Vec); + +impl Event { + /// Create an empty event. + pub fn empty() -> Self { + Self(Vec::new()) + } + + /// Get the number of field elements in this event. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if this event is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Actions (sequenced events) emitted by an account update. +/// +/// Unlike events, actions are accumulated in the account's action state +/// and can be processed by subsequent transactions. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/zkapp_account.ml` (Actions) +#[derive(Debug, Clone, PartialEq)] +pub struct Actions(pub Vec); + +impl Actions { + /// Create an empty actions list. + pub fn empty() -> Self { + Self(Vec::new()) + } + + /// Check if the actions list is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Timing information for token vesting schedules. +/// +/// Controls when tokens become available for spending through a +/// cliff and vesting schedule. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/account_update.ml` (Timing) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Timing { + /// The initial minimum balance that must be maintained. + pub initial_minimum_balance: Balance, + + /// The slot at which the cliff occurs. + /// + /// Before this slot, `initial_minimum_balance` tokens are locked. + pub cliff_time: Slot, + + /// The amount released at the cliff. + pub cliff_amount: Amount, + + /// The number of slots between vesting releases. + pub vesting_period: SlotSpan, + + /// The amount released at each vesting period. + pub vesting_increment: Amount, +} + +// ============================================================================ +// Primitive types +// ============================================================================ + +/// Transaction memo field (34 bytes). +/// +/// The memo has a specific format: +/// - Byte 0: Tag byte (0x01 for text, 0x02 for binary) +/// - Byte 1: Length of the payload (0-32) +/// - Bytes 2-33: Payload (user data, padded with zeros) +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/signed_command_memo.ml` +#[derive(Clone, PartialEq, Eq)] +pub struct Memo(pub [u8; 34]); + +impl core::fmt::Debug for Memo { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // Display the memo as a string if it's valid UTF-8 text + if self.0[0] == 0x01 { + let len = self.0[1] as usize; + if len <= 32 { + if let Ok(s) = core::str::from_utf8(&self.0[2..2 + len]) { + return write!(f, "Memo({:?})", s); + } + } + } + write!(f, "Memo({:?})", &self.0[..]) + } +} + +/// Token identifier. +/// +/// The default token ID represents MINA. Custom tokens have unique IDs +/// derived from the token owner's account. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TokenId(pub Fp); + +/// Hash of a verification key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerificationKeyHash(pub Fp); + +/// zkApp URI for off-chain resources. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ZkAppUri(pub Vec); + +/// Human-readable token symbol. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TokenSymbol(pub Vec); + +/// Voting-for field (governance). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VotingFor(pub Fp); + +/// Account permissions controlling operation authorization. +/// +/// Each permission specifies what authorization is required for a +/// particular operation. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/permissions.ml` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Permissions { + /// Permission to edit the account state. + pub edit_state: AuthRequired, + + /// Permission to send tokens. + pub send: AuthRequired, + + /// Permission to receive tokens. + pub receive: AuthRequired, + + /// Permission to access the account. + pub access: AuthRequired, + + /// Permission to set the delegate. + pub set_delegate: AuthRequired, + + /// Permission to set permissions. + pub set_permissions: AuthRequired, + + /// Permission to set the verification key. + pub set_verification_key: SetVerificationKeyPerm, + + /// Permission to set the zkApp URI. + pub set_zkapp_uri: AuthRequired, + + /// Permission to edit action state. + pub edit_action_state: AuthRequired, + + /// Permission to set the token symbol. + pub set_token_symbol: AuthRequired, + + /// Permission to increment the nonce. + pub increment_nonce: AuthRequired, + + /// Permission to set voting-for. + pub set_voting_for: AuthRequired, + + /// Permission to set timing. + pub set_timing: AuthRequired, +} + +/// Permission for setting the verification key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SetVerificationKeyPerm { + /// The authorization required. + pub auth: AuthRequired, + + /// The transaction version this permission applies to. + pub txn_version: u32, +} + +/// Authorization requirement for an operation. +/// +/// # References +/// +/// OCaml: `src/lib/mina_base/permissions.ml` (Auth_required) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AuthRequired { + /// No authorization required. + None, + + /// Either signature or proof is acceptable. + Either, + + /// Only a zero-knowledge proof is acceptable. + Proof, + + /// Only a signature is acceptable. + Signature, + + /// Operation is not permitted (impossible). + Impossible, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_set_or_keep() { + let set: SetOrKeep = SetOrKeep::Set(42); + let keep: SetOrKeep = SetOrKeep::Keep; + + assert!(set.is_set()); + assert!(!set.is_keep()); + assert!(keep.is_keep()); + assert!(!keep.is_set()); + } + + #[test] + fn test_memo_debug() { + // Text memo with "hello" + let mut memo_bytes = [0u8; 34]; + memo_bytes[0] = 0x01; // text tag + memo_bytes[1] = 5; // length + memo_bytes[2..7].copy_from_slice(b"hello"); + + let memo = Memo(memo_bytes); + let debug_str = format!("{:?}", memo); + assert!(debug_str.contains("hello")); + } +}