From a4dddd426938ce28e6af90bd280620096ab677dd Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Wed, 8 Oct 2025 16:35:30 +0200 Subject: [PATCH 01/26] Website/doc: rename documentation guidelines into ref code in doc --- ...guidelines.md => referencing-code-in-documentation.md} | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) rename website/docs/developers/{documentation-guidelines.md => referencing-code-in-documentation.md} (96%) diff --git a/website/docs/developers/documentation-guidelines.md b/website/docs/developers/referencing-code-in-documentation.md similarity index 96% rename from website/docs/developers/documentation-guidelines.md rename to website/docs/developers/referencing-code-in-documentation.md index e6d74a625..e525c55a1 100644 --- a/website/docs/developers/documentation-guidelines.md +++ b/website/docs/developers/referencing-code-in-documentation.md @@ -1,11 +1,11 @@ --- sidebar_position: 10 -title: Documentation Guidelines +title: Referencing code in documentation description: Best practices for writing and maintaining documentation -slug: /developers/documentation-guidelines +slug: /developers/referencing-code-in-documentation --- -# Documentation Guidelines +# Referencing code in documentation This guide explains how to write and maintain documentation for the Mina Rust project, including how to reference code from the codebase. Referencing code @@ -13,8 +13,6 @@ from codebases can be useful to check compatibility between implementations. For instance, we can have pages where the Rust code is compared to the OCaml code to discuss the differences or similarities. -## Referencing Code in Documentation - To keep documentation synchronized with the actual codebase, we use the [`docusaurus-theme-github-codeblock`](https://github.com/christian-bromann/docusaurus-theme-github-codeblock) plugin that automatically fetches code from GitHub. From 390d0f342f29a2ab31102c0c632bb86c7909cd93 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Wed, 8 Oct 2025 16:36:15 +0200 Subject: [PATCH 02/26] CLAUDE: add instructions to check OCaml refs --- CLAUDE.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 0b59440f8..f8644f217 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,6 +162,40 @@ The website is available at http://localhost:3000 when running locally. The website supports versioning and will be automatically deployed when commits are made to `develop` or when tags are created. +### OCaml Reference Tracking + +The Rust codebase maintains references to the corresponding OCaml implementation +to track correspondence and detect when updates are needed. + +**Comment format:** + +```rust +/// OCaml reference: src/lib/mina_base/transaction_status.ml L:9-113 +/// Commit: 55582d249cdb225f722dbbb3b1420ce7570d501f +/// Last verified: 2025-10-08 +pub enum TransactionFailure { + // ... +} +``` + +**Validation:** + +```bash +# Check all OCaml references +./.github/scripts/check-ocaml-refs.sh + +# Update stale commit hashes +./.github/scripts/check-ocaml-refs.sh --update + +# Check against specific branch +./.github/scripts/check-ocaml-refs.sh --branch develop +``` + +The validation script verifies that referenced OCaml files exist, line ranges +are valid, code at the referenced commit matches the current branch, and tracks +commit staleness. A GitHub Actions workflow runs weekly to automatically update +references and create PRs. + ## Additional Resources - `docs/handover/` - Comprehensive architecture documentation From 75d4f4c013c6870ed2e49a6f017e586214e4f6b6 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Wed, 8 Oct 2025 16:51:39 +0200 Subject: [PATCH 03/26] transaction_logic: add new format for checking OCaml references The script will also fail for now. The update script will be tested in a PR. --- ledger/src/scan_state/transaction_logic/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 7d486d3ca..95f9fff7c 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -60,7 +60,9 @@ pub use transaction_union_payload::{ ExistingOrNew, Tag, TimingValidation, TransactionUnion, TransactionUnionPayload, }; -/// +/// OCaml reference: src/lib/mina_base/transaction_status.ml L:9-51 +/// Commit: 2ee6e004ba8c6a0541056076aab22ea162f7eb3a +/// Last verified: 2025-10-08 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub enum TransactionFailure { Predicate, @@ -176,7 +178,9 @@ impl Display for TransactionFailure { } } -/// +/// OCaml reference: src/lib/mina_base/transaction_status.ml L:452-454 +/// Commit: 2ee6e004ba8c6a0541056076aab22ea162f7eb3a +/// Last verified: 2025-10-08 #[derive(SerdeYojsonEnum, Debug, Clone, PartialEq, Eq)] pub enum TransactionStatus { Applied, @@ -192,7 +196,9 @@ impl TransactionStatus { } } -/// +/// OCaml reference: src/lib/mina_base/with_status.ml L:6-10 +/// Commit: 2ee6e004ba8c6a0541056076aab22ea162f7eb3a +/// Last verified: 2025-10-08 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] pub struct WithStatus { pub data: T, From b9938ae1a9d1b48ae0cd68489f5d1731c7c36cf1 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Wed, 8 Oct 2025 16:52:24 +0200 Subject: [PATCH 04/26] Website: add documentation to reference OCaml code --- .../developers/ocaml-reference-tracking.md | 135 ++++++++++++++++++ website/sidebars.ts | 3 +- 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 website/docs/developers/ocaml-reference-tracking.md diff --git a/website/docs/developers/ocaml-reference-tracking.md b/website/docs/developers/ocaml-reference-tracking.md new file mode 100644 index 000000000..8fd9612d6 --- /dev/null +++ b/website/docs/developers/ocaml-reference-tracking.md @@ -0,0 +1,135 @@ +--- +sidebar_position: 11 +title: OCaml reference tracking +description: + System for tracking correspondence between Rust and OCaml implementations +slug: /developers/ocaml-reference-tracking +--- + +# OCaml reference tracking + +This document describes the system for tracking correspondence between Rust code +in this repository and the original OCaml implementation in the +[Mina Protocol repository](https://github.com/MinaProtocol/mina). + +## Overview + +As mina-rust is a reimplementation of the Mina OCaml client, we maintain inline +comments that reference the corresponding OCaml code. This helps developers: + +1. Understand the original implementation context +2. Verify that the Rust implementation matches the OCaml behavior +3. Track changes in the OCaml codebase that may require updates in Rust + +## Comment format + +OCaml references are added as doc comments directly above the Rust type or +function: + +```rust +/// OCaml reference: src/lib/mina_base/transaction_status.ml L:9-113 +/// Commit: 55582d249cdb225f722dbbb3b1420ce7570d501f +/// Last verified: 2025-10-08 +pub enum TransactionFailure { + // ... +} +``` + +### Format specification + +- **Line 1**: `/// OCaml reference: L:-` + - ``: Path to the OCaml file relative to the Mina repository root + - `L:-`: Line range in the OCaml file (optional but recommended) +- **Line 2**: `/// Commit: ` + - Full commit hash from the Mina repository +- **Line 3**: `/// Last verified: ` + - Date when the reference was last verified to be accurate + +## Validation script + +The `.github/scripts/check-ocaml-refs.sh` script validates all OCaml references: + +```bash +# Validate against compatible branch (default) +./.github/scripts/check-ocaml-refs.sh + +# Validate against a specific branch +./.github/scripts/check-ocaml-refs.sh --branch develop + +# Validate against a specific repository +./.github/scripts/check-ocaml-refs.sh --repo https://github.com/MinaProtocol/mina.git --branch develop + +# Automatically update stale commit hashes +./.github/scripts/check-ocaml-refs.sh --update +``` + +### What the script checks + +1. **File existence**: Verifies the OCaml file exists at the specified path +2. **Line ranges**: Validates that line ranges don't exceed the file length +3. **Code consistency**: Verifies that the code at the referenced commit matches + the code on the current branch (ensures the reference is still accurate) +4. **Commit staleness**: Checks if the commit hash matches the current HEAD + +### Exit codes + +- `0`: All references are valid or only stale commits (warning) +- `1`: Invalid references found (missing files or invalid line ranges) + +## Automated verification + +A GitHub Actions workflow runs weekly to: + +1. Validate all OCaml references against the latest `compatible` branch +2. Automatically update stale commit hashes and verification dates +3. Create a pull request with the updates + +The workflow can also be triggered manually via the Actions tab. + +## Adding new references + +When implementing new features from the OCaml codebase: + +1. Add the OCaml reference comment above your Rust type/function +2. Use the current commit hash from the Mina repository +3. Set the verification date to today +4. Include line ranges to make it easy to find the exact code + +Example: + +```rust +/// OCaml reference: src/lib/mina_base/fee_transfer.ml L:19-45 +/// Commit: 55582d249cdb225f722dbbb3b1420ce7570d501f +/// Last verified: 2025-10-08 +#[derive(Debug, Clone, PartialEq)] +pub struct SingleFeeTransfer { + pub receiver_pk: CompressedPubKey, + pub fee: Fee, + pub fee_token: TokenId, +} +``` + +## Finding the correct line range + +To find the line range for an OCaml reference: + +1. Navigate to the file in the Mina repository +2. Find the relevant type or function definition +3. Note the starting and ending line numbers +4. Use format `L:-` + +For single-line references, use the same number: `L:42-42` + +## Best practices + +1. **Be specific**: Include line ranges to point to exact definitions +2. **Verify regularly**: Run the validation script before committing +3. **Update when needed**: If you update Rust code based on OCaml changes, + update the commit hash and date +4. **Document differences**: If the Rust implementation intentionally differs, + add a note explaining why + +## Example references + +See `ledger/src/scan_state/transaction_logic/mod.rs` for examples of properly +formatted OCaml references. diff --git a/website/sidebars.ts b/website/sidebars.ts index 962b0621d..2b2411cd4 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -137,7 +137,8 @@ const sidebars: SidebarsConfig = { type: 'category', label: 'Documentation', items: [ - 'developers/documentation-guidelines', + 'developers/referencing-code-in-documentation', + 'developers/ocaml-reference-tracking', ], }, { From f26dd13779dff9f73b09bfe27c4a6ad830e62d25 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Wed, 8 Oct 2025 16:54:19 +0200 Subject: [PATCH 05/26] ocaml-ref-check: add script and CI --- .github/scripts/check-ocaml-refs.sh | 241 ++++++++++++++++++++++++ .github/workflows/check-ocaml-refs.yaml | 125 ++++++++++++ 2 files changed, 366 insertions(+) create mode 100755 .github/scripts/check-ocaml-refs.sh create mode 100644 .github/workflows/check-ocaml-refs.yaml diff --git a/.github/scripts/check-ocaml-refs.sh b/.github/scripts/check-ocaml-refs.sh new file mode 100755 index 000000000..e6167c248 --- /dev/null +++ b/.github/scripts/check-ocaml-refs.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# Script to validate OCaml reference comments in Rust code +# Usage: ./.github/scripts/check-ocaml-refs.sh [--repo REPO_URL] [--branch BRANCH] [--update] + +set -euo pipefail + +# Default configuration +OCAML_REPO="${OCAML_REPO:-https://github.com/MinaProtocol/mina.git}" +OCAML_BRANCH="${OCAML_BRANCH:-compatible}" +UPDATE_MODE="${UPDATE_MODE:-false}" +RUST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --repo) + OCAML_REPO="$2" + shift 2 + ;; + --branch) + OCAML_BRANCH="$2" + shift 2 + ;; + --update) + UPDATE_MODE="true" + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: ./.github/scripts/check-ocaml-refs.sh [--repo REPO_URL] [--branch BRANCH] [--update]" + exit 1 + ;; + esac +done + +echo "Checking OCaml references against ${OCAML_REPO} (branch: ${OCAML_BRANCH})" + +# Create temporary directory +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +# Extract GitHub owner and repo from URL (e.g., https://github.com/MinaProtocol/mina.git) +GITHUB_URL_PATTERN="https://github.com/([^/]+)/(.+)" +if [[ "$OCAML_REPO" =~ $GITHUB_URL_PATTERN ]]; then + GITHUB_OWNER="${BASH_REMATCH[1]}" + GITHUB_REPO="${BASH_REMATCH[2]%.git}" # Remove .git suffix if present +else + echo "Error: Repository URL must be a GitHub URL" + exit 1 +fi + +# Get current commit hash for the branch using GitHub API +echo "Fetching current commit from ${OCAML_BRANCH}..." +CURRENT_COMMIT=$(curl -s "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/commits/${OCAML_BRANCH}" | grep -o '"sha": "[^"]*"' | head -1 | cut -d'"' -f4) + +if [ -z "$CURRENT_COMMIT" ]; then + echo "Error: Could not fetch current commit for branch ${OCAML_BRANCH}" + exit 1 +fi + +echo "Current OCaml commit: ${CURRENT_COMMIT}" + +# Find all Rust files with OCaml references +cd "${RUST_ROOT}" +RUST_FILES=$(rg -l "^/// OCaml reference:" --type rust || true) + +if [ -z "$RUST_FILES" ]; then + echo "No OCaml references found in Rust code" + exit 0 +fi + +# Use temporary files to accumulate results +RESULTS_FILE="${TEMP_DIR}/results.txt" +touch "$RESULTS_FILE" + +echo "" +echo "Validating references..." +echo "========================" + +# Process each file +echo "$RUST_FILES" | while IFS= read -r rust_file; do + # Extract OCaml reference comments from the file + awk ' + /^\/\/\/ OCaml reference:/ { + ref = $0 + getline + if ($0 ~ /^\/\/\/ Commit:/) { + commit = $0 + getline + if ($0 ~ /^\/\/\/ Last verified:/) { + verified = $0 + print ref + print commit + print verified + print "---" + } + } + } + ' "$rust_file" | while IFS= read -r line; do + if [[ "$line" == "/// OCaml reference:"* ]]; then + # Extract file path and line range + # Format: src/lib/mina_base/transaction_status.ml L:9-113 + FULL_REF="${line#/// OCaml reference: }" + OCAML_PATH="${FULL_REF%% L:*}" + LINE_RANGE=$(echo "$FULL_REF" | grep -o 'L:[0-9-]*' | sed 's/L://' || echo "") + + # Read next two lines + read -r commit_line + read -r _verified_line + read -r _separator + + COMMIT="${commit_line#/// Commit: }" + # LAST_VERIFIED could be extracted from _verified_line if needed for future validation + + # Fetch the OCaml file from the current branch + CURRENT_FILE="${TEMP_DIR}/current_${rust_file//\//_}_${OCAML_PATH//\//_}" + CURRENT_URL="https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/${OCAML_BRANCH}/${OCAML_PATH}" + + if ! curl -sf "$CURRENT_URL" -o "$CURRENT_FILE"; then + echo "INVALID|${rust_file}|${OCAML_PATH}|FILE_NOT_FOUND" >> "$RESULTS_FILE" + echo "❌ INVALID: ${rust_file}" + echo " OCaml file not found: ${OCAML_PATH}" + else + # Validate line range if specified + RANGE_VALID=true + if [ -n "$LINE_RANGE" ]; then + FILE_LINES=$(wc -l < "$CURRENT_FILE") + # START_LINE is not currently used but could be useful for validation + # START_LINE=$(echo "$LINE_RANGE" | cut -d'-' -f1) + END_LINE=$(echo "$LINE_RANGE" | cut -d'-' -f2) + + if [ "$END_LINE" -gt "$FILE_LINES" ]; then + echo "INVALID|${rust_file}|${OCAML_PATH}|LINE_RANGE_EXCEEDED|L:${LINE_RANGE}|${FILE_LINES}" >> "$RESULTS_FILE" + echo "❌ INVALID: ${rust_file}" + echo " Line range L:${LINE_RANGE} exceeds file length (${FILE_LINES} lines): ${OCAML_PATH}" + RANGE_VALID=false + fi + fi + + if [ "$RANGE_VALID" = "true" ]; then + # Verify that the code at the referenced commit matches the current branch + CODE_MATCHES=true + if [ -n "$LINE_RANGE" ]; then + START_LINE=$(echo "$LINE_RANGE" | cut -d'-' -f1) + END_LINE=$(echo "$LINE_RANGE" | cut -d'-' -f2) + + # Fetch the file from the referenced commit + COMMIT_FILE="${TEMP_DIR}/commit_${rust_file//\//_}_${OCAML_PATH//\//_}" + COMMIT_URL="https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/${COMMIT}/${OCAML_PATH}" + + if ! curl -sf "$COMMIT_URL" -o "$COMMIT_FILE"; then + echo "INVALID|${rust_file}|${OCAML_PATH}|COMMIT_NOT_FOUND|${COMMIT}" >> "$RESULTS_FILE" + echo "❌ INVALID: ${rust_file}" + echo " Referenced commit does not exist: ${COMMIT}" + CODE_MATCHES=false + else + # Extract the specific line ranges from both files and compare + CURRENT_LINES=$(sed -n "${START_LINE},${END_LINE}p" "$CURRENT_FILE") + COMMIT_LINES=$(sed -n "${START_LINE},${END_LINE}p" "$COMMIT_FILE") + + if [ "$CURRENT_LINES" != "$COMMIT_LINES" ]; then + echo "INVALID|${rust_file}|${OCAML_PATH}|CODE_MISMATCH|${COMMIT}" >> "$RESULTS_FILE" + echo "❌ INVALID: ${rust_file}" + echo " Code at L:${LINE_RANGE} differs between commit ${COMMIT} and current branch" + echo " Referenced: https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/blob/${COMMIT}/${OCAML_PATH}#L${START_LINE}-L${END_LINE}" + echo " Current: https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/blob/${OCAML_BRANCH}/${OCAML_PATH}#L${START_LINE}-L${END_LINE}" + CODE_MATCHES=false + fi + fi + fi + + if [ "$CODE_MATCHES" = "true" ]; then + # Check if commit is stale + if [ "$COMMIT" != "$CURRENT_COMMIT" ]; then + echo "STALE|${rust_file}|${OCAML_PATH}|${COMMIT}|${LINE_RANGE}" >> "$RESULTS_FILE" + echo "✓ VALID: ${rust_file} -> ${OCAML_PATH} L:${LINE_RANGE}" + echo " ⚠ STALE COMMIT: ${COMMIT} (current: ${CURRENT_COMMIT})" + else + echo "VALID|${rust_file}|${OCAML_PATH}|${LINE_RANGE}" >> "$RESULTS_FILE" + echo "✓ VALID: ${rust_file} -> ${OCAML_PATH} L:${LINE_RANGE}" + fi + fi + fi + fi + fi + done +done + +# Count results +TOTAL_REFS=$(wc -l < "$RESULTS_FILE") +VALID_REFS=$(grep -c "^VALID|" "$RESULTS_FILE" || true) +INVALID_REFS=$(grep -c "^INVALID|" "$RESULTS_FILE" || true) +STALE_COMMITS=$(grep -c "^STALE|" "$RESULTS_FILE" || true) + +echo "" +echo "Summary" +echo "=======" +echo "Total references found: ${TOTAL_REFS}" +echo "Valid references: $((VALID_REFS + STALE_COMMITS))" +echo "Invalid references: ${INVALID_REFS}" +echo "Stale commits: ${STALE_COMMITS}" + +if [ "$UPDATE_MODE" = "true" ] && [ "${STALE_COMMITS}" -gt 0 ]; then + echo "" + echo "Updating stale commit hashes and verification dates..." + + CURRENT_DATE=$(date +%Y-%m-%d) + + # Update each file with stale commits + grep "^STALE|" "$RESULTS_FILE" | while IFS='|' read -r _status rust_file ocaml_path _old_commit _line_range; do + echo "Updating ${rust_file}..." + + # Find and replace the old commit with the new one + sed -i.bak \ + -e "/^\/\/\/ OCaml reference: ${ocaml_path//\//\\/}/,/^\/\/\/ Last verified:/ { + s/^\/\/\/ Commit: .*/\/\/\/ Commit: ${CURRENT_COMMIT}/ + s/^\/\/\/ Last verified: .*/\/\/\/ Last verified: ${CURRENT_DATE}/ + }" \ + "${RUST_ROOT}/${rust_file}" + rm -f "${RUST_ROOT}/${rust_file}.bak" + done + + echo "Updated ${STALE_COMMITS} reference(s)" +fi + +# Exit with error if there are invalid references +if [ "${INVALID_REFS}" -gt 0 ]; then + echo "" + echo "❌ Validation failed: ${INVALID_REFS} invalid reference(s) found" + exit 1 +fi + +if [ "${STALE_COMMITS}" -gt 0 ] && [ "$UPDATE_MODE" = "false" ]; then + echo "" + echo "⚠ Warning: ${STALE_COMMITS} reference(s) have stale commits" + echo "Run with --update to update them automatically" + exit 0 +fi + +echo "" +echo "✓ All OCaml references are valid!" diff --git a/.github/workflows/check-ocaml-refs.yaml b/.github/workflows/check-ocaml-refs.yaml new file mode 100644 index 000000000..ad2a4dcf7 --- /dev/null +++ b/.github/workflows/check-ocaml-refs.yaml @@ -0,0 +1,125 @@ +# Check OCaml References +# +# This workflow validates OCaml reference comments in the Rust codebase +# and automatically updates stale commit hashes. +# +# Run locally with: +# gh act schedule -W .github/workflows/check-ocaml-refs.yaml +# gh act workflow_dispatch -W .github/workflows/check-ocaml-refs.yaml +# gh act workflow_dispatch -W .github/workflows/check-ocaml-refs.yaml --input branch=develop + +name: Check OCaml References + +on: + schedule: + # Run every Monday at 9:00 AM UTC + - cron: '0 9 * * 1' + workflow_dispatch: + inputs: + repo: + description: 'OCaml repository URL' + required: false + default: 'https://github.com/MinaProtocol/mina.git' + branch: + description: 'OCaml repository branch' + required: false + default: 'compatible' + +permissions: + contents: write + pull-requests: write + +jobs: + check-refs: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Install ripgrep + run: sudo apt-get update && sudo apt-get install -y ripgrep + + - name: Run OCaml reference validation + id: check + env: + OCAML_REPO: ${{ github.event.inputs.repo || 'https://github.com/MinaProtocol/mina.git' }} + OCAML_BRANCH: ${{ github.event.inputs.branch || 'compatible' }} + run: | + set +e + ./.github/scripts/check-ocaml-refs.sh \ + --repo "$OCAML_REPO" \ + --branch "$OCAML_BRANCH" + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "has_issues=true" >> $GITHUB_OUTPUT + exit 0 + fi + + - name: Update references if stale + if: steps.check.outputs.has_issues != 'true' + env: + OCAML_REPO: ${{ github.event.inputs.repo || 'https://github.com/MinaProtocol/mina.git' }} + OCAML_BRANCH: ${{ github.event.inputs.branch || 'compatible' }} + run: | + ./.github/scripts/check-ocaml-refs.sh \ + --repo "$OCAML_REPO" \ + --branch "$OCAML_BRANCH" \ + --update + + - name: Check for changes + id: changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + Update OCaml reference verification dates + + Automated update of OCaml reference commit hashes and verification + dates based on latest compatible branch. + branch: update-ocaml-refs-${{ github.run_number }} + delete-branch: true + title: 'Update OCaml reference verification dates' + body: | + ## Automated OCaml Reference Update + + This PR updates the OCaml reference comments in the Rust codebase to + reflect the latest commit from the OCaml repository. + + ### Changes + - Updated commit hashes to match latest OCaml compatible branch + - Updated verification dates to today + + ### Validation + All OCaml file references have been validated to ensure they still + exist at the specified paths. + + **Repository**: ${{ github.event.inputs.repo || 'https://github.com/MinaProtocol/mina.git' }} + **Branch**: ${{ github.event.inputs.branch || 'compatible' }} + + This PR was automatically generated by the `check-ocaml-refs` + workflow. + labels: | + automation + documentation + + - name: Post summary + if: always() + run: | + echo "## OCaml Reference Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.check.outputs.has_issues }}" == "true" ]; then + echo "❌ Validation failed - some OCaml references are invalid" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.changes.outputs.has_changes }}" == "true" ]; then + echo "✓ Validation passed - created PR to update stale references" >> $GITHUB_STEP_SUMMARY + else + echo "✓ All references are up to date" >> $GITHUB_STEP_SUMMARY + fi From 55b993c5ce40763a89803347f643152c80911f14 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Thu, 9 Oct 2025 17:27:26 +0200 Subject: [PATCH 06/26] GH workflow: add step to post a comment on PR when failed --- .github/workflows/check-ocaml-refs.yaml | 66 +++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-ocaml-refs.yaml b/.github/workflows/check-ocaml-refs.yaml index ad2a4dcf7..6a3fad764 100644 --- a/.github/workflows/check-ocaml-refs.yaml +++ b/.github/workflows/check-ocaml-refs.yaml @@ -11,6 +11,10 @@ name: Check OCaml References on: + pull_request: + branches: + - develop + - main schedule: # Run every Monday at 9:00 AM UTC - cron: '0 9 * * 1' @@ -46,17 +50,68 @@ jobs: OCAML_BRANCH: ${{ github.event.inputs.branch || 'compatible' }} run: | set +e + # Capture output to file ./.github/scripts/check-ocaml-refs.sh \ --repo "$OCAML_REPO" \ - --branch "$OCAML_BRANCH" + --branch "$OCAML_BRANCH" 2>&1 | tee validation_output.txt exit_code=$? if [ $exit_code -ne 0 ]; then echo "has_issues=true" >> $GITHUB_OUTPUT - exit 0 + else + echo "has_issues=false" >> $GITHUB_OUTPUT fi + # Store output for PR comment + echo "output_file=validation_output.txt" >> $GITHUB_OUTPUT + exit 0 + + - name: Prepare PR comment + if: github.event_name == 'pull_request' + id: prepare-comment + env: + HAS_ISSUES: ${{ steps.check.outputs.has_issues }} + OCAML_REPO: ${{ github.event.inputs.repo || 'https://github.com/MinaProtocol/mina.git' }} + OCAML_BRANCH: ${{ github.event.inputs.branch || 'compatible' }} + run: | + # Determine status message + if [ "$HAS_ISSUES" = "true" ]; then + STATUS_MSG="❌ Validation failed" + else + STATUS_MSG="✓ Validation passed" + fi + + # Create comment body + cat > comment.md < + Click to see full validation output + + \`\`\` + COMMENT_EOF + + # Append validation output + cat validation_output.txt >> comment.md + + # Close the code block and details + cat >> comment.md <<'COMMENT_EOF' + ``` + + + COMMENT_EOF + + - name: Post PR comment with validation results + if: github.event_name == 'pull_request' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body-path: comment.md - name: Update references if stale - if: steps.check.outputs.has_issues != 'true' + if: steps.check.outputs.has_issues != 'true' && github.event_name != 'pull_request' env: OCAML_REPO: ${{ github.event.inputs.repo || 'https://github.com/MinaProtocol/mina.git' }} OCAML_BRANCH: ${{ github.event.inputs.branch || 'compatible' }} @@ -67,6 +122,7 @@ jobs: --update - name: Check for changes + if: github.event_name != 'pull_request' id: changes run: | if git diff --quiet; then @@ -76,7 +132,7 @@ jobs: fi - name: Create Pull Request - if: steps.changes.outputs.has_changes == 'true' + if: steps.changes.outputs.has_changes == 'true' && github.event_name != 'pull_request' uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -118,6 +174,8 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.check.outputs.has_issues }}" == "true" ]; then echo "❌ Validation failed - some OCaml references are invalid" >> $GITHUB_STEP_SUMMARY + elif [ "${{ github.event_name }}" == "pull_request" ]; then + echo "✓ Validation completed - results posted as PR comment" >> $GITHUB_STEP_SUMMARY elif [ "${{ steps.changes.outputs.has_changes }}" == "true" ]; then echo "✓ Validation passed - created PR to update stale references" >> $GITHUB_STEP_SUMMARY else From edf3120015f6cc7e3b12d71a9ec0821d49d61729 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:13:48 +0200 Subject: [PATCH 07/26] Ledger: update OCaml references in mod.rs --- .../src/scan_state/transaction_logic/mod.rs | 98 ++++++++++++++----- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 95f9fff7c..3b844f4a0 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -61,7 +61,7 @@ pub use transaction_union_payload::{ }; /// OCaml reference: src/lib/mina_base/transaction_status.ml L:9-51 -/// Commit: 2ee6e004ba8c6a0541056076aab22ea162f7eb3a +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 /// Last verified: 2025-10-08 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub enum TransactionFailure { @@ -179,7 +179,7 @@ impl Display for TransactionFailure { } /// OCaml reference: src/lib/mina_base/transaction_status.ml L:452-454 -/// Commit: 2ee6e004ba8c6a0541056076aab22ea162f7eb3a +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 /// Last verified: 2025-10-08 #[derive(SerdeYojsonEnum, Debug, Clone, PartialEq, Eq)] pub enum TransactionStatus { @@ -197,7 +197,7 @@ impl TransactionStatus { } /// OCaml reference: src/lib/mina_base/with_status.ml L:6-10 -/// Commit: 2ee6e004ba8c6a0541056076aab22ea162f7eb3a +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 /// Last verified: 2025-10-08 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] pub struct WithStatus { @@ -265,7 +265,9 @@ where } } -/// +/// OCaml reference: src/lib/mina_base/fee_transfer.ml L:76-80 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 #[derive(Debug, Clone, PartialEq)] pub struct SingleFeeTransfer { pub receiver_pk: CompressedPubKey, @@ -290,7 +292,9 @@ impl SingleFeeTransfer { } } -/// +/// OCaml reference: src/lib/mina_base/fee_transfer.ml L:68-69 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 #[derive(Debug, Clone, PartialEq)] pub struct FeeTransfer(pub(super) OneOrTwo); @@ -318,7 +322,9 @@ impl FeeTransfer { }) } - /// + /// OCaml reference: src/lib/mina_base/fee_transfer.ml L:110-114 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn fee_excess(&self) -> Result { let one_or_two = self.0.map(|SingleFeeTransfer { fee, fee_token, .. }| { (fee_token.clone(), Signed::::of_unsigned(*fee).negate()) @@ -326,7 +332,9 @@ impl FeeTransfer { FeeExcess::of_one_or_two(one_or_two) } - /// + /// OCaml reference: src/lib/mina_base/fee_transfer.ml L:85-97 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn of_singles(singles: OneOrTwo) -> Result { match singles { OneOrTwo::One(a) => Ok(Self(OneOrTwo::One(a))), @@ -365,7 +373,9 @@ impl CoinbaseFeeTransfer { } } -/// +/// OCaml reference: src/lib/mina_base/coinbase.ml L:17-21 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 #[derive(Debug, Clone, PartialEq)] pub struct Coinbase { pub receiver: CompressedPubKey, @@ -407,7 +417,9 @@ impl Coinbase { } } - /// + /// OCaml reference: src/lib/mina_base/coinbase.ml L:92-100 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 fn expected_supply_increase(&self) -> Result { let Self { amount, @@ -429,12 +441,16 @@ impl Coinbase { self.expected_supply_increase().map(|_| FeeExcess::empty()) } - /// + /// OCaml reference: src/lib/mina_base/coinbase.ml L:39-39 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn receiver(&self) -> AccountId { AccountId::new(self.receiver.clone(), TokenId::default()) } - /// + /// OCaml reference: src/lib/mina_base/coinbase.ml L:51-65 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn account_access_statuses( &self, status: &TransactionStatus, @@ -455,7 +471,9 @@ impl Coinbase { ids } - /// + /// OCaml reference: src/lib/mina_base/coinbase.ml L:67-69 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn accounts_referenced(&self) -> Vec { self.account_access_statuses(&TransactionStatus::Applied) .into_iter() @@ -558,7 +576,9 @@ impl Memo { self.0.as_slice() } - /// + /// OCaml reference: src/lib/mina_base/signed_command_memo.ml L:156-156 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn dummy() -> Self { // TODO Self([0; 34]) @@ -585,7 +605,9 @@ impl Memo { Self(s.into_bytes().try_into().unwrap()) } - /// + /// OCaml reference: src/lib/mina_base/signed_command_memo.ml L:117-120 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 fn create_by_digesting_string_exn(s: &str) -> Self { if s.len() > Self::MAX_DIGESTIBLE_STRING_LENGTH { panic!("Too_long_digestible_string"); @@ -606,7 +628,9 @@ impl Memo { Self(memo) } - /// + /// OCaml reference: src/lib/mina_base/signed_command_memo.ml L:205-207 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn gen() -> Self { use rand::distributions::{Alphanumeric, DistString}; let random_string = Alphanumeric.sample_string(&mut rand::thread_rng(), 50); @@ -667,7 +691,9 @@ impl binprot::BinProtRead for UserCommand { } impl UserCommand { - /// + /// OCaml reference: src/lib/mina_base/user_command.ml L:239 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn account_access_statuses( &self, status: &TransactionStatus, @@ -678,7 +704,9 @@ impl UserCommand { } } - /// + /// OCaml reference: src/lib/mina_base/user_command.ml L:306-307 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn accounts_referenced(&self) -> Vec { self.account_access_statuses(&TransactionStatus::Applied) .into_iter() @@ -714,7 +742,9 @@ impl UserCommand { self.applicable_at_nonce().succ() } - /// + /// OCaml reference: src/lib/mina_base/user_command.ml L:283-287 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn fee(&self) -> Fee { match self { UserCommand::SignedCommand(cmd) => cmd.fee(), @@ -748,7 +778,9 @@ impl UserCommand { } } - /// + /// OCaml reference: src/lib/mina_base/user_command.ml L:388-401 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn to_valid_unsafe(self) -> valid::UserCommand { match self { UserCommand::SignedCommand(cmd) => valid::UserCommand::SignedCommand(cmd), @@ -760,7 +792,9 @@ impl UserCommand { } } - /// + /// OCaml reference: src/lib/mina_base/user_command.ml L:220-226 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn to_verifiable( &self, status: &TransactionStatus, @@ -983,7 +1017,9 @@ impl Transaction { } } - /// + /// OCaml reference: src/lib/transaction/transaction.ml L:98-110 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn public_keys(&self) -> Vec { use Transaction::*; use UserCommand::*; @@ -998,7 +1034,9 @@ impl Transaction { } } - /// + /// OCaml reference: src/lib/transaction/transaction.ml L:112-124 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn account_access_statuses( &self, status: &TransactionStatus, @@ -1017,7 +1055,9 @@ impl Transaction { } } - /// + /// OCaml reference: src/lib/transaction/transaction.ml L:126-128 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn accounts_referenced(&self) -> Vec { self.account_access_statuses(&TransactionStatus::Applied) .into_iter() @@ -1094,11 +1134,15 @@ pub mod for_tests { } } - /// + /// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2285-2285 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 #[derive(Debug)] pub struct InitLedger(pub Vec<(Keypair, u64)>); - /// + /// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2351-2356 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 #[derive(Debug)] pub struct TransactionSpec { pub fee: Fee, @@ -1107,7 +1151,9 @@ pub mod for_tests { pub amount: Amount, } - /// + /// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2407 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 #[derive(Debug)] pub struct TestSpec { pub init_ledger: InitLedger, From c4646cf84b80d94e5ab7224cb32706ba98ae499a Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:16:42 +0200 Subject: [PATCH 08/26] CHANGELOG: add description for patch 1525 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 118808c5a..2f3e4b173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1493](https://github.com/o1-labs/mina-rust/pull/1509)) - **Ledger/ZkAppCommand**: show how to build a ZkAppCommand from scratch, with dummy values ([#1514](https://github.com/o1-labs/mina-rust/pull/1514)) +- **CI/Documentation**: add a script to check the references to the OCaml code + ([#1525](https://github.com/o1-labs/mina-rust/pull/1525)). ### Changed @@ -60,6 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1513](https://github.com/o1-labs/mina-rust/pull/1513)) - **scan_state**: refactorize `transaction_logic.rs` in smaller modules ([#1515](https://github.com/o1-labs/mina-rust/pull/1515)) +- **ledger/scan_state/transaction_logic**: update OCaml references in `mod.rs` + ([#1525](https://github.com/o1-labs/mina-rust/pull/1525)) ## v0.17.0 From 8e7f822df06a8c67c1280d506f669d1a7bffdfe0 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:18:07 +0200 Subject: [PATCH 09/26] GH/scripts: update links to doc guidelines --- .github/scripts/verify-code-references.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/verify-code-references.sh b/.github/scripts/verify-code-references.sh index 92fcbe3be..8b86d20f0 100755 --- a/.github/scripts/verify-code-references.sh +++ b/.github/scripts/verify-code-references.sh @@ -221,7 +221,7 @@ Please follow this workflow: 2. Create a follow-up PR with the documentation updates that reference the merged code 3. The verification will pass once the code is available on \`develop\` -See the [documentation guidelines](https://o1-labs.github.io/mina-rust/developers/documentation-guidelines) for more information about the two-PR workflow. +See the [documentation guidelines](https://o1-labs.github.io/mina-rust/docs/developers/documentation-guidelines) for more information about the two-PR workflow. EOF echo "" echo "PR comment written to: ${COMMENT_FILE}" From 16f4051edf79146d19de27e4be69fc0979b07539 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:22:59 +0200 Subject: [PATCH 10/26] CI/check-ocaml-refs: update the existing commits instead of creating --- .github/workflows/check-ocaml-refs.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-ocaml-refs.yaml b/.github/workflows/check-ocaml-refs.yaml index 6a3fad764..dbc43d314 100644 --- a/.github/workflows/check-ocaml-refs.yaml +++ b/.github/workflows/check-ocaml-refs.yaml @@ -79,8 +79,9 @@ jobs: STATUS_MSG="✓ Validation passed" fi - # Create comment body + # Create comment body with hidden identifier cat > comment.md < ## OCaml Reference Validation Results **Repository**: ${OCAML_REPO} @@ -103,12 +104,23 @@ jobs: COMMENT_EOF + - name: Find existing OCaml validation comment + if: github.event_name == 'pull_request' + uses: peter-evans/find-comment@v3 + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: '' + - name: Post PR comment with validation results if: github.event_name == 'pull_request' uses: peter-evans/create-or-update-comment@v4 with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body-path: comment.md + edit-mode: replace - name: Update references if stale if: steps.check.outputs.has_issues != 'true' && github.event_name != 'pull_request' From e9852db2f5463b840214893ef411bdf6cb4ff73e Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:29:17 +0200 Subject: [PATCH 11/26] CI/GH actions: update find-comments version to v4 --- .github/workflows/check-ocaml-refs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-ocaml-refs.yaml b/.github/workflows/check-ocaml-refs.yaml index dbc43d314..47df74483 100644 --- a/.github/workflows/check-ocaml-refs.yaml +++ b/.github/workflows/check-ocaml-refs.yaml @@ -106,7 +106,7 @@ jobs: - name: Find existing OCaml validation comment if: github.event_name == 'pull_request' - uses: peter-evans/find-comment@v3 + uses: peter-evans/find-comment@v4 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} From 53afdd1cb05be3b9b7af8e3364535ed4328297d1 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:30:03 +0200 Subject: [PATCH 12/26] CI/GH actions: update create-or-update-comment to v5 --- .github/workflows/check-ocaml-refs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-ocaml-refs.yaml b/.github/workflows/check-ocaml-refs.yaml index 47df74483..12184e24c 100644 --- a/.github/workflows/check-ocaml-refs.yaml +++ b/.github/workflows/check-ocaml-refs.yaml @@ -115,7 +115,7 @@ jobs: - name: Post PR comment with validation results if: github.event_name == 'pull_request' - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@v5 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} From 5e939f4d2278170b6efcb797be9437eb950f171e Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:52:26 +0200 Subject: [PATCH 13/26] Ledger/tx-logic: move for_tests into a submodule --- .../scan_state/transaction_logic/for_tests.rs | 292 ++++++++++++++++++ .../src/scan_state/transaction_logic/mod.rs | 289 +---------------- 2 files changed, 293 insertions(+), 288 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/for_tests.rs diff --git a/ledger/src/scan_state/transaction_logic/for_tests.rs b/ledger/src/scan_state/transaction_logic/for_tests.rs new file mode 100644 index 000000000..e37c9f16c --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/for_tests.rs @@ -0,0 +1,292 @@ +use super::{ + zkapp_command, Account, AccountId, Amount, Balance, Fee, Memo, Nonce, TokenId, + VerificationKeyWire, +}; +use crate::{ + gen_keypair, + scan_state::{currency::Magnitude, parallel_scan::ceil_log2}, + sparse_ledger::LedgerIntf, + AuthRequired, BaseLedger, Mask, Permissions, VerificationKey, ZkAppAccount, + TXN_VERSION_CURRENT, +}; +use mina_curves::pasta::Fp; +use mina_signer::{CompressedPubKey, Keypair}; +use rand::Rng; +use std::collections::{HashMap, HashSet}; + +const MIN_INIT_BALANCE: u64 = 8000000000; +const MAX_INIT_BALANCE: u64 = 8000000000000; +const NUM_ACCOUNTS: u64 = 10; +const NUM_TRANSACTIONS: u64 = 10; +const DEPTH: u64 = ceil_log2(NUM_ACCOUNTS + NUM_TRANSACTIONS); + +/// Use this for tests only +/// Hashmaps are not deterministic +#[derive(Debug, PartialEq, Eq)] +pub struct HashableKeypair(pub Keypair); + +impl std::hash::Hash for HashableKeypair { + fn hash(&self, state: &mut H) { + let compressed = self.0.public.into_compressed(); + HashableCompressedPubKey(compressed).hash(state); + } +} + +/// Use this for tests only +/// Hashmaps are not deterministic +#[derive(Clone, Debug, Eq, derive_more::From)] +pub struct HashableCompressedPubKey(pub CompressedPubKey); + +impl PartialEq for HashableCompressedPubKey { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl std::hash::Hash for HashableCompressedPubKey { + fn hash(&self, state: &mut H) { + self.0.x.hash(state); + self.0.is_odd.hash(state); + } +} + +impl PartialOrd for HashableCompressedPubKey { + fn partial_cmp(&self, other: &Self) -> Option { + match self.0.x.partial_cmp(&other.0.x) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + }; + self.0.is_odd.partial_cmp(&other.0.is_odd) + } +} + +/// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2285-2285 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 +#[derive(Debug)] +pub struct InitLedger(pub Vec<(Keypair, u64)>); + +/// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2351-2356 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 +#[derive(Debug)] +pub struct TransactionSpec { + pub fee: Fee, + pub sender: (Keypair, Nonce), + pub receiver: CompressedPubKey, + pub amount: Amount, +} + +/// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2407 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 +#[derive(Debug)] +pub struct TestSpec { + pub init_ledger: InitLedger, + pub specs: Vec, +} + +impl InitLedger { + pub fn init(&self, zkapp: Option, ledger: &mut impl LedgerIntf) { + let zkapp = zkapp.unwrap_or(true); + + self.0.iter().for_each(|(kp, amount)| { + let (_tag, mut account, loc) = ledger + .get_or_create(&AccountId::new( + kp.public.into_compressed(), + TokenId::default(), + )) + .unwrap(); + + use AuthRequired::Either; + let permissions = Permissions { + edit_state: Either, + access: AuthRequired::None, + send: Either, + receive: AuthRequired::None, + set_delegate: Either, + set_permissions: Either, + set_verification_key: crate::SetVerificationKey { + auth: Either, + txn_version: TXN_VERSION_CURRENT, + }, + set_zkapp_uri: Either, + edit_action_state: Either, + set_token_symbol: Either, + increment_nonce: Either, + set_voting_for: Either, + set_timing: Either, + }; + + let zkapp = if zkapp { + let zkapp = ZkAppAccount { + verification_key: Some(VerificationKeyWire::new( + crate::dummy::trivial_verification_key(), + )), + ..Default::default() + }; + + Some(zkapp.into()) + } else { + None + }; + + account.balance = Balance::from_u64(*amount); + account.permissions = permissions; + account.zkapp = zkapp; + + ledger.set(&loc, account); + }); + } + + pub fn gen() -> Self { + let mut rng = rand::thread_rng(); + + let mut tbl = HashSet::with_capacity(256); + + let init = (0..NUM_ACCOUNTS) + .map(|_| { + let kp = loop { + let keypair = gen_keypair(); + let compressed = keypair.public.into_compressed(); + if !tbl.contains(&HashableCompressedPubKey(compressed)) { + break keypair; + } + }; + + let amount = rng.gen_range(MIN_INIT_BALANCE..MAX_INIT_BALANCE); + tbl.insert(HashableCompressedPubKey(kp.public.into_compressed())); + (kp, amount) + }) + .collect(); + + Self(init) + } +} + +impl TransactionSpec { + pub fn gen(init_ledger: &InitLedger, nonces: &mut HashMap) -> Self { + let mut rng = rand::thread_rng(); + + let pk = |(kp, _): (Keypair, u64)| kp.public.into_compressed(); + + let receiver_is_new: bool = rng.gen(); + + let mut gen_index = || rng.gen_range(0..init_ledger.0.len().checked_sub(1).unwrap()); + + let receiver_index = if receiver_is_new { + None + } else { + Some(gen_index()) + }; + + let receiver = match receiver_index { + None => gen_keypair().public.into_compressed(), + Some(i) => pk(init_ledger.0[i].clone()), + }; + + let sender = { + let i = match receiver_index { + None => gen_index(), + Some(j) => loop { + let i = gen_index(); + if i != j { + break i; + } + }, + }; + init_ledger.0[i].0.clone() + }; + + let nonce = nonces + .get(&HashableKeypair(sender.clone())) + .cloned() + .unwrap(); + + let amount = Amount::from_u64(rng.gen_range(1_000_000..100_000_000)); + let fee = Fee::from_u64(rng.gen_range(1_000_000..100_000_000)); + + let old = nonces.get_mut(&HashableKeypair(sender.clone())).unwrap(); + *old = old.incr(); + + Self { + fee, + sender: (sender, nonce), + receiver, + amount, + } + } +} + +impl TestSpec { + fn mk_gen(num_transactions: Option) -> TestSpec { + let num_transactions = num_transactions.unwrap_or(NUM_TRANSACTIONS); + + let init_ledger = InitLedger::gen(); + + let mut map = init_ledger + .0 + .iter() + .map(|(kp, _)| (HashableKeypair(kp.clone()), Nonce::zero())) + .collect(); + + let specs = (0..num_transactions) + .map(|_| TransactionSpec::gen(&init_ledger, &mut map)) + .collect(); + + Self { init_ledger, specs } + } + + pub fn gen() -> Self { + Self::mk_gen(Some(NUM_TRANSACTIONS)) + } +} + +#[derive(Debug)] +pub struct UpdateStatesSpec { + pub fee: Fee, + pub sender: (Keypair, Nonce), + pub fee_payer: Option<(Keypair, Nonce)>, + pub receivers: Vec<(CompressedPubKey, Amount)>, + pub amount: Amount, + pub zkapp_account_keypairs: Vec, + pub memo: Memo, + pub new_zkapp_account: bool, + pub snapp_update: zkapp_command::Update, + // Authorization for the update being performed + pub current_auth: AuthRequired, + pub actions: Vec>, + pub events: Vec>, + pub call_data: Fp, + pub preconditions: Option, +} + +pub fn trivial_zkapp_account( + permissions: Option>, + vk: VerificationKey, + pk: CompressedPubKey, +) -> Account { + let id = AccountId::new(pk, TokenId::default()); + let mut account = Account::create_with(id, Balance::from_u64(1_000_000_000_000_000)); + account.permissions = permissions.unwrap_or_else(Permissions::user_default); + account.zkapp = Some( + ZkAppAccount { + verification_key: Some(VerificationKeyWire::new(vk)), + ..Default::default() + } + .into(), + ); + account +} + +pub fn create_trivial_zkapp_account( + permissions: Option>, + vk: VerificationKey, + ledger: &mut Mask, + pk: CompressedPubKey, +) { + let id = AccountId::new(pk.clone(), TokenId::default()); + let account = trivial_zkapp_account(permissions, vk, pk); + assert!(BaseLedger::location_of_account(ledger, &id).is_none()); + ledger.get_or_create_account(id, account).unwrap(); +} diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 3b844f4a0..36e7e2ef0 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1077,291 +1077,4 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { } #[cfg(any(test, feature = "fuzzing"))] -pub mod for_tests { - use mina_signer::Keypair; - use rand::Rng; - - use crate::{ - gen_keypair, scan_state::parallel_scan::ceil_log2, AuthRequired, Mask, Permissions, - VerificationKey, ZkAppAccount, TXN_VERSION_CURRENT, - }; - - use super::*; - - const MIN_INIT_BALANCE: u64 = 8000000000; - const MAX_INIT_BALANCE: u64 = 8000000000000; - const NUM_ACCOUNTS: u64 = 10; - const NUM_TRANSACTIONS: u64 = 10; - const DEPTH: u64 = ceil_log2(NUM_ACCOUNTS + NUM_TRANSACTIONS); - - /// Use this for tests only - /// Hashmaps are not deterministic - #[derive(Debug, PartialEq, Eq)] - pub struct HashableKeypair(pub Keypair); - - impl std::hash::Hash for HashableKeypair { - fn hash(&self, state: &mut H) { - let compressed = self.0.public.into_compressed(); - HashableCompressedPubKey(compressed).hash(state); - } - } - - /// Use this for tests only - /// Hashmaps are not deterministic - #[derive(Clone, Debug, Eq, derive_more::From)] - pub struct HashableCompressedPubKey(pub CompressedPubKey); - - impl PartialEq for HashableCompressedPubKey { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } - } - - impl std::hash::Hash for HashableCompressedPubKey { - fn hash(&self, state: &mut H) { - self.0.x.hash(state); - self.0.is_odd.hash(state); - } - } - - impl PartialOrd for HashableCompressedPubKey { - fn partial_cmp(&self, other: &Self) -> Option { - match self.0.x.partial_cmp(&other.0.x) { - Some(core::cmp::Ordering::Equal) => {} - ord => return ord, - }; - self.0.is_odd.partial_cmp(&other.0.is_odd) - } - } - - /// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2285-2285 - /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 - /// Last verified: 2025-10-10 - #[derive(Debug)] - pub struct InitLedger(pub Vec<(Keypair, u64)>); - - /// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2351-2356 - /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 - /// Last verified: 2025-10-10 - #[derive(Debug)] - pub struct TransactionSpec { - pub fee: Fee, - pub sender: (Keypair, Nonce), - pub receiver: CompressedPubKey, - pub amount: Amount, - } - - /// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2407 - /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 - /// Last verified: 2025-10-10 - #[derive(Debug)] - pub struct TestSpec { - pub init_ledger: InitLedger, - pub specs: Vec, - } - - impl InitLedger { - pub fn init(&self, zkapp: Option, ledger: &mut impl LedgerIntf) { - let zkapp = zkapp.unwrap_or(true); - - self.0.iter().for_each(|(kp, amount)| { - let (_tag, mut account, loc) = ledger - .get_or_create(&AccountId::new( - kp.public.into_compressed(), - TokenId::default(), - )) - .unwrap(); - - use AuthRequired::Either; - let permissions = Permissions { - edit_state: Either, - access: AuthRequired::None, - send: Either, - receive: AuthRequired::None, - set_delegate: Either, - set_permissions: Either, - set_verification_key: crate::SetVerificationKey { - auth: Either, - txn_version: TXN_VERSION_CURRENT, - }, - set_zkapp_uri: Either, - edit_action_state: Either, - set_token_symbol: Either, - increment_nonce: Either, - set_voting_for: Either, - set_timing: Either, - }; - - let zkapp = if zkapp { - let zkapp = ZkAppAccount { - verification_key: Some(VerificationKeyWire::new( - crate::dummy::trivial_verification_key(), - )), - ..Default::default() - }; - - Some(zkapp.into()) - } else { - None - }; - - account.balance = Balance::from_u64(*amount); - account.permissions = permissions; - account.zkapp = zkapp; - - ledger.set(&loc, account); - }); - } - - pub fn gen() -> Self { - let mut rng = rand::thread_rng(); - - let mut tbl = HashSet::with_capacity(256); - - let init = (0..NUM_ACCOUNTS) - .map(|_| { - let kp = loop { - let keypair = gen_keypair(); - let compressed = keypair.public.into_compressed(); - if !tbl.contains(&HashableCompressedPubKey(compressed)) { - break keypair; - } - }; - - let amount = rng.gen_range(MIN_INIT_BALANCE..MAX_INIT_BALANCE); - tbl.insert(HashableCompressedPubKey(kp.public.into_compressed())); - (kp, amount) - }) - .collect(); - - Self(init) - } - } - - impl TransactionSpec { - pub fn gen(init_ledger: &InitLedger, nonces: &mut HashMap) -> Self { - let mut rng = rand::thread_rng(); - - let pk = |(kp, _): (Keypair, u64)| kp.public.into_compressed(); - - let receiver_is_new: bool = rng.gen(); - - let mut gen_index = || rng.gen_range(0..init_ledger.0.len().checked_sub(1).unwrap()); - - let receiver_index = if receiver_is_new { - None - } else { - Some(gen_index()) - }; - - let receiver = match receiver_index { - None => gen_keypair().public.into_compressed(), - Some(i) => pk(init_ledger.0[i].clone()), - }; - - let sender = { - let i = match receiver_index { - None => gen_index(), - Some(j) => loop { - let i = gen_index(); - if i != j { - break i; - } - }, - }; - init_ledger.0[i].0.clone() - }; - - let nonce = nonces - .get(&HashableKeypair(sender.clone())) - .cloned() - .unwrap(); - - let amount = Amount::from_u64(rng.gen_range(1_000_000..100_000_000)); - let fee = Fee::from_u64(rng.gen_range(1_000_000..100_000_000)); - - let old = nonces.get_mut(&HashableKeypair(sender.clone())).unwrap(); - *old = old.incr(); - - Self { - fee, - sender: (sender, nonce), - receiver, - amount, - } - } - } - - impl TestSpec { - fn mk_gen(num_transactions: Option) -> TestSpec { - let num_transactions = num_transactions.unwrap_or(NUM_TRANSACTIONS); - - let init_ledger = InitLedger::gen(); - - let mut map = init_ledger - .0 - .iter() - .map(|(kp, _)| (HashableKeypair(kp.clone()), Nonce::zero())) - .collect(); - - let specs = (0..num_transactions) - .map(|_| TransactionSpec::gen(&init_ledger, &mut map)) - .collect(); - - Self { init_ledger, specs } - } - - pub fn gen() -> Self { - Self::mk_gen(Some(NUM_TRANSACTIONS)) - } - } - - #[derive(Debug)] - pub struct UpdateStatesSpec { - pub fee: Fee, - pub sender: (Keypair, Nonce), - pub fee_payer: Option<(Keypair, Nonce)>, - pub receivers: Vec<(CompressedPubKey, Amount)>, - pub amount: Amount, - pub zkapp_account_keypairs: Vec, - pub memo: Memo, - pub new_zkapp_account: bool, - pub snapp_update: zkapp_command::Update, - // Authorization for the update being performed - pub current_auth: AuthRequired, - pub actions: Vec>, - pub events: Vec>, - pub call_data: Fp, - pub preconditions: Option, - } - - pub fn trivial_zkapp_account( - permissions: Option>, - vk: VerificationKey, - pk: CompressedPubKey, - ) -> Account { - let id = AccountId::new(pk, TokenId::default()); - let mut account = Account::create_with(id, Balance::from_u64(1_000_000_000_000_000)); - account.permissions = permissions.unwrap_or_else(Permissions::user_default); - account.zkapp = Some( - ZkAppAccount { - verification_key: Some(VerificationKeyWire::new(vk)), - ..Default::default() - } - .into(), - ); - account - } - - pub fn create_trivial_zkapp_account( - permissions: Option>, - vk: VerificationKey, - ledger: &mut Mask, - pk: CompressedPubKey, - ) { - let id = AccountId::new(pk.clone(), TokenId::default()); - let account = trivial_zkapp_account(permissions, vk, pk); - assert!(BaseLedger::location_of_account(ledger, &id).is_none()); - ledger.get_or_create_account(id, account).unwrap(); - } -} +pub mod for_tests; From 5cfd6fc5dc36e2df2a706019234bc19c35115e8a Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 12:14:51 +0200 Subject: [PATCH 14/26] CHANGELOG: add description for patch 1527 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3e4b173..ab2d4af2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1515](https://github.com/o1-labs/mina-rust/pull/1515)) - **ledger/scan_state/transaction_logic**: update OCaml references in `mod.rs` ([#1525](https://github.com/o1-labs/mina-rust/pull/1525)) +- **ledger/scan_state/transaction_logic**: move submodule `for_tests` into a + new file `zkapp_command/for_tests.rs` + ([#1527](https://github.com/o1-labs/mina-rust/pull/1527)). ## v0.17.0 From 5dcd8d752a8f92b825e30232100d72e706146ad7 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 12:05:05 +0200 Subject: [PATCH 15/26] Ledger/scan_state/tx-logic: split zkapp_command into submodules --- .../zkapp_command/from_applied_sequence.rs | 32 ++ .../zkapp_command/from_unapplied_sequence.rs | 32 ++ .../mod.rs} | 294 ++---------------- .../transaction_logic/zkapp_command/valid.rs | 36 +++ .../zkapp_command/verifiable.rs | 157 ++++++++++ .../zkapp_command/zkapp_weight.rs | 18 ++ 6 files changed, 295 insertions(+), 274 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs rename ledger/src/scan_state/transaction_logic/{zkapp_command.rs => zkapp_command/mod.rs} (91%) create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/valid.rs create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs new file mode 100644 index 000000000..3f00b3f58 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs @@ -0,0 +1,32 @@ +use mina_curves::pasta::Fp; +use std::collections::HashMap; + +use super::{AccountId, ToVerifiableCache, ToVerifiableStrategy, VerificationKeyWire}; + +pub struct Cache { + cache: HashMap, +} + +impl Cache { + pub fn new(cache: HashMap) -> Self { + Self { cache } + } +} + +impl ToVerifiableCache for Cache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { + self.cache + .get(account_id) + .filter(|vk| &vk.hash() == vk_hash) + } + + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { + self.cache.insert(account_id, vk); + } +} + +pub struct FromAppliedSequence; + +impl ToVerifiableStrategy for FromAppliedSequence { + type Cache = Cache; +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs new file mode 100644 index 000000000..aed8f65ab --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs @@ -0,0 +1,32 @@ +use mina_curves::pasta::Fp; +use std::collections::HashMap; + +use super::{AccountId, ToVerifiableCache, ToVerifiableStrategy, VerificationKeyWire}; + +pub struct Cache { + cache: HashMap>, +} + +impl Cache { + pub fn new(cache: HashMap>) -> Self { + Self { cache } + } +} + +impl ToVerifiableCache for Cache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { + let vks = self.cache.get(account_id)?; + vks.get(vk_hash) + } + + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { + let vks = self.cache.entry(account_id).or_default(); + vks.insert(vk.hash(), vk); + } +} + +pub struct FromUnappliedSequence; + +impl ToVerifiableStrategy for FromUnappliedSequence { + type Cache = Cache; +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs similarity index 91% rename from ledger/src/scan_state/transaction_logic/zkapp_command.rs rename to ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs index 66764af1b..66a9b6b78 100644 --- a/ledger/src/scan_state/transaction_logic/zkapp_command.rs +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs @@ -19,7 +19,6 @@ use crate::{ fee_excess::FeeExcess, GenesisConstant, GENESIS_CONSTANT, }, - sparse_ledger::LedgerIntf, zkapps::checks::{ZkappCheck, ZkappCheckOps}, AccountId, AuthRequired, ControlTag, MutableFp, MyCow, Permissions, SetVerificationKey, ToInputs, TokenId, TokenSymbol, VerificationKey, VerificationKeyWire, VotingFor, ZkAppAccount, @@ -39,7 +38,13 @@ use poseidon::hash::{ Inputs, }; use rand::{seq::SliceRandom, Rng}; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; + +pub mod from_applied_sequence; +pub mod from_unapplied_sequence; +pub mod valid; +pub mod verifiable; +pub mod zkapp_weight; #[derive(Debug, Clone, PartialEq)] pub struct Event(pub Vec); @@ -48,9 +53,11 @@ impl Event { pub fn empty() -> Self { Self(Vec::new()) } + pub fn hash(&self) -> Fp { hash_with_kimchi(&MINA_ZKAPP_EVENT, &self.0[..]) } + pub fn len(&self) -> usize { let Self(list) = self; list.len() @@ -86,23 +93,30 @@ pub trait MakeEvents { const DERIVER_NAME: (); // Unused here for now fn get_salt_phrase() -> &'static LazyParam; + fn get_hash_prefix() -> &'static LazyParam; + fn events(&self) -> &[Event]; + fn empty_hash() -> Fp; } /// impl MakeEvents for Events { const DERIVER_NAME: () = (); + fn get_salt_phrase() -> &'static LazyParam { &NO_INPUT_MINA_ZKAPP_EVENTS_EMPTY } + fn get_hash_prefix() -> &'static poseidon::hash::LazyParam { &MINA_ZKAPP_EVENTS } + fn events(&self) -> &[Event] { self.0.as_slice() } + fn empty_hash() -> Fp { cache_one!(Fp, events_to_field(&Events::empty())) } @@ -111,15 +125,19 @@ impl MakeEvents for Events { /// impl MakeEvents for Actions { const DERIVER_NAME: () = (); + fn get_salt_phrase() -> &'static LazyParam { &NO_INPUT_MINA_ZKAPP_ACTIONS_EMPTY } + fn get_hash_prefix() -> &'static poseidon::hash::LazyParam { &MINA_ZKAPP_SEQ_EVENTS } + fn events(&self) -> &[Event] { self.0.as_slice() } + fn empty_hash() -> Fp { cache_one!(Fp, events_to_field(&Actions::empty())) } @@ -2944,196 +2962,6 @@ impl ZkAppCommand { } } -pub mod verifiable { - use mina_p2p_messages::v2::MinaBaseZkappCommandVerifiableStableV1; - - use super::*; - - #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] - #[serde(try_from = "MinaBaseZkappCommandVerifiableStableV1")] - #[serde(into = "MinaBaseZkappCommandVerifiableStableV1")] - pub struct ZkAppCommand { - pub fee_payer: FeePayer, - pub account_updates: CallForest<(AccountUpdate, Option)>, - pub memo: Memo, - } - - fn ok_if_vk_hash_expected( - got: VerificationKeyWire, - expected: Fp, - ) -> Result { - if got.hash() == expected { - return Ok(got.clone()); - } - Err(format!( - "Expected vk hash doesn't match hash in vk we received\ - expected: {:?}\ - got: {:?}", - expected, got - )) - } - - pub fn find_vk_via_ledger( - ledger: L, - expected_vk_hash: Fp, - account_id: &AccountId, - ) -> Result - where - L: LedgerIntf + Clone, - { - let vk = ledger - .location_of_account(account_id) - .and_then(|location| ledger.get(&location)) - .and_then(|account| { - account - .zkapp - .as_ref() - .and_then(|zkapp| zkapp.verification_key.clone()) - }); - - match vk { - Some(vk) => ok_if_vk_hash_expected(vk, expected_vk_hash), - None => Err(format!( - "No verification key found for proved account update\ - account_id: {:?}", - account_id - )), - } - } - - fn check_authorization(p: &AccountUpdate) -> Result<(), String> { - use AuthorizationKind as AK; - use Control as C; - - match (&p.authorization, &p.body.authorization_kind) { - (C::NoneGiven, AK::NoneGiven) - | (C::Proof(_), AK::Proof(_)) - | (C::Signature(_), AK::Signature) => Ok(()), - _ => Err(format!( - "Authorization kind does not match the authorization\ - expected={:#?}\ - got={:#?}", - p.body.authorization_kind, p.authorization - )), - } - } - - /// Ensures that there's a verification_key available for all account_updates - /// and creates a valid command associating the correct keys with each - /// account_id. - /// - /// If an account_update replaces the verification_key (or deletes it), - /// subsequent account_updates use the replaced key instead of looking in the - /// ledger for the key (ie set by a previous transaction). - pub fn create( - zkapp: &super::ZkAppCommand, - is_failed: bool, - find_vk: impl Fn(Fp, &AccountId) -> Result, - ) -> Result { - let super::ZkAppCommand { - fee_payer, - account_updates, - memo, - } = zkapp; - - let mut tbl = HashMap::with_capacity(128); - // Keep track of the verification keys that have been set so far - // during this transaction. - let mut vks_overridden: HashMap> = - HashMap::with_capacity(128); - - let account_updates = account_updates.try_map_to(|p| { - let account_id = p.account_id(); - - check_authorization(p)?; - - let result = match (&p.body.authorization_kind, is_failed) { - (AuthorizationKind::Proof(vk_hash), false) => { - let prioritized_vk = { - // only lookup _past_ vk setting, ie exclude the new one we - // potentially set in this account_update (use the non-' - // vks_overrided) . - - match vks_overridden.get(&account_id) { - Some(Some(vk)) => ok_if_vk_hash_expected(vk.clone(), *vk_hash)?, - Some(None) => { - // we explicitly have erased the key - return Err(format!( - "No verification key found for proved account \ - update: the verification key was removed by a \ - previous account update\ - account_id={:?}", - account_id - )); - } - None => { - // we haven't set anything; lookup the vk in the fallback - find_vk(*vk_hash, &account_id)? - } - } - }; - - tbl.insert(account_id, prioritized_vk.hash()); - - Ok((p.clone(), Some(prioritized_vk))) - } - - _ => Ok((p.clone(), None)), - }; - - // NOTE: we only update the overriden map AFTER verifying the update to make sure - // that the verification for the VK update itself is done against the previous VK. - if let SetOrKeep::Set(vk_next) = &p.body.update.verification_key { - vks_overridden.insert(p.account_id().clone(), Some(vk_next.clone())); - } - - result - })?; - - Ok(ZkAppCommand { - fee_payer: fee_payer.clone(), - account_updates, - memo: memo.clone(), - }) - } -} - -pub mod valid { - use crate::scan_state::transaction_logic::zkapp_command::verifiable::create; - - use super::*; - - #[derive(Clone, Debug, PartialEq)] - pub struct ZkAppCommand { - pub zkapp_command: super::ZkAppCommand, - } - - impl ZkAppCommand { - pub fn forget(self) -> super::ZkAppCommand { - self.zkapp_command - } - pub fn forget_ref(&self) -> &super::ZkAppCommand { - &self.zkapp_command - } - } - - /// - pub fn of_verifiable(cmd: verifiable::ZkAppCommand) -> ZkAppCommand { - ZkAppCommand { - zkapp_command: super::ZkAppCommand::of_verifiable(cmd), - } - } - - /// - pub fn to_valid( - zkapp_command: super::ZkAppCommand, - status: &TransactionStatus, - find_vk: impl Fn(Fp, &AccountId) -> Result, - ) -> Result { - create(&zkapp_command, status.is_failed(), find_vk).map(of_verifiable) - } -} - pub struct MaybeWithStatus { pub cmd: T, pub status: Option, @@ -3213,85 +3041,3 @@ pub trait ToVerifiableStrategy { Ok(verified_cmd) } } - -pub mod from_unapplied_sequence { - use super::*; - - pub struct Cache { - cache: HashMap>, - } - - impl Cache { - pub fn new(cache: HashMap>) -> Self { - Self { cache } - } - } - - impl ToVerifiableCache for Cache { - fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { - let vks = self.cache.get(account_id)?; - vks.get(vk_hash) - } - fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { - let vks = self.cache.entry(account_id).or_default(); - vks.insert(vk.hash(), vk); - } - } - - pub struct FromUnappliedSequence; - - impl ToVerifiableStrategy for FromUnappliedSequence { - type Cache = Cache; - } -} - -pub mod from_applied_sequence { - use super::*; - - pub struct Cache { - cache: HashMap, - } - - impl Cache { - pub fn new(cache: HashMap) -> Self { - Self { cache } - } - } - - impl ToVerifiableCache for Cache { - fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { - self.cache - .get(account_id) - .filter(|vk| &vk.hash() == vk_hash) - } - fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { - self.cache.insert(account_id, vk); - } - } - - pub struct FromAppliedSequence; - - impl ToVerifiableStrategy for FromAppliedSequence { - type Cache = Cache; - } -} - -/// -pub mod zkapp_weight { - use crate::scan_state::transaction_logic::zkapp_command::{ - AccountUpdate, CallForest, FeePayer, - }; - - pub fn account_update(_: &AccountUpdate) -> u64 { - 1 - } - pub fn fee_payer(_: &FeePayer) -> u64 { - 1 - } - pub fn account_updates(list: &CallForest) -> u64 { - list.fold(0, |acc, p| acc + account_update(p)) - } - pub fn memo(_: &super::Memo) -> u64 { - 0 - } -} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/valid.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/valid.rs new file mode 100644 index 000000000..85c53993b --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/valid.rs @@ -0,0 +1,36 @@ +use mina_curves::pasta::Fp; + +use super::{ + verifiable::{self, create}, + AccountId, TransactionStatus, VerificationKeyWire, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct ZkAppCommand { + pub zkapp_command: super::ZkAppCommand, +} + +impl ZkAppCommand { + pub fn forget(self) -> super::ZkAppCommand { + self.zkapp_command + } + pub fn forget_ref(&self) -> &super::ZkAppCommand { + &self.zkapp_command + } +} + +/// +pub fn of_verifiable(cmd: verifiable::ZkAppCommand) -> ZkAppCommand { + ZkAppCommand { + zkapp_command: super::ZkAppCommand::of_verifiable(cmd), + } +} + +/// +pub fn to_valid( + zkapp_command: super::ZkAppCommand, + status: &TransactionStatus, + find_vk: impl Fn(Fp, &AccountId) -> Result, +) -> Result { + create(&zkapp_command, status.is_failed(), find_vk).map(of_verifiable) +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs new file mode 100644 index 000000000..7f46db0f1 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs @@ -0,0 +1,157 @@ +use mina_curves::pasta::Fp; +use mina_p2p_messages::v2::MinaBaseZkappCommandVerifiableStableV1; +use std::collections::HashMap; + +use super::{ + AccountId, AccountUpdate, AuthorizationKind, CallForest, Control, FeePayer, Memo, SetOrKeep, + VerificationKeyWire, +}; +use crate::sparse_ledger::LedgerIntf; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(try_from = "MinaBaseZkappCommandVerifiableStableV1")] +#[serde(into = "MinaBaseZkappCommandVerifiableStableV1")] +pub struct ZkAppCommand { + pub fee_payer: FeePayer, + pub account_updates: CallForest<(AccountUpdate, Option)>, + pub memo: Memo, +} + +fn ok_if_vk_hash_expected( + got: VerificationKeyWire, + expected: Fp, +) -> Result { + if got.hash() == expected { + return Ok(got.clone()); + } + Err(format!( + "Expected vk hash doesn't match hash in vk we received\ + expected: {:?}\ + got: {:?}", + expected, got + )) +} + +pub fn find_vk_via_ledger( + ledger: L, + expected_vk_hash: Fp, + account_id: &AccountId, +) -> Result +where + L: LedgerIntf + Clone, +{ + let vk = ledger + .location_of_account(account_id) + .and_then(|location| ledger.get(&location)) + .and_then(|account| { + account + .zkapp + .as_ref() + .and_then(|zkapp| zkapp.verification_key.clone()) + }); + + match vk { + Some(vk) => ok_if_vk_hash_expected(vk, expected_vk_hash), + None => Err(format!( + "No verification key found for proved account update\ + account_id: {:?}", + account_id + )), + } +} + +fn check_authorization(p: &AccountUpdate) -> Result<(), String> { + use AuthorizationKind as AK; + use Control as C; + + match (&p.authorization, &p.body.authorization_kind) { + (C::NoneGiven, AK::NoneGiven) + | (C::Proof(_), AK::Proof(_)) + | (C::Signature(_), AK::Signature) => Ok(()), + _ => Err(format!( + "Authorization kind does not match the authorization\ + expected={:#?}\ + got={:#?}", + p.body.authorization_kind, p.authorization + )), + } +} + +/// Ensures that there's a verification_key available for all account_updates +/// and creates a valid command associating the correct keys with each +/// account_id. +/// +/// If an account_update replaces the verification_key (or deletes it), +/// subsequent account_updates use the replaced key instead of looking in the +/// ledger for the key (ie set by a previous transaction). +pub fn create( + zkapp: &super::ZkAppCommand, + is_failed: bool, + find_vk: impl Fn(Fp, &AccountId) -> Result, +) -> Result { + let super::ZkAppCommand { + fee_payer, + account_updates, + memo, + } = zkapp; + + let mut tbl = HashMap::with_capacity(128); + // Keep track of the verification keys that have been set so far + // during this transaction. + let mut vks_overridden: HashMap> = + HashMap::with_capacity(128); + + let account_updates = account_updates.try_map_to(|p| { + let account_id = p.account_id(); + + check_authorization(p)?; + + let result = match (&p.body.authorization_kind, is_failed) { + (AuthorizationKind::Proof(vk_hash), false) => { + let prioritized_vk = { + // only lookup _past_ vk setting, ie exclude the new one we + // potentially set in this account_update (use the non-' + // vks_overrided) . + + match vks_overridden.get(&account_id) { + Some(Some(vk)) => ok_if_vk_hash_expected(vk.clone(), *vk_hash)?, + Some(None) => { + // we explicitly have erased the key + return Err(format!( + "No verification key found for proved account \ + update: the verification key was removed by a \ + previous account update\ + account_id={:?}", + account_id + )); + } + None => { + // we haven't set anything; lookup the vk in the fallback + find_vk(*vk_hash, &account_id)? + } + } + }; + + tbl.insert(account_id, prioritized_vk.hash()); + + Ok((p.clone(), Some(prioritized_vk))) + } + + _ => Ok((p.clone(), None)), + }; + + // NOTE: we only update the overriden map AFTER verifying the update to make sure + // that the verification for the VK update itself is done against the previous VK. + if let SetOrKeep::Set(vk_next) = &p.body.update.verification_key { + vks_overridden.insert(p.account_id().clone(), Some(vk_next.clone())); + } + + result + })?; + + Ok(ZkAppCommand { + fee_payer: fee_payer.clone(), + account_updates, + memo: memo.clone(), + }) +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs new file mode 100644 index 000000000..050c5620f --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs @@ -0,0 +1,18 @@ +/// +use super::{AccountUpdate, CallForest, FeePayer, Memo}; + +pub fn account_update(_: &AccountUpdate) -> u64 { + 1 +} + +pub fn fee_payer(_: &FeePayer) -> u64 { + 1 +} + +pub fn account_updates(list: &CallForest) -> u64 { + list.fold(0, |acc, p| acc + account_update(p)) +} + +pub fn memo(_: &Memo) -> u64 { + 0 +} From e9689804c29272093c5c73297ff5bee92882f2b4 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 12:18:57 +0200 Subject: [PATCH 16/26] CHANGELOG: add description for patch 1528 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2d4af2e..2cb4310ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ledger/scan_state/transaction_logic**: move submodule `for_tests` into a new file `zkapp_command/for_tests.rs` ([#1527](https://github.com/o1-labs/mina-rust/pull/1527)). +- **Ledger/scan-state/transaction-logic**: split + `ledger::scan_state::transaction_logic::zkapp_command` into submodules in a + new directory `zkapp_command` + ([#1528](https://github.com/o1-labs/mina-rust/pull/1528/)) ## v0.17.0 From 121ef4c3f3a120a64c620f18775a2683cff05ec1 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 15:16:23 +0200 Subject: [PATCH 17/26] Tx logic: signed-commands update doc --- .../transaction_logic/signed_command.rs | 97 +++++++++++++++---- 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/ledger/src/scan_state/transaction_logic/signed_command.rs b/ledger/src/scan_state/transaction_logic/signed_command.rs index ad38350e9..0f50aed51 100644 --- a/ledger/src/scan_state/transaction_logic/signed_command.rs +++ b/ledger/src/scan_state/transaction_logic/signed_command.rs @@ -12,53 +12,90 @@ use crate::{ use super::{zkapp_command::AccessedOrNot, Memo, TransactionStatus}; -/// +/// Common fields shared by all signed command payloads. +/// +/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:34-48 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 #[derive(Debug, Clone, PartialEq)] pub struct Common { + /// Fee paid to the block producer pub fee: Fee, + /// Public key paying the fee pub fee_payer_pk: CompressedPubKey, + /// Account nonce for replay protection pub nonce: Nonce, + /// Slot after which the transaction expires pub valid_until: Slot, + /// Optional memo field (34 bytes) pub memo: Memo, } +/// Payment payload for transferring MINA tokens. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PaymentPayload { + /// Recipient's public key pub receiver_pk: CompressedPubKey, + /// Amount to transfer pub amount: Amount, } -/// +/// Stake delegation payload for delegating stake to another account. +/// +/// OCaml reference: src/lib/mina_base/stake_delegation.ml L:11-13 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 #[derive(Debug, Clone, PartialEq, Eq)] pub enum StakeDelegationPayload { - SetDelegate { new_delegate: CompressedPubKey }, + /// Delegate stake to a new delegate + SetDelegate { + /// Public key of the new delegate + new_delegate: CompressedPubKey, + }, } impl StakeDelegationPayload { - /// + /// OCaml reference: src/lib/mina_base/stake_delegation.ml L:35-37 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn receiver(&self) -> AccountId { let Self::SetDelegate { new_delegate } = self; AccountId::new(new_delegate.clone(), TokenId::default()) } - /// + /// OCaml reference: src/lib/mina_base/stake_delegation.ml L:33-33 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn receiver_pk(&self) -> &CompressedPubKey { let Self::SetDelegate { new_delegate } = self; new_delegate } } -/// +/// The body of a signed command, which can be either a payment or stake +/// delegation. +/// +/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:179-181 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 #[derive(Debug, Clone, PartialEq, Eq)] pub enum Body { + /// Transfer MINA tokens from fee payer to receiver Payment(PaymentPayload), + /// Delegate fee payer's stake to another account StakeDelegation(StakeDelegationPayload), } -/// +/// Signed command payload containing common fields and the transaction body. +/// +/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:239-243 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 #[derive(Debug, Clone, PartialEq)] pub struct SignedCommandPayload { + /// Common fields (fee, fee payer, nonce, valid_until, memo) pub common: Common, + /// Transaction body (payment or stake delegation) pub body: Body, } @@ -84,7 +121,9 @@ impl SignedCommandPayload { } } -/// +/// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:352-362 +/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-10 mod weight { use super::*; @@ -102,12 +141,20 @@ mod weight { } } +/// A signed command is a transaction that transfers value or delegates stake. +/// +/// Signed commands are authorized by a cryptographic signature and consist of a +/// payload (containing the transaction details) and the signature proving +/// authorization. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(into = "MinaBaseSignedCommandStableV2")] #[serde(try_from = "MinaBaseSignedCommandStableV2")] pub struct SignedCommand { + /// The transaction payload (common fields and body) pub payload: SignedCommandPayload, + /// The public key that signed the transaction pub signer: CompressedPubKey, // TODO: This should be a `mina_signer::PubKey` + /// The cryptographic signature pub signature: Signature, } @@ -116,13 +163,17 @@ impl SignedCommand { self.payload.common.valid_until } - /// + /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:292-292 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn fee_payer(&self) -> AccountId { let public_key = self.payload.common.fee_payer_pk.clone(); AccountId::new(public_key, TokenId::default()) } - /// + /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:290-290 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn fee_payer_pk(&self) -> &CompressedPubKey { &self.payload.common.fee_payer_pk } @@ -136,7 +187,9 @@ impl SignedCommand { weight::of_body(body) } - /// + /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:288-288 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn fee_token(&self) -> TokenId { TokenId::default() } @@ -145,7 +198,9 @@ impl SignedCommand { self.payload.common.fee } - /// + /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:304-304 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn receiver(&self) -> AccountId { match &self.payload.body { Body::Payment(payload) => { @@ -155,7 +210,9 @@ impl SignedCommand { } } - /// + /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:302-302 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn receiver_pk(&self) -> &CompressedPubKey { match &self.payload.body { Body::Payment(payload) => &payload.receiver_pk, @@ -178,7 +235,9 @@ impl SignedCommand { FeeExcess::of_single((self.fee_token(), Signed::::of_unsigned(self.fee()))) } - /// + /// OCaml reference: src/lib/mina_base/signed_command_payload.ml L:320-338 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn account_access_statuses( &self, status: &TransactionStatus, @@ -189,7 +248,7 @@ impl SignedCommand { match status { Applied => vec![(self.fee_payer(), Accessed), (self.receiver(), Accessed)], // Note: The fee payer is always accessed, even if the transaction fails - // + // OCaml reference: src/lib/mina_base/signed_command_payload.mli L:205-209 Failed(_) => vec![(self.fee_payer(), Accessed), (self.receiver(), NotAccessed)], } } @@ -201,12 +260,16 @@ impl SignedCommand { .collect() } - /// + /// OCaml reference: src/lib/mina_base/signed_command.ml L:417-420 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn public_keys(&self) -> [&CompressedPubKey; 2] { [self.fee_payer_pk(), self.receiver_pk()] } - /// + /// OCaml reference: src/lib/mina_base/signed_command.ml L:422-424 + /// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3 + /// Last verified: 2025-10-10 pub fn check_valid_keys(&self) -> bool { self.public_keys() .into_iter() From 7881a986d7f7960215ab64b8f6f4d59abaafb11c Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 17:01:51 +0200 Subject: [PATCH 18/26] Core: document ConstraintConstants for protocol --- core/src/constants.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/src/constants.rs b/core/src/constants.rs index f2514f108..5d391a9ef 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -21,17 +21,28 @@ pub struct ForkConstants { pub global_slot_since_genesis: u32, } +/// Protocol constraint constants #[derive(Clone, Debug)] pub struct ConstraintConstants { + /// Number of sub-windows in a single slot window pub sub_windows_per_window: u64, + /// Depth of the account ledger Merkle tree pub ledger_depth: u64, + /// Number of slots to delay SNARK work for proof generation pub work_delay: u64, + /// Duration of each block window in milliseconds pub block_window_duration_ms: u64, + /// Log2 of maximum transactions per block pub transaction_capacity_log_2: u64, + /// Depth of the pending coinbase Merkle tree pub pending_coinbase_depth: usize, + /// Base amount awarded for producing a block pub coinbase_amount: u64, + /// Multiplier for coinbase when account is "supercharged" pub supercharged_coinbase_factor: u64, + /// Fee charged for creating a new account pub account_creation_fee: u64, + /// Optional fork configuration for protocol upgrades pub fork: Option, } #[derive(Clone, Debug, BinProtWrite)] From 4a11bcb992ecf17c71571d312cf6edbeeb86024b Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 17:03:00 +0200 Subject: [PATCH 19/26] Ledger/db_impl: remove comments --- ledger/src/database/database_impl.rs | 217 +-------------------------- 1 file changed, 1 insertion(+), 216 deletions(-) diff --git a/ledger/src/database/database_impl.rs b/ledger/src/database/database_impl.rs index b5a2ae19f..1b1637732 100644 --- a/ledger/src/database/database_impl.rs +++ b/ledger/src/database/database_impl.rs @@ -44,15 +44,9 @@ impl std::fmt::Debug for DatabaseImpl { } } -// #[derive(Debug, PartialEq, Eq)] -// pub enum DatabaseError { -// OutOfLeaves, -// } - impl DatabaseImpl { pub fn clone_db(&self, new_directory: PathBuf) -> Self { Self { - // root: self.root.clone(), accounts: self.accounts.clone(), id_to_addr: self.id_to_addr.clone(), token_owners: self.token_owners.clone(), @@ -62,7 +56,6 @@ impl DatabaseImpl { uuid: next_uuid(), directory: new_directory, hashes_matrix: self.hashes_matrix.clone(), - // root_hash: RefCell::new(*self.root_hash.borrow()), } } @@ -82,10 +75,6 @@ impl DatabaseImpl { account_id: AccountId, account: Account, ) -> Result { - // if self.root.is_none() { - // self.root = Some(NodeOrLeaf::Node(Node::default())); - // } - if let Some(addr) = self.id_to_addr.get(&account_id).cloned() { return Ok(GetOrCreated::Existed(addr)); } @@ -99,9 +88,6 @@ impl DatabaseImpl { assert_eq!(location.to_index(), self.accounts.len()); self.accounts.push(Some(account)); - // let root = self.root.as_mut().unwrap(); - // root.add_account_on_path(account, location.iter()); - self.last_location = Some(location.clone()); self.naccounts += 1; @@ -112,8 +98,6 @@ impl DatabaseImpl { } self.id_to_addr.insert(account_id, location.clone()); - // self.root_hash.borrow_mut().take(); - Ok(GetOrCreated::Added(location)) } @@ -139,15 +123,6 @@ impl DatabaseImpl { return *hash; }; - // let tree_depth = self.depth() as usize; - // let mut children = addr.iter_children(tree_depth); - - // // First child - // let first_account_index = children.next().unwrap().to_index().0 as u64; - // let mut nremaining = self - // .naccounts() - // .saturating_sub(first_account_index as usize); - let last_account = self .last_filled() .unwrap_or_else(|| Address::first(self.depth as usize)); @@ -155,7 +130,6 @@ impl DatabaseImpl { self.emulate_tree_recursive(addr, &last_account) } - // fn emulate_recursive(&mut self, addr: Address, nremaining: &mut usize) -> Fp { pub fn emulate_tree_recursive(&mut self, addr: Address, last_account: &Address) -> Fp { let tree_depth = self.depth as usize; let current_depth = tree_depth - addr.length(); @@ -290,10 +264,6 @@ impl DatabaseImpl { _account_id: (), account: AccountLegacy, ) -> Result { - // if self.root.is_none() { - // self.root = Some(NodeOrLeaf::Node(Node::default())); - // } - let location = match self.last_location.as_ref() { Some(last) => last.next().ok_or(DatabaseError::OutOfLeaves)?, None => Address::first(self.depth as usize), @@ -302,10 +272,6 @@ impl DatabaseImpl { assert_eq!(location.to_index(), self.accounts.len()); self.accounts.push(Some(account)); - // let root = self.root.as_mut().unwrap(); - // let path_iter = location.clone().into_iter(); - // root.add_account_on_path(account, path_iter); - self.last_location = Some(location.clone()); self.naccounts += 1; @@ -333,16 +299,6 @@ impl DatabaseImpl { } }; - // elog!( - // "DB depth={:?} uuid={:?} pid={:?} path={:?}", - // depth, - // uuid, - // crate::util::pid(), - // path - // ); - - // std::fs::create_dir_all(&path).ok(); - Self { depth, accounts: Vec::with_capacity(Self::NACCOUNTS), @@ -353,7 +309,6 @@ impl DatabaseImpl { uuid, directory: path, hashes_matrix: HashesMatrix::new(depth as usize), - // root_hash: Default::default(), } } @@ -386,20 +341,6 @@ impl DatabaseImpl { self.accounts.iter().filter_map(Option::as_ref).count() } - // fn naccounts_recursive(&self, elem: &NodeOrLeaf, naccounts: &mut usize) { - // match elem { - // NodeOrLeaf::Leaf(_) => *naccounts += 1, - // NodeOrLeaf::Node(node) => { - // if let Some(left) = node.left.as_ref() { - // self.naccounts_recursive(left, naccounts); - // }; - // if let Some(right) = node.right.as_ref() { - // self.naccounts_recursive(right, naccounts); - // }; - // } - // } - // } - fn get_account_ref(&self, addr: Address) -> Option<&Account> { let index = addr.to_index(); let index: usize = index.0 as usize; @@ -415,19 +356,6 @@ impl BaseLedger for DatabaseImpl { .filter_map(Option::as_ref) .cloned() .collect() - // let root = match self.root.as_ref() { - // Some(root) => root, - // None => return Vec::new(), - // }; - - // let mut accounts = Vec::with_capacity(100); - - // root.iter_recursive(&mut |account| { - // accounts.push(account.clone()); - // ControlFlow::Continue(()) - // }); - - // accounts } fn iter(&self, fun: F) @@ -438,16 +366,6 @@ impl BaseLedger for DatabaseImpl { .iter() .filter_map(Option::as_ref) .for_each(fun); - - // let root = match self.root.as_ref() { - // Some(root) => root, - // None => return, - // }; - - // root.iter_recursive(&mut |account| { - // fun(account); - // ControlFlow::Continue(()) - // }); } fn fold(&self, init: B, mut fun: F) -> B @@ -459,20 +377,6 @@ impl BaseLedger for DatabaseImpl { accum = fun(accum, account); } accum - - // let root = match self.root.as_ref() { - // Some(root) => root, - // None => return init, - // }; - - // let mut accum = Some(init); - // root.iter_recursive(&mut |account| { - // let res = fun(accum.take().unwrap(), account); - // accum = Some(res); - // ControlFlow::Continue(()) - // }); - - // accum.unwrap() } fn fold_with_ignored_accounts( @@ -493,15 +397,6 @@ impl BaseLedger for DatabaseImpl { } } accum - // self.fold(init, |accum, account| { - // let account_id = account.id(); - - // if !ignoreds.contains(&account_id) { - // fun(accum, account) - // } else { - // accum - // } - // }) } fn fold_until(&self, init: B, mut fun: F) -> B @@ -521,25 +416,6 @@ impl BaseLedger for DatabaseImpl { } } accum - - // let root = match self.root.as_ref() { - // Some(root) => root, - // None => return init, - // }; - - // let mut accum = Some(init); - // root.iter_recursive(&mut |account| match fun(accum.take().unwrap(), account) { - // ControlFlow::Continue(account) => { - // accum = Some(account); - // ControlFlow::Continue(()) - // } - // ControlFlow::Break(account) => { - // accum = Some(account); - // ControlFlow::Break(()) - // } - // }); - - // accum.unwrap() } fn accounts(&self) -> HashSet { @@ -560,31 +436,11 @@ impl BaseLedger for DatabaseImpl { set.insert(account.token_id.clone()); } } - - // let root = match self.root.as_ref() { - // Some(root) => root, - // None => return HashSet::default(), - // }; - - // let mut set = HashSet::with_capacity(self.naccounts); - - // root.iter_recursive(&mut |account| { - // if account.public_key == public_key { - // set.insert(account.token_id.clone()); - // } - - // ControlFlow::Continue(()) - // }); - set } fn location_of_account(&self, account_id: &AccountId) -> Option
{ - let res = self.id_to_addr.get(account_id).cloned(); - - // elog!("location_of_account id={:?}\n{:?}", account_id, res); - - res + self.id_to_addr.get(account_id).cloned() } fn location_of_account_batch( @@ -672,16 +528,6 @@ impl BaseLedger for DatabaseImpl { .map(|addr| (addr.clone(), self.get(addr.clone()))) .collect(); - // let root = match self.root.as_ref() { - // Some(root) => Cow::Borrowed(root), - // None => Cow::Owned(NodeOrLeaf::Node(Node::default())), - // }; - - // let res: Vec<_> = addr - // .iter() - // .map(|addr| (addr.clone(), root.get_on_path(addr.iter()).cloned())) - // .collect(); - elog!("get_batch addrs={:?}\nres={:?}={:?}", addr, res.len(), res); res @@ -698,12 +544,7 @@ impl BaseLedger for DatabaseImpl { self.accounts.resize(index + 1, None); } - // if self.root.is_none() { - // self.root = Some(NodeOrLeaf::Node(Node::default())); - // } - let id = account.id(); - // let root = self.root.as_mut().unwrap(); // Remove account at the address and it's index if let Some(account) = self.get(addr.clone()) { @@ -725,7 +566,6 @@ impl BaseLedger for DatabaseImpl { } self.id_to_addr.insert(id, addr.clone()); self.accounts[index] = Some(*account); - // root.add_account_on_path(account, addr.iter()); if self .last_location @@ -735,13 +575,10 @@ impl BaseLedger for DatabaseImpl { { self.last_location = Some(addr); } - - // self.root_hash.borrow_mut().take(); } fn set_batch(&mut self, list: &[(Address, Box)]) { elog!("SET_BATCH {:?}", list.len()); - // elog!("SET_BATCH {:?} {:?}", list.len(), list); for (addr, account) in list { assert_eq!(addr.length(), self.depth as usize, "addr={:?}", addr); self.set(addr.clone(), account.clone()); @@ -757,8 +594,6 @@ impl BaseLedger for DatabaseImpl { let addr = Address::from_index(index, self.depth as usize); self.set(addr, account); - // self.root_hash.borrow_mut().take(); - Ok(()) } @@ -767,30 +602,7 @@ impl BaseLedger for DatabaseImpl { } fn merkle_root(&mut self) -> Fp { - // let now = crate::util::Instant::now(); - self.root_hash() - - // let root = match *self.root_hash.borrow() { - // Some(root) => root, - // None => self.root_hash(), - // }; - - // elog!( - // "uuid={:?} ROOT={} num_account={:?} elapsed={:?}", - // self.get_uuid(), - // root, - // self.num_accounts(), - // now.elapsed(), - // ); - - // self.root_hash.borrow_mut().replace(root); - - // elog!("PATH={:#?}", self.merkle_path(Address::first(self.depth as usize))); - - // self.merkle_path(Address::first(self.depth as usize)); - - // root } fn merkle_path(&mut self, addr: Address) -> Vec { @@ -804,8 +616,6 @@ impl BaseLedger for DatabaseImpl { .last_filled() .unwrap_or_else(|| Address::first(self.depth as usize)); - // let tree_index = TreeIndex::root(self.depth() as usize); - self.emulate_tree_to_get_path(addr, &last_account, &mut path, &mut merkle_path); merkle_path @@ -817,11 +627,6 @@ impl BaseLedger for DatabaseImpl { } fn remove_accounts(&mut self, ids: &[AccountId]) { - // let root = match self.root.as_mut() { - // Some(root) => root, - // None => return, - // }; - let mut addrs = ids .iter() .map(|accound_id| self.id_to_addr.remove(accound_id).unwrap()) @@ -829,16 +634,6 @@ impl BaseLedger for DatabaseImpl { addrs.sort_by_key(Address::to_index); for addr in addrs.iter().rev() { - // let leaf = match root.get_mut_leaf_on_path(addr.iter()) { - // Some(leaf) => leaf, - // None => continue, - // }; - - // let account = match leaf.account.take() { - // Some(account) => account, - // None => continue, - // }; - let account_index = addr.to_index(); self.hashes_matrix.invalidate_hashes(account_index); @@ -847,9 +642,6 @@ impl BaseLedger for DatabaseImpl { None => continue, }; - // let index = addr.to_index(); - // let account = std::mem::take() - let id = account.id(); self.id_to_addr.remove(&id); if !id.token_id.is_default() { @@ -872,8 +664,6 @@ impl BaseLedger for DatabaseImpl { self.last_location = addr.prev(); } } - - // self.root_hash.borrow_mut().take(); } fn detached_signal(&mut self) { @@ -926,11 +716,6 @@ impl BaseLedger for DatabaseImpl { return None; } - // let root = match self.root.as_ref() { - // Some(root) => root, - // None => return None, - // }; - let children = addr.iter_children(self.depth as usize); let mut accounts = Vec::with_capacity(children.len()); From 647bc70b715f456b65710af5197b1eb29a9ae85f Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 17:03:15 +0200 Subject: [PATCH 20/26] Ledger/user_cmd: move all imports at the top of the file --- ledger/src/generators/user_command.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/ledger/src/generators/user_command.rs b/ledger/src/generators/user_command.rs index 02cff541e..6a290f24b 100644 --- a/ledger/src/generators/user_command.rs +++ b/ledger/src/generators/user_command.rs @@ -1,8 +1,7 @@ -use std::{collections::HashMap, rc::Rc}; - -use mina_signer::Keypair; -use rand::Rng; - +use super::{ + zkapp_command::GenZkappCommandParams, Failure, Role, LEDGER_DEPTH, MAX_ACCOUNT_UPDATES, + MAX_TOKEN_UPDATES, MINIMUM_USER_COMMAND_FEE, +}; use crate::{ gen_keypair, scan_state::{ @@ -11,16 +10,15 @@ use crate::{ for_tests::HashableCompressedPubKey, valid, zkapp_command::{self, verifiable}, + TransactionStatus::Applied, }, }, util, Account, AccountId, AuthRequired, BaseLedger, Mask, MyCowMut, Permissions, TokenId, VerificationKey, VerificationKeyWire, ZkAppAccount, TXN_VERSION_CURRENT, }; - -use super::{ - zkapp_command::GenZkappCommandParams, Failure, Role, LEDGER_DEPTH, MAX_ACCOUNT_UPDATES, - MAX_TOKEN_UPDATES, MINIMUM_USER_COMMAND_FEE, -}; +use mina_signer::Keypair; +use rand::Rng; +use std::{collections::HashMap, rc::Rc}; fn zkapp_command_with_ledger( num_keypairs: Option, @@ -187,8 +185,6 @@ fn zkapp_command_with_ledger( global_slot: None, }); - use crate::scan_state::transaction_logic::TransactionStatus::Applied; - let zkapp_command = zkapp_command::valid::to_valid(zkapp_command, &Applied, |hash, account_id| { verifiable::find_vk_via_ledger(ledger.clone(), hash, account_id) @@ -265,7 +261,6 @@ pub fn sequence_zkapp_command_with_ledger( global_slot: None, }); - use crate::scan_state::transaction_logic::TransactionStatus::Applied; let zkapp_command = zkapp_command::valid::to_valid(zkapp_command, &Applied, |hash, account_id| { verifiable::find_vk_via_ledger(ledger.clone(), hash, account_id) From c558a61ccdb2387b469ddb0a31545933d682cc2c Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 17:04:25 +0200 Subject: [PATCH 21/26] Ledger/tx_logic: document top level module --- .../src/scan_state/transaction_logic/mod.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 36e7e2ef0..82e033656 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1,3 +1,58 @@ +//! Transaction logic module +//! +//! This module implements the core logic for applying and validating all +//! transaction types in the Mina protocol. It is a direct port of the OCaml +//! implementation from `src/lib/transaction_logic/mina_transaction_logic.ml` +//! and maintains identical business logic. +//! +//! # Transaction Types +//! +//! The module handles three main categories of transactions: +//! +//! ## User Commands +//! - **Signed Commands**: Payments and stake delegations +//! - **zkApp Commands**: Complex multi-account zero-knowledge operations +//! +//! ## Protocol Transactions +//! - **Fee Transfers**: Distribution of transaction fees to block producers +//! - **Coinbase**: Block rewards for successful block production +//! +//! # Two-Phase Application +//! +//! Transaction application follows a two-phase model to enable efficient proof +//! generation: +//! +//! 1. **First Pass** ([`apply_transaction_first_pass`]): Validates +//! preconditions and begins application. For zkApp commands, applies the fee +//! payer and first phase of account updates. +//! +//! 2. **Second Pass** ([`apply_transaction_second_pass`]): Completes +//! application. For zkApp commands, applies the second phase of account +//! updates and finalizes state. +//! +//! # Key Types +//! +//! - [`Transaction`]: Top-level enum for all transaction types +//! - [`UserCommand`]: User-initiated transactions (signed or zkApp) +//! - [`TransactionStatus`]: Applied or failed with specific error codes +//! - [`TransactionFailure`]: 50+ specific failure reasons +//! - [`FeeTransfer`]: Fee distribution transaction +//! - [`Coinbase`]: Block reward transaction +//! +//! # Module Organization +//! +//! - [`local_state`]: Local state management during zkApp application +//! - [`protocol_state`]: Protocol state views for transaction application +//! - [`signed_command`]: Payment and stake delegation logic +//! - [`transaction_applied`]: Final transaction application results +//! - [`transaction_partially_applied`]: Two-phase transaction application +//! - [`transaction_union_payload`]: Unified transaction representation for SNARK circuits +//! - [`transaction_witness`]: Witness generation for transaction proofs +//! - [`valid`]: Valid (but not yet verified) user commands +//! - [`verifiable`]: Verifiable user commands ready for proof verification +//! - [`zkapp_command`]: zkApp command processing +//! - [`zkapp_statement`]: zkApp statement types for proof generation + use self::{ local_state::{apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, LocalStateEnv}, protocol_state::{GlobalState, ProtocolStateView}, @@ -243,12 +298,15 @@ impl WithStatus { pub trait GenericCommand { fn fee(&self) -> Fee; + fn forget(&self) -> UserCommand; } pub trait GenericTransaction: Sized { fn is_fee_transfer(&self) -> bool; + fn is_coinbase(&self) -> bool; + fn is_command(&self) -> bool; } From fe1e2055338636d9f3fbc29c0ec59df842e2945f Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 17:22:45 +0200 Subject: [PATCH 22/26] transaction_logic: make methods public --- .../transaction_logic/transaction_partially_applied.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs index 6b988d443..c96de9ab1 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs @@ -188,7 +188,7 @@ where .collect() } -struct FailureCollection { +pub struct FailureCollection { inner: Vec>, } @@ -245,7 +245,7 @@ impl FailureCollection { /// `[[failure-of-fee-transfer];[]]` /// /// -fn apply_coinbase( +pub fn apply_coinbase( constraint_constants: &ConstraintConstants, txn_global_slot: &Slot, ledger: &mut L, @@ -405,7 +405,7 @@ where } /// -fn apply_fee_transfer( +pub fn apply_fee_transfer( constraint_constants: &ConstraintConstants, txn_global_slot: &Slot, ledger: &mut L, @@ -516,7 +516,7 @@ fn get_new_accounts(action: AccountState, data: T) -> Option { /// [[];[failure-of-second-fee-transfer]] /// First fails and second succeeds: /// [[failure-of-first-fee-transfer];[]] -fn process_fee_transfer( +pub fn process_fee_transfer( ledger: &mut L, fee_transfer: &FeeTransfer, modify_balance: FunBalance, From 53fde7ebaf7d45590f3535507ca19cae6b61cc11 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 18:10:26 +0200 Subject: [PATCH 23/26] ledger/tx-logic: document tx_partially_applied --- .../transaction_partially_applied.rs | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs index c96de9ab1..f12dc0b25 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs @@ -1,3 +1,44 @@ +//! Two-phase transaction application +//! +//! This module implements the two-phase transaction application model used in Mina. +//! This approach enables efficient proof generation, particularly for zkApp commands. +//! +//! # Application Phases +//! +//! ## First Pass +//! +//! The first pass ([`apply_transaction_first_pass`]) performs: +//! - Transaction validation (nonces, balances, permissions) +//! - Fee payment +//! - For zkApp commands: applies fee payer and begins account update processing +//! - For other transactions: completes the entire application +//! - Records the previous ledger hash +//! +//! ## Second Pass +//! +//! The second pass ([`apply_transaction_second_pass`]) performs: +//! - For zkApp commands: completes account update processing and emits events/actions +//! - For other transactions: simply packages the results from first pass +//! +//! # Key Types +//! +//! - [`TransactionPartiallyApplied`]: Intermediate state between passes +//! - [`ZkappCommandPartiallyApplied`]: zkApp-specific intermediate state +//! - [`FullyApplied`]: Wrapper for non-zkApp transactions that complete in first pass +//! +//! # Fee Transfers and Coinbase +//! +//! Fee transfers and coinbase transactions also use helper functions in this module: +//! - [`apply_fee_transfer`]: Distributes fees to block producers +//! - [`apply_coinbase`]: Handles block rewards and optional fee transfers +//! - [`process_fee_transfer`]: Core logic for fee distribution with permission checks +//! +//! Both transactions have structured failure status to indicate which part failed: +//! - Single transfer: `[[failure]]` +//! - Two transfers both fail: `[[failure1]; [failure2]]` +//! - First succeeds, second fails: `[[]; [failure2]]` +//! - First fails, second succeeds: `[[failure1]; []]` + use super::{ transaction_applied::{CoinbaseApplied, FeeTransferApplied}, *, @@ -46,6 +87,106 @@ where } } +/// Applies the first pass of transaction application. +/// +/// This function performs the initial phase of transaction processing, which includes +/// validation, fee payment, and partial application. The behavior differs based on +/// transaction type: +/// +/// # Transaction Type Handling +/// +/// - **Signed Commands** (payments, stake delegations): Fully applied in the first pass. +/// The result is wrapped in [`FullyApplied`] since no second pass work is needed. +/// +/// - **zkApp Commands**: Partially applied. The first pass: +/// - Validates the zkApp command structure and permissions +/// - Applies the fee payer account update +/// - Begins processing the first phase of account updates +/// - Records intermediate state in [`ZkappCommandPartiallyApplied`] +/// +/// - **Fee Transfers**: Fully applied in the first pass, distributing fees to block +/// producers according to the protocol rules. +/// +/// - **Coinbase**: Fully applied in the first pass, crediting block rewards and any +/// associated fee transfers to the designated accounts. +/// +/// # Ledger State Changes +/// +/// The ledger is mutated during the first pass as follows: +/// +/// - **Signed Commands**: +/// - Fee payer balance decreased by fee amount +/// - Fee payer nonce incremented +/// - Fee payer receipt chain hash updated +/// - Fee payer timing updated based on vesting schedule +/// - For payments: sender balance decreased, receiver balance increased +/// - For payments: new account created if receiver doesn't exist +/// - For stake delegations: delegate field updated +/// +/// - **zkApp Commands**: +/// - Fee payer account fully updated (balance, nonce, receipt chain, timing) +/// - First phase account updates applied to ledger +/// - New accounts may be created +/// +/// - **Fee Transfers**: +/// - Receiver account balances increased by fee amounts +/// - Timing updated when balances increase +/// - New accounts created if receivers don't exist +/// +/// - **Coinbase**: +/// - Block producer balance increased by reward amount +/// - Fee transfer recipient balance increased (if applicable) +/// - Timing updated when balances increase +/// - New accounts created if recipients don't exist +/// +/// # Parameters +/// +/// - `constraint_constants`: Protocol constants including account creation fees and limits +/// - `global_slot`: Current global slot number for timing validation +/// - `txn_state_view`: View of the protocol state for validating transaction preconditions +/// - `ledger`: Mutable reference to the ledger being updated +/// - `transaction`: The transaction to apply +/// +/// # Returns +/// +/// Returns a [`TransactionPartiallyApplied`] containing either: +/// - [`FullyApplied`] result for transactions that complete in first pass +/// - [`ZkappCommandPartiallyApplied`] for zkApp commands needing second pass +/// +/// # Errors +/// +/// Returns an error if: +/// - Transaction validation fails (invalid nonce, insufficient balance, etc.) +/// - Fee payment fails +/// - Account permissions are insufficient +/// - Timing constraints are violated +/// +/// ## Error Side Effects +/// +/// When an error occurs, the ledger state depends on where the error occurred: +/// +/// - **Errors during fee payment** (invalid nonce, nonexistent fee payer): Ledger +/// remains completely unchanged. +/// +/// - **Errors after fee payment** (insufficient balance for payment, permission +/// errors): The fee has already been charged to ensure network compensation. The +/// fee payer's account will have: balance decreased by fee, nonce incremented, +/// receipt chain hash updated. However, the actual payment/operation is NOT +/// performed. +/// +/// # Tests +/// +/// Test coverage (in `ledger/tests/test_transaction_logic_first_pass.rs`): +/// +/// - [`test_apply_payment_success`]: successful payment with ledger state verification +/// - [`test_apply_payment_insufficient_balance`]: payment exceeding sender balance +/// - [`test_apply_payment_invalid_nonce`]: payment with incorrect nonce +/// - [`test_apply_payment_nonexistent_fee_payer`]: payment from nonexistent account +/// +/// [`test_apply_payment_success`]: ../../tests/test_transaction_logic_first_pass.rs +/// [`test_apply_payment_insufficient_balance`]: ../../tests/test_transaction_logic_first_pass.rs +/// [`test_apply_payment_invalid_nonce`]: ../../tests/test_transaction_logic_first_pass.rs +/// [`test_apply_payment_nonexistent_fee_payer`]: ../../tests/test_transaction_logic_first_pass.rs pub fn apply_transaction_first_pass( constraint_constants: &ConstraintConstants, global_slot: Slot, @@ -108,6 +249,71 @@ where } } +/// Completes the second pass of transaction application. +/// +/// This function finalizes transaction processing by completing any remaining work +/// from the first pass. The behavior differs based on transaction type: +/// +/// # Transaction Type Handling +/// +/// - **Signed Commands**: No additional work needed. Simply unwraps the [`FullyApplied`] +/// result from the first pass and packages it into a [`TransactionApplied`]. +/// +/// - **zkApp Commands**: Completes the second phase of application: +/// - Processes the second phase of account updates +/// - Emits events and actions from the zkApp execution +/// - Updates the zkApp's on-chain state +/// - Validates all preconditions are satisfied +/// +/// - **Fee Transfers**: No additional work needed. Simply packages the first pass result. +/// +/// - **Coinbase**: No additional work needed. Simply packages the first pass result. +/// +/// # Ledger State Changes +/// +/// The ledger is mutated during the second pass only for zkApp commands: +/// +/// - **Signed Commands**: No ledger changes (all modifications completed in first pass) +/// +/// - **zkApp Commands**: +/// - Second phase account updates applied +/// - Account balances modified based on zkApp logic +/// - Account app state fields updated +/// - Account permissions may be modified +/// - Action state and event sequence numbers updated +/// - New accounts may be created +/// +/// - **Fee Transfers**: No ledger changes (all modifications completed in first pass) +/// +/// - **Coinbase**: No ledger changes (all modifications completed in first pass) +/// +/// # Parameters +/// +/// - `constraint_constants`: Protocol constants including account creation fees and limits +/// - `ledger`: Mutable reference to the ledger being updated +/// - `partial_transaction`: The partially applied transaction from the first pass +/// +/// # Returns +/// +/// Returns a [`TransactionApplied`] containing the complete application result with: +/// - Previous ledger hash (recorded during first pass) +/// - Full transaction status (Applied or Failed with specific error codes) +/// - Account updates and new account information +/// - Events and actions (for zkApp commands) +/// +/// # Errors +/// +/// Returns an error if: +/// - Second phase zkApp account updates fail +/// - zkApp preconditions fail during second pass +/// - Account permissions are insufficient +/// +/// # Notes +/// +/// For non-zkApp transactions, this function performs minimal work since the first +/// pass already completed the application. The two-phase model exists primarily to +/// enable efficient zkApp proof generation where different account updates can be +/// processed in separate circuit phases. pub fn apply_transaction_second_pass( constraint_constants: &ConstraintConstants, ledger: &mut L, From 21ac9e90a5d295f5a48f2b2779043a5b6507edd6 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 18:10:49 +0200 Subject: [PATCH 24/26] ledger/scan_state: wrap comments at 80 chars --- .../transaction_union_payload.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs index c68c9080e..15eb2747b 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs @@ -283,14 +283,17 @@ pub struct TransactionUnion { } impl TransactionUnion { - /// For SNARK purposes, we inject [Transaction.t]s into a single-variant 'tagged-union' record capable of - /// representing all the variants. We interpret the fields of this union in different ways depending on - /// the value of the [payload.body.tag] field, which represents which variant of [Transaction.t] the value - /// corresponds to. + /// For SNARK purposes, we inject [Transaction.t]s into a single-variant + /// 'tagged-union' record capable of representing all the variants. We + /// interpret the fields of this union in different ways depending on the + /// value of the [payload.body.tag] field, which represents which variant of + /// [Transaction.t] the value corresponds to. /// - /// Sometimes we interpret fields in surprising ways in different cases to save as much space in the SNARK as possible (e.g., - /// [payload.body.public_key] is interpreted as the recipient of a payment, the new delegate of a stake - /// delegation command, and a fee transfer recipient for both coinbases and fee-transfers. + /// Sometimes we interpret fields in surprising ways in different cases to + /// save as much space in the SNARK as possible (e.g., + /// [payload.body.public_key] is interpreted as the recipient of a payment, + /// the new delegate of a stake delegation command, and a fee transfer + /// recipient for both coinbases and fee-transfers. pub fn of_transaction(tx: &Transaction) -> Self { match tx { Transaction::Command(cmd) => { From a721dbecf22d3bf438be067eff9f3bb1ebfa4ea2 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 18:11:06 +0200 Subject: [PATCH 25/26] ledger/scan_state: document tx_union_payload --- .../transaction_union_payload.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs index 15eb2747b..024274bb1 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs @@ -1,3 +1,67 @@ +//! Unified transaction representation for SNARK circuits +//! +//! This module provides a single, unified structure ([`TransactionUnion`]) that +//! can represent all transaction types (payments, stake delegations, fee +//! transfers, coinbase) for SNARK circuit processing. This enables efficient +//! proof generation by using a single circuit design regardless of the specific +//! transaction type. +//! +//! # Transaction Union +//! +//! The [`TransactionUnion`] type encodes all transaction variants using a +//! tagged union approach: +//! +//! - [`Common`]: Fields present in all transactions (fee, nonce, memo, etc.) +//! - [`Body`]: Transaction-specific fields with interpretation based on [`Tag`] +//! - [`Tag`]: Discriminates between Payment, StakeDelegation, FeeTransfer, and +//! Coinbase +//! +//! # Field Interpretation +//! +//! Fields in [`Body`] are interpreted differently based on the [`Tag`] value: +//! +//! - **Payment**: `source_pk` and `receiver_pk` are sender and recipient +//! - **Stake Delegation**: `receiver_pk` is the new delegate +//! - **Fee Transfer**: `receiver_pk` is the fee recipient, `amount` is the fee +//! - **Coinbase**: `receiver_pk` is the block producer, `amount` is the reward +//! +//! # Receipt Chain Hash +//! +//! This module also provides functions for computing receipt chain hashes, +//! which commit to the sequence of transactions applied to an account: +//! +//! - [`cons_signed_command_payload`]: Updates receipt chain hash for signed +//! commands +//! - [`cons_zkapp_command_commitment`]: Updates receipt chain hash for zkApp +//! commands +//! - [`checked_cons_signed_command_payload`]: Checked version for use in +//! circuits +//! +//! # Timing and Vesting +//! +//! The module implements timing validation for timed (vested) accounts: +//! +//! - [`validate_timing`]: Ensures timing constraints are met for an account +//! deduction +//! - [`validate_nonces`]: Validates transaction nonce matches account nonce +//! - [`account_check_timing`]: Checks timing status for an account +//! - [`timing_error_to_user_command_status`]: Converts timing errors to +//! transaction failures +//! +//! Timed accounts have a minimum balance that decreases over time according to +//! a vesting schedule. When the minimum balance reaches zero, the account +//! automatically becomes untimed. +//! +//! # Account Helpers +//! +//! Utility functions for account operations: +//! +//! - [`get_with_location`]: Retrieves an account or creates a placeholder for +//! new accounts +//! - [`ExistingOrNew`]: Indicates whether an account exists or is newly created +//! - [`add_amount`]/[`sub_amount`]: Safe balance arithmetic with +//! overflow/underflow checking + use super::{ signed_command::{ self, PaymentPayload, SignedCommand, SignedCommandPayload, StakeDelegationPayload, From f08d55190b4e1fe40a39611c47892fd1851cc55b Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 18:11:18 +0200 Subject: [PATCH 26/26] ledger/tests: add tests for first pass of the tx logic --- .../test_transaction_logic_first_pass.rs | 441 ++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 ledger/tests/test_transaction_logic_first_pass.rs diff --git a/ledger/tests/test_transaction_logic_first_pass.rs b/ledger/tests/test_transaction_logic_first_pass.rs new file mode 100644 index 000000000..18aeb4505 --- /dev/null +++ b/ledger/tests/test_transaction_logic_first_pass.rs @@ -0,0 +1,441 @@ +//! Tests for apply_transaction_first_pass +//! +//! Run with: cargo test --test test_transaction_logic_first_pass +//! +//! Tests the first pass of two-phase transaction application, covering: +//! - Successful payment transactions +//! - Insufficient balance errors +//! - Invalid nonce errors +//! - Nonexistent fee payer errors + +use ark_ff::Zero; +use mina_core::constants::ConstraintConstants; +use mina_curves::pasta::Fp; +use mina_tree::{ + scan_state::{ + currency::{Amount, Balance, Fee, Length, Magnitude, Nonce, Slot}, + transaction_logic::{ + protocol_state::{EpochData, EpochLedger, ProtocolStateView}, + signed_command::{Body, Common, PaymentPayload, SignedCommand, SignedCommandPayload}, + transaction_partially_applied::apply_transaction_first_pass, + Memo, Transaction, UserCommand, + }, + }, + Account, AccountId, BaseLedger, Database, Mask, +}; + +fn dummy_epoch_data() -> EpochData { + EpochData { + ledger: EpochLedger { + hash: Fp::zero(), + total_currency: Amount::zero(), + }, + seed: Fp::zero(), + start_checkpoint: Fp::zero(), + lock_checkpoint: Fp::zero(), + epoch_length: Length::from_u32(0), + } +} + +fn test_constraint_constants() -> ConstraintConstants { + ConstraintConstants { + sub_windows_per_window: 11, + ledger_depth: 15, + work_delay: 2, + block_window_duration_ms: 180_000, + transaction_capacity_log_2: 7, + pending_coinbase_depth: 5, + coinbase_amount: 720_000_000_000, + supercharged_coinbase_factor: 2, + account_creation_fee: 1_000_000_000, + fork: None, + } +} + +fn create_test_ledger() -> Mask { + let db = Database::create(15); + let mut ledger = Mask::new_root(db); + let alice = mina_signer::PubKey::from_address( + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", + ) + .unwrap() + .into_compressed(); + let bob = mina_signer::PubKey::from_address( + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", + ) + .unwrap() + .into_compressed(); + + // Create Alice's account with balance + let alice_id = AccountId::new(alice, Default::default()); + let alice_account = Account::create_with(alice_id.clone(), Balance::from_u64(1_000_000_000)); + ledger + .get_or_create_account(alice_id, alice_account) + .unwrap(); + + // Create Bob's account + let bob_id = AccountId::new(bob, Default::default()); + let bob_account = Account::create_with(bob_id.clone(), Balance::from_u64(500_000_000)); + ledger.get_or_create_account(bob_id, bob_account).unwrap(); + + ledger +} + +fn create_payment( + from_pk: &mina_signer::CompressedPubKey, + to_pk: &mina_signer::CompressedPubKey, + amount: u64, + fee: u64, + nonce: u32, +) -> SignedCommand { + let payload = SignedCommandPayload { + common: Common { + fee: Fee::from_u64(fee), + fee_payer_pk: from_pk.clone(), + nonce: Nonce::from_u32(nonce), + valid_until: Slot::max(), + memo: Memo::empty(), + }, + body: Body::Payment(PaymentPayload { + receiver_pk: to_pk.clone(), + amount: Amount::from_u64(amount), + }), + }; + + SignedCommand { + payload, + signer: from_pk.clone(), + signature: mina_signer::Signature::dummy(), + } +} + +#[test] +fn test_apply_payment_success() { + let mut ledger = create_test_ledger(); + + let alice_pk = mina_signer::PubKey::from_address( + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", + ) + .unwrap() + .into_compressed(); + let bob_pk = mina_signer::PubKey::from_address( + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", + ) + .unwrap() + .into_compressed(); + + let alice_id = AccountId::new(alice_pk.clone(), Default::default()); + let bob_id = AccountId::new(bob_pk.clone(), Default::default()); + + // Record initial state + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_before = ledger.get(alice_location).unwrap(); + let bob_location = ledger.location_of_account(&bob_id).unwrap(); + let bob_before = ledger.get(bob_location).unwrap(); + + let initial_alice_balance = alice_before.balance; + let initial_bob_balance = bob_before.balance; + let initial_alice_nonce = alice_before.nonce; + let initial_alice_receipt_hash = alice_before.receipt_chain_hash; + + let amount = 100_000_000; + let fee = 10_000_000; + let nonce = 0; + let payment = create_payment(&alice_pk, &bob_pk, amount, fee, nonce); + + let constraint_constants = &test_constraint_constants(); + let state_view = ProtocolStateView { + snarked_ledger_hash: Fp::zero(), + blockchain_length: Length::from_u32(0), + min_window_density: Length::from_u32(0), + total_currency: Amount::zero(), + global_slot_since_genesis: Slot::from_u32(0), + staking_epoch_data: dummy_epoch_data(), + next_epoch_data: dummy_epoch_data(), + }; + let result = apply_transaction_first_pass( + &constraint_constants, + Slot::from_u32(0), + &state_view, + &mut ledger, + &Transaction::Command(UserCommand::SignedCommand(Box::new(payment))), + ); + + assert!(result.is_ok()); + + // Verify ledger state changes + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_after = ledger.get(alice_location).unwrap(); + let bob_location = ledger.location_of_account(&bob_id).unwrap(); + let bob_after = ledger.get(bob_location).unwrap(); + + // Verify Alice's balance decreased by fee + payment amount + let expected_alice_balance = initial_alice_balance + .sub_amount(Amount::from_u64(fee)) + .unwrap() + .sub_amount(Amount::from_u64(amount)) + .unwrap(); + assert_eq!( + alice_after.balance, expected_alice_balance, + "Alice's balance should decrease by fee + payment amount" + ); + + // Verify Alice's nonce incremented + assert_eq!( + alice_after.nonce, + initial_alice_nonce.incr(), + "Alice's nonce should be incremented" + ); + + // Verify Alice's receipt chain hash updated + assert_ne!( + alice_after.receipt_chain_hash, initial_alice_receipt_hash, + "Alice's receipt chain hash should be updated" + ); + + // Verify Bob's balance increased by payment amount + let expected_bob_balance = initial_bob_balance + .add_amount(Amount::from_u64(amount)) + .unwrap(); + assert_eq!( + bob_after.balance, expected_bob_balance, + "Bob's balance should increase by payment amount" + ); + + // Verify Bob's nonce unchanged (he's the receiver, not sender) + assert_eq!( + bob_after.nonce, bob_before.nonce, + "Bob's nonce should not change" + ); +} + +/// Test payment with insufficient balance for the payment amount. +/// +/// Even though the fee can be paid, the payment amount exceeds the remaining +/// balance after fee deduction. The transaction returns an error but the fee +/// has already been charged to ensure the network is compensated. +/// +/// Ledger state: Fee charged, nonce incremented, receipt chain hash updated. +/// Payment amount NOT transferred. +#[test] +fn test_apply_payment_insufficient_balance() { + let mut ledger = create_test_ledger(); + + let alice_pk = mina_signer::PubKey::from_address( + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", + ) + .unwrap() + .into_compressed(); + let bob_pk = mina_signer::PubKey::from_address( + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", + ) + .unwrap() + .into_compressed(); + + let alice_id = AccountId::new(alice_pk.clone(), Default::default()); + let bob_id = AccountId::new(bob_pk.clone(), Default::default()); + + // Record initial state + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_before = ledger.get(alice_location).unwrap(); + let bob_location = ledger.location_of_account(&bob_id).unwrap(); + let bob_before = ledger.get(bob_location).unwrap(); + let initial_alice_balance = alice_before.balance; + let initial_alice_nonce = alice_before.nonce; + let initial_alice_receipt_hash = alice_before.receipt_chain_hash; + let initial_bob_balance = bob_before.balance; + + let amount = 2_000_000_000; // More than Alice's balance + let fee = 10_000_000; + let nonce = 0; + let payment = create_payment(&alice_pk, &bob_pk, amount, fee, nonce); + + let constraint_constants = &test_constraint_constants(); + let state_view = ProtocolStateView { + snarked_ledger_hash: Fp::zero(), + blockchain_length: Length::from_u32(0), + min_window_density: Length::from_u32(0), + total_currency: Amount::zero(), + global_slot_since_genesis: Slot::from_u32(0), + staking_epoch_data: dummy_epoch_data(), + next_epoch_data: dummy_epoch_data(), + }; + let result = apply_transaction_first_pass( + &constraint_constants, + Slot::from_u32(0), + &state_view, + &mut ledger, + &Transaction::Command(UserCommand::SignedCommand(Box::new(payment))), + ); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Source_insufficient_balance"); + + // Verify ledger state: fee charged but payment not transferred + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_after = ledger.get(alice_location).unwrap(); + let bob_location = ledger.location_of_account(&bob_id).unwrap(); + let bob_after = ledger.get(bob_location).unwrap(); + + // Fee was charged + let expected_alice_balance = initial_alice_balance + .sub_amount(Amount::from_u64(fee)) + .unwrap(); + assert_eq!( + alice_after.balance, expected_alice_balance, + "Alice's balance should decrease by fee only" + ); + + // Nonce was incremented + assert_eq!( + alice_after.nonce, + initial_alice_nonce.incr(), + "Alice's nonce should be incremented" + ); + + // Receipt chain hash was updated + assert_ne!( + alice_after.receipt_chain_hash, initial_alice_receipt_hash, + "Alice's receipt chain hash should be updated" + ); + + // Payment was NOT transferred to Bob + assert_eq!( + bob_after.balance, initial_bob_balance, + "Bob's balance should remain unchanged" + ); +} + +/// Test payment with incorrect nonce. +/// +/// The transaction is rejected during fee payment validation because the +/// provided nonce does not match the account's current nonce. +/// +/// Ledger state: Remains unchanged (no fee charged, no nonce incremented). +#[test] +fn test_apply_payment_invalid_nonce() { + let mut ledger = create_test_ledger(); + + let alice_pk = mina_signer::PubKey::from_address( + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", + ) + .unwrap() + .into_compressed(); + let bob_pk = mina_signer::PubKey::from_address( + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", + ) + .unwrap() + .into_compressed(); + + let alice_id = AccountId::new(alice_pk.clone(), Default::default()); + + // Record initial state + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_before = ledger.get(alice_location).unwrap(); + let initial_alice_balance = alice_before.balance; + let initial_alice_nonce = alice_before.nonce; + + let amount = 100_000_000; + let fee = 10_000_000; + let nonce = 5; // Wrong nonce (should be 0) + let payment = create_payment(&alice_pk, &bob_pk, amount, fee, nonce); + + let constraint_constants = &test_constraint_constants(); + let state_view = ProtocolStateView { + snarked_ledger_hash: Fp::zero(), + blockchain_length: Length::from_u32(0), + min_window_density: Length::from_u32(0), + total_currency: Amount::zero(), + global_slot_since_genesis: Slot::from_u32(0), + staking_epoch_data: dummy_epoch_data(), + next_epoch_data: dummy_epoch_data(), + }; + let result = apply_transaction_first_pass( + &constraint_constants, + Slot::from_u32(0), + &state_view, + &mut ledger, + &Transaction::Command(UserCommand::SignedCommand(Box::new(payment))), + ); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + "Nonce in account Nonce(0) different from nonce in transaction Nonce(5)" + ); + + // Verify ledger state unchanged + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_after = ledger.get(alice_location).unwrap(); + assert_eq!( + alice_after.balance, initial_alice_balance, + "Alice's balance should remain unchanged" + ); + assert_eq!( + alice_after.nonce, initial_alice_nonce, + "Alice's nonce should remain unchanged" + ); +} + +/// Test payment from a nonexistent fee payer account. +/// +/// The transaction is rejected during fee payment validation because the +/// fee payer account does not exist in the ledger. +/// +/// Ledger state: Remains unchanged (no new account created). +#[test] +fn test_apply_payment_nonexistent_fee_payer() { + let db = Database::create(15); + let mut ledger = Mask::new_root(db); + + let alice_pk = mina_signer::PubKey::from_address( + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", + ) + .unwrap() + .into_compressed(); + let bob_pk = mina_signer::PubKey::from_address( + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", + ) + .unwrap() + .into_compressed(); + + let alice_id = AccountId::new(alice_pk.clone(), Default::default()); + + // Verify Alice's account does not exist before the transaction + assert!( + ledger.location_of_account(&alice_id).is_none(), + "Alice's account should not exist before transaction" + ); + + let amount = 100_000_000; + let fee = 10_000_000; + let nonce = 0; + let payment = create_payment(&alice_pk, &bob_pk, amount, fee, nonce); + + let constraint_constants = &test_constraint_constants(); + let state_view = ProtocolStateView { + snarked_ledger_hash: Fp::zero(), + blockchain_length: Length::from_u32(0), + min_window_density: Length::from_u32(0), + total_currency: Amount::zero(), + global_slot_since_genesis: Slot::from_u32(0), + staking_epoch_data: dummy_epoch_data(), + next_epoch_data: dummy_epoch_data(), + }; + let result = apply_transaction_first_pass( + &constraint_constants, + Slot::from_u32(0), + &state_view, + &mut ledger, + &Transaction::Command(UserCommand::SignedCommand(Box::new(payment))), + ); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "The fee-payer account does not exist"); + + // Verify Alice's account still does not exist after the error + assert!( + ledger.location_of_account(&alice_id).is_none(), + "Alice's account should still not exist after transaction error" + ); +}