diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5619c12082..d3d10ccb9e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -17,6 +17,10 @@ env: MINA_PANIC_ON_BUG: true CARGO_INCREMENTAL: 1 RUSTFLAGS: "-C overflow-checks=off -C debug-assertions=off" + RUST_STABLE_VERSION: "1.84" + RUST_NIGHTLY_VERSION: "nightly" + OCAML_VERSION: "4.14.2" + CACHE_VERSION: "v0" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -31,7 +35,7 @@ jobs: - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: 1.84 + toolchain: ${{ env.RUST_STABLE_VERSION }} enable-cache: false - name: Clean cargo cache run: cargo clean @@ -39,9 +43,6 @@ jobs: ledger-tests: timeout-minutes: 20 runs-on: ubuntu-24.04 - strategy: - matrix: - ocaml_version: [4.14.2] steps: - name: Git checkout uses: actions/checkout@v5 @@ -52,13 +53,13 @@ jobs: - name: Use shared OCaml setting up steps uses: ./.github/actions/setup-ocaml with: - ocaml_version: ${{ matrix.ocaml_version }} + ocaml_version: ${{ env.OCAML_VERSION }} - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: nightly - cache-prefix: ledger-v0 + toolchain: ${{ env.RUST_NIGHTLY_VERSION }} + cache-prefix: ledger-${{ env.CACHE_VERSION }} - name: Download circuits files uses: ./.github/actions/setup-circuits @@ -82,8 +83,8 @@ jobs: - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: nightly - cache-prefix: p2p-messages-v0 + toolchain: ${{ env.RUST_NIGHTLY_VERSION }} + cache-prefix: p2p-messages-${{ env.CACHE_VERSION }} - name: Download circuits files uses: ./.github/actions/setup-circuits @@ -127,9 +128,6 @@ jobs: vrf-tests: timeout-minutes: 20 runs-on: ubuntu-24.04 - strategy: - matrix: - ocaml_version: [4.14.2] steps: - name: Git checkout uses: actions/checkout@v5 @@ -140,13 +138,13 @@ jobs: - name: Use shared OCaml setting up steps uses: ./.github/actions/setup-ocaml with: - ocaml_version: ${{ matrix.ocaml_version }} + ocaml_version: ${{ env.OCAML_VERSION }} - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: nightly - cache-prefix: vrf-v0 + toolchain: ${{ env.RUST_NIGHTLY_VERSION }} + cache-prefix: vrf-${{ env.CACHE_VERSION }} - name: Build vrf tests run: make build-vrf @@ -167,8 +165,8 @@ jobs: - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: 1.84 - cache-prefix: p2p-v0 + toolchain: ${{ env.RUST_STABLE_VERSION }} + cache-prefix: p2p-${{ env.CACHE_VERSION }} - name: Test p2p crate run: make test-p2p @@ -187,13 +185,13 @@ jobs: - name: Use shared OCaml setting up steps uses: ./.github/actions/setup-ocaml with: - ocaml_version: 4.14.2 + ocaml_version: ${{ env.OCAML_VERSION }} - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: 1.84 - cache-prefix: build-v0 + toolchain: ${{ env.RUST_STABLE_VERSION }} + cache-prefix: build-${{ env.CACHE_VERSION }} - name: Release build run: make build-release @@ -222,6 +220,25 @@ jobs: path: target/release/mina retention-days: 7 + account-tests: + timeout-minutes: 20 + runs-on: ubuntu-24.04 + steps: + - name: Git checkout + uses: actions/checkout@v5 + + - name: Setup build dependencies + uses: ./.github/actions/setup-build-deps + + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + toolchain: ${{ env.RUST_STABLE_VERSION }} + cache-prefix: build-${{ env.CACHE_VERSION }} + + - name: Run account tests + run: make test-account + build-tests: timeout-minutes: 60 runs-on: ubuntu-22.04 @@ -235,13 +252,13 @@ jobs: - name: Use shared OCaml setting up steps uses: ./.github/actions/setup-ocaml with: - ocaml_version: 4.14.2 + ocaml_version: ${{ env.OCAML_VERSION }} - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: 1.84 - cache-prefix: build-tests-v0 + toolchain: ${{ env.RUST_STABLE_VERSION }} + cache-prefix: build-tests-${{ env.CACHE_VERSION }} - name: Build tests run: make build-tests @@ -266,13 +283,13 @@ jobs: - name: Use shared OCaml setting up steps uses: ./.github/actions/setup-ocaml with: - ocaml_version: 4.14.2 + ocaml_version: ${{ env.OCAML_VERSION }} - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: 1.84 - cache-prefix: build-tests-webrtc-v0 + toolchain: ${{ env.RUST_STABLE_VERSION }} + cache-prefix: build-tests-webrtc-${{ env.CACHE_VERSION }} - name: Build tests run: make build-tests-webrtc @@ -297,8 +314,8 @@ jobs: - name: Setup Rust uses: ./.github/actions/setup-rust with: - toolchain: nightly - cache-prefix: build-wasm-v0 + toolchain: ${{ env.RUST_NIGHTLY_VERSION }} + cache-prefix: build-wasm-${{ env.CACHE_VERSION }} - name: Setup wasm tooling run: make setup-wasm diff --git a/CHANGELOG.md b/CHANGELOG.md index e0202e0531..c0d09da06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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)). +- **mina-node-account**: move tests into `node/account/tests`, document the + library and run the tests in CI + ([#1540](https://github.com/o1-labs/mina-rust/pull/1540)). ### Changed diff --git a/Makefile b/Makefile index 1753da2892..0c19b43472 100644 --- a/Makefile +++ b/Makefile @@ -302,7 +302,12 @@ test-release: ## Run tests in release mode .PHONY: test-vrf test-vrf: ## Run VRF tests, requires nightly Rust - @cd vrf && cargo +$(NIGHTLY_RUST_VERSION) test --release -- -Z unstable-options --report-time + @cd vrf && cargo +$(NIGHTLY_RUST_VERSION) test --release -- \ + -Z unstable-options --report-time + +.PHONY: test-account +test-account: ## Run account tests + @cargo test -p mina-node-account .PHONY: test-p2p-messages test-p2p-messages: diff --git a/node/account/Cargo.toml b/node/account/Cargo.toml index 0e6839012d..f75dd276a1 100644 --- a/node/account/Cargo.toml +++ b/node/account/Cargo.toml @@ -3,6 +3,7 @@ name = "mina-node-account" version = "0.17.0" edition = "2021" license = "Apache-2.0" +description = "Account management for Mina nodes, including key generation, encryption, and address handling" [dependencies] anyhow = { workspace = true } diff --git a/node/account/src/lib.rs b/node/account/src/lib.rs index 2862ada6fe..15a6f84d4d 100644 --- a/node/account/src/lib.rs +++ b/node/account/src/lib.rs @@ -1,5 +1,68 @@ -mod secret_key; -pub use secret_key::AccountSecretKey; +//! Account management for Mina nodes +//! +//! This crate provides a high-level interface for managing Mina accounts, +//! built on top of the +//! [`mina-signer`](https://github.com/o1-labs/proof-systems/tree/master/signer) +//! crate. It handles cryptographic key generation, encryption/decryption of +//! secret keys, and address handling. +//! +//! # Overview +//! +//! The crate exports two main types: +//! - [`AccountSecretKey`] - Represents a private key that can be used to +//! sign transactions +//! - [`AccountPublicKey`] - Represents a public key/address for receiving +//! transactions +//! +//! # Key Features +//! +//! - **Key Generation**: Generate new random keypairs for Mina accounts +//! - **Key Encryption**: Encrypt and decrypt secret keys using password-based +//! encryption +//! - **Address Format**: Encode and decode Mina addresses using the standard +//! Base58Check format +//! - **Key Import/Export**: Read and write encrypted keys from/to files +//! +//! # Example Usage +//! +//! ``` +//! use mina_node_account::{AccountSecretKey, AccountPublicKey}; +//! use std::env; +//! +//! // Generate a new keypair +//! let secret_key = AccountSecretKey::rand(); +//! let public_key = secret_key.public_key(); +//! +//! // Save encrypted key to file in temp directory +//! let temp_dir = env::temp_dir(); +//! let path = temp_dir.join(format!("test-wallet-{}", public_key)); +//! let password = "secure-password"; +//! secret_key.to_encrypted_file(&path, password) +//! .expect("Failed to save key"); +//! +//! // Load encrypted key from file +//! let loaded_key = AccountSecretKey::from_encrypted_file(&path, password) +//! .expect("Failed to load key"); +//! +//! // Get the public address +//! let address = AccountPublicKey::from(loaded_key.public_key()); +//! println!("Address: {}", address); +//! +//! // Verify the keys match +//! assert_eq!(secret_key.public_key().to_string(), +//! loaded_key.public_key().to_string()); +//! +//! // Clean up +//! std::fs::remove_file(&path).ok(); +//! ``` +//! +//! # Cryptography +//! +//! Mina uses the Pasta curves (Pallas and Vesta) for its cryptographic +//! operations. These curves are specifically designed for efficient +//! recursive zero-knowledge proof composition. mod public_key; +mod secret_key; pub use public_key::AccountPublicKey; +pub use secret_key::AccountSecretKey; diff --git a/node/account/src/public_key.rs b/node/account/src/public_key.rs index a72b994916..df1439f54a 100644 --- a/node/account/src/public_key.rs +++ b/node/account/src/public_key.rs @@ -1,5 +1,3 @@ -use std::{fmt, str::FromStr}; - use mina_p2p_messages::{ b58::FromBase58CheckError, binprot::{ @@ -8,9 +6,9 @@ use mina_p2p_messages::{ }, v2::{NonZeroCurvePoint, NonZeroCurvePointUncompressedStableV1}, }; -use serde::{Deserialize, Serialize}; - use mina_signer::{CompressedPubKey, PubKey}; +use serde::{Deserialize, Serialize}; +use std::{fmt, str::FromStr}; #[derive( BinProtWrite, BinProtRead, Serialize, Deserialize, Debug, Ord, PartialOrd, Eq, PartialEq, Clone, diff --git a/node/account/src/secret_key.rs b/node/account/src/secret_key.rs index 5be8c7b658..aeaa8a029b 100644 --- a/node/account/src/secret_key.rs +++ b/node/account/src/secret_key.rs @@ -1,5 +1,4 @@ -use std::{fmt, fs, io, path::Path, str::FromStr}; - +use super::AccountPublicKey; use mina_core::{ constants::GENESIS_PRODUCER_SK, EncryptedSecretKey, EncryptedSecretKeyFile, EncryptionError, }; @@ -7,8 +6,7 @@ use mina_p2p_messages::{bigint::BigInt, v2::SignatureLibPrivateKeyStableV1}; use mina_signer::{keypair::KeypairError, seckey::SecKeyError, CompressedPubKey, Keypair}; use rand::{rngs::StdRng, CryptoRng, Rng, SeedableRng}; use serde::{Deserialize, Serialize}; - -use super::AccountPublicKey; +use std::{fmt, fs, io, path::Path, str::FromStr}; #[derive(Clone)] pub struct AccountSecretKey(Keypair); @@ -193,70 +191,3 @@ impl<'de> serde::Deserialize<'de> for AccountSecretKey { b58.parse().map_err(serde::de::Error::custom) } } - -#[cfg(test)] -mod tests { - use std::env; - - use super::*; - - #[test] - fn test_account_secret_key_bs58check_decode() { - let parsed: AccountSecretKey = "EKFWgzXsoMYcP1Hnj7dBhsefxNucZ6wyz676Qg5uMFNzytXAi2Ww" - .parse() - .unwrap(); - assert_eq!( - parsed.0.get_address(), - "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS" - ); - } - - #[test] - fn test_account_secret_key_display() { - let parsed: AccountSecretKey = "EKFWgzXsoMYcP1Hnj7dBhsefxNucZ6wyz676Qg5uMFNzytXAi2Ww" - .parse() - .unwrap(); - assert_eq!( - &parsed.to_string(), - "EKFWgzXsoMYcP1Hnj7dBhsefxNucZ6wyz676Qg5uMFNzytXAi2Ww" - ); - } - - #[test] - fn test_encrypt_decrypt() { - let password = "not-very-secure-pass"; - - let new_key = AccountSecretKey::rand(); - let tmp_dir = env::temp_dir(); - let tmp_path = format!("{}/{}-key", tmp_dir.display(), new_key.public_key()); - - // dump encrypted file - new_key - .to_encrypted_file(&tmp_path, password) - .expect("Failed to encrypt secret key"); - - // load and decrypt - let decrypted = AccountSecretKey::from_encrypted_file(&tmp_path, password) - .unwrap_or_else(|_| panic!("Failed to decrypt secret key file: {}", tmp_path)); - - assert_eq!( - new_key.public_key(), - decrypted.public_key(), - "Encrypted and decrypted public keys do not match" - ); - } - - #[test] - fn test_ocaml_key_decrypt() { - let password = "not-very-secure-pass"; - let key_path = "../tests/files/accounts/test-key-1"; - let expected_public_key = "B62qmg7n4XqU3SFwx9KD9B7gxsKwxJP5GmxtBpHp1uxyN3grujii9a1"; - let decrypted = AccountSecretKey::from_encrypted_file(key_path, password) - .unwrap_or_else(|_| panic!("Failed to decrypt secret key file: {}", key_path)); - - assert_eq!( - expected_public_key.to_string(), - decrypted.public_key().to_string() - ) - } -} diff --git a/node/account/tests/test_secret_key.rs b/node/account/tests/test_secret_key.rs new file mode 100644 index 0000000000..63d4ed5dee --- /dev/null +++ b/node/account/tests/test_secret_key.rs @@ -0,0 +1,50 @@ +use std::env; + +use mina_node_account::AccountSecretKey; + +#[test] +fn test_account_secret_key_bs58check_decode() { + let parsed: AccountSecretKey = "EKFWgzXsoMYcP1Hnj7dBhsefxNucZ6wyz676Qg5uMFNzytXAi2Ww" + .parse() + .unwrap(); + // Test by comparing the public key + assert_eq!( + parsed.public_key().to_string(), + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS" + ); +} + +#[test] +fn test_account_secret_key_display() { + let parsed: AccountSecretKey = "EKFWgzXsoMYcP1Hnj7dBhsefxNucZ6wyz676Qg5uMFNzytXAi2Ww" + .parse() + .unwrap(); + assert_eq!( + &parsed.to_string(), + "EKFWgzXsoMYcP1Hnj7dBhsefxNucZ6wyz676Qg5uMFNzytXAi2Ww" + ); +} + +#[test] +fn test_encrypt_decrypt() { + let password = "not-very-secure-pass"; + + let new_key = AccountSecretKey::rand(); + let tmp_dir = env::temp_dir(); + let tmp_path = format!("{}/{}-key", tmp_dir.display(), new_key.public_key()); + + // dump encrypted file + new_key + .to_encrypted_file(&tmp_path, password) + .expect("Failed to encrypt secret key"); + + // load and decrypt + let decrypted = AccountSecretKey::from_encrypted_file(&tmp_path, password) + .unwrap_or_else(|_| panic!("Failed to decrypt secret key file: {}", tmp_path)); + + assert_eq!( + new_key.public_key(), + decrypted.public_key(), + "Encrypted and decrypted public keys do not match" + ); +}