diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e0aaae1a..1c11bf4e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ New to RustChain? Get 10 RTC for your **first merged PR** — even for small imp ## What Gets Merged -- Code that works against the live node (`https://rustchain.org`) +- Code that works against the live node (`https://50.28.86.131`) - Tests that actually test something meaningful - Documentation that a human can follow end-to-end - Security fixes with proof of concept @@ -80,82 +80,13 @@ python3 -m venv venv && source venv/bin/activate pip install -r requirements.txt # Test against live node -curl -sk https://rustchain.org/health -curl -sk https://rustchain.org/api/miners -curl -sk https://rustchain.org/epoch +curl -sk https://50.28.86.131/health +curl -sk https://50.28.86.131/api/miners +curl -sk https://50.28.86.131/epoch ``` ## Live Infrastructure | Endpoint | URL | |----------|-----| -| Node Health | `https://rustchain.org/health` | -| Active Miners | `https://rustchain.org/api/miners` | -| Current Epoch | `https://rustchain.org/epoch` | -| Block Explorer | `https://rustchain.org/explorer` | -| wRTC Bridge | `https://bottube.ai/bridge` | - -## RTC Payout Process - -1. PR gets reviewed and merged -2. We comment asking for your wallet address -3. RTC is transferred from the community fund -4. Bridge RTC to wRTC (Solana) via [bottube.ai/bridge](https://bottube.ai/bridge) -5. Trade on [Raydium](https://raydium.io/swap/?inputMint=sol&outputMint=12TAdKXxcGf6oCv4rqDz2NkgxjyHq6HQKoxKZYGf5i4X) - - -## Documentation Quality Checklist - -Before opening a docs PR, please verify: - -- [ ] Instructions work exactly as written (commands are copy-pastable). -- [ ] OS/architecture assumptions are explicit (Linux/macOS/Windows). -- [ ] New terms are defined at first use. -- [ ] Broken links are removed or corrected. -- [ ] At least one `example` command/output is updated if behavior changed. -- [ ] File and section names follow existing naming conventions. - -## Common Troubleshooting Entries - -If you changed setup or CLI docs, add at least one section covering common failures, for example: - -- `Command not found`: verify PATH and virtualenv activation. -- `Permission denied` on scripts: ensure execute bit and shell compatibility. -- `Connection error to live node`: include curl timeout/retry guidance and fallback endpoint checks. - -This keeps bounty-quality docs usable by new contributors and operators. - -## Code Style - -- Python 3.8+ compatible -- Type hints appreciated but not yet enforced -- Keep PRs focused — one issue per PR -- Test against the live node, not just local mocks - -## BCOS (Beacon Certified Open Source) - -RustChain uses BCOS checks to keep contributions auditable and license-clean without forcing rewrites of legacy code. - -- **Tier label required (non-doc PRs)**: Add `BCOS-L1` or `BCOS-L2` (also accepted: `bcos:l1`, `bcos:l2`). -- **Doc-only exception**: PRs that only touch `docs/**`, `*.md`, or common image/PDF files do not require a tier label. -- **SPDX required (new code files only)**: Newly added code files must include an SPDX header near the top, e.g. `# SPDX-License-Identifier: MIT`. -- **Evidence artifacts**: CI uploads `bcos-artifacts` (SBOM, license report, hashes, and a machine-readable attestation JSON). - -When to pick a tier: -- `BCOS-L1`: normal features, refactors, non-sensitive changes. -- `BCOS-L2`: security-sensitive changes, transfer/wallet logic, consensus/rewards, auth/crypto, supply-chain touching changes. - -## Start Mining - -Don't just code — mine! Install the miner and earn RTC while you contribute: - -```bash -pip install clawrtc -clawrtc --wallet YOUR_NAME -``` - -Vintage hardware (PowerPC G4/G5, POWER8) earns **2-2.5x** more than modern PCs. - -## Questions? - -Open an issue or join the community. We're friendly. +| Node Health | `https://50.28.86.131/health` | diff --git a/README.md b/README.md index 9eb874265..9ec90c376 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![CI](https://github.com/Scottcjn/Rustchain/actions/workflows/ci.yml/badge.svg)](https://github.com/Scottcjn/Rustchain/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Stars](https://img.shields.io/github/stars/Scottcjn/Rustchain?style=flat&color=gold)](https://github.com/Scottcjn/Rustchain/stargazers) -[![Nodes](https://img.shields.io/badge/Nodes-5%20Active-brightgreen)](https://rustchain.org/explorer/) +[![Nodes](https://img.shields.io/badge/Nodes-5%20Active-brightgreen)](https://rustchain.org/explorer) [![DePIN](https://img.shields.io/badge/DePIN-Vintage%20Hardware-8B4513)](https://rustchain.org) [![Proof of Antiquity](https://img.shields.io/badge/Consensus-Proof%20of%20Antiquity-DAA520)](docs/RustChain_Whitepaper_Flameholder_v0.97-1.pdf) [![DOI](https://zenodo.org/badge/doi/10.5281/zenodo.19442753.svg)](https://doi.org/10.5281/zenodo.19442753) @@ -18,7 +18,7 @@ A PowerBook G4 from 2003 earns **2.5x** more than a modern Threadripper. A Power Mac G5 earns **2.0x**. A 486 with rusty serial ports earns the most respect of all. -[Explorer](https://rustchain.org/explorer/) · [Machines Preserved](https://rustchain.org/preserved.html) · [Install Miner](#quickstart) · [Beginner Guide](docs/QUICKSTART.md) · [Manifesto](https://rustchain.org/manifesto.html) · [Whitepaper](docs/RustChain_Whitepaper_Flameholder_v0.97-1.pdf) +[Explorer](https://rustchain.org/explorer) · [Machines Preserved](https://rustchain.org/preserved.html) · [Install Miner](#quickstart) · [Beginner Guide](docs/QUICKSTART.md) · [Manifesto](https://rustchain.org/manifesto.html) · [Whitepaper](docs/RustChain_Whitepaper_Flameholder_v0.97-1.pdf) @@ -244,7 +244,7 @@ curl -sk https://rustchain.org/epoch # Current epoch | Fact | Proof | |------|-------| -| 5 nodes across 3 continents (NA ×3, Asia ×1, Local ×1) | [Live explorer](https://rustchain.org/explorer/) | +| 5 nodes across 3 continents (NA ×3, Asia ×1, Local ×1) | [Live explorer](https://rustchain.org/explorer) | | 26+ miners attesting | `curl -sk https://rustchain.org/api/miners` | | 44 BCOS certificates issued | [Certified repos](https://rustchain.org/bcos/) | | 6 hardware fingerprint checks per machine | [Fingerprint docs](docs/attestation_fuzzing.md) | @@ -303,7 +303,7 @@ Epoch: 10 minutes | Pool: 1.5 RTC/epoch | Split by antiquity weight G4 Mac (2.5x): 0.30 RTC ████████████████████ G5 Mac (2.0x): 0.24 RTC ████████████████ -Modern PC (1.0x): 0.12 RTC ████████ +Modern PC (0.8x): 0.12 RTC ████████ ``` ### Anti-VM Enforcement diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 205498f59..334751630 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -223,7 +223,7 @@ hardware's **antiquity multiplier** -- older hardware gets a bigger slice. | RISC-V | 1.4x | Open hardware, the future | | Apple Silicon (M1-M4) | 1.2x | Modern but welcome | | Modern x86 (AMD/Intel) | 0.8x | Baseline | -| ARM NAS/SBC | 0.0005x | Too cheap, too farmable | +| ARM NAS/SBC | 0.0005x | Low attestation weight, easily replicated | **Got a PowerBook G4 gathering dust in a closet?** Plug it in. It earns 2.5x what your gaming PC does. diff --git a/docs/README.md b/docs/README.md index 73ce644e4..5079fe62a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,7 +45,7 @@ curl -sk https://rustchain.org/epoch | jq . ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Vintage Miner │────▶│ Attestation Node │────▶│ Ergo Anchor │ -│ (G4/G5/SPARC) │ │ (50.28.86.131) │ │ (Immutability) │ +│ (G4/G5/SPARC) │ │ (https://rustchain.org) │ │ (Immutability) │ └─────────────────┘ └──────────────────┘ └─────────────────┘ │ │ │ Hardware Fingerprint │ Epoch Settlement diff --git a/docs/RustChain_Whitepaper_Flameholder_v0.97.pdf b/docs/RustChain_Whitepaper_Flameholder_v0.97.pdf deleted file mode 100644 index 523425f44..000000000 Binary files a/docs/RustChain_Whitepaper_Flameholder_v0.97.pdf and /dev/null differ diff --git a/install.sh b/install.sh index b2b0054e7..bb8100c3f 100755 --- a/install.sh +++ b/install.sh @@ -23,8 +23,8 @@ CYAN='\033[0;36m' NC='\033[0m' # No Color INSTALL_DIR="$HOME/.rustchain" -MINER_URL="https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/rustchain_universal_miner.py" -FINGERPRINT_URL="https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/fingerprint_checks.py" +MINER_URL="https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/linux/rustchain_linux_miner.py" +FINGERPRINT_URL="https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/linux/fingerprint_checks.py" NODE_URL="https://50.28.86.131" VERSION="1.0.0" diff --git a/node/rustchain_p2p_gossip.py b/node/rustchain_p2p_gossip.py index 268526f50..5e573d775 100644 --- a/node/rustchain_p2p_gossip.py +++ b/node/rustchain_p2p_gossip.py @@ -883,14 +883,20 @@ def _handle_get_state(self, msg: GossipMessage) -> Dict: # Uses the Phase A signed-content shape (msg_type:sender_id:payload) # so verify_message() on the requester side accepts it. payload = {"state": state_data} - content = self._signed_content(MessageType.STATE.value, self.node_id, payload) + # FIX #2288: _signed_content requires 5 args (msg_type, sender_id, msg_id, ttl, payload) + state_msg_id = hashlib.sha256( + f"STATE:{self.node_id}:{json.dumps(payload, sort_keys=True)}:{time.time()}".encode() + ).hexdigest()[:24] + content = self._signed_content(MessageType.STATE.value, self.node_id, state_msg_id, 0, payload) signature, timestamp = self._sign_message(content) return { "status": "ok", "state": state_data, "signature": signature, "timestamp": timestamp, - "sender_id": self.node_id + "sender_id": self.node_id, + "msg_id": state_msg_id, + "ttl": 0 } def _handle_state(self, msg: GossipMessage) -> Dict: diff --git a/rustchain_telegram_bot/README.md b/rustchain_telegram_bot/README.md new file mode 100644 index 000000000..cf94f21ea --- /dev/null +++ b/rustchain_telegram_bot/README.md @@ -0,0 +1,148 @@ +# RustChain Telegram Bot + +A Telegram bot for checking RustChain wallet balances and miner status. + +## Features + +- `/balance ` - Query wallet balance +- `/miners` - List active miners +- `/epoch` - Show current epoch +- `/price` - Show RTC/USD price ($0.10) +- `/help` - List available commands +- Rate limiting: 1 request per 5 seconds per user +- Error handling for offline nodes + +## Configuration + +Edit `bot.py` and set your values: + +```python +BOT_TOKEN = "YOUR_BOT_TOKEN_HERE" # Telegram bot token from @BotFather +NODE_BASE_URL = "http://localhost:8080" # RustChain node URL +RTC_USD_PRICE = 0.10 # RTC price in USD +RATE_LIMIT_SECONDS = 5 # Rate limit per user +``` + +## Local Development + +```bash +# Install dependencies +pip install -r requirements.txt + +# Set environment variables or edit bot.py +export BOT_TOKEN="your_bot_token_here" + +# Run the bot +python bot.py +``` + +## Deployment + +### Railway + +1. Create a new Railway project +2. Connect your GitHub repository +3. Add environment variables: + - `BOT_TOKEN` = your Telegram bot token + - `NODE_BASE_URL` = your RustChain node URL +4. Railway auto-detects Python and installs from `requirements.txt` +5. Set the start command: `python bot.py` + +### Fly.io + +1. Install flyctl: `curl -L https://fly.io/install.sh | sh` +2. Login: `fly auth login` +3. Create `fly.toml`: + +```toml +app = "rustchain-telegram-bot" +primary_region = "iad" + +[build] + builder = "python" + +[deploy] + release_command = "pip install -r requirements.txt" + +[processes] + app = "python bot.py" + +[env] + PORT = "8080" + +[http_service] + internal_port = 8080 + force_https = true +``` + +4. Deploy: `fly launch && fly deploy` + +### systemd Service + +1. Create the service file: + +```ini +[Unit] +Description=RustChain Telegram Bot +After=network.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/opt/rustchain-telegram-bot +Environment=BOT_TOKEN=your_bot_token_here +Environment=NODE_BASE_URL=http://localhost:8080 +ExecStart=/usr/bin/python3 bot.py +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +``` + +2. Install: + +```bash +sudo cp rustchain-telegram-bot.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable rustchain-telegram-bot +sudo systemctl start rustchain-telegram-bot +sudo journalctl -u rustchain-telegram-bot -f +``` + +## Docker + +```dockerfile +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY bot.py . +CMD ["python", "bot.py"] +``` + +```bash +docker build -t rustchain-bot . +docker run -d \ + --name rustchain-bot \ + -e BOT_TOKEN=your_token \ + -e NODE_BASE_URL=http://node:8080 \ + rustchain-bot +``` + +## API Endpoints + +The bot expects these RustChain node endpoints: + +| Endpoint | Description | +|----------|-------------| +| `GET /health` | Node health check | +| `GET /wallet/balance?miner_id=` | Wallet balance | +| `GET /api/miners` | List of active miners | +| `GET /epoch` | Current epoch info | + +## License + +MIT diff --git a/rustchain_telegram_bot/bot.py b/rustchain_telegram_bot/bot.py new file mode 100644 index 000000000..09ad71009 --- /dev/null +++ b/rustchain_telegram_bot/bot.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +""" +RustChain Telegram Bot +Directly queries the RustChain node at https://50.28.86.131 + +Commands: + /start - Welcome message + /balance - Check wallet balance + /stats - Network statistics + /epoch - Current epoch info + /price - Show RTC reference rate + /help - List all commands + +Rate limit: 1 request per 5 seconds per user +""" + +import asyncio +import logging +import os +import time +import urllib.error +import urllib.request +import json +from typing import Optional + +from telegram import Update +from telegram.ext import ( + Application, + CommandHandler, + MessageHandler, + CallbackContext, + filters, +) +from telegram.constants import ParseMode + +# Configuration via environment variables +BOT_TOKEN = os.environ.get("BOT_TOKEN", "YOUR_BOT_TOKEN_HERE") +NODE_BASE_URL = os.environ.get("NODE_BASE_URL", "https://50.28.86.131") +RTC_USD_PRICE = 0.10 +RATE_LIMIT_SECONDS = 5 + +# Logging +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + level=logging.INFO, +) +logger = logging.getLogger(__name__) + + +# --- Rate Limiting --- +user_last_request: dict[int, float] = {} +lock = asyncio.Lock() + + +async def is_rate_limited(user_id: int) -> bool: + """Check if user is rate limited. Returns True if limited.""" + async with lock: + now = time.time() + if user_id in user_last_request: + elapsed = now - user_last_request[user_id] + if elapsed < RATE_LIMIT_SECONDS: + return True + user_last_request[user_id] = now + return False + + +# --- API Helpers --- + + +async def fetch_json(endpoint: str, timeout: int = 10) -> Optional[dict]: + """Fetch JSON from RustChain node API with error handling.""" + url = f"{NODE_BASE_URL}{endpoint}" + try: + req = urllib.request.Request(url) + with await asyncio.to_thread( + urllib.request.urlopen, req, timeout=timeout + ) as response: + body = response.read().decode() + return json.loads(body) + except urllib.error.HTTPError as e: + logger.error(f"HTTP error {e.code} for {url}: {e.reason}") + return None + except urllib.error.URLError as e: + logger.error(f"Node unreachable {url}: {e.reason}") + return None + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON from {url}: {e}") + return None + + +async def check_node_health() -> bool: + """Check if RustChain node is online.""" + data = await fetch_json("/health") + return data is not None + + +# --- Command Handlers --- + + +async def cmd_start(update: Update, context: CallbackContext) -> None: + """Handle /start command.""" + await update.message.reply_text( + "Welcome to RustChain Bot!\n" + "Your vintage hardware earns more as it ages.\n\n" + "Use /help to see all commands." + ) + + +async def cmd_help(update: Update, context: CallbackContext) -> None: + """Handle /help command.""" + help_text = ( + "RustChain Bot Commands\n\n" + "/balance <wallet_id> - Check wallet balance\n" + "/stats - Network statistics (miners, supply)\n" + "/epoch - Current epoch and slot info\n" + "/price - Show RTC/USD reference rate\n" + "/help - Show this help message\n\n" + "Rate limit: 1 request per 5 seconds" + ) + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + + +async def cmd_balance(update: Update, context: CallbackContext) -> None: + """Handle /balance command.""" + user_id = update.effective_user.id + + if await is_rate_limited(user_id): + await update.message.reply_text( + "⏳ Rate limited. Please wait 5 seconds between requests." + ) + return + + if not context.args: + await update.message.reply_text( + "Usage: /balance \n" + "Example: /balance Ivan-houzhiwen" + ) + return + + wallet_id = context.args[0] + + if not await check_node_health(): + await update.message.reply_text( + "❌ Node is offline. Please try again later." + ) + return + + data = await fetch_json(f"/wallet/balance?miner_id={wallet_id}") + if data is None: + await update.message.reply_text( + f"❌ Could not fetch balance for: {wallet_id}\n" + "Check the wallet ID and try again.", + parse_mode=ParseMode.HTML, + ) + return + + balance = data.get("amount_rtc", 0) + amount_i64 = data.get("amount_i64", 0) + miner_id = data.get("miner_id", wallet_id) + + await update.message.reply_text( + f"💰 Wallet: {miner_id}\n" + f"💵 Balance: {balance} RTC\n" + f"🔢 Raw units: {amount_i64}", + parse_mode=ParseMode.HTML, + ) + + +async def cmd_stats(update: Update, context: CallbackContext) -> None: + """Handle /stats command - network statistics.""" + user_id = update.effective_user.id + + if await is_rate_limited(user_id): + await update.message.reply_text( + "⏳ Rate limited. Please wait 5 seconds between requests." + ) + return + + if not await check_node_health(): + await update.message.reply_text( + "❌ Node is offline. Please try again later." + ) + return + + epoch_data = await fetch_json("/epoch") + if epoch_data is None: + await update.message.reply_text( + "❌ Could not fetch network statistics." + ) + return + + enrolled = epoch_data.get("enrolled_miners", "N/A") + total_supply = epoch_data.get("total_supply_rtc", 0) + blocks_per_epoch = epoch_data.get("blocks_per_epoch", "N/A") + epoch_pot = epoch_data.get("epoch_pot", "N/A") + + health_data = await fetch_json("/health") + version = "N/A" + uptime_h = "N/A" + if health_data: + version = health_data.get("version", "N/A") + uptime_s = health_data.get("uptime_s", 0) + uptime_h = round(uptime_s / 3600, 1) + + text = ( + f"📊 RustChain Network Stats\n\n" + f"⛏️ Active Miners: {enrolled}\n" + f"📦 Blocks/Epoch: {blocks_per_epoch}\n" + f"💰 Total Supply: {total_supply} RTC\n" + f"🎯 Epoch Pot: {epoch_pot} RTC\n" + f"⏱️ Node Uptime: {uptime_h} hours\n" + f"🔧 Node Version: {version}" + ) + await update.message.reply_text(text, parse_mode=ParseMode.HTML) + + +async def cmd_epoch(update: Update, context: CallbackContext) -> None: + """Handle /epoch command.""" + user_id = update.effective_user.id + + if await is_rate_limited(user_id): + await update.message.reply_text( + "⏳ Rate limited. Please wait 5 seconds between requests." + ) + return + + if not await check_node_health(): + await update.message.reply_text( + "❌ Node is offline. Please try again later." + ) + return + + data = await fetch_json("/epoch") + if data is None: + await update.message.reply_text( + "❌ Could not fetch epoch info." + ) + return + + epoch = data.get("epoch", "N/A") + slot = data.get("slot", "N/A") + height = data.get("height", "N/A") + blocks_per_epoch = data.get("blocks_per_epoch", "N/A") + + await update.message.reply_text( + f"📈 Epoch Info\n\n" + f"🔢 Epoch: {epoch}\n" + f"🔪 Slot: {slot}\n" + f"📦 Block Height: {height}\n" + f"📊 Blocks/Epoch: {blocks_per_epoch}", + parse_mode=ParseMode.HTML, + ) + + +async def cmd_price(update: Update, context: CallbackContext) -> None: + """Handle /price command.""" + user_id = update.effective_user.id + + if await is_rate_limited(user_id): + await update.message.reply_text( + "⏳ Rate limited. Please wait 5 seconds between requests." + ) + return + + await update.message.reply_text( + f"💲 RTC Reference Rate\n\n" + f"${RTC_USD_PRICE:.2f} per RTC\n\n" + f"RTC is earned through mining on the RustChain network.\n" + f"The best hardware to mine? Old hardware.\n" + f"A PowerBook G4 from 2003 earns 2.5x more than a Threadripper.", + parse_mode=ParseMode.HTML, + ) + + +async def error_handler( + update: object, context: CallbackContext +) -> None: + """Handle unexpected errors.""" + logger.error(f"Exception: {context.error}") + if isinstance(update, Update) and update.message: + await update.message.reply_text( + "❌ An unexpected error occurred. Please try again." + ) + + +def main() -> None: + """Start the bot.""" + if BOT_TOKEN == "YOUR_BOT_TOKEN_HERE": + logger.error("BOT_TOKEN not set! Set the BOT_TOKEN environment variable.") + return + + application = Application.builder().token(BOT_TOKEN).build() + + # Register handlers + application.add_handler(CommandHandler("start", cmd_start)) + application.add_handler(CommandHandler("balance", cmd_balance)) + application.add_handler(CommandHandler("stats", cmd_stats)) + application.add_handler(CommandHandler("epoch", cmd_epoch)) + application.add_handler(CommandHandler("price", cmd_price)) + application.add_handler(CommandHandler("help", cmd_help)) + application.add_error_handler(error_handler) + + # Start polling + logger.info("RustChain Telegram Bot starting...") + application.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main() diff --git a/rustchain_telegram_bot/requirements.txt b/rustchain_telegram_bot/requirements.txt new file mode 100644 index 000000000..7d5efd515 --- /dev/null +++ b/rustchain_telegram_bot/requirements.txt @@ -0,0 +1 @@ +python-telegram-bot==20.7