diff --git a/.gitignore b/.gitignore index 72dd8e88..5e33e0e2 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ soljson* configuration/toml remappings.txt local.env +.claude/ diff --git a/Cargo.toml b/Cargo.toml index 1a0738ff..49846510 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,4 @@ resolver = "2" [workspace.package] version = "0.1.0" -edition = "2024" +edition = "2021" diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a5df5d01 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 83491834..c77f65c2 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/blockchain_assets/contracts/RoundRobin.sol b/blockchain_assets/contracts/RoundRobin.sol index efd7f287..47b6bd9a 100644 --- a/blockchain_assets/contracts/RoundRobin.sol +++ b/blockchain_assets/contracts/RoundRobin.sol @@ -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; // ------------------------------------------------------------------------ @@ -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( diff --git a/blockchain_assets/test_contracts/RoundRobin.t.sol b/blockchain_assets/test_contracts/RoundRobin.t.sol index 7ce45fab..c76e2b4b 100644 --- a/blockchain_assets/test_contracts/RoundRobin.t.sol +++ b/blockchain_assets/test_contracts/RoundRobin.t.sol @@ -266,4 +266,21 @@ contract RoundRobinTest is Test { ); } -} \ No newline at end of file + /// @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"); + } + +} diff --git a/configuration/Cargo.toml b/configuration/Cargo.toml index 7ae669d9..f07c5e78 100644 --- a/configuration/Cargo.toml +++ b/configuration/Cargo.toml @@ -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" diff --git a/configuration/src/addresses.rs b/configuration/src/addresses.rs index 9bc4cb7d..a46de70f 100644 --- a/configuration/src/addresses.rs +++ b/configuration/src/addresses.rs @@ -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}; diff --git a/configuration/src/logging.rs b/configuration/src/logging.rs index cd311b36..9282223b 100644 --- a/configuration/src/logging.rs +++ b/configuration/src/logging.rs @@ -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); + })); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 33e77c19..e16c4121 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -50,7 +50,7 @@ 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" @@ -58,6 +58,7 @@ 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" } diff --git a/lib/src/error.rs b/lib/src/error.rs index 37958945..035214fc 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -6,33 +6,22 @@ use ark_serialize::SerializationError; use jf_plonk::errors::PlonkError; use jf_primitives::poseidon::PoseidonError; use jf_relation::errors::CircuitError; -use std::{ - error::Error, - fmt::{self, Debug, Display}, -}; +use std::error::Error; +use std::fmt; use warp::reject::Reject; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, thiserror::Error)] pub enum HexError { + #[error("Invalid string length")] InvalidStringLength, + #[error("Invalid string")] InvalidString, + #[error("Invalid hex format")] InvalidHexFormat, + #[error("Invalid conversion")] InvalidConversion, } -impl std::fmt::Display for HexError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - HexError::InvalidStringLength => write!(f, "Invalid string length"), - HexError::InvalidString => write!(f, "Invalid string"), - HexError::InvalidHexFormat => write!(f, "Invalid hex format"), - HexError::InvalidConversion => write!(f, "Invalid conversion"), - } - } -} - -impl std::error::Error for HexError {} - #[derive(Debug)] pub struct CertificateVerificationError { message: String, @@ -98,268 +87,124 @@ impl Error for KeyVerificationError {} impl Reject for KeyVerificationError {} /// Errors that can be throw when working with a blockchain client connector -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum BlockchainClientConnectionError { - RpcError(RpcError), - TransportError(TransportError), + #[error("RPC error: {0}")] + RpcError(#[from] RpcError), + #[error("Transport error: {0}")] + TransportError(#[from] TransportError), + #[error("Provider error: {0}")] ProviderError(String), - WalletError(WalletError), - AzureError(Box), + #[error("Wallet error: {0}")] + WalletError(#[from] WalletError), + #[error("Azure error: {0}")] + AzureError(#[from] Box), + #[error("InvalidWalletType: {0}")] InvalidWalletType(String), } -impl Display for BlockchainClientConnectionError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - BlockchainClientConnectionError::RpcError(e) => write!(f, "RPC error: {e}"), - BlockchainClientConnectionError::TransportError(e) => write!(f, "Transport error: {e}"), - BlockchainClientConnectionError::ProviderError(e) => write!(f, "Provider error: {e}"), - BlockchainClientConnectionError::WalletError(e) => write!(f, "Wallet error: {e}"), - BlockchainClientConnectionError::AzureError(e) => write!(f, "Azure error: {e}"), - BlockchainClientConnectionError::InvalidWalletType(e) => { - write!(f, "InvalidWalletType: {e}") - } - } - } -} - -impl Error for BlockchainClientConnectionError {} - impl From for BlockchainClientConnectionError { fn from(e: String) -> Self { BlockchainClientConnectionError::ProviderError(e) } } -impl From> for BlockchainClientConnectionError { - fn from(e: RpcError) -> Self { - BlockchainClientConnectionError::RpcError(e) - } -} - -impl From for BlockchainClientConnectionError { - fn from(e: WalletError) -> Self { - BlockchainClientConnectionError::WalletError(e) - } -} - -impl From> for BlockchainClientConnectionError { - fn from(e: Box) -> Self { - BlockchainClientConnectionError::AzureError(e) - } -} -impl From for BlockchainClientConnectionError { - fn from(e: TransportError) -> Self { - BlockchainClientConnectionError::TransportError(e) - } -} /// An error that we can throw during type conversion -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ConversionError { + #[error("Overflow during conversion. Uints cannot be bigger than (q-1)/2 where q is the modulus of the scalar field")] Overflow, + #[error("Error during proof decompression")] ProofDecompression, + #[error("Error during proof compression: {0}")] ProofCompression(SerializationError), - SerialisationError(SerializationError), + #[error("Error during serialisation: {0}")] + SerialisationError(#[from] SerializationError), + #[error("Could not convert the public data bytes into ERC20 deposit data")] NotErc20DepositData, + #[error("Failed to convert to a fixed length array")] FixedLengthArrayError, + #[error("Failed to parse data")] ParseFailed, - PoseidonError(PoseidonError), + #[error("Poseidon Error: {0}")] + PoseidonError(#[from] PoseidonError), + #[error("Invalid token type")] InvalidTokenType, } -impl Error for ConversionError {} - -impl Display for ConversionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConversionError::Overflow => write!(f, "Overflow during conversion. Uints cannot be bigger than (q-1)/2 where q is the modulus of the scalar field"), - ConversionError::ProofDecompression => write!(f, "Error during proof decompression"), - ConversionError::SerialisationError(e) => write!(f, "Error during serialisation: {e}"), - ConversionError::NotErc20DepositData => write!(f, "Could not convert the public data bytes into ERC20 deposit data"), - ConversionError::ProofCompression(e) => write!(f, "Error during proof compression: {e}"), - ConversionError::FixedLengthArrayError => write!(f, "Failed to convert to a fixed length array"), - ConversionError::ParseFailed => write!(f, "Failed to parse data"), - ConversionError::PoseidonError(e) => write!(f, "Poseidon Error: {e}"), - ConversionError::InvalidTokenType => write!(f, "Invalid token type"), - } - } -} impl Reject for ConversionError {} -impl From for ConversionError { - fn from(e: SerializationError) -> Self { - ConversionError::SerialisationError(e) - } -} - -impl From for ConversionError { - fn from(e: PoseidonError) -> Self { - Self::PoseidonError(e) - } -} - /// Error type used by the Event Listener, that listens for blockchain events and processes them. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum EventHandlerError { + #[error("Could not connect to event stream")] NoEventStream, + #[error("Event stream terminated")] StreamTerminated, + #[error("Invalid calldata")] InvalidCalldata, + #[error("IO Error: {0}")] IOError(String), + #[error("Missing layer 2 blocks. Last processed was: {0}")] MissingBlocks(usize), + #[error("Hashing error")] HashError, + #[error("Block not found: {0}")] BlockNotFound(u64), + #[error("Block hash error, expected block hash: {0}, got block hash: {1}")] BlockHashError(Fr254, Fr254), } - -impl Display for EventHandlerError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - EventHandlerError::NoEventStream => write!(f, "Could not connect to event stream"), - EventHandlerError::StreamTerminated => write!(f, "Event stream terminated"), - EventHandlerError::InvalidCalldata => write!(f, "Invalid calldata"), - EventHandlerError::IOError(s) => write!(f, "IO Error: {s}"), - EventHandlerError::MissingBlocks(n) => { - write!(f, "Missing layer 2 blocks. Last processed was: {n}") - } - EventHandlerError::HashError => write!(f, "Hashing error"), - EventHandlerError::BlockNotFound(block_number) => { - write!(f, "Block not found: {block_number}") - } - EventHandlerError::BlockHashError(a, b) => write!( - f, - "Block hash error, expected block hash: {a}, got block hash: {b}" - ), - } - } -} - -impl Error for EventHandlerError {} impl Reject for EventHandlerError {} /// Error type for handling calls to a token contract -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum NightfallContractError { - BlockchainClientConnectionError(BlockchainClientConnectionError), - ConversionError(ConversionError), + #[error("Nightfall Contract Error: Blockchain Client Connection Error: {0}")] + BlockchainClientConnectionError(#[from] BlockchainClientConnectionError), + #[error("Nightfall Contract Error: Error while converting to Solidity type: {0}")] + ConversionError(#[from] ConversionError), + #[error("Did not receive a transaction receipt")] TransactionError, + #[error("Escrow Funds Error: {0}")] EscrowError(String), + #[error("De-Escrow Funds Error: {0}")] DeEscrowError(String), + #[error("Contract Verification Error: {0}")] ContractVerificationError(String), - PoseidonError(PoseidonError), + #[error("Hashing Error: {0}")] + PoseidonError(#[from] PoseidonError), + #[error("Layer 2 block number {0} not found on-chain")] BlockNotFound(u64), + #[error("Blockchain provider error: {0}")] ProviderError(String), + #[error("Missing transaction hash: {0}")] MissingTransactionHash(String), + #[error("Transaction not found: {0}")] TransactionNotFound(alloy::primitives::TxHash), + #[error("ABI decode error: {0}")] AbiDecodeError(String), + #[error("Decoded call error: {0}")] DecodedCallError(String), + #[error("X509 error: {0}")] X509Error(String), + #[error("Block proposal error: {0}")] BlockProposalError(String), } -impl Display for NightfallContractError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - NightfallContractError::BlockchainClientConnectionError(e) => write!( - f, - "Nightfall Contract Error: Blockchain Client Connection Error: {e}" - ), - NightfallContractError::ConversionError(e) => write!( - f, - "Nightfall Contract Error: Error while converting to Solidity type: {e}" - ), - NightfallContractError::TransactionError => { - write!(f, "Did not receive a transaction receipt") - } - NightfallContractError::EscrowError(s) => write!(f, "Escrow Funds Error: {s}"), - NightfallContractError::DeEscrowError(s) => write!(f, "De-Escrow Funds Error: {s}"), - NightfallContractError::ContractVerificationError(s) => { - write!(f, "Contract Verification Error: {s}") - } - NightfallContractError::PoseidonError(e) => write!(f, "Hashing Error: {e}"), - NightfallContractError::BlockNotFound(n) => { - write!(f, "Layer 2 block number {n} not found on-chain") - } - NightfallContractError::ProviderError(e) => { - write!(f, "Blockchain provider error: {e}") - } - NightfallContractError::MissingTransactionHash(s) => { - write!(f, "Missing transaction hash: {s}") - } - NightfallContractError::TransactionNotFound(tx_hash) => { - write!(f, "Transaction not found: {tx_hash}") - } - NightfallContractError::AbiDecodeError(s) => { - write!(f, "ABI decode error: {s}") - } - NightfallContractError::DecodedCallError(s) => { - write!(f, "Decoded call error: {s}") - } - NightfallContractError::X509Error(s) => { - write!(f, "X509 error: {s}") - } - NightfallContractError::BlockProposalError(s) => { - write!(f, "Block proposal error: {s}") - } - } - } -} - -impl Error for NightfallContractError {} - -impl From for NightfallContractError { - fn from(e: BlockchainClientConnectionError) -> Self { - Self::BlockchainClientConnectionError(e) - } -} - -impl From for NightfallContractError { - fn from(e: ConversionError) -> Self { - Self::ConversionError(e) - } -} - -impl From for NightfallContractError { - fn from(e: PoseidonError) -> Self { - Self::PoseidonError(e) - } -} - /// Error type for proposer rotation -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ProposerError { + #[error("Failed to get list of Proposers")] FailedToGetProposers, + #[error("Provider error")] ProviderError(String), } -impl std::fmt::Display for ProposerError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ProposerError::FailedToGetProposers => { - write!(f, "Failed to get list of Proposers") - } - ProposerError::ProviderError(_) => { - write!(f, "Provider error") - } - } - } -} - -impl std::error::Error for ProposerError {} - impl warp::reject::Reject for ProposerError {} -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ConfigError { + #[error("Invalid block size: {0}")] InvalidBlockSize(String), + #[error("Configuration error: {0}")] Other(String), } - -impl fmt::Display for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ConfigError::InvalidBlockSize(msg) => write!(f, "Invalid block size: {msg}"), - ConfigError::Other(msg) => write!(f, "Configuration error: {msg}"), - } - } -} - -impl std::error::Error for ConfigError {} diff --git a/lib/src/log_fetcher.rs b/lib/src/log_fetcher.rs index 2d1e390b..977e3370 100644 --- a/lib/src/log_fetcher.rs +++ b/lib/src/log_fetcher.rs @@ -6,7 +6,7 @@ use alloy::providers::Provider; use alloy::rpc::types::{Filter, Log}; use configuration::settings::get_settings; -use log::{debug, info, warn}; +use tracing::{debug, info, warn}; use std::error::Error; use std::fmt; diff --git a/lib/src/merkle_trees/indexed.rs b/lib/src/merkle_trees/indexed.rs index 734f5b54..fd8c6cd8 100644 --- a/lib/src/merkle_trees/indexed.rs +++ b/lib/src/merkle_trees/indexed.rs @@ -11,7 +11,7 @@ use jf_primitives::{ Directions, MembershipProof, PathElement, }, }; -use log::{debug, error}; +use tracing::{debug, error}; use mongodb::bson::doc; use std::convert::TryFrom; // already in prelude, but explicit is fine diff --git a/lib/src/merkle_trees/mutable.rs b/lib/src/merkle_trees/mutable.rs index 4fe30cae..c9b0d64e 100644 --- a/lib/src/merkle_trees/mutable.rs +++ b/lib/src/merkle_trees/mutable.rs @@ -14,7 +14,7 @@ use jf_primitives::{ poseidon::{Poseidon, PoseidonParams}, trees::{CircuitInsertionInfo, Directions, MembershipProof, PathElement, TreeHasher}, }; -use log::debug; +use tracing::debug; use mongodb::{ bson::{doc, to_bson}, options::{UpdateOneModel, WriteModel}, diff --git a/lib/src/nf_token_id.rs b/lib/src/nf_token_id.rs index 0079de4a..07d4cf24 100644 --- a/lib/src/nf_token_id.rs +++ b/lib/src/nf_token_id.rs @@ -4,7 +4,7 @@ use crate::{error::ConversionError, hex_conversion::HexConvertible}; use alloy::primitives::{Address, U256}; use ark_bn254::Fr as Fr254; use ark_ff::{BigInteger, PrimeField}; -use log::debug; +use tracing::debug; use num::BigUint; use sha2::{Digest, Sha256}; diff --git a/lib/src/plonk_prover/mod.rs b/lib/src/plonk_prover/mod.rs index 9688a005..9a96d463 100644 --- a/lib/src/plonk_prover/mod.rs +++ b/lib/src/plonk_prover/mod.rs @@ -10,7 +10,7 @@ use ark_bn254::Bn254; use ark_serialize::CanonicalDeserialize; use jf_plonk::nightfall::ipa_structs::ProvingKey; use jf_primitives::pcs::prelude::UnivariateKzgPCS; -use log::warn; +use tracing::warn; use std::sync::{Arc, OnceLock}; /// This function is used to retrieve the client proving key. diff --git a/lib/src/plonk_prover/plonk_proof.rs b/lib/src/plonk_prover/plonk_proof.rs index 9d5c65c0..2ee270e2 100644 --- a/lib/src/plonk_prover/plonk_proof.rs +++ b/lib/src/plonk_prover/plonk_proof.rs @@ -19,7 +19,7 @@ use crate::{ use alloy::primitives::Bytes; use jf_primitives::{pcs::prelude::UnivariateKzgPCS, rescue::sponge::RescueCRHF}; use jf_relation::PlonkCircuit; -use log::{debug, error}; +use tracing::{debug, error}; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/lib/src/shared_entities.rs b/lib/src/shared_entities.rs index e381db88..92651bd3 100644 --- a/lib/src/shared_entities.rs +++ b/lib/src/shared_entities.rs @@ -14,7 +14,7 @@ use ark_serialize::SerializationError; use ark_std::UniformRand; use ark_std::Zero; use jf_primitives::poseidon::{FieldHasher, Poseidon, PoseidonError}; -use log::{error, warn}; +use tracing::{error, warn}; use nf_curves::ed_on_bn254::BabyJubjub as BabyJubJub; use nightfall_bindings::artifacts::Nightfall; use serde::{Deserialize, Serialize}; diff --git a/lib/src/tests_utils.rs b/lib/src/tests_utils.rs index 0630387a..a3b9cfa4 100644 --- a/lib/src/tests_utils.rs +++ b/lib/src/tests_utils.rs @@ -1,4 +1,4 @@ -use log::{info, warn}; +use tracing::{info, warn}; use mongodb::bson::doc; use std::time::Duration; use testcontainers::{ diff --git a/lib/src/utils.rs b/lib/src/utils.rs index 31100ab7..56b22017 100644 --- a/lib/src/utils.rs +++ b/lib/src/utils.rs @@ -5,7 +5,7 @@ use ark_std::path::PathBuf; /// A module containing uncategorised functions used by more than one component use configuration::settings::get_settings; use futures::StreamExt; -use log::{debug, info, warn}; +use tracing::{debug, info, warn}; use serde::ser::StdError; use std::{fmt, time::Duration}; use tokio::{runtime::Handle, task::block_in_place}; diff --git a/lib/src/validate_certificate.rs b/lib/src/validate_certificate.rs index 8e2ab3d8..c633fcb8 100644 --- a/lib/src/validate_certificate.rs +++ b/lib/src/validate_certificate.rs @@ -12,7 +12,7 @@ use alloy::{ }; use configuration::{addresses::get_addresses, settings::get_settings}; use futures::stream::TryStreamExt; -use log::{debug, error, trace, warn}; +use tracing::{debug, error, trace, warn}; use nightfall_bindings::artifacts::X509; use openssl::{ asn1::Asn1Time, diff --git a/lib/src/validate_keys.rs b/lib/src/validate_keys.rs index 1115337e..2ba84cc9 100644 --- a/lib/src/validate_keys.rs +++ b/lib/src/validate_keys.rs @@ -42,7 +42,7 @@ use jf_plonk::{ }, }; use jf_primitives::{pcs::prelude::UnivariateKzgPCS, rescue::sponge::RescueCRHF}; -use log::{debug, error, info}; +use tracing::{debug, error, info}; use nightfall_bindings::artifacts::{RollupProofVerifier, VKHashProvider}; use reqwest::{Client, StatusCode}; use sha3::{Digest, Keccak256}; diff --git a/lib/src/verify_contract.rs b/lib/src/verify_contract.rs index febac7e7..9c1fafb7 100644 --- a/lib/src/verify_contract.rs +++ b/lib/src/verify_contract.rs @@ -8,7 +8,7 @@ use configuration::{ settings::Settings, }; use eyre::eyre; -use log::debug; +use tracing::debug; use nightfall_bindings::artifacts::{Nightfall, RoundRobin, X509}; use nightfall_bindings::artifacts::{ Nightfall::NightfallInstance, RoundRobin::RoundRobinInstance, X509::X509Instance, diff --git a/lib/src/wallets.rs b/lib/src/wallets.rs index 72d6abad..97fbc8b8 100644 --- a/lib/src/wallets.rs +++ b/lib/src/wallets.rs @@ -15,7 +15,7 @@ use base64::prelude::*; use configuration::settings::WalletTypeConfig; use k256::ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey}; use k256::EncodedPoint; -use log::{debug, info}; +use tracing::{debug, info}; use std::sync::Arc; use url::Url; diff --git a/nightfall_bindings/Cargo.toml b/nightfall_bindings/Cargo.toml index 1cf66cdd..76ef2ebc 100644 --- a/nightfall_bindings/Cargo.toml +++ b/nightfall_bindings/Cargo.toml @@ -6,11 +6,11 @@ edition = "2021" [dependencies] alloy = { version = "1.0.23", features = ["full"] } configuration = { path = "../configuration" } -log = "0.4.27" +tracing = "0.1" [build-dependencies] configuration = { path = "../configuration" } -log = "0.4.27" +tracing = "0.1" serde = { version = "1", features = ["derive"] } serde_json = "1" alloy = { version = "1.0.23", features = ["full"] } diff --git a/nightfall_bindings/build.rs b/nightfall_bindings/build.rs index 7023b9bc..50342fd2 100644 --- a/nightfall_bindings/build.rs +++ b/nightfall_bindings/build.rs @@ -1,4 +1,4 @@ -use log::info; +use tracing::info; use std::{env, os::unix::process::ExitStatusExt, path::Path, path::PathBuf, process::Command}; fn main() { diff --git a/nightfall_client/Cargo.toml b/nightfall_client/Cargo.toml index 81cf171e..5a13fa05 100644 --- a/nightfall_client/Cargo.toml +++ b/nightfall_client/Cargo.toml @@ -47,17 +47,18 @@ serde_with = "2.3.3" config = "0.13.4" configuration = { path = "../configuration" } nightfall_bindings = { path = "../nightfall_bindings" } -log = "0.4.27" nf-curves = { git = "https://git@github.com/EYBlockchain/nightfish_CE.git" } reqwest = "0.11.27" url = "2.5.4" rustc-hex = "2.1.0" uint = "0.9.5" lazy_static = "1.5.0" +prometheus = "0.14" testcontainers = { version = "0.24.0", features = ["blocking"] } lib = { path = "../lib" } uuid = { version = "1.16.0", features = ["v4"] } ark-crypto-primitives = { version = "0.4", features = ["sponge"] } +thiserror = "2" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } diff --git a/nightfall_client/src/domain/error.rs b/nightfall_client/src/domain/error.rs index f81eef39..67d11af1 100644 --- a/nightfall_client/src/domain/error.rs +++ b/nightfall_client/src/domain/error.rs @@ -1,25 +1,14 @@ -use std::{ - error::Error, - fmt::{Debug, Display, Formatter}, -}; +use std::fmt::{Debug, Display, Formatter}; use jf_primitives::poseidon::PoseidonError; use lib::error::ConversionError; use lib::error::{BlockchainClientConnectionError, EventHandlerError, NightfallContractError}; -use warp::reject::{self}; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("Failed to perform client operation")] pub struct FailedClientOperation; -impl Error for FailedClientOperation {} - -impl std::fmt::Display for FailedClientOperation { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Failed to perform client operation") - } -} - -impl reject::Reject for FailedClientOperation {} +impl warp::reject::Reject for FailedClientOperation {} /// errors for a merkle tree #[derive(Debug)] @@ -35,7 +24,7 @@ pub enum MerkleTreeError { InvalidProof, } -impl Error for MerkleTreeError {} +impl std::error::Error for MerkleTreeError {} impl Display for MerkleTreeError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -52,166 +41,77 @@ impl Display for MerkleTreeError { } } -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] /// Error type used by the handler that processes deposit, transfer and withdraw transactions pub enum TransactionHandlerError { + #[error("Json conversion error: {0}")] JsonConversionError(serde_json::Error), - DepositError(DepositError), + #[error("Deposit error: {0}")] + DepositError(#[from] DepositError), + #[error("Database error")] DatabaseError, + #[error("Transaction error: {0}")] CustomError(String), + #[error("Transaction error")] Error, + #[error("Client not synchronized")] ClientNotSynchronized, } -impl Display for TransactionHandlerError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - TransactionHandlerError::JsonConversionError(e) => { - write!(f, "Json conversion error: {e}") - } - TransactionHandlerError::DepositError(e) => write!(f, "Deposit error: {e}"), - TransactionHandlerError::DatabaseError => write!(f, "Database error"), - TransactionHandlerError::CustomError(s) => write!(f, "Transaction error: {s}"), - TransactionHandlerError::Error => write!(f, "Transaction error"), - TransactionHandlerError::ClientNotSynchronized => write!(f, "Client not synchronized"), - } - } -} - -impl Error for TransactionHandlerError {} - /// Error type for handling calls to a token contract -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum TokenContractError { - BlockchainClientConnectionError(BlockchainClientConnectionError), - ConversionError(ConversionError), + #[error("Token Contract Error: Blockchain Client Connection Error: {0}")] + BlockchainClientConnectionError(#[from] BlockchainClientConnectionError), + #[error("Token Contract Error: Error while converting to Solidity type: {0}")] + ConversionError(#[from] ConversionError), + #[error("Did not receive a transaction receipt")] TransactionError, + #[error("Token Type Error: {0}")] TokenTypeError(String), } -impl Display for TokenContractError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TokenContractError::BlockchainClientConnectionError(e) => write!( - f, - "Token Contract Error: Blockchain Client Connection Error: {e}" - ), - TokenContractError::ConversionError(e) => write!( - f, - "Token Contract Error: Error while converting to Solidity type: {e}" - ), - TokenContractError::TransactionError => { - write!(f, "Did not receive a transaction receipt") - } - TokenContractError::TokenTypeError(s) => write!(f, "Token Type Error: {s}"), - } - } -} - -impl Error for TokenContractError {} - -impl From for TokenContractError { - fn from(e: BlockchainClientConnectionError) -> Self { - Self::BlockchainClientConnectionError(e) - } -} - -impl From for TokenContractError { - fn from(e: ConversionError) -> Self { - Self::ConversionError(e) - } -} - -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum DepositError { - TokenError(TokenContractError), - NightfallError(NightfallContractError), - PoseidonError(PoseidonError), + #[error("Deposit Error: {0}")] + TokenError(#[from] TokenContractError), + #[error("Deposit Error: {0}")] + NightfallError(#[from] NightfallContractError), + #[error("Deposit Error: {0}")] + PoseidonError(#[from] PoseidonError), } -impl Display for DepositError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DepositError::TokenError(e) => write!(f, "Deposit Error: {e}"), - DepositError::NightfallError(e) => write!(f, "Deposit Error: {e}"), - DepositError::PoseidonError(e) => write!(f, "Deposit Error: {e}"), - } - } -} - -impl Error for DepositError {} - -impl From for DepositError { - fn from(e: TokenContractError) -> Self { - Self::TokenError(e) - } -} - -impl From for DepositError { - fn from(e: NightfallContractError) -> Self { - Self::NightfallError(e) - } -} - -impl From for DepositError { - fn from(e: PoseidonError) -> Self { - Self::PoseidonError(e) - } -} - -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("Could not sync {0}")] pub struct SyncingError(pub EventHandlerError); -impl Display for SyncingError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - SyncingError(e) => write!(f, "Could not sync {e}"), - } - } -} - -impl Error for SyncingError {} - /// Custom rejection type for REST API errors -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ClientRejection { + #[error("No such token found")] NoSuchToken, + #[error("Invalid token id")] InvalidTokenId, + #[error("Invalid token type")] InvalidTokenType, + #[error("Invalid request id")] InvalidRequestId, + #[error("Queue is full")] QueueFull, + #[error("Database error or duplicate transaction")] DatabaseError, + #[error("Invalid commitment key")] InvalidCommitmentKey, + #[error("Commitment not found")] CommitmentNotFound, + #[error("Failed to get list of Proposers")] ProposerError, + #[error("No such request")] RequestNotFound, + #[error("Failed to de-escrow funds")] FailedDeEscrow, + #[error("Synchronisation service unavailable")] SynchronisationUnavailable, } -impl std::fmt::Display for ClientRejection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ClientRejection::NoSuchToken => write!(f, "No such token found"), - ClientRejection::InvalidTokenId => write!(f, "Invalid token id"), - ClientRejection::InvalidTokenType => write!(f, "Invalid token type"), - ClientRejection::InvalidRequestId => write!(f, "Invalid request id"), - ClientRejection::QueueFull => write!(f, "Queue is full"), - ClientRejection::DatabaseError => { - write!(f, "Database error or duplicate transaction") - } - ClientRejection::InvalidCommitmentKey => write!(f, "Invalid commitment key"), - ClientRejection::CommitmentNotFound => write!(f, "Commitment not found"), - ClientRejection::ProposerError => write!(f, "Failed to get list of Proposers"), - ClientRejection::RequestNotFound => write!(f, "No such request"), - ClientRejection::FailedDeEscrow => write!(f, "Failed to de-escrow funds"), - ClientRejection::SynchronisationUnavailable => { - write!(f, "Synchronisation service unavailable") - } - } - } -} - -impl std::error::Error for ClientRejection {} - impl warp::reject::Reject for ClientRejection {} diff --git a/nightfall_client/src/driven/contract_functions/nightfall_contract.rs b/nightfall_client/src/driven/contract_functions/nightfall_contract.rs index bcd6c55c..359d5e22 100644 --- a/nightfall_client/src/driven/contract_functions/nightfall_contract.rs +++ b/nightfall_client/src/driven/contract_functions/nightfall_contract.rs @@ -23,7 +23,7 @@ use lib::{ shared_entities::{DepositSecret, TokenType, WithdrawData}, verify_contract::VerifiedContracts, }; -use log::{debug, info}; +use tracing::{debug, info}; use nightfall_bindings::artifacts::{Nightfall, IERC3525}; use num::BigUint; diff --git a/nightfall_client/src/driven/contract_functions/token_contracts.rs b/nightfall_client/src/driven/contract_functions/token_contracts.rs index c8f490a2..bd06e103 100644 --- a/nightfall_client/src/driven/contract_functions/token_contracts.rs +++ b/nightfall_client/src/driven/contract_functions/token_contracts.rs @@ -12,7 +12,7 @@ use lib::{ error::BlockchainClientConnectionError, initialisation::get_blockchain_client_connection, }; -use log::debug; +use tracing::debug; use nightfall_bindings::artifacts::{IERC1155, IERC20, IERC3525, IERC721}; impl TokenContract for IERC20::IERC20Calls { diff --git a/nightfall_client/src/driven/db/mongo.rs b/nightfall_client/src/driven/db/mongo.rs index f269b10e..1bf0b2cf 100644 --- a/nightfall_client/src/driven/db/mongo.rs +++ b/nightfall_client/src/driven/db/mongo.rs @@ -21,7 +21,7 @@ use lib::{ shared_entities::{Preimage, WithdrawData}, }; use lib::{hex_conversion::HexConvertible, shared_entities::TokenType}; -use log::{debug, error, info}; +use tracing::{debug, error, info}; use mongodb::{ bson::doc, error::{ErrorKind, WriteFailure::WriteError}, @@ -431,12 +431,20 @@ impl RequestCommitmentMappingDB for Client { impl CommitmentDB for Client { async fn get_all_commitments( &self, + limit: Option, + offset: Option, ) -> Result, mongodb::error::Error> { - let mut cursor = self + let mut find = self .database(DB) .collection::("commitments") - .find(doc! {}) - .await?; + .find(doc! {}); + if let Some(offset) = offset { + find = find.skip(offset); + } + if let Some(limit) = limit { + find = find.limit(limit.min(i64::MAX as u64) as i64); + } + let mut cursor = find.await?; let mut result: Vec<(Fr254, CommitmentEntry)> = Vec::new(); while cursor.advance().await? { let v = cursor.deserialize_current()?; @@ -553,7 +561,7 @@ impl CommitmentDB for Client { let k_string = k.to_hex_string(); debug!("Getting commitment with key: {k_string}"); let commitment_1 = self - .get_all_commitments() + .get_all_commitments(None, None) .await .expect("Database error") .into_iter() diff --git a/nightfall_client/src/driven/event_handlers/nightfall_event.rs b/nightfall_client/src/driven/event_handlers/nightfall_event.rs index 8cabdffc..e5cf8dff 100644 --- a/nightfall_client/src/driven/event_handlers/nightfall_event.rs +++ b/nightfall_client/src/driven/event_handlers/nightfall_event.rs @@ -37,7 +37,7 @@ use lib::{ initialisation::get_blockchain_client_connection, shared_entities::{CompressedSecrets, OnChainTransaction, Preimage, Salt}, }; -use log::{debug, error, info, warn}; +use tracing::{debug, error, info, warn}; use nightfall_bindings::artifacts::Nightfall; use std::{collections::HashSet, sync::OnceLock}; use tokio::{join, sync::Mutex}; diff --git a/nightfall_client/src/driven/primitives/kemdem_functions.rs b/nightfall_client/src/driven/primitives/kemdem_functions.rs index b13d5291..bff9d0c4 100644 --- a/nightfall_client/src/driven/primitives/kemdem_functions.rs +++ b/nightfall_client/src/driven/primitives/kemdem_functions.rs @@ -3,7 +3,7 @@ use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use jf_primitives::poseidon::{FieldHasher, Poseidon, PoseidonError}; use lib::plonk_prover::circuits::DOMAIN_SHARED_SALT; -use log::error; +use tracing::error; use nf_curves::ed_on_bn254::{BabyJubjub, Fq as Fr254, Fr as BJJScalar}; use super::*; diff --git a/nightfall_client/src/driven/queue.rs b/nightfall_client/src/driven/queue.rs index 842b2494..0d0c2ee7 100644 --- a/nightfall_client/src/driven/queue.rs +++ b/nightfall_client/src/driven/queue.rs @@ -17,7 +17,7 @@ use lib::{ nf_client_proof::{Proof, ProvingEngine}, shared_entities::SynchronisationPhase::Desynchronized, }; -use log::{debug, error, info, warn}; +use tracing::{debug, error, info, warn}; use std::{collections::VecDeque, time::Duration}; use tokio::{ sync::{OnceCell, RwLock}, diff --git a/nightfall_client/src/drivers/blockchain/event_listener_manager.rs b/nightfall_client/src/drivers/blockchain/event_listener_manager.rs index 362d1e53..1ef2686b 100644 --- a/nightfall_client/src/drivers/blockchain/event_listener_manager.rs +++ b/nightfall_client/src/drivers/blockchain/event_listener_manager.rs @@ -9,7 +9,7 @@ use crate::drivers::blockchain::nightfall_event_listener::get_synchronisation_st use ark_bn254::Fr as Fr254; use configuration::settings::get_settings; use lib::shared_entities::SynchronisationPhase::Synchronized; -use log::{debug, info, warn}; +use tracing::{debug, info, warn}; use mongodb::Client as MongoClient; use tokio::{ sync::{Mutex, OnceCell, RwLock}, diff --git a/nightfall_client/src/drivers/blockchain/nightfall_event_listener.rs b/nightfall_client/src/drivers/blockchain/nightfall_event_listener.rs index 91af36d1..bbc625eb 100644 --- a/nightfall_client/src/drivers/blockchain/nightfall_event_listener.rs +++ b/nightfall_client/src/drivers/blockchain/nightfall_event_listener.rs @@ -24,7 +24,7 @@ use lib::{ log_fetcher::get_logs_paginated, shared_entities::{OnChainTransaction, SynchronisationPhase, SynchronisationStatus}, }; -use log::{debug, info, warn}; +use tracing::{debug, info, warn}; use nightfall_bindings::artifacts::Nightfall; use std::{panic, time::Duration}; use tokio::time::sleep; diff --git a/nightfall_client/src/drivers/rest/client_nf_3.rs b/nightfall_client/src/drivers/rest/client_nf_3.rs deleted file mode 100644 index 15225f6c..00000000 --- a/nightfall_client/src/drivers/rest/client_nf_3.rs +++ /dev/null @@ -1,1108 +0,0 @@ -use super::client_operation::handle_client_operation; -use crate::{ - domain::{ - entities::{ - CommitmentStatus, ERCAddress, Operation, OperationType, RequestStatus, Transport, - }, - error::TransactionHandlerError, - notifications::NotificationPayload, - }, - driven::{ - db::mongo::CommitmentEntry, - queue::{get_queue, QueuedRequest, TransactionRequest}, - }, - get_zkp_keys, - initialisation::get_db_connection, - ports::{ - contracts::NightfallContract, - db::{CommitmentDB, CommitmentEntryDB, RequestCommitmentMappingDB, RequestDB}, - }, - services::{ - client_operation::deposit_operation, commitment_selection::find_usable_commitments, - }, -}; -use ark_bn254::Fr as Fr254; -use ark_ec::twisted_edwards::Affine; -use ark_ff::{BigInteger256, Zero}; -use ark_std::{rand::thread_rng, UniformRand}; -use configuration::{addresses::get_addresses, settings::get_settings}; -use jf_primitives::poseidon::{FieldHasher, Poseidon}; -use lib::{ - client_models::{DeEscrowDataReq, NF3DepositRequest, NF3TransferRequest, NF3WithdrawRequest}, - commitments::{Commitment, Nullifiable}, - contract_conversions::FrBn254, - derive_key::ZKPKeys, - get_fee_token_id, - hex_conversion::HexConvertible, - nf_client_proof::{Proof, ProvingEngine}, - nf_token_id::to_nf_token_id_from_str, - plonk_prover::circuits::DOMAIN_SHARED_SALT, - serialization::ark_de_hex, - shared_entities::{DepositSecret, Preimage, Salt, TokenType}, -}; -use log::{debug, error, info}; -use nf_curves::ed_on_bn254::{BJJTEAffine as JubJub, BabyJubjub, Fr as BJJScalar}; -use nightfall_bindings::artifacts::{Nightfall, IERC1155, IERC20, IERC3525, IERC721}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use warp::{ - hyper::StatusCode, - path, - reply::{self, json, Reply}, - Filter, -}; -#[derive(Serialize, Deserialize)] -pub struct WithdrawResponse { - success: bool, - message: String, - pub withdraw_fund_salt: String, // Return the withdraw_fund_salt -} -#[derive(Deserialize)] -struct JubJubPubKey(#[serde(deserialize_with = "ark_de_hex")] JubJub); -// A simplified client interface, which provides Deposit, Transfer and Withdraw operations, -// with automated commitment selection, but without the flexibility of the lower-level -// client_operation API. -// It matches the API of NF_3 so it can be used with the NF_3 client, under the hood, it calls -// the client_operation handler - -pub fn deposit_request

( -) -> impl Filter + Clone -where - P: Proof, -{ - path!("v1" / "deposit") - .and(warp::body::json()) - .and_then(queue_deposit_request) -} - -pub fn transfer_request

( -) -> impl Filter + Clone -where - P: Proof, -{ - path!("v1" / "transfer") - .and(warp::body::json()) - .and_then(queue_transfer_request) -} - -pub fn withdraw_request

( -) -> impl Filter + Clone -where - P: Proof, -{ - path!("v1" / "withdraw") - .and(warp::body::json()) - .and_then(queue_withdraw_request) -} - -/// function to queue the deposit requests -async fn queue_deposit_request( - deposit_req: NF3DepositRequest, -) -> Result { - let transaction_request = TransactionRequest::Deposit(deposit_req); - let uuid_string = Uuid::new_v4().to_string(); - - debug!("Queueing deposit request"); - queue_request(transaction_request, uuid_string).await -} - -/// function to queue the transfer requests -async fn queue_transfer_request( - transfer_req: NF3TransferRequest, -) -> Result { - let transaction_request = TransactionRequest::Transfer(transfer_req); - let uuid_string = Uuid::new_v4().to_string(); - - queue_request(transaction_request, uuid_string).await -} - -/// function to queue the withdraw requests -async fn queue_withdraw_request( - withdraw_req: NF3WithdrawRequest, -) -> Result { - let transaction_request = TransactionRequest::Withdraw(withdraw_req); - let uuid_string = Uuid::new_v4().to_string(); - - queue_request(transaction_request, uuid_string).await -} - -/// This function queues all types of transaction request -async fn queue_request( - transaction_request: TransactionRequest, - request_id: String, -) -> Result { - let settings = get_settings(); - let max_queue_size = settings - .nightfall_client - .max_queue_size - .unwrap_or(1000) - .try_into() - .unwrap(); - - // check if the id is a valid uuid - if Uuid::parse_str(&request_id).is_err() { - return Err(warp::reject::custom( - crate::domain::error::ClientRejection::InvalidRequestId, - )); - }; - - // add the request to the queue - debug!("Adding request to queue"); - let mut q = get_queue().await.write().await; - // check if the queue is full - if q.len() >= max_queue_size { - return Ok(reply::with_header( - reply::with_status( - json(&"Queue is full".to_string()), - StatusCode::SERVICE_UNAVAILABLE, - ), - "X-Request-ID", - request_id, - )); - } - debug!("got lock on queue"); - q.push_back(QueuedRequest { - transaction_request, - uuid: request_id.clone(), - }); - drop(q); // drop the lock so other processes can access the queue - debug!("Added request to queue"); - // record the request as queued - let db = get_db_connection().await; - if db - .store_request(&request_id, RequestStatus::Queued) - .await - .is_none() - { - return Err(warp::reject::custom( - crate::domain::error::ClientRejection::DatabaseError, - )); - } - debug!("Stored request status in database"); - - // return a 202 Accepted response with the request ID - Ok(reply::with_header( - reply::with_status(json(&"Request queued".to_string()), StatusCode::ACCEPTED), - "X-Request-ID", - request_id, - )) -} - -/// This function wraps the various transaction handlers, so that the queue can call the correct handler -/// based on the request type. -pub async fn handle_request( - request: TransactionRequest, - request_id: &str, -) -> Result -where - P: Proof, - E: ProvingEngine

, - N: NightfallContract, -{ - match request { - TransactionRequest::Deposit(deposit_req) => { - handle_deposit::(deposit_req, request_id).await - } - TransactionRequest::Transfer(transfer_req) => { - handle_transfer::(transfer_req, request_id).await - } - TransactionRequest::Withdraw(withdraw_req) => { - handle_withdraw::(withdraw_req, request_id).await - } - } -} - -/// handle_client_deposit_request is the entry point for deposit requests from the client. -pub async fn handle_deposit( - req: NF3DepositRequest, - id: &str, -) -> Result { - info!("Deposit raw request: {req:?}"); - - // We convert the request into values - let NF3DepositRequest { - erc_address, - token_id, - token_type, - value, - fee, - deposit_fee, - .. - } = req; - - let erc_address = ERCAddress::try_from_hex_string(&erc_address).map_err(|err| { - error!("{id} Could not convert ERC address {err}"); - TransactionHandlerError::CustomError(err.to_string()) - })?; - - let token_id: BigInteger256 = - BigInteger256::from_hex_string(token_id.as_str()).map_err(|err| { - error!("{id} Could not convert hex string to BigInteger256"); - TransactionHandlerError::CustomError(err.to_string()) - })?; - - let token_type: TokenType = u8::from_str_radix(&token_type, 16) - .map_err(|err| { - error!("{id} Could not convert token type"); - TransactionHandlerError::CustomError(err.to_string()) - })? - .into(); - - let fee: Fr254 = Fr254::from_hex_string(fee.as_str()).map_err(|err| { - error!("{id} Could not convert fee"); - TransactionHandlerError::CustomError(err.to_string()) - })?; - - let deposit_fee: Fr254 = Fr254::from_hex_string(deposit_fee.as_str()).map_err(|err| { - error!("{id} Could not convert deposit fee"); - TransactionHandlerError::CustomError(err.to_string()) - })?; - - let value: Fr254 = Fr254::from_hex_string(value.as_str()).map_err(|err| { - error!("{id} Could not wrangle value {err}"); - TransactionHandlerError::CustomError(err.to_string()) - })?; - - let (secret_preimage_one, secret_preimage_two, secret_preimage_three) = { - // RNG is Send and scoped to this block - let mut rng = thread_rng(); - ( - Fr254::rand(&mut rng), - Fr254::rand(&mut rng), - Fr254::rand(&mut rng), - ) - }; - - let secret_preimage = DepositSecret::new( - secret_preimage_one, - secret_preimage_two, - secret_preimage_three, - ); - - let db: &'static mongodb::Client = get_db_connection().await; - - // Then match on the token type and call the correct function - let (preimage_value, preimage_fee_option) = match token_type { - TokenType::ERC20 => { - deposit_operation::( - erc_address, - value, - fee, - deposit_fee, - token_id, - secret_preimage, - token_type, - id, - ) - .await - } - TokenType::ERC721 => { - deposit_operation::( - erc_address, - value, - fee, - deposit_fee, - token_id, - secret_preimage, - token_type, - id, - ) - .await - } - TokenType::ERC1155 => { - deposit_operation::( - erc_address, - value, - fee, - deposit_fee, - token_id, - secret_preimage, - token_type, - id, - ) - .await - } - TokenType::ERC3525 => { - deposit_operation::( - erc_address, - value, - fee, - deposit_fee, - token_id, - secret_preimage, - token_type, - id, - ) - .await - } - TokenType::FeeToken => todo!(), - } - .map_err(TransactionHandlerError::DepositError)?; - - // Insert the preimage into the commitments DB as pending creation - // TODO remove the blocknumber - let ZKPKeys { nullifier_key, .. } = *get_zkp_keys().lock().expect("Poisoned Mutex lock"); - let nullifier = preimage_value - .nullifier_hash(&nullifier_key) - .expect("Could not hash commitment {}"); - let commitment_hash = preimage_value.hash().expect("Could not hash commitment"); - let commitment_entry = CommitmentEntry::new( - preimage_value, - nullifier, - CommitmentStatus::PendingCreation, - token_type, - None, - None, - ); - - db.store_commitment(commitment_entry) - .await - .ok_or(TransactionHandlerError::DatabaseError)?; - - debug!("{id} Deposit commitment stored successfully"); - - // Add the mapping between request and commitment - let commitment_hex = commitment_hash.to_hex_string(); - match db.add_mapping(id, &commitment_hex).await { - Ok(_) => debug!("{id} Mapped commitment to request"), - Err(e) => error!("{id} Failed to map commitment to request: {e}"), - } - - // Check if preimage_fee_option is Some, and store it in the DB if it exists - if let Some(preimage_fee) = preimage_fee_option { - let nullifier = preimage_fee - .nullifier_hash(&nullifier_key) - .expect("Could not hash commitment"); - let commitment_hash = preimage_fee.hash().expect("Could not hash commitment"); - - // Add the mapping for fee commitment as well - let commitment_hex = commitment_hash.to_hex_string(); - match db.add_mapping(id, &commitment_hex).await { - Ok(_) => debug!("{id} Mapped deposit fee commitment to request"), - Err(e) => error!("{id} Failed to map deposit fee commitment to request: {e}"), - } - - let commitment_entry = CommitmentEntry::new( - preimage_fee, - nullifier, - CommitmentStatus::PendingCreation, - TokenType::FeeToken, - None, - None, - ); - // Store the fee commitment in the database, error if storage fails - db.store_commitment(commitment_entry) - .await - .ok_or(TransactionHandlerError::DatabaseError)?; - } - - debug!("{id} Deposit fee commitment stored successfully"); - - let response_data = match preimage_fee_option { - Some(preimage_fee) => vec![ - preimage_value - .hash() - .expect("Preimage must be hashable - this should not happen") - .to_hex_string(), - preimage_fee - .hash() - .expect("Preimage must be hashable - this should not happen") - .to_hex_string(), - ], - None => vec![preimage_value - .hash() - .expect("Preimage must be hashable - this should not happen") - .to_hex_string()], - }; - debug!("{id} Deposit request completed successfully - returning reply to caller"); - - let response = serde_json::to_string(&response_data).map_err(|e| { - error!("{id} Error when serialising response: {e}"); - TransactionHandlerError::JsonConversionError(e) - })?; - let uuid = serde_json::to_string(&id).map_err(|e| { - error!("{id} Error when serialising request ID: {e}"); - TransactionHandlerError::JsonConversionError(e) - })?; - - Ok(NotificationPayload::TransactionEvent { response, uuid }) -} - -async fn handle_transfer( - transfer_req: NF3TransferRequest, - id: &str, -) -> Result -where - P: Proof, - E: ProvingEngine

, - N: NightfallContract, -{ - debug!("Handling transfer request: {transfer_req:?}"); - let NF3TransferRequest { - erc_address, - token_id, - recipient_data, - fee, - .. - } = transfer_req; - - // Convert the request into the relevant types. - let nf_token_id = - to_nf_token_id_from_str(erc_address.as_str(), token_id.as_str()).map_err(|e| { - error!( - "{id} Error when retrieving the Nightfall token id from the erc address and token ID {e}" - ); - TransactionHandlerError::CustomError(e.to_string()) - })?; - let keys = get_zkp_keys().lock().expect("Poisoned Mutex lock").clone(); - - let value = - Fr254::from_hex_string(recipient_data.values.first().unwrap().as_str()).map_err(|e| { - error!("{id} Error when reading value: {e}"); - TransactionHandlerError::CustomError(e.to_string()) - })?; - - let fee: Fr254 = Fr254::from_hex_string(fee.as_str()).map_err(|e| { - error!("{id} Error when reading fee: {e}"); - TransactionHandlerError::CustomError(e.to_string()) - })?; - - let first_key = recipient_data - .recipient_compressed_zkp_public_keys - .first() - .ok_or_else(|| { - error!("{id} No recipient public key provided"); - TransactionHandlerError::CustomError("missing recipient public key".into()) - })?; - - // Create a JSON string that represents the tuple struct content - let json_wrapped = format!("\"{first_key}\""); - - // Note: ark_de_hex deserialization additionally ensures the point is on-curve and in correct subgroup - // Unit tests verify this validation behavior remains consistent - let deserialized_public_key: JubJubPubKey = - serde_json::from_str(&json_wrapped).map_err(|e| { - error!("{id} Could not deserialize recipient public key: {e}"); - TransactionHandlerError::CustomError(format!( - "Could not deserialize recipient public key: {e}" - )) - })?; - - let recipient_public_key = deserialized_public_key.0; - - // Check that the recipient public key is not the identity point - if recipient_public_key.is_zero() { - error!("{id} Recipient public key cannot be the identity point"); - return Err(TransactionHandlerError::CustomError( - "Recipient public key cannot be the identity point".to_string(), - )); - } - - let ephemeral_private_key = { - let mut rng = ark_std::rand::thread_rng(); // TODO initialise in main and pass around as a rwlock - BJJScalar::rand(&mut rng) - }; - let shared_secret: Affine = (recipient_public_key * ephemeral_private_key).into(); - - // add the id to the request database - - // Select the commitments to be spent. - let spend_commitments; - { - let db = get_db_connection().await; - let fee_token_id = get_fee_token_id(); - let spend_value_commitments = find_usable_commitments(nf_token_id, value,db) - .await.map_err(|e|{ - error!("{id} Could not find enough usable value commitments to complete this transfer, suggest depositing more tokens: {e}"); - TransactionHandlerError::CustomError(e.to_string())})?; - let spend_fee_commitments = if fee.is_zero() { - [Preimage::default(), Preimage::default()] - } else { - match find_usable_commitments(fee_token_id, fee, db).await { - Ok(commitments) => commitments, - Err(e) => { - debug!("{id} Could not find enough usable fee commitments, suggest depositing more fee: {e}"); - // rollback the value commitments to unspent if fails to find fee commitments - let value_commitment_ids = spend_value_commitments - .iter() - .filter_map(|c| c.hash().ok()) - .collect::>(); - - for commitment_id in &value_commitment_ids { - if let Some(existing) = db.get_commitment(commitment_id).await { - let _ = db - .mark_commitments_unspent( - &[*commitment_id], - existing.layer_1_transaction_hash, - existing.layer_2_block_number, - ) - .await; - } - } - return Err(TransactionHandlerError::CustomError(e.to_string())); - } - } - }; - spend_commitments = [ - spend_value_commitments[0], - spend_value_commitments[1], - spend_fee_commitments[0], - spend_fee_commitments[1], - ]; - } - - // Work out how much change is needed. - let total_token_value = spend_commitments[..2] - .iter() - .map(|c| c.get_value()) - .sum::(); - - let token_change = total_token_value - value; - let total_fee_value = spend_commitments[2..] - .iter() - .map(|c| c.get_value()) - .sum::(); - let fee_change = total_fee_value - fee; - - let poseidon = Poseidon::::new(); - // Derive a shared salt from the shared secret using domain-separated Poseidon hash. - let shared_salt_hash = poseidon - .hash(&[shared_secret.x, shared_secret.y, DOMAIN_SHARED_SALT]) - .map_err(|e| { - error!("{id} Failed to derive shared salt with Poseidon: {e}"); - TransactionHandlerError::CustomError(e.to_string()) - })?; - let shared_salt = Salt::Transfer(shared_salt_hash); - - // transferred value commitment, salt is derived from the shared secret - let new_commitment_one = Preimage::new( - value, - nf_token_id, - spend_commitments[0].get_nf_slot_id(), - recipient_public_key, - shared_salt, - ); - - let new_commitment_two = if !token_change.is_zero() { - Preimage::new( - token_change, - nf_token_id, - spend_commitments[0].get_nf_slot_id(), - keys.zkp_public_key, - Salt::new_transfer_salt(), - ) - } else { - Preimage::default() - }; - - let nightfall_address = FrBn254::from(get_addresses().nightfall()).0; - let contract_nf_address = Affine::::new_unchecked(Fr254::zero(), nightfall_address); - - let fee_token_id = get_fee_token_id(); - // if fee is zero, then no fee commitment is needed - let new_commitment_three = if !fee.is_zero() { - Preimage::new( - fee, - fee_token_id, - fee_token_id, - contract_nf_address, - Salt::new_transfer_salt(), - ) - } else { - Preimage::default() - }; - - let new_commitment_four = if !fee_change.is_zero() { - Preimage::new( - fee_change, - fee_token_id, - fee_token_id, - keys.zkp_public_key, - Salt::new_transfer_salt(), - ) - } else { - Preimage::default() - }; - - let new_commitments = [ - new_commitment_one, - new_commitment_two, - new_commitment_three, - new_commitment_four, - ]; - - dbg!(new_commitments - .iter() - .map(|c| c.hash().unwrap().to_hex_string()) - .collect::>()); - - let secret_preimages = [ - spend_commitments[0].get_secret_preimage(), - spend_commitments[1].get_secret_preimage(), - spend_commitments[2].get_secret_preimage(), - spend_commitments[3].get_secret_preimage(), - ]; - let op = Operation { - transport: Transport::OffChain, - operation_type: OperationType::Transfer, - }; - match handle_client_operation::( - op, - spend_commitments, - new_commitments, - ephemeral_private_key, - Fr254::zero(), - secret_preimages, - id, - ) - .await - { - Ok(res) => Ok(res), - Err(e) => { - // rollback to UNSPENT status if handle_client_operation fails - let db = get_db_connection().await; - - // Rollback the spend commitments to unspent - let commitment_ids = spend_commitments - .iter() - .map(|c| c.hash().unwrap()) - .collect::>(); - - info!( - "{id} Rolling back {} spend commitments", - commitment_ids.len() - ); - - for commitment_id in &commitment_ids { - if let Some(existing) = db.get_commitment(commitment_id).await { - let _ = db - .mark_commitments_unspent( - &[*commitment_id], - existing.layer_1_transaction_hash, - existing.layer_2_block_number, - ) - .await; - } - } - // Delete new commitments - let new_commitment_ids = new_commitments - .iter() - .map(|c| c.hash().unwrap()) - .collect::>(); - - info!("{id} Deleting {} new commitments", new_commitment_ids.len()); - let _ = db.delete_commitments(new_commitment_ids).await; - - Err(TransactionHandlerError::CustomError(e.to_string())) - } - } -} - -async fn handle_withdraw( - withdraw_req: NF3WithdrawRequest, - id: &str, -) -> Result -where - P: Proof, - E: ProvingEngine

, - N: NightfallContract, -{ - let NF3WithdrawRequest { - erc_address, - token_id, - value, - recipient_address, - fee, - .. - } = withdraw_req; - - // add the id to the request database - - // Convert the request into the relevant types. - let nf_token_id = - to_nf_token_id_from_str(erc_address.as_str(), token_id.as_str()).map_err(|e| { - error!( - "{id} Error when retrieving the Nightfall token id from the erc address and token ID {e}"); - TransactionHandlerError::CustomError(e.to_string()) - })?; - - let keys = get_zkp_keys().lock().expect("Poisoned Mutex lock").clone(); - - let value = Fr254::from_hex_string(value.as_str()).map_err(|e| { - error!("{id} Error when reading value: {e}"); - TransactionHandlerError::CustomError(e.to_string()) - })?; - - let fee: Fr254 = Fr254::from_hex_string(fee.as_str()).map_err(|e| { - error!("{id} Error when reading fee: {e}"); - TransactionHandlerError::CustomError(e.to_string()) - })?; - - let recipient_address: Fr254 = - Fr254::from_hex_string(recipient_address.as_str()).map_err(|e| { - error!("{id} Error when reading recipeint address: {e}"); - TransactionHandlerError::CustomError(e.to_string()) - })?; - // For now we just use the commitment selection algorithm to minimise change. - let spend_commitments; - let db = get_db_connection().await; - - { - let fee_token_id = get_fee_token_id(); - let spend_value_commitments = find_usable_commitments(nf_token_id, value,db) - .await.map_err(|e|{ - error!("{id} Could not find enough usable value commitments to complete this withdraw, suggest depositing more tokens: {e}"); - TransactionHandlerError::CustomError(e.to_string())})?; - let spend_fee_commitments = if fee.is_zero() { - [Preimage::default(), Preimage::default()] - } else { - match find_usable_commitments(fee_token_id, fee, db).await { - Ok(commitments) => commitments, - Err(e) => { - error!("{id} Could not find enough usable fee commitments to complete this withdraw, suggest depositing more fee: {e}"); - // rollback the value commitments to unspent if fails to find fee commitments - let value_commitment_ids = spend_value_commitments - .iter() - .filter_map(|c| c.hash().ok()) - .collect::>(); - for commitment_id in &value_commitment_ids { - if let Some(existing) = db.get_commitment(commitment_id).await { - let _ = db - .mark_commitments_unspent( - &[*commitment_id], - existing.layer_1_transaction_hash, - existing.layer_2_block_number, - ) - .await; - } - } - return Err(TransactionHandlerError::CustomError(e.to_string())); - } - } - }; - spend_commitments = [ - spend_value_commitments[0], - spend_value_commitments[1], - spend_fee_commitments[0], - spend_fee_commitments[1], - ]; - } - // Work out how much change is needed. - let total_token_value = spend_commitments[..2] - .iter() - .map(|c| c.get_value()) - .sum::(); - let token_change = total_token_value - value; - - let total_fee_value = spend_commitments[2..] - .iter() - .map(|c| c.get_value()) - .sum::(); - let fee_change = total_fee_value - fee; - - let nightfall_address = FrBn254::from(get_addresses().nightfall()).0; - let contract_nf_address = Affine::::new_unchecked(Fr254::zero(), nightfall_address); - - // The first commitment of the withdraw is 0, which will be calculated in the circuit - // here, we set new_commitment_one to have the withdraw value so we can check that value is conserved for transfer and withdraw in client_operation services. - // We set public_key of this preimage to the contract_nf_address, so that it won't be added in PendingCommitment later (as we only add preimages in PendingCommitment iff commitment.get_public_key() == zkp_public_key). - let new_commitment_one = Preimage::new( - value, - nf_token_id, - spend_commitments[0].get_nf_slot_id(), - contract_nf_address, - Salt::new_transfer_salt(), - ); - - let new_commitment_two = if !token_change.is_zero() { - Preimage::new( - token_change, - nf_token_id, - spend_commitments[0].get_nf_slot_id(), - keys.zkp_public_key, - Salt::new_transfer_salt(), - ) - } else { - Preimage::default() - }; - - let fee_token_id = get_fee_token_id(); - - let new_commitment_three = if !fee.is_zero() { - Preimage::new( - fee, - fee_token_id, - fee_token_id, - contract_nf_address, - Salt::new_transfer_salt(), - ) - } else { - Preimage::default() - }; - let new_commitment_four = if !fee_change.is_zero() { - Preimage::new( - fee_change, - fee_token_id, - fee_token_id, - keys.zkp_public_key, - Salt::new_transfer_salt(), - ) - } else { - Preimage::default() - }; - - let new_commitments = [ - new_commitment_one, - new_commitment_two, - new_commitment_three, - new_commitment_four, - ]; - - let secret_preimages = [ - spend_commitments[0].get_secret_preimage(), - spend_commitments[1].get_secret_preimage(), - spend_commitments[2].get_secret_preimage(), - spend_commitments[3].get_secret_preimage(), - ]; - let op = Operation { - transport: Transport::OffChain, - operation_type: OperationType::Withdraw, - }; - let withdraw_fund_salt = spend_commitments[0] - .nullifier_hash(&keys.nullifier_key) - .expect("Failed to compute nullifier hash"); - match handle_client_operation::( - op, - spend_commitments, - new_commitments, - BJJScalar::zero(), - recipient_address, - secret_preimages, - id, - ) - .await - { - Ok(res) => { - let de_escrow_req = DeEscrowDataReq { - token_id: token_id.clone(), - erc_address: erc_address.clone(), - recipient_address: recipient_address.to_hex_string(), - value: value.to_hex_string(), - token_type: withdraw_req.token_type.clone(), - withdraw_fund_salt: withdraw_fund_salt.to_hex_string(), - }; - match serde_json::to_string(&de_escrow_req) { - Ok(child_args_json) => { - if db - .update_request_child_args(id, &child_args_json) - .await - .is_none() - { - error!("{id} Failed to store child_request_args in database"); - } else { - debug!("{id} Successfully stored child_request_args in request collection"); - } - } - Err(e) => { - error!("{id} Failed to serialize de_escrow_req: {e}"); - } - } - res - } - Err(e) => { - // Rollback to UNSPENT status if handle_client_operation fails - let db = get_db_connection().await; - - // Rollback spend commitments - let commitment_ids = spend_commitments - .iter() - .map(|c| c.hash().unwrap()) - .collect::>(); - - info!( - "{id} Rolling back {} spend commitments to Unspent", - commitment_ids.len() - ); - for commitment_id in &commitment_ids { - if let Some(existing) = db.get_commitment(commitment_id).await { - let _ = db - .mark_commitments_unspent( - &[*commitment_id], - existing.layer_1_transaction_hash, - existing.layer_2_block_number, - ) - .await; - } - } - - // Delete new commitments - let new_commitment_ids = new_commitments - .iter() - .map(|c| c.hash().unwrap()) - .collect::>(); - - info!("{id} Deleting {} new commitments", new_commitment_ids.len()); - let _ = db.delete_commitments(new_commitment_ids).await; - return Err(e); - } - }; - - // Build the response - let withdraw_response = WithdrawResponse { - success: true, - message: "Withdraw operation completed successfully".to_string(), - withdraw_fund_salt: withdraw_fund_salt.to_hex_string(), - }; - - let response = serde_json::to_string(&withdraw_response).map_err(|e| { - error!("{id} Error when serialising response: {e}"); - TransactionHandlerError::JsonConversionError(e) - })?; - let uuid = serde_json::to_string(&id).map_err(|e| { - error!("{id} Error when serialising request ID: {e}"); - TransactionHandlerError::JsonConversionError(e) - })?; - - // Return the response as JSON - Ok(NotificationPayload::TransactionEvent { response, uuid }) -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_ff::One; - use ark_serialize::{CanonicalSerialize, Compress}; - use ark_std::Zero; - use lib::{ - client_models::NF3RecipientData, - plonk_prover::plonk_proof::{PlonkProof, PlonkProvingEngine}, - }; - use nf_curves::ed_on_bn254::BabyJubjub; - use nf_curves::ed_on_bn254::Fq; - - /// Tests that transfer API rejects invalid recipient public keys - #[tokio::test] - async fn test_transfer_api_rejects_invalid_recipient_keys() { - // Invalid compressed point (not on the curve) should fail early in handle_transfer - let invalid_transfer_req = NF3TransferRequest { - erc_address: "0x1234567890123456789012345678901234567890".to_string(), - token_id: "0x00".to_string(), - recipient_data: NF3RecipientData { - values: vec!["0x04".to_string()], - recipient_compressed_zkp_public_keys: vec![ - "0x000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000" - .to_string(), - ], - }, - fee: "0x00".to_string(), - }; - - let result = handle_transfer::( - invalid_transfer_req, - "test-id-1", - ) - .await; - - // This should fail at the recipient key validation stage, demonstrating the API validates keys - assert!( - result.is_err(), - "Transfer API should reject invalid recipient public key" - ); - if let Err(TransactionHandlerError::CustomError(msg)) = result { - assert!( - msg.contains("Could not deserialize recipient public key: the input buffer contained invalid data"), - "Error should indicate recipient public key deserialization failure, got: {msg}" - ); - } else { - panic!("Expected TransactionHandlerError::CustomError with recipient public key deserialization failure"); - } - } - - #[tokio::test] - async fn test_transfer_api_rejects_identity_recipient_keys() { - // Identity point should fail early in handle_transfer - let identity_point = Affine::::zero(); - let mut compressed_bytes = Vec::new(); - identity_point - .serialize_with_mode(&mut compressed_bytes, Compress::Yes) - .unwrap(); - compressed_bytes.reverse(); // Convert to big-endian to match ark_se_hex format - let identity_point_hex = format!("0x{}", hex::encode(compressed_bytes)); - - let identity_point_transfer_req = NF3TransferRequest { - erc_address: "0x1234567890123456789012345678901234567890".to_string(), - token_id: "0x00".to_string(), - recipient_data: NF3RecipientData { - values: vec!["0x04".to_string()], - recipient_compressed_zkp_public_keys: vec![identity_point_hex], - }, - fee: "0x00".to_string(), - }; - - let result = handle_transfer::( - identity_point_transfer_req, - "test-id-2", - ) - .await; - - assert!( - result.is_err(), - "Transfer API should reject recipient public key if it is the identity" - ); - if let Err(TransactionHandlerError::CustomError(msg)) = result { - assert!( - msg.contains("Recipient public key cannot be the identity point"), - "Error should indicate recipient public key cannot be the identity point, got: {msg}" - ); - } else { - panic!("Expected TransactionHandlerError::CustomError with recipient public key cannot be the identity point"); - } - } - - #[tokio::test] - async fn test_transfer_api_rejects_low_order_recipient_keys() { - // A point that is low order but on the curve should fail early in handle_transfer - // We use point (0, -1) which is order 2 on BabyJubJub - let zero_x = Fq::zero(); - let neg_one_y = -Fq::one(); - - let low_order_point = Affine::::new_unchecked(zero_x, neg_one_y); - - let mut compressed_bytes = Vec::new(); - low_order_point - .serialize_with_mode(&mut compressed_bytes, Compress::Yes) - .unwrap(); - compressed_bytes.reverse(); // Convert to big-endian to match ark_se_hex format - let low_order_hex = format!("0x{}", hex::encode(compressed_bytes)); - - let low_order_transfer_req = NF3TransferRequest { - erc_address: "0x1234567890123456789012345678901234567890".to_string(), - token_id: "0x00".to_string(), - recipient_data: NF3RecipientData { - values: vec!["0x04".to_string()], - recipient_compressed_zkp_public_keys: vec![low_order_hex], - }, - fee: "0x00".to_string(), - }; - - let result = handle_transfer::( - low_order_transfer_req, - "test-id-3", - ) - .await; - - // This should fail at the explicit .check() stage since zero point is low-order - assert!( - result.is_err(), - "Transfer API should reject low-order recipient public key" - ); - if let Err(TransactionHandlerError::CustomError(msg)) = result { - assert!( - msg.contains("Could not deserialize recipient public key: the input buffer contained invalid data"), - "Error should indicate recipient public key deserialization failure, got: {msg}" - ); - } else { - panic!("Expected TransactionHandlerError::CustomError with recipient public key deserialization failure"); - } - } -} diff --git a/nightfall_client/src/drivers/rest/client_nf_3/deposit.rs b/nightfall_client/src/drivers/rest/client_nf_3/deposit.rs new file mode 100644 index 00000000..a0f679bc --- /dev/null +++ b/nightfall_client/src/drivers/rest/client_nf_3/deposit.rs @@ -0,0 +1,273 @@ +use crate::{ + domain::{ + entities::CommitmentStatus, + error::TransactionHandlerError, + notifications::NotificationPayload, + }, + driven::db::mongo::CommitmentEntry, + get_zkp_keys, + initialisation::get_db_connection, + ports::{ + contracts::NightfallContract, + db::{CommitmentDB, CommitmentEntryDB, RequestCommitmentMappingDB}, + }, + services::client_operation::deposit_operation, +}; +use ark_bn254::Fr as Fr254; +use ark_ff::BigInteger256; +use ark_std::{rand::thread_rng, UniformRand}; +use lib::{ + client_models::NF3DepositRequest, + commitments::{Commitment, Nullifiable}, + derive_key::ZKPKeys, + hex_conversion::HexConvertible, + shared_entities::{DepositSecret, TokenType}, +}; +use tracing::{debug, error, info}; +use nightfall_bindings::artifacts::{Nightfall, IERC1155, IERC20, IERC3525, IERC721}; + +use super::ERCAddress; + +/// handle_client_deposit_request is the entry point for deposit requests from the client. +pub async fn handle_deposit( + req: NF3DepositRequest, + id: &str, +) -> Result { + info!("Deposit raw request: {req:?}"); + + // We convert the request into values + let NF3DepositRequest { + erc_address, + token_id, + token_type, + value, + fee, + deposit_fee, + .. + } = req; + + let erc_address = ERCAddress::try_from_hex_string(&erc_address).map_err(|err| { + error!("{id} Could not convert ERC address {err}"); + TransactionHandlerError::CustomError(err.to_string()) + })?; + + let token_id: BigInteger256 = + BigInteger256::from_hex_string(token_id.as_str()).map_err(|err| { + error!("{id} Could not convert hex string to BigInteger256"); + TransactionHandlerError::CustomError(err.to_string()) + })?; + + let token_type: TokenType = u8::from_str_radix(&token_type, 16) + .map_err(|err| { + error!("{id} Could not convert token type"); + TransactionHandlerError::CustomError(err.to_string()) + })? + .into(); + + let fee: Fr254 = Fr254::from_hex_string(fee.as_str()).map_err(|err| { + error!("{id} Could not convert fee"); + TransactionHandlerError::CustomError(err.to_string()) + })?; + + let deposit_fee: Fr254 = Fr254::from_hex_string(deposit_fee.as_str()).map_err(|err| { + error!("{id} Could not convert deposit fee"); + TransactionHandlerError::CustomError(err.to_string()) + })?; + + let value: Fr254 = Fr254::from_hex_string(value.as_str()).map_err(|err| { + error!("{id} Could not wrangle value {err}"); + TransactionHandlerError::CustomError(err.to_string()) + })?; + + let (secret_preimage_one, secret_preimage_two, secret_preimage_three) = { + // RNG is Send and scoped to this block + let mut rng = thread_rng(); + ( + Fr254::rand(&mut rng), + Fr254::rand(&mut rng), + Fr254::rand(&mut rng), + ) + }; + + let secret_preimage = DepositSecret::new( + secret_preimage_one, + secret_preimage_two, + secret_preimage_three, + ); + + let db: &'static mongodb::Client = get_db_connection().await; + + // Then match on the token type and call the correct function + let (preimage_value, preimage_fee_option) = match token_type { + TokenType::ERC20 => { + deposit_operation::( + erc_address, + value, + fee, + deposit_fee, + token_id, + secret_preimage, + token_type, + id, + ) + .await + } + TokenType::ERC721 => { + deposit_operation::( + erc_address, + value, + fee, + deposit_fee, + token_id, + secret_preimage, + token_type, + id, + ) + .await + } + TokenType::ERC1155 => { + deposit_operation::( + erc_address, + value, + fee, + deposit_fee, + token_id, + secret_preimage, + token_type, + id, + ) + .await + } + TokenType::ERC3525 => { + deposit_operation::( + erc_address, + value, + fee, + deposit_fee, + token_id, + secret_preimage, + token_type, + id, + ) + .await + } + TokenType::FeeToken => todo!(), + } + .map_err(TransactionHandlerError::DepositError)?; + + // Insert the preimage into the commitments DB as pending creation + let ZKPKeys { nullifier_key, .. } = *get_zkp_keys().lock().expect("Poisoned Mutex lock"); + let nullifier = preimage_value + .nullifier_hash(&nullifier_key) + .map_err(|e| { + error!("{id} Could not compute nullifier hash: {e}"); + TransactionHandlerError::CustomError(format!("Could not compute nullifier hash: {e}")) + })?; + let commitment_hash = preimage_value.hash().map_err(|e| { + error!("{id} Could not hash commitment: {e}"); + TransactionHandlerError::CustomError(format!("Could not hash commitment: {e}")) + })?; + let commitment_entry = CommitmentEntry::new( + preimage_value, + nullifier, + CommitmentStatus::PendingCreation, + token_type, + None, + None, + ); + + db.store_commitment(commitment_entry) + .await + .ok_or(TransactionHandlerError::DatabaseError)?; + + debug!("{id} Deposit commitment stored successfully"); + + // Add the mapping between request and commitment + let commitment_hex = commitment_hash.to_hex_string(); + match db.add_mapping(id, &commitment_hex).await { + Ok(_) => debug!("{id} Mapped commitment to request"), + Err(e) => error!("{id} Failed to map commitment to request: {e}"), + } + + // Check if preimage_fee_option is Some, and store it in the DB if it exists + if let Some(preimage_fee) = preimage_fee_option { + let nullifier = preimage_fee + .nullifier_hash(&nullifier_key) + .map_err(|e| { + error!("{id} Could not compute fee nullifier hash: {e}"); + TransactionHandlerError::CustomError(format!( + "Could not compute fee nullifier hash: {e}" + )) + })?; + let commitment_hash = preimage_fee.hash().map_err(|e| { + error!("{id} Could not hash fee commitment: {e}"); + TransactionHandlerError::CustomError(format!("Could not hash fee commitment: {e}")) + })?; + + // Add the mapping for fee commitment as well + let commitment_hex = commitment_hash.to_hex_string(); + match db.add_mapping(id, &commitment_hex).await { + Ok(_) => debug!("{id} Mapped deposit fee commitment to request"), + Err(e) => error!("{id} Failed to map deposit fee commitment to request: {e}"), + } + + let commitment_entry = CommitmentEntry::new( + preimage_fee, + nullifier, + CommitmentStatus::PendingCreation, + TokenType::FeeToken, + None, + None, + ); + // Store the fee commitment in the database, error if storage fails + db.store_commitment(commitment_entry) + .await + .ok_or(TransactionHandlerError::DatabaseError)?; + } + + debug!("{id} Deposit fee commitment stored successfully"); + + let response_data = match preimage_fee_option { + Some(preimage_fee) => vec![ + preimage_value + .hash() + .map_err(|e| { + error!("{id} Could not hash preimage value: {e}"); + TransactionHandlerError::CustomError(format!( + "Could not hash preimage value: {e}" + )) + })? + .to_hex_string(), + preimage_fee + .hash() + .map_err(|e| { + error!("{id} Could not hash preimage fee: {e}"); + TransactionHandlerError::CustomError(format!( + "Could not hash preimage fee: {e}" + )) + })? + .to_hex_string(), + ], + None => vec![preimage_value + .hash() + .map_err(|e| { + error!("{id} Could not hash preimage value: {e}"); + TransactionHandlerError::CustomError(format!( + "Could not hash preimage value: {e}" + )) + })? + .to_hex_string()], + }; + debug!("{id} Deposit request completed successfully - returning reply to caller"); + + let response = serde_json::to_string(&response_data).map_err(|e| { + error!("{id} Error when serialising response: {e}"); + TransactionHandlerError::JsonConversionError(e) + })?; + let uuid = serde_json::to_string(&id).map_err(|e| { + error!("{id} Error when serialising request ID: {e}"); + TransactionHandlerError::JsonConversionError(e) + })?; + + Ok(NotificationPayload::TransactionEvent { response, uuid }) +} diff --git a/nightfall_client/src/drivers/rest/client_nf_3/mod.rs b/nightfall_client/src/drivers/rest/client_nf_3/mod.rs new file mode 100644 index 00000000..af4f8c40 --- /dev/null +++ b/nightfall_client/src/drivers/rest/client_nf_3/mod.rs @@ -0,0 +1,219 @@ +mod deposit; +mod transfer; +mod withdraw; + +pub use deposit::handle_deposit; + +use super::client_operation::handle_client_operation; +use crate::{ + domain::{ + entities::{ + CommitmentStatus, ERCAddress, Operation, OperationType, RequestStatus, Transport, + }, + error::TransactionHandlerError, + notifications::NotificationPayload, + }, + driven::{ + db::mongo::CommitmentEntry, + queue::{get_queue, QueuedRequest, TransactionRequest}, + }, + get_zkp_keys, + initialisation::get_db_connection, + ports::{ + contracts::NightfallContract, + db::{CommitmentDB, CommitmentEntryDB, RequestCommitmentMappingDB, RequestDB}, + }, + services::{ + client_operation::deposit_operation, commitment_selection::find_usable_commitments, + }, +}; +use ark_bn254::Fr as Fr254; +use ark_ec::twisted_edwards::Affine; +use ark_ff::{BigInteger256, Zero}; +use ark_std::{rand::thread_rng, UniformRand}; +use configuration::{addresses::get_addresses, settings::get_settings}; +use jf_primitives::poseidon::{FieldHasher, Poseidon}; +use lib::{ + client_models::{DeEscrowDataReq, NF3DepositRequest, NF3TransferRequest, NF3WithdrawRequest}, + commitments::{Commitment, Nullifiable}, + contract_conversions::FrBn254, + derive_key::ZKPKeys, + get_fee_token_id, + hex_conversion::HexConvertible, + nf_client_proof::{Proof, ProvingEngine}, + nf_token_id::to_nf_token_id_from_str, + plonk_prover::circuits::DOMAIN_SHARED_SALT, + serialization::ark_de_hex, + shared_entities::{DepositSecret, Preimage, Salt, TokenType}, +}; +use tracing::{debug, error, info, warn}; +use nf_curves::ed_on_bn254::{BJJTEAffine as JubJub, BabyJubjub, Fr as BJJScalar}; +use nightfall_bindings::artifacts::{Nightfall, IERC1155, IERC20, IERC3525, IERC721}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use warp::{ + hyper::StatusCode, + path, + reply::{self, json, Reply}, + Filter, +}; +#[derive(Serialize, Deserialize)] +pub struct WithdrawResponse { + success: bool, + message: String, + pub withdraw_fund_salt: String, // Return the withdraw_fund_salt +} +// A simplified client interface, which provides Deposit, Transfer and Withdraw operations, +// with automated commitment selection, but without the flexibility of the lower-level +// client_operation API. +// It matches the API of NF_3 so it can be used with the NF_3 client, under the hood, it calls +// the client_operation handler + +pub fn deposit_request

( +) -> impl Filter + Clone +where + P: Proof, +{ + path!("v1" / "deposit") + .and(warp::body::json()) + .and_then(queue_deposit_request) +} + +pub fn transfer_request

( +) -> impl Filter + Clone +where + P: Proof, +{ + path!("v1" / "transfer") + .and(warp::body::json()) + .and_then(queue_transfer_request) +} + +pub fn withdraw_request

( +) -> impl Filter + Clone +where + P: Proof, +{ + path!("v1" / "withdraw") + .and(warp::body::json()) + .and_then(queue_withdraw_request) +} + +/// function to queue the deposit requests +async fn queue_deposit_request( + deposit_req: NF3DepositRequest, +) -> Result { + let transaction_request = TransactionRequest::Deposit(deposit_req); + let uuid_string = Uuid::new_v4().to_string(); + + debug!("Queueing deposit request"); + queue_request(transaction_request, uuid_string).await +} + +/// function to queue the transfer requests +async fn queue_transfer_request( + transfer_req: NF3TransferRequest, +) -> Result { + let transaction_request = TransactionRequest::Transfer(transfer_req); + let uuid_string = Uuid::new_v4().to_string(); + + queue_request(transaction_request, uuid_string).await +} + +/// function to queue the withdraw requests +async fn queue_withdraw_request( + withdraw_req: NF3WithdrawRequest, +) -> Result { + let transaction_request = TransactionRequest::Withdraw(withdraw_req); + let uuid_string = Uuid::new_v4().to_string(); + + queue_request(transaction_request, uuid_string).await +} + +/// This function queues all types of transaction request +async fn queue_request( + transaction_request: TransactionRequest, + request_id: String, +) -> Result { + let settings = get_settings(); + let max_queue_size: usize = settings + .nightfall_client + .max_queue_size + .unwrap_or(1000) + .try_into() + .map_err(|_| { + warp::reject::custom(crate::domain::error::ClientRejection::DatabaseError) + })?; + + // check if the id is a valid uuid + if Uuid::parse_str(&request_id).is_err() { + return Err(warp::reject::custom( + crate::domain::error::ClientRejection::InvalidRequestId, + )); + }; + + // add the request to the queue + debug!("Adding request to queue"); + let mut q = get_queue().await.write().await; + // check if the queue is full + if q.len() >= max_queue_size { + return Ok(reply::with_header( + reply::with_status( + json(&"Queue is full".to_string()), + StatusCode::SERVICE_UNAVAILABLE, + ), + "X-Request-ID", + request_id, + )); + } + debug!("got lock on queue"); + q.push_back(QueuedRequest { + transaction_request, + uuid: request_id.clone(), + }); + drop(q); // drop the lock so other processes can access the queue + debug!("Added request to queue"); + // record the request as queued + let db = get_db_connection().await; + if db + .store_request(&request_id, RequestStatus::Queued) + .await + .is_none() + { + return Err(warp::reject::custom( + crate::domain::error::ClientRejection::DatabaseError, + )); + } + debug!("Stored request status in database"); + + // return a 202 Accepted response with the request ID + Ok(reply::with_header( + reply::with_status(json(&"Request queued".to_string()), StatusCode::ACCEPTED), + "X-Request-ID", + request_id, + )) +} + +/// This function wraps the various transaction handlers, so that the queue can call the correct handler +/// based on the request type. +pub async fn handle_request( + request: TransactionRequest, + request_id: &str, +) -> Result +where + P: Proof, + E: ProvingEngine

, + N: NightfallContract, +{ + match request { + TransactionRequest::Deposit(deposit_req) => { + handle_deposit::(deposit_req, request_id).await + } + TransactionRequest::Transfer(transfer_req) => { + transfer::handle_transfer::(transfer_req, request_id).await + } + TransactionRequest::Withdraw(withdraw_req) => { + withdraw::handle_withdraw::(withdraw_req, request_id).await + } + } +} diff --git a/nightfall_client/src/drivers/rest/client_nf_3/transfer.rs b/nightfall_client/src/drivers/rest/client_nf_3/transfer.rs new file mode 100644 index 00000000..42fa47d2 --- /dev/null +++ b/nightfall_client/src/drivers/rest/client_nf_3/transfer.rs @@ -0,0 +1,482 @@ +use super::handle_client_operation; +use crate::{ + domain::{ + entities::{Operation, OperationType, Transport}, + error::TransactionHandlerError, + notifications::NotificationPayload, + }, + get_zkp_keys, + initialisation::get_db_connection, + ports::{ + contracts::NightfallContract, + db::{CommitmentDB, CommitmentEntryDB}, + }, + services::commitment_selection::find_usable_commitments, +}; +use ark_bn254::Fr as Fr254; +use ark_ec::twisted_edwards::Affine; +use ark_ff::Zero; +use configuration::addresses::get_addresses; +use jf_primitives::poseidon::{FieldHasher, Poseidon}; +use lib::{ + client_models::NF3TransferRequest, + commitments::{Commitment, Nullifiable}, + contract_conversions::FrBn254, + get_fee_token_id, + hex_conversion::HexConvertible, + nf_client_proof::{Proof, ProvingEngine}, + nf_token_id::to_nf_token_id_from_str, + plonk_prover::circuits::DOMAIN_SHARED_SALT, + serialization::ark_de_hex, + shared_entities::{Preimage, Salt}, +}; +use tracing::{debug, error, info, warn}; +use nf_curves::ed_on_bn254::{BJJTEAffine as JubJub, BabyJubjub, Fr as BJJScalar}; +use nightfall_bindings::artifacts::Nightfall; +use serde::Deserialize; +use ark_std::UniformRand; +use tracing::{debug, error, info, warn}; + +#[derive(Deserialize)] +struct JubJubPubKey(#[serde(deserialize_with = "ark_de_hex")] JubJub); + +pub(crate) async fn handle_transfer( + transfer_req: NF3TransferRequest, + id: &str, +) -> Result +where + P: Proof, + E: ProvingEngine

, + N: NightfallContract, +{ + debug!("Handling transfer request: {transfer_req:?}"); + let NF3TransferRequest { + erc_address, + token_id, + recipient_data, + fee, + .. + } = transfer_req; + + // Convert the request into the relevant types. + let nf_token_id = + to_nf_token_id_from_str(erc_address.as_str(), token_id.as_str()).map_err(|e| { + error!( + "{id} Error when retrieving the Nightfall token id from the erc address and token ID {e}" + ); + TransactionHandlerError::CustomError(e.to_string()) + })?; + let keys = get_zkp_keys().lock().expect("Poisoned Mutex lock").clone(); + + let first_value = recipient_data.values.first().ok_or_else(|| { + error!("{id} No value provided in recipient data"); + TransactionHandlerError::CustomError("No value provided in recipient data".into()) + })?; + let value = Fr254::from_hex_string(first_value.as_str()).map_err(|e| { + error!("{id} Error when reading value: {e}"); + TransactionHandlerError::CustomError(e.to_string()) + })?; + + let fee: Fr254 = Fr254::from_hex_string(fee.as_str()).map_err(|e| { + error!("{id} Error when reading fee: {e}"); + TransactionHandlerError::CustomError(e.to_string()) + })?; + + let first_key = recipient_data + .recipient_compressed_zkp_public_keys + .first() + .ok_or_else(|| { + error!("{id} No recipient public key provided"); + TransactionHandlerError::CustomError("missing recipient public key".into()) + })?; + + // Create a JSON string that represents the tuple struct content + let json_wrapped = format!("\"{first_key}\""); + + // Note: ark_de_hex deserialization additionally ensures the point is on-curve and in correct subgroup + // Unit tests verify this validation behavior remains consistent + let deserialized_public_key: JubJubPubKey = + serde_json::from_str(&json_wrapped).map_err(|e| { + error!("{id} Could not deserialize recipient public key: {e}"); + TransactionHandlerError::CustomError(format!( + "Could not deserialize recipient public key: {e}" + )) + })?; + + let recipient_public_key = deserialized_public_key.0; + + // Check that the recipient public key is not the identity point + if recipient_public_key.is_zero() { + error!("{id} Recipient public key cannot be the identity point"); + return Err(TransactionHandlerError::CustomError( + "Recipient public key cannot be the identity point".to_string(), + )); + } + + let ephemeral_private_key = { + // thread_rng() is already thread-local and cheap to create; no need to share via RwLock. + let mut rng = ark_std::rand::thread_rng(); + BJJScalar::rand(&mut rng) + }; + let shared_secret: Affine = (recipient_public_key * ephemeral_private_key).into(); + + // add the id to the request database + + // Select the commitments to be spent. + let spend_commitments; + { + let db = get_db_connection().await; + let fee_token_id = get_fee_token_id(); + let spend_value_commitments = find_usable_commitments(nf_token_id, value,db) + .await.map_err(|e|{ + error!("{id} Could not find enough usable value commitments to complete this transfer, suggest depositing more tokens: {e}"); + TransactionHandlerError::CustomError(e.to_string())})?; + let spend_fee_commitments = if fee.is_zero() { + [Preimage::default(), Preimage::default()] + } else { + match find_usable_commitments(fee_token_id, fee, db).await { + Ok(commitments) => commitments, + Err(e) => { + debug!("{id} Could not find enough usable fee commitments, suggest depositing more fee: {e}"); + // rollback the value commitments to unspent if fails to find fee commitments + let value_commitment_ids = spend_value_commitments + .iter() + .filter_map(|c| match c.hash() { + Ok(h) => Some(h), + Err(e) => { + error!("{id} Failed to hash commitment during rollback, commitment may be orphaned: {e}"); + None + } + }) + .collect::>(); + + for commitment_id in &value_commitment_ids { + if let Some(existing) = db.get_commitment(commitment_id).await { + let _ = db + .mark_commitments_unspent( + &[*commitment_id], + existing.layer_1_transaction_hash, + existing.layer_2_block_number, + ) + .await; + } + } + return Err(TransactionHandlerError::CustomError(e.to_string())); + } + } + }; + spend_commitments = [ + spend_value_commitments[0], + spend_value_commitments[1], + spend_fee_commitments[0], + spend_fee_commitments[1], + ]; + } + + // Work out how much change is needed. + let total_token_value = spend_commitments[..2] + .iter() + .map(|c| c.get_value()) + .sum::(); + + let token_change = total_token_value - value; + let total_fee_value = spend_commitments[2..] + .iter() + .map(|c| c.get_value()) + .sum::(); + let fee_change = total_fee_value - fee; + + let poseidon = Poseidon::::new(); + // Derive a shared salt from the shared secret using domain-separated Poseidon hash. + let shared_salt_hash = poseidon + .hash(&[shared_secret.x, shared_secret.y, DOMAIN_SHARED_SALT]) + .map_err(|e| { + error!("{id} Failed to derive shared salt with Poseidon: {e}"); + TransactionHandlerError::CustomError(e.to_string()) + })?; + let shared_salt = Salt::Transfer(shared_salt_hash); + + // transferred value commitment, salt is derived from the shared secret + let new_commitment_one = Preimage::new( + value, + nf_token_id, + spend_commitments[0].get_nf_slot_id(), + recipient_public_key, + shared_salt, + ); + + let new_commitment_two = if !token_change.is_zero() { + Preimage::new( + token_change, + nf_token_id, + spend_commitments[0].get_nf_slot_id(), + keys.zkp_public_key, + Salt::new_transfer_salt(), + ) + } else { + Preimage::default() + }; + + let nightfall_address = FrBn254::from(get_addresses().nightfall()).0; + let contract_nf_address = Affine::::new_unchecked(Fr254::zero(), nightfall_address); + + let fee_token_id = get_fee_token_id(); + // if fee is zero, then no fee commitment is needed + let new_commitment_three = if !fee.is_zero() { + Preimage::new( + fee, + fee_token_id, + fee_token_id, + contract_nf_address, + Salt::new_transfer_salt(), + ) + } else { + Preimage::default() + }; + + let new_commitment_four = if !fee_change.is_zero() { + Preimage::new( + fee_change, + fee_token_id, + fee_token_id, + keys.zkp_public_key, + Salt::new_transfer_salt(), + ) + } else { + Preimage::default() + }; + + let new_commitments = [ + new_commitment_one, + new_commitment_two, + new_commitment_three, + new_commitment_four, + ]; + + debug!( + "{id} New commitment hashes: {:?}", + new_commitments + .iter() + .filter_map(|c| match c.hash() { + Ok(h) => Some(h.to_hex_string()), + Err(e) => { + warn!("{id} Failed to hash commitment for debug output: {e}"); + None + } + }) + .collect::>() + ); + + let secret_preimages = [ + spend_commitments[0].get_secret_preimage(), + spend_commitments[1].get_secret_preimage(), + spend_commitments[2].get_secret_preimage(), + spend_commitments[3].get_secret_preimage(), + ]; + let op = Operation { + transport: Transport::OffChain, + operation_type: OperationType::Transfer, + }; + match handle_client_operation::( + op, + spend_commitments, + new_commitments, + ephemeral_private_key, + Fr254::zero(), + secret_preimages, + id, + ) + .await + { + Ok(res) => Ok(res), + Err(e) => { + // rollback to UNSPENT status if handle_client_operation fails + let db = get_db_connection().await; + + // Rollback the spend commitments to unspent + let commitment_ids = spend_commitments + .iter() + .filter_map(|c| match c.hash() { + Ok(h) => Some(h), + Err(e) => { + error!("{id} Failed to hash commitment during rollback, commitment may be orphaned: {e}"); + None + } + }) + .collect::>(); + + info!( + "{id} Rolling back {} spend commitments", + commitment_ids.len() + ); + + for commitment_id in &commitment_ids { + if let Some(existing) = db.get_commitment(commitment_id).await { + let _ = db + .mark_commitments_unspent( + &[*commitment_id], + existing.layer_1_transaction_hash, + existing.layer_2_block_number, + ) + .await; + } + } + // Delete new commitments + let new_commitment_ids = new_commitments + .iter() + .filter_map(|c| match c.hash() { + Ok(h) => Some(h), + Err(e) => { + error!("{id} Failed to hash commitment during rollback, commitment may be orphaned: {e}"); + None + } + }) + .collect::>(); + + info!("{id} Deleting {} new commitments", new_commitment_ids.len()); + let _ = db.delete_commitments(new_commitment_ids).await; + + Err(TransactionHandlerError::CustomError(e.to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ark_ff::One; + use ark_serialize::{CanonicalSerialize, Compress}; + use ark_std::Zero; + use lib::{ + client_models::NF3RecipientData, + plonk_prover::plonk_proof::{PlonkProof, PlonkProvingEngine}, + }; + use nf_curves::ed_on_bn254::BabyJubjub; + use nf_curves::ed_on_bn254::Fq; + + /// Tests that transfer API rejects invalid recipient public keys + #[tokio::test] + async fn test_transfer_api_rejects_invalid_recipient_keys() { + // Invalid compressed point (not on the curve) should fail early in handle_transfer + let invalid_transfer_req = NF3TransferRequest { + erc_address: "0x1234567890123456789012345678901234567890".to_string(), + token_id: "0x00".to_string(), + recipient_data: NF3RecipientData { + values: vec!["0x04".to_string()], + recipient_compressed_zkp_public_keys: vec![ + "0x000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000" + .to_string(), + ], + }, + fee: "0x00".to_string(), + }; + + let result = handle_transfer::( + invalid_transfer_req, + "test-id-1", + ) + .await; + + // This should fail at the recipient key validation stage, demonstrating the API validates keys + assert!( + result.is_err(), + "Transfer API should reject invalid recipient public key" + ); + if let Err(TransactionHandlerError::CustomError(msg)) = result { + assert!( + msg.contains("Could not deserialize recipient public key: the input buffer contained invalid data"), + "Error should indicate recipient public key deserialization failure, got: {msg}" + ); + } else { + panic!("Expected TransactionHandlerError::CustomError with recipient public key deserialization failure"); + } + } + + #[tokio::test] + async fn test_transfer_api_rejects_identity_recipient_keys() { + // Identity point should fail early in handle_transfer + let identity_point = Affine::::zero(); + let mut compressed_bytes = Vec::new(); + identity_point + .serialize_with_mode(&mut compressed_bytes, Compress::Yes) + .unwrap(); + compressed_bytes.reverse(); // Convert to big-endian to match ark_se_hex format + let identity_point_hex = format!("0x{}", hex::encode(compressed_bytes)); + + let identity_point_transfer_req = NF3TransferRequest { + erc_address: "0x1234567890123456789012345678901234567890".to_string(), + token_id: "0x00".to_string(), + recipient_data: NF3RecipientData { + values: vec!["0x04".to_string()], + recipient_compressed_zkp_public_keys: vec![identity_point_hex], + }, + fee: "0x00".to_string(), + }; + + let result = handle_transfer::( + identity_point_transfer_req, + "test-id-2", + ) + .await; + + assert!( + result.is_err(), + "Transfer API should reject recipient public key if it is the identity" + ); + if let Err(TransactionHandlerError::CustomError(msg)) = result { + assert!( + msg.contains("Recipient public key cannot be the identity point"), + "Error should indicate recipient public key cannot be the identity point, got: {msg}" + ); + } else { + panic!("Expected TransactionHandlerError::CustomError with recipient public key cannot be the identity point"); + } + } + + #[tokio::test] + async fn test_transfer_api_rejects_low_order_recipient_keys() { + // A point that is low order but on the curve should fail early in handle_transfer + // We use point (0, -1) which is order 2 on BabyJubJub + let zero_x = Fq::zero(); + let neg_one_y = -Fq::one(); + + let low_order_point = Affine::::new_unchecked(zero_x, neg_one_y); + + let mut compressed_bytes = Vec::new(); + low_order_point + .serialize_with_mode(&mut compressed_bytes, Compress::Yes) + .unwrap(); + compressed_bytes.reverse(); // Convert to big-endian to match ark_se_hex format + let low_order_hex = format!("0x{}", hex::encode(compressed_bytes)); + + let low_order_transfer_req = NF3TransferRequest { + erc_address: "0x1234567890123456789012345678901234567890".to_string(), + token_id: "0x00".to_string(), + recipient_data: NF3RecipientData { + values: vec!["0x04".to_string()], + recipient_compressed_zkp_public_keys: vec![low_order_hex], + }, + fee: "0x00".to_string(), + }; + + let result = handle_transfer::( + low_order_transfer_req, + "test-id-3", + ) + .await; + + // This should fail at the explicit .check() stage since zero point is low-order + assert!( + result.is_err(), + "Transfer API should reject low-order recipient public key" + ); + if let Err(TransactionHandlerError::CustomError(msg)) = result { + assert!( + msg.contains("Could not deserialize recipient public key: the input buffer contained invalid data"), + "Error should indicate recipient public key deserialization failure, got: {msg}" + ); + } else { + panic!("Expected TransactionHandlerError::CustomError with recipient public key deserialization failure"); + } + } +} diff --git a/nightfall_client/src/drivers/rest/client_nf_3/withdraw.rs b/nightfall_client/src/drivers/rest/client_nf_3/withdraw.rs new file mode 100644 index 00000000..2e759ee9 --- /dev/null +++ b/nightfall_client/src/drivers/rest/client_nf_3/withdraw.rs @@ -0,0 +1,321 @@ +use super::{handle_client_operation, WithdrawResponse}; +use crate::{ + domain::{ + entities::{Operation, OperationType, Transport}, + error::TransactionHandlerError, + notifications::NotificationPayload, + }, + get_zkp_keys, + initialisation::get_db_connection, + ports::{ + contracts::NightfallContract, + db::{CommitmentDB, CommitmentEntryDB, RequestDB}, + }, + services::commitment_selection::find_usable_commitments, +}; +use ark_bn254::Fr as Fr254; +use ark_ec::twisted_edwards::Affine; +use ark_ff::Zero; +use configuration::addresses::get_addresses; +use lib::{ + client_models::{DeEscrowDataReq, NF3WithdrawRequest}, + commitments::{Commitment, Nullifiable}, + contract_conversions::FrBn254, + get_fee_token_id, + hex_conversion::HexConvertible, + nf_client_proof::{Proof, ProvingEngine}, + nf_token_id::to_nf_token_id_from_str, + shared_entities::{Preimage, Salt}, +}; +use tracing::{debug, error, info}; +use nf_curves::ed_on_bn254::{BabyJubjub, Fr as BJJScalar}; + +pub(crate) async fn handle_withdraw( + withdraw_req: NF3WithdrawRequest, + id: &str, +) -> Result +where + P: Proof, + E: ProvingEngine

, + N: NightfallContract, +{ + let NF3WithdrawRequest { + erc_address, + token_id, + value, + recipient_address, + fee, + .. + } = withdraw_req; + + // add the id to the request database + + // Convert the request into the relevant types. + let nf_token_id = + to_nf_token_id_from_str(erc_address.as_str(), token_id.as_str()).map_err(|e| { + error!( + "{id} Error when retrieving the Nightfall token id from the erc address and token ID {e}"); + TransactionHandlerError::CustomError(e.to_string()) + })?; + + let keys = get_zkp_keys().lock().expect("Poisoned Mutex lock").clone(); + + let value = Fr254::from_hex_string(value.as_str()).map_err(|e| { + error!("{id} Error when reading value: {e}"); + TransactionHandlerError::CustomError(e.to_string()) + })?; + + let fee: Fr254 = Fr254::from_hex_string(fee.as_str()).map_err(|e| { + error!("{id} Error when reading fee: {e}"); + TransactionHandlerError::CustomError(e.to_string()) + })?; + + let recipient_address: Fr254 = + Fr254::from_hex_string(recipient_address.as_str()).map_err(|e| { + error!("{id} Error when reading recipeint address: {e}"); + TransactionHandlerError::CustomError(e.to_string()) + })?; + // For now we just use the commitment selection algorithm to minimise change. + let spend_commitments; + let db = get_db_connection().await; + + { + let fee_token_id = get_fee_token_id(); + let spend_value_commitments = find_usable_commitments(nf_token_id, value,db) + .await.map_err(|e|{ + error!("{id} Could not find enough usable value commitments to complete this withdraw, suggest depositing more tokens: {e}"); + TransactionHandlerError::CustomError(e.to_string())})?; + let spend_fee_commitments = if fee.is_zero() { + [Preimage::default(), Preimage::default()] + } else { + match find_usable_commitments(fee_token_id, fee, db).await { + Ok(commitments) => commitments, + Err(e) => { + error!("{id} Could not find enough usable fee commitments to complete this withdraw, suggest depositing more fee: {e}"); + // rollback the value commitments to unspent if fails to find fee commitments + let value_commitment_ids = spend_value_commitments + .iter() + .filter_map(|c| match c.hash() { + Ok(h) => Some(h), + Err(e) => { + error!("{id} Failed to hash commitment during rollback, commitment may be orphaned: {e}"); + None + } + }) + .collect::>(); + for commitment_id in &value_commitment_ids { + if let Some(existing) = db.get_commitment(commitment_id).await { + let _ = db + .mark_commitments_unspent( + &[*commitment_id], + existing.layer_1_transaction_hash, + existing.layer_2_block_number, + ) + .await; + } + } + return Err(TransactionHandlerError::CustomError(e.to_string())); + } + } + }; + spend_commitments = [ + spend_value_commitments[0], + spend_value_commitments[1], + spend_fee_commitments[0], + spend_fee_commitments[1], + ]; + } + // Work out how much change is needed. + let total_token_value = spend_commitments[..2] + .iter() + .map(|c| c.get_value()) + .sum::(); + let token_change = total_token_value - value; + + let total_fee_value = spend_commitments[2..] + .iter() + .map(|c| c.get_value()) + .sum::(); + let fee_change = total_fee_value - fee; + + let nightfall_address = FrBn254::from(get_addresses().nightfall()).0; + let contract_nf_address = Affine::::new_unchecked(Fr254::zero(), nightfall_address); + + // The first commitment of the withdraw is 0, which will be calculated in the circuit + // here, we set new_commitment_one to have the withdraw value so we can check that value is conserved for transfer and withdraw in client_operation services. + // We set public_key of this preimage to the contract_nf_address, so that it won't be added in PendingCommitment later (as we only add preimages in PendingCommitment iff commitment.get_public_key() == zkp_public_key). + let new_commitment_one = Preimage::new( + value, + nf_token_id, + spend_commitments[0].get_nf_slot_id(), + contract_nf_address, + Salt::new_transfer_salt(), + ); + + let new_commitment_two = if !token_change.is_zero() { + Preimage::new( + token_change, + nf_token_id, + spend_commitments[0].get_nf_slot_id(), + keys.zkp_public_key, + Salt::new_transfer_salt(), + ) + } else { + Preimage::default() + }; + + let fee_token_id = get_fee_token_id(); + + let new_commitment_three = if !fee.is_zero() { + Preimage::new( + fee, + fee_token_id, + fee_token_id, + contract_nf_address, + Salt::new_transfer_salt(), + ) + } else { + Preimage::default() + }; + let new_commitment_four = if !fee_change.is_zero() { + Preimage::new( + fee_change, + fee_token_id, + fee_token_id, + keys.zkp_public_key, + Salt::new_transfer_salt(), + ) + } else { + Preimage::default() + }; + + let new_commitments = [ + new_commitment_one, + new_commitment_two, + new_commitment_three, + new_commitment_four, + ]; + + let secret_preimages = [ + spend_commitments[0].get_secret_preimage(), + spend_commitments[1].get_secret_preimage(), + spend_commitments[2].get_secret_preimage(), + spend_commitments[3].get_secret_preimage(), + ]; + let op = Operation { + transport: Transport::OffChain, + operation_type: OperationType::Withdraw, + }; + let withdraw_fund_salt = spend_commitments[0] + .nullifier_hash(&keys.nullifier_key) + .map_err(|e| { + error!("{id} Failed to compute nullifier hash: {e}"); + TransactionHandlerError::CustomError(format!("Failed to compute nullifier hash: {e}")) + })?; + match handle_client_operation::( + op, + spend_commitments, + new_commitments, + BJJScalar::zero(), + recipient_address, + secret_preimages, + id, + ) + .await + { + Ok(res) => { + let de_escrow_req = DeEscrowDataReq { + token_id: token_id.clone(), + erc_address: erc_address.clone(), + recipient_address: recipient_address.to_hex_string(), + value: value.to_hex_string(), + token_type: withdraw_req.token_type.clone(), + withdraw_fund_salt: withdraw_fund_salt.to_hex_string(), + }; + match serde_json::to_string(&de_escrow_req) { + Ok(child_args_json) => { + if db + .update_request_child_args(id, &child_args_json) + .await + .is_none() + { + error!("{id} Failed to store child_request_args in database"); + } else { + debug!("{id} Successfully stored child_request_args in request collection"); + } + } + Err(e) => { + error!("{id} Failed to serialize de_escrow_req: {e}"); + } + } + res + } + Err(e) => { + // Rollback to UNSPENT status if handle_client_operation fails + let db = get_db_connection().await; + + // Rollback spend commitments + let commitment_ids = spend_commitments + .iter() + .filter_map(|c| match c.hash() { + Ok(h) => Some(h), + Err(e) => { + error!("{id} Failed to hash commitment during rollback, commitment may be orphaned: {e}"); + None + } + }) + .collect::>(); + + info!( + "{id} Rolling back {} spend commitments to Unspent", + commitment_ids.len() + ); + for commitment_id in &commitment_ids { + if let Some(existing) = db.get_commitment(commitment_id).await { + let _ = db + .mark_commitments_unspent( + &[*commitment_id], + existing.layer_1_transaction_hash, + existing.layer_2_block_number, + ) + .await; + } + } + + // Delete new commitments + let new_commitment_ids = new_commitments + .iter() + .filter_map(|c| match c.hash() { + Ok(h) => Some(h), + Err(e) => { + error!("{id} Failed to hash commitment during rollback, commitment may be orphaned: {e}"); + None + } + }) + .collect::>(); + + info!("{id} Deleting {} new commitments", new_commitment_ids.len()); + let _ = db.delete_commitments(new_commitment_ids).await; + return Err(e); + } + }; + + // Build the response + let withdraw_response = WithdrawResponse { + success: true, + message: "Withdraw operation completed successfully".to_string(), + withdraw_fund_salt: withdraw_fund_salt.to_hex_string(), + }; + + let response = serde_json::to_string(&withdraw_response).map_err(|e| { + error!("{id} Error when serialising response: {e}"); + TransactionHandlerError::JsonConversionError(e) + })?; + let uuid = serde_json::to_string(&id).map_err(|e| { + error!("{id} Error when serialising request ID: {e}"); + TransactionHandlerError::JsonConversionError(e) + })?; + + // Return the response as JSON + Ok(NotificationPayload::TransactionEvent { response, uuid }) +} diff --git a/nightfall_client/src/drivers/rest/client_operation.rs b/nightfall_client/src/drivers/rest/client_operation.rs index 68f45ecd..46a11887 100644 --- a/nightfall_client/src/drivers/rest/client_operation.rs +++ b/nightfall_client/src/drivers/rest/client_operation.rs @@ -27,7 +27,7 @@ use lib::{ secret_hash::SecretHash, shared_entities::{ClientTransaction, Preimage}, }; -use log::{debug, error, info, warn}; +use tracing::{debug, error, info, warn}; use nf_curves::ed_on_bn254::Fr as BJJScalar; use nightfall_bindings::artifacts::ProposerManager; use reqwest::{Client, Error as ReqwestError}; diff --git a/nightfall_client/src/drivers/rest/commitment.rs b/nightfall_client/src/drivers/rest/commitment.rs index 680c2ffb..532c9dd2 100644 --- a/nightfall_client/src/drivers/rest/commitment.rs +++ b/nightfall_client/src/drivers/rest/commitment.rs @@ -1,3 +1,4 @@ +use serde::Deserialize; use warp::{hyper::StatusCode, path, reply, Filter, Reply}; use crate::driven::db::mongo::CommitmentEntry; @@ -7,6 +8,18 @@ use ark_bn254::Fr as Fr254; use ark_ff::{BigInteger, One, PrimeField, Zero}; use lib::{hex_conversion::HexConvertible, shared_entities::TokenType}; +#[derive(Debug, Deserialize)] +struct PaginationParams { + #[serde(default = "default_limit")] + limit: u64, + #[serde(default)] + offset: u64, +} + +fn default_limit() -> u64 { + 100 +} + /// GET request for a specific commitment by key pub fn get_commitment( ) -> impl Filter + Clone { @@ -29,18 +42,21 @@ pub async fn handle_get_commitment(key: String) -> Result impl Filter + Clone { path!("v1" / "commitments") .and(warp::get()) + .and(warp::query::()) .and_then(handle_get_all_commitments) } -pub async fn handle_get_all_commitments() -> Result { +pub async fn handle_get_all_commitments( + params: PaginationParams, +) -> Result { let commitment_db = get_db_connection().await; let res = commitment_db - .get_all_commitments() + .get_all_commitments(Some(params.limit), Some(params.offset)) .await .map_err(|_| warp::reject::custom(crate::domain::error::ClientRejection::DatabaseError))?; let values: Vec = res.into_iter().map(|c| c.1).collect(); diff --git a/nightfall_client/src/drivers/rest/metrics.rs b/nightfall_client/src/drivers/rest/metrics.rs new file mode 100644 index 00000000..0860589d --- /dev/null +++ b/nightfall_client/src/drivers/rest/metrics.rs @@ -0,0 +1,49 @@ +use lazy_static::lazy_static; +use prometheus::{ + default_registry, register_histogram_vec, register_int_counter_vec, register_int_gauge, + Encoder, HistogramVec, IntCounterVec, IntGauge, TextEncoder, +}; +use warp::Filter; + +lazy_static! { + pub static ref CLIENT_REQUESTS_TOTAL: IntCounterVec = register_int_counter_vec!( + "client_requests_total", + "Total number of HTTP requests handled by the client", + &["endpoint", "method", "status"] + ) + .expect("failed to register client_requests_total"); + pub static ref CLIENT_REQUEST_DURATION_SECONDS: HistogramVec = register_histogram_vec!( + "client_request_duration_seconds", + "Duration of HTTP requests handled by the client in seconds", + &["endpoint"] + ) + .expect("failed to register client_request_duration_seconds"); + pub static ref CLIENT_QUEUE_DEPTH: IntGauge = register_int_gauge!( + "client_queue_depth", + "Current depth of the client request queue" + ) + .expect("failed to register client_queue_depth"); +} + +pub fn metrics() -> impl Filter + Clone { + warp::path("metrics") + .and(warp::get()) + .map(|| { + // Ensure lazy_static metrics are initialised by touching them. + let _ = CLIENT_REQUESTS_TOTAL.desc(); + let _ = CLIENT_REQUEST_DURATION_SECONDS.desc(); + let _ = CLIENT_QUEUE_DEPTH.desc(); + + let encoder = TextEncoder::new(); + let metric_families = default_registry().gather(); + let mut buffer = Vec::new(); + encoder + .encode(&metric_families, &mut buffer) + .expect("failed to encode metrics"); + + warp::http::Response::builder() + .header("Content-Type", encoder.format_type()) + .body(buffer) + .unwrap() + }) +} diff --git a/nightfall_client/src/drivers/rest/mod.rs b/nightfall_client/src/drivers/rest/mod.rs index 8aea363f..50c223a7 100644 --- a/nightfall_client/src/drivers/rest/mod.rs +++ b/nightfall_client/src/drivers/rest/mod.rs @@ -4,7 +4,7 @@ use lib::{ health_check::health_route, nf_client_proof::Proof, validate_certificate::certification_validation_request, validate_keys::keys_validation_request, }; -use log::error; +use tracing::error; use proposers::get_proposers; use reqwest::StatusCode; use std::fmt::Debug; @@ -34,7 +34,9 @@ mod keys; pub mod proposers; mod request_status; mod synchronisation; +pub mod metrics; mod token_info; +pub mod readiness; pub mod withdraw; pub fn routes() -> impl Filter + Clone @@ -43,6 +45,7 @@ where N: NightfallContract, { health_route() + .or(readiness::readiness_check()) .or(deposit_request::

()) .or(transfer_request::

()) .or(withdraw_request::

()) @@ -61,6 +64,7 @@ where .or(get_queue_length()) .or(get_token_info::()) .or(get_l1_balance()) + .or(metrics::metrics()) .recover(handle_rejection) } diff --git a/nightfall_client/src/drivers/rest/readiness.rs b/nightfall_client/src/drivers/rest/readiness.rs new file mode 100644 index 00000000..47cdec3f --- /dev/null +++ b/nightfall_client/src/drivers/rest/readiness.rs @@ -0,0 +1,42 @@ +use crate::initialisation::get_db_connection; +use mongodb::bson::doc; +use serde_json::json; +use warp::{hyper::StatusCode, path, reply, Filter}; + +pub fn readiness_check( +) -> impl Filter + Clone { + path!("v1" / "ready") + .and(warp::get()) + .and_then(handle_readiness) +} + +async fn handle_readiness() -> Result { + let db = get_db_connection().await; + match db.database("nightfall").run_command(doc! { "ping": 1 }).await { + Ok(_) => { + let body = json!({ + "status": "ready", + "checks": { "database": "ok" } + }); + Ok(reply::with_status(reply::json(&body), StatusCode::OK)) + } + Err(e) => { + let body = json!({ + "status": "not_ready", + "checks": { "database": format!("error: {e}") } + }); + Ok(reply::with_status( + reply::json(&body), + StatusCode::SERVICE_UNAVAILABLE, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Note: integration tests requiring a real MongoDB connection are not included here. + // The readiness endpoint should be tested via integration tests with a running database. +} diff --git a/nightfall_client/src/drivers/rest/request_status.rs b/nightfall_client/src/drivers/rest/request_status.rs index 250aec31..939ab65a 100644 --- a/nightfall_client/src/drivers/rest/request_status.rs +++ b/nightfall_client/src/drivers/rest/request_status.rs @@ -1,5 +1,5 @@ use crate::{driven::queue::get_queue, initialisation::get_db_connection, ports::db::RequestDB}; -use log::debug; +use tracing::debug; use uuid::Uuid; use warp::{http::StatusCode, path, reply::Reply, Filter}; diff --git a/nightfall_client/src/drivers/rest/withdraw.rs b/nightfall_client/src/drivers/rest/withdraw.rs index b884db74..422010c5 100644 --- a/nightfall_client/src/drivers/rest/withdraw.rs +++ b/nightfall_client/src/drivers/rest/withdraw.rs @@ -1,7 +1,7 @@ use crate::ports::contracts::NightfallContract; use ::nightfall_bindings::artifacts::Nightfall; use lib::{client_models::DeEscrowDataReq, shared_entities::WithdrawData as NFWithdrawData}; -use log::{debug, error}; +use tracing::{debug, error}; use reqwest::StatusCode; use warp::{reject, Reply}; diff --git a/nightfall_client/src/main.rs b/nightfall_client/src/main.rs index 87901125..f303c5b4 100644 --- a/nightfall_client/src/main.rs +++ b/nightfall_client/src/main.rs @@ -6,7 +6,7 @@ use lib::{ shared_entities::Node, utils, }; -use log::{error, info}; +use tracing::{error, info}; use nightfall_bindings::artifacts::Nightfall; use nightfall_client::{ domain::entities::Request, diff --git a/nightfall_client/src/ports/db.rs b/nightfall_client/src/ports/db.rs index a3c3797e..a6f96b4e 100644 --- a/nightfall_client/src/ports/db.rs +++ b/nightfall_client/src/ports/db.rs @@ -26,7 +26,11 @@ where async fn store_commitment(&self, commitment_entry: V) -> Option<()>; async fn store_commitments(&self, commitment_entries: &[V], dup_key_check: bool) -> Option<()>; async fn delete_commitments(&self, commitment_ids: Vec) -> Option<()>; - async fn get_all_commitments(&self) -> Result, mongodb::error::Error>; + async fn get_all_commitments( + &self, + limit: Option, + offset: Option, + ) -> Result, mongodb::error::Error>; async fn get_commitments_by_token_type( &self, token_type: &str, diff --git a/nightfall_client/src/services/client_operation.rs b/nightfall_client/src/services/client_operation.rs index d65a9988..aaebbf38 100644 --- a/nightfall_client/src/services/client_operation.rs +++ b/nightfall_client/src/services/client_operation.rs @@ -24,7 +24,7 @@ use lib::{ ClientTransaction, CompressedSecrets, DepositSecret, Preimage, Salt, TokenType, }, }; -use log::{debug, error, info, warn}; +use tracing::{debug, error, info, warn}; use nf_curves::ed_on_bn254::{BabyJubjub as BabyJubJub, Fr as BJJScalar}; #[allow(clippy::too_many_arguments)] diff --git a/nightfall_client/src/services/commitment_selection.rs b/nightfall_client/src/services/commitment_selection.rs index a0eb3fec..aaeff29e 100644 --- a/nightfall_client/src/services/commitment_selection.rs +++ b/nightfall_client/src/services/commitment_selection.rs @@ -14,7 +14,7 @@ use lib::{ hex_conversion::HexConvertible, shared_entities::{Preimage, TokenType}, }; -use log::{debug, trace}; +use tracing::{debug, trace}; use mongodb::options::FindOneAndUpdateOptions; use mongodb::{Client, Database}; use nf_curves::ed_on_bn254::BJJTEAffine as JubJub; diff --git a/nightfall_deployer/Cargo.toml b/nightfall_deployer/Cargo.toml index 42820158..41f5809a 100644 --- a/nightfall_deployer/Cargo.toml +++ b/nightfall_deployer/Cargo.toml @@ -28,7 +28,7 @@ lib = { path = "../lib" } nightfall_test = { path = "../nightfall_test" } toml = "0.8.22" serde = "1.0.219" -log = "0.4.27" +tracing = "0.1" warp = "0.3.7" sha2 = "0.10" nightfall_client = { path = "../nightfall_client" } @@ -51,7 +51,7 @@ url = "2.5.4" [build-dependencies] configuration = { path = "../configuration" } -log = "0.4.27" +tracing = "0.1" [dev-dependencies] url = "2.5.4" diff --git a/nightfall_deployer/build.rs b/nightfall_deployer/build.rs index 8e56e924..a46124f4 100644 --- a/nightfall_deployer/build.rs +++ b/nightfall_deployer/build.rs @@ -1,4 +1,4 @@ -use log::info; +use tracing::info; use std::{os::unix::process::ExitStatusExt, process::Command}; fn main() { diff --git a/nightfall_deployer/src/deployment.rs b/nightfall_deployer/src/deployment.rs index 10cc290c..6459b225 100644 --- a/nightfall_deployer/src/deployment.rs +++ b/nightfall_deployer/src/deployment.rs @@ -7,7 +7,7 @@ use configuration::{ use jf_plonk::recursion::RecursiveProver; use lib::blockchain_client::BlockchainClientConnection; -use log::{debug, error, info}; +use tracing::{debug, error, info}; use nightfall_proposer::driven::rollup_prover::RollupProver; use serde_json::Value; use std::{ diff --git a/nightfall_deployer/src/main.rs b/nightfall_deployer/src/main.rs index a68e5a5d..171fb86c 100644 --- a/nightfall_deployer/src/main.rs +++ b/nightfall_deployer/src/main.rs @@ -1,5 +1,5 @@ use configuration::{logging::init_logging, settings::Settings}; -use log::{info, warn}; +use tracing::{info, warn}; use nightfall_deployer::deployment::deploy_contracts; #[tokio::main] diff --git a/nightfall_proposer/Cargo.toml b/nightfall_proposer/Cargo.toml index 9eec91f6..0d2bdacc 100644 --- a/nightfall_proposer/Cargo.toml +++ b/nightfall_proposer/Cargo.toml @@ -26,7 +26,6 @@ futures = "0.3.31" configuration = { path = "../configuration" } lib = { path = "../lib" } tokio = { version = "1.45.0", features = ["full"] } -log = "0.4.27" warp = "0.3.7" async-trait = "0.1.88" mongodb = "3.2.3" @@ -37,6 +36,7 @@ ark-std = { version = "0.4.0", default-features = false, features = [ hex = "0.4.3" either = "1.15.0" lazy_static = "1.5.0" +prometheus = "0.14" jf-primitives = { git = "https://git@github.com/EYBlockchain/nightfish_CE.git" } testcontainers = { version = "0.24.0", features = ["blocking"] } url = "2.5.4" @@ -50,6 +50,7 @@ jf-utils = { git = "https://git@github.com/EYBlockchain/nightfish_CE.git", featu nf-curves = { git = "https://git@github.com/EYBlockchain/nightfish_CE.git" } itertools = { version = "0.10.5", default-features = false } sha2 = { version = "0.10.9", default-features = false } +thiserror = "2" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } diff --git a/nightfall_proposer/src/domain/entities.rs b/nightfall_proposer/src/domain/entities.rs index 1e5ef240..93f45604 100644 --- a/nightfall_proposer/src/domain/entities.rs +++ b/nightfall_proposer/src/domain/entities.rs @@ -5,7 +5,7 @@ use lib::{ shared_entities::DepositData, shared_entities::{ClientTransaction, OnChainTransaction}, }; -use log::error; +use tracing::error; use serde::{Deserialize, Serialize}; use sha3::{Digest, Keccak256}; use std::fmt::Debug; diff --git a/nightfall_proposer/src/domain/error.rs b/nightfall_proposer/src/domain/error.rs index 7b9c6741..b839fe0e 100644 --- a/nightfall_proposer/src/domain/error.rs +++ b/nightfall_proposer/src/domain/error.rs @@ -1,7 +1,4 @@ -use std::{ - error::Error, - fmt::{Debug, Display, Formatter}, -}; +use std::fmt::{Debug, Display, Formatter}; /// errors for a merkle tree #[derive(Debug)] @@ -17,7 +14,7 @@ pub enum MerkleTreeError { InvalidProof, } -impl Error for MerkleTreeError {} +impl std::error::Error for MerkleTreeError {} impl Display for MerkleTreeError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -34,31 +31,22 @@ impl Display for MerkleTreeError { } } -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ProposerRejection { + #[error("Block data unavailable")] BlockDataUnavailable, + #[error("Client transaction failed")] ClientTransactionFailed, + #[error("Failed to rotate proposer")] FailedToRotateProposer, + #[error("Failed to add proposer")] FailedToAddProposer, + #[error("Failed to remove proposer")] FailedToRemoveProposer, + #[error("Failed to withdraw stake")] FailedToWithdrawStake, + #[error("Provider error")] ProviderError, } -impl std::fmt::Display for ProposerRejection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ProposerRejection::BlockDataUnavailable => write!(f, "Block data unavailable"), - ProposerRejection::ClientTransactionFailed => write!(f, "Client transaction failed"), - ProposerRejection::FailedToRotateProposer => write!(f, "Failed to rotate proposer"), - ProposerRejection::FailedToAddProposer => write!(f, "Failed to add proposer"), - ProposerRejection::FailedToRemoveProposer => write!(f, "Failed to remove proposer"), - ProposerRejection::FailedToWithdrawStake => write!(f, "Failed to withdraw stake"), - ProposerRejection::ProviderError => write!(f, "Provider error"), - } - } -} - -impl std::error::Error for ProposerRejection {} - impl warp::reject::Reject for ProposerRejection {} diff --git a/nightfall_proposer/src/driven/block_assembler.rs b/nightfall_proposer/src/driven/block_assembler.rs index cd44e218..2192aad5 100644 --- a/nightfall_proposer/src/driven/block_assembler.rs +++ b/nightfall_proposer/src/driven/block_assembler.rs @@ -16,7 +16,7 @@ use lib::{ shared_entities::OnChainTransaction, utils::get_block_size, }; -use log::{debug, error, warn}; +use tracing::{debug, error, warn}; use std::marker::PhantomData; use tokio::{ sync::RwLock, diff --git a/nightfall_proposer/src/driven/mock_prover.rs b/nightfall_proposer/src/driven/mock_prover.rs index 21071434..4bdb35a3 100644 --- a/nightfall_proposer/src/driven/mock_prover.rs +++ b/nightfall_proposer/src/driven/mock_prover.rs @@ -21,7 +21,7 @@ use lib::{ }; use std::collections::HashMap; -use log::debug; +use tracing::debug; use mongodb::{bson::doc, Client}; use super::rollup_prover::RollupProofError; diff --git a/nightfall_proposer/src/driven/nightfall_client_transaction.rs b/nightfall_proposer/src/driven/nightfall_client_transaction.rs index ac4d7e4a..25070f84 100644 --- a/nightfall_proposer/src/driven/nightfall_client_transaction.rs +++ b/nightfall_proposer/src/driven/nightfall_client_transaction.rs @@ -11,7 +11,7 @@ use lib::{ nf_client_proof::{Proof, ProvingEngine, PublicInputs}, shared_entities::{ClientTransaction, OnChainTransaction}, }; -use log::{error, info}; +use tracing::{error, info}; use std::{ error::Error, fmt::{Debug, Display, Formatter}, diff --git a/nightfall_proposer/src/driven/nightfall_contract.rs b/nightfall_proposer/src/driven/nightfall_contract.rs index b3c70369..c029dbd9 100644 --- a/nightfall_proposer/src/driven/nightfall_contract.rs +++ b/nightfall_proposer/src/driven/nightfall_contract.rs @@ -10,7 +10,7 @@ use lib::{ blockchain_client::BlockchainClientConnection, error::NightfallContractError, verify_contract::VerifiedContracts, }; -use log::info; +use tracing::info; use nightfall_bindings::artifacts::Nightfall; #[async_trait::async_trait] diff --git a/nightfall_proposer/src/driven/nightfall_event.rs b/nightfall_proposer/src/driven/nightfall_event.rs index 3d7e7ea0..06c2fe15 100644 --- a/nightfall_proposer/src/driven/nightfall_event.rs +++ b/nightfall_proposer/src/driven/nightfall_event.rs @@ -28,7 +28,7 @@ use lib::{ shared_entities::DepositData, shared_entities::OnChainTransaction, }; -use log::{debug, error, info, warn}; +use tracing::{debug, error, info, warn}; use mongodb::Client; use nightfall_bindings::artifacts::Nightfall; use serde::Serialize; diff --git a/nightfall_proposer/src/driven/rollup_prover.rs b/nightfall_proposer/src/driven/rollup_prover.rs index 7a6f9925..ad022c37 100644 --- a/nightfall_proposer/src/driven/rollup_prover.rs +++ b/nightfall_proposer/src/driven/rollup_prover.rs @@ -40,7 +40,7 @@ use jf_primitives::{ rescue::sponge::RescueCRHF, }; use jf_relation::{errors::CircuitError, PlonkCircuit, Variable}; -use log::{debug, warn}; +use tracing::{debug, warn}; use mongodb::{bson::doc, Client}; use lib::{ diff --git a/nightfall_proposer/src/drivers/blockchain/block_assembly.rs b/nightfall_proposer/src/drivers/blockchain/block_assembly.rs index a4edb290..78047816 100644 --- a/nightfall_proposer/src/drivers/blockchain/block_assembly.rs +++ b/nightfall_proposer/src/drivers/blockchain/block_assembly.rs @@ -20,7 +20,7 @@ use lib::{ nf_client_proof::Proof, verify_contract::VerifiedContracts, }; -use log::{debug, error, info, warn}; +use tracing::{debug, error, info, warn}; use nightfall_bindings::artifacts::RoundRobin; use std::{ error::Error, diff --git a/nightfall_proposer/src/drivers/blockchain/event_listener_manager.rs b/nightfall_proposer/src/drivers/blockchain/event_listener_manager.rs index e73f15dd..fdc5d070 100644 --- a/nightfall_proposer/src/drivers/blockchain/event_listener_manager.rs +++ b/nightfall_proposer/src/drivers/blockchain/event_listener_manager.rs @@ -2,7 +2,7 @@ use crate::drivers::blockchain::nightfall_event_listener::start_event_listener; use crate::ports::contracts::NightfallContract; use configuration::settings::get_settings; use lib::nf_client_proof::{Proof, ProvingEngine}; -use log::{info, warn}; +use tracing::{info, warn}; use tokio::{ sync::{OnceCell, RwLock}, task::JoinHandle, diff --git a/nightfall_proposer/src/drivers/blockchain/nightfall_event_listener.rs b/nightfall_proposer/src/drivers/blockchain/nightfall_event_listener.rs index b95f234c..3f6ee396 100644 --- a/nightfall_proposer/src/drivers/blockchain/nightfall_event_listener.rs +++ b/nightfall_proposer/src/drivers/blockchain/nightfall_event_listener.rs @@ -22,7 +22,7 @@ use lib::{ nf_client_proof::{Proof, ProvingEngine}, shared_entities::{SynchronisationPhase::Desynchronized, SynchronisationStatus}, }; -use log::{debug, warn}; +use tracing::{debug, warn}; use mongodb::Client as MongoClient; use nightfall_bindings::artifacts::Nightfall; use std::time::Duration; diff --git a/nightfall_proposer/src/drivers/rest/block_assembly.rs b/nightfall_proposer/src/drivers/rest/block_assembly.rs index 3bfc51bd..364993e1 100644 --- a/nightfall_proposer/src/drivers/rest/block_assembly.rs +++ b/nightfall_proposer/src/drivers/rest/block_assembly.rs @@ -1,4 +1,4 @@ -use log::debug; +use tracing::debug; use warp::{path, Filter}; diff --git a/nightfall_proposer/src/drivers/rest/client_transactions.rs b/nightfall_proposer/src/drivers/rest/client_transactions.rs index 6ab4e8bc..f752d196 100644 --- a/nightfall_proposer/src/drivers/rest/client_transactions.rs +++ b/nightfall_proposer/src/drivers/rest/client_transactions.rs @@ -4,7 +4,7 @@ use lib::{ nf_client_proof::{Proof, ProvingEngine}, shared_entities::ClientTransaction, }; -use log::{error, info}; +use tracing::{error, info}; use warp::{hyper::StatusCode, path, Filter}; diff --git a/nightfall_proposer/src/drivers/rest/metrics.rs b/nightfall_proposer/src/drivers/rest/metrics.rs new file mode 100644 index 00000000..87668eee --- /dev/null +++ b/nightfall_proposer/src/drivers/rest/metrics.rs @@ -0,0 +1,54 @@ +use lazy_static::lazy_static; +use prometheus::{ + default_registry, register_histogram, register_int_counter, register_int_counter_vec, + register_int_gauge, Encoder, Histogram, IntCounter, IntCounterVec, IntGauge, TextEncoder, +}; +use warp::Filter; + +lazy_static! { + pub static ref PROPOSER_REQUESTS_TOTAL: IntCounterVec = register_int_counter_vec!( + "proposer_requests_total", + "Total number of HTTP requests handled by the proposer", + &["endpoint", "method", "status"] + ) + .expect("failed to register proposer_requests_total"); + pub static ref PROPOSER_BLOCKS_PROPOSED_TOTAL: IntCounter = register_int_counter!( + "proposer_blocks_proposed_total", + "Total number of blocks proposed" + ) + .expect("failed to register proposer_blocks_proposed_total"); + pub static ref PROPOSER_BLOCK_PROVING_DURATION_SECONDS: Histogram = register_histogram!( + "proposer_block_proving_duration_seconds", + "Duration of block proving in seconds" + ) + .expect("failed to register proposer_block_proving_duration_seconds"); + pub static ref PROPOSER_MEMPOOL_SIZE: IntGauge = register_int_gauge!( + "proposer_mempool_size", + "Current number of transactions in the proposer mempool" + ) + .expect("failed to register proposer_mempool_size"); +} + +pub fn metrics() -> impl Filter + Clone { + warp::path("metrics") + .and(warp::get()) + .map(|| { + // Ensure lazy_static metrics are initialised by touching them. + let _ = PROPOSER_REQUESTS_TOTAL.desc(); + let _ = PROPOSER_BLOCKS_PROPOSED_TOTAL.desc(); + let _ = PROPOSER_BLOCK_PROVING_DURATION_SECONDS.desc(); + let _ = PROPOSER_MEMPOOL_SIZE.desc(); + + let encoder = TextEncoder::new(); + let metric_families = default_registry().gather(); + let mut buffer = Vec::new(); + encoder + .encode(&metric_families, &mut buffer) + .expect("failed to encode metrics"); + + warp::http::Response::builder() + .header("Content-Type", encoder.format_type()) + .body(buffer) + .unwrap() + }) +} diff --git a/nightfall_proposer/src/drivers/rest/mod.rs b/nightfall_proposer/src/drivers/rest/mod.rs index 23dc9d39..5516d4a4 100644 --- a/nightfall_proposer/src/drivers/rest/mod.rs +++ b/nightfall_proposer/src/drivers/rest/mod.rs @@ -21,6 +21,8 @@ pub mod block_assembly; pub mod block_data; pub mod client_transactions; pub mod proposers; +pub mod metrics; +pub mod readiness; pub mod synchronisation; pub fn routes() -> impl Filter + Clone @@ -29,6 +31,7 @@ where E: ProvingEngine

+ Sync + Send + 'static, { health_route() + .or(readiness::readiness_check()) .or(client_transaction::()) .or(rotate_proposer()) .or(get_block_data()) @@ -40,6 +43,7 @@ where .or(synchronisation()) .or(pause_block_assembly()) .or(resume_block_assembly()) + .or(metrics::metrics()) .recover(handle_rejection) } diff --git a/nightfall_proposer/src/drivers/rest/proposers.rs b/nightfall_proposer/src/drivers/rest/proposers.rs index 4c28772f..e7de5030 100644 --- a/nightfall_proposer/src/drivers/rest/proposers.rs +++ b/nightfall_proposer/src/drivers/rest/proposers.rs @@ -5,7 +5,7 @@ use lib::{ blockchain_client::BlockchainClientConnection, error::ProposerError, verify_contract::VerifiedContracts, }; -use log::{info, warn}; +use tracing::{info, warn}; /// APIs for managing proposers use warp::{hyper::StatusCode, path, reply::Reply, Filter}; diff --git a/nightfall_proposer/src/drivers/rest/readiness.rs b/nightfall_proposer/src/drivers/rest/readiness.rs new file mode 100644 index 00000000..47cdec3f --- /dev/null +++ b/nightfall_proposer/src/drivers/rest/readiness.rs @@ -0,0 +1,42 @@ +use crate::initialisation::get_db_connection; +use mongodb::bson::doc; +use serde_json::json; +use warp::{hyper::StatusCode, path, reply, Filter}; + +pub fn readiness_check( +) -> impl Filter + Clone { + path!("v1" / "ready") + .and(warp::get()) + .and_then(handle_readiness) +} + +async fn handle_readiness() -> Result { + let db = get_db_connection().await; + match db.database("nightfall").run_command(doc! { "ping": 1 }).await { + Ok(_) => { + let body = json!({ + "status": "ready", + "checks": { "database": "ok" } + }); + Ok(reply::with_status(reply::json(&body), StatusCode::OK)) + } + Err(e) => { + let body = json!({ + "status": "not_ready", + "checks": { "database": format!("error: {e}") } + }); + Ok(reply::with_status( + reply::json(&body), + StatusCode::SERVICE_UNAVAILABLE, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Note: integration tests requiring a real MongoDB connection are not included here. + // The readiness endpoint should be tested via integration tests with a running database. +} diff --git a/nightfall_proposer/src/lib.rs b/nightfall_proposer/src/lib.rs index 711edf75..d20e7d9d 100644 --- a/nightfall_proposer/src/lib.rs +++ b/nightfall_proposer/src/lib.rs @@ -19,7 +19,7 @@ use lib::{ rollup_circuit_checks::{find_file_with_path, get_configuration_keys_path}, utils::{load_key_from_server, load_key_locally}, }; -use log::warn; +use tracing::warn; use std::{ collections::HashMap, sync::{Arc, OnceLock, RwLock}, diff --git a/nightfall_proposer/src/main.rs b/nightfall_proposer/src/main.rs index b01daa5d..59433c8b 100644 --- a/nightfall_proposer/src/main.rs +++ b/nightfall_proposer/src/main.rs @@ -1,6 +1,6 @@ use configuration::{logging::init_logging, settings::get_settings}; use lib::plonk_prover::plonk_proof::{PlonkProof, PlonkProvingEngine}; -use log::{error, info}; +use tracing::{error, info}; use nightfall_bindings::artifacts::Nightfall; use nightfall_proposer::drivers::blockchain::event_listener_manager::ensure_running; use nightfall_proposer::{ diff --git a/nightfall_proposer/src/ports/trees.rs b/nightfall_proposer/src/ports/trees.rs index 34878070..91c3e639 100644 --- a/nightfall_proposer/src/ports/trees.rs +++ b/nightfall_proposer/src/ports/trees.rs @@ -6,7 +6,7 @@ use bson::Document; use configuration::settings::get_settings; use jf_primitives::{poseidon::PoseidonParams, trees::MembershipProof}; use lib::merkle_trees::trees::{IndexedTree, MerkleTreeError, MutableTree}; -use log::debug; +use tracing::debug; use mongodb::Client; /// Trait defining the functionality of a commitment tree. diff --git a/nightfall_proposer/src/services/assemble_block.rs b/nightfall_proposer/src/services/assemble_block.rs index fd5650f8..88415da2 100644 --- a/nightfall_proposer/src/services/assemble_block.rs +++ b/nightfall_proposer/src/services/assemble_block.rs @@ -19,7 +19,7 @@ use lib::{ shared_entities::DepositData, utils::get_block_size, }; -use log::{info, warn}; +use tracing::{info, warn}; use std::cmp::Reverse; use tokio::time::Instant; diff --git a/nightfall_sync_test/Cargo.toml b/nightfall_sync_test/Cargo.toml index bb2c34b9..c907ae09 100644 --- a/nightfall_sync_test/Cargo.toml +++ b/nightfall_sync_test/Cargo.toml @@ -11,6 +11,6 @@ nightfall_test = { path = "../nightfall_test" } reqwest = { version = "0.12.15", features = ["json"] } tokio = { version = "1.45.0", features = ["full"] } configuration = { path = "../configuration" } -log = "0.4.27" +tracing = "0.1" nightfall_client = { path = "../nightfall_client" } nightfall_bindings = {path = "../nightfall_bindings"} diff --git a/nightfall_sync_test/src/main.rs b/nightfall_sync_test/src/main.rs index 99595b4e..106a3949 100644 --- a/nightfall_sync_test/src/main.rs +++ b/nightfall_sync_test/src/main.rs @@ -1,6 +1,6 @@ use configuration::{logging::init_logging, settings::get_settings}; use lib::models::CertificateReq; -use log::{debug, info}; +use tracing::{debug, info}; use nightfall_client::domain::entities::Proposer; use nightfall_test::test::validate_certificate_with_server; use reqwest::{StatusCode, Url}; diff --git a/nightfall_test/Cargo.toml b/nightfall_test/Cargo.toml index 42a3b240..f78fd0d6 100644 --- a/nightfall_test/Cargo.toml +++ b/nightfall_test/Cargo.toml @@ -30,7 +30,6 @@ arkworks-utils = "1.0.1" ark-bn254 = "0.4.0" alloy = { version = "1.0.23", features = ["full"] } hex = "0.4.3" -log = "0.4.27" sha2 = "0.10.9" ark-ec = { version = "0.4.2", features = ["parallel"] } nf-curves = { git = "https://git@github.com/EYBlockchain/nightfish_CE.git" } diff --git a/nightfall_test/src/main.rs b/nightfall_test/src/main.rs index 474ff62c..bf23378f 100644 --- a/nightfall_test/src/main.rs +++ b/nightfall_test/src/main.rs @@ -1,5 +1,5 @@ use configuration::{logging::init_logging, settings::Settings}; -use log::{error, info}; +use tracing::{error, info}; use nightfall_test::{ run_tests::run_tests, webhook::{poll_queue, run_webhook_server}, diff --git a/nightfall_test/src/run_tests.rs b/nightfall_test/src/run_tests.rs index d04dfa2b..bba0dc0b 100644 --- a/nightfall_test/src/run_tests.rs +++ b/nightfall_test/src/run_tests.rs @@ -24,7 +24,7 @@ use lib::{ hex_conversion::HexConvertible, initialisation::get_blockchain_client_connection, utils::get_block_size, }; -use log::{debug, info, warn}; +use tracing::{debug, info, warn}; use nightfall_client::drivers::rest::client_nf_3::WithdrawResponse; use serde_json::Value; use test::{ diff --git a/nightfall_test/src/test.rs b/nightfall_test/src/test.rs index de5c590a..2b725a60 100644 --- a/nightfall_test/src/test.rs +++ b/nightfall_test/src/test.rs @@ -39,7 +39,7 @@ use lib::{ secret_hash::SecretHash, shared_entities::{DepositSecret, Preimage, Salt}, }; -use log::{debug, info, warn}; +use tracing::{debug, info, warn}; use nf_curves::ed_on_bn254::{BabyJubjub as BabyJubJub, Fr as BJJScalar}; use nightfall_client::{ domain::{ diff --git a/nightfall_test/src/validate_certs.rs b/nightfall_test/src/validate_certs.rs index 2e18f631..a17d284a 100644 --- a/nightfall_test/src/validate_certs.rs +++ b/nightfall_test/src/validate_certs.rs @@ -1,6 +1,6 @@ use crate::test::validate_certificate_with_server; use lib::models::CertificateReq; -use log::info; +use tracing::info; use std::fs; use url::Url; diff --git a/nightfall_test/src/webhook.rs b/nightfall_test/src/webhook.rs index 79808337..022828cd 100644 --- a/nightfall_test/src/webhook.rs +++ b/nightfall_test/src/webhook.rs @@ -1,5 +1,5 @@ use configuration::settings::get_settings; -use log::{debug, warn}; +use tracing::{debug, warn}; use std::sync::Arc; use tokio::sync::Mutex; /// Set up a warp server to listen for webhooks from the Nightfall client. diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 00000000..eb234a1b --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,1194 @@ +openapi: 3.0.3 +info: + title: Nightfall 4 CE API + description: | + REST API specification for the Nightfall 4 Community Edition protocol. + The system consists of two services: a **Client** (default port 3000) and a **Proposer** (default port 3001). + The Client handles deposits, transfers, withdrawals, commitment queries, and key management. + The Proposer handles block assembly, proposer registration/rotation, and receives client transactions. + version: 1.0.0 + license: + name: See repository for license details + +servers: + - url: http://localhost:3000 + description: Nightfall Client + - url: http://localhost:3001 + description: Nightfall Proposer + +tags: + - name: Client - Transactions + description: Deposit, transfer, and withdraw operations (Client) + - name: Client - Commitments + description: Commitment queries (Client) + - name: Client - Balance + description: Balance queries (Client) + - name: Client - Keys + description: Key derivation (Client) + - name: Client - Proposers + description: Proposer list queries (Client) + - name: Client - Request Status + description: Transaction request status and queue (Client) + - name: Client - Token Info + description: Token information queries (Client) + - name: Proposer - Transactions + description: Client transaction submission (Proposer) + - name: Proposer - Management + description: Proposer registration, deregistration, rotation, and stake withdrawal (Proposer) + - name: Proposer - Block Assembly + description: Block assembly control (Proposer) + - name: Proposer - Block Data + description: Block data queries (Proposer) + - name: Shared + description: Endpoints available on both Client and Proposer + +paths: + # ────────────────────────────────────────────── + # Shared endpoints (available on both services) + # ────────────────────────────────────────────── + /v1/health: + get: + tags: [Shared] + summary: Health check + description: Returns "Healthy" if the service is running. + operationId: healthCheck + responses: + "200": + description: Service is healthy + content: + text/plain: + schema: + type: string + example: Healthy + + /v1/ready: + get: + tags: [Shared] + summary: Readiness probe + description: | + Returns readiness status including dependency checks (e.g. database connectivity). + Returns 200 when all checks pass, 503 when any check fails. + operationId: readinessProbe + responses: + "200": + description: Service is ready + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ready + checks: + type: object + properties: + database: + type: string + example: ok + "503": + description: Service is not ready + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: not_ready + checks: + type: object + properties: + database: + type: string + example: "error: connection refused" + + /metrics: + get: + tags: [Shared] + summary: Prometheus metrics + description: Returns Prometheus metrics in text exposition format. + operationId: prometheusMetrics + responses: + "200": + description: Prometheus metrics + content: + text/plain: + schema: + type: string + description: Prometheus exposition format metrics + + /v1/certification: + post: + tags: [Shared] + summary: Validate an X.509 certificate on-chain + description: | + Submits an X.509 certificate and its corresponding private key for on-chain validation. + The service signs its own Ethereum address with the private key and calls the X509 smart + contract to bind the certificate to the address. Returns the resulting certification status. + operationId: certificationValidation + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - certificate + - certificate_private_key + properties: + certificate: + type: string + format: binary + description: DER-encoded X.509 certificate file + certificate_private_key: + type: string + format: binary + description: DER-encoded RSA private key file corresponding to the certificate + responses: + "202": + description: Certificate validation processed + content: + application/json: + schema: + $ref: "#/components/schemas/CertificationResponse" + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + + /v1/keys_validation: + post: + tags: [Shared] + summary: Validate verification keys + description: | + Validates the integrity and consistency of zero-knowledge proof verification keys + used in the Nightfall protocol. Ensures that keys stored on the key server and + on-chain verification keys have been generated honestly by the deployer. + operationId: keysValidation + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/KeysValidationRequest" + responses: + "200": + description: Keys validation result + content: + application/json: + schema: + type: object + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + + /v1/synchronisation: + get: + tags: [Shared] + summary: Get synchronisation status + description: | + Returns the current synchronisation status of the service with the blockchain. + On the Client, returns a JSON synchronisation status object. + On the Proposer, returns a plain text string ("Synchronised" or "Not synchronised"). + operationId: getSynchronisation + responses: + "200": + description: Synchronisation status + content: + application/json: + schema: + type: object + description: Synchronisation status (Client) + text/plain: + schema: + type: string + enum: [Synchronised, Not synchronised] + description: Synchronisation status (Proposer) + "503": + description: Synchronisation service unavailable (Client only) + content: + text/plain: + schema: + type: string + example: Synchronisation service unavailable + + # ────────────────────────────────────────────── + # Client endpoints + # ────────────────────────────────────────────── + /v1/deposit: + post: + tags: [Client - Transactions] + summary: Submit a deposit request + description: | + Queues a deposit request to move tokens from Layer 1 into the Nightfall Layer 2. + The request is queued and processed asynchronously. Returns 202 Accepted with an + X-Request-ID header that can be used to track the request status. + + The user pays `2 * fee + deposit_fee` to the Nightfall contract: one fee for the + value deposit commitment, one for the fee deposit commitment, plus the deposit_fee + which acts as a reserve for future proposer fees. + operationId: deposit + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NF3DepositRequest" + responses: + "202": + description: Deposit request queued successfully + headers: + X-Request-ID: + description: UUID to track the request status + schema: + type: string + format: uuid + content: + application/json: + schema: + type: string + example: Request queued + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + "503": + description: Transaction queue is full + content: + application/json: + schema: + type: string + example: Queue is full + + /v1/transfer: + post: + tags: [Client - Transactions] + summary: Submit a transfer request + description: | + Queues a transfer request to send tokens to another user within Nightfall Layer 2. + Nullifies one or two input commitments and creates two output commitments (one for + the recipient and one for change). The system automatically selects the most suitable + input commitments. + operationId: transfer + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NF3TransferRequest" + responses: + "202": + description: Transfer request queued successfully + headers: + X-Request-ID: + description: UUID to track the request status + schema: + type: string + format: uuid + content: + application/json: + schema: + type: string + example: Request queued + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + "503": + description: Transaction queue is full + content: + application/json: + schema: + type: string + example: Queue is full + + /v1/withdraw: + post: + tags: [Client - Transactions] + summary: Submit a withdraw request (Client) or withdraw proposer stake (Proposer) + description: | + **Client (port 3000):** Queues a withdraw request to move tokens from Layer 2 back + to Layer 1. Nullifies input commitments and de-escrows the corresponding funds. + + **Proposer (port 3001):** Withdraws the stake of a de-registered proposer (up to the + staked amount). Can only be called by the de-registered proposer. + operationId: withdraw + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/NF3WithdrawRequest" + - type: integer + format: int64 + description: Amount to withdraw (Proposer stake withdrawal) + example: 20 + responses: + "200": + description: Proposer stake withdrawal successful + "202": + description: Client withdraw request queued successfully + headers: + X-Request-ID: + description: UUID to track the request status + schema: + type: string + format: uuid + content: + application/json: + schema: + type: string + example: Request queued + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + "503": + description: Transaction queue is full (Client only) + content: + application/json: + schema: + type: string + example: Queue is full + + /v1/commitment/{key}: + get: + tags: [Client - Commitments] + summary: Get a specific commitment by its hash key + description: | + Returns the commitment entry from the database matching the given commitment hash. + The key should be a hex-encoded field element representing the commitment hash. + operationId: getCommitment + parameters: + - name: key + in: path + required: true + description: Hex-encoded commitment hash + schema: + $ref: "#/components/schemas/HexFieldElement" + responses: + "200": + description: Commitment found + content: + application/json: + schema: + $ref: "#/components/schemas/CommitmentEntry" + "400": + description: Invalid commitment key + content: + text/plain: + schema: + type: string + example: Invalid commitment key + "404": + description: Commitment not found + content: + text/plain: + schema: + type: string + example: Commitment not found + + /v1/commitments: + get: + tags: [Client - Commitments] + summary: Get all commitments + description: Returns all commitment entries stored in the client database. Use with care on large datasets. + operationId: getAllCommitments + responses: + "200": + description: List of all commitments + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CommitmentEntry" + "500": + $ref: "#/components/responses/InternalServerError" + + /v1/commitments/token_type/{token_type}: + get: + tags: [Client - Commitments] + summary: Get commitments by token type + description: Returns all commitment entries matching the given token type string. + operationId: getCommitmentsByTokenType + parameters: + - name: token_type + in: path + required: true + description: "Token type string (e.g., ERC20, ERC721, ERC1155, ERC3525, FeeToken)" + schema: + type: string + enum: [ERC20, ERC721, ERC1155, ERC3525, FeeToken] + responses: + "200": + description: Commitments matching the token type + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CommitmentEntry" + "500": + $ref: "#/components/responses/InternalServerError" + + /v1/commitments/max_transferable_amount/{token_type}/{nf_token_id}: + get: + tags: [Client - Commitments] + summary: Get maximum transferable amount for a token type + description: | + Returns the maximum transferable amount for the given token type and Nightfall token ID. + For fungible tokens (ERC20, ERC1155, ERC3525, FeeToken), this is the sum of the two + highest-value commitments. For ERC721, it is 1 if any commitment exists, otherwise 0. + The result is returned as a big-endian hex-encoded string. + operationId: getMaxTransferableAmount + parameters: + - name: token_type + in: path + required: true + description: "Token type string" + schema: + type: string + enum: [ERC20, ERC721, ERC1155, ERC3525, FeeToken] + - name: nf_token_id + in: path + required: true + description: Hex-encoded Nightfall token ID + schema: + $ref: "#/components/schemas/HexFieldElement" + responses: + "200": + description: Maximum transferable amount as a big-endian hex string + content: + text/plain: + schema: + type: string + pattern: "^[0-9a-fA-F]+$" + example: "0000000000000000000000000000000000000000000000000000000000000008" + "400": + description: Invalid token ID or token type + content: + text/plain: + schema: + type: string + "500": + $ref: "#/components/responses/InternalServerError" + + /v1/balance/{erc_address}/{token_id}: + get: + tags: [Client - Balance] + summary: Get Layer 2 token balance + description: | + Returns the Layer 2 balance for the given ERC contract address and token ID. + The balance is encoded as a big-endian hex string. For ERC20 tokens, use token_id "0x00". + For ERC721, returns "0x00" if the token exists in the wallet. + operationId: getBalance + parameters: + - name: erc_address + in: path + required: true + description: ERC contract address (hex string) + schema: + $ref: "#/components/schemas/HexString" + - name: token_id + in: path + required: true + description: Token ID (hex string, use "0x00" for ERC20) + schema: + $ref: "#/components/schemas/HexString" + responses: + "200": + description: Balance as a big-endian hex string + content: + text/plain: + schema: + type: string + pattern: "^[0-9a-fA-F]+$" + example: "0000000000000000000000000000000000000000000000000000000000000004" + "400": + description: Invalid token ID + content: + text/plain: + schema: + type: string + example: Invalid token id + "404": + description: No such token in the wallet + content: + text/plain: + schema: + type: string + example: No such token + + /v1/fee_balance: + get: + tags: [Client - Balance] + summary: Get Layer 2 fee token balance + description: Returns the balance of fee tokens in the user's Layer 2 wallet as a big-endian hex string. + operationId: getFeeBalance + responses: + "200": + description: Fee balance as a big-endian hex string + content: + text/plain: + schema: + type: string + pattern: "^[0-9a-fA-F]+$" + "404": + description: No fee token balance found + content: + text/plain: + schema: + type: string + example: No such token + + /v1/l1_balance: + get: + tags: [Client - Balance] + summary: Get Layer 1 wallet balance + description: Returns the on-chain (Layer 1) Ethereum balance of the client's wallet as a big-endian hex string. + operationId: getL1Balance + responses: + "200": + description: L1 balance as a hex string + content: + text/plain: + schema: + type: string + pattern: "^0x[0-9a-fA-F]+$" + "404": + description: Unable to retrieve balance + content: + text/plain: + schema: + type: string + example: No such token + + /v1/deriveKey: + post: + tags: [Client - Keys] + summary: Derive ZKP keys from a mnemonic + description: | + Derives a set of ZKP keys from the provided BIP-32 mnemonic and derivation path. + The derived key is stored as the active key for the client. If called without a body, + returns the current public key. + operationId: deriveKey + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/KeyRequest" + responses: + "200": + description: The ZKP public key (compressed, big-endian hex) + content: + application/json: + schema: + $ref: "#/components/schemas/ZKPPubKey" + "404": + description: Key derivation failed + + /v1/proposers: + get: + tags: [Client - Proposers] + summary: Get list of registered proposers + description: Returns a list of all proposers currently registered on-chain, including their URLs and stake information. + operationId: getProposers + responses: + "200": + description: List of proposers + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Proposer" + "503": + description: Failed to retrieve proposers + content: + text/plain: + schema: + type: string + example: Failed to get list of Proposers + + /v1/request/{uuid}: + get: + tags: [Client - Request Status] + summary: Get request status by UUID + description: | + Returns the status of a deposit, transfer, or withdraw request identified by its UUID + (the X-Request-ID value returned when the request was submitted). + operationId: getRequestStatus + parameters: + - name: uuid + in: path + required: true + description: The request UUID (X-Request-ID) + schema: + type: string + format: uuid + responses: + "200": + description: Request status + content: + application/json: + schema: + $ref: "#/components/schemas/RequestStatus" + "400": + description: Invalid request ID + content: + text/plain: + schema: + type: string + example: Invalid request id + "404": + description: Request not found + content: + text/plain: + schema: + type: string + example: No such request + + /v1/queue: + get: + tags: [Client - Request Status] + summary: Get transaction queue length + description: Returns the current number of pending requests in the transaction queue. + operationId: getQueueLength + responses: + "200": + description: Queue length + content: + application/json: + schema: + type: integer + minimum: 0 + example: 5 + + /v1/token/{nf_token_id}: + get: + tags: [Client - Token Info] + summary: Get token information by Nightfall token ID + description: | + Retrieves detailed information about a token using its Nightfall token ID. + Useful for looking up the Layer 1 contract address and token ID for a token + received via transfer. + operationId: getTokenInfo + parameters: + - name: nf_token_id + in: path + required: true + description: Nightfall token ID (hex string) + schema: + $ref: "#/components/schemas/HexFieldElement" + responses: + "200": + description: Token information + content: + application/json: + schema: + $ref: "#/components/schemas/TokenInfo" + "400": + description: Invalid token ID + "404": + description: Token not found + + # ────────────────────────────────────────────── + # Proposer endpoints + # ────────────────────────────────────────────── + /v1/transaction: + post: + tags: [Proposer - Transactions] + summary: Submit a client transaction to the proposer + description: | + Accepts a JSON-encoded ClientTransaction object from a client. This is the main + endpoint used by clients to submit their zero-knowledge proof transactions to a + proposer for inclusion in a Layer 2 block. + operationId: submitTransaction + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ClientTransaction" + responses: + "201": + description: Transaction accepted by the proposer + "400": + description: Client transaction failed validation + content: + text/plain: + schema: + type: string + example: Client transaction failed + + /v1/register: + post: + tags: [Proposer - Management] + summary: Register a new proposer + description: | + Registers a new proposer on-chain with the given URL. The proposer must send a stake + along with the registration transaction. The URL is where clients can reach the proposer. + operationId: registerProposer + requestBody: + required: true + content: + application/json: + schema: + type: string + description: The URL at which clients can reach the proposer + example: "http://proposer.example.com:3001" + responses: + "200": + description: Proposer registered successfully + "400": + description: Failed to add proposer + content: + text/plain: + schema: + type: string + example: Failed to add proposer + + /v1/deregister: + get: + tags: [Proposer - Management] + summary: Deregister a proposer + description: | + Removes (de-registers) the calling proposer from the on-chain registry. Only the + proposer itself can call this endpoint because the smart contract requires the + request to originate from the registered Ethereum address. If the proposer is the + current active proposer, an exit penalty is applied and a cooldown period begins. + operationId: deregisterProposer + responses: + "200": + description: Proposer deregistered successfully + "400": + description: Failed to remove proposer + content: + text/plain: + schema: + type: string + example: Failed to remove proposer + + /v1/rotate: + get: + tags: [Proposer - Management] + summary: Rotate the active proposer + description: | + Triggers proposer rotation if the current proposer has been active for more than + the allowed number of Layer 1 blocks (ROTATION_BLOCKS). Returns 423 Locked if + rotation is not permitted by the smart contract. + operationId: rotateProposer + responses: + "200": + description: Proposer rotated successfully + "423": + description: Proposer rotation not allowed + content: + text/plain: + schema: + type: string + example: Failed to rotate proposer + + /v1/pause: + get: + tags: [Proposer - Block Assembly] + summary: Pause block assembly + description: Pauses the proposer's block assembly process. No new blocks will be assembled until resumed. + operationId: pauseBlockAssembly + responses: + "200": + description: Block assembly paused + + /v1/resume: + get: + tags: [Proposer - Block Assembly] + summary: Resume block assembly + description: Resumes the proposer's block assembly process after it has been paused. + operationId: resumeBlockAssembly + responses: + "200": + description: Block assembly resumed + + /v1/blockdata: + get: + tags: [Proposer - Block Data] + summary: Get current Layer 2 block number + description: Returns the expected next Layer 2 block number as a JSON integer. + operationId: getBlockData + responses: + "200": + description: Current Layer 2 block number + content: + application/json: + schema: + type: integer + format: int64 + example: 42 + "503": + description: Block data unavailable + content: + text/plain: + schema: + type: string + example: Block data unavailable + +components: + schemas: + HexString: + type: string + pattern: "^0x[0-9a-fA-F]+$" + description: A hex-encoded string prefixed with 0x + example: "0x5FbDB2315678afecb367f032d93F642f64180aa3" + + HexFieldElement: + type: string + pattern: "^(0x)?[0-9a-fA-F]+$" + description: A hex-encoded BN254 field element, with or without 0x prefix + example: "18e7718c4db06feeae3a16c919e2a253a313d86bcd961a498079fbcfdb596f93" + + NF3DepositRequest: + type: object + required: + - ercAddress + - tokenId + - tokenType + - value + - fee + - deposit_fee + properties: + ercAddress: + $ref: "#/components/schemas/HexString" + description: Ethereum address of the ERC20/721/1155/3525 contract + tokenId: + $ref: "#/components/schemas/HexString" + description: "Token ID (use \"0x00\" for ERC20)" + tokenType: + type: string + description: "Token type: 0=ERC20, 1=ERC1155, 2=ERC721, 3=ERC3525" + enum: ["0", "1", "2", "3"] + value: + $ref: "#/components/schemas/HexString" + description: "Value to deposit (\"0x00\" for ERC721)" + fee: + $ref: "#/components/schemas/HexString" + description: Fee paid to the proposer (user pays 2x this amount) + deposit_fee: + $ref: "#/components/schemas/HexString" + description: Additional fee deposited as reserve for future transaction fees + example: + ercAddress: "0x5FbDB2315678afecb367f032d93F642f64180aa3" + tokenId: "0x00" + tokenType: "0" + value: "0x04" + fee: "0x02" + deposit_fee: "0x05" + + NF3TransferRequest: + type: object + required: + - ercAddress + - tokenId + - recipientData + - fee + properties: + ercAddress: + $ref: "#/components/schemas/HexString" + description: Ethereum address of the ERC contract + tokenId: + $ref: "#/components/schemas/HexString" + description: "Token ID (use \"0x00\" for ERC20)" + recipientData: + $ref: "#/components/schemas/NF3RecipientData" + fee: + $ref: "#/components/schemas/HexString" + description: Fee paid to the proposer + example: + ercAddress: "0x5FbDB2315678afecb367f032d93F642f64180aa3" + tokenId: "0x00" + recipientData: + values: ["0x01"] + recipientCompressedZkpPublicKeys: + - "0572aa70f4e62bcb8f53a28a1c259bd6d3538818afcccc0d8598486973ec2f2a" + fee: "0x01" + + NF3RecipientData: + type: object + required: + - values + - recipientCompressedZkpPublicKeys + properties: + values: + type: array + items: + $ref: "#/components/schemas/HexString" + description: Values to transfer to each recipient + recipientCompressedZkpPublicKeys: + type: array + items: + type: string + pattern: "^[0-9a-fA-F]+$" + description: Compressed ZKP public keys of the recipients (Layer 2 addresses) + + NF3WithdrawRequest: + type: object + required: + - ercAddress + - tokenId + - tokenType + - value + - recipientAddress + - fee + properties: + ercAddress: + $ref: "#/components/schemas/HexString" + description: Ethereum address of the ERC contract + tokenId: + $ref: "#/components/schemas/HexString" + description: "Token ID (use \"0x00\" for ERC20)" + tokenType: + type: string + description: "Token type: 0=ERC20, 1=ERC1155, 2=ERC721, 3=ERC3525" + enum: ["0", "1", "2", "3"] + value: + $ref: "#/components/schemas/HexString" + description: "Value to withdraw (\"0x00\" for ERC721)" + recipientAddress: + $ref: "#/components/schemas/HexString" + description: Ethereum address to receive the withdrawn tokens on Layer 1 + fee: + $ref: "#/components/schemas/HexString" + description: Fee paid to the proposer + example: + ercAddress: "0x98eddadcfde04dc22a0e62119617e74a6bc77313" + tokenId: "0x01" + tokenType: "1" + value: "0x00" + recipientAddress: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + fee: "0x09" + + KeyRequest: + type: object + required: + - mnemonic + - child_path + properties: + mnemonic: + type: string + description: BIP-39 mnemonic phrase (typically 24 words) + example: "spice split denial symbol resemble knock hunt trial make buzz attitude mom slice define clinic kid crawl guilt frozen there cage light secret work" + child_path: + type: string + description: BIP-32 derivation path + example: "m/44'/60'/0'/0/0" + + ZKPPubKey: + type: object + properties: + zkp_public_key: + type: string + description: Compressed BabyJubJub public key as a hex string + example: "0572aa70f4e62bcb8f53a28a1c259bd6d3538818afcccc0d8598486973ec2f2a" + + CommitmentEntry: + type: object + properties: + preimage: + $ref: "#/components/schemas/Preimage" + status: + $ref: "#/components/schemas/CommitmentStatus" + _id: + $ref: "#/components/schemas/HexFieldElement" + description: Commitment hash (key) + nullifier: + $ref: "#/components/schemas/HexFieldElement" + description: Nullifier for this commitment + token_type: + $ref: "#/components/schemas/TokenType" + layer_1_transaction_hash: + type: string + nullable: true + description: Hash of the L1 transaction that created this commitment + example: "0xabc123..." + layer_2_block_number: + type: integer + format: int64 + nullable: true + description: Layer 2 block number when this commitment was created + + Preimage: + type: object + properties: + value: + $ref: "#/components/schemas/HexFieldElement" + description: Token value + nf_token_id: + $ref: "#/components/schemas/HexFieldElement" + description: Nightfall token ID + nf_slot_id: + $ref: "#/components/schemas/HexFieldElement" + description: Nightfall slot ID + public_key: + type: string + description: Owner's BabyJubJub public key (compressed hex) + salt: + type: object + description: Salt used in the commitment hash + + CommitmentStatus: + type: string + enum: + - PendingSpend + - Spent + - PendingCreation + - Unspent + description: Current status of the commitment + + TokenType: + type: string + enum: + - ERC20 + - ERC1155 + - ERC721 + - ERC3525 + - FeeToken + + RequestStatus: + type: string + enum: + - Queued + - Submitted + - Failed + - Processing + - ProposerUnreachable + - Confirmed + description: | + Status of a transaction request: + - Queued: Waiting to be processed by the client + - Processing: Client is actively working on it + - Submitted: Successfully handed off to blockchain or proposer + - Failed: The hand-off to the next stage did not succeed + - ProposerUnreachable: Client was unable to reach the proposer + - Confirmed: Transaction lifecycle is complete, commitments are on-chain + + Proposer: + type: object + properties: + stake: + type: string + description: Proposer's staked amount (U256 as hex or decimal string) + addr: + type: string + description: Ethereum address of the proposer + example: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + url: + type: string + description: URL at which clients can reach the proposer + example: "http://proposer:3001" + next_addr: + type: string + description: Address of the next proposer in the linked list + previous_addr: + type: string + description: Address of the previous proposer in the linked list + + ClientTransaction: + type: object + description: | + A zero-knowledge proof transaction submitted by a client. Contains proof data, + commitments, nullifiers, and encrypted secrets. This is a complex structure + primarily used for client-to-proposer communication. + properties: + fee: + $ref: "#/components/schemas/HexFieldElement" + description: Transaction fee + historic_commitment_roots: + type: array + items: + $ref: "#/components/schemas/HexFieldElement" + minItems: 4 + maxItems: 4 + description: Four historic Merkle tree root values + commitments: + type: array + items: + $ref: "#/components/schemas/HexFieldElement" + minItems: 4 + maxItems: 4 + description: Four output commitment hashes + nullifiers: + type: array + items: + $ref: "#/components/schemas/HexFieldElement" + minItems: 4 + maxItems: 4 + description: Four nullifier values + compressed_secrets: + type: object + description: Encrypted secrets for the transaction recipients + swap_link: + $ref: "#/components/schemas/HexFieldElement" + description: Swap link field element + proof: + type: object + description: Zero-knowledge proof object + + TokenInfo: + type: object + description: Information about a token registered in Nightfall + properties: + erc_address: + $ref: "#/components/schemas/HexString" + description: The ERC contract address for the token + token_id: + $ref: "#/components/schemas/HexString" + description: The token ID (for ERC721/1155/3525) + example: + erc_address: "0x6fcb6af7f7947f8480c36e8ffca0c66f6f2be32b" + token_id: "0x00" + + CertificationResponse: + type: object + properties: + status: + type: string + example: "ok" + certified: + type: boolean + description: Whether the caller is now certified on-chain + + KeysValidationRequest: + type: object + required: + - configuration_url + properties: + configuration_url: + type: string + description: URL of the configuration/key server to validate against + example: "http://deployer:3003" + concurrency: + type: integer + description: Concurrency level for key downloads (default 2) + default: 2 + example: 2 + + responses: + BadRequest: + description: Bad request + content: + text/plain: + schema: + type: string + example: Bad request + + InternalServerError: + description: Internal server error + content: + text/plain: + schema: + type: string + example: INTERNAL_SERVER_ERROR