diff --git a/docs/MINE_YOUR_GRANDMAS_COMPUTER.md b/docs/MINE_YOUR_GRANDMAS_COMPUTER.md new file mode 100644 index 000000000..0f89413ba --- /dev/null +++ b/docs/MINE_YOUR_GRANDMAS_COMPUTER.md @@ -0,0 +1,103 @@ +# Mine Your Grandma's Computer: The 15-Minute RustChain Guide + +Got an old laptop collecting dust in the closet? Don't throw it away! That vintage hardware is exactly what the **RustChain** network values most. In under 15 minutes, you can turn e-waste into an RTC-earning node. + +This guide will take you from "I found an old computer" to "It's earning RTC" using real examples of a **Core 2 Duo Windows Laptop** and a **PowerPC G4 Mac**. + +--- + +## 🛑 Does my computer qualify? (The Quick Check) + +RustChain rewards **real silicon**, not raw power. +If your computer meets these two simple criteria, it qualifies: +1. **It boots an operating system** (Windows XP/7/10, Mac OS X, or Linux). +2. **It can connect to the internet** (Wi-Fi or Ethernet). + +**What DOESN'T qualify?** +- Virtual Machines (VMware, VirtualBox, Proxmox). *They earn 0.000000001x rewards.* +- Modern Cloud VPS (AWS, DigitalOcean). *They earn standard base rewards, but cost more than they earn.* + +--- + +## 📈 The Antiquity Multiplier (Explained in Plain English) + +Why use an old computer instead of a brand new gaming PC? +**The Antiquity Multiplier.** + +RustChain is designed to preserve computing history. The older and weirder your computer's processor is, the more RTC you earn per epoch: +- **Modern PC (Core i9):** 0.8x rewards +- **Old Core 2 Duo Laptop (2006):** 1.3x rewards +- **Power Mac G4 (2003):** 2.5x rewards + +A 20-year-old PowerBook G4 will earn **more than three times** the RTC of a brand new $3,000 gaming desktop! + +--- + +## 🛠️ Walkthrough 1: The Core 2 Duo Windows Laptop (2006-2009 Era) + +*Example Hardware: Dell Inspiron 1520 or ThinkPad T61* + +### Step 1: Download the Miner +1. Turn on the laptop and connect to Wi-Fi. +2. Open a web browser and download the latest `win-miner` bundle from the [RustChain Releases page](https://github.com/Scottcjn/Rustchain/releases). +3. Extract the ZIP file to your Desktop. + +### Step 2: Create a Wallet (Optional, if you don't have one) +Double-click `rustchain-wallet.exe` and follow the prompts to generate a new wallet address. Save your 12-word recovery phrase safely! + +### Step 3: Run the Fingerprint Check +RustChain needs to verify your hardware is real. +1. Open the extracted folder. +2. Hold `Shift` and right-click in the folder background, then select **"Open command window here"** (or PowerShell). +3. Type: `miner.exe --dry-run` and press Enter. + +*(Screenshot: A Windows command prompt showing a successful fingerprint check, with CPU identified as Core 2 Duo and "Hardware Check: PASSED")* +> **Look for this line:** `Fingerprint verification: SUCCESS. Architecture: x86_64 (Core 2 Duo)` + +### Step 4: Start Mining! +Double click the `start_mining.bat` file. +- It will ask for your Wallet Address. Paste it in. +- It will ask for a miner name (e.g., `grandmas-thinkpad`). +- Type `YES` to agree to the consent screen. + +*(Screenshot: The miner showing "Attestation submitted successfully" and waiting for the next epoch)* +**Boom. You're earning RTC.** + +--- + +## 🍎 Walkthrough 2: The PowerPC Mac G3/G4/G5 (1997-2005 Era) + +*Example Hardware: PowerBook G4 or iMac G3 (Running Mac OS X Leopard or Tiger)* + +### Step 1: Get the Python Miner +PowerPC Macs are legendary on RustChain (earning up to 2.5x rewards!). Because they are so old, we use the lightweight Python miner. +1. Open the Terminal application (in `Applications > Utilities`). +2. Clone the repository (or download the ZIP if git isn't installed): + ```bash + curl -LO https://github.com/Scottcjn/Rustchain/archive/refs/heads/main.zip + unzip main.zip + cd Rustchain-main/miners/linux + ``` + +### Step 2: The Fingerprint Check +Run the dry-run to ensure your PowerPC chip is correctly identified by the network. +```bash +python3 miner_threaded.py --dry-run +``` +*(Screenshot: A Mac Terminal window showing `sys_vendor: Apple Computer, Inc.`, `Architecture: PowerPC G4`, and `Fingerprint: SUCCESS`)* + +### Step 3: Start Attesting +Start the miner in the background! +```bash +python3 miner_threaded.py --wallet YOUR_RTC_WALLET_ADDRESS --name powerbook-g4 +``` +Type `OUI` (or `YES` depending on your locale) to agree to the consent screen. + +*(Screenshot: The terminal displaying "Epoch 1234: Attestation accepted. Multiplier: 2.5x")* + +--- + +## 💡 Pro-Tips for Vintage Mining +- **Keep it cool:** Old laptops get hot. Keep them on a hard surface. +- **Screen timeout:** Set your computer to never go to sleep, but allow the screen to turn off to save power. +- **Check your stats:** Enter your wallet address on the [RustChain Explorer](https://rustchain.org) to watch your vintage hardware rake in the rewards! diff --git a/node/sophia_elya_service.py b/node/sophia_elya_service.py index 2df0b6aad..a04e1f072 100644 --- a/node/sophia_elya_service.py +++ b/node/sophia_elya_service.py @@ -84,6 +84,37 @@ def _ensure_balance_micro_schema(conn): ) conn.execute("DROP TABLE balances_legacy_real") +def _ensure_epoch_state_settlement_schema(conn): + """Keep Sophia's epoch_state compatible with shared settlement guards.""" + conn.execute( + "CREATE TABLE IF NOT EXISTS epoch_state (" + "epoch INTEGER PRIMARY KEY, " + "accepted_blocks INTEGER DEFAULT 0, " + "finalized INTEGER DEFAULT 0, " + "settled INTEGER DEFAULT 0, " + "settled_ts INTEGER)" + ) + columns = {row[1] for row in conn.execute("PRAGMA table_info(epoch_state)").fetchall()} + newly_added_settled = False + if "settled" not in columns: + try: + conn.execute("ALTER TABLE epoch_state ADD COLUMN settled INTEGER DEFAULT 0") + newly_added_settled = True + except sqlite3.OperationalError: + pass # a concurrent migrator won the ADD COLUMN race; column now exists + if "settled_ts" not in columns: + try: + conn.execute("ALTER TABLE epoch_state ADD COLUMN settled_ts INTEGER") + except sqlite3.OperationalError: + pass + conn.execute("UPDATE epoch_state SET settled = 0 WHERE settled IS NULL") + # ONE-TIME backfill, only when we just added the column: rows finalized by the + # pre-settlement code path were already paid, so mark them settled exactly + # once during migration. Never re-run on later startups — that could suppress + # a legitimate finalized-but-not-yet-settled row in a two-phase/shared flow. + if newly_added_settled: + conn.execute("UPDATE epoch_state SET settled = 1 WHERE finalized = 1 AND COALESCE(settled, 0) = 0") + def init_db(): """Initialize database with epoch tables""" with sqlite3.connect(DB_PATH) as c: @@ -92,7 +123,7 @@ def init_db(): c.execute("CREATE TABLE IF NOT EXISTS tickets (ticket_id TEXT PRIMARY KEY, expires_at INTEGER, commitment TEXT)") # New epoch tables - c.execute("CREATE TABLE IF NOT EXISTS epoch_state (epoch INTEGER PRIMARY KEY, accepted_blocks INTEGER DEFAULT 0, finalized INTEGER DEFAULT 0)") + _ensure_epoch_state_settlement_schema(c) # `weight` is a non-financial pro-rata multiplier; balances are financial # and stay in integer micro-RTC units. c.execute("CREATE TABLE IF NOT EXISTS epoch_enroll (epoch INTEGER, miner_pk TEXT, weight REAL, PRIMARY KEY (epoch, miner_pk))") @@ -146,8 +177,11 @@ def _finite_float(value, default=1.0): def inc_epoch_block(epoch): """Increment accepted blocks for epoch""" with sqlite3.connect(DB_PATH) as c: - c.execute("INSERT OR IGNORE INTO epoch_state(epoch, accepted_blocks, finalized) VALUES (?,0,0)", (epoch,)) - c.execute("UPDATE epoch_state SET accepted_blocks = accepted_blocks + 1 WHERE epoch=?", (epoch,)) + c.execute("PRAGMA busy_timeout=5000") + c.execute("INSERT OR IGNORE INTO epoch_state(epoch, accepted_blocks, finalized, settled) VALUES (?,0,0,0)", (epoch,)) + # Do not inflate the block count once the epoch is finalized/settled — + # a late block must not change the count the reward was computed against. + c.execute("UPDATE epoch_state SET accepted_blocks = accepted_blocks + 1 WHERE epoch=? AND COALESCE(finalized,0)=0 AND COALESCE(settled,0)=0", (epoch,)) def enroll_epoch(epoch, miner_pk, weight): """Enroll miner in epoch with weight. @@ -164,28 +198,58 @@ def enroll_epoch(epoch, miner_pk, weight): def finalize_epoch(epoch, per_block_rtc): """Finalize epoch and distribute rewards""" with sqlite3.connect(DB_PATH) as c: - row = c.execute("SELECT finalized, accepted_blocks FROM epoch_state WHERE epoch=?", (epoch,)).fetchone() + c.execute("PRAGMA busy_timeout=5000") + c.execute("BEGIN IMMEDIATE") + # COALESCE settled so a legacy/shared row whose column was added without + # a value cannot crash int() here. + row = c.execute( + "SELECT COALESCE(finalized, 0), COALESCE(accepted_blocks, 0), COALESCE(settled, 0) " + "FROM epoch_state WHERE epoch=?", + (epoch,), + ).fetchone() if not row: + c.rollback() return {"ok": False, "reason": "no_state"} - finalized, blocks = int(row[0]), int(row[1]) + finalized, blocks, settled = int(row[0]), int(row[1]), int(row[2]) + if settled: + c.rollback() + return {"ok": False, "reason": "already_settled"} if finalized: + # Status probe only — do NOT mutate on this read path. Legacy + # finalized-but-unsettled rows are reconciled by the init-time + # backfill in _ensure_epoch_state_settlement_schema(). + c.rollback() return {"ok": False, "reason": "already_finalized"} - total_reward = per_block_rtc * blocks - miners = list(c.execute("SELECT miner_pk, weight FROM epoch_enroll WHERE epoch=?", (epoch,))) - sum_w = sum(w for _, w in miners) or 0.0 - payouts = [] - - if sum_w > 0 and total_reward > 0: - for pk, w in miners: - amt = total_reward * (w / sum_w) - c.execute("INSERT OR IGNORE INTO balances(miner_pk, balance_rtc) VALUES (?,0)", (pk,)) - amount_micro = _rtc_to_micro(amt) - c.execute("UPDATE balances SET balance_rtc = balance_rtc + ? WHERE miner_pk=?", (amount_micro, pk)) - payouts.append((pk, _micro_to_rtc(amount_micro))) + claim = c.execute( + "UPDATE epoch_state SET settled=1, settled_ts=?, finalized=1 WHERE epoch=? AND COALESCE(settled,0)=0", + (int(time.time()), epoch), + ) + if claim.rowcount != 1: + c.rollback() + return {"ok": False, "reason": "already_settled"} - c.execute("UPDATE epoch_state SET finalized=1 WHERE epoch=?", (epoch,)) + try: + total_reward = per_block_rtc * blocks + miners = list(c.execute("SELECT miner_pk, weight FROM epoch_enroll WHERE epoch=?", (epoch,))) + sum_w = sum(w for _, w in miners) or 0.0 + payouts = [] + + if sum_w > 0 and total_reward > 0: + for pk, w in miners: + amt = total_reward * (w / sum_w) + c.execute("INSERT OR IGNORE INTO balances(miner_pk, balance_rtc) VALUES (?,0)", (pk,)) + amount_micro = _rtc_to_micro(amt) + c.execute("UPDATE balances SET balance_rtc = balance_rtc + ? WHERE miner_pk=?", (amount_micro, pk)) + payouts.append((pk, _micro_to_rtc(amount_micro))) + + c.commit() + except Exception: + # Roll back the settlement claim + any partial credits together so the + # epoch stays unsettled and can be retried (no half-paid epoch). + c.rollback() + raise return {"ok": True, "blocks": blocks, "total_reward": total_reward, "sum_w": sum_w, "payouts": payouts} def get_balance(miner_pk): @@ -244,9 +308,11 @@ def get_epoch(): # Get epoch state with sqlite3.connect(DB_PATH) as c: - row = c.execute("SELECT accepted_blocks, finalized FROM epoch_state WHERE epoch=?", (epoch,)).fetchone() + row = c.execute("SELECT accepted_blocks, finalized, COALESCE(settled,0), settled_ts FROM epoch_state WHERE epoch=?", (epoch,)).fetchone() blocks = int(row[0]) if row else 0 finalized = bool(row[1]) if row else False + settled = bool(row[2]) if row else False + settled_ts = (row[3] if row else None) # Count enrolled miners miners = c.execute("SELECT COUNT(*), SUM(weight) FROM epoch_enroll WHERE epoch=?", (epoch,)).fetchone() @@ -263,6 +329,8 @@ def get_epoch(): "enrolled_miners": miner_count, "total_weight": total_weight, "finalized": finalized, + "settled": settled, + "settled_ts": settled_ts, "epoch_pot": PER_BLOCK_RTC * blocks }) diff --git a/node/tests/test_sophia_elya_service_money_units.py b/node/tests/test_sophia_elya_service_money_units.py index 82708a2be..66bb33a31 100644 --- a/node/tests/test_sophia_elya_service_money_units.py +++ b/node/tests/test_sophia_elya_service_money_units.py @@ -60,6 +60,89 @@ def test_finalize_epoch_stores_integer_micro_rtc_and_returns_public_rtc(): assert stored_value == 100_000 +def test_epoch_state_schema_adds_settlement_columns_to_legacy_table(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: + service = load_service(Path(tmp)) + + with sqlite3.connect(service.DB_PATH) as conn: + conn.execute( + "CREATE TABLE epoch_state (" + "epoch INTEGER PRIMARY KEY, " + "accepted_blocks INTEGER DEFAULT 0, " + "finalized INTEGER DEFAULT 0)" + ) + conn.execute( + "INSERT INTO epoch_state(epoch, accepted_blocks, finalized) VALUES (?,?,?)", + (7, 1, 1), + ) + + service.init_db() + + with sqlite3.connect(service.DB_PATH) as conn: + columns = {row[1] for row in conn.execute("PRAGMA table_info(epoch_state)")} + row = conn.execute( + "SELECT finalized, settled FROM epoch_state WHERE epoch=?", + (7,), + ).fetchone() + + assert {"settled", "settled_ts"} <= columns + assert row == (1, 1) + + +def test_finalize_epoch_marks_settled_and_blocks_second_credit(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: + service = load_service(Path(tmp)) + service.init_db() + + with sqlite3.connect(service.DB_PATH) as conn: + conn.execute( + "INSERT INTO epoch_state(epoch, accepted_blocks, finalized, settled) VALUES (?,?,?,?)", + (7, 1, 0, 0), + ) + conn.execute( + "INSERT INTO epoch_enroll(epoch, miner_pk, weight) VALUES (?,?,?)", + (7, "RTC_miner", 1.0), + ) + + first = service.finalize_epoch(7, 0.1) + second = service.finalize_epoch(7, 0.1) + + assert first["ok"] is True + assert second == {"ok": False, "reason": "already_settled"} + assert service.get_balance("RTC_miner") == 0.1 + + with sqlite3.connect(service.DB_PATH) as conn: + row = conn.execute( + "SELECT finalized, settled, settled_ts FROM epoch_state WHERE epoch=?", + (7,), + ).fetchone() + + assert row[0] == 1 + assert row[1] == 1 + assert isinstance(row[2], int) + + +def test_finalize_epoch_respects_existing_settled_marker_without_crediting(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: + service = load_service(Path(tmp)) + service.init_db() + + with sqlite3.connect(service.DB_PATH) as conn: + conn.execute( + "INSERT INTO epoch_state(epoch, accepted_blocks, finalized, settled) VALUES (?,?,?,?)", + (7, 1, 0, 1), + ) + conn.execute( + "INSERT INTO epoch_enroll(epoch, miner_pk, weight) VALUES (?,?,?)", + (7, "RTC_miner", 1.0), + ) + + result = service.finalize_epoch(7, 0.1) + + assert result == {"ok": False, "reason": "already_settled"} + assert service.get_balance("RTC_miner") == 0.0 + + def test_legacy_real_balances_are_migrated_to_micro_rtc(): with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: service = load_service(Path(tmp)) @@ -86,3 +169,50 @@ def test_legacy_real_balances_are_migrated_to_micro_rtc(): assert stored_type == "integer" assert stored_value == 1_234_567 assert service.get_balance("RTC_legacy") == 1.234567 + + +def test_finalize_epoch_idempotent_pays_once(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: + service = load_service(Path(tmp)) + service.init_db() + for _ in range(2): + service.inc_epoch_block(5) + service.enroll_epoch(5, "m1", 1.0) + r1 = service.finalize_epoch(5, 1.5) + bal1 = service.get_balance("m1") + r2 = service.finalize_epoch(5, 1.5) + bal2 = service.get_balance("m1") + assert r1["ok"] is True + assert r2["ok"] is False and r2["reason"] == "already_settled" + assert bal1 == bal2 # double-settlement guard: paid exactly once + + +def test_inc_epoch_block_does_not_inflate_count_after_finalize(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: + service = load_service(Path(tmp)) + service.init_db() + for _ in range(3): + service.inc_epoch_block(7) + service.enroll_epoch(7, "m1", 1.0) + assert service.finalize_epoch(7, 1.5)["blocks"] == 3 + service.inc_epoch_block(7) # late block after settlement + with sqlite3.connect(service.DB_PATH) as conn: + blocks = conn.execute( + "SELECT accepted_blocks FROM epoch_state WHERE epoch=7" + ).fetchone()[0] + assert blocks == 3 # count the reward was computed against is frozen + + +def test_null_settled_row_is_still_payable(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: + service = load_service(Path(tmp)) + service.init_db() + service.enroll_epoch(9, "m1", 1.0) + with sqlite3.connect(service.DB_PATH) as conn: + conn.execute( + "INSERT OR REPLACE INTO epoch_state(epoch, accepted_blocks, finalized, settled) " + "VALUES (9, 2, 0, NULL)" + ) + res = service.finalize_epoch(9, 1.5) + assert res["ok"] is True # NULL settled must not make the epoch unpayable + assert service.get_balance("m1") > 0