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/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}" diff --git a/.github/workflows/check-ocaml-refs.yaml b/.github/workflows/check-ocaml-refs.yaml new file mode 100644 index 000000000..12184e24c --- /dev/null +++ b/.github/workflows/check-ocaml-refs.yaml @@ -0,0 +1,195 @@ +# 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: + pull_request: + branches: + - develop + - main + 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 + # Capture output to file + ./.github/scripts/check-ocaml-refs.sh \ + --repo "$OCAML_REPO" \ + --branch "$OCAML_BRANCH" 2>&1 | tee validation_output.txt + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "has_issues=true" >> $GITHUB_OUTPUT + 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 with hidden identifier + cat > comment.md < + ## OCaml Reference Validation Results + + **Repository**: ${OCAML_REPO} + **Branch**: ${OCAML_BRANCH} + **Status**: ${STATUS_MSG} + +
+ 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: Find existing OCaml validation comment + if: github.event_name == 'pull_request' + uses: peter-evans/find-comment@v4 + 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@v5 + 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' + 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 + if: github.event_name != 'pull_request' + 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' && github.event_name != 'pull_request' + 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 [ "${{ 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 + echo "✓ All references are up to date" >> $GITHUB_STEP_SUMMARY + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 118808c5a..2cb4310ea 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,15 @@ 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)) +- **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 diff --git a/CLAUDE.md b/CLAUDE.md index 0b59440f8..865288a9c 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 @@ -238,10 +272,10 @@ files. **Capitalization in headings and bullet points:** -- Use lowercase for section headings (e.g., "Test design", "Resource - management") -- Use lowercase for bullet point labels (e.g., "**Connection policies**", - "**State inspection**") +- Capitalize the first letter of section headings (e.g., "Test design", + "Resource management") +- Use lowercase for bullet point labels (e.g., "**connection policies**", + "**state inspection**") - Maintain proper capitalization for proper nouns and technical terms - Apply this style consistently across all documentation files 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 7d486d3ca..36e7e2ef0 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: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// 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: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// 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: 5da42ccd72e791f164d4d200cf1ce300262873b3 +/// Last verified: 2025-10-08 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] pub struct WithStatus { pub data: T, @@ -259,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, @@ -284,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); @@ -312,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()) @@ -320,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))), @@ -359,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, @@ -401,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, @@ -423,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, @@ -449,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() @@ -552,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]) @@ -579,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"); @@ -600,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); @@ -661,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, @@ -672,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() @@ -708,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(), @@ -742,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), @@ -754,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, @@ -977,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::*; @@ -992,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, @@ -1011,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() @@ -1031,285 +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) - } - } - - /// - #[derive(Debug)] - pub struct InitLedger(pub Vec<(Keypair, u64)>); - - /// - #[derive(Debug)] - pub struct TransactionSpec { - pub fee: Fee, - pub sender: (Keypair, Nonce), - pub receiver: CompressedPubKey, - pub amount: Amount, - } - - /// - #[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; diff --git a/ledger/src/scan_state/transaction_logic/signed_command.rs b/ledger/src/scan_state/transaction_logic/signed_command.rs index ad38350e9..37172933f 100644 --- a/ledger/src/scan_state/transaction_logic/signed_command.rs +++ b/ledger/src/scan_state/transaction_logic/signed_command.rs @@ -12,26 +12,42 @@ use crate::{ use super::{zkapp_command::AccessedOrNot, Memo, TransactionStatus}; -/// +/// Common fields shared by all signed command payloads. +/// +/// OCaml reference: #[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: #[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 { @@ -48,17 +64,25 @@ impl StakeDelegationPayload { } } -/// +/// The body of a signed command, which can be either a payment or stake delegation. +/// +/// OCaml reference: #[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: #[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, } @@ -102,12 +126,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, } 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 +} 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/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. diff --git a/website/docs/developers/transactions.md b/website/docs/developers/transactions.md new file mode 100644 index 000000000..4a2090c0a --- /dev/null +++ b/website/docs/developers/transactions.md @@ -0,0 +1,346 @@ +--- +title: Transactions +description: Understanding how transactions modify the ledger in Mina +--- + +# Transactions + +Transactions are the primary mechanism for modifying the ledger state in Mina. +This document explains the different types of transactions, their structures, +and how they interact with the ledger. + +## Overview + +The transaction system in Mina is implemented in the ledger crate, specifically +in +[`ledger/src/scan_state/transaction_logic/mod.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs). +Transactions modify account balances, permissions, and other state in the ledger +through a two-pass application process. + +## Transaction types + +All transactions in Mina are represented by the `Transaction` enum: + + + +```rust reference title="ledger/src/scan_state/transaction_logic/mod.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L996-L1001 +``` + +### User commands + +User commands are transactions initiated by users. They are represented by the +`UserCommand` enum: + + + +```rust reference title="ledger/src/scan_state/transaction_logic/mod.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L642-L646 +``` + +#### Signed commands + +Signed commands are traditional transactions that transfer value or delegate +stake. A `SignedCommand` consists of a payload, signer, and signature: + + + +```rust reference title="ledger/src/scan_state/transaction_logic/signed_command.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/signed_command.rs#L129-L144 +``` + +The payload includes common fields shared by all signed commands: + + + +```rust reference title="ledger/src/scan_state/transaction_logic/signed_command.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/signed_command.rs#L15-L30 +``` + +The body can be one of two types: + + + +```rust reference title="ledger/src/scan_state/transaction_logic/signed_command.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/signed_command.rs#L67-L76 +``` + +**Payment** + +Transfers MINA tokens from the fee payer to a receiver: + + + +```rust reference title="ledger/src/scan_state/transaction_logic/signed_command.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/signed_command.rs#L32-L39 +``` + +**Stake delegation** + +Delegates the fee payer's stake to another account: + + + +```rust reference title="ledger/src/scan_state/transaction_logic/signed_command.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/signed_command.rs#L41-L51 +``` + +#### zkApp commands + +zkApp commands are more complex transactions that can update multiple accounts +and execute zero-knowledge smart contracts. These are documented in detail in +the [zkApps documentation](./zkapps.md). + +### Fee transfers + +Fee transfers are created by block producers to collect fees from transactions +in a block. A +[`FeeTransfer`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L295-L308) +can contain one or two transfers: + +```rust +pub struct FeeTransfer(pub(super) OneOrTwo); +``` + +Each +[`SingleFeeTransfer`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L268-L276) +includes: + +- `receiver_pk`: Public key receiving the fee +- `fee`: Amount of the fee +- `fee_token`: Token ID for the fee (must be default token) + + + +:::note + +Fee transfers have an important invariant: when combining two single fee +transfers, they must use the same token. This is enforced in the +[`of_singles`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L335-L354) +method to ensure the transaction SNARK only handles fee excesses in a single +token. + +::: + + + +### Coinbase + +Coinbase transactions create new MINA tokens as block rewards. A +[`Coinbase`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L376-L384) +includes: + +- `receiver`: Public key receiving the reward +- `amount`: Total coinbase amount +- `fee_transfer`: Optional fee transfer to pay the SNARK worker + +The coinbase may include an optional +[`CoinbaseFeeTransfer`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L357-L375) +to compensate SNARK workers. If the receiver and fee transfer recipient are the +same, the fee transfer is removed. + +## Transaction application + +Transactions are applied to the ledger through a two-pass process implemented in +[`transaction_partially_applied.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs). + +### First pass + +The +[`apply_transaction_first_pass`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs#L49-L109) +function: + +1. Records the previous ledger hash +2. Validates the transaction +3. Applies initial state changes +4. For signed commands and fee transfers, fully applies the transaction +5. For zkApp commands, performs the first phase of application + +The function returns a +[`TransactionPartiallyApplied`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs#L23-L29) +which contains the transaction state after the first pass. + +### Second pass + +The +[`apply_transaction_second_pass`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs#L111-L158) +function: + +1. For signed commands, fee transfers, and coinbase: returns the already-applied + transaction +2. For zkApp commands: completes the second phase of application + +This two-pass system allows zkApp commands to properly handle their complex +state transitions while keeping simpler transactions efficient. + +### Applying user commands + +User commands (signed commands) are applied through +[`apply_user_command`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs#L982-L999), +which: + +1. Validates the transaction is not expired +2. Pays the fee from the fee payer's account +3. Updates the fee payer's nonce and receipt chain hash +4. Applies the command body (payment or stake delegation) +5. Returns the application status + +## Transaction status and failures + +Each transaction has a +[`TransactionStatus`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L181-L188) +indicating whether it succeeded or failed: + +```rust +pub enum TransactionStatus { + Applied, + Failed(Vec>), +} +``` + +When a transaction fails, it includes one or more +[`TransactionFailure`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L63-L113) +reasons: + +### Common failures + +- **`SourceNotPresent`**: Source account doesn't exist +- **`ReceiverNotPresent`**: Receiver account doesn't exist (for stake + delegation) +- **`SourceInsufficientBalance`**: Source doesn't have enough funds +- **`AmountInsufficientToCreateAccount`**: Payment amount is less than account + creation fee +- **`ReceiverAlreadyExists`**: Attempted to create an account that already + exists +- **`Overflow`**: Arithmetic overflow in balance calculations + +### Permission failures + +- **`UpdateNotPermittedBalance`**: Account permissions don't allow balance + changes +- **`UpdateNotPermittedAccess`**: Account permissions don't allow access +- **`UpdateNotPermittedDelegate`**: Account permissions don't allow delegate + changes +- **`UpdateNotPermittedNonce`**: Account permissions don't allow nonce changes + +### zkApp-specific failures + +For zkApp commands, additional failures are possible related to preconditions, +app state, verification keys, and more. These are documented in the zkApps +documentation. + +## Transaction fee handling + +### Fee payment + +Fees are paid from the fee payer's account before the transaction body is +applied. The +[`pay_fee`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs#L1001-L1030) +function: + +1. Verifies the signer matches the fee payer +2. Verifies the fee token is the default token +3. Deducts the fee from the account balance +4. Increments the account nonce +5. Updates the receipt chain hash +6. Validates timing constraints + + + +:::note + +Even if a transaction fails, the fee is still deducted and the nonce is +incremented. This ensures the network is compensated for processing the +transaction and prevents replay attacks. + +::: + + + +### Fee excess + +The +[`fee_excess`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L1008-L1018) +method calculates the net change in token supply from a transaction: + +- User commands: Positive (fee is paid) +- Fee transfers: Negative (fee is received) +- Coinbase: Zero (new supply equals coinbase amount) + +Fee excesses are tracked to ensure blocks maintain proper token supply +constraints. + +## Account creation + +When a transaction references a non-existent account: + +1. A new account is created with default permissions +2. An account creation fee is deducted from the transaction amount +3. The new account is added to the ledger + +The account creation fee is defined in +[`constraint_constants.account_creation_fee`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs#L447-L466) +and ensures the ledger doesn't grow unbounded with dust accounts. + +## Timing constraints + +Accounts can have timing constraints that control when funds can be spent. The +[`validate_timing`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs) +function (in `transaction_union_payload.rs`) checks: + +- Whether the account is timed or untimed +- If timed, whether the current slot allows the withdrawal +- If the withdrawal amount exceeds the currently available balance + +Timing validation is performed during transaction application and can cause +transactions to fail if constraints aren't met. + +## Memo field + +Every signed command includes a +[`Memo`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic/mod.rs#L485-L640) +field (34 bytes): + +- Byte 0: Tag (0x00 for digest, 0x01 for bytes) +- Byte 1: Length +- Bytes 2-33: Data (or hash of data if too long) + +Memos allow users to attach messages or identifiers to transactions. For strings +longer than 32 bytes, a Blake2b digest is stored instead. + +## Integration with scan state + +Transactions flow through the scan state for proof generation: + +1. Transactions enter the staged ledger +2. They're added to the scan state's parallel scan tree +3. SNARK workers generate proofs for transaction correctness +4. Completed proofs allow transactions to be finalized +5. The ledger is updated with the final state + +The scan state coordinates parallel proof generation to maximize throughput +while maintaining correctness. See the +[scan state documentation](../researchers/scan-state.md) for details. + +## Code organization + +The transaction logic is organized into several modules: + +- **`mod.rs`**: Core transaction types and top-level logic +- **`signed_command.rs`**: Signed command structures and validation +- **`transaction_partially_applied.rs`**: Transaction application logic +- **`transaction_applied.rs`**: Applied transaction results +- **`transaction_union_payload.rs`**: Shared payload handling +- **`protocol_state.rs`**: Protocol state views for validation +- **`local_state.rs`**: Local state for zkApp execution +- **`zkapp_command.rs`**: zkApp command structures (documented separately) +- **`valid.rs`**: Validated transaction types +- **`verifiable.rs`**: Verifiable transaction types for proof generation + +## Related documentation + +- [zkApps](./zkapps.md): Documentation for zkApp transactions +- [Ledger crate](./ledger-crate.md): Overall ledger architecture +- [Scan state](../researchers/scan-state.md): How transactions are proven +- [Architecture](./architecture.md): State machine patterns used in transaction + processing diff --git a/website/docs/developers/zkapps.md b/website/docs/developers/zkapps.md new file mode 100644 index 000000000..d34840164 --- /dev/null +++ b/website/docs/developers/zkapps.md @@ -0,0 +1,244 @@ +--- +sidebar_position: 6 +title: zkApps and zkApp Commands +description: + Understanding zkApps and zkApp commands in the Mina protocol implementation +slug: /developers/zkapps +--- + +# zkApps and zkApp Commands + +## Overview + +zkApps (zero-knowledge applications) are programmable smart contracts on the +Mina blockchain that leverage zero-knowledge proofs for private, verifiable +computation. A **zkApp command** is the transaction type used to interact with +zkApps, containing account updates that can be authorized by signatures or +zero-knowledge proofs. + +This document provides an entry point for developers joining the team to +understand how zkApps are implemented in the Rust codebase. + +## What is a zkApp? + +A zkApp is a smart contract on Mina that: + +- Stores state in on-chain accounts (8 field elements of app state) +- Executes logic verified by zero-knowledge proofs +- Can interact with multiple accounts atomically +- Supports custom permissions and verification keys + +Unlike traditional smart contracts that execute on-chain, zkApp logic executes +off-chain and produces a zero-knowledge proof that the computation was performed +correctly. Only the proof is verified on-chain, keeping the blockchain +lightweight. + +## What is a zkApp Command? + +A **zkApp command** (`ZkAppCommand`) is a transaction type that applies updates +to multiple accounts atomically. It consists of: + +### Structure + + + +```rust reference title="ledger/src/scan_state/transaction_logic.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L3588-L3592 +``` + +### Components + +#### 1. Fee Payer + +The account that pays for the transaction. It must be authorized with a +signature and its nonce must increase. + +#### 2. Account Updates + +A forest (list of trees) of account updates, where each `AccountUpdate`: + +- Specifies an account to modify (identified by public key and token) +- Declares preconditions that must be satisfied +- Specifies state changes (balance, app state, permissions, etc.) +- Provides authorization (signature, proof, or none) +- Can make nested calls to other accounts + +**Key type:** + + + +```rust reference title="ledger/src/scan_state/transaction_logic.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L2758-L2761 +``` + +#### 3. Memo + +Optional data field (up to 32 bytes) for auxiliary information. + +## Authorization Methods + + + +```rust reference +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L2645-L2659 +``` + +## Transaction Commitments and Signing + +zkApp commands use transaction commitments for signing: + +1. **Partial commitment** - Hash of account updates (for account update + signatures) +2. **Full commitment** - Includes memo and fee payer (for fee payer signature) + +Account updates can specify whether to use the full commitment via the +`use_full_commitment` flag. + +### Commitment Computation + + + +```rust reference title="ledger/src/generators/zkapp_command_builder.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/generators/zkapp_command_builder.rs#L11-L22 +``` + +### Signing Implementation + +The signing logic replaces dummy authorizations with valid signatures/proofs: + + + +```rust reference title="ledger/src/generators/zkapp_command_builder.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/generators/zkapp_command_builder.rs#L28-L44 +``` + + +:::note + +The actual cryptographic signature generation is currently a TODO in the +codebase (returns `Signature::dummy()`). + +::: + + +## Application Logic + +The core zkApp business logic is implemented in: + +- **[`ledger/src/zkapps/zkapp_logic.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/zkapp_logic.rs)** - + Main application logic for processing zkApp commands +- **[`ledger/src/zkapps/non_snark.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/non_snark.rs)** - + Non-SNARK verification paths +- **[`ledger/src/zkapps/snark.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/snark.rs)** - + SNARK-based verification +- **[`ledger/src/zkapps/checks.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/checks.rs)** - + Precondition and permission checks + +## Processing Pipeline + +When a zkApp command is applied to the ledger: + +1. **Validation** - Check transaction is well-formed and within cost limits +2. **Fee payer verification** - Verify fee payer signature and sufficient + balance +3. **Account update processing** - For each account update: + - Verify authorization (signature or proof) + - Check preconditions (account state, protocol state) + - Check permissions + - Apply state changes atomically +4. **Failure handling** - If any step fails, the entire command fails (except + fee is still deducted) + +**Key entry point:** +[`ledger/src/scan_state/transaction_logic.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs) +(search for `apply_zkapp_command`) + +## Cost and Weight Limits + +zkApp commands have cost limits to prevent resource exhaustion: + + + +```rust reference title="ledger/src/scan_state/transaction_logic.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L3668-L3684 +``` + +The cost considers: + +- Number of proof verifications +- Number of signature verifications +- Whether signatures verify single or paired account updates + +## Testing and Generators + +For testing, the codebase includes zkApp command generators: + +- **[`ledger/src/generators/zkapp_command.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/generators/zkapp_command.rs)** - + Generates random zkApp commands for testing +- **[`ledger/src/generators/zkapp_command_builder.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/generators/zkapp_command_builder.rs)** - + Utility functions for building and signing zkApp commands + +## Key Files Reference + +### Core Types + +- [`ledger/src/scan_state/transaction_logic.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs) - + `ZkAppCommand`, `AccountUpdate`, `Control`, `FeePayer` + +### Business Logic + +- [`ledger/src/zkapps/zkapp_logic.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/zkapp_logic.rs) - + Main zkApp processing logic +- [`ledger/src/zkapps/checks.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/checks.rs) - + Precondition and permission validation +- [`ledger/src/zkapps/snark.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/snark.rs) - + SNARK verification implementation +- [`ledger/src/zkapps/non_snark.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/non_snark.rs) - + Non-SNARK paths + +### Signing and Building + +- [`ledger/src/generators/zkapp_command_builder.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/generators/zkapp_command_builder.rs) - + Transaction signing and authorization + +### Proofs + +- [`ledger/src/proofs/zkapp.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/proofs/zkapp.rs) - + zkApp proof verification + +## Transaction Failures + + + +```rust reference title="ledger/src/scan_state/transaction_logic.rs" +https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L59-L154 +``` + +## Comparison to Other Transaction Types + +Mina has three transaction types: + +1. **[Signed Commands](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L849)** - + Simple payments and delegation changes, authorized by signature +2. **[zkApp Commands](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L3588)** - + Complex multi-account updates with proof or signature authorization +3. **[Coinbase](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs#L500)** - + Block rewards (internal, not user-submitted) + +zkApp commands are more powerful but have higher complexity and cost limits. + +## Getting Started + +To start working with zkApps in the codebase: + +1. Read the core types in + [`ledger/src/scan_state/transaction_logic.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/scan_state/transaction_logic.rs) + (search for `ZkAppCommand`) +2. Explore the application logic in + [`ledger/src/zkapps/zkapp_logic.rs`](https://github.com/o1-labs/mina-rust/blob/develop/ledger/src/zkapps/zkapp_logic.rs) +3. Review test generators in + [`ledger/src/generators/`](https://github.com/o1-labs/mina-rust/tree/develop/ledger/src/generators) +4. Study how zkApp commands flow through the transaction pool and staged ledger + +For questions, consult the inline documentation in the source files or refer to +the OCaml implementation for semantic clarity. diff --git a/website/sidebars.ts b/website/sidebars.ts index 962b0621d..bb54cf587 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -83,6 +83,8 @@ const sidebars: SidebarsConfig = { 'developers/architecture', 'developers/circuits', 'developers/ledger-crate', + 'developers/transactions', + 'developers/zkapps', ], }, { @@ -137,7 +139,8 @@ const sidebars: SidebarsConfig = { type: 'category', label: 'Documentation', items: [ - 'developers/documentation-guidelines', + 'developers/referencing-code-in-documentation', + 'developers/ocaml-reference-tracking', ], }, {