diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9b7b86f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,175 @@ +name: Enokiweave CI/CD + +permissions: {} # Default to no permissions + +on: + push: + branches: [ '**' ] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + REGISTRY: docker.io + REPOSITORY: myceliumai + IMAGE_NAME: enokiweave + +jobs: + changes: + runs-on: ubuntu-latest + permissions: + pull-requests: read + contents: read + outputs: + core: ${{ steps.filter.outputs.core }} + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + core: + - 'src/**' + - 'Cargo.*' + - '.github/workflows/**' + - 'Dockerfile' + + check: + needs: changes + if: ${{ needs.changes.outputs.core == 'true' }} + name: Check + runs-on: ubuntu-latest + continue-on-error: true + permissions: + contents: read + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Set up cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Install LMDB + run: sudo apt-get update && sudo apt-get install -y liblmdb-dev + + - name: Run cargo fmt + run: cargo fmt --all -- --check + + - name: Run cargo clippy + run: cargo clippy -- -D warnings + + test: + needs: changes + if: ${{ needs.changes.outputs.core == 'true' }} + name: Test Suite + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Set up cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Install LMDB + run: sudo apt-get update && sudo apt-get install -y liblmdb-dev + + - name: Run cargo test + run: cargo test + + security: + name: Security Checks + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + - uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + scanners: 'vuln,secret,config' + - uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + category: 'trivy' + + build: + needs: [changes, security, check, test] + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check if should build + id: should_build + run: | + if [[ + "${{ github.event_name }}" == "push" && + "${{ github.ref }}" == "refs/heads/main" && + "${{ needs.changes.outputs.core }}" == "true" + ]]; then + echo "run=true" >> $GITHUB_OUTPUT + fi + + - uses: actions/checkout@v4 + if: steps.should_build.outputs.run == 'true' + + - uses: docker/setup-buildx-action@v3 + if: steps.should_build.outputs.run == 'true' + + - uses: docker/login-action@v3 + if: steps.should_build.outputs.run == 'true' + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - uses: docker/metadata-action@v5 + if: steps.should_build.outputs.run == 'true' + id: meta + with: + images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=sha,format=long + + - uses: docker/build-push-action@v5 + if: steps.should_build.outputs.run == 'true' + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6001864 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# Build stage +FROM rust:1.75-slim-bullseye as builder + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + pkg-config \ + liblmdb-dev \ + && rm -rf /var/lib/apt/lists/* + +# Create a new empty shell project +WORKDIR /usr/src/enokiweave + +# Copy manifests +COPY Cargo.lock Cargo.toml ./ + +# Copy source code +COPY src ./src +COPY setup ./setup + +# Build for release +RUN cargo build --release + +# Runtime stage +FROM debian:bullseye-slim + +# Install runtime dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + liblmdb0 \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN groupadd -r enoki && useradd -r -g enoki enoki + +# Create necessary directories and set permissions +RUN mkdir -p /var/lib/enokiweave /etc/enokiweave && \ + chown -R enoki:enoki /var/lib/enokiweave /etc/enokiweave + +# Copy the build artifacts from builder +COPY --from=builder /usr/src/enokiweave/target/release/enokiweave /usr/local/bin/ +COPY --from=builder /usr/src/enokiweave/target/release/build-transaction /usr/local/bin/ + +# Copy configuration files +COPY setup/example_genesis_file.json /etc/enokiweave/genesis.json +COPY setup/example_initial_peers_file.txt /etc/enokiweave/peers.txt + +# Set working directory +WORKDIR /var/lib/enokiweave + +# Switch to non-root user +USER enoki + +# Expose ports +EXPOSE 3001 + +# Add healthcheck +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3001/health || exit 1 + +# Set entrypoint +ENTRYPOINT ["enokiweave"] +CMD ["--genesis-file-path", "/etc/enokiweave/genesis.json", "--rpc_port", "3001"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..937a294 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + enokiweave: + image: ${DOCKER_HUB_USERNAME}/enokiweave:latest + ports: + - "3001:3001" + volumes: + - enokiweave-data:/var/lib/enokiweave + - ./setup/example_genesis_file.json:/etc/enokiweave/genesis.json + - ./setup/example_initial_peers_file.txt:/etc/enokiweave/peers.txt + environment: + - RUST_LOG=info + restart: unless-stopped + +volumes: + enokiweave-data: \ No newline at end of file diff --git a/src/address.rs b/src/address.rs index 8ec491c..3e7da54 100644 --- a/src/address.rs +++ b/src/address.rs @@ -1,20 +1,24 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; +#[allow(dead_code)] pub const ZERO_ADDRESS: Address = Address([0; 32]); #[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone, Copy)] pub struct Address(pub [u8; 32]); impl Address { + #[allow(dead_code)] pub fn new(data: [u8; 32]) -> Self { Self(data) } + #[allow(dead_code)] pub fn as_hex(&self) -> String { hex::encode(self.0) } + #[allow(dead_code)] pub fn from_hex(hex_address: &str) -> Result
{ let decoded = hex::decode(hex_address)?; let mut address = [0u8; 32]; diff --git a/src/main.rs b/src/main.rs index e3650ac..46f3c5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use anyhow::{anyhow, Result}; use clap::Parser; use libp2p::futures::StreamExt; use libp2p::mdns::tokio::Tokio; @@ -17,7 +16,7 @@ use std::error::Error; use std::sync::Arc; use tcp::tokio::Transport as TokioTransport; use tokio::sync::Mutex; -use tracing::{error, info, trace, warn}; +use tracing::{info, trace, warn}; use transaction_manager::TransactionManager; use crate::rpc::run_http_rpc_server; @@ -27,7 +26,7 @@ mod rpc; mod transaction; mod transaction_manager; -const DB_NAME: &'static str = "./local_db/transaction_db"; +const DB_NAME: &str = "./local_db/transaction_db"; #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent")] @@ -43,13 +42,13 @@ impl From for OutEvent { } impl From for OutEvent { fn from(value: MdnsEvent) -> Self { - OutEvent::Mdns(value) + OutEvent::Mdns(Box::new(value)) } } enum OutEvent { Floodsub(FloodsubEvent), - Mdns(MdnsEvent), + Mdns(Box), } #[derive(Deserialize)] @@ -77,22 +76,24 @@ async fn handle_swarm_events(mut swarm: Swarm) { info!("Listening on {:?}", address); } SwarmEvent::Behaviour(OutEvent::Floodsub(FloodsubEvent::Message(_))) => {} - SwarmEvent::Behaviour(OutEvent::Mdns(MdnsEvent::Discovered(list))) => { - for (peer_id, _multiaddr) in list { - swarm - .behaviour_mut() - .floodsub - .add_node_to_partial_view(peer_id); + SwarmEvent::Behaviour(OutEvent::Mdns(mdns_event)) => match *mdns_event { + MdnsEvent::Discovered(list) => { + for (peer_id, _multiaddr) in list { + swarm + .behaviour_mut() + .floodsub + .add_node_to_partial_view(peer_id); + } } - } - SwarmEvent::Behaviour(OutEvent::Mdns(MdnsEvent::Expired(list))) => { - for (peer_id, _multiaddr) in list { - swarm - .behaviour_mut() - .floodsub - .remove_node_from_partial_view(&peer_id); + MdnsEvent::Expired(list) => { + for (peer_id, _multiaddr) in list { + swarm + .behaviour_mut() + .floodsub + .remove_node_from_partial_view(&peer_id); + } } - } + }, _ => {} } } @@ -113,7 +114,7 @@ fn are_all_peers_dead(peers: Vec, swarm: &mut Swarm { + Ok(0) => { trace!("Connection closed by client"); - return; } Ok(n) => { let request = String::from_utf8_lossy(&buf[..n]); @@ -215,7 +214,7 @@ async fn handle_rpc_request( Some("submitTransaction") => { let params = req["params"] .as_array() - .ok_or_else(|| "Invalid params - expected array")?; + .ok_or("Invalid params - expected array")?; if params.is_empty() { return Err("Empty params array".into()); @@ -248,7 +247,7 @@ async fn handle_rpc_request( Some("addressBalance") => { let params = req["params"] .as_str() - .ok_or_else(|| "Invalid params - expected str")?; + .ok_or("Invalid params - expected str")?; let address = Address::from_hex(params)?; // Create response channel diff --git a/src/transaction.rs b/src/transaction.rs index 219f8cc..e5a37a8 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -115,6 +115,7 @@ pub struct Transaction { } impl Transaction { + #[allow(dead_code)] pub fn new(from: Address, to: Address, amount: u64) -> Result { Ok(Self { from, @@ -127,14 +128,12 @@ impl Transaction { pub fn calculate_id(&self) -> Result<[u8; 32]> { let mut hasher = Sha256::new(); hasher.update(self.amount.to_be_bytes()); - hasher.update(&self.from); - hasher.update(&self.to); + hasher.update(self.from); + hasher.update(self.to); hasher.update(self.timestamp.to_be_bytes()); let hash = &hasher.finalize()[..]; - let id: [u8; 32] = hash.try_into().expect("Wrong length"); - Ok(id) } } diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index b5dc72c..aa11d9c 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -23,7 +23,7 @@ static LMDB_ENV: Lazy> = Lazy::new(|| { .set_max_dbs(1) .set_map_size(10 * 1024 * 1024) .set_max_readers(126) - .open(&Path::new(DB_NAME)) + .open(Path::new(DB_NAME)) .expect("Failed to create LMDB environment"), ) }); @@ -51,7 +51,7 @@ pub struct TransactionManager { impl TransactionManager { pub fn new() -> Result { let env = LMDB_ENV.clone(); - let db = env.create_db(Some(DB_NAME), lmdb::DatabaseFlags::empty())?; + let db = env.create_db(None, lmdb::DatabaseFlags::empty())?; Ok(TransactionManager { lmdb_transaction_env: env, @@ -231,6 +231,7 @@ impl TransactionManager { Ok(true) } + #[allow(dead_code)] pub fn get_transaction(&self, id: String) -> Result { let reader = self .lmdb_transaction_env @@ -249,6 +250,7 @@ impl TransactionManager { Ok(transaction) } + #[allow(dead_code)] pub fn get_all_transaction_ids(&self) -> Result> { let reader = self .lmdb_transaction_env