├── dockerhub.png ├── .gitignore ├── Cargo.toml ├── src ├── light-node │ ├── config.yml │ ├── register_lc.sh │ └── main.sh └── utils │ ├── id_generator.rs │ ├── sophon_node.rs │ └── release.sh ├── .env.example ├── Dockerfile.local ├── Dockerfile ├── .github └── workflows │ └── docker.yml └── README.md /dockerhub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sophon-org/sophon-light-node/HEAD/dockerhub.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | avail_path 4 | .DS_Store 5 | /identity 6 | /release 7 | environment -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sophon-light-node" 3 | version = "0.0.105" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | avail-rust = { git = "https://github.com/availproject/avail.git", rev = "4f0439f" } 8 | clap = "4.5.20" 9 | 10 | [[bin]] 11 | name = "sophon-node" 12 | path = "src/utils/sophon_node.rs" 13 | 14 | [[bin]] 15 | name = "generate_node_id" 16 | path = "src/utils/id_generator.rs" 17 | -------------------------------------------------------------------------------- /src/light-node/config.yml: -------------------------------------------------------------------------------- 1 | project_name = "sophon" 2 | bootstraps=['/dns/bootnode.1.lightclient.mainnet-mmp.avail.so/tcp/37000/p2p/12D3KooWHgPbEYcvZvZz4aT3KiBdjQmphG5qJ9StWoUps1ofKbJx'] 3 | full_node_ws=['wss://mainnet.avail-rpc.com', 'wss://avail.api.onfinality.io/public-ws', 'wss://avail-us.brightlystake.com', 'wss://avail-rpc.publicnode.com'] 4 | genesis_hash = "b91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a" 5 | ot_collector_endpoint = "http://otel.lightclient.sophon.avail.so:4317" 6 | http_server_port = 7007 7 | http_server_host = "0.0.0.0" 8 | block_processing_delay = 20 9 | sophon_minimum_required_version = "0.0.90" -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Only needed if using Docker 2 | OPERATOR_ADDRESS= # your operator wallet address. Required if you want to participate on the rewards programme. 3 | DESTINATION_ADDRESS= # the destination wallet address. Defaults to OPERATOR_ADDRESS if not set. 4 | PERCENTAGE= # percentage this node will charge as rewards fee from delegators. Required if OPERATOR_ADDRESS is set, ignored otherwise. 5 | PUBLIC_DOMAIN= # public domain URL where the node is running. Required if OPERATOR_ADDRESS is set. 6 | MONITOR_URL= # Sophon's node monitor URL. 7 | VERSION_CHECKER_INTERVAL= # Interval in seconds to check for new version 8 | AUTO_UPGRADE= # Set to true to enable auto upgrade -------------------------------------------------------------------------------- /src/utils/id_generator.rs: -------------------------------------------------------------------------------- 1 | use avail_rust::{ 2 | sp_core::crypto::{self, Ss58Codec}, 3 | subxt_signer::SecretUri, 4 | Keypair, 5 | }; 6 | use std::env; 7 | use std::str::FromStr; 8 | 9 | fn main() { 10 | // get secret_uri (mnemonic) from command-line arguments 11 | let args: Vec = env::args().collect(); 12 | if args.len() != 2 { 13 | eprintln!("Usage: {} ", args[0]); 14 | std::process::exit(1); 15 | } 16 | let secret_uri_str = &args[1]; 17 | 18 | // parse secret_uri 19 | let secret_uri = SecretUri::from_str(secret_uri_str).expect("Invalid secret_uri"); 20 | 21 | // generate Keypair 22 | let avail_key_pair = Keypair::from_uri(&secret_uri).expect("Failed to generate Keypair"); 23 | 24 | let avail_address = avail_key_pair.public_key().to_account_id(); 25 | let avail_address = crypto::AccountId32::from(avail_address.0).to_ss58check(); 26 | // let avail_public_key = hex::encode(avail_key_pair.public_key()); 27 | 28 | println!("{}", avail_address); 29 | } 30 | -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM --platform=linux/amd64 rust:latest as builder 3 | 4 | WORKDIR /app 5 | COPY . . 6 | 7 | # Build for Linux 8 | RUN cargo build --release 9 | 10 | # Runtime stage 11 | FROM --platform=linux/amd64 ubuntu:latest 12 | 13 | # Install minimal dependencies 14 | RUN apt-get update && \ 15 | apt-get install -y --no-install-recommends \ 16 | ca-certificates \ 17 | curl \ 18 | jq \ 19 | file \ 20 | coreutils \ 21 | bc \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | WORKDIR /app 25 | 26 | # Write environment type 27 | RUN echo "stg" > environment 28 | 29 | # Copy the binary from builder stage 30 | COPY --from=builder /app/target/release/sophon-node /app/ 31 | RUN chmod +x sophon-node 32 | 33 | ENTRYPOINT ["/bin/sh", "-c"] 34 | CMD ["/app/sophon-node ${OPERATOR_ADDRESS:+--operator $OPERATOR_ADDRESS} ${DESTINATION_ADDRESS:+--destination $DESTINATION_ADDRESS} ${PERCENTAGE:+--percentage $PERCENTAGE} ${IDENTITY:+--identity $IDENTITY} ${PUBLIC_DOMAIN:+--public-domain $PUBLIC_DOMAIN} ${MONITOR_URL:+--monitor-url $MONITOR_URL} ${NETWORK:+--network $NETWORK} ${AUTO_UPGRADE:+--auto-upgrade $AUTO_UPGRADE}"] -------------------------------------------------------------------------------- /src/utils/sophon_node.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::process::Command; 3 | 4 | const MAIN_SCRIPT: &str = include_str!("../light-node/main.sh"); 5 | const REGISTER_SCRIPT: &str = include_str!("../light-node/register_lc.sh"); 6 | 7 | fn main() { 8 | // check if --version exists 9 | if std::env::args().any(|arg| arg == "--version" || arg == "-v") { 10 | println!("{}", env!("CARGO_PKG_VERSION")); 11 | std::process::exit(0); 12 | } 13 | 14 | // write scripts to disk 15 | fs::write("main.sh", MAIN_SCRIPT).expect("Failed to write main.sh"); 16 | fs::write("register_lc.sh", REGISTER_SCRIPT).expect("Failed to write register_lc.sh"); 17 | 18 | // make them executable 19 | Command::new("chmod") 20 | .args(&["+x", "main.sh", "register_lc.sh"]) 21 | .status() 22 | .expect("Failed to make scripts executable"); 23 | 24 | // get command line arguments 25 | let args: Vec = std::env::args().collect(); 26 | 27 | // execute main.sh with all passed arguments 28 | let status = Command::new("./main.sh") 29 | .args(&args[1..]) // skip the first arg which is the program name 30 | .status() 31 | .expect("failed to execute main.sh"); 32 | 33 | std::process::exit(status.code().unwrap_or(1)); 34 | } 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ARG BUILD_TYPE=prod 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends \ 7 | ca-certificates \ 8 | curl \ 9 | jq \ 10 | file \ 11 | coreutils \ 12 | bc \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | RUN groupadd -g 1001 sophon && \ 16 | useradd -u 1001 -g sophon -m -s /bin/bash sophon 17 | 18 | WORKDIR /app 19 | 20 | RUN echo "${BUILD_TYPE}" > environment 21 | 22 | # download binary based on the image tag 23 | RUN set -x && \ 24 | GITHUB_BASE_URL="https://api.github.com/repos/sophon-org/sophon-light-node/releases" && \ 25 | if [ "$(cat environment)" = "stg" ]; then \ 26 | RELEASE_INFO=$(curl -s ${GITHUB_BASE_URL} | jq '[.[] | select(.prerelease == true)][0]'); \ 27 | else \ 28 | RELEASE_INFO=$(curl -s ${GITHUB_BASE_URL}/latest); \ 29 | fi && \ 30 | BINARY_FILE_ID=$(echo "${RELEASE_INFO}" | jq -r '.assets[0] | select(.name | endswith("tar.gz")) | .id') && \ 31 | BINARY_FILE_NAME=$(echo "${RELEASE_INFO}" | jq -r '.assets[0] | select(.name | endswith("tar.gz")) | .name') && \ 32 | curl -L -H "Accept: application/octet-stream" "${GITHUB_BASE_URL}/assets/${BINARY_FILE_ID}" -o "${BINARY_FILE_NAME}" && \ 33 | tar -xzvf "${BINARY_FILE_NAME}" && \ 34 | rm "${BINARY_FILE_NAME}" && \ 35 | chmod +x sophon-node 36 | 37 | RUN chown -R sophon:sophon /app && \ 38 | chmod -R 775 /app 39 | 40 | ENTRYPOINT ["/bin/sh", "-c"] 41 | CMD ["/app/sophon-node ${OPERATOR_ADDRESS:+--operator $OPERATOR_ADDRESS} ${DESTINATION_ADDRESS:+--destination $DESTINATION_ADDRESS} ${PERCENTAGE:+--percentage $PERCENTAGE} ${IDENTITY:+--identity $IDENTITY} ${PUBLIC_DOMAIN:+--public-domain $PUBLIC_DOMAIN} ${MONITOR_URL:+--monitor-url $MONITOR_URL} ${NETWORK:+--network $NETWORK} ${AUTO_UPGRADE:+--auto-upgrade $AUTO_UPGRADE} ${OVERWRITE_CONFIG:+--overwrite-config $OVERWRITE_CONFIG}"] -------------------------------------------------------------------------------- /src/utils/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get current version from Cargo.toml and increment patch version 4 | CURRENT_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') 5 | 6 | if [ -z "$CURRENT_VERSION" ]; then 7 | echo "Error: Could not get current version from Cargo.toml" 8 | exit 1 9 | fi 10 | 11 | # Split version into parts 12 | IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" 13 | if [ ${#VERSION_PARTS[@]} -ne 3 ]; then 14 | echo "Error: Current version is not in format X.Y.Z" 15 | exit 1 16 | fi 17 | 18 | # Increment patch version 19 | ((VERSION_PARTS[2]++)) 20 | NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}" 21 | 22 | echo "Current version: $CURRENT_VERSION" 23 | echo "New version: $NEW_VERSION" 24 | 25 | # Ask for confirmation 26 | read -p "Proceed with release? (y/n) " -n 1 -r 27 | echo 28 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 29 | echo "Release cancelled" 30 | exit 1 31 | fi 32 | 33 | # Update version in Cargo.toml 34 | echo "Updating version in Cargo.toml..." 35 | if [[ "$OSTYPE" == "darwin"* ]]; then 36 | # macOS uses BSD sed which requires an empty string after -i 37 | sed -i '' "s/^version = \".*\"/version = \"${NEW_VERSION}\"/" Cargo.toml 38 | else 39 | # Linux version of sed 40 | sed -i "s/^version = \".*\"/version = \"${NEW_VERSION}\"/" Cargo.toml 41 | fi 42 | 43 | # Build release version 44 | echo "Building release version..." 45 | cargo build --release 46 | 47 | # Check if build was successful 48 | if [ $? -ne 0 ]; then 49 | echo "Error: Build failed" 50 | exit 1 51 | fi 52 | 53 | # Check if required binaries exist 54 | if [ ! -f "target/release/sophon-node" ] || [ ! -f "target/release/generate_node_id" ]; then 55 | echo "Error: Required binaries not found in target/release/" 56 | exit 1 57 | fi 58 | 59 | # Create release directory if it doesn't exist 60 | mkdir -p release 61 | 62 | # Copy binaries 63 | echo "Copying binaries to release directory..." 64 | cp target/release/sophon-node release/ 65 | cp target/release/generate_node_id release/ 66 | 67 | # Create tarball 68 | echo "Creating tarball..." 69 | cd release/ 70 | tar -czf "../binaries-${NEW_VERSION}.tar.gz" * 71 | cd .. 72 | 73 | # Check if gh command is available 74 | if ! command -v gh &> /dev/null; then 75 | echo "Error: GitHub CLI (gh) is not installed" 76 | echo "Please install it first: https://cli.github.com/" 77 | exit 1 78 | fi 79 | 80 | # Create GitHub release 81 | echo "Creating GitHub release v${NEW_VERSION}..." 82 | gh release create "v${NEW_VERSION}" \ 83 | --title "Version ${NEW_VERSION}" \ 84 | --notes "" \ 85 | --prerelease=false \ 86 | --generate-notes=false \ 87 | "binaries-${NEW_VERSION}.tar.gz" 88 | 89 | echo "Release v${NEW_VERSION} created successfully!" -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Release & Dockerize 2 | 3 | on: 4 | push: 5 | branches: [main, staging] 6 | 7 | jobs: 8 | build-and-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: stable 18 | override: true 19 | 20 | - id: version 21 | run: | 22 | CURRENT_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') 23 | 24 | if [[ ${{ github.ref }} == 'refs/heads/main' ]]; then 25 | IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" 26 | ((VERSION_PARTS[2]++)) 27 | NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}" 28 | else 29 | IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" 30 | ((VERSION_PARTS[2]++)) 31 | NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}-stg" 32 | fi 33 | 34 | echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT 35 | 36 | - name: Update version in Cargo.toml 37 | if: github.ref == 'refs/heads/main' 38 | run: | 39 | VERSION=${{ steps.version.outputs.version }} 40 | sed -i "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml 41 | git config user.name "GitHub Actions" 42 | git config user.email "github-actions@github.com" 43 | git add Cargo.toml 44 | git commit -m "Update Cargo.toml version to $VERSION" 45 | git push 46 | 47 | - name: Build release 48 | run: cargo build --release 49 | 50 | - name: Check binaries 51 | run: | 52 | if [ ! -f "target/release/sophon-node" ] || [ ! -f "target/release/generate_node_id" ]; then 53 | echo "Error: Required binaries not found in target/release/" 54 | exit 1 55 | fi 56 | 57 | - name: Create tarball 58 | run: | 59 | VERSION=${{ steps.version.outputs.version }} 60 | mkdir -p release 61 | cp target/release/sophon-node release/ 62 | cp target/release/generate_node_id release/ 63 | cd release/ 64 | tar -czf "../binaries-v${VERSION}.tar.gz" * 65 | cd .. 66 | 67 | - name: Create Git Tag 68 | run: | 69 | VERSION=${{ steps.version.outputs.version }} 70 | 71 | # check if tag exists 72 | if git rev-parse "v$VERSION" >/dev/null 2>&1; then 73 | echo "Tag v$VERSION already exists, skipping tag creation" 74 | exit 0 75 | else 76 | git config user.name "GitHub Actions" 77 | git config user.email "github-actions@github.com" 78 | git tag -a "v$VERSION" -m "Release v$VERSION" 79 | git push origin "v$VERSION" 80 | fi 81 | 82 | - name: Delete Tag and Release 83 | uses: ClementTsang/delete-tag-and-release@v0.3.1 84 | with: 85 | delete_release: true 86 | tag_name: v${{ steps.version.outputs.version }} 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | 90 | - name: Create Release 91 | id: create_release 92 | uses: actions/create-release@v1 93 | env: 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | with: 96 | tag_name: v${{ steps.version.outputs.version }} 97 | release_name: Release v${{ steps.version.outputs.version }} 98 | draft: false 99 | prerelease: ${{ github.ref != 'refs/heads/main' }} 100 | 101 | - name: Upload Release Asset 102 | uses: actions/upload-release-asset@v1 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | with: 106 | upload_url: ${{ steps.create_release.outputs.upload_url }} 107 | asset_path: ./binaries-v${{ steps.version.outputs.version }}.tar.gz 108 | asset_name: binaries-v${{ steps.version.outputs.version }}.tar.gz 109 | asset_content_type: application/gzip 110 | 111 | - uses: docker/login-action@v2 112 | with: 113 | username: ${{ secrets.DOCKER_USERNAME }} 114 | password: ${{ secrets.DOCKER_PASSWORD }} 115 | 116 | - uses: docker/setup-buildx-action@v2 117 | 118 | - name: Build and Push to Dockerhub 119 | run: | 120 | VERSION=${{ steps.version.outputs.version }} 121 | 122 | if [[ ${{ github.ref }} == 'refs/heads/main' ]]; then 123 | docker build --build-arg BUILD_TYPE=prod --platform linux/amd64 \ 124 | -t sophonhub/sophon-light-node:v${VERSION} \ 125 | -t sophonhub/sophon-light-node:latest . 126 | docker push sophonhub/sophon-light-node:v${VERSION} 127 | docker push sophonhub/sophon-light-node:latest 128 | else 129 | docker build --build-arg BUILD_TYPE=stg --platform linux/amd64 \ 130 | -t sophonhub/sophon-light-node:v${VERSION} \ 131 | -t sophonhub/sophon-light-node:latest-stg . 132 | docker push sophonhub/sophon-light-node:v${VERSION} 133 | docker push sophonhub/sophon-light-node:latest-stg 134 | fi -------------------------------------------------------------------------------- /src/light-node/register_lc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Function definitions 5 | log() { 6 | echo "$(date '+%Y-%m-%d %H:%M:%S') $1" 7 | } 8 | 9 | die() { 10 | log "🛑 $1" >&2 11 | exit 1 12 | } 13 | 14 | validate_requirements() { 15 | command -v jq >/dev/null 2>&1 || die "jq is required but not installed" 16 | command -v curl >/dev/null 2>&1 || die "curl is required but not installed" 17 | 18 | [ -n "${operator:-}" ] || die "\`operator\` parameter is required" 19 | 20 | # validate operator-related parameters 21 | if [ -n "${operator:-}" ]; then 22 | [ -n "${percentage:-}" ] || die "\`percentage\` parameter is required when operator is set" 23 | [[ "$percentage" =~ ^[0-9]+(\.[0-9]{1,2})?$ ]] || die "\`percentage\` must be a decimal value with at most 2 decimal places" 24 | [ -n "${public_domain:-}" ] || die "\`public-domain\` parameter is required when operator is set" 25 | [ -n "${identity:-}" ] || die "\`identity\` parameter is required" 26 | [ -n "${monitor_url:-}" ] || die "\`monitor-url\` parameter is required" 27 | fi 28 | } 29 | 30 | parse_args() { 31 | while [ $# -gt 0 ]; do 32 | if [[ $1 == "--"* ]]; then 33 | local key="${1/--/}" 34 | key="${key//-/_}" 35 | eval "$key=\"$2\"" 36 | shift 2 37 | else 38 | shift 39 | fi 40 | done 41 | } 42 | 43 | extract_secret_uri() { 44 | local identity_file="$1" 45 | local secret_uri 46 | 47 | [ -f "$identity_file" ] || die "identity.toml file not found at $identity_file" 48 | 49 | secret_uri=$(sed -n "s/^[[:space:]]*avail_secret_uri[[:space:]]*=[[:space:]]*'\(.*\)'/\1/p" "$identity_file") 50 | [ -n "$secret_uri" ] || die "Could not extract avail_secret_uri from $identity_file" 51 | 52 | echo "$secret_uri" 53 | } 54 | 55 | generate_node_id() { 56 | local secret_uri="$1" 57 | local generator_path 58 | local node_id 59 | 60 | generator_path=$([ -f "./generate_node_id" ] && echo "./generate_node_id" || echo "./release/generate_node_id") 61 | [ -f "$generator_path" ] || die "generate_node_id binary not found" 62 | 63 | node_id=$("$generator_path" "$secret_uri") || die "Failed to generate node ID" 64 | [ -n "$node_id" ] || die "Generated node ID is empty" 65 | 66 | echo "$node_id" 67 | } 68 | 69 | register_node() { 70 | local endpoint="$1" 71 | local payload="$2" 72 | local response 73 | local http_status 74 | 75 | log "🔗 Registering node at $endpoint with payload: $payload" 76 | response=$(curl -s -w "\n%{http_code}" -X POST "$endpoint" \ 77 | -H "Content-Type: application/json" \ 78 | -d "$payload") 79 | 80 | http_status=$(echo "$response" | tail -n1) 81 | response_body=$(echo "$response" | sed '$d') 82 | error=$(echo "$response_body" | jq -r '.error // empty') 83 | 84 | case $http_status in 85 | 200) 86 | log "☎️ Response: $response_body" 87 | log "✅ Node registered/sync'd successfully!" 88 | ;; 89 | 400) 90 | die "Bad request: $response_body" 91 | ;; 92 | 403) 93 | echo $error 94 | # if error exists, die with error message 95 | [ -z "$error" ] || die "$error" 96 | return 0 97 | ;; 98 | 500) 99 | die "Server error: $response_body" 100 | ;; 101 | *) 102 | die "Unexpected HTTP status code: $http_status" 103 | ;; 104 | esac 105 | } 106 | 107 | main() { 108 | log "📝 Registering Light Node on Sophon's monitor" 109 | 110 | parse_args "$@" 111 | validate_requirements 112 | 113 | # Normalize public_domain format 114 | [[ "$public_domain" =~ ^http ]] || public_domain="https://$public_domain" 115 | 116 | # Extract and validate secret URI 117 | log "🔍 Processing identity file at $identity..." 118 | secret_uri=$(extract_secret_uri "$identity") 119 | 120 | # Generate node ID 121 | node_id=$(generate_node_id "$secret_uri") 122 | 123 | # Prepare registration payload 124 | json_payload=$(jq -n \ 125 | --arg identity "$node_id" \ 126 | --arg url "$public_domain" \ 127 | --arg operator "$operator" \ 128 | --argjson percentage "$percentage" \ 129 | '{identity: $identity, url: $url, operator: $operator, percentage: $percentage}') 130 | 131 | if [ -n "${destination:-}" ]; then 132 | json_payload=$(echo "$json_payload" | jq --arg destination "$destination" '.destination = $destination') 133 | fi 134 | 135 | # Display node info 136 | log "+$(printf '%*s' "100" | tr ' ' '-')+" 137 | log "| 🆔 Node identity: $node_id" 138 | log "| 🌐 Public domain: $public_domain" 139 | log "| 👛 Operator address: $operator" 140 | log "| 🏦 Destination address: ${destination:-N/A}" 141 | log "| 💰 Percentage: $percentage%" 142 | log "| 📡 Monitor URL: $monitor_url" 143 | log "+$(printf '%*s' "100" | tr ' ' '-')+" 144 | 145 | # Register node 146 | register_node "$monitor_url/nodes" "$json_payload" 147 | } 148 | 149 | main "$@" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sophon Light Node 2 | 3 | The Sophon Light Node are lightweight software designed to perform simpler tasks within the Sophon network. It is a cost-effective way to participate in the network, increasing the opportunity for Sophon Guardians to be rewarded, and for the community to participate. 4 | 5 | Know more in our [Docs](https://docs.sophon.xyz/sophon/sophon-guardians-and-nodes/sophon-nodes). 6 | 7 | ## How to run your Sophon's Light Node 8 | 9 | For a guided experience, you can visit the [Guardians Dashboard](https://guardian.sophon.xyz). 10 | 11 | ### Using third party node as a service providers 12 | 13 | Here is a list of providers that allow you to run a Sophon Light Node directly in their interface: 14 | 15 | - [Easeflow](https://easeflow.io) 16 | 17 | ### Using Railway 18 | 19 | Use our Railway template to spin up an Sophon Light Node in one click 20 | 21 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/wEhaxi?referralCode=qB-i6S) 22 | 23 | Bare in mind that you must have a Railway account and that costs might apply. 24 | 25 | ### Using Docker 26 | 27 | Use our Docker image to run the Light Node anywhere you want. The main requirement is that you must be able to expose the container to the internet through a fixed domain/URL/IP. 28 | 29 | 30 | Docker 31 | 32 | 33 | More info on the following env variables on [Environment variables](#environment-variables) 34 | 35 | ``` 36 | # pull docker image 37 | docker pull --platform linux/amd64 sophonhub/sophon-light-node 38 | 39 | # if you want to use the testnet environment 40 | docker pull --platform linux/amd64 sophonhub/sophon-light-node:latest-stg 41 | 42 | # run node 43 | docker run -d --name sophon-light-node sophonhub/sophon-light-node 44 | 45 | # if you want to be eligible for rewards you must pass the required env vars 46 | docker run -d --name sophon-light-node \ 47 | --restart on-failure:5 \ 48 | -e OPERATOR_ADDRESS= \ 49 | -e DESTINATION_ADDRESS= \ 50 | -e PERCENTAGE= \ 51 | -e PUBLIC_DOMAIN= \ 52 | -e PORT= \ 53 | -p : \ 54 | sophonhub/sophon-light-node 55 | 56 | # Don't forget to change to sophonhub/sophon-light-node:latest-stg for testnet in case you have both images 57 | ``` 58 | 59 | ## Reliability 60 | 61 | If you are running the node on any VPS (including Railway) or even locally on your machine, it is important you set up some monitoring to ensure your node is always up and running. Even though we will have some tolerance to failures and downtimes, our rewards programme will calculate rewards based on uptime. 62 | 63 | ## Environment variables 64 | 65 | While this is not required to run a node, bear in mind that if you want to participate in Sophon's Guardians Reward Program, you MUST set your operator wallet address. _If you either do not pass any address or you pass an address that doesn't contain any Guardian Membership delegations, the node will run but you won't be eligible for rewards. Now more about rewards in our [Docs](https://docs.sophon.xyz/sophon/sophon-guardians-and-nodes/node-rewards)._ 66 | 67 | If you're using Railway, all variables are pre-populated for you except for your **operator wallet address** and **percentage**. 68 | If decide not to use Railway, you can use our Docker image making sure to set the following environment variables: 69 | 70 | ``` 71 | OPERATOR_ADDRESS= # [OPTIONAL] Your Light Node operator address, which is the one that must receive delegations to be eligible to receive rewards. The more delegations, the more rewards, with a cap limit of 20 delegations. **Required** if you want to participate on the rewards programme. 72 | 73 | DESTINATION_ADDRESS= # [OPTIONAL] this is the wallet address that will receive rewards from the Guardians programme (based on the percetage defined above). Most of the times it will be the operator address, but you can define a different one. Defaults to OPERATOR_ADDRESS if not set. 74 | 75 | PERCENTAGE= # [OPTIONAL] The percentage this node will charge as rewards fee from delegators. Basically, rewards are calculated based on delegated amount, and this percentage defines how much goes to you as node operator, and the rest goes to delegators. It must be a decimal from 0.00 to 100. Only 2 decimals allowed. **Required** if OPERATOR_ADDRESS is set, ignored otherwise. Once it is set, it CAN NOT be modified. 76 | 77 | PUBLIC_DOMAIN= # [OPTIONAL] this is the public domain URL/IP where the node is running so it can be reach by the monitoring servers. Please include protocol or it defaults to HTTPS. **Required** if OPERATOR_ADDRESS is set. 78 | 79 | PORT= # [OPTIONAL] In case you want the service to run on a different PORT than 7007 80 | ``` 81 | 82 | ## FAQ 83 | 84 | ### How do I earn rewards? 85 | 86 | To be able to earn rewards you need to make sure that the Light Node is registered on Sophon so we can monitor your node's activity. 87 | 88 | By using the Railway template (or the Docker image), we automatically register your node for you given the right environment variables are passed. 89 | 90 | ### I want to retrieve all nodes information 91 | 92 | ``` 93 | curl -X GET "https://monitor.sophon.xyz/nodes" 94 | ``` 95 | 96 | ### I want to retrieve my node information 97 | 98 | You can filter nodes by using the `operators` filter. It takes comma-separated addresses. 99 | 100 | ``` 101 | curl -X GET "https://monitor.sophon.xyz/nodes?operators=0xOPERATOR1,0xOPERATOR2" 102 | ``` 103 | 104 | Additionally, you can also define the `page` and `per_page` params. For example: 105 | 106 | ``` 107 | curl -X GET "https://monitor.sophon.xyz/nodes?page=3&per_page=10" 108 | ``` 109 | 110 | will return 10 nodes per page and we are asking for the 3rd page. 111 | 112 | ### I want to change some params of my node 113 | 114 | You can change your node's URL (or IP) and/or destination address: 115 | 116 | ``` 117 | curl -X PUT "https://monitor.sophon.xyz/nodes" \ 118 | -H "Content-Type: application/json" \ 119 | -H "Authorization: Bearer SIGNED_MESSAGE" \ 120 | -d '{ "operator": "OPERATOR_ADDRESS", "url": "NEW_URL", "destination": "NEW_DESTINATION", "timestamp": TIMESTAMP}' 121 | ``` 122 | 123 | _This calls requires you to sign a message so we can verify you are the owner of the operator address._ 124 | 125 | ### How do I change the percentage? 126 | 127 | Once you have set a percentage, it CAN NOT be modified. 128 | You will have to create a new operator address and ask for new delegations. 129 | 130 | ### I want to delete my node 131 | 132 | Registered nodes can not be deleted. 133 | 134 | ### How do I sign the authorization message? 135 | 136 | The signed message is a UNIX timestamp (in seconds format) signed with your operator wallet. Signatures expire after 15 minutes. 137 | 138 | You can use [Etherscan](https://etherscan.io/verifiedSignatures#) to sign messages. 139 | 140 | **Example to change your node's URL** 141 | 142 | 1. Grab the current UNIX timestamp (you can use this website https://www.unixtimestamp.com). Let's say its `1736417259` 143 | 144 | 2. Sign the message `1736417259` string using your operator wallet. One easy way to do this would be to use https://etherscan.io/verifiedSignatures#. Open the link, click on the "Sign Message" button on the top right corner, connect your wallet, then input your message, which is simply the UNIX timestamp (e.g `1736417259`). 145 | 146 | 3. Copy the signed message and use it on the `Authorization` header -> `Authorization: Bearer SIGNED_MESSAGE` 147 | 148 | 4. Send the request: 149 | ``` 150 | curl -X PUT "https://monitor.sophon.xyz/nodes" \ 151 | -H "Content-Type: application/json" \ 152 | -H "Authorization: Bearer SIGNED_MESSAGE" \ 153 | -d '{ "operator": "OPERATOR_ADDRESS", "url": "NEW_URL", "timestamp": 1736417259}' 154 | ``` 155 | 156 | ### Do these endpoints have a rate limit? 157 | 158 | All endpoints are protected by rate limiting. The current configuration allows: 159 | 160 | - 60 requests per minute per IP address 161 | - Rate limit counters are stored in Redis with a 1-minute expiry 162 | - When limit is exceeded, returns 429 Too Many Requests status code 163 | 164 | Rate limit headers in responses: 165 | 166 | - `X-RateLimit-Limit`: Maximum number of requests allowed per minute 167 | - `X-RateLimit-Remaining`: Number of requests remaining in the current window 168 | -------------------------------------------------------------------------------- /src/light-node/main.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Constants 5 | readonly DEFAULT_NETWORK="mainnet" 6 | readonly PROD_MONITOR_URL="https://monitor.sophon.xyz" 7 | readonly STG_MONITOR_URL="https://monitor-stg.sophon.xyz" 8 | readonly DEFAULT_VERSION_CHECKER_INTERVAL=86400 # 1 day 9 | readonly DEFAULT_ENV="prod" 10 | readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 11 | readonly CONFIG_URL="https://raw.githubusercontent.com/sophon-org/sophon-light-node/refs/heads/main/src/light-node/config.yml" 12 | 13 | # Version checks 14 | get_latest_version_info() { 15 | if [ $1 = "stg" ]; then 16 | result=$(curl -s -H "Cache-Control: no-cache" https://api.github.com/repos/sophon-org/sophon-light-node/releases | 17 | jq '[.[] | select(.prerelease == true)][0]') 18 | if [ -z "$result" ]; then 19 | echo "No staging release found." 20 | exit 1 21 | fi 22 | echo "$result" 23 | else 24 | curl -s -H "Cache-Control: no-cache" https://api.github.com/repos/sophon-org/sophon-light-node/releases/latest 25 | fi 26 | } 27 | 28 | get_latest_version() { 29 | # returns raw tag without the 'v' prefix 30 | echo $(get_latest_version_info $1 | jq -r '.tag_name' | sed 's/^v//') 31 | } 32 | 33 | get_minimum_version() { 34 | local config_response 35 | 36 | config_response=$(curl -s "$CONFIG_URL") 37 | if [ $? -ne 0 ]; then 38 | echo "0.0.0" 39 | fi 40 | 41 | local min_version 42 | min_version=$(echo "$config_response" | grep "sophon_minimum_required_version" | cut -d'"' -f2 || echo "") 43 | 44 | if [ -z "$min_version" ]; then 45 | echo "0.0.0" 46 | fi 47 | 48 | # strip any -stg suffix for minimum version comparison 49 | echo "$min_version" | sed 's/-stg$//' 50 | } 51 | 52 | get_current_version() { 53 | local version="0.0.0" 54 | 55 | if [ -f "./sophon-node" ] && [ -x "./sophon-node" ]; then 56 | version=$(./sophon-node --version 2>/dev/null || echo "0.0.0") 57 | elif [ -f "./target/release/sophon-node" ] && [ -x "./target/release/sophon-node" ]; then 58 | version=$(./target/release/sophon-node --version 2>/dev/null || echo "0.0.0") 59 | fi 60 | 61 | # remove 'v' prefix if present 62 | echo "$version" | sed 's/^v//' 63 | } 64 | 65 | compare_versions() { 66 | # remove the -stg suffix if present and v prefix for comparison 67 | local v1=$(echo "$1" | sed 's/^v//; s/-stg$//') 68 | local v2=$(echo "$2" | sed 's/^v//; s/-stg$//') 69 | 70 | if [[ -z "$v1" || -z "$v2" ]]; then 71 | echo "Error: Missing version input" >&2 72 | return 1 73 | fi 74 | 75 | if [[ "$v1" == "$v2" ]]; then 76 | echo 0 77 | elif [[ "$(echo -e "$v1\n$v2" | sort -V | head -n1)" == "$v1" ]]; then 78 | echo -1 # v1 is lower 79 | else 80 | echo 1 # v1 is higher 81 | fi 82 | } 83 | 84 | update_version() { 85 | local env="$1" 86 | local latest_version="$2" 87 | log "📥 Downloading version $latest_version..." 88 | 89 | # Get release info 90 | local release_info=$(get_latest_version_info $env) 91 | local asset_url=$(echo "$release_info" | jq -r '.assets[0].url') 92 | local binary_name=$(echo "$release_info" | jq -r '.assets[0].name') 93 | 94 | if [ -z "$asset_url" ] || [ "$asset_url" = "null" ]; then 95 | die "Error: No assets found in release" 96 | fi 97 | 98 | # Create temp directory for update 99 | local temp_dir=$(mktemp -d) 100 | cd "$temp_dir" 101 | 102 | log "🔍 Downloading from: $asset_url" 103 | curl -L \ 104 | -H "Accept: application/octet-stream" \ 105 | -o "$binary_name" \ 106 | "$asset_url" 107 | 108 | # Verify download 109 | if [ ! -f "$binary_name" ] || [ ! -s "$binary_name" ]; then 110 | rm -rf "$temp_dir" 111 | die "Error: Download failed or file is empty" 112 | fi 113 | 114 | # Basic tar check 115 | if ! tar -tzf "$binary_name" >/dev/null 2>&1; then 116 | rm -rf "$temp_dir" 117 | die "Error: Downloaded file is not a valid tar.gz archive" 118 | fi 119 | 120 | # Extract archive 121 | log "📦 Extracting new version..." 122 | tar -xzf "$binary_name" || { 123 | rm -rf "$temp_dir" 124 | die "Error: Failed to extract archive" 125 | } 126 | 127 | # Look for the binary (assuming it's named sophon-node or has similar name) 128 | local extracted_binary 129 | for possible_name in "sophon-node" "sophon" "node"; do 130 | if [ -f "$possible_name" ]; then 131 | extracted_binary="$possible_name" 132 | break 133 | fi 134 | done 135 | 136 | if [ -z "${extracted_binary:-}" ]; then 137 | # If not found by name, take the first file that's not the archive 138 | extracted_binary=$(ls -1 | grep -v "$binary_name" | head -n1) 139 | fi 140 | 141 | if [ -z "${extracted_binary:-}" ]; then 142 | rm -rf "$temp_dir" 143 | die "Error: Could not find binary in archive" 144 | fi 145 | 146 | # Update binary 147 | log "🔄 Updating binary..." 148 | chmod +x "$extracted_binary" 149 | mv "$extracted_binary" "$SCRIPT_DIR/sophon-node" 150 | 151 | # Cleanup 152 | cd - > /dev/null 153 | rm -rf "$temp_dir" 154 | 155 | log "✅ Successfully updated to version $latest_version!" 156 | return 0 157 | } 158 | 159 | check_version() { 160 | log "🔍 Checking version requirements..." 161 | local env="$1" 162 | local auto_upgrade="${2:-false}" 163 | 164 | 165 | # if staging environment, we skip the version check 166 | if [ "$env" = "stg" ]; then 167 | log "🔍 Skipping version check for staging environment" 168 | return 1 169 | fi 170 | 171 | local latest_version current_version minimum_version 172 | { 173 | latest_version=$(get_latest_version $env) 174 | current_version=$(get_current_version) 175 | minimum_version=$(get_minimum_version) 176 | } 2>/dev/null 177 | 178 | # If current version is 0.0.0, assume it's a new installation 179 | if [ "$current_version" = "0.0.0" ]; then 180 | log "🚀 New installation detected" 181 | return 1 182 | fi 183 | 184 | # If below minimum version - die 185 | if [ $(compare_versions $current_version $minimum_version) -lt 0 ]; then 186 | die "Current version ($current_version) is below minimum required version ($minimum_version). Node process will be terminated." 187 | fi 188 | 189 | # Check if update is available 190 | if [ $(compare_versions $current_version $latest_version) -lt 0 ]; then 191 | if [ "$auto_upgrade" = "true" ]; then 192 | log "$(box "🔔 [VERSION OUTDATED]" "🔄 Auto-upgrade enabled. Upgrading from $current_version to $latest_version...")" 193 | if update_version "$env" "$latest_version"; then 194 | return 0 # Signal to restart 195 | else 196 | log "❌ Update failed, continuing with current version." 197 | return 1 198 | fi 199 | else 200 | log "$(box "🔔 [VERSION OUTDATED]" "🔔 Minimum required version: $minimum_version 201 | | 🔔 Current version: $current_version 202 | | 🔔 Latest version: $latest_version 203 | | 🔔 Consider upgrading or use --auto-upgrade true to enable automatic updates.")" 204 | return 1 205 | fi 206 | fi 207 | 208 | log "✅ Running latest version: $current_version" 209 | return 1 210 | } 211 | 212 | # Function definitions 213 | box() { 214 | local title="$1" 215 | local message="${2:-}" 216 | 217 | if [ -z "$message" ]; then 218 | # only print title 219 | cat << EOF 220 | 221 | +$(printf '%*s' "100" | tr ' ' '-')+ 222 | | $title 223 | +$(printf '%*s' "100" | tr ' ' '-')+ 224 | EOF 225 | else 226 | # print title and message 227 | cat << EOF 228 | 229 | +$(printf '%*s' "100" | tr ' ' '-')+ 230 | | $title 231 | | $message 232 | +$(printf '%*s' "100" | tr ' ' '-')+ 233 | EOF 234 | fi 235 | } 236 | 237 | log() { 238 | local message="$(date '+%Y-%m-%d %H:%M:%S') $1" 239 | echo -e "$message" 240 | } 241 | 242 | die() { 243 | log "❌ $1" >&2 244 | exit 1 245 | } 246 | 247 | validate_requirements() { 248 | [ -f "$SCRIPT_DIR/register_lc.sh" ] || die "register_lc.sh not found" 249 | chmod +x "$SCRIPT_DIR/register_lc.sh" 250 | command -v jq >/dev/null 2>&1 || die "jq is required but not installed" 251 | command -v curl >/dev/null 2>&1 || die "curl is required but not installed" 252 | 253 | # validate operator-related parameters 254 | if [ -n "${operator:-}" ]; then 255 | [ -n "${percentage:-}" ] || die "\`percentage\` parameter is required when operator is set" 256 | [[ "$percentage" =~ ^[0-9]+(\.[0-9]{1,2})?$ ]] || die "\`percentage\` must be a decimal value with at most 2 decimal places" 257 | [ -n "${public_domain:-}" ] || die "\`public-domain\` parameter is required when operator is set" 258 | [ -n "${identity:-}" ] || die "\`identity\` parameter is required" 259 | [ -n "${monitor_url:-}" ] || die "\`monitor-url\` parameter is required" 260 | fi 261 | } 262 | 263 | detect_environment() { 264 | # try environment config file in current directory 265 | if [ -f "environment" ]; then 266 | cat environment 267 | else 268 | # default to prod for local development 269 | echo "prod" 270 | fi 271 | } 272 | 273 | parse_args() { 274 | # Initialize variables with defaults 275 | env="$DEFAULT_ENV" 276 | network="$DEFAULT_NETWORK" 277 | operator="" 278 | destination="" 279 | percentage="" 280 | public_domain="" 281 | identity="$HOME/.avail/identity/identity.toml" 282 | auto_upgrade="false" 283 | overwrite_config="true" 284 | VERSION_CHECKER_INTERVAL="${VERSION_CHECKER_INTERVAL:-$DEFAULT_VERSION_CHECKER_INTERVAL}" 285 | 286 | # Parse command line arguments 287 | while [ $# -gt 0 ]; do 288 | case "$1" in 289 | --operator) 290 | operator="$2" 291 | shift 2 292 | ;; 293 | --destination) 294 | destination="$2" 295 | shift 2 296 | ;; 297 | --percentage) 298 | percentage="$2" 299 | shift 2 300 | ;; 301 | --identity) 302 | identity="$2" 303 | shift 2 304 | ;; 305 | --public-domain) 306 | public_domain="$2" 307 | shift 2 308 | ;; 309 | --monitor-url) 310 | monitor_url="$2" 311 | shift 2 312 | ;; 313 | --network) 314 | network="$2" 315 | shift 2 316 | ;; 317 | --auto-upgrade) 318 | auto_upgrade="$2" 319 | shift 2 320 | ;; 321 | --overwrite-config) 322 | overwrite_config="$2" 323 | shift 2 324 | ;; 325 | *) 326 | die "Unknown option: $1" 327 | ;; 328 | esac 329 | done 330 | 331 | # read the baked-in environment that can't be overridden 332 | env=$(detect_environment) 333 | log "🔍 Environment: $env" 334 | 335 | # set monitor_url based on baked-in environment 336 | case "$env" in 337 | stg) 338 | monitor_url="$STG_MONITOR_URL" 339 | ;; 340 | *) 341 | monitor_url="$PROD_MONITOR_URL" 342 | ;; 343 | esac 344 | log "🔍 Monitor URL: $monitor_url" 345 | 346 | # export variables for child scripts 347 | export env network monitor_url operator destination percentage public_domain identity auto_upgrade 348 | } 349 | 350 | wait_for_node() { 351 | local public_domain="$1" 352 | # ensure public_domain starts with https:// if it doesn't contain http or https 353 | if [[ ! "$public_domain" =~ ^http ]]; then 354 | public_domain="https://$public_domain" 355 | fi 356 | local health_endpoint="$public_domain/v2/status" 357 | local timeout=300 # 5 minutes 358 | local interval=5 359 | local start_time 360 | local elapsed_time=0 361 | 362 | start_time=$(date +%s) 363 | 364 | log "🏥 Waiting for node at: $health_endpoint to be ready... ($timeout seconds remaining)" 365 | while [ $elapsed_time -lt $timeout ]; do 366 | if status_code=$(curl -s -w "%{http_code}" -o /tmp/health_response "$health_endpoint") && \ 367 | [ "$status_code" = "200" ] && \ 368 | response=$(cat /tmp/health_response) && \ 369 | first_block=$(echo "$response" | jq -r '.blocks.available.first') && \ 370 | [ "$first_block" != "null" ]; then 371 | log "☀️ Node is up! First available block: $first_block" 372 | return 0 373 | fi 374 | 375 | elapsed_time=$(($(date +%s) - start_time)) 376 | remaining=$((timeout - elapsed_time)) 377 | 378 | [ -n "${response:-}" ] && log "🔗 Node health response: $response" 379 | log "🏥 Waiting for node at: $health_endpoint to be ready... ($remaining seconds remaining)" 380 | sleep $interval 381 | done 382 | 383 | die "Timeout waiting for node to start" 384 | } 385 | 386 | run_node() { 387 | log "🏁 Running availup..." 388 | availup_pid="" 389 | avail_light_pid="" 390 | 391 | cleanup_and_exit() { 392 | local message="$1" 393 | log "🔍 Debug: Cleanup triggered with message: $message" 394 | 395 | if [ -n "$availup_pid" ]; then 396 | log "🔍 Debug: Killing availup process $availup_pid" 397 | kill "$availup_pid" 2>/dev/null || true 398 | fi 399 | 400 | if [ -n "$avail_light_pid" ]; then 401 | log "🔍 Debug: Killing avail-light process $avail_light_pid" 402 | kill "$avail_light_pid" 2>/dev/null || true 403 | fi 404 | exit 1 405 | } 406 | 407 | check_process_health() { 408 | # Only care about SIGCHLD if avail-light process dies 409 | if [ -n "$avail_light_pid" ] && ! ps -p $avail_light_pid > /dev/null 2>&1; then 410 | cleanup_and_exit "Avail-light process died unexpectedly" 411 | fi 412 | } 413 | 414 | # Check if we need custom config 415 | config_file=$(create_avail_config) 416 | 417 | # Convert true/false to yes/no for upgrade parameter 418 | avail_upgrade_value=$([ "$auto_upgrade" = "true" ] && echo "y" || echo "n") 419 | 420 | # Start availup in background 421 | curl -sL1 avail.sh | bash -s -- \ 422 | --network "$network" \ 423 | --config "$config_file" \ 424 | --upgrade $avail_upgrade_value \ 425 | --identity "$identity" > >(while read -r line; do 426 | log "$line" 427 | done) \ 428 | 2> >(while read -r line; do 429 | log "$line" 430 | done) & 431 | 432 | availup_pid=$! 433 | log "🔍 Availup started with PID: $availup_pid" 434 | 435 | # Set up traps 436 | trap 'cleanup_and_exit "Node terminated by SIGINT"' SIGINT 437 | trap 'cleanup_and_exit "Node terminated by SIGTERM"' SIGTERM 438 | trap 'check_process_health' SIGCHLD 439 | 440 | # Wait a bit for avail-light to start 441 | sleep 5 442 | 443 | # Get avail-light process PID 444 | avail_light_pid=$(pgrep -f "avail-light") 445 | if [ -n "$avail_light_pid" ]; then 446 | log "🔍 Avail-light process found with PID: $avail_light_pid" 447 | else 448 | log "❌ Avail-light process not found" 449 | cleanup_and_exit 450 | fi 451 | 452 | # Only register if operator is provided 453 | if [ -n "$operator" ]; then 454 | if [ -z "$public_domain" ]; then 455 | die "public-domain is required when operator is specified" 456 | fi 457 | 458 | # Wait for node to be ready before registration 459 | wait_for_node "$public_domain" 460 | 461 | "$SCRIPT_DIR/register_lc.sh" \ 462 | --operator "$operator" \ 463 | --destination "$destination" \ 464 | --percentage "$percentage" \ 465 | --identity "$identity" \ 466 | --public-domain "$public_domain" \ 467 | --monitor-url "$monitor_url" || { 468 | kill $availup_pid 2>/dev/null || true 469 | die "Registration failed - node terminated" 470 | } 471 | else 472 | log "$(box "🔔 [NOT ELIGIBLE FOR REWARDS]" "🔔 You have not provided an operator address. Your Sophon Light Node will run but not participate in the rewards program.")" 473 | fi 474 | } 475 | 476 | wait_for_monitor() { 477 | local health_endpoint="$monitor_url/health" 478 | 479 | # Wait for monitor service 480 | log "🕐 Waiting for monitor service to be up..." 481 | until curl -s "$health_endpoint" > /dev/null; do 482 | log "🕐 Waiting for monitor service to be up..." 483 | sleep 2 484 | done 485 | 486 | log "✅ Monitor service is up!" 487 | } 488 | 489 | create_avail_config() { 490 | local config_dir="$HOME/.avail/$network/config" 491 | local config_file="$config_dir/config.yml" 492 | 493 | if [ -e "$config_file" ] && [ "$overwrite_config" != "true" ]; then 494 | echo "$config_file" 495 | else 496 | # Create config directory if it doesn't exist 497 | mkdir -p "$config_dir" 498 | 499 | # Download config file 500 | curl -s -H "Cache-Control: no-cache" "$CONFIG_URL" -o "$config_file" 501 | 502 | # if PORT is set, update the port in the config file 503 | if [ -n "${PORT:-}" ] && [ "$PORT" != "7007" ]; then 504 | temp_file=$(mktemp) 505 | sed "s/http_server_port = .*/http_server_port = $PORT/" "$config_file" > "$temp_file" 506 | mv "$temp_file" "$config_file" 507 | fi 508 | echo "$config_file" 509 | fi 510 | } 511 | 512 | cleanup() { 513 | log "🧹 Cleaning up..." 514 | 515 | if [ -n "${availup_pid:-}" ]; then 516 | kill "$availup_pid" 2>/dev/null || true 517 | fi 518 | 519 | if [ -n "${avail_light_pid:-}" ]; then 520 | kill "$avail_light_pid" 2>/dev/null || true 521 | fi 522 | 523 | # clean temporary files 524 | rm -f /tmp/health_response 525 | find /tmp -name "sophon-*" -mtime +1 -delete 2>/dev/null || true 526 | } 527 | 528 | check_memory_details() { 529 | if [ -z "$avail_light_pid" ]; then 530 | return 1 531 | fi 532 | 533 | local stats=$(ps -p "$avail_light_pid" -o rss=,vsz=,%mem=) 534 | read rss vsz mem <<< "$stats" 535 | 536 | echo "$(date '+%Y-%m-%d %H:%M:%S') RSS:${rss}KB VSZ:${vsz}KB MEM:${mem}%" >> memory_trends.log 537 | 538 | if [ "$previous_rss" -gt 0 ] && [ "$rss" -gt "$previous_rss" ]; then 539 | log "🔔 Memory increased: ${previous_rss}KB -> ${rss}KB (Δ=$((rss - previous_rss))KB)" 540 | fi 541 | previous_rss=$rss 542 | } 543 | 544 | check_process_memory() { 545 | pgrep -f "avail-light" | xargs ps -o rss= -p | awk '{printf "%.2f", $1/1024}' 546 | } 547 | 548 | declare -i previous_rss=0 549 | 550 | main() { 551 | 552 | log "$(box "🚀 Starting Sophon Light Node")" 553 | 554 | trap cleanup EXIT 555 | 556 | parse_args "$@" 557 | validate_requirements 558 | 559 | wait_for_monitor 560 | check_version "$env" "$auto_upgrade" || true 561 | run_node 562 | 563 | # Version checking 564 | previous=0 565 | while true; do 566 | log "💤 Next version check in $VERSION_CHECKER_INTERVAL seconds..." 567 | 568 | sleep "$VERSION_CHECKER_INTERVAL" 569 | 570 | # check memory usage 571 | current=$(check_process_memory) 572 | if [ -n "$current" ] && [ $(echo "$previous > 0" | bc) -eq 1 ]; then 573 | diff=$(echo "$current - $previous" | bc) 574 | log "📊 RSS: ${current}MB (Δ${diff}MB)" 575 | fi 576 | previous=$current 577 | 578 | if check_version "$env" "$auto_upgrade" && [ "$?" -eq 0 ]; then 579 | log "🔄 Version update required, restarting node..." 580 | cleanup 581 | exec "$0" "$@" 582 | fi 583 | done 584 | } 585 | 586 | main "$@" --------------------------------------------------------------------------------