From d9729be4e8bfa4b1f18399aeea56c3a1a7cefeaa Mon Sep 17 00:00:00 2001 From: JustinOros Date: Thu, 6 Nov 2025 20:02:50 -0700 Subject: [PATCH] MacMiner - Bitcoin Miner for Apple Silicon --- macminer.py | 547 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 macminer.py diff --git a/macminer.py b/macminer.py new file mode 100644 index 0000000..8204a2c --- /dev/null +++ b/macminer.py @@ -0,0 +1,547 @@ +#!/usr/bin/env python3 +# Description: macminer.py - Bitcoin Miner optimized for Apple Silicon +# Usage: python3 macminer.py +# Author: Justin Oros +# Source: https://github.com/JustinOros + +import json +import hashlib +import struct +import time +import os +import requests +from datetime import datetime +from multiprocessing import Process, Queue, cpu_count +from binascii import unhexlify, hexlify + +CONFIG_FILE = "macminer.json" + +def get_default_config(): + """Generate default configuration with dynamic values""" + return { + "pool_enabled": False, + "pool_url": "stratum+tcp://pool.example.com:3333", + "pool_username": "your_bitcoin_address", + "pool_password": "x", + "solo_rpc_url": "http://127.0.0.1:8332", + "solo_rpc_user": "bitcoinrpc", + "solo_rpc_password": "your_rpc_password", + "coinbase_address": "your_bitcoin_address_for_rewards", + "threads": cpu_count(), + "scan_time": 60, + "update_interval": 5 + } + + +def load_or_create_config(): + """Load config from file or create default config""" + if not os.path.exists(CONFIG_FILE): + print(f"[INFO] Config file not found. Creating {CONFIG_FILE}...") + default_config = get_default_config() + with open(CONFIG_FILE, 'w') as f: + json.dump(default_config, f, indent=4) + print(f"\n{'='*70}") + print(f" āœ… Created {CONFIG_FILE}") + print(f"{'='*70}") + print("\nPlease configure the following settings:") + print("\nšŸ“ FOR SOLO MINING:") + print(" • solo_rpc_user: Your Bitcoin Core RPC username") + print(" • solo_rpc_password: Your Bitcoin Core RPC password") + print(" • coinbase_address: Your Bitcoin address (where rewards go)") + print("\nšŸ“ FOR POOL MINING:") + print(" • Set pool_enabled: true") + print(" • pool_url: Your mining pool's stratum URL") + print(" • pool_username: Usually your Bitcoin address") + print(" • pool_password: Usually 'x' or pool-specific password") + print("\nšŸ“ OPTIONAL:") + print(f" • threads: Number of CPU cores to use (default: {default_config['threads']} detected)") + print(" • update_interval: Seconds between work updates (default: 5)") + print(f"\n{'='*70}") + print("Edit macminer.json and run this script again.") + print(f"{'='*70}\n") + return None + + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + print(f"[INFO] Loaded configuration from {CONFIG_FILE}") + return config + + +def sha256d(data): + """Double SHA-256 hash (Bitcoin's mining algorithm)""" + return hashlib.sha256(hashlib.sha256(data).digest()).digest() + + +def bits_to_target(bits): + """Convert compact bits representation to full target""" + exp = bits >> 24 + mant = bits & 0xffffff + target = mant * (1 << (8 * (exp - 3))) + return target + + +def target_to_bits(target): + """Convert target to compact bits representation""" + # Find the most significant byte + s = hex(target)[2:].rstrip('L') + if len(s) % 2: + s = '0' + s + s = bytes.fromhex(s) + + # Find first non-zero byte + for i, b in enumerate(s): + if b != 0: + break + s = s[i:] + + # Take first 3 bytes + if len(s) > 3: + mantissa = struct.unpack('>I', b'\x00' + s[:3])[0] + exponent = len(s) + else: + mantissa = struct.unpack('>I', b'\x00' * (4 - len(s)) + s)[0] + exponent = len(s) + + return (exponent << 24) | mantissa + + +def get_work_solo(config): + """Get mining work from Bitcoin Core RPC (solo mining)""" + headers = {'content-type': 'application/json'} + + # First, get block template + payload = { + "jsonrpc": "2.0", + "id": "macminer", + "method": "getblocktemplate", + "params": [{"rules": ["segwit"]}] + } + + try: + response = requests.post( + config['solo_rpc_url'], + auth=(config['solo_rpc_user'], config['solo_rpc_password']), + headers=headers, + json=payload, + timeout=10 + ) + + if response.status_code != 200: + print(f"[ERROR] RPC returned status {response.status_code}: {response.text}") + return None + + result = response.json() + + if 'error' in result and result['error']: + print(f"[ERROR] RPC error: {result['error']}") + return None + + if 'result' not in result: + print(f"[ERROR] No result in response: {result}") + return None + + template = result['result'] + + # Build coinbase transaction + coinbase_tx = build_coinbase_transaction(template, config['coinbase_address']) + + # Calculate merkle root with coinbase + transactions = [coinbase_tx] + [unhexlify(tx['data']) for tx in template.get('transactions', [])] + merkle_root = calculate_merkle_root(transactions) + + work = { + 'version': template['version'], + 'previousblockhash': template['previousblockhash'], + 'merkleroot': hexlify(merkle_root).decode(), + 'time': template['curtime'], + 'bits': int(template['bits'], 16), + 'height': template['height'], + 'target': bits_to_target(int(template['bits'], 16)), + 'coinbase_tx': hexlify(coinbase_tx).decode(), + 'transactions': template.get('transactions', []) + } + + return work + + except requests.exceptions.ConnectionError: + print(f"[ERROR] Cannot connect to Bitcoin Core at {config['solo_rpc_url']}") + print("[ERROR] Make sure Bitcoin Core is running with RPC enabled") + return None + except Exception as e: + print(f"[ERROR] Exception getting work: {e}") + import traceback + traceback.print_exc() + return None + + +def build_coinbase_transaction(template, address): + """Build a coinbase transaction""" + # Simplified coinbase - in production, this needs proper scriptSig + coinbase_script = ( + struct.pack(' 1: + if len(hashes) % 2 == 1: + hashes.append(hashes[-1]) + + hashes = [sha256d(hashes[i] + hashes[i+1]) for i in range(0, len(hashes), 2)] + + return hashes[0] + + +def build_block_header(work, nonce, extra_nonce=0): + """Build block header for mining""" + header = b'' + header += struct.pack(' 0 else 0 + stats_queue.put({ + 'worker_id': worker_id, + 'hashes': hashes, + 'hashrate': hashrate + }) + + # Check stop flag + if stop_flag.value: + break + + +def display_stats(stats_queue, num_workers, config, work_info): + """Display mining statistics""" + worker_stats = {i: {'hashes': 0, 'hashrate': 0} for i in range(num_workers)} + last_update = time.time() + + while True: + try: + # Update worker stats + while not stats_queue.empty(): + stat = stats_queue.get_nowait() + worker_id = stat['worker_id'] + worker_stats[worker_id] = { + 'hashes': stat['hashes'], + 'hashrate': stat['hashrate'] + } + + # Update display every second + if time.time() - last_update >= 1.0: + # Calculate total hashrate + total_hashrate = sum(w['hashrate'] for w in worker_stats.values()) + total_hashes = sum(w['hashes'] for w in worker_stats.values()) + + # Clear screen and display stats + os.system('clear' if os.name == 'posix' else 'cls') + print("=" * 70) + print(" MacMiner - Bitcoin Miner for Apple Silicon") + print("=" * 70) + print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"Mode: {'Pool Mining' if config['pool_enabled'] else 'Solo Mining'}") + + if work_info['height']: + print(f"Block Height: {work_info['height']}") + print(f"Difficulty: {work_info['difficulty']:,.2f}") + + print(f"Workers: {num_workers}") + print(f"Total Hashes: {total_hashes:,}") + print(f"Total Hashrate: {total_hashrate/1000:,.2f} KH/s ({total_hashrate:,.0f} H/s)") + print("-" * 70) + + for worker_id in sorted(worker_stats.keys()): + stats = worker_stats[worker_id] + print(f"Worker {worker_id:2d}: {stats['hashrate']/1000:>8.2f} KH/s | {stats['hashes']:>12,} hashes") + + print("=" * 70) + print("Press Ctrl+C to stop mining") + + last_update = time.time() + + time.sleep(0.1) + + except KeyboardInterrupt: + break + except Exception as e: + print(f"[ERROR] Stats display error: {e}") + time.sleep(1) + + +def main(): + """Main mining loop""" + print("=" * 70) + print(" MacMiner - Bitcoin Miner for Apple Silicon") + print("=" * 70) + + # Load configuration + config = load_or_create_config() + + if config is None: + return + + # Display configuration + print(f"\n{'='*70}") + print(f" MACMINER CONFIGURATION") + print(f"{'='*70}") + print(f"Mining Mode: {'🌊 Pool Mining' if config['pool_enabled'] else 'ā›ļø Solo Mining'}") + print(f"Threads: {config['threads']} CPU cores") + + if config['pool_enabled']: + print(f"Pool URL: {config['pool_url']}") + print(f"Pool Username: {config['pool_username']}") + else: + print(f"RPC URL: {config['solo_rpc_url']}") + print(f"Coinbase Address: {config.get('coinbase_address', 'NOT SET')}") + + print(f"{'='*70}\n") + + if config['pool_enabled']: + print(f"\n[ERROR] Pool mining not yet implemented.") + print("Set 'pool_enabled': false in config for solo mining.") + return + + # Test connection to Bitcoin Core + print(f"\n[INFO] Testing connection to Bitcoin Core...") + test_work = get_work_solo(config) + if test_work is None: + print("\n" + "="*70) + print(" āš ļø BITCOIN CORE NOT DETECTED") + print("="*70) + print("\nMacMiner requires Bitcoin Core to be running and synchronized.") + print("\nOption 1 - Install Bitcoin Core locally:") + print(" 1. Download Bitcoin Core from: https://bitcoin.org") + print(" 2. Install and run Bitcoin Core") + print(" 3. Wait for blockchain to synchronize (this may take several days)") + print(" 4. Configure RPC in ~/Library/Application Support/Bitcoin/bitcoin.conf:") + print(" server=1") + print(" rpcuser=yourusername") + print(" rpcpassword=yourpassword") + print(" rpcallowip=127.0.0.1") + print(" 5. Restart Bitcoin Core") + print(" 6. Update macminer.json with your RPC credentials") + print(" 7. Run macminer.py again") + print("\nOption 2 - Connect to remote Bitcoin Core:") + print(" 1. Update 'solo_rpc_url' in macminer.json with remote host IP/hostname") + print(" Example: \"http://192.168.1.100:8332\"") + print(" 2. Ensure remote Bitcoin Core has rpcallowip configured for your IP") + print(" 3. Update RPC credentials in macminer.json") + print(" 4. Run macminer.py again") + print("\n" + "="*70) + return + + print(f"[SUCCESS] Connected! Block height: {test_work['height']}") + print(f"[INFO] Current difficulty: {bits_to_target(test_work['bits']) / (2**224):,.2f}") + + # Initialize queues and shared state + work_queue = Queue() + result_queue = Queue() + stats_queue = Queue() + + from multiprocessing import Value + stop_flag = Value('i', 0) + + work_info = {'height': test_work['height'], 'difficulty': bits_to_target(test_work['bits']) / (2**224)} + + # Start worker processes + num_workers = config['threads'] + workers = [] + + print(f"\n[INFO] Starting {num_workers} mining workers...") + + for i in range(num_workers): + p = Process(target=mine_worker, args=(i, config, work_queue, result_queue, stats_queue, stop_flag)) + p.start() + workers.append(p) + + time.sleep(1) + + # Start stats display in separate process + stats_process = Process(target=display_stats, args=(stats_queue, num_workers, config, work_info)) + stats_process.start() + + # Distribute initial work + for _ in range(num_workers): + work_queue.put(test_work) + + try: + last_update = time.time() + + # Main loop - get new work and check for results + while True: + # Get new work periodically + if time.time() - last_update >= config['update_interval']: + new_work = get_work_solo(config) + if new_work: + work_info['height'] = new_work['height'] + work_info['difficulty'] = bits_to_target(new_work['bits']) / (2**224) + + # Distribute to all workers + for _ in range(num_workers * 2): # Extra work items in queue + work_queue.put(new_work) + + last_update = time.time() + + # Check for found blocks + while not result_queue.empty(): + result = result_queue.get() + if result['found']: + # Submit the block + submit_work_solo(config, result['work'], result['nonce']) + + time.sleep(1) + + except KeyboardInterrupt: + print("\n\n[INFO] Stopping miners...") + stop_flag.value = 1 + + # Wait for workers to stop + for w in workers: + w.join(timeout=3) + if w.is_alive(): + w.terminate() + + stats_process.terminate() + stats_process.join(timeout=1) + + print("[INFO] Mining stopped. Goodbye!") + + +if __name__ == "__main__": + main()