diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 0000000..5572ae2 --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,98 @@ +name: Merge Requirements + +on: [pull_request] + +env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-D warnings" + +jobs: + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --release + + checks: + name: Enforce Clippy constraints + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Run Clippy + run: cargo clippy --all-targets --all-features + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt --all -- --check + + tests: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run tests + run: cargo test --release + + test-publish: + name: Dry run publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - name: Dry run publish + run: cargo publish --dry-run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..96f90f2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,59 @@ +name: Release + +on: + workflow_dispatch: + +permissions: + contents: write + +env: + RUST_BACKTRACE: 1 + +jobs: + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo build + run: cargo build --release + + publish: + name: Publish + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: "0" + - uses: dtolnay/rust-toolchain@stable + + - shell: bash + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - uses: cargo-bins/cargo-binstall@main + - shell: bash + run: cargo binstall --no-confirm release-plz + + - name: Publish crates + shell: bash + run: | + cargo login "${{ secrets.CRATES_IO_TOKEN }}" + release-plz release --git-token ${{ secrets.GITHUB_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index ef2ae32..ad7d45a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,11 @@ edition = "2024" homepage = "https://maidsafe.net" license = "GPL-3.0" name = "evmlib" -repository = "https://github.com/maidsafe/autonomi" -version = "0.4.9" +repository = "https://github.com/WithAutonomi/evmlib" +version = "0.5.0" [features] external-signer = [] -test-utils = ["dirs-next", "serde_json"] [dependencies] alloy = { version = "1.0.32", default-features = false, features = ["contract", "json-rpc", "network", "node-bindings", "provider-http", "reqwest-rustls-tls", "rpc-client", "rpc-types", "signer-local", "std"] } @@ -27,9 +26,7 @@ rmp-serde = "1" tiny-keccak = { version = "~2.0.2", features = ["sha3"] } ant-merkle = "1.5.1" -# Optional dependencies for disk-based smart contract mock (test-utils feature) -dirs-next = { version = "~2.0", optional = true } -serde_json = { version = "1.0.108", optional = true } - [dev-dependencies] tracing-subscriber = { version = "0.3", features = ["env-filter"] } +dirs-next = "~2.0" +serde_json = "1.0.108" diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 0000000..1d766a6 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,8 @@ +[workspace] +changelog_update = false +semver_check = false + +[[package]] +name = "evmlib" +git_tag_name = "v{{ version }}" +git_release_enable = false diff --git a/src/contract/merkle_payment_vault/mod.rs b/src/contract/merkle_payment_vault/mod.rs index 76bcd80..74eb16c 100644 --- a/src/contract/merkle_payment_vault/mod.rs +++ b/src/contract/merkle_payment_vault/mod.rs @@ -196,22 +196,47 @@ mod tests { "Should have paid nodes" ); - // Test 4: Try to pay again for the same tree (should fail with PaymentAlreadyExists) + // Test 4: Try to pay again with the same pools. + // + // The contract picks a winner from submitted pools using block.prevrandao. + // With 4 pools, the second call may select a different winner (different block), + // which is valid — only the SAME pool hash should be rejected as duplicate. println!("\nTest 4: Testing duplicate payment detection..."); - let pool_commitments_packed: Vec<_> = pool_commitments + let all_packed: Vec<_> = pool_commitments .iter() .map(|c| c.to_packed().expect("cost unit packing")) .collect(); let duplicate_result = vault_handler - .pay_for_merkle_tree(depth, pool_commitments_packed, timestamp, &tx_config) + .pay_for_merkle_tree(depth, all_packed, timestamp, &tx_config) .await; - match duplicate_result { + // The contract selects a winner using block.prevrandao, so it may pick a different + // pool than the first call. If it picks the same winner, we get PaymentAlreadyExists. + // If it picks a different winner, it succeeds (paying for a new pool is valid). + // Both outcomes are correct behavior — the important thing is that the SAME pool + // hash can't be paid twice. + match &duplicate_result { Err(error::Error::PaymentAlreadyExists(_)) => { - println!("Correctly detected duplicate payment!"); + println!("Correctly detected duplicate payment (same winner selected)!"); + } + Ok((new_winner, _, _)) => { + println!( + "Different winner selected: {} (original: {})", + hex::encode(new_winner), + hex::encode(winner_pool_hash) + ); + assert_ne!( + *new_winner, winner_pool_hash, + "Same winner should have been rejected as duplicate" + ); + // Verify original payment still exists + let original_info = vault_handler + .get_payment_info(winner_pool_hash) + .await + .expect("Original payment should still exist"); + assert_eq!(original_info.depth, depth); } - Err(e) => panic!("Expected PaymentAlreadyExists error, got: {e:?}"), - Ok(_) => panic!("Should not allow duplicate payment"), + Err(e) => panic!("Unexpected error: {e:?}"), } println!("\nāœ… All tests passed!"); diff --git a/src/merkle_batch_payment.rs b/src/merkle_batch_payment.rs index 8ad39a8..9320a2d 100644 --- a/src/merkle_batch_payment.rs +++ b/src/merkle_batch_payment.rs @@ -17,13 +17,13 @@ use crate::contract::data_type_conversion; use crate::quoting_metrics::QuotingMetrics; use serde::{Deserialize, Serialize}; -#[cfg(any(test, feature = "test-utils"))] +#[cfg(test)] use crate::common::Amount; -#[cfg(any(test, feature = "test-utils"))] +#[cfg(test)] use std::path::PathBuf; -#[cfg(any(test, feature = "test-utils"))] +#[cfg(test)] use thiserror::Error; /// Error returned when `total_cost_unit` exceeds the 248-bit limit during packing. @@ -214,7 +214,7 @@ impl PoolCommitment { } } -#[cfg(any(test, feature = "test-utils"))] +#[cfg(test)] /// Errors that can occur during smart contract operations #[derive(Debug, Error)] pub enum SmartContractError { @@ -251,7 +251,7 @@ pub struct OnChainPaymentInfo { pub paid_node_addresses: Vec<(RewardsAddress, usize)>, } -#[cfg(any(test, feature = "test-utils"))] +#[cfg(test)] /// Disk-based Merkle payment contract (mock for testing) /// /// This simulates smart contract behavior by storing payment data to disk. @@ -260,7 +260,7 @@ pub struct DiskMerklePaymentContract { storage_path: PathBuf, // ~/.autonomi/merkle_payments/ } -#[cfg(any(test, feature = "test-utils"))] +#[cfg(test)] impl DiskMerklePaymentContract { /// Create a new contract with a specific storage path pub fn new_with_path(storage_path: PathBuf) -> Result { diff --git a/src/merkle_payments/mod.rs b/src/merkle_payments/mod.rs index 6afd970..ef6d1b4 100644 --- a/src/merkle_payments/mod.rs +++ b/src/merkle_payments/mod.rs @@ -11,7 +11,7 @@ pub use crate::merkle_batch_payment::{ expected_reward_pools, }; -#[cfg(any(test, feature = "test-utils"))] +#[cfg(test)] pub use crate::merkle_batch_payment::SmartContractError; // Export payment types (nodes, pools, proofs)