Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ soljson*
configuration/toml
remappings.txt
local.env
.claude/
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ resolver = "2"

[workspace.package]
version = "0.1.0"
edition = "2024"
edition = "2021"
85 changes: 85 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.PHONY: help dev dev-build dev-down test-unit test-sync test-forge fmt fmt-check clippy build build-release build-contracts key-gen clean docker-clean

# Default target: show help
help:
@echo "Nightfall 4 CE - Development Commands"
@echo ""
@echo " make dev Start the full development stack"
@echo " make dev-build Build and start the development stack"
@echo " make dev-down Stop all development services"
@echo ""
@echo " make build Build all Rust crates"
@echo " make build-release Build all Rust crates (release mode)"
@echo " make build-contracts Build Solidity contracts with Foundry"
@echo ""
@echo " make test-unit Run Rust unit tests"
@echo " make test-sync Run synchronization tests via Docker"
@echo " make test-forge Run Solidity contract tests"
@echo ""
@echo " make fmt Format Rust code"
@echo " make fmt-check Check Rust code formatting"
@echo " make clippy Run clippy linter"
@echo ""
@echo " make key-gen Generate ZK proving keys (heavy)"
@echo " make clean Clean Rust and Solidity build artifacts"
@echo " make docker-clean Remove Docker containers, volumes, and images"

# Start the full development stack
dev:
docker compose --profile development --env-file .env up

# Build and start the development stack
dev-build:
docker compose --profile development --env-file .env up --build

# Stop all development services
dev-down:
docker compose --profile development down

# Run Rust unit tests
test-unit:
cargo test

# Run synchronization tests via Docker
test-sync:
docker compose --profile sync_test --env-file .env up

# Run Solidity contract tests
test-forge:
forge test

# Format Rust code (requires nightly)
fmt:
cargo +nightly fmt

# Check Rust code formatting
fmt-check:
cargo +nightly fmt -- --check

# Run clippy linter (strict mode)
clippy:
cargo clippy --all-targets -- -D warnings

# Build all Rust crates
build:
cargo build

# Build all Rust crates in release mode
build-release:
cargo build --release

# Build Solidity contracts with Foundry
build-contracts:
forge clean && forge build

# Generate ZK proving keys (resource-intensive)
key-gen:
NF4_MOCK_PROVER=false cargo run --release --bin key_generation

# Clean Rust and Solidity build artifacts
clean:
cargo clean && forge clean

# Remove Docker containers, volumes, and locally-built images
docker-clean:
docker compose down -v --rmi local
107 changes: 103 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,108 @@
# nightfall_4_CE
Community edition of Nightfall_4
# Nightfall 4 CE

Community Edition of Nightfall\_4

Nightfall\_4 is a Zero-Knowledge Proof (ZKP)-based Layer 2 ZK-ZK rollup for transferring ERC20, ERC721, ERC1155, and ERC3525 tokens privately on Ethereum. Unlike Nightfall\_3's optimistic rollup, Nightfall\_4 uses cryptographic proofs for near-instant finality. A private transfer typically costs around 6000 gas.

_This code is not owned by EY and EY provides no warranty and disclaims any and all liability for use of this code. Users must conduct their own diligence with respect to use for their purposes and any and all usage is on an as-is basis and at your own risk._

Nightfall_4 is a ZK rollup build around the ZK Privacy of Nightfall. It enables one to transfer ERC20, ERC721, ERC1155 and ERC3525 tokens in privacy. Full details can be found in the /doc folder of this repository.
**This software is experimental. It should not be used to make significant value transactions.**

## Architecture

The project is a Rust workspace with the following crates:

| Crate | Description |
|---|---|
| `nightfall_client` | Client service for creating private transactions (deposit, transfer, withdraw) |
| `nightfall_proposer` | Block proposer that assembles L2 blocks and generates rollup proofs |
| `nightfall_deployer` | Smart contract deployment and ZK proving key generation |
| `nightfall_bindings` | Auto-generated Rust bindings for Solidity contracts |
| `lib` | Shared cryptographic and blockchain utilities (Merkle trees, Poseidon hashing, PLONK proofs) |
| `configuration` | Configuration management via TOML files and environment variables |

Smart contracts are in `blockchain_assets/contracts/` and use the UUPS upgradeable proxy pattern.

## Prerequisites

- [Docker](https://docs.docker.com/get-docker/) and Docker Compose
- [Rust](https://rustup.rs/) 1.88.0+ (pinned via `rust-toolchain.toml`)
- [Foundry](https://book.getfoundry.sh/getting-started/installation) (forge, anvil)

## Quick Start

Start the full local development stack (Anvil chain, deployer, proposer, two clients, and MongoDB):

```bash
make dev
```

This uses the `development` profile with a local Anvil chain and mock provers, so no heavy ZK key generation is required.

Once running:
- Client 1 API: `http://localhost:3000`
- Client 2 API: `http://localhost:3002`
- Proposer API: `http://localhost:3001`
- Anvil RPC: `http://localhost:8545`

## Development Commands

Run `make help` to see all available targets:

| Command | Description |
|---|---|
| `make dev` | Start the full development stack |
| `make dev-build` | Build and start the development stack |
| `make dev-down` | Stop all development services |
| `make build` | Build all Rust crates |
| `make build-contracts` | Build Solidity contracts with Foundry |
| `make test-unit` | Run Rust unit tests |
| `make test-sync` | Run synchronization tests via Docker |
| `make test-forge` | Run Solidity contract tests |
| `make fmt` | Format Rust code (requires nightly) |
| `make clippy` | Run clippy linter (strict mode) |
| `make key-gen` | Generate ZK proving keys (resource-intensive) |
| `make clean` | Clean build artifacts |

## API Endpoints

### Client (port 3000)

| Method | Path | Description |
|---|---|---|
| POST | `/v1/deposit` | Deposit tokens into Nightfall |
| POST | `/v1/transfer` | Private token transfer |
| POST | `/v1/withdraw` | Withdraw tokens from Nightfall |
| GET | `/v1/commitments` | List commitments (supports `?limit=&offset=`) |
| GET | `/v1/commitment/{key}` | Get a single commitment |
| GET | `/v1/balance/{token}/{owner}` | Get ERC token balance |
| GET | `/v1/fee_balance` | Get fee balance |
| GET | `/v1/l1_balance` | Get Layer 1 balance |
| GET | `/v1/synchronisation` | Check sync status |
| POST | `/v1/certification` | Submit X.509 certificate |
| GET | `/v1/health` | Health check |

### Proposer (port 3001)

| Method | Path | Description |
|---|---|---|
| POST | `/v1/transaction` | Submit client transaction |
| POST | `/v1/register` | Register as a proposer |
| POST | `/v1/deregister` | Deregister a proposer |
| GET | `/v1/rotate` | Rotate active proposer |
| POST | `/v1/pause` | Pause block assembly |
| POST | `/v1/resume` | Resume block assembly |
| GET | `/v1/blockdata` | Get current block data |
| POST | `/v1/certification` | Submit X.509 certificate |
| GET | `/v1/health` | Health check |

## Documentation

- [Architecture and API Reference](doc/nf_4.md) - Full documentation
- [Testnet Setup Guide](doc/Setup%20Testnet%20Guide.md) - Deploy to a host chain
- [Upgradable Contracts Guide](doc/Upgradable%20Contracts%20Guide.md) - Contract upgrade procedures
- [Changelog](doc/CHANGELOG.md) - Version history

Please note that this software should be treated as experimental. It should not be used to make significant value transactions.
## License

See [LICENSE](LICENSE).
6 changes: 6 additions & 0 deletions blockchain_assets/contracts/RoundRobin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ contract RoundRobin is ProposerManager, Certified, UUPSUpgradeable {
// number of blocks to wait before finalizing a rotation
uint public constant FINALIZATION_BLOCKS = 64;

/// @notice Hard cap on the number of proposers that can be registered at the same time.
/// @dev Prevents gas DoS via unbounded iteration in on-chain loops such as
/// `get_proposers()` and `rotate_proposer()`, which traverse the full linked list.
uint256 public constant MAX_PROPOSERS = 100;

Nightfall private nightfall;

// ------------------------------------------------------------------------
Expand Down Expand Up @@ -179,6 +184,7 @@ contract RoundRobin is ProposerManager, Certified, UUPSUpgradeable {
function add_proposer(
string calldata proposer_url
) external payable override onlyCertified {
require(proposer_count < MAX_PROPOSERS, "Maximum proposer count reached");
// Enforce cooldown only if they have previously exited
if (last_exit_block[msg.sender] != 0) {
require(
Expand Down
19 changes: 18 additions & 1 deletion blockchain_assets/test_contracts/RoundRobin.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,21 @@ contract RoundRobinTest is Test {
);
}

}
/// @dev Adding a proposer when the ring is already at MAX_PROPOSERS must revert.
function test_addProposer_revertsWhenMaxReached() public {
x509Contract.enableAllowlisting(false);

// Storage slot 70 holds proposer_count (from `forge inspect RoundRobin storage-layout`).
// Re-derive with: forge inspect RoundRobin storage-layout | grep proposer_count
// Write MAX_PROPOSERS directly into the proxy's storage to simulate a full ring.
uint256 maxProposers = roundRobin.MAX_PROPOSERS();
vm.store(address(roundRobin), bytes32(uint256(70)), bytes32(maxProposers));

assertEq(roundRobin.proposer_count(), maxProposers);

// The next add_proposer call should revert
vm.expectRevert("Maximum proposer count reached");
roundRobin.add_proposer{value: 5}("http://localhost:9999");
}

}
5 changes: 2 additions & 3 deletions configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ serde_json = "1.0.140"
reqwest = { version = "0.11.27", features = ["json", "blocking"] }
url = "2.5.4"
toml = "0.7.8"
log = "0.4.27"
env_logger = "0.10.2"
log-panics = { version = "2", default-features = false }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
lazy_static = "1.5.0"
figment = { version = "0.10.19", features = ["toml", "env"] }
hex = "0.4.3"
Expand Down
2 changes: 1 addition & 1 deletion configuration/src/addresses.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::settings::Settings;
use alloy::primitives::Address;
use log::{info, warn};
use tracing::{info, warn};
use rand::Rng;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
Expand Down
68 changes: 32 additions & 36 deletions configuration/src/logging.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,45 @@
use env_logger::Builder;
use log::LevelFilter;
use log_panics;
use std::env;
use tracing_subscriber::EnvFilter;

pub fn init_logging(log_level: &str, app_only: bool) {
log_panics::init(); // this ensures that panics are logged

// Check if RUST_LOG is set - if so, use it to allow fine-grained control
let use_rust_log = env::var("RUST_LOG").is_ok();

if use_rust_log {
let base_filter = if use_rust_log {
// Use RUST_LOG environment variable for fine-grained control
Builder::from_env(env_logger::Env::default())
.filter_module("alloy_provider", LevelFilter::Error)
.filter_module("warp", LevelFilter::Warn)
.filter_module("hyper", LevelFilter::Warn)
.filter_module("tungstenite", LevelFilter::Warn)
.init();
EnvFilter::from_default_env()
} else if app_only {
match log_level {
"debug" => Builder::new()
.filter_level(LevelFilter::Debug)
.filter_module("alloy_provider", LevelFilter::Error)
.filter_module("warp", LevelFilter::Warn)
.filter_module("hyper", LevelFilter::Warn)
.filter_module("tungstenite", LevelFilter::Warn)
.init(),
"info" => Builder::new()
.filter_level(LevelFilter::Info)
.filter_module("alloy_provider", LevelFilter::Error)
.filter_module("warp", LevelFilter::Warn)
.filter_module("hyper", LevelFilter::Warn)
.filter_module("tungstenite", LevelFilter::Warn)
.init(),
"warn" => Builder::new().filter_level(LevelFilter::Warn).init(),
"error" => Builder::new().filter_level(LevelFilter::Error).init(),
_ => Builder::new().filter_level(LevelFilter::Info).init(),
};
"debug" => EnvFilter::new("debug"),
"info" => EnvFilter::new("info"),
"warn" => EnvFilter::new("warn"),
"error" => EnvFilter::new("error"),
_ => EnvFilter::new("info"),
}
} else {
match log_level {
"debug" => Builder::new().filter_level(LevelFilter::Debug).init(),
"info" => Builder::new().filter_level(LevelFilter::Info).init(),
"warn" => Builder::new().filter_level(LevelFilter::Warn).init(),
"error" => Builder::new().filter_level(LevelFilter::Error).init(),
_ => Builder::new().filter_level(LevelFilter::Info).init(),
};
"debug" => EnvFilter::new("debug"),
"info" => EnvFilter::new("info"),
"warn" => EnvFilter::new("warn"),
"error" => EnvFilter::new("error"),
_ => EnvFilter::new("info"),
}
};

// Apply module-level overrides when using RUST_LOG or app_only mode
let filter = if use_rust_log || app_only {
base_filter
.add_directive("alloy_provider=error".parse().unwrap())
.add_directive("warp=warn".parse().unwrap())
.add_directive("hyper=warn".parse().unwrap())
.add_directive("tungstenite=warn".parse().unwrap())
} else {
base_filter
};

tracing_subscriber::fmt().with_env_filter(filter).init();

std::panic::set_hook(Box::new(|panic_info| {
tracing::error!("{}", panic_info);
}));
}
3 changes: 2 additions & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ rand = "0.8"
serde = "1.0.219"
sha2 = "0.10"
sha3 = "0.10.8"
log = "0.4.27"
tracing = "0.1"
url = "2.5.4"
testcontainers = { version = "0.24.0", features = ["blocking"] }
futures = "0.3.31"
azure_identity = "0.21.0"
azure_security_keyvault = "0.21.0"
bincode = "1.3.3"
base64 = "0.22.1"
thiserror = "2"
zeroize = { version = "1.6", features = ["derive"] }
[build-dependencies]
configuration = { path = "../configuration" }
Expand Down
Loading