From 51e394aa8787fc291667009ceaeef30b72619f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Sun, 1 Dec 2024 23:42:34 +0000 Subject: [PATCH 01/16] feat: making testnet script write a docker compose file --- spartan/releases/rough-rhino/validator.sh | 89 +++++++++++++---------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/spartan/releases/rough-rhino/validator.sh b/spartan/releases/rough-rhino/validator.sh index 98e81124227..3eb87c97c09 100755 --- a/spartan/releases/rough-rhino/validator.sh +++ b/spartan/releases/rough-rhino/validator.sh @@ -1,42 +1,57 @@ #!/bin/bash - set -eu -# get host arch +: "${P2P_PORT:?P2P_PORT is not set}" +: "${NODE_PORT:?NODE_PORT is not set}" +: "${VALIDATOR_PKEY:?VALIDATOR_PKEY is not set}" + ARCH=$(uname -m) -IMAGE="aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH}" +IMAGE=${IMAGE:-"aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH}"} + +cat > docker-compose.yml < Date: Sun, 1 Dec 2024 23:57:50 +0000 Subject: [PATCH 02/16] feat: making testnet script write a docker compose file --- spartan/releases/rough-rhino/validator.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spartan/releases/rough-rhino/validator.sh b/spartan/releases/rough-rhino/validator.sh index 3eb87c97c09..3dfb8bd16fc 100755 --- a/spartan/releases/rough-rhino/validator.sh +++ b/spartan/releases/rough-rhino/validator.sh @@ -5,6 +5,8 @@ set -eu : "${NODE_PORT:?NODE_PORT is not set}" : "${VALIDATOR_PKEY:?VALIDATOR_PKEY is not set}" +PUBLIC_IP=$(curl -s https://ipinfo.io/ip) + ARCH=$(uname -m) IMAGE=${IMAGE:-"aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH}"} From f8539d2afb2301c363b6f34e3278ddcd34e2f806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Mon, 2 Dec 2024 20:04:05 +0000 Subject: [PATCH 03/16] feat: going the extra mile with a cli tool --- spartan/releases/rough-rhino/.gitignore | 176 ++++++++ spartan/releases/rough-rhino/README.md | 33 ++ .../releases/rough-rhino/assets/banner.jpeg | Bin 0 -> 68390 bytes spartan/releases/rough-rhino/bun.lockb | Bin 0 -> 62186 bytes spartan/releases/rough-rhino/index.test.ts | 101 +++++ spartan/releases/rough-rhino/index.ts | 419 ++++++++++++++++++ spartan/releases/rough-rhino/lib.ts | 0 spartan/releases/rough-rhino/package.json | 30 ++ .../rough-rhino/{ => scripts}/full-node.sh | 0 spartan/releases/rough-rhino/validator.sh | 59 --- 10 files changed, 759 insertions(+), 59 deletions(-) create mode 100644 spartan/releases/rough-rhino/.gitignore create mode 100644 spartan/releases/rough-rhino/README.md create mode 100644 spartan/releases/rough-rhino/assets/banner.jpeg create mode 100755 spartan/releases/rough-rhino/bun.lockb create mode 100644 spartan/releases/rough-rhino/index.test.ts create mode 100644 spartan/releases/rough-rhino/index.ts create mode 100644 spartan/releases/rough-rhino/lib.ts create mode 100644 spartan/releases/rough-rhino/package.json rename spartan/releases/rough-rhino/{ => scripts}/full-node.sh (100%) delete mode 100755 spartan/releases/rough-rhino/validator.sh diff --git a/spartan/releases/rough-rhino/.gitignore b/spartan/releases/rough-rhino/.gitignore new file mode 100644 index 00000000000..23ce2843a4a --- /dev/null +++ b/spartan/releases/rough-rhino/.gitignore @@ -0,0 +1,176 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +docker-compose.yml diff --git a/spartan/releases/rough-rhino/README.md b/spartan/releases/rough-rhino/README.md new file mode 100644 index 00000000000..b13501b3caa --- /dev/null +++ b/spartan/releases/rough-rhino/README.md @@ -0,0 +1,33 @@ +# Aztec Spartan + +This tool helps easing the entry barrier to boot an Aztec Sequencer and Prover (S&P) Testnet. + +![Aztec Sparta Meme](./assets/banner.jpeg) + +For once, there's no rocket science here. This script does the following: + +- Checks for the presence of Docker in your machine +- Prompts you for some environment variables +- Outputs a templated docker-compose file with your variables +- Runs the docker compose file + +## Prerequisites + +This script should work in most UNIX-based machines. You should [have Node installed](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script). + +## Installation + +To configure a new node, create a new directory and run the install script: + +```bash +cd val1 +npx aztec-spartan install +``` + +If you don't have Docker installed, the script will do it for you. It will then prompt for any required environment variables and output a `docker-compose.yml` file and a `.env` file. + +You can run the command with `-h` to see all available options, and pass them as flags, i.e. `npx aztec-spartan install -p 8080 -p2p 40400 -n nameme`. + +## Running + +To spare you a few keystrokes, you can use `npx aztec-spartan [start/stop/logs/update]` to start, stop, output logs or pull the latest docker images. diff --git a/spartan/releases/rough-rhino/assets/banner.jpeg b/spartan/releases/rough-rhino/assets/banner.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e91ed867f600d25d3711709e4736e9b691c4b014 GIT binary patch literal 68390 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

~N!*vKtOF`r29<+<} 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 GKmXak4z2D0 literal 0 HcmV?d00001 diff --git a/spartan/releases/rough-rhino/bun.lockb b/spartan/releases/rough-rhino/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..f7b4d9fe83610852a15fe0f301bf7ced97b70f5e GIT binary patch literal 62186 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 { + // Clean up any existing files before each test + [DOCKER_COMPOSE_PATH, ENV_PATH].forEach((file) => { + if (existsSync(file)) { + unlinkSync(file); + } + }); +}); + +afterAll(() => { + // Clean up after all tests + [DOCKER_COMPOSE_PATH, ENV_PATH].forEach((file) => { + if (existsSync(file)) { + unlinkSync(file); + } + }); +}); + +describe("Test Suite", () => { + describe("CLI commands", () => { + beforeAll(() => { + // Mock axios response for IP + mockedAxios.mockResolvedValue({ data: { ip: "1.2.3.4" } }); + }); + + test("shows version", () => { + const output = execSync(`bun ${CLI_PATH} --version`).toString(); + expect(output).toContain("1.0.0"); + }); + + test("shows help", () => { + const output = execSync(`bun ${CLI_PATH} --help`).toString(); + expect(output).toContain("Aztec Testnet Node CLI"); + expect(output).toContain("Commands:"); + }); + + test("start command fails without configuration", () => { + try { + execSync(`bun ${CLI_PATH} start`, { + encoding: "utf8", + }); + } catch (error: any) { + expect(error.message).toContain( + 'Configuration not found. Please run "aztec-node init" first.' + ); + } + }); + }); + + describe("Install and Run", () => { + beforeAll(() => { + execSync( + `bun ${CLI_PATH} install -p 8080 -p2p 40400 -ip 7.7.7.7 -k 0x00 -n nameme`, + { + encoding: "utf8", + } + ); + }); + + test("install command creates necessary files", async () => { + // Check if files were created + expect(existsSync(ENV_PATH)).toBe(true); + expect(existsSync(DOCKER_COMPOSE_PATH)).toBe(true); + + // Verify .env content + const envContent = readFileSync(ENV_PATH, "utf8"); + expect(envContent).toContain("p2pPort=40400"); + expect(envContent).toContain("port=8080"); + expect(envContent).toContain("key=0x00"); + expect(envContent).toContain("ip=7.7.7.7"); + expect(envContent).toContain("name=nameme"); + + // Verify docker-compose.yml content + const composeContent = readFileSync(DOCKER_COMPOSE_PATH, "utf8"); + expect(composeContent).toContain("name: nameme"); + expect(composeContent).toContain(`P2P_UDP_ANNOUNCE_ADDR=7.7.7.7:40400`); + expect(composeContent).toContain("AZTEC_PORT=8080"); + }); + }); +}); diff --git a/spartan/releases/rough-rhino/index.ts b/spartan/releases/rough-rhino/index.ts new file mode 100644 index 00000000000..7d350418754 --- /dev/null +++ b/spartan/releases/rough-rhino/index.ts @@ -0,0 +1,419 @@ +#!/usr/bin/env node +import { Command } from "commander"; +import ora from "ora"; +import pino from "pino"; +import pretty from "pino-pretty"; +import axios from "axios"; +import { execSync } from "child_process"; +import { writeFileSync, readFileSync, existsSync } from "fs"; +import { join } from "path"; +import figlet from "figlet"; +import chalk from "chalk"; +import inquirer from "inquirer"; +import input from "@inquirer/input"; +import path from "path"; +const program = new Command(); + +// Global spinner instance used throughout the application +const spinner = ora({ color: "blue", discardStdin: false }); + +// Configure logging +const logger = pino({ + transport: { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "HH:MM:ss", + ignore: "pid,hostname", + }, + }, +}); + +// ASCII Art Banner +const showBanner = () => { + console.log( + chalk.blue( + figlet.textSync("Aztec Testnet", { + font: "Standard", + horizontalLayout: "full", + }) + ) + ); +}; + +// Check Docker Installation +const checkDocker = async () => { + try { + spinner.start("Checking Docker installation..."); + execSync("docker --version", { stdio: "ignore" }); + execSync("docker compose version", { stdio: "ignore" }); + spinner.succeed("Docker and Docker Compose are installed"); + return true; + } catch (error) { + spinner.fail("Docker or Docker Compose not found"); + spinner.stop(); + + const { install } = await inquirer.prompt([ + { + type: "confirm", + name: "install", + message: "Would you like to install Docker?", + default: true, + }, + ]); + + if (install) { + return await installDocker(); + } + return false; + } +}; + +// Install Docker +const installDocker = async () => { + try { + spinner.start("Installing Docker..."); + + // Docker installation script + execSync("curl -fsSL https://get.docker.com | sh"); + + // Add user to docker group + execSync("sudo usermod -aG docker $USER"); + + spinner.succeed("Docker installed successfully"); + spinner.stop(); + logger.info("Please log out and back in for group changes to take effect"); + return true; + } catch (error) { + spinner.fail("Failed to install Docker"); + spinner.stop(); + logger.error(error); + return false; + } +}; + +// Get Public IP +const getPublicIP = async () => { + try { + const { data } = await axios.get("https://api.ipify.org?format=json"); + return data.ip; + } catch (error) { + logger.error("Failed to get public IP"); + return null; + } +}; + +// Environment configuration +const defaultConfig = { + p2pPort: "40400", + port: "8080", + key: "0x0000000000000000000000000000000000000000000000000000000000000001", + ip: "8.8.8.8", + name: "validator-1", +}; + +const configureEnvironment = async (options: any) => { + try { + spinner.stopAndPersist({ text: "Configuring environment..." }); + // Get public IP first + spinner.start("Fetching public IP..."); + const publicIP = await getPublicIP(); + spinner.succeed(`Public IP: ${publicIP}`); + + // Load existing config + spinner.stopAndPersist({ text: "Loading configuration..." }); + const currentConfig = existsSync(".env") + ? Object.fromEntries( + readFileSync(".env", "utf8") + .split("\n") + .filter(Boolean) + .map((line) => line.split("=")) + ) + : {}; + + if (!options.name) { + options.name = await input({ + message: "Validator Name:", + default: currentConfig.name || defaultConfig.name, + }); + } + + if (!options.p2pPort) { + options.p2pPort = await input({ + message: "P2P Port:", + default: currentConfig.p2pPort || defaultConfig.p2pPort, + }); + } + + if (!options.port) { + options.port = await input({ + message: "Node Port:", + default: currentConfig.port || defaultConfig.port, + }); + } + + if (!options.key) { + options.key = await input({ + message: "Validator Private Key:", + required: true, + }); + } + + if (!options.ip) { + options.ip = await input({ + message: "Public IP:", + default: publicIP || defaultConfig.ip, + }); + } + + // Restart spinner for saving config + spinner.start("Saving configuration..."); + + const envContent = Object.entries(options) + .map(([key, value]) => `${key}=${value}`) + .join("\n"); + + writeFileSync(".env", envContent, { + encoding: "utf8", + flag: "w", + }); + + spinner.succeed("Environment configured successfully"); + + // Generate docker-compose.yml + spinner.start("Generating docker-compose configuration..."); + const composeConfig = ` +name: ${options.name} +services: + validator: + network_mode: host + restart: unless-stopped + env_file: + - .env + environment: + - P2P_UDP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} + - P2P_TCP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} + - COINBASE=0xbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - VALIDATOR_DISABLED=false + - VALIDATOR_PRIVATE_KEY=${options.key} + - SEQ_PUBLISHER_PRIVATE_KEY=${options.key} + - L1_PRIVATE_KEY=${options.key} + - DEBUG=aztec:*,-aztec:avm_simulator*,-aztec:circuits:artifact_hash,-aztec:libp2p_service,-json-rpc*,-aztec:world-state:database,-aztec:l2_block_stream* + - LOG_LEVEL=debug + - AZTEC_PORT=${options.port} + - P2P_ENABLED=true + - L1_CHAIN_ID=1337 + - PROVER_REAL_PROOFS=true + - PXE_PROVER_ENABLED=true + - ETHEREUM_SLOT_DURATION=12sec + - AZTEC_SLOT_DURATION=36 + - AZTEC_EPOCH_DURATION=32 + - AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS=13 + - ETHEREUM_HOST=http://35.221.3.35:8545 + - BOOTSTRAP_NODES=enr:-Jq4QKIJisajcICBVMoMwFtbmPgmHt3KoonypbBIQCAMNjhMc6DKW0J4vJzDpGPFUX7T2fzyyjezHgKKzeZY_DbRz_kGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCPdAyOJc2VjcDI1NmsxoQK92C7GObzDvCt9uwzW0lhKJKGCvOWkmAZjd2E2w-svuoN0Y3CCndCDdWRwgp3Q + - REGISTRY_CONTRACT_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3 + - GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 + - FEE_JUICE_CONTRACT_ADDRESS=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 + - ROLLUP_CONTRACT_ADDRESS=0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6 + - REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=0x5fc8d32690cc91d4c39d9d3abcbd16989f875707 + - GOVERNANCE_CONTRACT_ADDRESS=0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9 + - COIN_ISSUER_CONTRACT_ADDRESS=0xdc64a140aa3e981100a9beca4e685f962f0cf6c9 + - FEE_JUICE_PORTAL_CONTRACT_ADDRESS=0x0165878a594ca255338adfa4d48449f69242eb8f + - INBOX_CONTRACT_ADDRESS=0xed179b78d5781f93eb169730d8ad1be7313123f4 + - OUTBOX_CONTRACT_ADDRESS=0x1016b5aaa3270a65c315c664ecb238b6db270b64 + - P2P_UDP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} + - P2P_TCP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} + image: aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${process.arch} + command: start --node --archiver --sequencer +`; + writeFileSync("docker-compose.yml", composeConfig); + spinner.succeed( + "Docker compose file generated successfully, run `aztec-spartan start` to launch your node" + ); + return true; + } catch (error) { + spinner.fail("Failed to configure environment"); + logger.error(error); + return false; + } +}; + +// Docker commands +const dockerCommands = { + start: async () => { + try { + spinner.start("Starting containers..."); + const child = require("child_process").spawn( + "docker", + ["compose", "up", "-d"], + { + stdio: "inherit", + } + ); + + // Handle SIGINT (Ctrl+C) + process.on("SIGINT", () => { + child.kill("SIGINT"); + process.exit(0); + }); + + // Wait for the process to finish + await new Promise((resolve, reject) => { + child.on("exit", (code: number | null) => { + if (code === 0 || code === null) { + resolve(); + } else { + reject(new Error(`Process exited with code ${code}`)); + } + }); + child.on("error", reject); + }); + spinner.succeed("Containers started successfully"); + } catch (error) { + spinner.fail("Failed to start containers. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, + + stop: async () => { + try { + spinner.start("Stopping containers..."); + execSync("docker compose down"); + spinner.succeed("Containers stopped successfully"); + } catch (error) { + spinner.fail("Failed to stop containers. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, + + pull: async () => { + spinner.start("Pulling latest images..."); + try { + spinner.stop(); + execSync("docker compose pull"); + spinner.succeed("Images updated successfully"); + } catch (error) { + spinner.fail("Failed to pull images. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, + + logs: async () => { + try { + spinner.start("Fetching logs..."); + // Use spawn instead of execSync to handle SIGINT properly + const child = require("child_process").spawn( + "docker", + ["compose", "logs", "-f"], + { + stdio: "inherit", + } + ); + + // Handle SIGINT (Ctrl+C) + process.on("SIGINT", () => { + child.kill("SIGINT"); + process.exit(0); + }); + + // Wait for the process to finish + await new Promise((resolve, reject) => { + child.on("exit", (code: number | null) => { + if (code === 0 || code === null) { + resolve(); + } else { + reject(new Error(`Process exited with code ${code}`)); + } + }); + child.on("error", reject); + }); + } catch (error) { + spinner.fail("Failed to fetch logs. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, +}; + +// CLI Commands +program + .name("aztec testnet") + .description("Aztec Testnet Node CLI") + .version("1.0.0"); + +program + .command("install") + .option("-p, --port ", "Node port") + .option("-p2p, --p2p-port ", "P2P port") + .option("-ip, --ip ", "Public IP") + .option("-k, --key ", "Validator private key") + .option("-n, --name ", "Validator name") + .option("-d, --skip-docker", "Skip Docker installation") + .description("Install Aztec Testnet node configuration") + .action(async (options) => { + showBanner(); + + if (options.skipDocker) { + logger.warn("Skipping Docker installation"); + } else { + await checkDocker(); + } + + await configureEnvironment(options); + + logger.info( + 'Initialization complete! Use "aztec-spartan start" to launch your node.' + ); + process.exit(0); + }); + +program + .command("start") + .description("Start Aztec Testnet node") + .action(async () => { + if (!existsSync(".env")) { + console.error( + 'Configuration not found. Please run "aztec-spartan init" first.' + ); + process.exit(1); + } + await dockerCommands.start(); + process.exit(0); + }); + +program + .command("stop") + .description("Stop Aztec Testnet node") + .action(async () => { + await dockerCommands.stop(); + process.exit(0); + }); + +program + .command("update") + .description("Update Aztec Testnet node images") + .action(async () => { + await dockerCommands.pull(); + process.exit(0); + }); + +program + .command("logs") + .description("Show Aztec Testnet node logs") + .action(async () => { + await dockerCommands.logs(); + process.exit(0); + }); + +program.parse(); diff --git a/spartan/releases/rough-rhino/lib.ts b/spartan/releases/rough-rhino/lib.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spartan/releases/rough-rhino/package.json b/spartan/releases/rough-rhino/package.json new file mode 100644 index 00000000000..0b20271b409 --- /dev/null +++ b/spartan/releases/rough-rhino/package.json @@ -0,0 +1,30 @@ +{ + "name": "aztec-spartan", + "version": "1.0.0", + "type": "module", + "bin": "./index.ts", + "scripts": { + "test": "bun test", + "start": "bun index.ts" + }, + "dependencies": { + "@inquirer/input": "^4.0.2", + "@inquirer/password": "^4.0.2", + "@types/bun": "^1.1.14", + "axios": "^1.6.7", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "figlet": "^1.7.0", + "inquirer": "^9.2.15", + "ora": "^8.0.1", + "pino": "^9.5.0", + "pino-pretty": "^10.3.1" + }, + "devDependencies": { + "@inquirer/testing": "^2.1.37", + "@types/figlet": "^1.5.8", + "@types/inquirer": "^9.0.7", + "@types/jest": "^29.5.12", + "bun-types": "latest" + } +} diff --git a/spartan/releases/rough-rhino/full-node.sh b/spartan/releases/rough-rhino/scripts/full-node.sh similarity index 100% rename from spartan/releases/rough-rhino/full-node.sh rename to spartan/releases/rough-rhino/scripts/full-node.sh diff --git a/spartan/releases/rough-rhino/validator.sh b/spartan/releases/rough-rhino/validator.sh deleted file mode 100755 index 3dfb8bd16fc..00000000000 --- a/spartan/releases/rough-rhino/validator.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -set -eu - -: "${P2P_PORT:?P2P_PORT is not set}" -: "${NODE_PORT:?NODE_PORT is not set}" -: "${VALIDATOR_PKEY:?VALIDATOR_PKEY is not set}" - -PUBLIC_IP=$(curl -s https://ipinfo.io/ip) - -ARCH=$(uname -m) -IMAGE=${IMAGE:-"aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH}"} - -cat > docker-compose.yml < Date: Tue, 3 Dec 2024 11:20:20 +0000 Subject: [PATCH 04/16] feat: adding release-please --- .release-please-manifest.json | 3 +- spartan/releases/rough-rhino/.npmignore | 3 + spartan/releases/rough-rhino/Earthfile | 40 +++ spartan/releases/rough-rhino/bun.lockb | Bin 62186 -> 39884 bytes spartan/releases/rough-rhino/index.js | 354 +++++++++++++++++++++ spartan/releases/rough-rhino/index.test.ts | 14 +- spartan/releases/rough-rhino/index.ts | 40 +-- spartan/releases/rough-rhino/lib.ts | 0 spartan/releases/rough-rhino/package.json | 29 +- spartan/releases/rough-rhino/tsconfig.json | 27 ++ 10 files changed, 453 insertions(+), 57 deletions(-) create mode 100644 spartan/releases/rough-rhino/.npmignore create mode 100644 spartan/releases/rough-rhino/Earthfile create mode 100755 spartan/releases/rough-rhino/index.js delete mode 100644 spartan/releases/rough-rhino/lib.ts create mode 100644 spartan/releases/rough-rhino/tsconfig.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fe3c2693b52..de1df840d1a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -3,5 +3,6 @@ "yarn-project/cli": "0.35.1", "yarn-project/aztec": "0.65.2", "barretenberg": "0.65.2", - "barretenberg/ts": "0.65.2" + "barretenberg/ts": "0.65.2", + "spartan/releases/rough-rhino": "1.0.0" } diff --git a/spartan/releases/rough-rhino/.npmignore b/spartan/releases/rough-rhino/.npmignore new file mode 100644 index 00000000000..405737df2f9 --- /dev/null +++ b/spartan/releases/rough-rhino/.npmignore @@ -0,0 +1,3 @@ +node_modules +bun.lockb +*.ts diff --git a/spartan/releases/rough-rhino/Earthfile b/spartan/releases/rough-rhino/Earthfile new file mode 100644 index 00000000000..3074957ef13 --- /dev/null +++ b/spartan/releases/rough-rhino/Earthfile @@ -0,0 +1,40 @@ +VERSION 0.7 + +FROM ubuntu:22.04 +WORKDIR /app + +deps: + RUN apt-get update && apt-get install -y \ + curl \ + git \ + make \ + nodejs \ + npm \ + unzip + + RUN curl -fsSL https://bun.sh/install | bash + ENV PATH="/root/.bun/bin:${PATH}" + + COPY package.json . + COPY bun.lockb . + + RUN bun install + +test-install: + FROM +deps + + COPY index.ts . + + RUN yes | bun index.ts install -p 8080 -p2p 40400 -ip 7.7.7.7 -k 0x00 -n test-node + + +test-functionality: + FROM +deps + + COPY index.ts . + COPY index.test.ts . + + RUN curl -fsSL https://get.docker.com | sh + + RUN bun test + diff --git a/spartan/releases/rough-rhino/bun.lockb b/spartan/releases/rough-rhino/bun.lockb index f7b4d9fe83610852a15fe0f301bf7ced97b70f5e..6c7739f72eef14d30304233ba19ea168eafcdaf8 100755 GIT binary patch delta 7946 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 delta 21415 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 Wo2G^45ga`SAtlOaBY16Z~}m diff --git a/spartan/releases/rough-rhino/index.js b/spartan/releases/rough-rhino/index.js new file mode 100755 index 00000000000..c7ed447ef7b --- /dev/null +++ b/spartan/releases/rough-rhino/index.js @@ -0,0 +1,354 @@ +#!/usr/bin/env node +import { Command } from "commander"; +import ora from "ora"; +import axios from "axios"; +import { execSync } from "child_process"; +import { writeFileSync, readFileSync, existsSync } from "fs"; +import figlet from "figlet"; +import chalk from "chalk"; +import inquirer from "inquirer"; +import input from "@inquirer/input"; +const program = new Command(); +// Global spinner instance used throughout the application +const spinner = ora({ color: "blue", discardStdin: false }); +const logger = require("pino")(); +// ASCII Art Banner +const showBanner = () => { + console.log(chalk.blue(figlet.textSync("Aztec Testnet", { + font: "Standard", + horizontalLayout: "full", + }))); +}; +// Check Docker Installation +const checkDocker = async () => { + try { + spinner.start("Checking Docker installation..."); + execSync("docker --version", { stdio: "ignore" }); + execSync("docker compose version", { stdio: "ignore" }); + spinner.succeed("Docker and Docker Compose are installed"); + return true; + } + catch (error) { + spinner.fail("Docker or Docker Compose not found"); + spinner.stop(); + const { install } = await inquirer.prompt([ + { + type: "confirm", + name: "install", + message: "Would you like to install Docker?", + default: true, + }, + ]); + if (install) { + return await installDocker(); + } + return false; + } +}; +// Install Docker +const installDocker = async () => { + try { + spinner.start("Installing Docker..."); + // Docker installation script + execSync("curl -fsSL https://get.docker.com | sh"); + // Add user to docker group + execSync("sudo usermod -aG docker $USER"); + spinner.succeed("Docker installed successfully"); + spinner.stop(); + logger.info("Please log out and back in for group changes to take effect"); + return true; + } + catch (error) { + spinner.fail("Failed to install Docker"); + spinner.stop(); + logger.error(error); + return false; + } +}; +// Get Public IP +const getPublicIP = async () => { + try { + const { data } = await axios.get("https://api.ipify.org?format=json"); + return data.ip; + } + catch (error) { + logger.error("Failed to get public IP"); + return null; + } +}; +// Environment configuration +const defaultConfig = { + p2pPort: "40400", + port: "8080", + key: "0x0000000000000000000000000000000000000000000000000000000000000001", + ip: "8.8.8.8", + name: "validator-1", +}; +const configureEnvironment = async (options) => { + try { + spinner.stopAndPersist({ text: "Configuring environment..." }); + // Get public IP first + spinner.start("Fetching public IP..."); + const publicIP = await getPublicIP(); + spinner.succeed(`Public IP: ${publicIP}`); + // Load existing config + spinner.stopAndPersist({ text: "Loading configuration..." }); + const currentConfig = existsSync(".env") + ? Object.fromEntries(readFileSync(".env", "utf8") + .split("\n") + .filter(Boolean) + .map((line) => line.split("="))) + : {}; + if (!options.name) { + options.name = await input({ + message: "Validator Name:", + default: currentConfig.name || defaultConfig.name, + }); + } + if (!options.p2pPort) { + options.p2pPort = await input({ + message: "P2P Port:", + default: currentConfig.p2pPort || defaultConfig.p2pPort, + }); + } + if (!options.port) { + options.port = await input({ + message: "Node Port:", + default: currentConfig.port || defaultConfig.port, + }); + } + if (!options.key) { + options.key = await input({ + message: "Validator Private Key:", + required: true, + }); + } + if (!options.ip) { + options.ip = await input({ + message: "Public IP:", + default: publicIP || defaultConfig.ip, + }); + } + // Restart spinner for saving config + spinner.start("Saving configuration..."); + const envContent = Object.entries(options) + .map(([key, value]) => `${key}=${value}`) + .join("\n"); + writeFileSync(".env", envContent, { + encoding: "utf8", + flag: "w", + }); + spinner.succeed("Environment configured successfully"); + // Generate docker-compose.yml + spinner.start("Generating docker-compose configuration..."); + const composeConfig = ` +name: ${options.name} +services: + validator: + network_mode: host + restart: unless-stopped + env_file: + - .env + environment: + - P2P_UDP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} + - P2P_TCP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} + - COINBASE=0xbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - VALIDATOR_DISABLED=false + - VALIDATOR_PRIVATE_KEY=${options.key} + - SEQ_PUBLISHER_PRIVATE_KEY=${options.key} + - L1_PRIVATE_KEY=${options.key} + - DEBUG=aztec:*,-aztec:avm_simulator*,-aztec:circuits:artifact_hash,-aztec:libp2p_service,-json-rpc*,-aztec:world-state:database,-aztec:l2_block_stream* + - LOG_LEVEL=debug + - AZTEC_PORT=${options.port} + - P2P_ENABLED=true + - L1_CHAIN_ID=1337 + - PROVER_REAL_PROOFS=true + - PXE_PROVER_ENABLED=true + - ETHEREUM_SLOT_DURATION=12sec + - AZTEC_SLOT_DURATION=36 + - AZTEC_EPOCH_DURATION=32 + - AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS=13 + - ETHEREUM_HOST=http://35.221.3.35:8545 + - BOOTSTRAP_NODES=enr:-Jq4QKIJisajcICBVMoMwFtbmPgmHt3KoonypbBIQCAMNjhMc6DKW0J4vJzDpGPFUX7T2fzyyjezHgKKzeZY_DbRz_kGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCPdAyOJc2VjcDI1NmsxoQK92C7GObzDvCt9uwzW0lhKJKGCvOWkmAZjd2E2w-svuoN0Y3CCndCDdWRwgp3Q + - REGISTRY_CONTRACT_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3 + - GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 + - FEE_JUICE_CONTRACT_ADDRESS=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 + - ROLLUP_CONTRACT_ADDRESS=0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6 + - REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=0x5fc8d32690cc91d4c39d9d3abcbd16989f875707 + - GOVERNANCE_CONTRACT_ADDRESS=0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9 + - COIN_ISSUER_CONTRACT_ADDRESS=0xdc64a140aa3e981100a9beca4e685f962f0cf6c9 + - FEE_JUICE_PORTAL_CONTRACT_ADDRESS=0x0165878a594ca255338adfa4d48449f69242eb8f + - INBOX_CONTRACT_ADDRESS=0xed179b78d5781f93eb169730d8ad1be7313123f4 + - OUTBOX_CONTRACT_ADDRESS=0x1016b5aaa3270a65c315c664ecb238b6db270b64 + - P2P_UDP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} + - P2P_TCP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} + image: aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${process.arch} + command: start --node --archiver --sequencer +`; + writeFileSync("docker-compose.yml", composeConfig); + spinner.succeed("Docker compose file generated successfully, run `aztec-spartan start` to launch your node"); + return true; + } + catch (error) { + spinner.fail("Failed to configure environment"); + logger.error(error); + return false; + } +}; +// Docker commands +const dockerCommands = { + start: async () => { + try { + spinner.start("Starting containers..."); + const child = require("child_process").spawn("docker", ["compose", "up", "-d"], { + stdio: "inherit", + }); + // Handle SIGINT (Ctrl+C) + process.on("SIGINT", () => { + child.kill("SIGINT"); + process.exit(0); + }); + // Wait for the process to finish + await new Promise((resolve, reject) => { + child.on("exit", (code) => { + if (code === 0 || code === null) { + resolve(); + } + else { + reject(new Error(`Process exited with code ${code}`)); + } + }); + child.on("error", reject); + }); + spinner.succeed("Containers started successfully"); + } + catch (error) { + spinner.fail("Failed to start containers. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, + stop: async () => { + try { + spinner.start("Stopping containers..."); + execSync("docker compose down"); + spinner.succeed("Containers stopped successfully"); + } + catch (error) { + spinner.fail("Failed to stop containers. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, + pull: async () => { + spinner.start("Pulling latest images..."); + try { + spinner.stop(); + execSync("docker compose pull"); + spinner.succeed("Images updated successfully"); + } + catch (error) { + spinner.fail("Failed to pull images. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, + logs: async () => { + try { + spinner.start("Fetching logs..."); + // Use spawn instead of execSync to handle SIGINT properly + const child = require("child_process").spawn("docker", ["compose", "logs", "-f"], { + stdio: "inherit", + }); + // Handle SIGINT (Ctrl+C) + process.on("SIGINT", () => { + child.kill("SIGINT"); + process.exit(0); + }); + // Wait for the process to finish + await new Promise((resolve, reject) => { + child.on("exit", (code) => { + if (code === 0 || code === null) { + resolve(); + } + else { + reject(new Error(`Process exited with code ${code}`)); + } + }); + child.on("error", reject); + }); + } + catch (error) { + spinner.fail("Failed to fetch logs. Is Docker running?"); + if (error instanceof Error) { + const message = error.message.split("\n")[0]; + logger.error(message); + } + } + }, +}; +// CLI Commands +program + .name("aztec testnet") + .description("Aztec Testnet Node CLI") + .version("1.0.0"); +program + .command("install") + .option("-p, --port ", "Node port") + .option("-p2p, --p2p-port ", "P2P port") + .option("-ip, --ip ", "Public IP") + .option("-k, --key ", "Validator private key") + .option("-n, --name ", "Validator name") + .option("-d, --skip-docker", "Skip Docker installation") + .description("Install Aztec Testnet node configuration") + .action(async (options) => { + showBanner(); + if (options.skipDocker) { + logger.warn("Skipping Docker installation"); + } + else { + await checkDocker(); + } + await configureEnvironment(options); + logger.info('Initialization complete! Use "aztec-spartan start" to launch your node.'); + process.exit(0); +}); +program + .command("start") + .description("Start Aztec Testnet node") + .action(async () => { + if (!existsSync(".env")) { + console.error('Configuration not found. Please run "aztec-spartan init" first.'); + process.exit(1); + } + await dockerCommands.start(); + process.exit(0); +}); +program + .command("stop") + .description("Stop Aztec Testnet node") + .action(async () => { + await dockerCommands.stop(); + process.exit(0); +}); +program + .command("update") + .description("Update Aztec Testnet node images") + .action(async () => { + await dockerCommands.pull(); + process.exit(0); +}); +program + .command("logs") + .description("Show Aztec Testnet node logs") + .action(async () => { + await dockerCommands.logs(); + process.exit(0); +}); +program.parse(); diff --git a/spartan/releases/rough-rhino/index.test.ts b/spartan/releases/rough-rhino/index.test.ts index dc846a0132d..63cc49a29aa 100644 --- a/spartan/releases/rough-rhino/index.test.ts +++ b/spartan/releases/rough-rhino/index.test.ts @@ -1,13 +1,5 @@ -import { - describe, - expect, - test, - beforeAll, - afterAll, - beforeEach, - mock, -} from "bun:test"; -import { execSync, spawnSync } from "child_process"; +import { describe, expect, test, beforeAll, afterAll, mock } from "bun:test"; +import { execSync } from "child_process"; import { existsSync, unlinkSync, readFileSync } from "fs"; import { join } from "path"; import axios from "axios"; @@ -62,7 +54,7 @@ describe("Test Suite", () => { }); } catch (error: any) { expect(error.message).toContain( - 'Configuration not found. Please run "aztec-node init" first.' + 'Configuration not found. Please run "aztec-spartan init" first.' ); } }); diff --git a/spartan/releases/rough-rhino/index.ts b/spartan/releases/rough-rhino/index.ts index 7d350418754..35c47b694da 100644 --- a/spartan/releases/rough-rhino/index.ts +++ b/spartan/releases/rough-rhino/index.ts @@ -1,34 +1,18 @@ #!/usr/bin/env node import { Command } from "commander"; import ora from "ora"; -import pino from "pino"; -import pretty from "pino-pretty"; import axios from "axios"; import { execSync } from "child_process"; import { writeFileSync, readFileSync, existsSync } from "fs"; -import { join } from "path"; import figlet from "figlet"; import chalk from "chalk"; import inquirer from "inquirer"; import input from "@inquirer/input"; -import path from "path"; const program = new Command(); // Global spinner instance used throughout the application const spinner = ora({ color: "blue", discardStdin: false }); -// Configure logging -const logger = pino({ - transport: { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "HH:MM:ss", - ignore: "pid,hostname", - }, - }, -}); - // ASCII Art Banner const showBanner = () => { console.log( @@ -82,12 +66,12 @@ const installDocker = async () => { spinner.succeed("Docker installed successfully"); spinner.stop(); - logger.info("Please log out and back in for group changes to take effect"); + spinner.info("Please log out and back in for group changes to take effect"); return true; - } catch (error) { + } catch (error: any) { spinner.fail("Failed to install Docker"); spinner.stop(); - logger.error(error); + spinner.fail(error.message); return false; } }; @@ -98,7 +82,7 @@ const getPublicIP = async () => { const { data } = await axios.get("https://api.ipify.org?format=json"); return data.ip; } catch (error) { - logger.error("Failed to get public IP"); + spinner.warn("Failed to get public IP"); return null; } }; @@ -231,9 +215,9 @@ services: "Docker compose file generated successfully, run `aztec-spartan start` to launch your node" ); return true; - } catch (error) { + } catch (error: any) { spinner.fail("Failed to configure environment"); - logger.error(error); + spinner.fail(error); return false; } }; @@ -273,7 +257,7 @@ const dockerCommands = { spinner.fail("Failed to start containers. Is Docker running?"); if (error instanceof Error) { const message = error.message.split("\n")[0]; - logger.error(message); + spinner.fail(message); } } }, @@ -287,7 +271,7 @@ const dockerCommands = { spinner.fail("Failed to stop containers. Is Docker running?"); if (error instanceof Error) { const message = error.message.split("\n")[0]; - logger.error(message); + spinner.fail(message); } } }, @@ -302,7 +286,7 @@ const dockerCommands = { spinner.fail("Failed to pull images. Is Docker running?"); if (error instanceof Error) { const message = error.message.split("\n")[0]; - logger.error(message); + spinner.fail(message); } } }, @@ -340,7 +324,7 @@ const dockerCommands = { spinner.fail("Failed to fetch logs. Is Docker running?"); if (error instanceof Error) { const message = error.message.split("\n")[0]; - logger.error(message); + spinner.fail(message); } } }, @@ -365,14 +349,14 @@ program showBanner(); if (options.skipDocker) { - logger.warn("Skipping Docker installation"); + spinner.warn("Skipping Docker installation"); } else { await checkDocker(); } await configureEnvironment(options); - logger.info( + spinner.info( 'Initialization complete! Use "aztec-spartan start" to launch your node.' ); process.exit(0); diff --git a/spartan/releases/rough-rhino/lib.ts b/spartan/releases/rough-rhino/lib.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/spartan/releases/rough-rhino/package.json b/spartan/releases/rough-rhino/package.json index 0b20271b409..a82447249f1 100644 --- a/spartan/releases/rough-rhino/package.json +++ b/spartan/releases/rough-rhino/package.json @@ -1,30 +1,25 @@ { - "name": "aztec-spartan", - "version": "1.0.0", + "name": "rough-rhino", + "module": "index.ts", "type": "module", - "bin": "./index.ts", - "scripts": { - "test": "bun test", - "start": "bun index.ts" + "version": "1.0.0", + "devDependencies": { + "@types/bun": "latest", + "@types/figlet": "^1.5.8", + "@types/inquirer": "^9.0.7", + "@types/node": "^22.10.1" + }, + "peerDependencies": { + "typescript": "^5.0.0" }, "dependencies": { "@inquirer/input": "^4.0.2", - "@inquirer/password": "^4.0.2", - "@types/bun": "^1.1.14", "axios": "^1.6.7", "chalk": "^5.3.0", "commander": "^12.1.0", "figlet": "^1.7.0", "inquirer": "^9.2.15", "ora": "^8.0.1", - "pino": "^9.5.0", - "pino-pretty": "^10.3.1" - }, - "devDependencies": { - "@inquirer/testing": "^2.1.37", - "@types/figlet": "^1.5.8", - "@types/inquirer": "^9.0.7", - "@types/jest": "^29.5.12", - "bun-types": "latest" + "pino": "^9.5.0" } } diff --git a/spartan/releases/rough-rhino/tsconfig.json b/spartan/releases/rough-rhino/tsconfig.json new file mode 100644 index 00000000000..f063633934c --- /dev/null +++ b/spartan/releases/rough-rhino/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true + } +} From f604ae6790af7e6accd544051c927deba61197d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 11:32:12 +0000 Subject: [PATCH 05/16] feat: adding tests to CI, fingers crossed --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f4095c671f..8f876494190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -875,6 +875,17 @@ jobs: timeout-minutes: 40 run: earthly-ci -P --no-output +test --box=${{ matrix.box }} --browser=${{ matrix.browser }} --mode=cache + rough-rhino-installer: + needs: [configure] + runs-on: ${{ needs.configure.outputs.username }}-x86 + steps: + - name: Rough Rhino Docker Installer + working-directory: ./spartan/releases/rough-rhino + run: earthly-ci +test-install + - name: Rough Rhino Installer + working-directory: ./spartan/releases/rough-rhino + run: earthly-ci +test-functionality + protocol-circuits-gates-report: needs: [build, configure] if: needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true' From 415e6b9149eab509a38c1410c912044c9c086729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 12:43:57 +0000 Subject: [PATCH 06/16] feat: adding tests to CI, fingers crossed --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f876494190..ae2d1b24a53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -880,10 +880,10 @@ jobs: runs-on: ${{ needs.configure.outputs.username }}-x86 steps: - name: Rough Rhino Docker Installer - working-directory: ./spartan/releases/rough-rhino + working-directory: spartan/releases/rough-rhino run: earthly-ci +test-install - name: Rough Rhino Installer - working-directory: ./spartan/releases/rough-rhino + working-directory: spartan/releases/rough-rhino run: earthly-ci +test-functionality protocol-circuits-gates-report: From 69080294f50c67cb5991122ad2fd34de7fd9008e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 12:50:04 +0000 Subject: [PATCH 07/16] feat: adding tests to CI, fingers crossed --- .github/workflows/ci.yml | 6 ++++-- spartan/releases/rough-rhino/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae2d1b24a53..5204e564caf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -879,11 +879,13 @@ jobs: needs: [configure] runs-on: ${{ needs.configure.outputs.username }}-x86 steps: + - uses: actions/checkout@v4 + with: { ref: "${{ github.event.pull_request.head.sha }}" } - name: Rough Rhino Docker Installer - working-directory: spartan/releases/rough-rhino + working-directory: ./spartan/releases/rough-rhino run: earthly-ci +test-install - name: Rough Rhino Installer - working-directory: spartan/releases/rough-rhino + working-directory: ./spartan/releases/rough-rhino run: earthly-ci +test-functionality protocol-circuits-gates-report: diff --git a/spartan/releases/rough-rhino/index.ts b/spartan/releases/rough-rhino/index.ts index 35c47b694da..4359b5af0b7 100644 --- a/spartan/releases/rough-rhino/index.ts +++ b/spartan/releases/rough-rhino/index.ts @@ -193,8 +193,8 @@ services: - AZTEC_SLOT_DURATION=36 - AZTEC_EPOCH_DURATION=32 - AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS=13 - - ETHEREUM_HOST=http://35.221.3.35:8545 - - BOOTSTRAP_NODES=enr:-Jq4QKIJisajcICBVMoMwFtbmPgmHt3KoonypbBIQCAMNjhMc6DKW0J4vJzDpGPFUX7T2fzyyjezHgKKzeZY_DbRz_kGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCPdAyOJc2VjcDI1NmsxoQK92C7GObzDvCt9uwzW0lhKJKGCvOWkmAZjd2E2w-svuoN0Y3CCndCDdWRwgp3Q + - ETHEREUM_HOST=http://34.48.76.131:8545 + - BOOTSTRAP_NODES=enr:-Jq4QO_3szmgtG2cbEdnFDIhpGAQkc1HwfNy4-M6sG9QmQbPTmp9PMOHR3xslfR23hORiU-GpA7uM9uXw49lFcnuuvYGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCIwTIOJc2VjcDI1NmsxoQKQTN17XKCwjYSSwmTc-6YzCMhd3v6Ofl8TS-WunX6LCoN0Y3CCndCDdWRwgp3Q - REGISTRY_CONTRACT_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3 - GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 - FEE_JUICE_CONTRACT_ADDRESS=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 From 239bdf58ed903ce949014fd1a48887765d56afc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 12:57:52 +0000 Subject: [PATCH 08/16] feat: adding tests to CI, fingers crossed --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5204e564caf..6678a63d7f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -881,6 +881,9 @@ jobs: steps: - uses: actions/checkout@v4 with: { ref: "${{ github.event.pull_request.head.sha }}" } + - uses: ./.github/ci-setup-action + with: + concurrency_key: rough-rhino-installer - name: Rough Rhino Docker Installer working-directory: ./spartan/releases/rough-rhino run: earthly-ci +test-install From 0a70a2b3f1158882c819d281e19be12be8e128b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 17:29:38 +0000 Subject: [PATCH 09/16] feat: removing the whole .env file --- spartan/releases/rough-rhino/.gitignore | 1 + spartan/releases/rough-rhino/index.js | 354 --------------------- spartan/releases/rough-rhino/index.test.ts | 29 +- spartan/releases/rough-rhino/index.ts | 43 +-- spartan/releases/rough-rhino/package.json | 6 + 5 files changed, 27 insertions(+), 406 deletions(-) delete mode 100755 spartan/releases/rough-rhino/index.js diff --git a/spartan/releases/rough-rhino/.gitignore b/spartan/releases/rough-rhino/.gitignore index 23ce2843a4a..a4f916d4dd7 100644 --- a/spartan/releases/rough-rhino/.gitignore +++ b/spartan/releases/rough-rhino/.gitignore @@ -174,3 +174,4 @@ dist # Finder (MacOS) folder config .DS_Store docker-compose.yml +index.js diff --git a/spartan/releases/rough-rhino/index.js b/spartan/releases/rough-rhino/index.js deleted file mode 100755 index c7ed447ef7b..00000000000 --- a/spartan/releases/rough-rhino/index.js +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/env node -import { Command } from "commander"; -import ora from "ora"; -import axios from "axios"; -import { execSync } from "child_process"; -import { writeFileSync, readFileSync, existsSync } from "fs"; -import figlet from "figlet"; -import chalk from "chalk"; -import inquirer from "inquirer"; -import input from "@inquirer/input"; -const program = new Command(); -// Global spinner instance used throughout the application -const spinner = ora({ color: "blue", discardStdin: false }); -const logger = require("pino")(); -// ASCII Art Banner -const showBanner = () => { - console.log(chalk.blue(figlet.textSync("Aztec Testnet", { - font: "Standard", - horizontalLayout: "full", - }))); -}; -// Check Docker Installation -const checkDocker = async () => { - try { - spinner.start("Checking Docker installation..."); - execSync("docker --version", { stdio: "ignore" }); - execSync("docker compose version", { stdio: "ignore" }); - spinner.succeed("Docker and Docker Compose are installed"); - return true; - } - catch (error) { - spinner.fail("Docker or Docker Compose not found"); - spinner.stop(); - const { install } = await inquirer.prompt([ - { - type: "confirm", - name: "install", - message: "Would you like to install Docker?", - default: true, - }, - ]); - if (install) { - return await installDocker(); - } - return false; - } -}; -// Install Docker -const installDocker = async () => { - try { - spinner.start("Installing Docker..."); - // Docker installation script - execSync("curl -fsSL https://get.docker.com | sh"); - // Add user to docker group - execSync("sudo usermod -aG docker $USER"); - spinner.succeed("Docker installed successfully"); - spinner.stop(); - logger.info("Please log out and back in for group changes to take effect"); - return true; - } - catch (error) { - spinner.fail("Failed to install Docker"); - spinner.stop(); - logger.error(error); - return false; - } -}; -// Get Public IP -const getPublicIP = async () => { - try { - const { data } = await axios.get("https://api.ipify.org?format=json"); - return data.ip; - } - catch (error) { - logger.error("Failed to get public IP"); - return null; - } -}; -// Environment configuration -const defaultConfig = { - p2pPort: "40400", - port: "8080", - key: "0x0000000000000000000000000000000000000000000000000000000000000001", - ip: "8.8.8.8", - name: "validator-1", -}; -const configureEnvironment = async (options) => { - try { - spinner.stopAndPersist({ text: "Configuring environment..." }); - // Get public IP first - spinner.start("Fetching public IP..."); - const publicIP = await getPublicIP(); - spinner.succeed(`Public IP: ${publicIP}`); - // Load existing config - spinner.stopAndPersist({ text: "Loading configuration..." }); - const currentConfig = existsSync(".env") - ? Object.fromEntries(readFileSync(".env", "utf8") - .split("\n") - .filter(Boolean) - .map((line) => line.split("="))) - : {}; - if (!options.name) { - options.name = await input({ - message: "Validator Name:", - default: currentConfig.name || defaultConfig.name, - }); - } - if (!options.p2pPort) { - options.p2pPort = await input({ - message: "P2P Port:", - default: currentConfig.p2pPort || defaultConfig.p2pPort, - }); - } - if (!options.port) { - options.port = await input({ - message: "Node Port:", - default: currentConfig.port || defaultConfig.port, - }); - } - if (!options.key) { - options.key = await input({ - message: "Validator Private Key:", - required: true, - }); - } - if (!options.ip) { - options.ip = await input({ - message: "Public IP:", - default: publicIP || defaultConfig.ip, - }); - } - // Restart spinner for saving config - spinner.start("Saving configuration..."); - const envContent = Object.entries(options) - .map(([key, value]) => `${key}=${value}`) - .join("\n"); - writeFileSync(".env", envContent, { - encoding: "utf8", - flag: "w", - }); - spinner.succeed("Environment configured successfully"); - // Generate docker-compose.yml - spinner.start("Generating docker-compose configuration..."); - const composeConfig = ` -name: ${options.name} -services: - validator: - network_mode: host - restart: unless-stopped - env_file: - - .env - environment: - - P2P_UDP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} - - P2P_TCP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} - - COINBASE=0xbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - - VALIDATOR_DISABLED=false - - VALIDATOR_PRIVATE_KEY=${options.key} - - SEQ_PUBLISHER_PRIVATE_KEY=${options.key} - - L1_PRIVATE_KEY=${options.key} - - DEBUG=aztec:*,-aztec:avm_simulator*,-aztec:circuits:artifact_hash,-aztec:libp2p_service,-json-rpc*,-aztec:world-state:database,-aztec:l2_block_stream* - - LOG_LEVEL=debug - - AZTEC_PORT=${options.port} - - P2P_ENABLED=true - - L1_CHAIN_ID=1337 - - PROVER_REAL_PROOFS=true - - PXE_PROVER_ENABLED=true - - ETHEREUM_SLOT_DURATION=12sec - - AZTEC_SLOT_DURATION=36 - - AZTEC_EPOCH_DURATION=32 - - AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS=13 - - ETHEREUM_HOST=http://35.221.3.35:8545 - - BOOTSTRAP_NODES=enr:-Jq4QKIJisajcICBVMoMwFtbmPgmHt3KoonypbBIQCAMNjhMc6DKW0J4vJzDpGPFUX7T2fzyyjezHgKKzeZY_DbRz_kGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCPdAyOJc2VjcDI1NmsxoQK92C7GObzDvCt9uwzW0lhKJKGCvOWkmAZjd2E2w-svuoN0Y3CCndCDdWRwgp3Q - - REGISTRY_CONTRACT_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3 - - GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 - - FEE_JUICE_CONTRACT_ADDRESS=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 - - ROLLUP_CONTRACT_ADDRESS=0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6 - - REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=0x5fc8d32690cc91d4c39d9d3abcbd16989f875707 - - GOVERNANCE_CONTRACT_ADDRESS=0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9 - - COIN_ISSUER_CONTRACT_ADDRESS=0xdc64a140aa3e981100a9beca4e685f962f0cf6c9 - - FEE_JUICE_PORTAL_CONTRACT_ADDRESS=0x0165878a594ca255338adfa4d48449f69242eb8f - - INBOX_CONTRACT_ADDRESS=0xed179b78d5781f93eb169730d8ad1be7313123f4 - - OUTBOX_CONTRACT_ADDRESS=0x1016b5aaa3270a65c315c664ecb238b6db270b64 - - P2P_UDP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} - - P2P_TCP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} - image: aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${process.arch} - command: start --node --archiver --sequencer -`; - writeFileSync("docker-compose.yml", composeConfig); - spinner.succeed("Docker compose file generated successfully, run `aztec-spartan start` to launch your node"); - return true; - } - catch (error) { - spinner.fail("Failed to configure environment"); - logger.error(error); - return false; - } -}; -// Docker commands -const dockerCommands = { - start: async () => { - try { - spinner.start("Starting containers..."); - const child = require("child_process").spawn("docker", ["compose", "up", "-d"], { - stdio: "inherit", - }); - // Handle SIGINT (Ctrl+C) - process.on("SIGINT", () => { - child.kill("SIGINT"); - process.exit(0); - }); - // Wait for the process to finish - await new Promise((resolve, reject) => { - child.on("exit", (code) => { - if (code === 0 || code === null) { - resolve(); - } - else { - reject(new Error(`Process exited with code ${code}`)); - } - }); - child.on("error", reject); - }); - spinner.succeed("Containers started successfully"); - } - catch (error) { - spinner.fail("Failed to start containers. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - logger.error(message); - } - } - }, - stop: async () => { - try { - spinner.start("Stopping containers..."); - execSync("docker compose down"); - spinner.succeed("Containers stopped successfully"); - } - catch (error) { - spinner.fail("Failed to stop containers. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - logger.error(message); - } - } - }, - pull: async () => { - spinner.start("Pulling latest images..."); - try { - spinner.stop(); - execSync("docker compose pull"); - spinner.succeed("Images updated successfully"); - } - catch (error) { - spinner.fail("Failed to pull images. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - logger.error(message); - } - } - }, - logs: async () => { - try { - spinner.start("Fetching logs..."); - // Use spawn instead of execSync to handle SIGINT properly - const child = require("child_process").spawn("docker", ["compose", "logs", "-f"], { - stdio: "inherit", - }); - // Handle SIGINT (Ctrl+C) - process.on("SIGINT", () => { - child.kill("SIGINT"); - process.exit(0); - }); - // Wait for the process to finish - await new Promise((resolve, reject) => { - child.on("exit", (code) => { - if (code === 0 || code === null) { - resolve(); - } - else { - reject(new Error(`Process exited with code ${code}`)); - } - }); - child.on("error", reject); - }); - } - catch (error) { - spinner.fail("Failed to fetch logs. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - logger.error(message); - } - } - }, -}; -// CLI Commands -program - .name("aztec testnet") - .description("Aztec Testnet Node CLI") - .version("1.0.0"); -program - .command("install") - .option("-p, --port ", "Node port") - .option("-p2p, --p2p-port ", "P2P port") - .option("-ip, --ip ", "Public IP") - .option("-k, --key ", "Validator private key") - .option("-n, --name ", "Validator name") - .option("-d, --skip-docker", "Skip Docker installation") - .description("Install Aztec Testnet node configuration") - .action(async (options) => { - showBanner(); - if (options.skipDocker) { - logger.warn("Skipping Docker installation"); - } - else { - await checkDocker(); - } - await configureEnvironment(options); - logger.info('Initialization complete! Use "aztec-spartan start" to launch your node.'); - process.exit(0); -}); -program - .command("start") - .description("Start Aztec Testnet node") - .action(async () => { - if (!existsSync(".env")) { - console.error('Configuration not found. Please run "aztec-spartan init" first.'); - process.exit(1); - } - await dockerCommands.start(); - process.exit(0); -}); -program - .command("stop") - .description("Stop Aztec Testnet node") - .action(async () => { - await dockerCommands.stop(); - process.exit(0); -}); -program - .command("update") - .description("Update Aztec Testnet node images") - .action(async () => { - await dockerCommands.pull(); - process.exit(0); -}); -program - .command("logs") - .description("Show Aztec Testnet node logs") - .action(async () => { - await dockerCommands.logs(); - process.exit(0); -}); -program.parse(); diff --git a/spartan/releases/rough-rhino/index.test.ts b/spartan/releases/rough-rhino/index.test.ts index 63cc49a29aa..5f117f52201 100644 --- a/spartan/releases/rough-rhino/index.test.ts +++ b/spartan/releases/rough-rhino/index.test.ts @@ -6,27 +6,22 @@ import axios from "axios"; const CLI_PATH = join(__dirname, "index.ts"); const DOCKER_COMPOSE_PATH = join(__dirname, "docker-compose.yml"); -const ENV_PATH = join(__dirname, ".env"); // Mock axios for getPublicIP const mockedAxios = mock(axios); beforeAll(() => { // Clean up any existing files before each test - [DOCKER_COMPOSE_PATH, ENV_PATH].forEach((file) => { - if (existsSync(file)) { - unlinkSync(file); - } - }); + if (existsSync(DOCKER_COMPOSE_PATH)) { + unlinkSync(DOCKER_COMPOSE_PATH); + } }); afterAll(() => { // Clean up after all tests - [DOCKER_COMPOSE_PATH, ENV_PATH].forEach((file) => { - if (existsSync(file)) { - unlinkSync(file); - } - }); + if (existsSync(DOCKER_COMPOSE_PATH)) { + unlinkSync(DOCKER_COMPOSE_PATH); + } }); describe("Test Suite", () => { @@ -54,7 +49,7 @@ describe("Test Suite", () => { }); } catch (error: any) { expect(error.message).toContain( - 'Configuration not found. Please run "aztec-spartan init" first.' + 'Configuration not found. Please run "aztec-spartan install" first.' ); } }); @@ -72,22 +67,14 @@ describe("Test Suite", () => { test("install command creates necessary files", async () => { // Check if files were created - expect(existsSync(ENV_PATH)).toBe(true); expect(existsSync(DOCKER_COMPOSE_PATH)).toBe(true); - // Verify .env content - const envContent = readFileSync(ENV_PATH, "utf8"); - expect(envContent).toContain("p2pPort=40400"); - expect(envContent).toContain("port=8080"); - expect(envContent).toContain("key=0x00"); - expect(envContent).toContain("ip=7.7.7.7"); - expect(envContent).toContain("name=nameme"); - // Verify docker-compose.yml content const composeContent = readFileSync(DOCKER_COMPOSE_PATH, "utf8"); expect(composeContent).toContain("name: nameme"); expect(composeContent).toContain(`P2P_UDP_ANNOUNCE_ADDR=7.7.7.7:40400`); expect(composeContent).toContain("AZTEC_PORT=8080"); + expect(composeContent).toContain("VALIDATOR_PRIVATE_KEY=0x00"); }); }); }); diff --git a/spartan/releases/rough-rhino/index.ts b/spartan/releases/rough-rhino/index.ts index 4359b5af0b7..c4ae251ce71 100644 --- a/spartan/releases/rough-rhino/index.ts +++ b/spartan/releases/rough-rhino/index.ts @@ -13,6 +13,8 @@ const program = new Command(); // Global spinner instance used throughout the application const spinner = ora({ color: "blue", discardStdin: false }); +const ARCH = execSync("uname -m").toString().trim(); + // ASCII Art Banner const showBanner = () => { console.log( @@ -105,34 +107,24 @@ const configureEnvironment = async (options: any) => { spinner.succeed(`Public IP: ${publicIP}`); // Load existing config - spinner.stopAndPersist({ text: "Loading configuration..." }); - const currentConfig = existsSync(".env") - ? Object.fromEntries( - readFileSync(".env", "utf8") - .split("\n") - .filter(Boolean) - .map((line) => line.split("=")) - ) - : {}; - if (!options.name) { options.name = await input({ message: "Validator Name:", - default: currentConfig.name || defaultConfig.name, + default: defaultConfig.name, }); } if (!options.p2pPort) { options.p2pPort = await input({ message: "P2P Port:", - default: currentConfig.p2pPort || defaultConfig.p2pPort, + default: defaultConfig.p2pPort, }); } if (!options.port) { options.port = await input({ message: "Node Port:", - default: currentConfig.port || defaultConfig.port, + default: defaultConfig.port, }); } @@ -146,22 +138,13 @@ const configureEnvironment = async (options: any) => { if (!options.ip) { options.ip = await input({ message: "Public IP:", - default: publicIP || defaultConfig.ip, + default: defaultConfig.ip, }); } // Restart spinner for saving config spinner.start("Saving configuration..."); - const envContent = Object.entries(options) - .map(([key, value]) => `${key}=${value}`) - .join("\n"); - - writeFileSync(".env", envContent, { - encoding: "utf8", - flag: "w", - }); - spinner.succeed("Environment configured successfully"); // Generate docker-compose.yml @@ -172,8 +155,6 @@ services: validator: network_mode: host restart: unless-stopped - env_file: - - .env environment: - P2P_UDP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} - P2P_TCP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} @@ -207,7 +188,7 @@ services: - OUTBOX_CONTRACT_ADDRESS=0x1016b5aaa3270a65c315c664ecb238b6db270b64 - P2P_UDP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} - P2P_TCP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} - image: aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${process.arch} + image: aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH} command: start --node --archiver --sequencer `; writeFileSync("docker-compose.yml", composeConfig); @@ -356,9 +337,9 @@ program await configureEnvironment(options); - spinner.info( - 'Initialization complete! Use "aztec-spartan start" to launch your node.' - ); + spinner.stopAndPersist({ + text: 'Initialization complete! Use "npx aztec-spartan start" to launch your node.', + }); process.exit(0); }); @@ -366,9 +347,9 @@ program .command("start") .description("Start Aztec Testnet node") .action(async () => { - if (!existsSync(".env")) { + if (!existsSync("docker-compose.yml")) { console.error( - 'Configuration not found. Please run "aztec-spartan init" first.' + 'Configuration not found. Please run "npx aztec-spartan install" first.' ); process.exit(1); } diff --git a/spartan/releases/rough-rhino/package.json b/spartan/releases/rough-rhino/package.json index a82447249f1..d620a3d1728 100644 --- a/spartan/releases/rough-rhino/package.json +++ b/spartan/releases/rough-rhino/package.json @@ -3,6 +3,12 @@ "module": "index.ts", "type": "module", "version": "1.0.0", + "scripts": { + "test": "bun test", + "start": "bun index.ts", + "compile": "tsc index.ts --skipLibCheck --module nodenext && chmod +x index.js", + "publish": "bun compile && bun publish" + }, "devDependencies": { "@types/bun": "latest", "@types/figlet": "^1.5.8", From 23e6e471e515e2fdf7136ad85bfbedd3d29d4a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 17:32:32 +0000 Subject: [PATCH 10/16] feat: nit --- spartan/releases/rough-rhino/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spartan/releases/rough-rhino/index.ts b/spartan/releases/rough-rhino/index.ts index c4ae251ce71..9764da015eb 100644 --- a/spartan/releases/rough-rhino/index.ts +++ b/spartan/releases/rough-rhino/index.ts @@ -274,7 +274,7 @@ const dockerCommands = { logs: async () => { try { - spinner.start("Fetching logs..."); + spinner.stopAndPersist({ text: "Fetching logs..." }); // Use spawn instead of execSync to handle SIGINT properly const child = require("child_process").spawn( "docker", From 05e9bc8217156b934f8672b4cfacf0d4d8ef1c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 17:45:33 +0000 Subject: [PATCH 11/16] feat: making the CI deploy to NPM --- spartan/releases/rough-rhino/Earthfile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spartan/releases/rough-rhino/Earthfile b/spartan/releases/rough-rhino/Earthfile index 3074957ef13..ece606c0fa3 100644 --- a/spartan/releases/rough-rhino/Earthfile +++ b/spartan/releases/rough-rhino/Earthfile @@ -38,3 +38,17 @@ test-functionality: RUN bun test + +publish-npm: + FROM +deps + ARG VERSION + ARG DIST_TAG + ARG DRY_RUN=0 + RUN --secret NPM_TOKEN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > /usr/src/spartan/releases/rough-rhino/.npmrc + WORKDIR /usr/src/spartan/releases/rough-rhino + RUN jq --arg v $VERSION '.version = $v' package.json > _tmp.json && mv _tmp.json package.json + RUN if [ "$DRY_RUN" = "1" ]; then \ + npm publish --tag $DIST_TAG --access public --dry-run; \ + else \ + npm publish --tag $DIST_TAG --access public; \ + fi From 8c45b2fedfc781a0aa8811557aac2a7358a25ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 17:45:41 +0000 Subject: [PATCH 12/16] feat: making the CI deploy to NPM --- .github/workflows/publish-aztec-packages.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/publish-aztec-packages.yml b/.github/workflows/publish-aztec-packages.yml index d28c577f875..bf12f93e2d4 100644 --- a/.github/workflows/publish-aztec-packages.yml +++ b/.github/workflows/publish-aztec-packages.yml @@ -312,6 +312,18 @@ jobs: --VERSION=$VERSION \ --DRY_RUN=${{ (github.event.inputs.publish == 'false') && '1' || '0' }} + - name: Publish spartan NPM package + run: | + DEPLOY_TAG=${{ env.DEPLOY_TAG }} + VERSION=${DEPLOY_TAG#aztec-packages-v} + earthly-ci \ + --no-output \ + --secret NPM_TOKEN=${{ env.NPM_TOKEN }} \ + ./spartan/releases/rough-rhino+publish-npm \ + --DIST_TAG=latest \ + --VERSION=$VERSION \ + --DRY_RUN=${{ (github.event.inputs.publish == 'false') && '1' || '0' }} + publish-aztec-up: needs: [configure, publish-manifests] runs-on: ubuntu-latest From b4dd3dc2570e74b18c5f5b6f9aef91a62410b3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 17:49:25 +0000 Subject: [PATCH 13/16] feat: making the CI deploy to NPM --- spartan/releases/rough-rhino/Earthfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spartan/releases/rough-rhino/Earthfile b/spartan/releases/rough-rhino/Earthfile index ece606c0fa3..aa6bbbd8661 100644 --- a/spartan/releases/rough-rhino/Earthfile +++ b/spartan/releases/rough-rhino/Earthfile @@ -48,7 +48,7 @@ publish-npm: WORKDIR /usr/src/spartan/releases/rough-rhino RUN jq --arg v $VERSION '.version = $v' package.json > _tmp.json && mv _tmp.json package.json RUN if [ "$DRY_RUN" = "1" ]; then \ - npm publish --tag $DIST_TAG --access public --dry-run; \ + bun run publish --tag $DIST_TAG --access public --dry-run; \ else \ - npm publish --tag $DIST_TAG --access public; \ + bun run publish --tag $DIST_TAG --access public; \ fi From f02740d18f1a516b1a2dc0394f48332bc1ac80b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 18:37:03 +0000 Subject: [PATCH 14/16] feat: Cursor rewrote it all in shell, AI will take our jobs --- .github/workflows/ci.yml | 7 +- .release-please-manifest.json | 3 +- spartan/releases/rough-rhino/.gitignore | 1 - spartan/releases/rough-rhino/.npmignore | 3 - spartan/releases/rough-rhino/Earthfile | 107 +++-- spartan/releases/rough-rhino/README.md | 10 +- spartan/releases/rough-rhino/aztec-spartan.sh | 285 +++++++++++++ spartan/releases/rough-rhino/bun.lockb | Bin 39884 -> 0 bytes spartan/releases/rough-rhino/index.test.ts | 80 ---- spartan/releases/rough-rhino/index.ts | 384 ------------------ spartan/releases/rough-rhino/package.json | 31 -- .../releases/rough-rhino/scripts/full-node.sh | 39 -- spartan/releases/rough-rhino/tsconfig.json | 27 -- 13 files changed, 369 insertions(+), 608 deletions(-) delete mode 100644 spartan/releases/rough-rhino/.npmignore create mode 100755 spartan/releases/rough-rhino/aztec-spartan.sh delete mode 100755 spartan/releases/rough-rhino/bun.lockb delete mode 100644 spartan/releases/rough-rhino/index.test.ts delete mode 100644 spartan/releases/rough-rhino/index.ts delete mode 100644 spartan/releases/rough-rhino/package.json delete mode 100755 spartan/releases/rough-rhino/scripts/full-node.sh delete mode 100644 spartan/releases/rough-rhino/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6678a63d7f4..74ed73f6c77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -884,12 +884,9 @@ jobs: - uses: ./.github/ci-setup-action with: concurrency_key: rough-rhino-installer - - name: Rough Rhino Docker Installer + - name: Rough Rhino Installer Helper Script working-directory: ./spartan/releases/rough-rhino - run: earthly-ci +test-install - - name: Rough Rhino Installer - working-directory: ./spartan/releases/rough-rhino - run: earthly-ci +test-functionality + run: earthly-ci +test-all protocol-circuits-gates-report: needs: [build, configure] diff --git a/.release-please-manifest.json b/.release-please-manifest.json index de1df840d1a..fe3c2693b52 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -3,6 +3,5 @@ "yarn-project/cli": "0.35.1", "yarn-project/aztec": "0.65.2", "barretenberg": "0.65.2", - "barretenberg/ts": "0.65.2", - "spartan/releases/rough-rhino": "1.0.0" + "barretenberg/ts": "0.65.2" } diff --git a/spartan/releases/rough-rhino/.gitignore b/spartan/releases/rough-rhino/.gitignore index a4f916d4dd7..23ce2843a4a 100644 --- a/spartan/releases/rough-rhino/.gitignore +++ b/spartan/releases/rough-rhino/.gitignore @@ -174,4 +174,3 @@ dist # Finder (MacOS) folder config .DS_Store docker-compose.yml -index.js diff --git a/spartan/releases/rough-rhino/.npmignore b/spartan/releases/rough-rhino/.npmignore deleted file mode 100644 index 405737df2f9..00000000000 --- a/spartan/releases/rough-rhino/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -bun.lockb -*.ts diff --git a/spartan/releases/rough-rhino/Earthfile b/spartan/releases/rough-rhino/Earthfile index aa6bbbd8661..259a9fd96dc 100644 --- a/spartan/releases/rough-rhino/Earthfile +++ b/spartan/releases/rough-rhino/Earthfile @@ -12,43 +12,90 @@ deps: npm \ unzip - RUN curl -fsSL https://bun.sh/install | bash - ENV PATH="/root/.bun/bin:${PATH}" - - COPY package.json . - COPY bun.lockb . - - RUN bun install - -test-install: +test-setup: FROM +deps + COPY aztec-spartan.sh . + RUN chmod +x aztec-spartan.sh + # Mock docker and docker compose commands for testing + RUN mkdir -p /usr/local/bin && \ + echo '#!/bin/bash\necho "Docker command: $@"' > /usr/local/bin/docker && \ + echo '#!/bin/bash\necho "Docker compose command: $@"' > /usr/local/bin/docker-compose && \ + chmod +x /usr/local/bin/docker /usr/local/bin/docker-compose - COPY index.ts . +test-help: + FROM +test-setup + RUN ./aztec-spartan.sh | grep -q "Commands:" && \ + echo "✅ Help command test passed" || \ + (echo "❌ Help command test failed" && exit 1) - RUN yes | bun index.ts install -p 8080 -p2p 40400 -ip 7.7.7.7 -k 0x00 -n test-node +test-no-config: + FROM +test-setup + RUN if ./aztec-spartan.sh start 2>&1 | grep -q "Configuration not found"; then \ + echo "✅ No config test passed"; \ + else \ + echo "❌ No config test failed" && exit 1; \ + fi +test-install: + FROM +test-setup + # Test installation with CLI arguments + RUN echo -e "\n\n" | ./aztec-spartan.sh install \ + -p 8080 \ + -p2p 40400 \ + -ip 1.2.3.4 \ + -k 0x00 \ + -n test-validator + # Verify docker-compose.yml was created and contains correct values + RUN test -f docker-compose.yml && \ + grep -q "name: test-validator" docker-compose.yml && \ + grep -q "P2P_UDP_ANNOUNCE_ADDR=1.2.3.4:40400" docker-compose.yml && \ + grep -q "AZTEC_PORT=8080" docker-compose.yml && \ + grep -q "VALIDATOR_PRIVATE_KEY=0x00" docker-compose.yml && \ + echo "✅ Install test passed" || \ + (echo "❌ Install test failed" && exit 1) -test-functionality: +test-docker-check: FROM +deps + COPY aztec-spartan.sh . + RUN chmod +x aztec-spartan.sh + # Remove docker to test docker installation check + RUN rm -f /usr/local/bin/docker /usr/local/bin/docker-compose + # Test docker check (should fail since docker is not installed) + RUN if ./aztec-spartan.sh install 2>&1 | grep -q "Docker or Docker Compose not found"; then \ + echo "✅ Docker check test passed"; \ + else \ + echo "❌ Docker check test failed" && exit 1; \ + fi - COPY index.ts . - COPY index.test.ts . - - RUN curl -fsSL https://get.docker.com | sh +test-start-stop: + FROM +test-setup + # First install with test configuration + RUN echo -e "\n\n" | ./aztec-spartan.sh install \ + -p 8080 \ + -p2p 40400 \ + -ip 1.2.3.4 \ + -k 0x00 \ + -n test-validator + # Test start command + RUN ./aztec-spartan.sh start 2>&1 | grep -q "Starting containers" && \ + echo "✅ Start command test passed" || \ + (echo "❌ Start command test failed" && exit 1) + # Test stop command + RUN ./aztec-spartan.sh stop 2>&1 | grep -q "Stopping containers" && \ + echo "✅ Stop command test passed" || \ + (echo "❌ Stop command test failed" && exit 1) - RUN bun test +test-update: + FROM +test-setup + RUN ./aztec-spartan.sh update 2>&1 | grep -q "Pulling latest images" && \ + echo "✅ Update command test passed" || \ + (echo "❌ Update command test failed" && exit 1) +test-all: + BUILD +test-help + BUILD +test-no-config + BUILD +test-install + BUILD +test-docker-check + BUILD +test-start-stop + BUILD +test-update -publish-npm: - FROM +deps - ARG VERSION - ARG DIST_TAG - ARG DRY_RUN=0 - RUN --secret NPM_TOKEN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > /usr/src/spartan/releases/rough-rhino/.npmrc - WORKDIR /usr/src/spartan/releases/rough-rhino - RUN jq --arg v $VERSION '.version = $v' package.json > _tmp.json && mv _tmp.json package.json - RUN if [ "$DRY_RUN" = "1" ]; then \ - bun run publish --tag $DIST_TAG --access public --dry-run; \ - else \ - bun run publish --tag $DIST_TAG --access public; \ - fi diff --git a/spartan/releases/rough-rhino/README.md b/spartan/releases/rough-rhino/README.md index b13501b3caa..cf7e6f57d75 100644 --- a/spartan/releases/rough-rhino/README.md +++ b/spartan/releases/rough-rhino/README.md @@ -11,9 +11,7 @@ For once, there's no rocket science here. This script does the following: - Outputs a templated docker-compose file with your variables - Runs the docker compose file -## Prerequisites - -This script should work in most UNIX-based machines. You should [have Node installed](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script). +It should work in most UNIX-based machines. ## Installation @@ -21,12 +19,12 @@ To configure a new node, create a new directory and run the install script: ```bash cd val1 -npx aztec-spartan install +curl -L https://github.com/AztecProtocol/aztec-packages/blob/master/spartan/releases/rough-rhino/validator.sh | bash ``` -If you don't have Docker installed, the script will do it for you. It will then prompt for any required environment variables and output a `docker-compose.yml` file and a `.env` file. +If you don't have Docker installed, the script will do it for you. It will then prompt for any required environment variables and output a `docker-compose.yml` file. -You can run the command with `-h` to see all available options, and pass them as flags, i.e. `npx aztec-spartan install -p 8080 -p2p 40400 -n nameme`. +You can run the command without any command to see all available options, and pass them as flags, i.e. `npx aztec-spartan install -p 8080 -p2p 40400 -n nameme`. ## Running diff --git a/spartan/releases/rough-rhino/aztec-spartan.sh b/spartan/releases/rough-rhino/aztec-spartan.sh new file mode 100755 index 00000000000..2ed3a206226 --- /dev/null +++ b/spartan/releases/rough-rhino/aztec-spartan.sh @@ -0,0 +1,285 @@ +#!/bin/bash + +# Colors and formatting +BLUE='\033[0;34m' +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Global variables +ARCH=$(uname -m) +DEFAULT_P2P_PORT="40400" +DEFAULT_PORT="8080" +DEFAULT_KEY="0x0000000000000000000000000000000000000000000000000000000000000001" +# Try to get default IP from ipify API, otherwise leave empty to require user input +DEFAULT_IP=$(curl -s --connect-timeout 5 https://api.ipify.org?format=json | grep -o '"ip":"[^"]*' | cut -d'"' -f4 || echo "") +DEFAULT_NAME="validator-1" + +# Parse command line arguments +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + -p|--port) + CLI_PORT="$2" + shift 2 + ;; + -p2p|--p2p-port) + CLI_P2P_PORT="$2" + shift 2 + ;; + -ip|--ip) + CLI_IP="$2" + shift 2 + ;; + -k|--key) + CLI_KEY="$2" + shift 2 + ;; + -n|--name) + CLI_NAME="$2" + shift 2 + ;; + *) + shift + ;; + esac + done +} + +# Show banner function +show_banner() { + echo -e "${BLUE}" + echo " _ ____ _____ _____ ____ _____ _____ ____ _____ _ _ _____ _____ " + echo " / \ |_ /|_ _| ____| _ \ |_ _| ____/ ___|_ _| \ | | ____|_ _|" + echo " / _ \ / / | | | _| | |_) | | | | _| \___ \ | | | \| | _| | | " + echo " / ___ \/ /_ | | | |___| _ < | | | |___ ___) || | | |\ | |___ | | " + echo "/_/ \_\____| |_| |_____|_| \_\ |_| |_____|____/ |_| |_| \_|_____| |_| " + echo -e "${NC}" +} + +# Check if Docker is installed +check_docker() { + echo -e "${BLUE}Checking Docker installation...${NC}" + if command -v docker >/dev/null 2>&1 && command -v docker compose >/dev/null 2>&1; then + echo -e "${GREEN}Docker and Docker Compose are installed${NC}" + return 0 + else + echo -e "${RED}Docker or Docker Compose not found${NC}" + read -p "Would you like to install Docker? [Y/n] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then + install_docker + return $? + fi + return 1 + fi +} + +# Install Docker +install_docker() { + echo -e "${BLUE}Installing Docker...${NC}" + if curl -fsSL https://get.docker.com | sh; then + sudo usermod -aG docker $USER + echo -e "${GREEN}Docker installed successfully${NC}" + echo -e "${YELLOW}Please log out and back in for group changes to take effect${NC}" + return 0 + else + echo -e "${RED}Failed to install Docker${NC}" + return 1 + fi +} + +# Get public IP +get_public_ip() { + echo -e "${BLUE}Fetching public IP...${NC}" + PUBLIC_IP=$(curl -s https://api.ipify.org?format=json | grep -o '"ip":"[^"]*' | cut -d'"' -f4) + if [ -n "$PUBLIC_IP" ]; then + echo -e "${GREEN}Public IP: $PUBLIC_IP${NC}" + return 0 + else + echo -e "${YELLOW}Failed to get public IP${NC}" + return 1 + fi +} + +# Configure environment +configure_environment() { + local args=("$@") + parse_args "${args[@]}" + + echo -e "${BLUE}Configuring environment...${NC}" + + # Use CLI arguments if provided, otherwise use defaults or prompt + if [ -n "$CLI_NAME" ]; then + NAME="$CLI_NAME" + else + read -p "Validator Name [$DEFAULT_NAME]: " NAME + NAME=${NAME:-$DEFAULT_NAME} + fi + + if [ -n "$CLI_P2P_PORT" ]; then + P2P_PORT="$CLI_P2P_PORT" + else + read -p "P2P Port [$DEFAULT_P2P_PORT]: " P2P_PORT + P2P_PORT=${P2P_PORT:-$DEFAULT_P2P_PORT} + fi + + if [ -n "$CLI_PORT" ]; then + PORT="$CLI_PORT" + else + read -p "Node Port [$DEFAULT_PORT]: " PORT + PORT=${PORT:-$DEFAULT_PORT} + fi + + if [ -n "$CLI_KEY" ]; then + KEY="$CLI_KEY" + else + while true; do + read -p "Validator Private Key: " KEY + if [ -z "$KEY" ]; then + echo -e "${RED}Error: Validator Private Key is required${NC}" + else + break + fi + done + fi + + if [ -n "$CLI_IP" ]; then + IP="$CLI_IP" + else + if [ -z "$DEFAULT_IP" ]; then + while true; do + read -p "Public IP: " IP + if [ -z "$IP" ]; then + echo -e "${RED}Error: Public IP is required${NC}" + else + break + fi + done + else + read -p "Public IP [$DEFAULT_IP]: " IP + IP=${IP:-$DEFAULT_IP} + fi + fi + + # Generate docker-compose.yml + cat > docker-compose.yml << EOF +name: ${NAME} +services: + validator: + network_mode: host + restart: unless-stopped + environment: + - P2P_UDP_ANNOUNCE_ADDR=${IP}:${P2P_PORT} + - P2P_TCP_ANNOUNCE_ADDR=${IP}:${P2P_PORT} + - COINBASE=0xbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - VALIDATOR_DISABLED=false + - VALIDATOR_PRIVATE_KEY=${KEY} + - SEQ_PUBLISHER_PRIVATE_KEY=${KEY} + - L1_PRIVATE_KEY=${KEY} + - DEBUG=aztec:*,-aztec:avm_simulator*,-aztec:circuits:artifact_hash,-aztec:libp2p_service,-json-rpc*,-aztec:world-state:database,-aztec:l2_block_stream* + - LOG_LEVEL=debug + - AZTEC_PORT=${PORT} + - P2P_ENABLED=true + - L1_CHAIN_ID=1337 + - PROVER_REAL_PROOFS=true + - PXE_PROVER_ENABLED=true + - ETHEREUM_SLOT_DURATION=12sec + - AZTEC_SLOT_DURATION=36 + - AZTEC_EPOCH_DURATION=32 + - AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS=13 + - ETHEREUM_HOST=http://34.48.76.131:8545 + - BOOTSTRAP_NODES=enr:-Jq4QO_3szmgtG2cbEdnFDIhpGAQkc1HwfNy4-M6sG9QmQbPTmp9PMOHR3xslfR23hORiU-GpA7uM9uXw49lFcnuuvYGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCIwTIOJc2VjcDI1NmsxoQKQTN17XKCwjYSSwmTc-6YzCMhd3v6Ofl8TS-WunX6LCoN0Y3CCndCDdWRwgp3Q + - REGISTRY_CONTRACT_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3 + - GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 + - FEE_JUICE_CONTRACT_ADDRESS=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 + - ROLLUP_CONTRACT_ADDRESS=0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6 + - REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=0x5fc8d32690cc91d4c39d9d3abcbd16989f875707 + - GOVERNANCE_CONTRACT_ADDRESS=0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9 + - COIN_ISSUER_CONTRACT_ADDRESS=0xdc64a140aa3e981100a9beca4e685f962f0cf6c9 + - FEE_JUICE_PORTAL_CONTRACT_ADDRESS=0x0165878a594ca255338adfa4d48449f69242eb8f + - INBOX_CONTRACT_ADDRESS=0xed179b78d5781f93eb169730d8ad1be7313123f4 + - OUTBOX_CONTRACT_ADDRESS=0x1016b5aaa3270a65c315c664ecb238b6db270b64 + - P2P_UDP_LISTEN_ADDR=0.0.0.0:${P2P_PORT} + - P2P_TCP_LISTEN_ADDR=0.0.0.0:${P2P_PORT} + image: aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH} + command: start --node --archiver --sequencer +EOF + + echo -e "${GREEN}Configuration complete! Use './aztec-spartan.sh start' to launch your node.${NC}" +} + +# Docker commands +start_node() { + if [ ! -f "docker-compose.yml" ]; then + echo -e "${RED}Configuration not found. Please run './aztec-spartan.sh install' first.${NC}" + exit 1 + fi + echo -e "${BLUE}Starting containers...${NC}" + if docker compose up -d; then + echo -e "${GREEN}Containers started successfully${NC}" + else + echo -e "${RED}Failed to start containers${NC}" + exit 1 + fi +} + +stop_node() { + echo -e "${BLUE}Stopping containers...${NC}" + if docker compose down; then + echo -e "${GREEN}Containers stopped successfully${NC}" + else + echo -e "${RED}Failed to stop containers${NC}" + exit 1 + fi +} + +update_node() { + echo -e "${BLUE}Pulling latest images...${NC}" + if docker compose pull; then + echo -e "${GREEN}Images updated successfully${NC}" + else + echo -e "${RED}Failed to update images${NC}" + exit 1 + fi +} + +show_logs() { + echo -e "${BLUE}Fetching logs...${NC}" + if ! docker compose logs -f; then + echo -e "${RED}Failed to fetch logs${NC}" + exit 1 + fi +} + +# Main script +case "$1" in + "install") + show_banner + check_docker + configure_environment "$@" + ;; + "start") + start_node + ;; + "stop") + stop_node + ;; + "update") + update_node + ;; + "logs") + show_logs + ;; + *) + echo "Usage: $0 {install|start|stop|update|logs}" + echo "Commands:" + echo " install - Install and configure Aztec Testnet node" + echo " start - Start Aztec Testnet node" + echo " stop - Stop Aztec Testnet node" + echo " update - Update Aztec Testnet node images" + echo " logs - Show Aztec Testnet node logs" + exit 1 + ;; +esac diff --git a/spartan/releases/rough-rhino/bun.lockb b/spartan/releases/rough-rhino/bun.lockb deleted file mode 100755 index 6c7739f72eef14d30304233ba19ea168eafcdaf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39884 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 We*!^61PaHqbSPW$GW!3y;Qs)bXW&i% diff --git a/spartan/releases/rough-rhino/index.test.ts b/spartan/releases/rough-rhino/index.test.ts deleted file mode 100644 index 5f117f52201..00000000000 --- a/spartan/releases/rough-rhino/index.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, expect, test, beforeAll, afterAll, mock } from "bun:test"; -import { execSync } from "child_process"; -import { existsSync, unlinkSync, readFileSync } from "fs"; -import { join } from "path"; -import axios from "axios"; - -const CLI_PATH = join(__dirname, "index.ts"); -const DOCKER_COMPOSE_PATH = join(__dirname, "docker-compose.yml"); - -// Mock axios for getPublicIP -const mockedAxios = mock(axios); - -beforeAll(() => { - // Clean up any existing files before each test - if (existsSync(DOCKER_COMPOSE_PATH)) { - unlinkSync(DOCKER_COMPOSE_PATH); - } -}); - -afterAll(() => { - // Clean up after all tests - if (existsSync(DOCKER_COMPOSE_PATH)) { - unlinkSync(DOCKER_COMPOSE_PATH); - } -}); - -describe("Test Suite", () => { - describe("CLI commands", () => { - beforeAll(() => { - // Mock axios response for IP - mockedAxios.mockResolvedValue({ data: { ip: "1.2.3.4" } }); - }); - - test("shows version", () => { - const output = execSync(`bun ${CLI_PATH} --version`).toString(); - expect(output).toContain("1.0.0"); - }); - - test("shows help", () => { - const output = execSync(`bun ${CLI_PATH} --help`).toString(); - expect(output).toContain("Aztec Testnet Node CLI"); - expect(output).toContain("Commands:"); - }); - - test("start command fails without configuration", () => { - try { - execSync(`bun ${CLI_PATH} start`, { - encoding: "utf8", - }); - } catch (error: any) { - expect(error.message).toContain( - 'Configuration not found. Please run "aztec-spartan install" first.' - ); - } - }); - }); - - describe("Install and Run", () => { - beforeAll(() => { - execSync( - `bun ${CLI_PATH} install -p 8080 -p2p 40400 -ip 7.7.7.7 -k 0x00 -n nameme`, - { - encoding: "utf8", - } - ); - }); - - test("install command creates necessary files", async () => { - // Check if files were created - expect(existsSync(DOCKER_COMPOSE_PATH)).toBe(true); - - // Verify docker-compose.yml content - const composeContent = readFileSync(DOCKER_COMPOSE_PATH, "utf8"); - expect(composeContent).toContain("name: nameme"); - expect(composeContent).toContain(`P2P_UDP_ANNOUNCE_ADDR=7.7.7.7:40400`); - expect(composeContent).toContain("AZTEC_PORT=8080"); - expect(composeContent).toContain("VALIDATOR_PRIVATE_KEY=0x00"); - }); - }); -}); diff --git a/spartan/releases/rough-rhino/index.ts b/spartan/releases/rough-rhino/index.ts deleted file mode 100644 index 9764da015eb..00000000000 --- a/spartan/releases/rough-rhino/index.ts +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/env node -import { Command } from "commander"; -import ora from "ora"; -import axios from "axios"; -import { execSync } from "child_process"; -import { writeFileSync, readFileSync, existsSync } from "fs"; -import figlet from "figlet"; -import chalk from "chalk"; -import inquirer from "inquirer"; -import input from "@inquirer/input"; -const program = new Command(); - -// Global spinner instance used throughout the application -const spinner = ora({ color: "blue", discardStdin: false }); - -const ARCH = execSync("uname -m").toString().trim(); - -// ASCII Art Banner -const showBanner = () => { - console.log( - chalk.blue( - figlet.textSync("Aztec Testnet", { - font: "Standard", - horizontalLayout: "full", - }) - ) - ); -}; - -// Check Docker Installation -const checkDocker = async () => { - try { - spinner.start("Checking Docker installation..."); - execSync("docker --version", { stdio: "ignore" }); - execSync("docker compose version", { stdio: "ignore" }); - spinner.succeed("Docker and Docker Compose are installed"); - return true; - } catch (error) { - spinner.fail("Docker or Docker Compose not found"); - spinner.stop(); - - const { install } = await inquirer.prompt([ - { - type: "confirm", - name: "install", - message: "Would you like to install Docker?", - default: true, - }, - ]); - - if (install) { - return await installDocker(); - } - return false; - } -}; - -// Install Docker -const installDocker = async () => { - try { - spinner.start("Installing Docker..."); - - // Docker installation script - execSync("curl -fsSL https://get.docker.com | sh"); - - // Add user to docker group - execSync("sudo usermod -aG docker $USER"); - - spinner.succeed("Docker installed successfully"); - spinner.stop(); - spinner.info("Please log out and back in for group changes to take effect"); - return true; - } catch (error: any) { - spinner.fail("Failed to install Docker"); - spinner.stop(); - spinner.fail(error.message); - return false; - } -}; - -// Get Public IP -const getPublicIP = async () => { - try { - const { data } = await axios.get("https://api.ipify.org?format=json"); - return data.ip; - } catch (error) { - spinner.warn("Failed to get public IP"); - return null; - } -}; - -// Environment configuration -const defaultConfig = { - p2pPort: "40400", - port: "8080", - key: "0x0000000000000000000000000000000000000000000000000000000000000001", - ip: "8.8.8.8", - name: "validator-1", -}; - -const configureEnvironment = async (options: any) => { - try { - spinner.stopAndPersist({ text: "Configuring environment..." }); - // Get public IP first - spinner.start("Fetching public IP..."); - const publicIP = await getPublicIP(); - spinner.succeed(`Public IP: ${publicIP}`); - - // Load existing config - if (!options.name) { - options.name = await input({ - message: "Validator Name:", - default: defaultConfig.name, - }); - } - - if (!options.p2pPort) { - options.p2pPort = await input({ - message: "P2P Port:", - default: defaultConfig.p2pPort, - }); - } - - if (!options.port) { - options.port = await input({ - message: "Node Port:", - default: defaultConfig.port, - }); - } - - if (!options.key) { - options.key = await input({ - message: "Validator Private Key:", - required: true, - }); - } - - if (!options.ip) { - options.ip = await input({ - message: "Public IP:", - default: defaultConfig.ip, - }); - } - - // Restart spinner for saving config - spinner.start("Saving configuration..."); - - spinner.succeed("Environment configured successfully"); - - // Generate docker-compose.yml - spinner.start("Generating docker-compose configuration..."); - const composeConfig = ` -name: ${options.name} -services: - validator: - network_mode: host - restart: unless-stopped - environment: - - P2P_UDP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} - - P2P_TCP_ANNOUNCE_ADDR=${options.ip}:${options.p2pPort} - - COINBASE=0xbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - - VALIDATOR_DISABLED=false - - VALIDATOR_PRIVATE_KEY=${options.key} - - SEQ_PUBLISHER_PRIVATE_KEY=${options.key} - - L1_PRIVATE_KEY=${options.key} - - DEBUG=aztec:*,-aztec:avm_simulator*,-aztec:circuits:artifact_hash,-aztec:libp2p_service,-json-rpc*,-aztec:world-state:database,-aztec:l2_block_stream* - - LOG_LEVEL=debug - - AZTEC_PORT=${options.port} - - P2P_ENABLED=true - - L1_CHAIN_ID=1337 - - PROVER_REAL_PROOFS=true - - PXE_PROVER_ENABLED=true - - ETHEREUM_SLOT_DURATION=12sec - - AZTEC_SLOT_DURATION=36 - - AZTEC_EPOCH_DURATION=32 - - AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS=13 - - ETHEREUM_HOST=http://34.48.76.131:8545 - - BOOTSTRAP_NODES=enr:-Jq4QO_3szmgtG2cbEdnFDIhpGAQkc1HwfNy4-M6sG9QmQbPTmp9PMOHR3xslfR23hORiU-GpA7uM9uXw49lFcnuuvYGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCIwTIOJc2VjcDI1NmsxoQKQTN17XKCwjYSSwmTc-6YzCMhd3v6Ofl8TS-WunX6LCoN0Y3CCndCDdWRwgp3Q - - REGISTRY_CONTRACT_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3 - - GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 - - FEE_JUICE_CONTRACT_ADDRESS=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 - - ROLLUP_CONTRACT_ADDRESS=0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6 - - REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=0x5fc8d32690cc91d4c39d9d3abcbd16989f875707 - - GOVERNANCE_CONTRACT_ADDRESS=0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9 - - COIN_ISSUER_CONTRACT_ADDRESS=0xdc64a140aa3e981100a9beca4e685f962f0cf6c9 - - FEE_JUICE_PORTAL_CONTRACT_ADDRESS=0x0165878a594ca255338adfa4d48449f69242eb8f - - INBOX_CONTRACT_ADDRESS=0xed179b78d5781f93eb169730d8ad1be7313123f4 - - OUTBOX_CONTRACT_ADDRESS=0x1016b5aaa3270a65c315c664ecb238b6db270b64 - - P2P_UDP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} - - P2P_TCP_LISTEN_ADDR=0.0.0.0:${options.p2pPort} - image: aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH} - command: start --node --archiver --sequencer -`; - writeFileSync("docker-compose.yml", composeConfig); - spinner.succeed( - "Docker compose file generated successfully, run `aztec-spartan start` to launch your node" - ); - return true; - } catch (error: any) { - spinner.fail("Failed to configure environment"); - spinner.fail(error); - return false; - } -}; - -// Docker commands -const dockerCommands = { - start: async () => { - try { - spinner.start("Starting containers..."); - const child = require("child_process").spawn( - "docker", - ["compose", "up", "-d"], - { - stdio: "inherit", - } - ); - - // Handle SIGINT (Ctrl+C) - process.on("SIGINT", () => { - child.kill("SIGINT"); - process.exit(0); - }); - - // Wait for the process to finish - await new Promise((resolve, reject) => { - child.on("exit", (code: number | null) => { - if (code === 0 || code === null) { - resolve(); - } else { - reject(new Error(`Process exited with code ${code}`)); - } - }); - child.on("error", reject); - }); - spinner.succeed("Containers started successfully"); - } catch (error) { - spinner.fail("Failed to start containers. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - spinner.fail(message); - } - } - }, - - stop: async () => { - try { - spinner.start("Stopping containers..."); - execSync("docker compose down"); - spinner.succeed("Containers stopped successfully"); - } catch (error) { - spinner.fail("Failed to stop containers. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - spinner.fail(message); - } - } - }, - - pull: async () => { - spinner.start("Pulling latest images..."); - try { - spinner.stop(); - execSync("docker compose pull"); - spinner.succeed("Images updated successfully"); - } catch (error) { - spinner.fail("Failed to pull images. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - spinner.fail(message); - } - } - }, - - logs: async () => { - try { - spinner.stopAndPersist({ text: "Fetching logs..." }); - // Use spawn instead of execSync to handle SIGINT properly - const child = require("child_process").spawn( - "docker", - ["compose", "logs", "-f"], - { - stdio: "inherit", - } - ); - - // Handle SIGINT (Ctrl+C) - process.on("SIGINT", () => { - child.kill("SIGINT"); - process.exit(0); - }); - - // Wait for the process to finish - await new Promise((resolve, reject) => { - child.on("exit", (code: number | null) => { - if (code === 0 || code === null) { - resolve(); - } else { - reject(new Error(`Process exited with code ${code}`)); - } - }); - child.on("error", reject); - }); - } catch (error) { - spinner.fail("Failed to fetch logs. Is Docker running?"); - if (error instanceof Error) { - const message = error.message.split("\n")[0]; - spinner.fail(message); - } - } - }, -}; - -// CLI Commands -program - .name("aztec testnet") - .description("Aztec Testnet Node CLI") - .version("1.0.0"); - -program - .command("install") - .option("-p, --port ", "Node port") - .option("-p2p, --p2p-port ", "P2P port") - .option("-ip, --ip ", "Public IP") - .option("-k, --key ", "Validator private key") - .option("-n, --name ", "Validator name") - .option("-d, --skip-docker", "Skip Docker installation") - .description("Install Aztec Testnet node configuration") - .action(async (options) => { - showBanner(); - - if (options.skipDocker) { - spinner.warn("Skipping Docker installation"); - } else { - await checkDocker(); - } - - await configureEnvironment(options); - - spinner.stopAndPersist({ - text: 'Initialization complete! Use "npx aztec-spartan start" to launch your node.', - }); - process.exit(0); - }); - -program - .command("start") - .description("Start Aztec Testnet node") - .action(async () => { - if (!existsSync("docker-compose.yml")) { - console.error( - 'Configuration not found. Please run "npx aztec-spartan install" first.' - ); - process.exit(1); - } - await dockerCommands.start(); - process.exit(0); - }); - -program - .command("stop") - .description("Stop Aztec Testnet node") - .action(async () => { - await dockerCommands.stop(); - process.exit(0); - }); - -program - .command("update") - .description("Update Aztec Testnet node images") - .action(async () => { - await dockerCommands.pull(); - process.exit(0); - }); - -program - .command("logs") - .description("Show Aztec Testnet node logs") - .action(async () => { - await dockerCommands.logs(); - process.exit(0); - }); - -program.parse(); diff --git a/spartan/releases/rough-rhino/package.json b/spartan/releases/rough-rhino/package.json deleted file mode 100644 index d620a3d1728..00000000000 --- a/spartan/releases/rough-rhino/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "rough-rhino", - "module": "index.ts", - "type": "module", - "version": "1.0.0", - "scripts": { - "test": "bun test", - "start": "bun index.ts", - "compile": "tsc index.ts --skipLibCheck --module nodenext && chmod +x index.js", - "publish": "bun compile && bun publish" - }, - "devDependencies": { - "@types/bun": "latest", - "@types/figlet": "^1.5.8", - "@types/inquirer": "^9.0.7", - "@types/node": "^22.10.1" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "dependencies": { - "@inquirer/input": "^4.0.2", - "axios": "^1.6.7", - "chalk": "^5.3.0", - "commander": "^12.1.0", - "figlet": "^1.7.0", - "inquirer": "^9.2.15", - "ora": "^8.0.1", - "pino": "^9.5.0" - } -} diff --git a/spartan/releases/rough-rhino/scripts/full-node.sh b/spartan/releases/rough-rhino/scripts/full-node.sh deleted file mode 100755 index 75a0d33bca1..00000000000 --- a/spartan/releases/rough-rhino/scripts/full-node.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -set -eu - -# get host arch -ARCH=$(uname -m) -IMAGE="aztecprotocol/aztec:698cd3d62680629a3f1bfc0f82604534cedbccf3-${ARCH}" - -docker run --rm --network=host \ - -e P2P_UDP_ANNOUNCE_ADDR=$PUBLIC_IP:$P2P_PORT \ - -e P2P_TCP_ANNOUNCE_ADDR=$PUBLIC_IP:$P2P_PORT \ - -e COINBASE=0xbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ - -e DEBUG="aztec:*,-aztec:avm_simulator*,-aztec:circuits:artifact_hash,-aztec:libp2p_service,-json-rpc*,-aztec:world-state:database,-aztec:l2_block_stream*" \ - -e LOG_LEVEL=debug \ - -e AZTEC_PORT=$NODE_PORT \ - -e P2P_ENABLED=true \ - -e VALIDATOR_DISABLED=true \ - -e L1_CHAIN_ID=1337 \ - -e PROVER_REAL_PROOFS=true \ - -e PXE_PROVER_ENABLED=true \ - -e ETHEREUM_SLOT_DURATION=12sec \ - -e AZTEC_SLOT_DURATION=36 \ - -e AZTEC_EPOCH_DURATION=32 \ - -e AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS=13 \ - -e ETHEREUM_HOST=http://34.48.76.131:8545 \ - -e BOOTSTRAP_NODES=enr:-Jq4QO_3szmgtG2cbEdnFDIhpGAQkc1HwfNy4-M6sG9QmQbPTmp9PMOHR3xslfR23hORiU-GpA7uM9uXw49lFcnuuvYGjWF6dGVjX25ldHdvcmsBgmlkgnY0gmlwhCIwTIOJc2VjcDI1NmsxoQKQTN17XKCwjYSSwmTc-6YzCMhd3v6Ofl8TS-WunX6LCoN0Y3CCndCDdWRwgp3Q \ - -e REGISTRY_CONTRACT_ADDRESS=0x5fbdb2315678afecb367f032d93f642f64180aa3 \ - -e GOVERNANCE_PROPOSER_CONTRACT_ADDRESS=0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 \ - -e FEE_JUICE_CONTRACT_ADDRESS=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 \ - -e ROLLUP_CONTRACT_ADDRESS=0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6 \ - -e REWARD_DISTRIBUTOR_CONTRACT_ADDRESS=0x5fc8d32690cc91d4c39d9d3abcbd16989f875707 \ - -e GOVERNANCE_CONTRACT_ADDRESS=0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9 \ - -e COIN_ISSUER_CONTRACT_ADDRESS=0xdc64a140aa3e981100a9beca4e685f962f0cf6c9 \ - -e FEE_JUICE_PORTAL_CONTRACT_ADDRESS=0x0165878a594ca255338adfa4d48449f69242eb8f \ - -e INBOX_CONTRACT_ADDRESS=0xed179b78d5781f93eb169730d8ad1be7313123f4 \ - -e OUTBOX_CONTRACT_ADDRESS=0x1016b5aaa3270a65c315c664ecb238b6db270b64 \ - -e P2P_UDP_LISTEN_ADDR=0.0.0.0:$P2P_PORT \ - -e P2P_TCP_LISTEN_ADDR=0.0.0.0:$P2P_PORT \ - $IMAGE start --node --archiver --sequencer diff --git a/spartan/releases/rough-rhino/tsconfig.json b/spartan/releases/rough-rhino/tsconfig.json deleted file mode 100644 index f063633934c..00000000000 --- a/spartan/releases/rough-rhino/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags - "noUnusedLocals": true, - "noUnusedParameters": true, - "noPropertyAccessFromIndexSignature": true - } -} From 47a3b5d13189a1d689cee5f05786164dc5307aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 18:48:08 +0000 Subject: [PATCH 15/16] feat: made a downloader for reusability --- .../releases/rough-rhino/create-spartan.sh | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100755 spartan/releases/rough-rhino/create-spartan.sh diff --git a/spartan/releases/rough-rhino/create-spartan.sh b/spartan/releases/rough-rhino/create-spartan.sh new file mode 100755 index 00000000000..870263926eb --- /dev/null +++ b/spartan/releases/rough-rhino/create-spartan.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# URL of the aztec-spartan.sh script +DEFAULT_URL="https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/zpedro/testnet_docker_compose/spartan/releases/rough-rhino/aztec-spartan.sh" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Download the script +echo "Downloading aztec-spartan.sh..." +if curl -L -o aztec-spartan.sh "${1:-$DEFAULT_URL}"; then + chmod +x aztec-spartan.sh + echo -e "${GREEN}✓ aztec-spartan.sh has been downloaded and made executable${NC}" + echo "You can now run it with: ./aztec-spartan.sh" +else + echo -e "${RED}✗ Failed to download aztec-spartan.sh${NC}" + exit 1 +fi From 43d8d0190cebe12774dce38976c1317529ca5dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 3 Dec 2024 18:54:10 +0000 Subject: [PATCH 16/16] feat: made a downloader for reusability --- spartan/releases/rough-rhino/Earthfile | 10 +++++----- spartan/releases/rough-rhino/README.md | 10 ++++++++-- spartan/releases/rough-rhino/aztec-spartan.sh | 8 ++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/spartan/releases/rough-rhino/Earthfile b/spartan/releases/rough-rhino/Earthfile index 259a9fd96dc..53e1f6365a7 100644 --- a/spartan/releases/rough-rhino/Earthfile +++ b/spartan/releases/rough-rhino/Earthfile @@ -39,7 +39,7 @@ test-no-config: test-install: FROM +test-setup # Test installation with CLI arguments - RUN echo -e "\n\n" | ./aztec-spartan.sh install \ + RUN echo -e "\n\n" | ./aztec-spartan.sh config \ -p 8080 \ -p2p 40400 \ -ip 1.2.3.4 \ @@ -51,8 +51,8 @@ test-install: grep -q "P2P_UDP_ANNOUNCE_ADDR=1.2.3.4:40400" docker-compose.yml && \ grep -q "AZTEC_PORT=8080" docker-compose.yml && \ grep -q "VALIDATOR_PRIVATE_KEY=0x00" docker-compose.yml && \ - echo "✅ Install test passed" || \ - (echo "❌ Install test failed" && exit 1) + echo "✅ Config test passed" || \ + (echo "❌ Config test failed" && exit 1) test-docker-check: FROM +deps @@ -61,7 +61,7 @@ test-docker-check: # Remove docker to test docker installation check RUN rm -f /usr/local/bin/docker /usr/local/bin/docker-compose # Test docker check (should fail since docker is not installed) - RUN if ./aztec-spartan.sh install 2>&1 | grep -q "Docker or Docker Compose not found"; then \ + RUN if ./aztec-spartan.sh config 2>&1 | grep -q "Docker or Docker Compose not found"; then \ echo "✅ Docker check test passed"; \ else \ echo "❌ Docker check test failed" && exit 1; \ @@ -70,7 +70,7 @@ test-docker-check: test-start-stop: FROM +test-setup # First install with test configuration - RUN echo -e "\n\n" | ./aztec-spartan.sh install \ + RUN echo -e "\n\n" | ./aztec-spartan.sh config \ -p 8080 \ -p2p 40400 \ -ip 1.2.3.4 \ diff --git a/spartan/releases/rough-rhino/README.md b/spartan/releases/rough-rhino/README.md index cf7e6f57d75..7e64b12a3aa 100644 --- a/spartan/releases/rough-rhino/README.md +++ b/spartan/releases/rough-rhino/README.md @@ -19,12 +19,18 @@ To configure a new node, create a new directory and run the install script: ```bash cd val1 -curl -L https://github.com/AztecProtocol/aztec-packages/blob/master/spartan/releases/rough-rhino/validator.sh | bash +curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/master/spartan/releases/rough-rhino/create-spartan.sh | bash +``` + +This will install `aztec-spartan.sh` in the current directory. You can now run it: + +```bash +./aztec-spartan.sh config ``` If you don't have Docker installed, the script will do it for you. It will then prompt for any required environment variables and output a `docker-compose.yml` file. -You can run the command without any command to see all available options, and pass them as flags, i.e. `npx aztec-spartan install -p 8080 -p2p 40400 -n nameme`. +You can run the command without any command to see all available options, and pass them as flags, i.e. `npx aztec-spartan config -p 8080 -p2p 40400 -n nameme`. ## Running diff --git a/spartan/releases/rough-rhino/aztec-spartan.sh b/spartan/releases/rough-rhino/aztec-spartan.sh index 2ed3a206226..5198a7bf78c 100755 --- a/spartan/releases/rough-rhino/aztec-spartan.sh +++ b/spartan/releases/rough-rhino/aztec-spartan.sh @@ -213,7 +213,7 @@ EOF # Docker commands start_node() { if [ ! -f "docker-compose.yml" ]; then - echo -e "${RED}Configuration not found. Please run './aztec-spartan.sh install' first.${NC}" + echo -e "${RED}Configuration not found. Please run './aztec-spartan.sh config' first.${NC}" exit 1 fi echo -e "${BLUE}Starting containers...${NC}" @@ -255,7 +255,7 @@ show_logs() { # Main script case "$1" in - "install") + "config") show_banner check_docker configure_environment "$@" @@ -273,9 +273,9 @@ case "$1" in show_logs ;; *) - echo "Usage: $0 {install|start|stop|update|logs}" + echo "Usage: $0 {config|start|stop|update|logs}" echo "Commands:" - echo " install - Install and configure Aztec Testnet node" + echo " config - Install and configure Aztec Testnet node" echo " start - Start Aztec Testnet node" echo " stop - Stop Aztec Testnet node" echo " update - Update Aztec Testnet node images"