diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 68002e99..906c71d4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ **foc-devnet** is a Rust CLI tool for managing local Filecoin networks with FOC (Filecoin Onchain Contracts) support for warm storage services. It orchestrates Docker containers running Lotus nodes, miners, databases, and deploys smart contracts using Foundry (Forge/Cast). -**Key Technologies**: Rust, Docker, Filecoin Lotus, FEVM (Filecoin EVM), Foundry, YugabyteDB, Solidity +**Key Technologies**: Rust, Docker, Filecoin Lotus, FEVM (Filecoin EVM), Foundry, PostgreSQL, ScyllaDB, Solidity ## Architecture Patterns @@ -36,7 +36,7 @@ Steps execute in this strict order (see `src/commands/start/mod.rs`): 2. **Lotus** - Start Filecoin daemon with FEVM enabled 3. **Lotus-Miner** - Start first-generation block producer 4. **FOC Deploy** - Deploy MockUSDFC token and FOC warm storage contracts -5. **Yugabyte** - Start database for Curio +5. **Databases** - Start Postgres + Scylla for Curio 6. **Curio** - Start second-generation miner (commented out, WIP) **Critical**: Each step depends on previous steps being healthy. DO NOT reorder. @@ -47,7 +47,7 @@ Steps execute in this strict order (see `src/commands/start/mod.rs`): foc-lotus # Lotus daemon (ports 1234 API, 1235 P2P) foc-lotus-miner # First-gen miner (port 2345) foc-builder # Foundry tools (--network host) -foc-yugabyte # Database for Curio (port 5433) +postgres / scylla # Databases for Curio (stock images, dynamic ports) foc-curio # Second-gen miner (WIP) ``` @@ -71,7 +71,6 @@ All persistent data lives under `~/.foc-devnet/` (see `src/paths.rs`): │ ├── genesis/ # Genesis block config │ ├── genesis-sectors/ # Pre-sealed sectors │ ├── lotus-keys/ # BLS signing keys -│ ├── yugabyte-data/ # Database data │ └── foc-contract-addresses.json # Deployed contract addresses ├── logs/ # Container logs ├── repos/ diff --git a/.github/workflows/ci_run.yml b/.github/workflows/ci_run.yml index 32fcfc9a..4482939d 100644 --- a/.github/workflows/ci_run.yml +++ b/.github/workflows/ci_run.yml @@ -143,8 +143,7 @@ jobs: echo "docker-hash=$DOCKER_HASH" >> $GITHUB_OUTPUT echo "DOCKER_HASH: $DOCKER_HASH" - # CACHE-DOCKER: Try to restore pre-built Docker images (foc-lotus, foc-lotus-miner, foc-builder, foc-curio, foc-yugabyte) - # These images contain YugabyteDB and all build dependencies + # CACHE-DOCKER: Try to restore pre-built Docker images (foc-lotus, foc-lotus-miner, foc-builder, foc-curio) - name: "CACHE_RESTORE: {C-docker-images-cache}" id: cache-docker-images uses: actions/cache/restore@v4 @@ -168,15 +167,14 @@ jobs: rm -rf ~/.docker-images-cache df -h - # If Docker images are cached, skip building them AND skip downloading YugabyteDB - # (YugabyteDB is already baked into the foc-yugabyte Docker image) + # If Docker images are cached, skip building them - name: "EXEC: {Initialize with cached Docker}, DEP: {C-docker-images-cache}" if: steps.cache-docker-images.outputs.cache-hit == 'true' run: | ./foc-devnet clean --all ./foc-devnet init --no-docker-build ${{ inputs.init_flags }} - # If Docker images are not cached, do full init (downloads YugabyteDB and builds all images) + # If Docker images are not cached, do full init (builds all images) - name: "EXEC: {Initialize without cache}, independent" if: steps.cache-docker-images.outputs.cache-hit != 'true' run: | @@ -193,7 +191,6 @@ jobs: docker save foc-lotus-miner -o ~/.docker-images-cache/foc-lotus-miner.tar docker save foc-builder -o ~/.docker-images-cache/foc-builder.tar docker save foc-curio -o ~/.docker-images-cache/foc-curio.tar - docker save foc-yugabyte -o ~/.docker-images-cache/foc-yugabyte.tar echo "Docker images saved to cache" ls -lath ~/.docker-images-cache/ df -h @@ -403,7 +400,7 @@ jobs: echo "Warning: could not resolve numeric job ID (GITHUB_JOB=$GITHUB_JOB)" fi - # Setup scenario test prerequisites (Foundry, Python 3.11 via pyenv, cqlsh) + # Setup scenario test prerequisites (Foundry; CQL queries use the Scylla container's bundled cqlsh) - name: "EXEC: {Setup scenario prerequisites}, independent" if: steps.start_cluster.outcome == 'success' run: ./scripts/setup-scenarios-prerequisites.sh diff --git a/Cargo.lock b/Cargo.lock index f195c515..c5002b43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,12 +82,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "auto_impl" version = "1.3.0" @@ -123,12 +117,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "base64ct" version = "1.8.0" @@ -616,19 +604,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "downloader" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac1e888d6830712d565b2f3a974be3200be9296bc1b03db8251a4cbf18a4a34" -dependencies = [ - "futures", - "rand 0.8.5", - "reqwest 0.12.24", - "thiserror", - "tokio", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -842,7 +817,7 @@ name = "foc-devnet" version = "1.0.0-rc.1" dependencies = [ "base32", - "base64 0.21.7", + "base64", "bip32", "bip39", "blake2", @@ -852,7 +827,6 @@ dependencies = [ "clap_complete", "crc32fast", "dirs", - "downloader", "ethers-core", "flate2", "hex", @@ -861,7 +835,7 @@ dependencies = [ "num_cpus", "rand 0.8.5", "regex", - "reqwest 0.11.27", + "reqwest", "serde", "serde_json", "sha2 0.10.9", @@ -904,21 +878,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -926,7 +885,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -935,34 +893,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - [[package]] name = "futures-sink" version = "0.3.31" @@ -981,11 +917,8 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", "futures-io", - "futures-macro", - "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1049,7 +982,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 0.2.12", + "http", "indexmap", "slab", "tokio", @@ -1127,17 +1060,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http-body" version = "0.4.6" @@ -1145,30 +1067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", "pin-project-lite", ] @@ -1195,8 +1094,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 0.2.12", - "http-body 0.4.6", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1208,27 +1107,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - [[package]] name = "hyper-tls" version = "0.5.0" @@ -1236,50 +1114,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.8.1", - "hyper-util", + "hyper", "native-tls", "tokio", "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.8.1", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.1", - "tokio", - "tower-service", - "tracing", ] [[package]] @@ -1475,16 +1313,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2103,16 +1931,16 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-tls 0.5.0", + "http", + "http-body", + "hyper", + "hyper-tls", "ipnet", "js-sys", "log", @@ -2125,7 +1953,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -2137,42 +1965,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "reqwest" -version = "0.12.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.8.1", - "hyper-tls 0.6.0", - "hyper-util", - "js-sys", - "log", - "native-tls", - "percent-encoding", - "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "tokio", - "tokio-native-tls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -2239,16 +2031,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" -dependencies = [ - "zeroize", + "base64", ] [[package]] @@ -2605,15 +2388,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -2847,45 +2621,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 1.0.2", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index d3b62ba1..0fbce003 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ toml = "0.8" dirs = "5.0" chrono = { version = "0.4", features = ["serde"] } tempfile = "3.0" -downloader = "0.2" regex = "1.0" base64 = "0.21" hex = "0.4" diff --git a/README_ADVANCED.md b/README_ADVANCED.md index 41dec557..7bf60724 100644 --- a/README_ADVANCED.md +++ b/README_ADVANCED.md @@ -38,8 +38,6 @@ foc-devnet init [OPTIONS] - `--curio ` - Curio source location - `--lotus ` - Lotus source location - `--filecoin-services ` - Filecoin Services source location -- `--yugabyte-url ` - Yugabyte download URL -- `--yugabyte-archive ` - Local Yugabyte archive file - `--proof-params-dir ` - Local proof params directory - `--rand` - Use random mnemonic instead of deterministic one. Use this for unique test scenarios. @@ -123,7 +121,7 @@ foc-devnet stop ``` **What it does:** -- Stops containers in reverse order (Curio → Yugabyte → Lotus-Miner → Lotus) +- Stops containers in reverse order (Curio → Databases → Lotus-Miner → Lotus) - Removes containers to ensure clean state - Deletes Docker networks - Preserves Portainer for persistent access @@ -234,7 +232,7 @@ If resolution fails, you'll get clear error messages with exact fix instructions ```toml # Port range for dynamic allocation # foc-devnet uses a contiguous range of ports to avoid conflicts with other -# services on your machine. All components (Lotus, Curio SPs, Yugabyte, etc.) +# services on your machine. All components (Lotus, Curio SPs, databases, etc.) # dynamically allocate ports from this range. Using a dedicated range ensures: # - No conflicts with system services (MySQL, PostgreSQL, etc.) # - Easy firewall configuration (just open one range) @@ -246,9 +244,6 @@ port_range_count = 100 approved_pdp_sp_count = 1 # SPs registered and approved in registry active_pdp_sp_count = 1 # Total SPs actually running -# Yugabyte database -yugabyte_download_url = "https://software.yugabyte.com/releases/2.25.1.0/..." - # Component sources [lotus] url = "https://github.com/filecoin-project/lotus.git" @@ -276,7 +271,6 @@ branch = "main" | `port_range_count` | u16 | 100 | Number of ports in the range | | `approved_pdp_sp_count` | usize | 1 | Number of approved service providers | | `active_pdp_sp_count` | usize | 1 | Number of running service providers | -| `yugabyte_download_url` | string | (URL) | Yugabyte database tarball URL | **Constraints:** - `approved_pdp_sp_count` ≤ `active_pdp_sp_count` ≤ `MAX_PDP_SP_COUNT` (5) @@ -379,7 +373,6 @@ $FOC_DEVNET_BASEDIR/ │ └── / # Each run has its own volumes │ ├── lotus-data/ # Lotus blockchain data │ ├── lotus-miner-data/ -│ ├── yugabyte-data/ │ ├── curio-1/ # First Curio SP │ ├── curio-2/ # Second Curio SP (if active) │ └── ... @@ -672,7 +665,7 @@ These keys are string literals used throughout the codebase (see step implementa # → Run: curio info (to check status) # 4. Check database connectivity -# → Containers → foc--yugabyte → Stats +# → Containers → foc--postgres-1 → Stats # → Verify it's running and consuming resources ``` @@ -687,8 +680,8 @@ These keys are string literals used throughout the codebase (see step implementa |-----------|-------|---------|-------| | `foc--lotus` | foc-lotus | Filecoin daemon (FEVM enabled) | 1234 (API), 1235 (P2P) | | `foc--lotus-miner` | foc-lotus-miner | First-gen miner (PoRep) | 2345 (API) | -| `foc--yugabyte-1` | foc-yugabyte | Database for Curio SP 1 | 5433 (PostgreSQL) | -| `foc--yugabyte-N` | foc-yugabyte | Database for Curio SP N (one per SP) | Dynamic from range | +| `foc--postgres-N` | postgres | HarmonyDB for Curio SP N (one per SP) | Dynamic from range | +| `foc--scylla-N` | scylladb/scylla | IndexStore for Curio SP N (one per SP) | Dynamic from range | | `foc--curio-1` | foc-curio | First Curio SP (PDP) | Dynamic from range | | `foc--curio-N` | foc-curio | Nth Curio SP (PDP) | Dynamic from range | | `foc-builder` | foc-builder | Foundry tools (contract deployment) | Host network | @@ -720,7 +713,7 @@ graph TB portainer["🌐 Portainer
:5700"] lotus_api["📡 Lotus API
:5701"] miner_api["⛏️ Miner API
:5702"] - yugabyte_api["🗄️ Yugabyte
:5710"] + db_api["🗄️ Postgres / Scylla
:5710"] end subgraph lotus_net["foc-<run-id>-lot-net (Lotus Network)"] @@ -734,25 +727,25 @@ graph TB end subgraph curio_net_n["foc-<run-id>-cur-m-net-n (Curio SP N Network)"] - yugabyte_n["foc-yugabyte-n
(Database)"] + dbs_n["foc-postgres-n + foc-scylla-n
(Databases)"] curio_n["foc-curio-n
(PDP Service Provider)"] end %% Container to Host connections lotus -.->|exposes| lotus_api miner -.->|exposes| miner_api - yugabyte_n -.->|exposes| yugabyte_api + dbs_n -.->|exposes| db_api %% Network connections builder -->|uses host network| lotus curio_n -->|same container| curio_n_lot miner -->|connects to| lotus curio_n_lot -->|connects to| lotus - yugabyte_n <-->|database| curio_n + dbs_n <-->|databases| curio_n %% Styling classDef container fill:#fff,stroke:#333,stroke-width:1px - class lotus,builder,curio_n_lot,miner,yugabyte_n,curio_n container + class lotus,builder,curio_n_lot,miner,dbs_n,curio_n container ``` **Legend:** @@ -773,8 +766,8 @@ graph TB 3. **Curio Networks (`foc--cur-m-net-N`)**: - Each Curio SP gets its own network - - Each Curio SP has its own Yugabyte database instance on its network - - Provides DNS: Curio SP N can use `foc--yugabyte-N` as database host + - Each Curio SP has its own Postgres and Scylla instances on its network + - Provides DNS: Curio SP N can use `foc--postgres-N` and `foc--scylla-N` as database hosts **Builder uses host network** (`--network host`) to access Lotus RPC at `http://localhost:1234/rpc/v1`. @@ -783,7 +776,7 @@ graph TB Despite network segregation, you can still access all services from your host: - Lotus API: `http://localhost:1234/rpc/v1` - Lotus Miner API: `http://localhost:2345` -- Yugabyte Database: `postgresql://localhost:5433` +- Databases: dynamic ports per SP (see `devnet-info.json`) - Portainer UI: `http://localhost:5700` - Curio instances: Dynamic ports (check `docker ps`) @@ -797,7 +790,7 @@ The networks only affect container-to-container communication, not host-to-conta **Port allocation order:** 1. **First port (5700):** Portainer web UI - always uses `port_range_start` -2. **Remaining ports:** Dynamically assigned to Curio instances, Yugabyte, and other services as needed +2. **Remaining ports:** Dynamically assigned to Curio instances, databases, and other services as needed ```bash # Configure in config.toml @@ -921,8 +914,8 @@ Steps run sequentially by default, or in parallel when using the `--parallel` fl | 2 | Lotus Miner | No | Needs Lotus running | | 3 | ETH Account Funding | No | Needs blockchain active | | 4 | MockUSDFC Deploy + Multicall3 Deploy | **⚡ YES** | Independent contract deployments | -| 5 | FOC Deploy + USDFC Funding + Yugabyte | **⚡ YES** | Parallel contract work + DB startup | -| 6 | Curio SPs | No | Needs Yugabyte ready | +| 5 | FOC Deploy + USDFC Funding + Databases | **⚡ YES** | Parallel contract work + DB startup | +| 6 | Curio SPs | No | Needs databases ready | | 7 | PDP SP Registration | No | Needs Curio running for ports | | 8 | User Setup Step | No | User setup step | @@ -963,9 +956,9 @@ Steps run sequentially by default, or in parallel when using the `--parallel` fl - Deploy PDPVerifier, ServiceProviderRegistry, etc. - Save all contract addresses -**Yugabyte Step:** - - Start Yugabyte database (one instance per Curio SP, on the SP's network) - - Verify PostgreSQL port +**Database Step:** + - Start Postgres and Scylla (one pair per Curio SP, on the SP's network) + - Verify Postgres and CQL ports **Curio Step:** - Initialize Curio database schemas @@ -1164,8 +1157,8 @@ docker logs foc--curio-2 # Query provider IDs cat ~/.foc-devnet/state/latest/pdp_sps/*.provider_id.json -# Access Yugabyte (one per SP, see below for more detail) -docker exec -it foc--yugabyte-1 ysqlsh -h localhost -p 5433 +# Access Postgres (one per SP, see below for more detail) +docker exec -it foc--postgres-1 psql -U curio -d curio # Query Lotus for miner info docker exec foc--lotus lotus state miner-info f01000 @@ -1177,15 +1170,16 @@ docker exec foc--builder cast call \ ``` -### Querying Yugabyte Database +### Querying the Databases -Each Curio has its own Yugabyte (curio-N → yugabyte-N). Tables are in `curio` schema. Credentials: `yugabyte`/`yugabyte`/`yugabyte` (user/pass/db). +Each Curio has its own Postgres and Scylla (curio-N → postgres-N / scylla-N). Postgres tables are in the `curio` schema. Credentials: `curio`/`curio`/`curio` (user/pass/db). ```bash -docker exec foc--yugabyte-1 bash -c "PGPASSWORD=yugabyte /yugabyte/bin/ysqlsh -h 127.0.0.1 -U yugabyte -d yugabyte -c \"\"" +docker exec foc--postgres-1 psql -U curio -d curio -c "" +docker exec foc--scylla-1 cqlsh -e "" ``` -Key tables: `curio.harmony_machines`, `curio.harmony_task`, `curio.harmony_task_history`, `curio.parked_pieces`. +Key tables: `curio.harmony_machines`, `curio.harmony_task`, `curio.harmony_task_history`, `curio.parked_pieces`. The Scylla IndexStore holds the piece index and PDP proof cache (`curio.pdp_cache_layer`). --- diff --git a/docker/yugabyte/Dockerfile b/docker/yugabyte/Dockerfile deleted file mode 100644 index fe691896..00000000 --- a/docker/yugabyte/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -FROM ubuntu:24.04 - -# Install required dependencies -RUN apt-get update && apt-get install -y \ - wget \ - ca-certificates \ - locales \ - python3 \ - && rm -rf /var/lib/apt/lists/* - -# Generate locale to avoid locale-related errors -RUN locale-gen en_US.UTF-8 -ENV LANG=en_US.UTF-8 -ENV LANGUAGE=en_US:en -ENV LC_ALL=en_US.UTF-8 - -# Create foc-user and foc-group with matching host UID/GID when it is run. The 1002 below is just a placeholder which will be replaced during build process. See `docker.rs` -ARG USER_ID=1002 -ARG GROUP_ID=1002 -RUN userdel ubuntu || true -RUN GROUP_NAME=$(getent group ${GROUP_ID} | cut -d: -f1 || echo "") && \ - if [ -z "$GROUP_NAME" ]; then \ - groupadd -g ${GROUP_ID} foc-group && GROUP_NAME=foc-group; \ - fi && \ - useradd -l -u ${USER_ID} -g $GROUP_NAME -m -s /bin/bash foc-user - -# Set working directory -WORKDIR /yugabyte - -# Copy YugabyteDB from build context -# Expects yugabyte directory to be available in the build context -ARG YUGABYTE_SOURCE=yugabyte -COPY ${YUGABYTE_SOURCE} /yugabyte - -# Run post-install script and set ownership -RUN GROUP_NAME=$(getent group ${GROUP_ID} | cut -d: -f1) && \ - /yugabyte/bin/post_install.sh && \ - mkdir -p /yugabyte/data && \ - chown -R foc-user:$GROUP_NAME /yugabyte - -# Switch to foc-user -USER foc-user - -# Expose YugabyteDB ports -# 7000 - YB-Master RPC -# 9000 - YB-Master Admin UI -# 7100 - YB-TServer RPC -# 9100 - YB-TServer Admin UI -# 5433 - YSQL (PostgreSQL-compatible API) -# 9042 - YCQL (Cassandra-compatible API) -# 15433 - YugabyteDB Web UI -EXPOSE 7000 9000 7100 9100 5433 9042 15433 diff --git a/docker/yugabyte/volumes_map.toml b/docker/yugabyte/volumes_map.toml deleted file mode 100644 index be4566f3..00000000 --- a/docker/yugabyte/volumes_map.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Volume mappings for foc-yugabyte Docker image -# Format: host_subdirectory = "container_path" -# Host paths will be relative to ~/.foc-devnet/artifacts/docker/volumes/foc-yugabyte/ - -[volumes] -data = "/var/ybdata" diff --git a/examples/devnet-schema.js b/examples/devnet-schema.js index fe08b5ed..70124601 100644 --- a/examples/devnet-schema.js +++ b/examples/devnet-schema.js @@ -8,10 +8,9 @@ import { z } from "zod"; -const YugabyteInfo = z.object({ - web_ui_url: z.string().url(), - master_rpc_port: z.number().int().positive(), - ysql_port: z.number().int().positive(), +const DatabaseInfo = z.object({ + postgres_port: z.number().int().positive(), + scylla_port: z.number().int().positive(), }); const CurioInfo = z.object({ @@ -23,7 +22,7 @@ const CurioInfo = z.object({ container_name: z.string().min(1), is_approved: z.boolean(), is_endorsed: z.boolean(), - yugabyte: YugabyteInfo, + database: DatabaseInfo, }); const ContractsInfo = z.object({ @@ -60,7 +59,7 @@ const LotusMinerInfo = z.object({ api_port: z.number().int().positive(), }); -const DevnetInfoV1 = z.object({ +const DevnetInfoV2 = z.object({ run_id: z.string().min(1), start_time: z.string(), startup_duration: z.string().min(1), @@ -72,8 +71,8 @@ const DevnetInfoV1 = z.object({ }); export const VersionedDevnetInfo = z.object({ - version: z.literal(1), - info: DevnetInfoV1, + version: z.literal(2), + info: DevnetInfoV2, }); /** diff --git a/examples/read-devnet-info.js b/examples/read-devnet-info.js index 73c9bcf6..a7d73127 100644 --- a/examples/read-devnet-info.js +++ b/examples/read-devnet-info.js @@ -101,9 +101,9 @@ function printCurioProviders(providers) { console.log(` PDP Service URL: ${provider.pdp_service_url}`); console.log(` Container: ${provider.container_name}`); console.log(` Container ID: ${provider.container_id.substring(0, 12)}...`); - console.log(` YugabyteDB:`); - console.log(` Web UI: ${provider.yugabyte.web_ui_url}`); - console.log(` YSQL Port: ${provider.yugabyte.ysql_port}`); + console.log(` Databases:`); + console.log(` Postgres Port: ${provider.database.postgres_port}`); + console.log(` Scylla Port: ${provider.database.scylla_port}`); console.log(); } } diff --git a/scenarios/synapse.py b/scenarios/synapse.py index 530aac18..9e330d8c 100644 --- a/scenarios/synapse.py +++ b/scenarios/synapse.py @@ -7,6 +7,8 @@ from scenarios.helpers import info, run_cmd, sh SYNAPSE_SDK_REPO = "https://github.com/FilOzone/synapse-sdk/" +# master with devnet-info v2 `database` block support (#821). +SYNAPSE_SDK_REF = "d80aad33aeaab51b177ec4fe514209a93f984355" def clone_and_build(tmp_dir: Path) -> Path | None: @@ -17,7 +19,9 @@ def clone_and_build(tmp_dir: Path) -> Path | None: ): return None if not run_cmd( - ["git", "checkout", "master"], cwd=str(sdk_dir), label="checkout master HEAD" + ["git", "checkout", SYNAPSE_SDK_REF], + cwd=str(sdk_dir), + label="checkout pinned synapse-sdk", ): return None sdk_commit = sh(f"git -C {sdk_dir} rev-parse HEAD") diff --git a/scenarios/test_caching_subsystem.py b/scenarios/test_caching_subsystem.py index e93f5b29..662510bd 100644 --- a/scenarios/test_caching_subsystem.py +++ b/scenarios/test_caching_subsystem.py @@ -2,8 +2,8 @@ """Caching subsystem scenario. Checks whether uploading a small piece does not trigger caching and -whether a larger piece does trigger caching (> 32MB). Ensures that -cassandra rows are populated. +whether a larger piece does trigger caching (> 32MB). Ensures the Scylla +CQL proof-cache (curio.pdp_cache_layer) rows are populated. Standalone run: python3 scenarios/test_caching_subsystem.py @@ -29,14 +29,8 @@ ) from scenarios.synapse import clone_and_build, upload_file -CASSANDRA_VERSION = "5.0.7" -PYTHON_VERSION = "3.11.10" -PYENV_ROOT = Path.home() / ".pyenv" -PYTHON_DIR = PYENV_ROOT / "versions" / PYTHON_VERSION -CASSANDRA_DIR = Path.home() / ".foc-devnet" / "artifacts" / "cassandra" -CASSANDRA_HOME = CASSANDRA_DIR / f"apache-cassandra-{CASSANDRA_VERSION}" -SMALL_FILE_SIZE = 20 * 1024 * 1024 # 20MB -- below 32MB threshold -LARGE_FILE_SIZE = 80 * 1024 * 1024 # 80MB -- above 32MB threshold +SMALL_FILE_SIZE = 20 * 1024 * 1024 # 20MB, below the 32MB threshold +LARGE_FILE_SIZE = 80 * 1024 * 1024 # 80MB, above the 32MB threshold RAND_SEED_SMALL = 42 RAND_SEED_LARGE = 85 RUN_COUNTER_FILE = ( @@ -57,47 +51,34 @@ def _next_run_index() -> int: return next_index -def _find_cqlsh(): - """Locate cqlsh and custom Python. Returns (cqlsh_path, python_path).""" - python_bin = PYTHON_DIR / "bin" / "python3" - if not python_bin.exists(): - raise RuntimeError( - f"Python {PYTHON_VERSION} not found at {python_bin}. " - f"Run scripts/setup-scenarios-prerequisites.sh first." - ) - cqlsh = CASSANDRA_HOME / "bin" / "cqlsh" - if not cqlsh.exists(): - raise RuntimeError( - f"cqlsh not found at {cqlsh}. " - f"Run scripts/setup-scenarios-prerequisites.sh first." - ) - info(f"cqlsh version: {sh(f'CQLSH_PYTHON={python_bin} {cqlsh} --version')}") - return str(cqlsh), str(python_bin) +def _scylla_container() -> str: + """Derive the first SP's Scylla container name from devnet-info.""" + dn = devnet_info()["info"] + provider_id = dn["pdp_sps"][0]["provider_id"] + return f"foc-{dn['run_id']}-scylla-{provider_id}" -def _ycql(cqlsh, python, ycql_port, query): - """Run a YCQL query via cqlsh, return raw output.""" - return sh( - f'{cqlsh} --python {python} localhost {ycql_port} -u cassandra -p cassandra -e "{query}"' - ) +def _cql(scylla_container, query): + """Run a CQL query via the Scylla container's bundled cqlsh, return raw output.""" + return sh(f'docker exec {scylla_container} cqlsh -e "{query}"') -def _row_count(ycql_output): +def _row_count(cql_output): """Extract row count from cqlsh output. Calls fail() if pattern not found.""" - match = re.search(r"\((\d+)\s+rows\)", ycql_output) + match = re.search(r"\((\d+)\s+rows\)", cql_output) if match is None: - fail(f"Could not parse row count from cqlsh output: {ycql_output!r}") + fail(f"Could not parse row count from cqlsh output: {cql_output!r}") return int(match.group(1)) -def _upload_and_count(sdk_dir, filepath, label, cqlsh, python, ycql_port): +def _upload_and_count(sdk_dir, filepath, label, scylla_container): """Upload a file and return the cache row count afterward.""" import time upload_file(sdk_dir, filepath.name, label) info(f"Waiting {CACHE_WAIT_SECS}s for caching tasks") time.sleep(CACHE_WAIT_SECS) - output = _ycql(cqlsh, python, ycql_port, "SELECT * FROM curio.pdp_cache_layer") + output = _cql(scylla_container, "SELECT * FROM curio.pdp_cache_layer") count = _row_count(output) info(f"row_count after '{label}' = {count}") return count @@ -113,11 +94,10 @@ def run(): seed_large = RAND_SEED_LARGE + run_index info(f"Run index: {run_index}, seeds: small={seed_small}, large={seed_large}") - cqlsh, python = _find_cqlsh() - ycql_port = devnet_info()["info"]["pdp_sps"][0]["yugabyte"]["ycql_port"] - info(f"Yugabyte cassandra port: localhost:{ycql_port}") + scylla_container = _scylla_container() + info(f"Scylla container: {scylla_container}") - init_output = _ycql(cqlsh, python, ycql_port, "SELECT * FROM curio.pdp_cache_layer") + init_output = _cql(scylla_container, "SELECT * FROM curio.pdp_cache_layer") init_count = _row_count(init_output) info(f"Initial row count = {init_count}") @@ -133,13 +113,13 @@ def run(): info("Uploading 20MB piece (below 32MB threshold)") after_small = _upload_and_count( - sdk_dir, small_file, "upload 20MB piece", cqlsh, python, ycql_port + sdk_dir, small_file, "upload 20MB piece", scylla_container ) assert_eq(after_small, init_count, "cache rows count should not increase") info("Uploading 80MB piece (above 32MB threshold)") after_large = _upload_and_count( - sdk_dir, large_file, "upload 80MB piece", cqlsh, python, ycql_port + sdk_dir, large_file, "upload 80MB piece", scylla_container ) assert_gt(after_large, init_count, "cache rows count should increase") diff --git a/scenarios/test_multi_copy_upload.py b/scenarios/test_multi_copy_upload.py index 4f5c6252..34a5905a 100644 --- a/scenarios/test_multi_copy_upload.py +++ b/scenarios/test_multi_copy_upload.py @@ -67,24 +67,21 @@ def output_text(value) -> str: return value or "" -_CONTENT_LENGTH_LINE_RE = re.compile( - r"^[ \t]*\.\.\.\(size == null \? \{\} : \{ 'Content-Length': size\.toString\(\) \}\),\r?\n", - re.MULTILINE, -) +_SCHEMA_YUGABYTE_RE = re.compile(r"yugabyte: YugabyteInfo,") +_SCHEMA_VERSION_RE = re.compile(r"version: z\.literal\(1\),") -def patch_synapse_core_streaming_upload(npm_dir: Path) -> bool: - """Strip Content-Length from @filoz/synapse-core's streaming upload headers. +def patch_synapse_core_devnet_schema(npm_dir: Path) -> bool: + """Accept devnet-info schema v2 in @filoz/synapse-core's validator. - synapse-core 0.4.1 (which older filecoin-pin releases resolved to) set a - Content-Length header on a body that flows through a streaming Transform. - Node/undici rejects that as 'invalid content-length header', which - filecoin-pin surfaces as - 'StorageContext store failed: Failed to store piece on service provider - - Network request failed'. + foc-devnet exports schema v2 (per-SP `database` block instead of + `yugabyte`), but synapse-core ships a strict v1 validator: version must + be 1 and a `yugabyte` block is required per SP. Widen the version literal + and make `yugabyte` optional; the yugabyte data is never read beyond + validation. - Keep this tolerant until the older workaround has been exercised against - the latest filecoin-pin release in CI. + TODO: drop this patch once filecoin-pin resolves a synapse-core release + that accepts schema v2 (FilOzone/synapse-sdk rvagg/devnet-dbs). Returns True when the target file is in the desired state (whether or not a change was applied). Returns False only when the file is missing. @@ -96,25 +93,27 @@ def patch_synapse_core_streaming_upload(npm_dir: Path) -> bool: / "synapse-core" / "dist" / "src" - / "sp" - / "upload-streaming.js" + / "devnet" + / "schema.js" ) if not target.exists(): return False text = target.read_text() - updated, count = _CONTENT_LENGTH_LINE_RE.subn("", text) - if count > 0: + updated, yugabyte_count = _SCHEMA_YUGABYTE_RE.subn( + "yugabyte: YugabyteInfo.optional(),", text + ) + updated, version_count = _SCHEMA_VERSION_RE.subn( + "version: z.union([z.literal(1), z.literal(2)]),", updated + ) + if yugabyte_count or version_count: target.write_text(updated) info( - "Patched @filoz/synapse-core streaming upload " - f"(stripped {count} Content-Length line(s))" + "Patched @filoz/synapse-core devnet schema " + f"(yugabyte optional: {yugabyte_count}, version union: {version_count})" ) else: - info( - "@filoz/synapse-core streaming upload already free of " - "Content-Length header; no patch needed" - ) + info("@filoz/synapse-core devnet schema already accepts v2; no patch needed") return True @@ -255,9 +254,9 @@ def run(): run_cmd(["npm", "install"], label="npm install", cwd=npm_dir) - if not patch_synapse_core_streaming_upload(npm_dir): + if not patch_synapse_core_devnet_schema(npm_dir): fail( - "Could not locate @filoz/synapse-core streaming upload " + "Could not locate @filoz/synapse-core devnet schema " "to patch (file missing under node_modules)" ) return diff --git a/scripts/setup-scenarios-prerequisites.sh b/scripts/setup-scenarios-prerequisites.sh index 56221f6d..65e18475 100755 --- a/scripts/setup-scenarios-prerequisites.sh +++ b/scripts/setup-scenarios-prerequisites.sh @@ -3,12 +3,8 @@ # setup-scenarios-prerequisites.sh — Install all scenario test # dependencies so that scenario scripts only run tests, not setup. # -# Installs (if not already present): -# 1. Foundry (cast, forge) -# 2. Python 3.11.10 via pyenv (for cqlsh / Cassandra) -# 3. cqlsh via Apache Cassandra tarball -# -# Also verifies that git, node, and pnpm are available. +# Installs Foundry (cast, forge) if not already present, and verifies +# that git, node, and pnpm are available. # # Usage: # ./scripts/setup-scenarios-prerequisites.sh @@ -26,15 +22,7 @@ info() { printf "${BLUE}ℹ${NC} %s\n" "$1"; } # ── Constants ──────────────────────────────────────────────── FOUNDRY_VERSION="v1.6.0-rc1" -PYENV_VERSION="v2.5.3" -CASSANDRA_VERSION="5.0.7" -PYTHON_VERSION="3.11.10" -PYENV_ROOT="${PYENV_ROOT:-$HOME/.pyenv}" -PYTHON_BIN="${PYENV_ROOT}/versions/${PYTHON_VERSION}/bin/python3" -CASSANDRA_URL="https://archive.apache.org/dist/cassandra/${CASSANDRA_VERSION}/apache-cassandra-${CASSANDRA_VERSION}-bin.tar.gz" -CASSANDRA_DIR="$HOME/.foc-devnet/artifacts/cassandra" FOUNDRY_DIR="$HOME/.foc-devnet/artifacts/foundry/bin" -CASSANDRA_HOME="${CASSANDRA_DIR}/apache-cassandra-${CASSANDRA_VERSION}" verify_checksum() { local file="$1" expected="$2" @@ -87,62 +75,6 @@ else fi fi -# ── 2. Python 3.11.10 via pyenv (for cqlsh) ───────────────── -info "Checking Python ${PYTHON_VERSION} via pyenv..." - -CUSTOM_PYTHON="${PYTHON_BIN}" - -if [[ -x "$CUSTOM_PYTHON" ]]; then - pass "Python ${PYTHON_VERSION} already installed (${CUSTOM_PYTHON})" -else - # Install pyenv if not present - if ! command -v pyenv &>/dev/null; then - if [[ -x "${PYENV_ROOT}/bin/pyenv" ]]; then - export PATH="${PYENV_ROOT}/bin:$PATH" - else - info "Installing pyenv ${PYENV_VERSION} from GitHub tarball..." - PYENV_TARBALL="/tmp/pyenv-${PYENV_VERSION}.tar.gz" - curl -fsSL -o "$PYENV_TARBALL" \ - "https://github.com/pyenv/pyenv/archive/refs/tags/${PYENV_VERSION}.tar.gz" - mkdir -p "${PYENV_ROOT}" - tar -xzf "$PYENV_TARBALL" -C "${PYENV_ROOT}" --strip-components=1 - rm -f "$PYENV_TARBALL" - export PATH="${PYENV_ROOT}/bin:$PATH" - fi - fi - pass "pyenv available ($(command -v pyenv))" - - info "Installing Python ${PYTHON_VERSION} via pyenv..." - pyenv install -s "${PYTHON_VERSION}" - if [[ -x "$CUSTOM_PYTHON" ]]; then - pass "Python ${PYTHON_VERSION} installed (${CUSTOM_PYTHON})" - else - fail "Python ${PYTHON_VERSION} installation failed — binary not found at ${CUSTOM_PYTHON}" - fi -fi - -# ── 3. cqlsh (Apache Cassandra tarball) ────────────────────── -info "Checking cqlsh..." - -CQLSH="${CASSANDRA_HOME}/bin/cqlsh" - -if [[ -x "$CQLSH" ]]; then - CQLSH_VERSION="$(CQLSH_PYTHON="$CUSTOM_PYTHON" "$CQLSH" --version 2>&1 || true)" - pass "cqlsh already installed (${CQLSH_VERSION})" -else - info "Downloading Apache Cassandra ${CASSANDRA_VERSION} for cqlsh..." - mkdir -p "$CASSANDRA_DIR" - TARBALL="${CASSANDRA_DIR}/apache-cassandra-${CASSANDRA_VERSION}-bin.tar.gz" - curl -fL -o "$TARBALL" "$CASSANDRA_URL" - tar -xzf "$TARBALL" -C "$CASSANDRA_DIR" - if [[ -x "$CQLSH" ]]; then - CQLSH_VERSION="$(CQLSH_PYTHON="$CUSTOM_PYTHON" "$CQLSH" --version 2>&1 || true)" - pass "cqlsh installed (${CQLSH_VERSION})" - else - fail "cqlsh installation failed — ${CQLSH} not found after extraction" - fi -fi - # ── Done ───────────────────────────────────────────────────── echo "" echo "════════════════════════════════════════════════════════" diff --git a/src/cli.rs b/src/cli.rs index 6875b664..552843a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -56,12 +56,6 @@ pub enum Commands { /// 'local:/path/to/filecoin-services'. #[arg(long)] filecoin_services: Option, - /// Yugabyte download URL - #[arg(long)] - yugabyte_url: Option, - /// Path to local Yugabyte archive file (.tar.gz) to use instead of downloading - #[arg(long)] - yugabyte_archive: Option, /// Path to local filecoin-proof-params directory to use instead of downloading #[arg(long)] proof_params_dir: Option, diff --git a/src/commands/init/artifacts.rs b/src/commands/init/artifacts.rs index 3a3500db..3529a8cd 100644 --- a/src/commands/init/artifacts.rs +++ b/src/commands/init/artifacts.rs @@ -1,230 +1,31 @@ -//! Artifact download utilities for foc-devnet initialization. +//! Artifact staging utilities for foc-devnet initialization. //! -//! This module handles the downloading and extraction of required artifacts, -//! primarily the Yugabyte database. +//! Handles staging optional local artifacts, currently the filecoin proof +//! parameters. -use downloader::Downloader; use indicatif::{ProgressBar, ProgressStyle}; use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::path::Path; use tracing::info; -use crate::config::Config; -use crate::paths::{foc_devnet_artifacts, foc_devnet_config, foc_devnet_proof_parameters}; +use crate::paths::{foc_devnet_artifacts, foc_devnet_proof_parameters}; -/// Download required artifacts for foc-devnet. -/// -/// This function downloads Yugabyte database and extracts it to the -/// artifacts directory, or copies from local paths if provided. +/// Stage optional local artifacts for foc-devnet. /// /// # Arguments -/// * `yugabyte_archive` - Optional path to local Yugabyte archive file -/// * `proof_params_dir` - Optional path to local filecoin-proof-params directory +/// * `proof_params_dir` - Optional path to a local filecoin-proof-params directory /// /// # Returns -/// Returns `Ok(())` if artifacts are downloaded successfully, or an error if download fails. -pub fn download_artifacts( - yugabyte_archive: Option, - proof_params_dir: Option, -) -> Result<(), Box> { - info!("Downloading artifacts..."); - - // Ensure artifacts directory exists - let artifacts_dir = foc_devnet_artifacts(); - fs::create_dir_all(&artifacts_dir)?; +/// Returns `Ok(())` if staging succeeds, or an error if it fails. +pub fn stage_artifacts(proof_params_dir: Option) -> Result<(), Box> { + // Ensure the artifacts directory exists for anything staged later + // (e.g. the Foundry toolchain installed by the scenario prerequisites). + fs::create_dir_all(foc_devnet_artifacts())?; - // Handle Yugabyte - if let Some(archive_path) = yugabyte_archive { - copy_yugabyte_from_local(&archive_path, &artifacts_dir)?; - } else { - // Load configuration to get download URL - let config_path = foc_devnet_config(); - let config_content = fs::read_to_string(&config_path) - .map_err(|e| format!("Failed to read config file at {:?}: {}", config_path, e))?; - let config: Config = toml::from_str(&config_content) - .map_err(|e| format!("Failed to parse config file: {}", e))?; - download_yugabyte(&config.yugabyte_download_url, &artifacts_dir)?; - } - - // Handle proof parameters if let Some(params_path) = proof_params_dir { copy_proof_params_from_local(¶ms_path)?; } - info!("Artifacts downloaded successfully."); - Ok(()) -} - -/// Downloads and extracts Yugabyte database. -/// -/// Downloads the Yugabyte tarball from the given URL and extracts it -/// to the specified directory. -/// -/// # Arguments -/// * `url` - Download URL for the Yugabyte tarball -/// * `artifacts_dir` - Directory to extract Yugabyte into -/// -/// # Returns -/// Returns `Ok(())` if download and extraction succeed, or an error if they fail. -fn download_yugabyte(url: &str, artifacts_dir: &Path) -> Result<(), Box> { - let tarball_path = download_yugabyte_tarball(url, artifacts_dir)?; - extract_yugabyte_tarball(&tarball_path, artifacts_dir)?; - Ok(()) -} - -/// Download the Yugabyte tarball from the given URL. -/// -/// # Arguments -/// * `url` - Download URL for the tarball -/// * `artifacts_dir` - Directory to save the tarball in -/// -/// # Returns -/// Returns the path to the downloaded tarball, or an error if download fails. -fn download_yugabyte_tarball( - url: &str, - artifacts_dir: &Path, -) -> Result> { - // Extract filename from URL - let filename = url - .split('/') - .next_back() - .ok_or("Invalid URL: no filename")?; - let tarball_path = artifacts_dir.join(filename); - - if tarball_path.exists() { - info!( - "Yugabyte tarball already exists at {}", - tarball_path.display() - ); - return Ok(tarball_path); - } - - // Create progress bar - let pb = ProgressBar::new_spinner(); - pb.set_style( - ProgressStyle::default_spinner() - .template("{spinner:.green} {msg}") - .unwrap(), - ); - pb.set_message(format!("Downloading Yugabyte from {}...", url)); - - // Download the tarball using downloader - let mut downloader = Downloader::builder() - .download_folder(artifacts_dir) - .build()?; - - let dl = downloader::Download::new(url).file_name(Path::new(&filename)); - downloader.download(&[dl])?; - - pb.finish_with_message("✓ Downloaded Yugabyte"); - - Ok(tarball_path) -} - -/// Extract the Yugabyte tarball to the artifacts directory. -/// -/// This function extracts the tarball and renames the extracted directory -/// to "yugabyte" for consistency. If the yugabyte directory already exists, -/// extraction is skipped. -/// -/// # Arguments -/// * `tarball_path` - Path to the downloaded tarball -/// * `artifacts_dir` - Directory containing the tarball and extraction target -/// -/// # Returns -/// Returns `Ok(())` if extraction succeeds or is skipped, or an error if extraction fails. -fn extract_yugabyte_tarball( - tarball_path: &Path, - artifacts_dir: &Path, -) -> Result<(), Box> { - let yugabyte_dir = artifacts_dir.join("yugabyte"); - - if yugabyte_dir.exists() { - info!( - "Yugabyte directory already exists at {}", - yugabyte_dir.display() - ); - return Ok(()); - } - - info!("Extracting Yugabyte tarball..."); - - // Extract the tarball - let pb_extract = ProgressBar::new_spinner(); - pb_extract.set_style( - ProgressStyle::default_spinner() - .template("{spinner:.green} {msg}") - .unwrap(), - ); - pb_extract.set_message("Extracting Yugabyte..."); - - let status = Command::new("tar") - .args(["xfz", &tarball_path.to_string_lossy()]) - .current_dir(artifacts_dir) - .status()?; - - if !status.success() { - return Err("Failed to extract Yugabyte tarball".to_string().into()); - } - - // Find the extracted directory and rename it to "yugabyte" - let mut extracted_dir = None; - for entry in fs::read_dir(artifacts_dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - if name.starts_with("yugabyte-") { - extracted_dir = Some(path); - break; - } - } - } - } - - if let Some(extracted) = extracted_dir { - fs::rename(&extracted, &yugabyte_dir)?; - } else { - return Err("Could not find extracted yugabyte directory".into()); - } - - pb_extract.finish_with_message("✓ Extracted Yugabyte"); - - info!("Yugabyte downloaded and installed successfully."); - - Ok(()) -} - -/// Copy Yugabyte from a local archive file. -/// -/// This function extracts a local Yugabyte tarball instead of downloading it. -/// -/// # Arguments -/// * `archive_path` - Path to the local Yugabyte tarball -/// * `artifacts_dir` - Directory to extract Yugabyte into -/// -/// # Returns -/// Returns `Ok(())` if copy and extraction succeed, or an error if they fail. -fn copy_yugabyte_from_local( - archive_path: &str, - artifacts_dir: &Path, -) -> Result<(), Box> { - let archive_path = Path::new(archive_path); - if !archive_path.exists() { - return Err(format!( - "Local Yugabyte archive not found: {}", - archive_path.display() - ) - .into()); - } - - info!( - "Copying Yugabyte from local archive: {}", - archive_path.display() - ); - - extract_yugabyte_tarball(archive_path, artifacts_dir)?; Ok(()) } diff --git a/src/commands/init/config.rs b/src/commands/init/config.rs index 73fe8b5f..3e478752 100644 --- a/src/commands/init/config.rs +++ b/src/commands/init/config.rs @@ -15,13 +15,12 @@ use crate::paths::foc_devnet_config; /// reused as-is with any CLI location overrides applied on top. Otherwise a /// fresh default config is created. /// -/// Location overrides can be provided for Curio, Lotus, Filecoin Services, and Yugabyte URL. +/// Location overrides can be provided for Curio, Lotus, and Filecoin Services. /// /// # Arguments /// * `curio_location` - Optional override for Curio repository location /// * `lotus_location` - Optional override for Lotus repository location /// * `filecoin_services_location` - Optional override for Filecoin Services repository location -/// * `yugabyte_url` - Optional override for Yugabyte download URL /// /// # Returns /// Returns `Ok(())` on successful config generation, or an error if generation fails. @@ -29,7 +28,6 @@ pub fn generate_default_config( curio_location: Option, lotus_location: Option, filecoin_services_location: Option, - yugabyte_url: Option, ) -> Result<(), Box> { let config_path = foc_devnet_config(); @@ -39,7 +37,6 @@ pub fn generate_default_config( if curio_location.is_some() || lotus_location.is_some() || filecoin_services_location.is_some() - || yugabyte_url.is_some() { let content = fs::read_to_string(&config_path)?; let mut config: Config = toml::from_str(&content) @@ -49,7 +46,6 @@ pub fn generate_default_config( curio_location, lotus_location, filecoin_services_location, - yugabyte_url, )?; let updated = toml::to_string(&config) .map_err(|e| format!("Failed to serialize config: {}", e))?; @@ -70,7 +66,6 @@ pub fn generate_default_config( curio_location, lotus_location, filecoin_services_location, - yugabyte_url, )?; let default_config = toml::to_string(&config) @@ -82,13 +77,12 @@ pub fn generate_default_config( Ok(()) } -/// Apply location and URL overrides to a config. +/// Apply location overrides to a config. fn apply_overrides( config: &mut Config, curio_location: Option, lotus_location: Option, filecoin_services_location: Option, - yugabyte_url: Option, ) -> Result<(), Box> { apply_location_override( &mut config.lotus, @@ -106,11 +100,6 @@ fn apply_overrides( "https://github.com/FilOzone/filecoin-services.git", )?; - // Override yugabyte URL if provided - if let Some(url) = yugabyte_url { - config.yugabyte_download_url = url; - } - Ok(()) } diff --git a/src/commands/init/mod.rs b/src/commands/init/mod.rs index d35c604c..47280ca7 100644 --- a/src/commands/init/mod.rs +++ b/src/commands/init/mod.rs @@ -24,8 +24,6 @@ pub struct InitOptions { pub curio_location: Option, pub lotus_location: Option, pub filecoin_services_location: Option, - pub yugabyte_url: Option, - pub yugabyte_archive: Option, pub proof_params_dir: Option, pub use_random_mnemonic: bool, pub no_docker_build: bool, @@ -49,7 +47,6 @@ pub fn init_environment(options: InitOptions) -> Result<(), Box Result<(), Box Result } // Get FOC service contracts - if let Some(payment) = addresses.foc_contracts.get("payment_contract") { + if let Some(payment) = addresses + .foc_contracts + .get("payment_contract") + .or_else(|| addresses.foc_contracts.get("filecoin_pay_v1_contract")) + { env_vars.push(format!("CURIO_DEVNET_PAYMENTS_ADDRESS={}", payment)); } if let Some(multicall) = addresses.foc_contracts.get("multicall_address") { @@ -80,16 +89,21 @@ pub fn build_db_env_vars( sp_index: usize, ) -> Result, Box> { let run_id = context.run_id(); - let yugabyte_name = format!("foc-{}-yugabyte-{}", run_id, sp_index); + let postgres_host = postgres_container_name(run_id, sp_index); + let scylla_host = scylla_container_name(run_id, sp_index); Ok(vec![ "CURIO_DB_SSLMODE=disable".to_string(), - format!("CURIO_DB_HOST={}", yugabyte_name), - "CURIO_DB_PORT=5433".to_string(), - "CURIO_DB_USER=yugabyte".to_string(), - "CURIO_DB_PASSWORD=yugabyte".to_string(), - "CURIO_DB_NAME=yugabyte".to_string(), + // HarmonyDB (Postgres-wire) + format!("CURIO_DB_HOST={}", postgres_host), + format!("CURIO_DB_PORT={}", POSTGRES_CONTAINER_PORT), + format!("CURIO_DB_USER={}", DB_USER), + format!("CURIO_DB_PASSWORD={}", DB_PASSWORD), + format!("CURIO_DB_NAME={}", DB_NAME), "CURIO_DB_LOAD_BALANCE=false".to_string(), + // IndexStore (Cassandra/CQL-wire): separate Scylla host + format!("CURIO_DB_HOST_CQL={}", scylla_host), + format!("CURIO_DB_CASSANDRA_PORT={}", SCYLLA_CQL_CONTAINER_PORT), ]) } diff --git a/src/commands/start/curio/mod.rs b/src/commands/start/curio/mod.rs index 649b3c42..9dfc124c 100644 --- a/src/commands/start/curio/mod.rs +++ b/src/commands/start/curio/mod.rs @@ -22,7 +22,7 @@ use tracing::info; /// /// This step: /// - Verifies Lotus is running and producing blocks -/// - Sets up Yugabyte database for each Curio SP +/// - Runs database migrations against each Curio SP's Postgres /// - Configures base and PDP layers /// - Starts Curio daemon with appropriate layers /// - Attaches storage locations diff --git a/src/commands/start/database/mod.rs b/src/commands/start/database/mod.rs new file mode 100644 index 00000000..5c4e4dc6 --- /dev/null +++ b/src/commands/start/database/mod.rs @@ -0,0 +1,319 @@ +//! Database step: per-SP PostgreSQL (HarmonyDB) + ScyllaDB (IndexStore). +//! +//! Curio needs two endpoints: a Postgres-wire database for HarmonyDB and a +//! Cassandra-wire database for the IndexStore. Each SP gets a stock `postgres` +//! and a tuned `scylladb` container on its per-SP network. Data is ephemeral. +//! The devnet is recreated each run and the Scylla IndexStore is a regenerable +//! cache. + +use super::step::{SetupContext, Step}; +use crate::constants::{ + DB_NAME, DB_PASSWORD, DB_USER, POSTGRES_CONTAINER_PORT, POSTGRES_DOCKER_IMAGE, + SCYLLA_CQL_CONTAINER_PORT, SCYLLA_DOCKER_IMAGE, +}; +use crate::docker::command_logger::run_and_log_command; +use crate::docker::containers::{postgres_container_name, scylla_container_name}; +use crate::docker::core::image_exists; +use crate::docker::network::pdp_miner_network_name; +use crate::docker::{container_exists, container_is_running, stop_and_remove_container}; +use std::error::Error; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; +use tracing::{info, warn}; + +/// Readiness polling. +const MAX_RETRIES: u32 = 60; +const RETRY_DELAY_SECS: u64 = 2; + +/// Start the PostgreSQL container for one SP. `fsync=off` is safe and faster for +/// throwaway devnet data and speeds up Curio's migration apply. +fn spawn_postgres_instance( + sp_idx: usize, + host_port: u16, + run_id: &str, + context: &SetupContext, +) -> Result<(), Box> { + let container_name = postgres_container_name(run_id, sp_idx); + let network_name = pdp_miner_network_name(run_id, sp_idx); + + if container_exists(&container_name)? { + warn!( + "⚠ Removing existing Postgres container {}...", + container_name + ); + stop_and_remove_container(&container_name)?; + } + + let port_mapping = format!("{}:{}", host_port, POSTGRES_CONTAINER_PORT); + let user_env = format!("POSTGRES_USER={}", DB_USER); + let password_env = format!("POSTGRES_PASSWORD={}", DB_PASSWORD); + let db_env = format!("POSTGRES_DB={}", DB_NAME); + + let docker_args = vec![ + "run", + "-d", + "--name", + &container_name, + "--network", + &network_name, + "-p", + &port_mapping, + "-e", + &user_env, + "-e", + &password_env, + "-e", + &db_env, + POSTGRES_DOCKER_IMAGE, + "-c", + "fsync=off", + "-c", + "full_page_writes=off", + ]; + + let key = format!("postgres_start_sp_{}", sp_idx); + let output = run_and_log_command("docker", &docker_args, context, &key)?; + if !output.status.success() { + return Err(format!( + "Failed to start Postgres container {}: {}", + container_name, + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + Ok(()) +} + +/// Start the tuned ScyllaDB container for one SP. The flags hold it to a single +/// shard and a small memory reservation. The IndexStore data is KB-scale. +fn spawn_scylla_instance( + sp_idx: usize, + host_port: u16, + run_id: &str, + context: &SetupContext, +) -> Result<(), Box> { + let container_name = scylla_container_name(run_id, sp_idx); + let network_name = pdp_miner_network_name(run_id, sp_idx); + + if container_exists(&container_name)? { + warn!("⚠ Removing existing Scylla container {}...", container_name); + stop_and_remove_container(&container_name)?; + } + + let port_mapping = format!("{}:{}", host_port, SCYLLA_CQL_CONTAINER_PORT); + + let docker_args = vec![ + "run", + "-d", + "--name", + &container_name, + "--network", + &network_name, + "-p", + &port_mapping, + SCYLLA_DOCKER_IMAGE, + "--smp", + "1", + "--memory", + "512M", + "--overprovisioned", + "1", + "--developer-mode", + "1", + "--reserve-memory", + "0", + ]; + + let key = format!("scylla_start_sp_{}", sp_idx); + let output = run_and_log_command("docker", &docker_args, context, &key)?; + if !output.status.success() { + return Err(format!( + "Failed to start Scylla container {}: {}", + container_name, + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + Ok(()) +} + +/// Wait until Postgres accepts connections. +fn verify_postgres(container_name: &str, context: &SetupContext) -> Result<(), Box> { + for attempt in 1..=MAX_RETRIES { + let key = format!("postgres_verify_{}_{}", container_name, attempt); + let output = run_and_log_command( + "docker", + &["exec", container_name, "pg_isready", "-U", DB_USER, "-q"], + context, + &key, + )?; + if output.status.success() { + return Ok(()); + } + if attempt < MAX_RETRIES { + thread::sleep(Duration::from_secs(RETRY_DELAY_SECS)); + } + } + Err(format!("Postgres {} did not become ready", container_name).into()) +} + +/// Wait until Scylla serves CQL. Uses the cqlsh bundled in the Scylla image. +fn verify_scylla(container_name: &str, context: &SetupContext) -> Result<(), Box> { + for attempt in 1..=MAX_RETRIES { + let key = format!("scylla_verify_{}_{}", container_name, attempt); + let output = run_and_log_command( + "docker", + &[ + "exec", + container_name, + "cqlsh", + "-e", + "SELECT now() FROM system.local", + ], + context, + &key, + )?; + if output.status.success() { + return Ok(()); + } + if attempt < MAX_RETRIES { + thread::sleep(Duration::from_secs(RETRY_DELAY_SECS)); + } + } + Err(format!("Scylla {} did not become ready", container_name).into()) +} + +/// Step that starts per-SP Postgres + Scylla containers for Curio. +pub struct DatabaseStep { + #[allow(dead_code)] + volumes_dir: PathBuf, + #[allow(dead_code)] + run_dir: PathBuf, + /// Number of PDP SPs to activate (1-5). + active_sp_count: usize, +} + +impl DatabaseStep { + pub fn new(volumes_dir: PathBuf, run_dir: PathBuf, active_sp_count: usize) -> Self { + Self { + volumes_dir, + run_dir, + active_sp_count, + } + } + + /// Read the allocated host ports for an SP from context. + fn instance_ports( + &self, + context: &SetupContext, + sp_idx: usize, + ) -> Result<(u16, u16), Box> { + let pg: u16 = context + .get(&format!("db_{}_postgres_port", sp_idx)) + .ok_or(format!("db_{}_postgres_port not found in context", sp_idx))? + .parse()?; + let scylla: u16 = context + .get(&format!("db_{}_scylla_port", sp_idx)) + .ok_or(format!("db_{}_scylla_port not found in context", sp_idx))? + .parse()?; + Ok((pg, scylla)) + } +} + +impl Step for DatabaseStep { + fn name(&self) -> &str { + "Start Databases (Postgres + Scylla)" + } + + fn pre_execute(&self, context: &SetupContext) -> Result<(), Box> { + // Stock images are pulled here rather than staged by init, so a fresh + // machine (or a CI cache carrying only the foc-built images) works. + for image in [POSTGRES_DOCKER_IMAGE, SCYLLA_DOCKER_IMAGE] { + if !image_exists(image)? { + info!("Image '{}' not present, pulling...", image); + let key = format!("docker_pull_{}", image.replace(['/', ':'], "_")); + let output = run_and_log_command("docker", &["pull", image], context, &key)?; + if !output.status.success() { + return Err(format!( + "Failed to pull image '{}': {}", + image, + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + info!("✓ Pulled {}", image); + } + } + + // Allocate and reserve a host port for each engine, for each SP. + for sp_idx in 1..=self.active_sp_count { + let ports = context.allocate_multiple_ports(2)?; + for (port, label) in [(ports[0], "postgres"), (ports[1], "scylla")] { + if !crate::docker::is_port_available(port) { + return Err(format!("Port {} ({}) is already in use", port, label).into()); + } + } + context.set(format!("db_{}_postgres_port", sp_idx), ports[0].to_string()); + context.set(format!("db_{}_scylla_port", sp_idx), ports[1].to_string()); + } + + info!( + "✓ DB images present, ports allocated for {} SP(s)", + self.active_sp_count + ); + Ok(()) + } + + fn execute(&self, context: &SetupContext) -> Result<(), Box> { + let run_id = context.run_id(); + info!( + "Starting Postgres + Scylla for {} SP(s)...", + self.active_sp_count + ); + + for sp_idx in 1..=self.active_sp_count { + let (pg_port, scylla_port) = self.instance_ports(context, sp_idx)?; + spawn_postgres_instance(sp_idx, pg_port, run_id, context)?; + spawn_scylla_instance(sp_idx, scylla_port, run_id, context)?; + info!("SP {}: postgres + scylla containers started", sp_idx); + } + + Ok(()) + } + + fn post_execute(&self, context: &SetupContext) -> Result<(), Box> { + let run_id = context.run_id(); + + for sp_idx in 1..=self.active_sp_count { + let pg_name = postgres_container_name(run_id, sp_idx); + let scylla_name = scylla_container_name(run_id, sp_idx); + + for name in [&pg_name, &scylla_name] { + if !container_is_running(name)? { + return Err(format!("Database container {} stopped unexpectedly", name).into()); + } + } + + info!("SP {}: waiting for Postgres...", sp_idx); + verify_postgres(&pg_name, context)?; + info!("SP {}: waiting for Scylla...", sp_idx); + verify_scylla(&scylla_name, context)?; + info!("✓ SP {}: databases ready", sp_idx); + } + + info!("✓ All database instances ready"); + Ok(()) + } + + fn run(&self, context: &SetupContext) -> Result> { + let start = std::time::Instant::now(); + self.pre_execute(context)?; + self.execute(context)?; + self.post_execute(context)?; + info!("✓ {} completed successfully", self.name()); + Ok(start.elapsed()) + } +} diff --git a/src/commands/start/mod.rs b/src/commands/start/mod.rs index ad743144..e98acfe8 100644 --- a/src/commands/start/mod.rs +++ b/src/commands/start/mod.rs @@ -1,4 +1,5 @@ mod curio; +mod database; mod endorsement; mod eth_acc_funding; mod foc_deploy; @@ -15,9 +16,9 @@ pub mod step; mod usdfc_deploy; mod usdfc_funding; mod user_setup; -mod yugabyte; use curio::CurioStep; +use database::DatabaseStep; use endorsement::EndorsementStep; use eth_acc_funding::ETHAccFundingStep; use foc_deploy::FOCDeployStep; @@ -30,7 +31,6 @@ use prerequisites_check::PrerequisitesCheckStep; pub use step::{execute_steps, execute_steps_parallel, SetupContext, Step}; use usdfc_deploy::USDFCDeployStep; use user_setup::UserSetupStep; -use yugabyte::YugabyteStep; use crate::commands::start::usdfc_funding::USDFCFundingStep; use crate::config::Config; @@ -152,7 +152,6 @@ fn stop_running_containers() -> Result<(), Box> { crate::constants::LOTUS_MINER_CONTAINER, crate::constants::LOTUS_CONTAINER, crate::constants::CURIO_CONTAINER, - crate::constants::YUGABYTE_CONTAINER, ]; for container in containers { if container_is_running(container)? { @@ -279,7 +278,7 @@ fn create_steps(volumes_dir: &Path, run_dir: &Path, config: &Config) -> Vec Vec Vec Vec>> { let prerequisites_check_step = PrerequisitesCheckStep::new(); let lotus_step = LotusStep::new(volumes_dir.to_path_buf(), run_dir.to_path_buf()); - let yugabyte_step = YugabyteStep::new( + let database_step = DatabaseStep::new( volumes_dir.to_path_buf(), run_dir.to_path_buf(), config.active_pdp_sp_count, @@ -387,11 +386,11 @@ fn create_step_epochs( Box::new(usdfc_deploy_step), Box::new(multicall3_deploy_step), ], - // Epoch 6: Fund accounts with USDFC, deploy foc (needs usdfc deployed), start yugabyte for curio later + // Epoch 6: Fund accounts with USDFC, deploy foc (needs usdfc deployed), start databases for curio later vec![ Box::new(foc_deploy_step), Box::new(usdfc_funding_step), - Box::new(yugabyte_step), + Box::new(database_step), ], // Epoch 7: Start Curio daemons vec![Box::new(curio_step)], diff --git a/src/commands/start/yugabyte/mod.rs b/src/commands/start/yugabyte/mod.rs deleted file mode 100644 index 0f13018b..00000000 --- a/src/commands/start/yugabyte/mod.rs +++ /dev/null @@ -1,475 +0,0 @@ -use super::step::{SetupContext, Step}; -use crate::constants::YUGABYTE_DOCKER_IMAGE; -use crate::docker::command_logger::run_and_log_command; -use std::error::Error; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; -use tracing::{error, info, warn}; - -use crate::docker::containers::yugabyte_container_name; -use crate::docker::network::pdp_miner_network_name; -use crate::docker::{ - container_exists, container_is_running, stop_and_remove_container, wait_for_port, -}; -use crate::paths::foc_devnet_yugabyte_sp_volume; - -/// Spawn a single Yugabyte instance (used for parallel spawning). -/// -/// This function is thread-safe and can be called concurrently. -fn spawn_yugabyte_instance( - sp_idx: usize, - total_instances: usize, - ports: &[u16], - _volumes_dir: &PathBuf, - run_id: &str, - context: &SetupContext, -) -> Result<(), Box> { - // Generate container name with instance suffix - // Format: foc-{run_id}-yugabyte-{instance_index} (always indexed for consistency) - let container_name = yugabyte_container_name(run_id, sp_idx); - let network_name = pdp_miner_network_name(run_id, sp_idx); - - // Create data directory for this instance - // This will be mounted to /home/foc-user/yb_base in the container - // yugabyted will create subdirectories (data/, conf/, logs/) under this base directory - let data_dir = foc_devnet_yugabyte_sp_volume(run_id, sp_idx); - std::fs::create_dir_all(&data_dir)?; - - // Stop and remove existing container if it exists - if container_exists(&container_name)? { - warn!( - "⚠ Removing existing Yugabyte container {} ...", - if total_instances == 1 { - "".to_string() - } else { - sp_idx.to_string() - } - ); - stop_and_remove_container(&container_name)?; - } - - // Build Docker run command - let mut docker_args = vec![ - "run", - "-d", - "--name", - &container_name, - "--network", - &network_name, - ]; - - // Add port mappings - let port_mappings = vec![ - format!("{}:5433", ports[0]), // YSQL - format!("{}:9042", ports[1]), // YCQL - format!("{}:7100", ports[2]), // Master RPC - format!("{}:7000", ports[3]), // Master UI - format!("{}:9100", ports[4]), // TServer RPC - format!("{}:9000", ports[5]), // TServer UI - format!("{}:15433", ports[6]), // Web UI - ]; - - for mapping in &port_mappings { - docker_args.push("-p"); - docker_args.push(mapping); - } - - // Add volume mount - mount to /home/foc-user/yb_base which yugabyted will use as base_dir - let data_dir_str = data_dir.to_str().ok_or("Invalid path")?; - let volume_mount = format!("{}:/home/foc-user/yb_base", data_dir_str); - docker_args.extend_from_slice(&["-v", &volume_mount]); - - // Add environment variables - docker_args.extend_from_slice(&[ - "-e", - "YSQL_PASSWORD=yugabyte", - "-e", - "YSQL_DB=yugabyte", - "-e", - "YSQL_USER=yugabyte", - ]); - - // Add image name - docker_args.push(YUGABYTE_DOCKER_IMAGE); - - // Add YugabyteDB startup command with full configuration - // CRITICAL: --base_dir must match the volume mount location - // NOTE: --advertise_address is intentionally omitted. Setting it to 0.0.0.0 caused - // seed DDL via yugabyted to succeed but left the YCQL/YSQL proxies unreachable from - // other containers. Without --advertise_address, yugabyted auto-detects the container's - // IP on the Docker bridge network, which is reachable by other containers on the same network. - docker_args.extend_from_slice(&[ - "/yugabyte/bin/yugabyted", - "start", - "--base_dir=/home/foc-user/yb_base", - "--ui=true", - "--callhome=false", - "--master_flags=rpc_bind_addresses=0.0.0.0", - "--tserver_flags=rpc_bind_addresses=0.0.0.0,pgsql_proxy_bind_address=0.0.0.0:5433,cql_proxy_bind_address=0.0.0.0:9042", - "--daemon=false", - ]); - - // Run the container - let key = format!("yugabyte_start_sp_{}", sp_idx); - let output = run_and_log_command("docker", &docker_args, context, &key)?; - - if !output.status.success() { - return Err(format!( - "Failed to start Yugabyte container: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into()); - } - - Ok(()) -} - -/// Verify PostgreSQL connectivity for a specific Yugabyte instance. -fn verify_postgres_connection_for_instance( - container_name: &str, - context: &SetupContext, -) -> Result<(), Box> { - const YUGABYTE_YSQL_CONTAINER_PORT: &str = "5433"; - const MAX_RETRIES: u32 = 30; - const RETRY_DELAY_SECS: u64 = 2; - - // YugabyteDB YSQL service takes time to initialize after the container starts - // Retry connection attempts with delays - for attempt in 1..=MAX_RETRIES { - let key = format!("yugabyte_verify_{}_{}", container_name, attempt); - let output = run_and_log_command( - "docker", - &[ - "exec", - "-e", - "PGPASSWORD=yugabyte", - container_name, - "/yugabyte/bin/ysqlsh", - "-h", - "localhost", - "-p", - YUGABYTE_YSQL_CONTAINER_PORT, - "-U", - "yugabyte", - "-d", - "yugabyte", - "-c", - "SELECT 1;", - ], - context, - &key, - )?; - - if output.status.success() { - return Ok(()); - } - - // If not the last attempt, wait before retrying - if attempt < MAX_RETRIES { - thread::sleep(Duration::from_secs(RETRY_DELAY_SECS)); - } else { - // Last attempt failed, return error - return Err(format!( - "Failed to query PostgreSQL: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into()); - } - } - - Ok(()) -} - -/// Step for starting YugabyteDB -pub struct YugabyteStep { - volumes_dir: PathBuf, - #[allow(dead_code)] - run_dir: PathBuf, - /// Number of PDP SPs to activate (1-5) - active_sp_count: usize, -} - -impl YugabyteStep { - /// Create a new YugabyteStep - pub fn new(volumes_dir: PathBuf, run_dir: PathBuf, active_sp_count: usize) -> Self { - Self { - volumes_dir, - run_dir, - active_sp_count, - } - } - - /// Get the ports for a specific Yugabyte instance from context - fn get_instance_ports( - &self, - context: &SetupContext, - instance_index: usize, - ) -> Result, Box> { - let prefix = format!("yugabyte_{}", instance_index); - let port_suffixes = [ - "ysql_port", - "ycql_port", - "master_rpc_port", - "master_ui_port", - "tserver_rpc_port", - "tserver_ui_port", - "web_ui_port", - ]; - - let mut ports = Vec::new(); - for suffix in &port_suffixes { - let key = format!("{}_{}", prefix, suffix); - let port: u16 = context - .get(&key) - .ok_or(format!("Port key {} not found in context", key))? - .parse()?; - ports.push(port); - } - Ok(ports) - } -} - -impl Step for YugabyteStep { - fn name(&self) -> &str { - "Start YugabyteDB" - } - - fn pre_execute(&self, context: &SetupContext) -> Result<(), Box> { - // Verify Docker image exists - if !crate::docker::core::image_exists(YUGABYTE_DOCKER_IMAGE).unwrap_or(true) { - return Err(format!( - "Docker image '{}' not found. Please run 'foc-devnet init' to build the image.", - YUGABYTE_DOCKER_IMAGE - ) - .into()); - } - info!("✓ Docker image '{}' found", YUGABYTE_DOCKER_IMAGE); - - // Check if ports are available - let sp_count = self.active_sp_count; - - info!( - "Checking port availability for {} Yugabyte instance(s)...", - sp_count - ); - - // Allocate ports for all instances upfront in pre_execute - for instance_index in 1..=sp_count { - let yugabyte_ports = context.allocate_multiple_ports(7)?; - let prefix = format!("yugabyte_{}", instance_index); - - context.set( - format!("{}_ysql_port", prefix), - yugabyte_ports[0].to_string(), - ); - context.set( - format!("{}_ycql_port", prefix), - yugabyte_ports[1].to_string(), - ); - context.set( - format!("{}_master_rpc_port", prefix), - yugabyte_ports[2].to_string(), - ); - context.set( - format!("{}_master_ui_port", prefix), - yugabyte_ports[3].to_string(), - ); - context.set( - format!("{}_tserver_rpc_port", prefix), - yugabyte_ports[4].to_string(), - ); - context.set( - format!("{}_tserver_ui_port", prefix), - yugabyte_ports[5].to_string(), - ); - context.set( - format!("{}_web_ui_port", prefix), - yugabyte_ports[6].to_string(), - ); - } - - let port_descriptions = [ - "YSQL (PostgreSQL API)", - "YCQL (Cassandra API)", - "YB-Master RPC", - "YB-Master Admin UI", - "YB-TServer RPC", - "YB-TServer Admin UI", - "YugabyteDB Web UI", - ]; - - // Check each port for availability - for i in 0..sp_count { - let ports = self.get_instance_ports(context, i + 1)?; - for (port, desc) in ports.iter().zip(port_descriptions.iter()) { - if !crate::docker::is_port_available(*port) { - warn!("⚠ Port {} ({}) is already in use", port, desc); - return Err(format!("Port {} is already in use", port).into()); - } - } - } - - info!("✓ All required ports are available"); - Ok(()) - } - - fn execute(&self, context: &SetupContext) -> Result<(), Box> { - let sp_count = self.active_sp_count; - - info!("Starting {} YugabyteDB instance(s)...", sp_count); - - // Get ports for all instances from context (already allocated in pre_execute) - let mut all_ports: Vec<(usize, Vec)> = Vec::new(); - for instance_index in 1..=sp_count { - let ports = self.get_instance_ports(context, instance_index)?; - all_ports.push((instance_index, ports)); - } - - // Spawn containers in parallel using threads - let errors: Arc>> = Arc::new(Mutex::new(Vec::new())); - let mut handles = Vec::new(); - let num_instances = self.active_sp_count; - - for (sp_idx, ports) in all_ports.into_iter() { - let volumes_dir = self.volumes_dir.clone(); - let run_id = context.run_id().to_string(); - let errors_clone = Arc::clone(&errors); - let context_clone = context.clone(); - - let handle = thread::spawn(move || { - match spawn_yugabyte_instance( - sp_idx, - num_instances, - &ports, - &volumes_dir, - &run_id, - &context_clone, - ) { - Ok(_) => { - info!( - "Yugabyte instance {} started successfully", - if num_instances == 1 { - "".to_string() - } else { - sp_idx.to_string() - } - ); - } - Err(e) => { - let error_msg = format!("Instance {}: {}", sp_idx, e); - errors_clone.lock().unwrap().push(error_msg); - } - } - }); - - handles.push(handle); - } - - // Wait for all threads to complete - for handle in handles { - if let Err(e) = handle.join() { - error!("Yugabyte spawn thread panicked: {:?}", e); - return Err("Thread panicked".into()); - } - } - - info!("✓ All YugabyteDB instances started"); - - Ok(()) - } - - fn post_execute(&self, context: &SetupContext) -> Result<(), Box> { - let num_instances = self.active_sp_count; - let run_id = context.run_id(); - - info!("Waiting for YugabyteDB instance(s) to start..."); - thread::sleep(Duration::from_secs(5)); - - // Verify all instances - for instance_index in 1..=num_instances { - let container_name = yugabyte_container_name(run_id, instance_index); - - // Verify container is running - if !container_is_running(&container_name)? { - return Err( - format!("Yugabyte instance {} stopped unexpectedly", container_name).into(), - ); - } - info!("Yugabyte instance {} is running", container_name); - - // Check all ports are accessible for this instance - let prefix = format!("yugabyte_{}", instance_index); - - let port_names = [ - ("ysql_port", "YSQL (PostgreSQL API)"), - ("ycql_port", "YCQL (Cassandra API)"), - ("master_rpc_port", "YB-Master RPC"), - ("master_ui_port", "YB-Master Admin UI"), - ("tserver_rpc_port", "YB-TServer RPC"), - ("tserver_ui_port", "YB-TServer Admin UI"), - ("web_ui_port", "YugabyteDB Web UI"), - ]; - - for (port_suffix, description) in port_names { - let port_key = format!("{}_{}", prefix, port_suffix); - let port: u16 = context - .get(&port_key) - .ok_or(format!("Port key {} not found in context", port_key))? - .parse()?; - - info!( - "{} - Checking port {} ({})...", - container_name, port, description - ); - if let Err(e) = wait_for_port(port, 30) { - return Err(format!("Port {} is not accessible: {}", port, e).into()); - } - } - - // Verify PostgreSQL connection for this instance - info!( - "Verifying PostgreSQL connectivity for {}...", - container_name - ); - thread::sleep(Duration::from_secs(2)); - - if let Err(e) = verify_postgres_connection_for_instance(&container_name, context) { - return Err(format!( - "PostgreSQL verification failed for {}: {}", - container_name, e - ) - .into()); - } - - info!("✓ PostgreSQL is ready for {}", container_name); - } - - info!( - "✓ All {} Yugabyte instance(s) verified successfully", - num_instances - ); - - // Show connection info - info!("✓ All YugabyteDB instance(s) ready!"); - - for instance_index in 1..=num_instances { - let prefix = format!("yugabyte_{}", instance_index); - let web_ui_port: u16 = context - .get(&format!("{}_web_ui_port", prefix)) - .unwrap() - .parse()?; - let ysql_port: u16 = context - .get(&format!("{}_ysql_port", prefix)) - .unwrap() - .parse()?; - info!( - "Instance {} - Web UI: http://localhost:{}, YSQL: localhost:{}", - instance_index, web_ui_port, ysql_port - ); - } - - Ok(()) - } -} diff --git a/src/commands/status/running.rs b/src/commands/status/running.rs index 5b5b498d..670f9fff 100644 --- a/src/commands/status/running.rs +++ b/src/commands/status/running.rs @@ -10,6 +10,10 @@ use tracing::info; +use crate::constants::MAX_PDP_SP_COUNT; +use crate::docker::containers::{ + curio_container_name, postgres_container_name, scylla_container_name, +}; use crate::docker::core::image_exists; use crate::docker::status::{ get_container_ports, get_container_uptime, get_running_foc_containers, @@ -35,7 +39,6 @@ pub fn print_running_status() -> Result<(), Box> { vec![ ("Lotus Daemon", format!("foc-{}-lotus", id)), ("Lotus Miner", format!("foc-{}-lotus-miner", id)), - ("YugabyteDB", format!("foc-{}-yugabyte", id)), ("Builder", format!("foc-{}-builder", id)), ] } else { @@ -49,10 +52,6 @@ pub fn print_running_status() -> Result<(), Box> { crate::constants::LOTUS_MINER_CONTAINER.to_string(), ), ("Curio", crate::constants::CURIO_CONTAINER.to_string()), - ( - "YugabyteDB", - crate::constants::YUGABYTE_CONTAINER.to_string(), - ), ("Builder", crate::constants::BUILDER_CONTAINER.to_string()), ] }; @@ -80,17 +79,24 @@ pub fn print_running_status() -> Result<(), Box> { } } - // Check for Curio instances if run ID exists + // Show per-SP instances (Curio + its databases) that exist for this run. + // The SP count is config-driven, so probe up to the maximum and report + // whatever is running. if let Some(ref id) = run_id { - for sp_idx in 1..=5 { - let curio_container = format!("foc-{}-curio-{}", id, sp_idx); - if containers.contains(&curio_container) { - let uptime = get_container_uptime(&curio_container)?; - let ports = format_container_ports(&curio_container)?; - info!( - "Curio SP-{}: Running | Uptime: {} | Ports: {}", - sp_idx, uptime, ports - ); + for sp_idx in 1..=MAX_PDP_SP_COUNT { + for (service, container_name) in [ + ("Curio", curio_container_name(id, sp_idx)), + ("Postgres", postgres_container_name(id, sp_idx)), + ("Scylla", scylla_container_name(id, sp_idx)), + ] { + if containers.contains(&container_name) { + let uptime = get_container_uptime(&container_name)?; + let ports = format_container_ports(&container_name)?; + info!( + "{} SP-{}: Running | Uptime: {} | Ports: {}", + service, sp_idx, uptime, ports + ); + } } } } diff --git a/src/commands/stop.rs b/src/commands/stop.rs index 5ef13855..904ee364 100644 --- a/src/commands/stop.rs +++ b/src/commands/stop.rs @@ -7,7 +7,6 @@ use tracing::{info, warn}; /// Container names for all services const CONTAINERS: &[(&str, &str)] = &[ (crate::constants::CURIO_CONTAINER, "Curio"), - (crate::constants::YUGABYTE_CONTAINER, "YugabyteDB"), (crate::constants::LOTUS_MINER_CONTAINER, "Lotus-Miner"), (crate::constants::LOTUS_CONTAINER, "Lotus"), ]; @@ -145,14 +144,22 @@ fn stop_and_remove_service_container( Ok(()) } -/// Get container names for a specific run ID +/// Get container names for a specific run ID, in reverse of start order so +/// dependents go before their dependencies: Curio SPs, then their databases, +/// then Lotus-Miner, then Lotus. Names for SP indices that were never started +/// are skipped by stop_and_remove_service_container. fn get_run_containers(run_id: &str) -> Vec<(String, &'static str)> { - vec![ - (format!("foc-{}-curio", run_id), "Curio"), - (format!("foc-{}-yugabyte", run_id), "YugabyteDB"), - (format!("foc-{}-lotus-miner", run_id), "Lotus-Miner"), - (format!("foc-{}-lotus", run_id), "Lotus"), - ] + let mut containers: Vec<(String, &'static str)> = (1..=crate::constants::MAX_PDP_SP_COUNT) + .map(|sp| (crate::docker::curio_container_name(run_id, sp), "Curio")) + .collect(); + // Per-SP database containers run stock images and are invisible to the + // image-based force-kill sweep. Remove them explicitly by name. + for name in crate::docker::db_container_names(run_id) { + containers.push((name, "Database")); + } + containers.push((format!("foc-{}-lotus-miner", run_id), "Lotus-Miner")); + containers.push((format!("foc-{}-lotus", run_id), "Lotus")); + containers } /// Force kill all foc-devnet containers, identified by exact image name. Avoids diff --git a/src/config.rs b/src/config.rs index 750c167e..2136ff93 100644 --- a/src/config.rs +++ b/src/config.rs @@ -236,14 +236,6 @@ pub struct Config { /// See [`Location`] for available options. pub multicall3: Location, - /// URL to download Yugabyte database tarball. - /// - /// This is the direct link to the Yugabyte tarball required for running curio. - /// The default URL is automatically selected based on system architecture: - /// - ARM64 (aarch64): yugabyte-2.25.1.0-b381-el8-aarch64.tar.gz - /// - x86_64: yugabyte-2.25.1.0-b381-linux-x86_64.tar.gz - pub yugabyte_download_url: String, - /// Number of approved PDP service providers. /// /// This is the total number of Curio SPs that will be registered and approved @@ -289,17 +281,21 @@ impl Default for Config { }, curio: Location::GitCommit { url: "https://github.com/filecoin-project/curio.git".to_string(), - commit: "60f77a618eee24cd3482be5fea545e01f26052a4".to_string(), + // main with the indexstore ALLOW FILTERING fix, so the proof + // cache works on Cassandra/Scylla. Re-pin to a tag when one exists. + commit: "46a3640e125b1089a11d68653c25dce795ba374d".to_string(), }, - filecoin_services: Location::GitTag { + filecoin_services: Location::GitCommit { url: "https://github.com/FilOzone/filecoin-services.git".to_string(), - tag: "v1.2.0".to_string(), + // main with the GA fee model (sybil burn removed, createDataSet + // fee with deposit). Must pair with the curio commit above, which + // sends zero-value dataset creation. Re-pin to a tag when one exists. + commit: "02de64a17847f59262b535ab548cae6be307917f".to_string(), }, multicall3: Location::GitTag { url: "https://github.com/mds1/multicall3.git".to_string(), tag: "v3.1.0".to_string(), }, - yugabyte_download_url: Self::get_default_yugabyte_url(), approved_pdp_sp_count: 2, endorsed_pdp_sp_count: 1, active_pdp_sp_count: 2, @@ -308,27 +304,6 @@ impl Default for Config { } impl Config { - /// Get the default YugabyteDB download URL based on system architecture. - /// - /// Returns the appropriate YugabyteDB tarball URL for the current platform: - /// - el8-aarch64 for ARM64 systems (Apple Silicon, AWS Graviton, etc.) - /// - linux-x86_64 for x86_64 systems - fn get_default_yugabyte_url() -> String { - const YUGABYTE_VERSION: &str = "2.25.1.0"; - const YUGABYTE_BUILD: &str = "b381"; - - let arch_suffix = if std::env::consts::ARCH == "aarch64" { - "el8-aarch64" - } else { - "linux-x86_64" - }; - - format!( - "https://software.yugabyte.com/releases/{}/yugabyte-{}-{}-{}.tar.gz", - YUGABYTE_VERSION, YUGABYTE_VERSION, YUGABYTE_BUILD, arch_suffix - ) - } - /// Validate configuration values. /// /// Ensures that: diff --git a/src/constants.rs b/src/constants.rs index 9038496f..05a7bdfd 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -7,10 +7,24 @@ pub const LOTUS_DOCKER_IMAGE: &str = "foc-lotus"; pub const LOTUS_MINER_DOCKER_IMAGE: &str = "foc-lotus-miner"; pub const BUILDER_DOCKER_IMAGE: &str = "foc-builder"; -pub const YUGABYTE_DOCKER_IMAGE: &str = "foc-yugabyte"; pub const CURIO_DOCKER_IMAGE: &str = "foc-curio"; pub const PORTAINER_DOCKER_IMAGE: &str = "foc-portainer"; +/// Stock database images, pulled on demand by the database step (not built). +/// Curio's HarmonyDB speaks Postgres and its IndexStore speaks Cassandra/CQL; +/// Scylla serves the latter. +pub const POSTGRES_DOCKER_IMAGE: &str = "postgres:18"; +pub const SCYLLA_DOCKER_IMAGE: &str = "scylladb/scylla:2026.1"; + +/// Database container ports (inside the container) and shared credentials. +/// The database step provisions Postgres with these and the Curio env wiring +/// (start/curio/db_setup.rs) connects with them; they must agree. +pub const POSTGRES_CONTAINER_PORT: u16 = 5432; +pub const SCYLLA_CQL_CONTAINER_PORT: u16 = 9042; +pub const DB_USER: &str = "curio"; +pub const DB_PASSWORD: &str = "curio"; +pub const DB_NAME: &str = "curio"; + /// Required binaries for cluster startup pub const REQUIRED_BINARIES: &[&str] = &[ "lotus", @@ -22,43 +36,44 @@ pub const REQUIRED_BINARIES: &[&str] = &[ "sptool", ]; -/// Required Docker images for cluster startup +/// Images foc-devnet builds itself. Used to scope destructive cleanup to our own +/// images; stock images (postgres, scylla) are shared and must not be swept. +pub const FOC_BUILT_IMAGES: &[&str] = &[ + LOTUS_DOCKER_IMAGE, + LOTUS_MINER_DOCKER_IMAGE, + BUILDER_DOCKER_IMAGE, + CURIO_DOCKER_IMAGE, +]; + +/// foc-built images that must exist before a cluster can start. The stock +/// database images (postgres/scylla) are not listed: the database step pulls +/// them on demand. pub const REQUIRED_DOCKER_IMAGES: &[&str] = &[ LOTUS_DOCKER_IMAGE, LOTUS_MINER_DOCKER_IMAGE, BUILDER_DOCKER_IMAGE, - YUGABYTE_DOCKER_IMAGE, CURIO_DOCKER_IMAGE, ]; /// Check whether a Docker image identifier (optionally tagged, e.g. "foc-lotus:latest") -/// belongs to foc-devnet. Used to scope destructive cleanup to our own images -/// and avoid sweeping up unrelated images that happen to start with "foc-" -/// (e.g. foc-observer-*). +/// is one foc-devnet builds. Used to scope destructive cleanup to our own images +/// and avoid sweeping up unrelated images that share a "foc-" prefix (e.g. +/// foc-observer-*) or shared stock images (postgres, scylla). pub fn is_foc_devnet_image(image: &str) -> bool { let repo = image.split(':').next().unwrap_or(image); - matches!( - repo, - LOTUS_DOCKER_IMAGE - | LOTUS_MINER_DOCKER_IMAGE - | BUILDER_DOCKER_IMAGE - | YUGABYTE_DOCKER_IMAGE - | CURIO_DOCKER_IMAGE - ) + FOC_BUILT_IMAGES.contains(&repo) } /// Docker container names (base - will be prefixed with foc-c-- in practice) pub const LOTUS_CONTAINER: &str = "foc-lotus"; pub const LOTUS_MINER_CONTAINER: &str = "foc-lotus-miner"; pub const BUILDER_CONTAINER: &str = "foc-builder"; -pub const YUGABYTE_CONTAINER: &str = "foc-yugabyte"; pub const CURIO_CONTAINER: &str = "foc-curio"; pub const PORTAINER_CONTAINER: &str = "foc-portainer"; /// Port numbers pub const LOTUS_RPC_PORT: u16 = 1234; pub const LOTUS_MINER_API_PORT: u16 = 2345; -pub const YUGABYTE_PORT: u16 = 5433; pub const PORTAINER_PORT: u16 = 9009; /// Sleep durations (in seconds) @@ -125,7 +140,7 @@ mod tests { #[test] fn test_is_foc_devnet_image_accepts_known_images() { - for image in REQUIRED_DOCKER_IMAGES { + for image in FOC_BUILT_IMAGES { assert!(is_foc_devnet_image(image), "{} should match", image); assert!( is_foc_devnet_image(&format!("{}:latest", image)), @@ -142,7 +157,10 @@ mod tests { assert!(!is_foc_devnet_image("foc-observer-ponder-mainnet")); assert!(!is_foc_devnet_image("portainer/portainer-ce:latest")); assert!(!is_foc_devnet_image("foc-portainer")); + assert!(!is_foc_devnet_image("foc-yugabyte")); assert!(!is_foc_devnet_image("nginx")); + assert!(!is_foc_devnet_image("postgres:18")); + assert!(!is_foc_devnet_image("scylladb/scylla:latest")); assert!(!is_foc_devnet_image("")); } } diff --git a/src/docker/build.rs b/src/docker/build.rs index 1cc47bfe..be26acee 100644 --- a/src/docker/build.rs +++ b/src/docker/build.rs @@ -100,68 +100,6 @@ pub fn build_image_with_args( docker_command(&args) } -/// Build the YugabyteDB Docker image with special context handling. -pub fn build_yugabyte_image(name: &str) -> Result<(), Box> { - let image_tag = format!("foc-{}", name); - - if image_exists(&image_tag)? { - info!("Docker image {} already exists, skipping build", image_tag); - return Ok(()); - } - - validate_yugabyte_artifacts()?; - perform_yugabyte_build(name, &image_tag) -} - -/// Validate that YugabyteDB artifacts are available for building. -fn validate_yugabyte_artifacts() -> Result<(), Box> { - use crate::paths::foc_devnet_artifacts; - - let artifacts_dir = foc_devnet_artifacts(); - let yugabyte_dir = artifacts_dir.join("yugabyte"); - - if !yugabyte_dir.exists() { - return Err(format!( - "Yugabyte directory not found at {}. Please ensure artifacts are downloaded first.", - yugabyte_dir.display() - ) - .into()); - } - Ok(()) -} - -/// Perform the actual YugabyteDB image build process. -fn perform_yugabyte_build(name: &str, image_tag: &str) -> Result<(), Box> { - use crate::paths::foc_devnet_artifacts; - - let dockerfile_content = embedded_assets::get_dockerfile(name) - .ok_or_else(|| format!("Embedded Dockerfile not found for: {}", name))?; - - let artifacts_dir = foc_devnet_artifacts(); - - print_yugabyte_build_info(image_tag, &artifacts_dir); - - let pb = setup_build_progress_bar(image_tag); - let (uid, gid) = get_build_user_ids()?; - let temp_dockerfile_path = - create_temp_dockerfile(name, std::str::from_utf8(dockerfile_content)?)?; - - let result = - execute_yugabyte_build(&temp_dockerfile_path, image_tag, &artifacts_dir, &uid, &gid); - - let _ = fs::remove_file(&temp_dockerfile_path); - finalize_build_progress(&pb, image_tag, result) -} - -/// Print build information for YugabyteDB image. -fn print_yugabyte_build_info(image_tag: &str, artifacts_dir: &Path) { - info!( - "Building Docker image: {} from embedded Dockerfile (Yugabyte)", - image_tag - ); - info!("Using build context: {}", artifacts_dir.display()); -} - /// Set up progress bar for Docker build. fn setup_build_progress_bar(image_tag: &str) -> ProgressBar { let pb = ProgressBar::new_spinner(); @@ -191,28 +129,6 @@ fn create_temp_dockerfile( Ok(temp_path) } -/// Execute the YugabyteDB Docker build. -fn execute_yugabyte_build( - dockerfile_path: &Path, - image_tag: &str, - artifacts_dir: &Path, - uid: &str, - gid: &str, -) -> Result<(), Box> { - let build_args = vec![("USER_ID", uid), ("GROUP_ID", gid)]; - let output = build_image_with_args( - &dockerfile_path.to_string_lossy(), - image_tag, - &artifacts_dir.to_string_lossy(), - &build_args, - )?; - - if !output.status.success() { - return Err(format!("Failed to build Docker image: {}", image_tag).into()); - } - Ok(()) -} - /// Finalize build progress and report results. fn finalize_build_progress( pb: &ProgressBar, @@ -239,21 +155,15 @@ fn finalize_build_progress( /// - BUILDER_DOCKER_IMAGE (Foundry tools) /// - LOTUS_DOCKER_IMAGE (Filecoin daemon) /// - LOTUS_MINER_DOCKER_IMAGE (Filecoin miner) -/// - YUGABYTE_DOCKER_IMAGE (Database) /// - CURIO_DOCKER_IMAGE (Second-generation miner) pub fn build_and_cache_docker_images() -> Result<(), Box> { info!("Building and caching Docker images..."); - let images = ["builder", "lotus", "lotus-miner", "yugabyte", "curio"]; + let images = ["builder", "lotus", "lotus-miner", "curio"]; for image_name in &images { info!("Building image: foc-{}", image_name); - // Yugabyte requires special handling with artifacts directory as build context - if image_name == &"yugabyte" { - build_yugabyte_image(image_name)?; - } else { - build_image_from_embedded(image_name)?; - } + build_image_from_embedded(image_name)?; } info!("✓ All Docker images built and cached"); diff --git a/src/docker/containers.rs b/src/docker/containers.rs index 9d0dd2b3..d8d38b25 100644 --- a/src/docker/containers.rs +++ b/src/docker/containers.rs @@ -1,7 +1,10 @@ //! Container naming utilities for run-isolated clusters. //! //! This module provides functions to generate container names with run ID prefixes. -//! Format: foc-- (e.g., foc-251203-1246-thirsty-wolf-lotus) +//! Format: foc-- (e.g. foc-20260401T1530_ZanyPip-lotus) + +use regex::Regex; +use std::sync::LazyLock; /// Generate the Lotus container name for a run ID pub fn lotus_container_name(run_id: &str) -> String { @@ -18,9 +21,45 @@ pub fn builder_container_name(run_id: &str) -> String { format!("foc-{}-builder", run_id) } -/// Generate the YugabyteDB container name for a run ID -pub fn yugabyte_container_name(run_id: &str, sp_idx: usize) -> String { - format!("foc-{}-yugabyte-{}", run_id, sp_idx) +/// Generate the Postgres (HarmonyDB) container name for a run ID and SP index +pub fn postgres_container_name(run_id: &str, sp_idx: usize) -> String { + format!("foc-{}-postgres-{}", run_id, sp_idx) +} + +/// Generate the Scylla (IndexStore) container name for a run ID and SP index +pub fn scylla_container_name(run_id: &str, sp_idx: usize) -> String { + format!("foc-{}-scylla-{}", run_id, sp_idx) +} + +/// Per-SP database container names (Postgres + Scylla) for a run. These run +/// stock images, so image-based container discovery cannot identify them; +/// enumerate by name instead. Indices above the active SP count simply won't +/// exist and are skipped by callers. +pub fn db_container_names(run_id: &str) -> Vec { + (1..=crate::constants::MAX_PDP_SP_COUNT) + .flat_map(|sp| { + [ + postgres_container_name(run_id, sp), + scylla_container_name(run_id, sp), + ] + }) + .collect() +} + +/// Matches the per-SP database container naming scheme produced by +/// `postgres_container_name`/`scylla_container_name`, including the run-ID +/// shape (`YYYYMMDDTHHMM_Word`), so devnet DB containers can be identified +/// without knowing the run ID (they run stock images, invisible to image-based +/// discovery). The strict run-ID segment keeps unrelated compose containers +/// (e.g. `foc-observer-postgres-calibnet-1`) out of destructive sweeps. +static DEVNET_DB_CONTAINER_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^foc-\d{8}T\d{4}_[A-Za-z0-9]+-(postgres|scylla)-\d+$") + .expect("static regex pattern") +}); + +/// Check whether a container name is a devnet per-SP database container. +pub fn is_devnet_db_container_name(name: &str) -> bool { + DEVNET_DB_CONTAINER_RE.is_match(name) } /// Generate the Curio container name for a run ID @@ -32,3 +71,36 @@ pub fn curio_container_name(run_id: &str, sp_idx: usize) -> String { pub fn portainer_container_name(run_id: &str) -> String { format!("foc-{}-portainer", run_id) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_devnet_db_container_name_matches_generated_names() { + let run_id = "20260603T1838_HonkyBean"; + for sp in [1, 2, 5] { + assert!(is_devnet_db_container_name(&postgres_container_name( + run_id, sp + ))); + assert!(is_devnet_db_container_name(&scylla_container_name( + run_id, sp + ))); + } + } + + #[test] + fn test_is_devnet_db_container_name_rejects_unrelated() { + assert!(!is_devnet_db_container_name( + "foc-observer-postgres-calibnet-1" + )); + assert!(!is_devnet_db_container_name("foc-observer-postgres-1")); + assert!(!is_devnet_db_container_name("curio-test-pg")); + assert!(!is_devnet_db_container_name("curio-test-scylla")); + assert!(!is_devnet_db_container_name( + "foc-20260603T1838_HonkyBean-curio-1" + )); + assert!(!is_devnet_db_container_name("postgres")); + assert!(!is_devnet_db_container_name("")); + } +} diff --git a/src/docker/core.rs b/src/docker/core.rs index a457b310..a8d0f1f2 100644 --- a/src/docker/core.rs +++ b/src/docker/core.rs @@ -96,9 +96,11 @@ pub fn is_port_available(port: u16) -> bool { pub fn image_exists(image_name: &str) -> Result> { let output = docker_command(&["images", "--format", "{{.Repository}}:{{.Tag}}"])?; let stdout = String::from_utf8_lossy(&output.stdout); + // image_name may be tagged ("postgres:18") -> exact match, or untagged + // ("foc-lotus") -> match any tag. Ok(stdout .lines() - .any(|line| line.starts_with(&format!("{}:", image_name)))) + .any(|line| line == image_name || line.starts_with(&format!("{}:", image_name)))) } /// Check if a container with the given name exists diff --git a/src/docker/init.rs b/src/docker/init.rs index 45c3e92f..c001332e 100644 --- a/src/docker/init.rs +++ b/src/docker/init.rs @@ -21,7 +21,7 @@ struct VolumesMap { /// Create volume directories for all Docker images. pub fn create_volume_directories_for_images() -> Result<(), Box> { let volumes_base_dir = foc_devnet_docker_volumes(); - let volume_map_names = ["builder", "curio", "lotus-miner", "lotus", "yugabyte"]; + let volume_map_names = ["builder", "curio", "lotus-miner", "lotus"]; for image_name in volume_map_names { create_volumes_for_image_from_embedded(image_name, &volumes_base_dir)?; diff --git a/src/docker/logs.rs b/src/docker/logs.rs index b3120c83..693e6e70 100644 --- a/src/docker/logs.rs +++ b/src/docker/logs.rs @@ -8,6 +8,7 @@ //! ~/.foc-devnet/run//logs/..docker.log use crate::constants::is_foc_devnet_image; +use crate::docker::containers::is_devnet_db_container_name; use crate::docker::core::{docker_command, get_container_logs}; use crate::paths::foc_devnet_run_dir; use std::error::Error; @@ -23,10 +24,8 @@ pub struct ContainerInfo { pub status: String, } -/// List all containers (running or stopped) whose image repository is recognized as a -/// foc-devnet image. Matching is based on the repository name via `is_foc_devnet_image` -/// and does not distinguish between tags; unrelated repositories are excluded. -pub fn list_foc_devnet_containers() -> Result, Box> { +/// List all containers (running or stopped) with name, image, and status. +fn list_all_containers() -> Result, Box> { let output = docker_command(&["ps", "-a", "--format", "{{.Names}}|{{.Image}}|{{.Status}}"])?; let stdout = String::from_utf8_lossy(&output.stdout); @@ -34,22 +33,30 @@ pub fn list_foc_devnet_containers() -> Result, Box for line in stdout.lines() { let parts: Vec<&str> = line.split('|').collect(); if parts.len() >= 3 { - let name = parts[0].trim().to_string(); - let image = parts[1].trim().to_string(); - let status = parts[2].trim().to_string(); - if is_foc_devnet_image(&image) { - result.push(ContainerInfo { - name, - image, - status, - }); - } + result.push(ContainerInfo { + name: parts[0].trim().to_string(), + image: parts[1].trim().to_string(), + status: parts[2].trim().to_string(), + }); } } Ok(result) } -/// Persist logs for all foc-devnet containers under the run logs directory. +/// List all containers (running or stopped) belonging to foc-devnet: those whose +/// image repository is recognized via `is_foc_devnet_image`, plus the per-SP +/// database containers, which run stock images (postgres/scylla) and so are +/// matched structurally by name instead. Unrelated containers (including +/// stock-image ones like foc-observer's postgres) are excluded. +pub fn list_foc_devnet_containers() -> Result, Box> { + Ok(list_all_containers()? + .into_iter() + .filter(|c| is_foc_devnet_image(&c.image) || is_devnet_db_container_name(&c.name)) + .collect()) +} + +/// Persist logs for all foc-devnet containers (including the per-SP database +/// containers) under the run logs directory. pub fn persist_foc_container_logs(run_id: &str) -> Result<(), Box> { let containers = list_foc_devnet_containers()?; let logs_dir = foc_devnet_run_dir(run_id).join("logs"); @@ -80,7 +87,8 @@ pub fn persist_foc_container_logs(run_id: &str) -> Result<(), Box> { Ok(()) } -/// Remove all foc-devnet containers that are not running. +/// Remove all foc-devnet containers (including the per-SP database containers) +/// that are not running. pub fn remove_dead_foc_containers() -> Result<(), Box> { let containers = list_foc_devnet_containers()?; let mut removed_count = 0; diff --git a/src/docker/mod.rs b/src/docker/mod.rs index 67aaffb9..162c3f4d 100644 --- a/src/docker/mod.rs +++ b/src/docker/mod.rs @@ -23,16 +23,15 @@ pub use core::{ stop_container, wait_for_port, }; -pub use build::{ - build_docker_image, build_image_from_embedded, build_image_with_args, build_yugabyte_image, -}; +pub use build::{build_docker_image, build_image_from_embedded, build_image_with_args}; pub use command_logger::{ format_command, format_command_strings, log_command, log_command_strings, run_and_log_command, run_and_log_command_strings, }; pub use containers::{ - builder_container_name, curio_container_name, lotus_container_name, lotus_miner_container_name, - portainer_container_name, yugabyte_container_name, + builder_container_name, curio_container_name, db_container_names, lotus_container_name, + lotus_miner_container_name, portainer_container_name, postgres_container_name, + scylla_container_name, }; pub use init::{create_volume_directories_for_images, set_volume_ownership}; pub use logs::{ diff --git a/src/docker/network.rs b/src/docker/network.rs index db88ba59..07ff1fdb 100644 --- a/src/docker/network.rs +++ b/src/docker/network.rs @@ -107,7 +107,7 @@ pub fn delete_network(network_name: &str) -> Result<(), Box> { /// Networks created: /// - `foc--filecoin-net`: For Lotus daemon containers /// - `foc--porep-miner-net`: For Lotus miner containers (also connected to filecoin-net) -/// - `foc--pdp-miner-net`: For Curio and YugabyteDB containers (Curio also connects to filecoin-net) +/// - `foc--pdp-miner-net`: For Curio and its Postgres/Scylla containers (Curio also connects to filecoin-net) /// /// # Arguments /// * `run_id` - The run ID for this cluster diff --git a/src/embedded_assets.rs b/src/embedded_assets.rs index 0b4363fb..83796721 100644 --- a/src/embedded_assets.rs +++ b/src/embedded_assets.rs @@ -14,7 +14,6 @@ pub static DOCKERFILE_BUILDER: &[u8] = include_bytes!("../docker/builder/Dockerf pub static DOCKERFILE_CURIO: &[u8] = include_bytes!("../docker/curio/Dockerfile"); pub static DOCKERFILE_LOTUS: &[u8] = include_bytes!("../docker/lotus/Dockerfile"); pub static DOCKERFILE_LOTUS_MINER: &[u8] = include_bytes!("../docker/lotus-miner/Dockerfile"); -pub static DOCKERFILE_YUGABYTE: &[u8] = include_bytes!("../docker/yugabyte/Dockerfile"); // Volumes maps pub static BUILDER_VOLUMES_MAP: &[u8] = include_bytes!("../docker/builder/volumes_map.toml"); @@ -22,7 +21,6 @@ pub static CURIO_VOLUMES_MAP: &[u8] = include_bytes!("../docker/curio/volumes_ma pub static LOTUS_MINER_VOLUMES_MAP: &[u8] = include_bytes!("../docker/lotus-miner/volumes_map.toml"); pub static LOTUS_VOLUMES_MAP: &[u8] = include_bytes!("../docker/lotus/volumes_map.toml"); -pub static YUGABYTE_VOLUMES_MAP: &[u8] = include_bytes!("../docker/yugabyte/volumes_map.toml"); // MockUSDFC Foundry Project (as tar.gz archive) pub static MOCKUSDFC_ARCHIVE: &[u8] = include_bytes!("../artifacts/MockUSDFC.tar.gz"); @@ -60,7 +58,6 @@ pub fn get_dockerfile(name: &str) -> Option<&'static [u8]> { "curio" => Some(DOCKERFILE_CURIO), "lotus" => Some(DOCKERFILE_LOTUS), "lotus-miner" => Some(DOCKERFILE_LOTUS_MINER), - "yugabyte" => Some(DOCKERFILE_YUGABYTE), _ => None, } } @@ -72,7 +69,6 @@ pub fn get_volumes_map(name: &str) -> Option<&'static [u8]> { "curio" => Some(CURIO_VOLUMES_MAP), "lotus-miner" => Some(LOTUS_MINER_VOLUMES_MAP), "lotus" => Some(LOTUS_VOLUMES_MAP), - "yugabyte" => Some(YUGABYTE_VOLUMES_MAP), _ => None, } } diff --git a/src/external_api/devnet_info.rs b/src/external_api/devnet_info.rs index f9467196..f3658ecf 100644 --- a/src/external_api/devnet_info.rs +++ b/src/external_api/devnet_info.rs @@ -13,14 +13,16 @@ pub struct VersionedDevnetInfo { /// Schema version number. Increment when breaking changes are made. pub version: u32, /// The actual DevNet information payload. - pub info: DevnetInfoV1, + pub info: DevnetInfoV2, } -/// DevNet information schema version 1. +/// DevNet information schema version 2. /// /// Contains all relevant information about a running DevNet instance. +/// v2 replaced the per-SP `yugabyte` block with a `database` block +/// (Postgres + Scylla). #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DevnetInfoV1 { +pub struct DevnetInfoV2 { /// Unique identifier for this run (e.g., "26jan20-1622_GoofyNubs") pub run_id: String, /// ISO 8601 formatted start time @@ -127,19 +129,18 @@ pub struct CurioInfo { pub is_approved: bool, /// Whether this provider is endorsed in the Endorsements contract pub is_endorsed: bool, - /// YugabyteDB information for this provider - pub yugabyte: YugabyteInfo, + /// Database connection info (Postgres + Scylla) for this provider + pub database: DatabaseInfo, } -/// YugabyteDB connection information. +/// Database connection information for a provider's Curio. +/// +/// HarmonyDB uses the Postgres endpoint; the IndexStore uses the Scylla (CQL) +/// endpoint. Both ports are host-mapped for tooling and tests. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct YugabyteInfo { - /// Web UI URL exposed to host - pub web_ui_url: String, - /// Master RPC port - pub master_rpc_port: u16, - /// YSQL port for Postgres-compatible connections - pub ysql_port: u16, - /// YCQL port for Cassandra-compatible connections - pub ycql_port: u16, +pub struct DatabaseInfo { + /// Postgres (HarmonyDB) port exposed to host + pub postgres_port: u16, + /// Scylla (IndexStore, CQL) port exposed to host + pub scylla_port: u16, } diff --git a/src/external_api/export.rs b/src/external_api/export.rs index 203dbba4..ee76ae06 100644 --- a/src/external_api/export.rs +++ b/src/external_api/export.rs @@ -12,8 +12,8 @@ use crate::constants::USER_ACCOUNT_COUNT; use crate::crypto::derive_ethereum_key; use crate::crypto::mnemonic::load_mnemonic; use crate::external_api::{ - ContractsInfo, CurioInfo, DevnetInfoV1, LotusInfo, LotusMinerInfo, UserInfo, - VersionedDevnetInfo, YugabyteInfo, DEVNET_INFO_SCHEMA_VERSION, + ContractsInfo, CurioInfo, DatabaseInfo, DevnetInfoV2, LotusInfo, LotusMinerInfo, UserInfo, + VersionedDevnetInfo, DEVNET_INFO_SCHEMA_VERSION, }; use crate::paths; @@ -35,9 +35,9 @@ pub fn export_devnet_info(context: &SetupContext) -> Result<(), Box Result> { - Ok(DevnetInfoV1 { +/// Build DevnetInfoV2 from SetupContext. +fn build_devnet_info(ctx: &SetupContext) -> Result> { + Ok(DevnetInfoV2 { run_id: ctx.run_id().to_string(), start_time: Utc::now().to_rfc3339(), startup_duration: ctx @@ -242,7 +242,7 @@ fn build_single_pdp_service_provider( .and_then(|v| v.parse::().ok()) .unwrap_or(false); - let yugabyte = build_yugabyte_info(ctx, provider_id)?; + let database = build_database_info(ctx, provider_id)?; Ok(CurioInfo { provider_id, @@ -253,52 +253,34 @@ fn build_single_pdp_service_provider( container_name, is_approved, is_endorsed, - yugabyte, + database, }) } -/// Build YugabyteDB info for a provider. -fn build_yugabyte_info( +/// Build database (Postgres + Scylla) info for a provider. +fn build_database_info( ctx: &SetupContext, provider_id: u32, -) -> Result> { - let web_ui_port: u16 = ctx - .get(&format!("yugabyte_{}_web_ui_port", provider_id)) +) -> Result> { + let postgres_port: u16 = ctx + .get(&format!("db_{}_postgres_port", provider_id)) .and_then(|p| p.parse().ok()) .ok_or(format!( - "yugabyte_{}_web_ui_port not found or invalid in context", + "db_{}_postgres_port not found or invalid in context", provider_id ))?; - let master_rpc_port: u16 = ctx - .get(&format!("yugabyte_{}_master_rpc_port", provider_id)) + let scylla_port: u16 = ctx + .get(&format!("db_{}_scylla_port", provider_id)) .and_then(|p| p.parse().ok()) .ok_or(format!( - "yugabyte_{}_master_rpc_port not found or invalid in context", + "db_{}_scylla_port not found or invalid in context", provider_id ))?; - let ysql_port: u16 = ctx - .get(&format!("yugabyte_{}_ysql_port", provider_id)) - .and_then(|p| p.parse().ok()) - .ok_or(format!( - "yugabyte_{}_ysql_port not found or invalid in context", - provider_id - ))?; - - let ycql_port: u16 = ctx - .get(&format!("yugabyte_{}_ycql_port", provider_id)) - .and_then(|p| p.parse().ok()) - .ok_or(format!( - "yugabyte_{}_ycql_port not found or invalid in context", - provider_id - ))?; - - Ok(YugabyteInfo { - web_ui_url: format!("http://localhost:{}", web_ui_port), - master_rpc_port, - ysql_port, - ycql_port, + Ok(DatabaseInfo { + postgres_port, + scylla_port, }) } diff --git a/src/external_api/mod.rs b/src/external_api/mod.rs index 90b6e788..d9b7c150 100644 --- a/src/external_api/mod.rs +++ b/src/external_api/mod.rs @@ -8,10 +8,11 @@ mod devnet_info; mod export; pub use devnet_info::{ - ContractsInfo, CurioInfo, DevnetInfoV1, LotusInfo, LotusMinerInfo, UserInfo, - VersionedDevnetInfo, YugabyteInfo, + ContractsInfo, CurioInfo, DatabaseInfo, DevnetInfoV2, LotusInfo, LotusMinerInfo, UserInfo, + VersionedDevnetInfo, }; pub use export::export_devnet_info; -/// Current schema version for DevNet info export. -pub const DEVNET_INFO_SCHEMA_VERSION: u32 = 1; +/// Current schema version for DevNet info export. v2 replaced the per-SP +/// `yugabyte` block with a `database` block (Postgres + Scylla). +pub const DEVNET_INFO_SCHEMA_VERSION: u32 = 2; diff --git a/src/main.rs b/src/main.rs index 63b54b06..e2fd8f1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,8 +29,6 @@ fn main() -> Result<(), Box> { curio, lotus, filecoin_services, - yugabyte_url, - yugabyte_archive, proof_params_dir, rand, no_docker_build, @@ -38,8 +36,6 @@ fn main() -> Result<(), Box> { curio, lotus, filecoin_services, - yugabyte_url, - yugabyte_archive, proof_params_dir, rand, no_docker_build, diff --git a/src/main_app/command_handlers.rs b/src/main_app/command_handlers.rs index f72fc19c..1b742fea 100644 --- a/src/main_app/command_handlers.rs +++ b/src/main_app/command_handlers.rs @@ -31,13 +31,10 @@ pub fn handle_clean(all: bool, images: bool) -> Result<(), Box, lotus: Option, filecoin_services: Option, - yugabyte_url: Option, - yugabyte_archive: Option, proof_params_dir: Option, rand: bool, no_docker_build: bool, @@ -55,8 +52,6 @@ pub fn handle_init( curio_location: curio, lotus_location: lotus, filecoin_services_location: filecoin_services, - yugabyte_url, - yugabyte_archive, proof_params_dir, use_random_mnemonic: rand, no_docker_build, diff --git a/src/main_app/version.rs b/src/main_app/version.rs index 0e53c054..13e70b01 100644 --- a/src/main_app/version.rs +++ b/src/main_app/version.rs @@ -57,11 +57,6 @@ pub fn handle_version(notty: bool) -> Result<(), Box> { &default_config.filecoin_services, ); print_location_info(plain, "default:code:multicall3", &default_config.multicall3); - emit!( - plain, - "default:yugabyte: {}", - default_config.yugabyte_download_url - ); Ok(()) } diff --git a/src/paths.rs b/src/paths.rs index 7f1fc02f..16db8e9f 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -223,18 +223,6 @@ pub fn foc_devnet_curio_sp_volume(run_id: &str, sp_index: usize) -> PathBuf { foc_devnet_curio_volumes(run_id).join(sp_index.to_string()) } -/// Returns the path to the yugabyte volumes directory -/// e.g., ~/.foc-devnet/docker/volumes/run-specific//yugabyte -pub fn foc_devnet_yugabyte_volumes(run_id: &str) -> PathBuf { - foc_devnet_docker_volumes_run_specific(run_id).join("yugabyte") -} - -/// Returns the path to a specific yugabyte instance volume directory (base-1 indexed) -/// e.g., ~/.foc-devnet/docker/volumes/run-specific//yugabyte/1 -pub fn foc_devnet_yugabyte_sp_volume(run_id: &str, sp_index: usize) -> PathBuf { - foc_devnet_yugabyte_volumes(run_id).join(sp_index.to_string()) -} - /// Returns the path to the project root directory /// This is determined by finding the directory containing Cargo.toml pub fn project_root() -> Result { diff --git a/src/port_allocator.rs b/src/port_allocator.rs index baec84d3..661a76f1 100644 --- a/src/port_allocator.rs +++ b/src/port_allocator.rs @@ -1,8 +1,9 @@ //! Port allocation module for managing dynamic port assignment. //! //! This module provides a `PortAllocator` that manages a contiguous range of ports -//! for the devnet cluster. All components (Lotus, Lotus-Miner, Curio, Yugabyte) -//! dynamically allocate ports from this pool, ensuring no conflicts. +//! for the devnet cluster. All components (Lotus, Lotus-Miner, Curio, the +//! Postgres/Scylla databases) dynamically allocate ports from this pool, +//! ensuring no conflicts. use std::collections::HashSet; use std::error::Error;