From 91b3bc568f23b9a0c1080f7ff33b9bd673a67e2b Mon Sep 17 00:00:00 2001 From: grumbach Date: Tue, 31 Mar 2026 14:36:39 +0900 Subject: [PATCH 1/7] ci: add PR checks and release workflows Add merge.yml for PR validation (build, clippy, fmt, tests, dry-run publish) and release.yml for publishing to crates.io via release-plz. --- .github/workflows/merge.yml | 95 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 57 +++++++++++++++++++++ release-plz.toml | 7 +++ 3 files changed, 159 insertions(+) create mode 100644 .github/workflows/merge.yml create mode 100644 .github/workflows/release.yml create mode 100644 release-plz.toml diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 0000000..502ad29 --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,95 @@ +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: 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..e4952ff --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + workflow_dispatch: + +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" + token: ${{ secrets.EVMLIB_PAT }} + - 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.EVMLIB_PAT }} diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 0000000..4daa486 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,7 @@ +[workspace] +changelog_update = false +semver_check = false + +[[package]] +name = "evmlib" +git_tag_name = "v{{ version }}" From 056d6dd4ac1dbd349480184186004abcba5970c3 Mon Sep 17 00:00:00 2001 From: grumbach Date: Tue, 31 Mar 2026 18:12:56 +0900 Subject: [PATCH 2/7] fix: remove test-utils feature, move test deps to dev-dependencies The test-utils feature was unused by any consumer. All cfg guards were already #[cfg(any(test, feature = "test-utils"))], so the test code compiled during cargo test anyway. Simplify by: - Removing the test-utils feature entirely - Moving dirs-next and serde_json to [dev-dependencies] - Replacing cfg guards with plain #[cfg(test)] --- Cargo.toml | 7 ++----- src/merkle_batch_payment.rs | 12 ++++++------ src/merkle_payments/mod.rs | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef2ae32..2eeccdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ version = "0.4.9" [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/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) From 19d790f868e8648f75031bffd7091ddb0560de48 Mon Sep 17 00:00:00 2001 From: grumbach Date: Tue, 31 Mar 2026 18:17:37 +0900 Subject: [PATCH 3/7] fix(ci): install foundry/anvil for test job --- .github/workflows/merge.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 502ad29..5572ae2 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -81,6 +81,9 @@ jobs: 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 From 759bed672c35f7a642d27b8738a253e22160964e Mon Sep 17 00:00:00 2001 From: grumbach Date: Tue, 31 Mar 2026 18:27:28 +0900 Subject: [PATCH 4/7] fix: ignore test_smart_contract (duplicate payment not yet in contract) --- src/contract/merkle_payment_vault/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contract/merkle_payment_vault/mod.rs b/src/contract/merkle_payment_vault/mod.rs index 76bcd80..12532bf 100644 --- a/src/contract/merkle_payment_vault/mod.rs +++ b/src/contract/merkle_payment_vault/mod.rs @@ -72,6 +72,7 @@ mod tests { use alloy::providers::WalletProvider; #[tokio::test] + #[ignore = "duplicate payment detection not yet implemented in smart contract"] async fn test_smart_contract() { // Start local Anvil node let (_anvil, rpc_url) = start_node().unwrap(); From d0e0b5195ec386ce132dfe4a1423414fad4432de Mon Sep 17 00:00:00 2001 From: grumbach Date: Tue, 31 Mar 2026 18:34:41 +0900 Subject: [PATCH 5/7] fix: correct duplicate payment test to handle random winner selection The contract selects a winner pool using block.prevrandao. With 4 pools, resubmitting the same data on a different block may select a different winner, which is valid (each pool hash can only be paid once, but different pools are independent). The test now correctly handles both outcomes: PaymentAlreadyExists if same winner, or success with a different winner verified against the original. --- src/contract/merkle_payment_vault/mod.rs | 40 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/contract/merkle_payment_vault/mod.rs b/src/contract/merkle_payment_vault/mod.rs index 12532bf..74eb16c 100644 --- a/src/contract/merkle_payment_vault/mod.rs +++ b/src/contract/merkle_payment_vault/mod.rs @@ -72,7 +72,6 @@ mod tests { use alloy::providers::WalletProvider; #[tokio::test] - #[ignore = "duplicate payment detection not yet implemented in smart contract"] async fn test_smart_contract() { // Start local Anvil node let (_anvil, rpc_url) = start_node().unwrap(); @@ -197,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!"); From 2d370de5f265f3f40716f231cf6f6b94895aa55d Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Tue, 31 Mar 2026 13:12:16 +0100 Subject: [PATCH 6/7] ci: disable GitHub releases, use GITHUB_TOKEN instead of PAT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a library crate — git tags and crates.io publishing are sufficient; GitHub releases add no value. With releases disabled, the default GITHUB_TOKEN can push tags, removing the need for the EVMLIB_PAT secret entirely. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 6 ++++-- release-plz.toml | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4952ff..96f90f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,9 @@ name: Release on: workflow_dispatch: +permissions: + contents: write + env: RUST_BACKTRACE: 1 @@ -38,7 +41,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: "0" - token: ${{ secrets.EVMLIB_PAT }} - uses: dtolnay/rust-toolchain@stable - shell: bash @@ -54,4 +56,4 @@ jobs: shell: bash run: | cargo login "${{ secrets.CRATES_IO_TOKEN }}" - release-plz release --git-token ${{ secrets.EVMLIB_PAT }} + release-plz release --git-token ${{ secrets.GITHUB_TOKEN }} diff --git a/release-plz.toml b/release-plz.toml index 4daa486..1d766a6 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -5,3 +5,4 @@ semver_check = false [[package]] name = "evmlib" git_tag_name = "v{{ version }}" +git_release_enable = false From 1bd0536496417c7076b2fb223e720c7a750b6d71 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Tue, 31 Mar 2026 13:19:00 +0100 Subject: [PATCH 7/7] chore: bump version to 0.5.0 Breaking changes were introduced in recent PRs (testnet functions now return Result types), so bump the minor version. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2eeccdf..ad7d45a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,8 @@ 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 = []