diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78f5a72cd1..4f0a5a34fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,11 @@ permissions: jobs: detect_changes: + timeout-minutes: 5 runs-on: 'ubuntu-latest' outputs: - rust_tests: ${{ steps.jobs.outputs.rust_tests }} + rust_unit_tests: ${{ steps.jobs.outputs.rust_unit_tests }} + rust_integration_tests: ${{ steps.jobs.outputs.rust_integration_tests }} ciphernode_e2e: ${{ steps.jobs.outputs.ciphernode_e2e }} crisp: ${{ steps.jobs.outputs.crisp }} templates: ${{ steps.jobs.outputs.templates }} @@ -47,6 +49,7 @@ jobs: build_circuits: ${{ steps.jobs.outputs.build_circuits }} integration_prebuild: ${{ steps.jobs.outputs.integration_prebuild }} zk_prover_integration: ${{ steps.jobs.outputs.zk_prover_integration }} + build_enclave_cli: ${{ steps.jobs.outputs.build_enclave_cli }} steps: - uses: actions/checkout@v6 - uses: dorny/paths-filter@v3 @@ -96,11 +99,12 @@ jobs: any() { for v in "$@"; do [ "$v" = "true" ] && echo "true" && return; done; echo "false"; } - echo "rust_tests=$(any $FORCE $RUST $CONTRACTS $CIRCUITS $CI)" >> $GITHUB_OUTPUT + echo "rust_unit_tests=$(any $FORCE $RUST $CONTRACTS $CIRCUITS $CI)" >> $GITHUB_OUTPUT + echo "rust_integration_tests=$(any $FORCE $RUST $CONTRACTS $CIRCUITS $CI)" >> $GITHUB_OUTPUT echo "ciphernode_e2e=$(any $FORCE $RUST $CONTRACTS $CIRCUITS $INTEGRATION $CI)" >> $GITHUB_OUTPUT echo "build_sdk=$(any $FORCE $RUST $CONTRACTS $SDK $INTEGRATION $CIRCUITS $CI $TEMPLATES)" >> $GITHUB_OUTPUT echo "crisp=$(any $FORCE $CRISP $CONTRACTS $CIRCUITS $RUST $CI)" >> $GITHUB_OUTPUT - echo "templates=$(any $FORCE $TEMPLATES $RUST $CONTRACTS $SDK $CI)" >> $GITHUB_OUTPUT + echo "templates=$(any $FORCE $TEMPLATES $CI)" >> $GITHUB_OUTPUT echo "zk=$(any $FORCE $RUST $CIRCUITS $CI)" >> $GITHUB_OUTPUT echo "contracts=$(any $FORCE $CONTRACTS $CI)" >> $GITHUB_OUTPUT echo "docker_support=$(any $FORCE $DOCKER $CI)" >> $GITHUB_OUTPUT @@ -111,13 +115,13 @@ jobs: echo "build_circuits=$(any $FORCE $RUST $CIRCUITS $CI)" >> $GITHUB_OUTPUT echo "integration_prebuild=$(any $FORCE $RUST $CONTRACTS $CIRCUITS $INTEGRATION $CI)" >> $GITHUB_OUTPUT echo "zk_prover_integration=$(any $FORCE $RUST $CIRCUITS $CI)" >> $GITHUB_OUTPUT + echo "build_enclave_cli=$(any $FORCE $RUST $CONTRACTS $CIRCUITS $INTEGRATION $TEMPLATES $CRISP $CI)" >> $GITHUB_OUTPUT - rust_tests: + rust_unit_tests: needs: [detect_changes] - if: needs.detect_changes.outputs.rust_tests == 'true' - runs-on: - group: enclave-ci - labels: [enclave-ci-runner] + if: needs.detect_changes.outputs.rust_unit_tests == 'true' + timeout-minutes: 20 + runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -158,12 +162,53 @@ jobs: - name: Run Unit Tests run: 'cargo test --lib && cargo test --doc' + rust_integration_tests: + needs: [detect_changes] + if: needs.detect_changes.outputs.rust_integration_tests == 'true' + timeout-minutes: 45 + runs-on: + group: enclave-ci + labels: [enclave-ci-runner] + steps: + - uses: actions/checkout@v6 + + - name: Cache Rust dependencies + uses: ./.github/actions/cache-dependencies + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install solc + run: | + sudo add-apt-repository ppa:ethereum/ethereum \ + && sudo apt-get update -y \ + && sudo apt-get install -y solc + + - name: pnpm-setup + uses: pnpm/action-setup@v4 + + - name: 'Setup node' + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + cache-dependency-path: pnpm-lock.yaml + + - name: 'Install the dependencies' + run: 'pnpm install --frozen-lockfile' + - name: Run Integration Tests run: 'cargo test --test integration -- --nocapture' zk_prover_integration: needs: [detect_changes] if: needs.detect_changes.outputs.zk_prover_integration == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -176,6 +221,12 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} + - name: Install solc + run: | + sudo add-apt-repository ppa:ethereum/ethereum \ + && sudo apt-get update -y \ + && sudo apt-get install -y solc + - name: pnpm-setup uses: pnpm/action-setup@v4 @@ -189,12 +240,19 @@ jobs: - name: 'Install the dependencies' run: 'pnpm install --frozen-lockfile' + - name: Cache Barretenberg binary + uses: actions/cache@v4 + with: + path: ~/.bb + key: bb-${{ env.BB_VERSION }}-${{ runner.arch }} + - name: Run ZK Prover Integration Tests (with network downloads) run: 'cargo test -p e3-zk-prover --features integration-tests --test integration_tests -- --nocapture' build_e3_support_risc0: needs: [detect_changes] if: needs.detect_changes.outputs.docker_support == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -234,6 +292,7 @@ jobs: build_ciphernode_image: needs: [detect_changes] if: needs.detect_changes.outputs.docker_ciphernode == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -276,6 +335,7 @@ jobs: test_contracts: needs: [detect_changes] if: needs.detect_changes.outputs.contracts == 'true' + timeout-minutes: 15 runs-on: 'ubuntu-latest' steps: - name: 'Check out the repo' @@ -313,6 +373,7 @@ jobs: test_net: needs: [detect_changes] if: needs.detect_changes.outputs.net == 'true' + timeout-minutes: 15 runs-on: 'ubuntu-latest' steps: - name: 'Check out the repo' @@ -328,6 +389,7 @@ jobs: integration_prebuild: needs: [detect_changes] if: needs.detect_changes.outputs.integration_prebuild == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - name: 'Check out the repo' @@ -386,6 +448,7 @@ jobs: ciphernode_integration_test: needs: [detect_changes, integration_prebuild, build_enclave_cli, build_sdk] if: needs.detect_changes.outputs.ciphernode_e2e == 'true' + timeout-minutes: 45 runs-on: group: enclave-ci labels: [enclave-ci-runner] @@ -452,8 +515,10 @@ jobs: echo "## Test results for ${{ matrix.test-suite }}" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - # Always runs — shared artifact dependency for many downstream jobs build_enclave_cli: + needs: [detect_changes] + if: needs.detect_changes.outputs.build_enclave_cli == 'true' + timeout-minutes: 20 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -487,6 +552,7 @@ jobs: crisp_unit: needs: [detect_changes, build_crisp_sdk] if: needs.detect_changes.outputs.crisp == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -561,6 +627,7 @@ jobs: crisp_e2e: needs: [detect_changes, build_enclave_cli, build_crisp_sdk] if: needs.detect_changes.outputs.crisp == 'true' + timeout-minutes: 45 runs-on: group: enclave-ci labels: [enclave-ci-runner] @@ -675,6 +742,7 @@ jobs: build_circuits: needs: [detect_changes] if: needs.detect_changes.outputs.build_circuits == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -686,14 +754,24 @@ jobs: with: toolchain: ${{ env.NOIR_TOOLCHAIN }} + - name: Cache Barretenberg binary + id: cache-bb + uses: actions/cache@v4 + with: + path: /usr/local/bin/bb + key: bb-bin-${{ env.BB_VERSION }}-amd64-linux + - name: Install Barretenberg (bb) + if: steps.cache-bb.outputs.cache-hit != 'true' run: | curl -fsSL "https://github.com/AztecProtocol/aztec-packages/releases/download/v${{ env.BB_VERSION }}/barretenberg-amd64-linux.tar.gz" -o bb.tar.gz mkdir -p bb_extract && tar -xzf bb.tar.gz -C bb_extract BB_BIN=$(find bb_extract -name bb -type f | head -n 1) sudo mv "$BB_BIN" /usr/local/bin/bb chmod +x /usr/local/bin/bb - bb --version + + - name: Verify bb + run: bb --version - name: Check formatting run: ./scripts/lint-circuits.sh @@ -733,6 +811,7 @@ jobs: zk_prover_e2e: needs: [detect_changes, build_circuits] if: needs.detect_changes.outputs.zk == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -745,14 +824,30 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} + - name: Cache Barretenberg binary + id: cache-bb + uses: actions/cache@v4 + with: + path: /usr/local/bin/bb + key: bb-bin-${{ env.BB_VERSION }}-amd64-linux + + - name: Install solc + run: | + sudo add-apt-repository ppa:ethereum/ethereum \ + && sudo apt-get update -y \ + && sudo apt-get install -y solc + - name: Install Barretenberg (bb) + if: steps.cache-bb.outputs.cache-hit != 'true' run: | curl -fsSL "https://github.com/AztecProtocol/aztec-packages/releases/download/v${{ env.BB_VERSION }}/barretenberg-amd64-linux.tar.gz" -o bb.tar.gz mkdir -p bb_extract && tar -xzf bb.tar.gz -C bb_extract BB_BIN=$(find bb_extract -name bb -type f | head -n 1) sudo mv "$BB_BIN" /usr/local/bin/bb chmod +x /usr/local/bin/bb - bb --version + + - name: Verify bb + run: bb --version - name: Download compiled circuit artifacts uses: actions/download-artifact@v4 @@ -797,6 +892,7 @@ jobs: build_e3_support_dev: needs: [detect_changes] if: needs.detect_changes.outputs.build_e3_support_dev == 'true' + timeout-minutes: 20 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -828,6 +924,7 @@ jobs: build_sdk: needs: [detect_changes] if: needs.detect_changes.outputs.build_sdk == 'true' + timeout-minutes: 30 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -886,6 +983,7 @@ jobs: build_crisp_sdk: needs: [detect_changes] if: needs.detect_changes.outputs.crisp == 'true' + timeout-minutes: 20 runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v6 @@ -935,6 +1033,7 @@ jobs: template_integration: needs: [detect_changes, build_enclave_cli, build_e3_support_dev, build_sdk] if: needs.detect_changes.outputs.templates == 'true' + timeout-minutes: 30 runs-on: group: enclave-ci labels: [enclave-ci-runner] @@ -1010,6 +1109,7 @@ jobs: test_enclave_init: needs: [detect_changes, build_enclave_cli, build_e3_support_dev] if: needs.detect_changes.outputs.init == 'true' + timeout-minutes: 10 runs-on: 'ubuntu-latest' steps: - name: Install pnpm @@ -1046,6 +1146,7 @@ jobs: enclave init mycitest --verbose --template=${{ github.server_url }}/${{ github.repository }}.git#${BRANCH}:templates/default contrib-readme-job: + timeout-minutes: 10 runs-on: 'ubuntu-latest' name: Populate Contributors List # Only run on main branch to avoid branch conflicts diff --git a/Cargo.lock b/Cargo.lock index 43fd8fa72d..7e2e3a7119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3871,6 +3871,7 @@ dependencies = [ "e3-fhe-params", "e3-polynomial", "e3-request", + "e3-test-helpers", "e3-utils", "e3-zk-helpers", "fhe", diff --git a/crates/test-helpers/src/lib.rs b/crates/test-helpers/src/lib.rs index 0b932e79fc..31d0809ac9 100644 --- a/crates/test-helpers/src/lib.rs +++ b/crates/test-helpers/src/lib.rs @@ -37,6 +37,58 @@ use rand_chacha::ChaCha20Rng; use std::sync::Arc; pub use utils::*; +use std::path::PathBuf; + +/// Find the bb binary on the system. +pub async fn find_bb() -> Option { + // Check PATH first via `which` + if let Ok(output) = tokio::process::Command::new("which") + .arg("bb") + .output() + .await + { + if output.status.success() { + let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !path.is_empty() { + return Some(PathBuf::from(path)); + } + } + } + // Check common install locations + if let Ok(home) = std::env::var("HOME") { + for path in [ + format!("{}/.bb/bb", home), + format!("{}/.nargo/bin/bb", home), + format!("{}/.enclave/noir/bin/bb", home), + ] { + if std::path::Path::new(&path).exists() { + return Some(PathBuf::from(path)); + } + } + } + None +} + +/// Check if anvil is available on the system. +pub async fn find_anvil() -> bool { + if let Ok(output) = tokio::process::Command::new("which") + .arg("anvil") + .output() + .await + { + if output.status.success() { + return true; + } + } + if let Ok(home) = std::env::var("HOME") { + let path = format!("{}/.foundry/bin/anvil", home); + if std::path::Path::new(&path).exists() { + return true; + } + } + false +} + pub fn create_shared_rng_from_u64(value: u64) -> Arc> { Arc::new(std::sync::Mutex::new(ChaCha20Rng::seed_from_u64(value))) } diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 1cd07afa63..9108c7dc92 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -25,7 +25,7 @@ use e3_net::NetEventTranslator; use e3_sortition::{calculate_buffer_size, RegisteredNode, ScoreSortition, Ticket}; use e3_test_helpers::ciphernode_system::CiphernodeSystemBuilder; use e3_test_helpers::{ - create_seed_from_u64, create_shared_rng_from_u64, with_tracing, AddToCommittee, + create_seed_from_u64, create_shared_rng_from_u64, find_bb, with_tracing, AddToCommittee, }; use e3_trbfv::helpers::calculate_error_size; use e3_utils::utility_types::ArcBytes; @@ -44,34 +44,6 @@ use tokio::{ time::sleep, }; -/// Find the bb binary on the system. -async fn find_bb() -> Option { - if let Ok(output) = tokio::process::Command::new("which") - .arg("bb") - .output() - .await - { - if output.status.success() { - let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !path.is_empty() { - return Some(PathBuf::from(path)); - } - } - } - if let Ok(home) = std::env::var("HOME") { - for path in [ - format!("{}/.bb/bb", home), - format!("{}/.nargo/bin/bb", home), - format!("{}/.enclave/noir/bin/bb", home), - ] { - if std::path::Path::new(&path).exists() { - return Some(PathBuf::from(path)); - } - } - } - None -} - /// Create a ZkBackend for integration tests. /// If a local bb binary is found, uses it with fixture files (fast path). /// Otherwise, calls `ensure_installed()` to download bb + circuits (CI path). diff --git a/crates/zk-prover/Cargo.toml b/crates/zk-prover/Cargo.toml index e8b80af115..ab7d1120df 100644 --- a/crates/zk-prover/Cargo.toml +++ b/crates/zk-prover/Cargo.toml @@ -47,6 +47,7 @@ tracing.workspace = true walkdir = "2.5" [dev-dependencies] +e3-test-helpers = { workspace = true } paste = "1" tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/zk-prover/tests/common/helpers.rs b/crates/zk-prover/tests/common/helpers.rs index 3cd01581bc..867b122380 100644 --- a/crates/zk-prover/tests/common/helpers.rs +++ b/crates/zk-prover/tests/common/helpers.rs @@ -5,34 +5,11 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use e3_config::BBPath; +pub use e3_test_helpers::{find_anvil, find_bb}; use e3_zk_prover::{ZkBackend, ZkConfig}; use std::{env, path::PathBuf}; use tempfile::TempDir; -use tokio::{fs, process::Command}; - -/// Returns `None` when bb is not found — tests should skip gracefully. -pub async fn find_bb() -> Option { - if let Ok(output) = Command::new("which").arg("bb").output().await { - if output.status.success() { - let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !path.is_empty() { - return Some(PathBuf::from(path)); - } - } - } - if let Ok(home) = std::env::var("HOME") { - for path in [ - format!("{}/.bb/bb", home), - format!("{}/.nargo/bin/bb", home), - format!("{}/.enclave/noir/bin/bb", home), - ] { - if std::path::Path::new(&path).exists() { - return Some(PathBuf::from(path)); - } - } - } - None -} +use tokio::fs; /// Root of the compiled circuit artifacts: `{workspace}/circuits/bin/`. fn circuits_build_root() -> PathBuf { @@ -154,21 +131,6 @@ pub async fn setup_compiled_circuit(backend: &ZkBackend, group: &str, circuit_na } } -pub async fn find_anvil() -> bool { - if let Ok(output) = Command::new("which").arg("anvil").output().await { - if output.status.success() { - return true; - } - } - if let Ok(home) = std::env::var("HOME") { - let path = format!("{}/.foundry/bin/anvil", home); - if std::path::Path::new(&path).exists() { - return true; - } - } - false -} - /// Creates a temp ZkBackend with the real bb binary symlinked in. /// Caller must hold onto the returned TempDir or it gets cleaned up. pub async fn setup_test_prover(bb: &PathBuf) -> (ZkBackend, TempDir) { diff --git a/packages/enclave-contracts/package.json b/packages/enclave-contracts/package.json index e9a82359a3..c294358eee 100644 --- a/packages/enclave-contracts/package.json +++ b/packages/enclave-contracts/package.json @@ -164,7 +164,7 @@ "upgrade:bondingRegistry": "hardhat run scripts/upgrade/bondingRegistry.ts", "upgrade:ciphernodeRegistryOwnable": "hardhat run scripts/upgrade/ciphernodeRegistryOwnable.ts", "e3:activate": "hardhat e3:activate", - "e3:enable": "hardhat e3:enable", + "e3:enable": "hardhat enclave:enableE3", "ciphernode:add": "hardhat ciphernode:add", "ciphernode:admin-add": "hardhat ciphernode:admin-add", "ciphernode:mint-tokens": "hardhat ciphernode:mint-tokens", diff --git a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts index ec7486ae42..ad25776a34 100644 --- a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts +++ b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -30,6 +30,7 @@ import { MockUSDC__factory as MockUSDCFactory, SlashingManager__factory as SlashingManagerFactory, } from "../../types"; +import { signAndEncodeAttestation } from "../fixtures"; const { ethers, ignition, networkHelpers } = await network.connect(); const { loadFixture, time } = networkHelpers; @@ -76,71 +77,6 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { // Lane A reason derived on-chain as keccak256(abi.encodePacked(proofType)) const REASON_PT_0 = ethers.keccak256(ethers.solidityPacked(["uint256"], [0])); - const VOTE_TYPEHASH = ethers.keccak256( - ethers.toUtf8Bytes( - "AccusationVote(uint256 chainId,uint256 e3Id,bytes32 accusationId,address voter,bool agrees,bytes32 dataHash)", - ), - ); - - /** - * Helper to create a committee-attestation evidence bundle for proposeSlash. - * Voters sign an AccusationVote digest via personal_sign (EIP-191). - */ - async function signAndEncodeAttestation( - voters: Signer[], - e3Id: number, - operator: string, - proofType: number = 0, - chainId: number = 31337, - dataHash: string = ethers.ZeroHash, - ): Promise { - const accusationId = ethers.keccak256( - ethers.solidityPacked( - ["uint256", "uint256", "address", "uint256"], - [chainId, e3Id, operator, proofType], - ), - ); - - // Sort voters by address ascending - const voterData = await Promise.all( - voters.map(async (v) => ({ signer: v, address: await v.getAddress() })), - ); - voterData.sort((a, b) => - a.address.toLowerCase().localeCompare(b.address.toLowerCase()), - ); - - const sortedAddresses: string[] = []; - const agrees: boolean[] = []; - const dataHashes: string[] = []; - const signatures: string[] = []; - - for (const { signer, address } of voterData) { - const digest = ethers.keccak256( - abiCoder.encode( - [ - "bytes32", - "uint256", - "uint256", - "bytes32", - "address", - "bool", - "bytes32", - ], - [VOTE_TYPEHASH, chainId, e3Id, accusationId, address, true, dataHash], - ), - ); - const sig = await signer.signMessage(ethers.getBytes(digest)); - sortedAddresses.push(address); - agrees.push(true); - dataHashes.push(dataHash); - signatures.push(sig); - } - - return abiCoder.encode( - ["uint256", "address[]", "bool[]", "bytes32[]", "bytes[]"], - [proofType, sortedAddresses, agrees, dataHashes, signatures], - ); - } const setup = async () => { // ── Signers ──────────────────────────────────────────────────────────────── diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 158bf48aab..3f6cbca731 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -26,6 +26,7 @@ import { } from "../types"; import type { Enclave } from "../types/contracts/Enclave"; import type { MockUSDC } from "../types/contracts/test/MockStableToken.sol/MockUSDC"; +import { setupOperatorForSortition } from "./fixtures"; const { ethers, ignition, networkHelpers } = await network.connect(); const { loadFixture, time, mine } = networkHelpers; @@ -98,40 +99,6 @@ describe("Enclave", function () { return enclaveContract.request(requestParams); }; - async function setupOperatorForSortition( - operator: Signer, - bondingRegistry: any, - licenseToken: any, - usdcToken: any, - ticketToken: any, - registry: any, - ): Promise { - const operatorAddress = await operator.getAddress(); - - await licenseToken.mintAllocation( - operatorAddress, - ethers.parseEther("10000"), - "Test allocation", - ); - await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); - - await licenseToken - .connect(operator) - .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); - await bondingRegistry - .connect(operator) - .bondLicense(ethers.parseEther("1000")); - await bondingRegistry.connect(operator).registerOperator(); - - const ticketAmount = ethers.parseUnits("100", 6); - await usdcToken - .connect(operator) - .approve(await ticketToken.getAddress(), ticketAmount); - await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); - - await registry.addCiphernode(operatorAddress); - } - const setup = async () => { // ── Signers ────────────────────────────────────────────────────────────── const [owner, notTheOwner, operator1, operator2, operator3] = diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index b3a40f8e4f..e4e56137ac 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -22,6 +22,7 @@ import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, Enclave__factory as EnclaveFactory, } from "../../types"; +import { setupOperatorForSortition } from "../fixtures"; const AddressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; @@ -42,39 +43,6 @@ describe("CiphernodeRegistryOwnable", function () { await registry.finalizeCommittee(e3Id); } - async function setupOperatorForSortition( - operator: Signer, - bondingRegistry: any, - licenseToken: any, - usdcToken: any, - ticketToken: any, - registry: any, - ): Promise { - const operatorAddress = await operator.getAddress(); - - await licenseToken.mintAllocation( - operatorAddress, - ethers.parseEther("10000"), - "Test allocation", - ); - await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); - - await licenseToken - .connect(operator) - .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); - await bondingRegistry - .connect(operator) - .bondLicense(ethers.parseEther("1000")); - await bondingRegistry.connect(operator).registerOperator(); - - const ticketAmount = ethers.parseUnits("100", 6); - await usdcToken - .connect(operator) - .approve(await ticketToken.getAddress(), ticketAmount); - await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); - - await registry.addCiphernode(operatorAddress); - } async function setup() { // ── Signers ──────────────────────────────────────────────────────────────── const [owner, notTheOwner, operator1, operator2, operator3] = diff --git a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts index 39c0c61b1d..b8476f5642 100644 --- a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts +++ b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -39,6 +39,7 @@ import { MockUSDC__factory as MockUSDCFactory, SlashingManager__factory as SlashingManagerFactory, } from "../../types"; +import { signAndEncodeAttestation } from "../fixtures"; const { ethers, ignition, networkHelpers } = await network.connect(); const { loadFixture, time } = networkHelpers; @@ -76,88 +77,6 @@ describe("Committee Expulsion & Fault Tolerance", function () { decryptionWindow: ONE_DAY, }; - // Must match the VOTE_TYPEHASH in SlashingManager.sol - const VOTE_TYPEHASH = ethers.keccak256( - ethers.toUtf8Bytes( - "AccusationVote(uint256 chainId,uint256 e3Id,bytes32 accusationId,address voter,bool agrees,bytes32 dataHash)", - ), - ); - - /** - * Helper to create signed committee attestation evidence for Lane A. - * Voters (other committee members) sign votes confirming the accused is faulty. - * Returns abi.encode(proofType, voters, agrees, dataHashes, signatures) - */ - async function signAndEncodeAttestation( - voterSigners: Signer[], - e3Id: number, - operator: string, - proofType: number = 0, - chainId: number = 31337, - dataHash: string = ethers.ZeroHash, - ): Promise { - const accusationId = ethers.keccak256( - ethers.solidityPacked( - ["uint256", "uint256", "address", "uint256"], - [chainId, e3Id, operator, proofType], - ), - ); - - const signersWithAddrs = await Promise.all( - voterSigners.map(async (s) => ({ - signer: s, - address: await s.getAddress(), - })), - ); - signersWithAddrs.sort((a, b) => - a.address.toLowerCase() < b.address.toLowerCase() - ? -1 - : a.address.toLowerCase() > b.address.toLowerCase() - ? 1 - : 0, - ); - - const voters: string[] = []; - const agrees: boolean[] = []; - const dataHashes: string[] = []; - const signatures: string[] = []; - - for (const { signer, address: voterAddress } of signersWithAddrs) { - voters.push(voterAddress); - agrees.push(true); - dataHashes.push(dataHash); - - const messageHash = ethers.keccak256( - abiCoder.encode( - [ - "bytes32", - "uint256", - "uint256", - "bytes32", - "address", - "bool", - "bytes32", - ], - [ - VOTE_TYPEHASH, - chainId, - e3Id, - accusationId, - voterAddress, - true, - dataHash, - ], - ), - ); - const signature = await signer.signMessage(ethers.getBytes(messageHash)); - signatures.push(signature); - } - - return abiCoder.encode( - ["uint256", "address[]", "bool[]", "bytes32[]", "bytes[]"], - [proofType, voters, agrees, dataHashes, signatures], - ); - } const setup = async () => { // ── Signers ──────────────────────────────────────────────────────────────── const [ diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 6625eccdf2..131b36383a 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -24,6 +24,7 @@ import { } from "../../types"; import type { MockCircuitVerifier } from "../../types"; import type { SlashingManager } from "../../types/contracts/slashing/SlashingManager"; +import { VOTE_TYPEHASH, signAndEncodeAttestation } from "../fixtures"; const { ethers, networkHelpers, ignition } = await network.connect(); const { loadFixture, time } = networkHelpers; @@ -47,103 +48,6 @@ describe("SlashingManager", function () { const abiCoder = ethers.AbiCoder.defaultAbiCoder(); - // Must match the VOTE_TYPEHASH in SlashingManager.sol - const VOTE_TYPEHASH = ethers.keccak256( - ethers.toUtf8Bytes( - "AccusationVote(uint256 chainId,uint256 e3Id,bytes32 accusationId,address voter,bool agrees,bytes32 dataHash)", - ), - ); - - /** - * Helper to create signed committee attestation evidence for Lane A. - * Each voter signs a VOTE_TYPEHASH-structured digest via personal_sign (EIP-191). - * Returns abi.encode(proofType, voters, agrees, dataHashes, signatures) - * with voters sorted ascending by address. - */ - async function signAndEncodeAttestation( - voterSigners: any[], - e3Id: number, - operator: string, - proofType: number = 0, - chainId: number = 31337, - dataHash: string = ethers.ZeroHash, - agreesOverride?: boolean[], - ): Promise { - // Compute accusationId matching AccusationManager::accusation_id() on Rust side - const accusationId = ethers.keccak256( - ethers.solidityPacked( - ["uint256", "uint256", "address", "uint256"], - [chainId, e3Id, operator, proofType], - ), - ); - - // Sort voters by address ascending (required by contract to prevent duplicates) - const signersWithAddrs = await Promise.all( - voterSigners.map(async (s, idx) => ({ - signer: s, - address: await s.getAddress(), - originalIndex: idx, - })), - ); - signersWithAddrs.sort((a, b) => - a.address.toLowerCase() < b.address.toLowerCase() - ? -1 - : a.address.toLowerCase() > b.address.toLowerCase() - ? 1 - : 0, - ); - - const voters: string[] = []; - const agrees: boolean[] = []; - const dataHashes: string[] = []; - const signatures: string[] = []; - - for (let i = 0; i < signersWithAddrs.length; i++) { - const { - signer, - address: voterAddress, - originalIndex, - } = signersWithAddrs[i]; - const voteAgrees = - agreesOverride !== undefined ? agreesOverride[originalIndex] : true; - - voters.push(voterAddress); - agrees.push(voteAgrees); - dataHashes.push(dataHash); - - // Reconstruct vote digest matching _verifyAttestationEvidence - const messageHash = ethers.keccak256( - abiCoder.encode( - [ - "bytes32", - "uint256", - "uint256", - "bytes32", - "address", - "bool", - "bytes32", - ], - [ - VOTE_TYPEHASH, - chainId, - e3Id, - accusationId, - voterAddress, - voteAgrees, - dataHash, - ], - ), - ); - const signature = await signer.signMessage(ethers.getBytes(messageHash)); - signatures.push(signature); - } - - return abiCoder.encode( - ["uint256", "address[]", "bool[]", "bytes32[]", "bytes[]"], - [proofType, voters, agrees, dataHashes, signatures], - ); - } - /** * Encodes a minimal attestation evidence for tests that check early * failures (before abi.decode is reached). diff --git a/packages/enclave-contracts/test/fixtures/attestation.ts b/packages/enclave-contracts/test/fixtures/attestation.ts new file mode 100644 index 0000000000..8740f304e2 --- /dev/null +++ b/packages/enclave-contracts/test/fixtures/attestation.ts @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +// Shared attestation helpers for committee-based slashing tests. +import type { Signer } from "ethers"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + +export const VOTE_TYPEHASH = ethers.keccak256( + ethers.toUtf8Bytes( + "AccusationVote(uint256 chainId,uint256 e3Id,bytes32 accusationId,address voter,bool agrees,bytes32 dataHash)", + ), +); + +/** + * Helper to create signed committee attestation evidence for Lane A. + * Each voter signs a VOTE_TYPEHASH-structured digest via personal_sign (EIP-191). + * Returns abi.encode(proofType, voters, agrees, dataHashes, signatures) + * with voters sorted ascending by address. + */ +export async function signAndEncodeAttestation( + voterSigners: Signer[], + e3Id: number, + operator: string, + proofType: number = 0, + chainId: number = 31337, + dataHash: string = ethers.ZeroHash, + agreesOverride?: boolean[], +): Promise { + const accusationId = ethers.keccak256( + ethers.solidityPacked( + ["uint256", "uint256", "address", "uint256"], + [chainId, e3Id, operator, proofType], + ), + ); + + const signersWithAddrs = await Promise.all( + voterSigners.map(async (s, idx) => ({ + signer: s, + address: await s.getAddress(), + originalIndex: idx, + })), + ); + signersWithAddrs.sort((a, b) => + a.address.toLowerCase() < b.address.toLowerCase() + ? -1 + : a.address.toLowerCase() > b.address.toLowerCase() + ? 1 + : 0, + ); + + const voters: string[] = []; + const agrees: boolean[] = []; + const dataHashes: string[] = []; + const signatures: string[] = []; + + for (let i = 0; i < signersWithAddrs.length; i++) { + const { + signer, + address: voterAddress, + originalIndex, + } = signersWithAddrs[i]!; + const voteAgrees = + agreesOverride !== undefined ? agreesOverride[originalIndex]! : true; + + voters.push(voterAddress); + agrees.push(voteAgrees); + dataHashes.push(dataHash); + + const messageHash = ethers.keccak256( + abiCoder.encode( + [ + "bytes32", + "uint256", + "uint256", + "bytes32", + "address", + "bool", + "bytes32", + ], + [ + VOTE_TYPEHASH, + chainId, + e3Id, + accusationId, + voterAddress, + voteAgrees, + dataHash, + ], + ), + ); + const signature = await signer.signMessage(ethers.getBytes(messageHash)); + signatures.push(signature); + } + + return abiCoder.encode( + ["uint256", "address[]", "bool[]", "bytes32[]", "bytes[]"], + [proofType, voters, agrees, dataHashes, signatures], + ); +} diff --git a/packages/enclave-contracts/test/fixtures/index.ts b/packages/enclave-contracts/test/fixtures/index.ts new file mode 100644 index 0000000000..92e1403f1f --- /dev/null +++ b/packages/enclave-contracts/test/fixtures/index.ts @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +export { VOTE_TYPEHASH, signAndEncodeAttestation } from "./attestation"; +export { setupOperatorForSortition } from "./operators"; diff --git a/packages/enclave-contracts/test/fixtures/operators.ts b/packages/enclave-contracts/test/fixtures/operators.ts new file mode 100644 index 0000000000..e600c08c77 --- /dev/null +++ b/packages/enclave-contracts/test/fixtures/operators.ts @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +// Shared operator setup helpers for sortition-based tests. +import type { Signer } from "ethers"; +import { network } from "hardhat"; + +const { ethers } = await network.connect(); + +/** + * Register an operator for sortition: mint license, bond, register, + * fund ticket balance, and add to the ciphernode registry. + */ +export async function setupOperatorForSortition( + operator: Signer, + bondingRegistry: any, + licenseToken: any, + usdcToken: any, + ticketToken: any, + registry: any, +): Promise { + const operatorAddress = await operator.getAddress(); + + await licenseToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await licenseToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + + await registry.addCiphernode(operatorAddress); +}