Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/merge.yml
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
11 changes: 4 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"
8 changes: 8 additions & 0 deletions release-plz.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[workspace]
changelog_update = false
semver_check = false

[[package]]
name = "evmlib"
git_tag_name = "v{{ version }}"
git_release_enable = false
39 changes: 32 additions & 7 deletions src/contract/merkle_payment_vault/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
Expand Down
12 changes: 6 additions & 6 deletions src/merkle_batch_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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<Self, SmartContractError> {
Expand Down
2 changes: 1 addition & 1 deletion src/merkle_payments/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading