From d8065671eba6bbe4e25e1fb0e2ea9c5ead9797e1 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Sun, 7 Dec 2025 22:55:13 -0300 Subject: [PATCH 1/3] Add mina-tx-type crate with zkApp transaction types Create a new standalone crate `mina-tx-type` that extracts transaction type structures from the ledger crate. This enables external projects to reuse these structures without duplicating code. The crate includes: - ZkAppCommand and related types (FeePayer, AccountUpdate, CallForest) - Account update body with all fields (update, balance_change, preconditions) - Preconditions (network, account, valid_while) - SetOrKeep and OrIgnore wrapper types - Authorization types (AuthorizationKind, MayUseToken, Permissions) - Primitive types (Fee, Amount, Balance, Nonce, Slot, etc.) Key features: - no_std only (no std feature) for embedded/constrained environments - Comprehensive rustdoc documentation for all fields - Generic over public key and field element types References issue #1665. --- Cargo.lock | 4 + Cargo.toml | 2 + mina-tx-type/Cargo.toml | 14 + mina-tx-type/src/lib.rs | 46 ++ mina-tx-type/src/zkapp.rs | 889 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 955 insertions(+) create mode 100644 mina-tx-type/Cargo.toml create mode 100644 mina-tx-type/src/lib.rs create mode 100644 mina-tx-type/src/zkapp.rs diff --git a/Cargo.lock b/Cargo.lock index f9fa798ff6..00bc75883b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5346,6 +5346,10 @@ dependencies = [ "zstd", ] +[[package]] +name = "mina-tx-type" +version = "0.18.1" + [[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/mina-tx-type/Cargo.toml b/mina-tx-type/Cargo.toml new file mode 100644 index 0000000000..90229ac51c --- /dev/null +++ b/mina-tx-type/Cargo.toml @@ -0,0 +1,14 @@ +[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] + +[dev-dependencies] diff --git a/mina-tx-type/src/lib.rs b/mina-tx-type/src/lib.rs new file mode 100644 index 0000000000..100626109c --- /dev/null +++ b/mina-tx-type/src/lib.rs @@ -0,0 +1,46 @@ +//! Transaction types for the Mina Protocol +//! +//! This crate provides standalone data structures representing Mina Protocol +//! transaction types. It is designed to be `no_std` compatible for use in +//! constrained environments such as hardware wallets and WebAssembly. +//! +//! # 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 +//! - +//! +//! # no_std Support +//! +//! This crate is `no_std` by design to support embedded and constrained +//! environments such as hardware wallets and WebAssembly. + +#![no_std] + +extern crate alloc; + +pub mod zkapp; + +pub use zkapp::*; diff --git a/mina-tx-type/src/zkapp.rs b/mina-tx-type/src/zkapp.rs new file mode 100644 index 0000000000..a5e16bcb85 --- /dev/null +++ b/mina-tx-type/src/zkapp.rs @@ -0,0 +1,889 @@ +//! 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 + +extern crate alloc; + +use alloc::vec::Vec; + +/// 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: Pk, + + /// 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>); + +/// 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: StackHash, +} + +/// 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: AccountUpdateDigest, + + /// 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: Auth, +} + +/// 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: Pk, + + /// 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() + } +} + +/// 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, +} + +/// 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>); + +/// 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); + +/// 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>); + +/// 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[..]) + } +} + +/// A cryptographic signature (r, s components as field elements). +/// +/// Signatures in Mina use the Schnorr signature scheme over the Pallas curve. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature { + /// The r component of the signature. + pub rx: [u8; 32], + + /// The s component of the signature. + pub s: [u8; 32], +} + +/// Transaction fee in nanomina (1 MINA = 10^9 nanomina). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Fee(pub u64); + +/// Token amount in the smallest unit. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Amount(pub u64); + +/// Account balance in the smallest unit. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Balance(pub u64); + +/// Account nonce (transaction sequence number). +/// +/// Incremented with each transaction from the account to prevent replay attacks. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Nonce(pub u32); + +/// Global slot number since genesis. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Slot(pub u32); + +/// A span of slots (duration). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct SlotSpan(pub u32); + +/// Blockchain length (block height). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Length(pub u32); + +/// A signed value with magnitude and sign. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Signed { + /// The absolute value. + pub magnitude: T, + + /// The sign (positive or negative). + pub sgn: Sgn, +} + +/// Sign of a value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Sgn { + /// Positive value. + Pos, + + /// Negative value. + Neg, +} + +/// 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, +} + +/// Stack hash for call forest commitment. +pub type StackHash = [u8; 32]; + +/// Account update digest for commitment. +pub type AccountUpdateDigest = [u8; 32]; + +#[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 = alloc::format!("{:?}", memo); + assert!(debug_str.contains("hello")); + } +} From 3d2edae2cc8b54a47f30cacc61bcf4d00d6f8f07 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Mon, 8 Dec 2025 00:16:42 -0300 Subject: [PATCH 2/3] mina-tx-type: integrate currency types with ledger crate Move currency types (Amount, Balance, Fee, Nonce, Slot, etc.) to mina-tx-type and have ledger crate re-export them. This includes: - Add mina-p2p-messages as dependency for type conversions - Add ark-ff dependency for field operations in Magnitude trait - Move From implementations for p2p message types to mina-tx-type - Move rand Distribution implementations to mina-tx-type - Update ledger/scan_state/currency.rs to re-export from mina-tx-type - Add extension traits (AmountToChecked, etc.) for checked conversions - Clean up unused imports across ledger crate - Add mina-tx-type to frontend Dockerfile for WASM builds --- Cargo.lock | 9 + frontend/Dockerfile | 1 + ledger/Cargo.toml | 1 + ledger/src/account/common.rs | 1 + ledger/src/proofs/block.rs | 3 +- ledger/src/proofs/merge.rs | 1 + ledger/src/proofs/numbers/currency.rs | 89 +- ledger/src/proofs/numbers/mod.rs | 10 + ledger/src/proofs/numbers/nat.rs | 90 +- ledger/src/proofs/transaction.rs | 4 + ledger/src/proofs/zkapp.rs | 1 + ledger/src/scan_state/conv.rs | 255 +--- ledger/src/scan_state/currency.rs | 581 ++------- ledger/src/scan_state/fee_excess.rs | 5 +- ledger/src/scan_state/pending_coinbase.rs | 1 + .../transaction_logic/local_state.rs | 2 +- .../transaction_logic/zkapp_command/mod.rs | 3 +- ledger/src/staged_ledger/staged_ledger.rs | 2 +- ledger/src/transaction_pool.rs | 28 +- ledger/src/zkapps/snark.rs | 2 + mina-tx-type/Cargo.toml | 6 + mina-tx-type/src/currency.rs | 1070 +++++++++++++++++ mina-tx-type/src/lib.rs | 19 +- mina-tx-type/src/zkapp.rs | 249 ++-- 24 files changed, 1555 insertions(+), 878 deletions(-) create mode 100644 mina-tx-type/src/currency.rs diff --git a/Cargo.lock b/Cargo.lock index 00bc75883b..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", @@ -5349,6 +5350,14 @@ dependencies = [ [[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" 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/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..31d52b34c9 100644 --- a/ledger/src/scan_state/currency.rs +++ b/ledger/src/scan_state/currency.rs @@ -1,196 +1,35 @@ -use std::cmp::Ordering::{Equal, Greater, Less}; +//! Currency types with ledger-specific implementations. +//! +//! Re-exports types from [`mina_tx_type::currency`] and adds implementations +//! for ledger-specific traits like [`ToFieldElements`], [`Check`], and [`ToInputs`]. -use ark_ff::{BigInteger256, Field}; -use mina_p2p_messages::v2::BlockTimeTimeStableV1; use rand::Rng; 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) - } - } - } -} +// 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, Slot, SlotSpan, TxnVersion, +}; -impl Signed { - pub fn to_fee(self) -> Signed { - let Self { magnitude, sgn } = self; +// ============================================================================ +// Extension traits for ledger-specific functionality +// ============================================================================ - Signed { - magnitude: Fee(magnitude.0), - sgn, - } - } +/// Extension trait for random generation of Signed values. +pub trait SignedRandExt { + fn gen() -> Signed; } -impl Signed +impl SignedRandExt for Signed where T: Magnitude + PartialOrd + Ord + Clone, rand::distributions::Standard: rand::distributions::Distribution, { - pub fn gen() -> Self { + fn gen() -> Signed { let mut rng = rand::thread_rng(); let magnitude: T = rng.gen(); @@ -200,352 +39,165 @@ where Sgn::Neg }; - Self::create(magnitude, sgn) + Signed::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) - } - } +/// Extension trait for Slot random generation. +pub trait SlotRandExt { + fn gen_small() -> Slot; +} - pub fn to_nanomina_int(&self) -> Self { - *self +impl SlotRandExt for Slot { + fn gen_small() -> Slot { + let mut rng = rand::thread_rng(); + Slot::from_u32(rng.gen::() % 10_000) } +} - pub fn to_mina_int(&self) -> Self { - Self(self.0.checked_div(Self::UNIT_TO_NANO).unwrap()) - } +// ============================================================================ +// Ledger-specific type: N (generic number) +// ============================================================================ - pub fn of_mina_int_exn(int: u64) -> Self { - Self::from_u64(int).scale(Self::UNIT_TO_NANO).unwrap() - } +/// A generic 64-bit number type for proof computations. +#[derive( + Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, +)] +pub struct N(pub(super) u64); - pub fn of_nanomina_int_exn(int: u64) -> Self { - Self::from_u64(int) +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 Balance { - pub fn sub_amount(&self, amount: Amount) -> Option { - self.0.checked_sub(amount.0).map(Self) - } +impl Magnitude for N { + const NBITS: usize = 64; - pub fn add_amount(&self, amount: Amount) -> Option { - self.0.checked_add(amount.0).map(Self) + fn zero() -> Self { + Self(0) } - pub fn add_signed_flagged(&self, rhs: Signed) -> (Self, bool) { - if let Sgn::Pos = rhs.sgn { - self.add_flagged(&rhs.magnitude) - } else { - self.sub_flagged(&rhs.magnitude) - } + fn is_zero(&self) -> bool { + self.0 == 0 } - pub fn add_signed_amount_flagged(&self, rhs: Signed) -> (Self, bool) { - let rhs = Signed { - magnitude: Balance::from_u64(rhs.magnitude.0), - sgn: rhs.sgn, - }; + fn wrapping_add(&self, rhs: &Self) -> Self { + Self(self.0.wrapping_add(rhs.0)) + } - if let Sgn::Pos = rhs.sgn { - self.add_flagged(&rhs.magnitude) - } else { - self.sub_flagged(&rhs.magnitude) - } + fn wrapping_mul(&self, rhs: &Self) -> Self { + Self(self.0.wrapping_mul(rhs.0)) } - pub fn to_amount(self) -> Amount { - Amount(self.0) + fn wrapping_sub(&self, rhs: &Self) -> Self { + Self(self.0.wrapping_sub(rhs.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) + fn checked_add(&self, rhs: &Self) -> Option { + self.0.checked_add(rhs.0).map(Self) } - pub fn of_nanomina_int_exn(int: u64) -> Self { - Self::from_u64(int) + fn checked_mul(&self, rhs: &Self) -> Option { + self.0.checked_mul(rhs.0).map(Self) } -} -impl Fee { - pub const fn of_nanomina_int_exn(int: u64) -> Self { - Self::from_u64(int) + fn checked_sub(&self, rhs: &Self) -> Option { + self.0.checked_sub(rhs.0).map(Self) } -} -impl Index { - // TODO: Not sure if OCaml wraps around here - pub fn incr(&self) -> Self { - Self(self.0.wrapping_add(1)) + fn checked_div(&self, rhs: &Self) -> Option { + self.0.checked_div(rhs.0).map(Self) } -} -impl Nonce { - // TODO: Not sure if OCaml wraps around here - pub fn incr(&self) -> Self { - Self(self.0.wrapping_add(1)) + fn checked_rem(&self, rhs: &Self) -> Option { + self.0.checked_rem(rhs.0).map(Self) } - pub fn succ(&self) -> Self { - self.incr() + fn abs_diff(&self, rhs: &Self) -> Self { + Self(self.0.abs_diff(rhs.0)) } - pub fn add_signed_flagged(&self, rhs: Signed) -> (Self, bool) { - if let Sgn::Pos = rhs.sgn { - self.add_flagged(&rhs.magnitude) - } else { - self.sub_flagged(&rhs.magnitude) - } + fn to_field(&self) -> F { + F::from(self.0) } - /// low <= self <= high - pub fn between(&self, low: &Self, high: &Self) -> bool { - low <= self && self <= high + fn of_field(field: F) -> Self { + use ark_ff::BigInteger256; + let bigint: BigInteger256 = field.into(); + Self(bigint.0[0]) } } -impl BlockTime { - pub fn add(&self, span: BlockTimeSpan) -> Self { - Self(self.0.checked_add(span.0).unwrap()) +impl MinMax for N { + fn min() -> Self { + Self(0) } - - pub fn sub(&self, span: BlockTimeSpan) -> Self { - Self(self.0.checked_sub(span.0).unwrap()) + fn max() -> Self { + Self(u64::MAX) } +} - pub fn diff(&self, other: Self) -> BlockTimeSpan { - BlockTimeSpan(self.0 - other.0) - } +impl N { + pub const NBITS: usize = 64; - pub fn to_span_since_epoch(&self) -> BlockTimeSpan { - let Self(ms) = self; - BlockTimeSpan(*ms) + pub fn as_u64(&self) -> u64 { + self.0 } - pub fn of_span_since_epoch(span: BlockTimeSpan) -> Self { - let BlockTimeSpan(ms) = span; - Self(ms) + pub const fn from_u64(value: u64) -> Self { + Self(value) } -} -impl From for BlockTime { - fn from(bt: BlockTimeTimeStableV1) -> Self { - Self(bt.0 .0 .0) + pub const fn scale(&self, n: u64) -> Option { + match self.0.checked_mul(n) { + Some(n) => Some(Self(n)), + None => None, + } } } -impl BlockTimeSpan { - pub fn of_ms(ms: u64) -> Self { - Self(ms) - } - pub fn to_ms(&self) -> u64 { - let Self(ms) = self; - *ms +impl rand::distributions::Distribution for rand::distributions::Standard { + fn sample(&self, rng: &mut R) -> N { + N(rng.next_u64()) } } -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()) +impl crate::ToInputs for N { + fn to_inputs(&self, inputs: &mut poseidon::hash::Inputs) { + inputs.append_u64(self.0); } +} - pub fn succ(&self) -> Self { - self.incr() +impl ToFieldElements for N { + fn to_field_elements(&self, fields: &mut Vec) { + fields.push(self.to_field()); } +} - pub fn gen_small() -> Self { - let mut rng = rand::thread_rng(); - Self(rng.gen::() % 10_000) +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); } } -macro_rules! impl_number { +// ============================================================================ +// Macro for implementing ledger traits on currency types +// ============================================================================ + +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, append_u32},);)* + $(impl_ledger_traits!({$name64, u64, as_u64, append_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()) - } - } - + ($({ $name:ident, $inner:ty, $as_name:ident, $append_name:ident },)*) => ($( impl crate::ToInputs for $name { fn to_inputs(&self, inputs: &mut poseidon::hash::Inputs) { - inputs.$append_name(self.0); + inputs.$append_name(self.$as_name()); } } @@ -568,11 +220,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 index 90229ac51c..b7c7283e6a 100644 --- a/mina-tx-type/Cargo.toml +++ b/mina-tx-type/Cargo.toml @@ -10,5 +10,11 @@ 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 } [dev-dependencies] diff --git a/mina-tx-type/src/currency.rs b/mina-tx-type/src/currency.rs new file mode 100644 index 0000000000..4c30791d1e --- /dev/null +++ b/mina-tx-type/src/currency.rs @@ -0,0 +1,1070 @@ +//! 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; +} + +/// 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) + } +} + +// ============================================================================ +// Random generation implementations +// ============================================================================ + +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 index 100626109c..45824b6dfe 100644 --- a/mina-tx-type/src/lib.rs +++ b/mina-tx-type/src/lib.rs @@ -1,8 +1,8 @@ //! Transaction types for the Mina Protocol //! //! This crate provides standalone data structures representing Mina Protocol -//! transaction types. It is designed to be `no_std` compatible for use in -//! constrained environments such as hardware wallets and WebAssembly. +//! transaction types. It can be used by external projects to work with Mina +//! transactions without depending on the full ledger crate. //! //! # Overview //! @@ -31,16 +31,13 @@ //! For detailed information about zkApp transaction signing, see: //! - TODO: Issue #1748 - zkApp transaction signing documentation //! - -//! -//! # no_std Support -//! -//! This crate is `no_std` by design to support embedded and constrained -//! environments such as hardware wallets and WebAssembly. - -#![no_std] - -extern crate alloc; +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 index a5e16bcb85..6a78196590 100644 --- a/mina-tx-type/src/zkapp.rs +++ b/mina-tx-type/src/zkapp.rs @@ -25,9 +25,9 @@ //! - Account state (balance, nonce, app state, etc.) //! - Time validity windows -extern crate alloc; - -use alloc::vec::Vec; +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. /// @@ -60,20 +60,20 @@ use alloc::vec::Vec; /// /// OCaml: `src/lib/mina_base/zkapp_command.ml` #[derive(Debug, Clone, PartialEq)] -pub struct ZkAppCommand { +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, + 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>, + pub account_updates: CallForest, /// User-defined transaction memo. /// @@ -95,9 +95,9 @@ pub struct ZkAppCommand { /// /// OCaml: `src/lib/mina_base/account_update.ml` (FeePayer module) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct FeePayer { +pub struct FeePayer { /// The fee payer's account details and fee information. - pub body: FeePayerBody, + pub body: FeePayerBody, /// The signature authorizing the fee payment. /// @@ -112,12 +112,12 @@ pub struct FeePayer { /// /// OCaml: `src/lib/mina_base/account_update.ml` (FeePayerBody) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct FeePayerBody { +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: Pk, + pub public_key: CompressedPubKey, /// The fee to pay for the transaction. /// @@ -155,14 +155,20 @@ pub struct FeePayerBody { /// /// OCaml: `src/lib/mina_base/zkapp_command.ml` (CallForest) #[derive(Debug, Clone, PartialEq)] -pub struct CallForest(pub Vec>); +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 { +pub struct WithStackHash { /// The account update and its children. pub elt: Tree, @@ -170,17 +176,17 @@ pub struct WithStackHash { /// /// This hash commits to this account update and all its descendants, /// enabling efficient Merkle-style proofs. - pub stack_hash: StackHash, + pub stack_hash: Fp, } /// A tree node containing an account update and its children. #[derive(Debug, Clone, PartialEq)] -pub struct Tree { +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: AccountUpdateDigest, + pub account_update_digest: Fp, /// Child account updates that depend on this update. pub calls: CallForest, @@ -207,14 +213,14 @@ pub struct Tree { /// /// OCaml: `src/lib/mina_base/account_update.ml` #[derive(Debug, Clone, PartialEq)] -pub struct AccountUpdate { +pub struct AccountUpdate { /// The body containing all update details and preconditions. - pub body: AccountUpdateBody, + pub body: AccountUpdateBody, /// The authorization for this update. /// /// Must match the `authorization_kind` specified in the body. - pub authorization: Auth, + pub authorization: Control, } /// The body of an account update containing all modification details. @@ -226,20 +232,20 @@ pub struct AccountUpdate { /// /// OCaml: `src/lib/mina_base/account_update.ml` (Body) #[derive(Debug, Clone, PartialEq)] -pub struct AccountUpdateBody { +pub struct AccountUpdateBody { /// The public key of the account to update. - pub public_key: Pk, + 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, + 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, + pub update: Update, /// The change to the account's balance. /// @@ -258,13 +264,13 @@ pub struct AccountUpdateBody { /// 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, + 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, + pub actions: Actions, /// Arbitrary field element for zkApp-specific data. /// @@ -276,7 +282,7 @@ pub struct AccountUpdateBody { /// /// Includes network state preconditions (blockchain length, slot, etc.) /// and account state preconditions (balance, nonce, app state, etc.). - pub preconditions: Preconditions, + pub preconditions: Preconditions, /// Whether to use the full transaction commitment for signing. /// @@ -300,7 +306,7 @@ pub struct AccountUpdateBody { /// /// Must be consistent with the `authorization` field in the parent /// `AccountUpdate` structure. - pub authorization_kind: AuthorizationKind, + pub authorization_kind: AuthorizationKind, } /// Updates to apply to an account's state. @@ -313,7 +319,7 @@ pub struct AccountUpdateBody { /// /// OCaml: `src/lib/mina_base/account_update.ml` (Update) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Update { +pub struct Update { /// Updates to the 8 app state field elements. /// /// zkApp accounts have 8 field elements of arbitrary state that can be @@ -324,13 +330,13 @@ pub struct Update { /// /// The delegate receives staking rewards on behalf of this account. /// Only applicable for accounts using the default MINA token. - pub delegate: SetOrKeep, + 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>, + pub verification_key: SetOrKeep, /// Update to the account's permissions. /// @@ -359,7 +365,7 @@ pub struct Update { /// /// Indicates which proposal or election this account is voting for /// in on-chain governance. - pub voting_for: SetOrKeep>, + pub voting_for: SetOrKeep, } /// A value that can be set to a new value or kept unchanged. @@ -391,6 +397,16 @@ impl SetOrKeep { } } +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 @@ -400,12 +416,12 @@ impl SetOrKeep { /// /// OCaml: `src/lib/mina_base/account_update.ml` (Preconditions) #[derive(Debug, Clone, PartialEq)] -pub struct Preconditions { +pub struct Preconditions { /// Network state preconditions (blockchain length, slot, etc.). - pub network: NetworkPreconditions, + pub network: NetworkPreconditions, /// Account state preconditions (balance, nonce, app state, etc.). - pub account: AccountPreconditions, + pub account: AccountPreconditions, /// Slot range during which this update is valid. /// @@ -422,7 +438,7 @@ pub struct Preconditions { /// /// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (Protocol_state) #[derive(Debug, Clone, PartialEq)] -pub struct NetworkPreconditions { +pub struct NetworkPreconditions { /// Expected hash of the snarked ledger. pub snarked_ledger_hash: OrIgnore, @@ -439,10 +455,10 @@ pub struct NetworkPreconditions { pub global_slot_since_genesis: Numeric, /// Preconditions on the current staking epoch. - pub staking_epoch_data: EpochData, + pub staking_epoch_data: EpochData, /// Preconditions on the next staking epoch. - pub next_epoch_data: EpochData, + pub next_epoch_data: EpochData, } /// Epoch data preconditions. @@ -451,9 +467,9 @@ pub struct NetworkPreconditions { /// /// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (EpochData) #[derive(Debug, Clone, PartialEq)] -pub struct EpochData { +pub struct EpochData { /// Expected epoch ledger hash. - pub ledger: EpochLedger, + pub ledger: EpochLedger, /// Expected epoch seed. pub seed: OrIgnore, @@ -470,7 +486,7 @@ pub struct EpochData { /// Epoch ledger preconditions. #[derive(Debug, Clone, PartialEq)] -pub struct EpochLedger { +pub struct EpochLedger { /// Expected ledger hash. pub hash: OrIgnore, @@ -487,7 +503,7 @@ pub struct EpochLedger { /// /// OCaml: `src/lib/mina_base/zkapp_precondition.ml` (Account) #[derive(Debug, Clone, PartialEq)] -pub struct AccountPreconditions { +pub struct AccountPreconditions { /// Expected account balance range. pub balance: Numeric, @@ -498,7 +514,7 @@ pub struct AccountPreconditions { pub receipt_chain_hash: OrIgnore, /// Expected delegate public key. - pub delegate: OrIgnore, + pub delegate: OrIgnore, /// Expected app state values (8 field elements). pub state: [OrIgnore; 8], @@ -559,7 +575,7 @@ pub struct ClosedInterval { /// /// OCaml: `src/lib/mina_base/account_update.ml` (AuthorizationKind) #[derive(Debug, Clone, PartialEq, Eq)] -pub enum AuthorizationKind { +pub enum AuthorizationKind { /// No authorization provided. /// /// Only valid for operations that don't require authorization @@ -595,6 +611,32 @@ pub enum MayUseToken { 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 @@ -604,7 +646,19 @@ pub enum MayUseToken { /// /// OCaml: `src/lib/mina_base/account_update.ml` (Events) #[derive(Debug, Clone, PartialEq)] -pub struct Events(pub Vec>); +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. /// @@ -612,7 +666,24 @@ pub struct Events(pub Vec>); /// /// OCaml: `src/lib/mina_base/account_update.ml` (Event) #[derive(Debug, Clone, PartialEq)] -pub struct Event(pub Vec); +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. /// @@ -623,7 +694,19 @@ pub struct Event(pub Vec); /// /// OCaml: `src/lib/mina_base/zkapp_account.ml` (Actions) #[derive(Debug, Clone, PartialEq)] -pub struct Actions(pub Vec>); +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. /// @@ -685,78 +768,16 @@ impl core::fmt::Debug for Memo { } } -/// A cryptographic signature (r, s components as field elements). -/// -/// Signatures in Mina use the Schnorr signature scheme over the Pallas curve. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Signature { - /// The r component of the signature. - pub rx: [u8; 32], - - /// The s component of the signature. - pub s: [u8; 32], -} - -/// Transaction fee in nanomina (1 MINA = 10^9 nanomina). -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Fee(pub u64); - -/// Token amount in the smallest unit. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Amount(pub u64); - -/// Account balance in the smallest unit. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Balance(pub u64); - -/// Account nonce (transaction sequence number). -/// -/// Incremented with each transaction from the account to prevent replay attacks. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Nonce(pub u32); - -/// Global slot number since genesis. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Slot(pub u32); - -/// A span of slots (duration). -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct SlotSpan(pub u32); - -/// Blockchain length (block height). -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Length(pub u32); - -/// A signed value with magnitude and sign. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Signed { - /// The absolute value. - pub magnitude: T, - - /// The sign (positive or negative). - pub sgn: Sgn, -} - -/// Sign of a value. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Sgn { - /// Positive value. - Pos, - - /// Negative value. - Neg, -} - /// 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); +pub struct TokenId(pub Fp); /// Hash of a verification key. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct VerificationKeyHash(pub Fp); +pub struct VerificationKeyHash(pub Fp); /// zkApp URI for off-chain resources. #[derive(Debug, Clone, PartialEq, Eq)] @@ -768,7 +789,7 @@ pub struct TokenSymbol(pub Vec); /// Voting-for field (governance). #[derive(Debug, Clone, PartialEq, Eq)] -pub struct VotingFor(pub Fp); +pub struct VotingFor(pub Fp); /// Account permissions controlling operation authorization. /// @@ -853,12 +874,6 @@ pub enum AuthRequired { Impossible, } -/// Stack hash for call forest commitment. -pub type StackHash = [u8; 32]; - -/// Account update digest for commitment. -pub type AccountUpdateDigest = [u8; 32]; - #[cfg(test)] mod tests { use super::*; @@ -883,7 +898,7 @@ mod tests { memo_bytes[2..7].copy_from_slice(b"hello"); let memo = Memo(memo_bytes); - let debug_str = alloc::format!("{:?}", memo); + let debug_str = format!("{:?}", memo); assert!(debug_str.contains("hello")); } } From 9952ccda5351e7a4d71744b2f856dc444271d1d9 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Mon, 8 Dec 2025 00:28:35 -0300 Subject: [PATCH 3/3] mina-tx-type: add N type, ToChecked, SignedRandExt, SlotRandExt Moves additional types and traits from ledger to mina-tx-type: - N type: Generic 64-bit number for proof computations - ToChecked trait: Convert currency types to checked circuit forms - SignedRandExt trait: Random signed value generation - SlotRandExt trait: Random slot generation for tests Also consolidates ToInputs implementations in ledger/src/hash.rs using the impl_to_inputs macro for currency types. Note: ToInputs trait cannot be moved to mina-tx-type due to Rust's orphan rule - ledger needs to implement ToInputs for foreign types. Issue: #1665 --- ledger/src/hash.rs | 100 +++++++--------- ledger/src/scan_state/currency.rs | 177 ++------------------------- mina-tx-type/Cargo.toml | 2 - mina-tx-type/src/currency.rs | 192 ++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 221 deletions(-) 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/scan_state/currency.rs b/ledger/src/scan_state/currency.rs index 31d52b34c9..5d34b4502a 100644 --- a/ledger/src/scan_state/currency.rs +++ b/ledger/src/scan_state/currency.rs @@ -1,9 +1,9 @@ //! Currency types with ledger-specific implementations. //! //! Re-exports types from [`mina_tx_type::currency`] and adds implementations -//! for ledger-specific traits like [`ToFieldElements`], [`Check`], and [`ToInputs`]. - -use rand::Rng; +//! 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, @@ -12,163 +12,14 @@ use crate::proofs::{ // 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, Slot, SlotSpan, TxnVersion, + MinMax, Nonce, Sgn, Signed, SignedRandExt, Slot, SlotRandExt, SlotSpan, ToChecked, TxnVersion, + N, }; // ============================================================================ -// Extension traits for ledger-specific functionality -// ============================================================================ - -/// Extension trait for random generation of Signed values. -pub trait SignedRandExt { - fn gen() -> Signed; -} - -impl SignedRandExt for Signed -where - T: Magnitude + PartialOrd + Ord + Clone, - rand::distributions::Standard: rand::distributions::Distribution, -{ - fn gen() -> Signed { - 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 Slot random generation. -pub trait SlotRandExt { - fn gen_small() -> Slot; -} - -impl SlotRandExt for Slot { - fn gen_small() -> Slot { - let mut rng = rand::thread_rng(); - Slot::from_u32(rng.gen::() % 10_000) - } -} - -// ============================================================================ -// Ledger-specific type: N (generic number) +// Ledger-specific trait implementations for N // ============================================================================ -/// A generic 64-bit number type for proof computations. -#[derive( - Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, -)] -pub struct N(pub(super) 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 { - pub const NBITS: usize = 64; - - pub fn as_u64(&self) -> u64 { - self.0 - } - - pub const fn from_u64(value: u64) -> Self { - Self(value) - } - - 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()) - } -} - -impl crate::ToInputs for N { - fn to_inputs(&self, inputs: &mut poseidon::hash::Inputs) { - inputs.append_u64(self.0); - } -} - impl ToFieldElements for N { fn to_field_elements(&self, fields: &mut Vec) { fields.push(self.to_field()); @@ -186,21 +37,17 @@ impl Check for N { } // ============================================================================ -// Macro for implementing ledger traits on currency types +// Macro for implementing ledger-specific traits on currency types // ============================================================================ +/// 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_ledger_traits!({$name32, u32, as_u32, append_u32},);)* - $(impl_ledger_traits!({$name64, u64, as_u64, append_u64},);)* + $(impl_ledger_traits!({$name32, u32, as_u32},);)* + $(impl_ledger_traits!({$name64, u64, as_u64},);)* }; - ($({ $name:ident, $inner:ty, $as_name:ident, $append_name:ident },)*) => ($( - impl crate::ToInputs for $name { - fn to_inputs(&self, inputs: &mut poseidon::hash::Inputs) { - inputs.$append_name(self.$as_name()); - } - } - + ($({ $name:ident, $inner:ty, $as_name:ident },)*) => ($( impl ToFieldElements for $name { fn to_field_elements(&self, fields: &mut Vec) { fields.push(self.to_field()); diff --git a/mina-tx-type/Cargo.toml b/mina-tx-type/Cargo.toml index b7c7283e6a..9ffaf388ee 100644 --- a/mina-tx-type/Cargo.toml +++ b/mina-tx-type/Cargo.toml @@ -16,5 +16,3 @@ mina-p2p-messages = { workspace = true } mina-signer = { workspace = true } rand = { workspace = true } serde = { workspace = true } - -[dev-dependencies] diff --git a/mina-tx-type/src/currency.rs b/mina-tx-type/src/currency.rs index 4c30791d1e..901c1fec8d 100644 --- a/mina-tx-type/src/currency.rs +++ b/mina-tx-type/src/currency.rs @@ -141,6 +141,36 @@ pub trait MinMax { 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 @@ -992,10 +1022,172 @@ impl From for BlockTime { } } +// ============================================================================ +// 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,)* },) => { $(