# #!/usr/bin/env python3 # """ # stratum_proxy.py # RinCoin Stratum Proxy Server # DEBUG RPC: we get node logs with 'docker logs --tail=200 rincoin-node' # MINE: /mnt/shared/DEV/repos/d-popov.com/mines/rin/miner/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://localhost:3334 -u x -p x -t 32 # """ # import socket # import threading # import json # import time # import requests # import hashlib # import struct # from requests.auth import HTTPBasicAuth # class RinCoinStratumProxy: # def __init__(self, stratum_host='0.0.0.0', stratum_port=3334, # rpc_host='127.0.0.1', rpc_port=9556, # rpc_user='rinrpc', rpc_password='745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90', # target_address='rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q', # submit_all_blocks=False, submit_threshold=0.1): # self.stratum_host = stratum_host # self.stratum_port = stratum_port # self.rpc_host = rpc_host # self.rpc_port = rpc_port # self.rpc_user = rpc_user # self.rpc_password = rpc_password # self.target_address = target_address # self.submit_all_blocks = submit_all_blocks # # For debugging: submit blocks that meet this fraction of network difficulty # self.submit_threshold = submit_threshold # Configurable percentage of network difficulty # self.clients = {} # self.job_counter = 0 # self.current_job = None # self.running = True # self.extranonce1_counter = 0 # # Dynamic difficulty adjustment # self.share_stats = {} # Track shares per client # self.last_difficulty_adjustment = time.time() # self.target_share_interval = 15 # Target: 1 share every 15 seconds (optimal for 1-min blocks) # # Production monitoring # self.stats = { # 'start_time': time.time(), # 'total_shares': 0, # 'accepted_shares': 0, # 'rejected_shares': 0, # 'blocks_found': 0, # 'total_hashrate': 0, # 'connections': 0, # 'last_share_time': time.time(), # 'shares_last_minute': [], # 'current_share_rate': 0.0 # } # self.max_connections = 50 # Production limit # # Logging setup # self.log_file = "mining_log.txt" # self.init_log_file() # print(f"RinCoin Stratum Proxy Server") # print(f"Stratum: {stratum_host}:{stratum_port}") # print(f"RPC: {rpc_host}:{rpc_port}") # print(f"Target: {target_address}") # def init_log_file(self): # """Initialize mining log file with header""" # try: # with open(self.log_file, 'w') as f: # f.write("=" * 80 + "\n") # f.write("RinCoin Mining Log\n") # f.write("=" * 80 + "\n") # f.write(f"Started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") # f.write(f"Target Address: {self.target_address}\n") # f.write(f"Stratum: {self.stratum_host}:{self.stratum_port}\n") # f.write(f"RPC: {self.rpc_host}:{self.rpc_port}\n") # f.write("=" * 80 + "\n\n") # except Exception as e: # print(f"Failed to initialize log file: {e}") # def log_hash_found(self, hash_hex, difficulty, height, reward, nonce, ntime): # """Log found hash to file""" # try: # timestamp = time.strftime('%Y-%m-%d %H:%M:%S') # with open(self.log_file, 'a') as f: # f.write(f"[{timestamp}] ๐ŸŽ‰ HASH FOUND!\n") # f.write(f" Hash: {hash_hex}\n") # f.write(f" Difficulty: {difficulty:.6f}\n") # f.write(f" Height: {height}\n") # f.write(f" Reward: {reward:.8f} RIN\n") # f.write(f" Nonce: {nonce}\n") # f.write(f" Time: {ntime}\n") # f.write("-" * 40 + "\n") # except Exception as e: # print(f"Failed to log hash: {e}") # def log_wallet_balance(self): # """Log current wallet balance to file""" # try: # balance = self.rpc_call("getbalance") # if balance is not None: # timestamp = time.strftime('%Y-%m-%d %H:%M:%S') # with open(self.log_file, 'a') as f: # f.write(f"[{timestamp}] ๐Ÿ’ฐ Wallet Balance: {balance:.8f} RIN\n") # except Exception as e: # print(f"Failed to log wallet balance: {e}") # def rpc_call(self, method, params=[]): # """Make RPC call to RinCoin node""" # try: # url = f"http://{self.rpc_host}:{self.rpc_port}/" # headers = {'content-type': 'text/plain'} # auth = HTTPBasicAuth(self.rpc_user, self.rpc_password) # payload = { # "jsonrpc": "1.0", # "id": "stratum_proxy", # "method": method, # "params": params # } # response = requests.post(url, json=payload, headers=headers, auth=auth, timeout=30) # if response.status_code == 200: # result = response.json() # if 'error' in result and result['error'] is not None: # print(f"RPC Error: {result['error']}") # return None # return result.get('result') # else: # print(f"HTTP Error: {response.status_code}") # return None # except Exception as e: # print(f"RPC Call Error: {e}") # return None # def encode_varint(self, n): # """Encode integer as Bitcoin-style varint""" # if n < 0xfd: # return bytes([n]) # elif n <= 0xffff: # return b"\xfd" + struct.pack(' bytes: # outputs_blob = b'' # outputs_list = [] # # Main output # outputs_list.append(struct.pack(' 1: # if len(hashes) % 2 == 1: # hashes.append(hashes[-1]) # Duplicate last hash if odd # next_level = [] # for i in range(0, len(hashes), 2): # combined = hashes[i] + hashes[i + 1] # next_level.append(hashlib.sha256(hashlib.sha256(combined).digest()).digest()) # hashes = next_level # return hashes[0] if hashes else b'\x00' * 32 # except Exception as e: # print(f"Merkle root calculation error: {e}") # return b'\x00' * 32 # def calculate_merkle_branches(self, tx_hashes, tx_index): # """Calculate merkle branches for a specific transaction index""" # try: # if not tx_hashes or tx_index >= len(tx_hashes): # return [] # branches = [] # current_index = tx_index # # Start with the full list of transaction hashes # hashes = tx_hashes[:] # while len(hashes) > 1: # if len(hashes) % 2 == 1: # hashes.append(hashes[-1]) # Duplicate last hash if odd # # Find the partner for current_index # partner_index = current_index ^ 1 # Flip the least significant bit # if partner_index < len(hashes): # # Add the partner hash to branches (in big-endian for stratum) # branches.append(hashes[partner_index][::-1].hex()) # else: # # This shouldn't happen, but add the duplicate if it does # branches.append(hashes[current_index][::-1].hex()) # # Move to next level # next_level = [] # for i in range(0, len(hashes), 2): # combined = hashes[i] + hashes[i + 1] # next_level.append(hashlib.sha256(hashlib.sha256(combined).digest()).digest()) # hashes = next_level # current_index //= 2 # return branches # except Exception as e: # print(f"Merkle branches calculation error: {e}") # return [] # def bits_to_target(self, bits_hex): # """Convert bits to target - FIXED VERSION""" # try: # bits = int(bits_hex, 16) # exponent = bits >> 24 # mantissa = bits & 0xffffff # # Bitcoin target calculation # if exponent <= 3: # target = mantissa >> (8 * (3 - exponent)) # else: # target = mantissa << (8 * (exponent - 3)) # return f"{target:064x}" # except Exception as e: # print(f"Bits to target error: {e}") # return "0000ffff00000000000000000000000000000000000000000000000000000000" # def get_block_template(self): # """Get new block template and create Stratum job""" # try: # template = self.rpc_call("getblocktemplate", [{"rules": ["segwit", "mweb"]}]) # if not template: # return None # self.job_counter += 1 # job = { # "job_id": f"job_{self.job_counter:08x}", # "template": template, # "prevhash": template.get("previousblockhash", "0" * 64), # "version": template.get('version', 1), # "bits": template.get('bits', '1d00ffff'), # "ntime": f"{int(time.time()):08x}", # "target": self.bits_to_target(template.get('bits', '1d00ffff')), # "height": template.get('height', 0), # "coinbasevalue": template.get('coinbasevalue', 0), # "transactions": template.get('transactions', []) # } # self.current_job = job # timestamp = time.strftime("%Y-%m-%d %H:%M:%S") # network_difficulty = self.calculate_network_difficulty(job['target']) # print(f"[{timestamp}] ๐Ÿ†• NEW JOB: {job['job_id']} | Height: {job['height']} | Reward: {job['coinbasevalue']/100000000:.2f} RIN") # print(f" ๐ŸŽฏ Network Difficulty: {network_difficulty:.6f} | Bits: {job['bits']}") # print(f" ๐Ÿ“ Target: {job['target'][:16]}... | Transactions: {len(job['transactions'])}") # return job # except Exception as e: # print(f"Get block template error: {e}") # return None # def calculate_share_difficulty(self, hash_hex, target_hex): # """Calculate actual share difficulty from hash - FIXED""" # try: # hash_int = int(hash_hex, 16) # if hash_int == 0: # return float('inf') # Perfect hash # # Bitcoin-style difficulty calculation using difficulty 1 target # # Difficulty 1 target for mainnet # diff1_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 # # Share difficulty = how much harder this hash was compared to diff 1 # difficulty = diff1_target / hash_int # return difficulty # except Exception as e: # print(f"Difficulty calculation error: {e}") # return 0.0 # def calculate_network_difficulty(self, target_hex): # """Calculate network difficulty from target - FIXED""" # try: # target_int = int(target_hex, 16) # # Bitcoin difficulty 1.0 target # diff1_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 # # Network difficulty = how much harder than difficulty 1.0 # network_difficulty = diff1_target / target_int # return network_difficulty # except Exception as e: # print(f"Network difficulty calculation error: {e}") # return 1.0 # def calculate_optimal_difficulty(self, addr, is_new_client=False): # """Calculate optimal stratum difficulty for a client based on hashrate and network conditions""" # try: # # Get current network difficulty # network_diff = self.calculate_network_difficulty(self.current_job['target']) if self.current_job else 1.0 # if is_new_client: # # Calculate difficulty for 4 shares per minute (15 second intervals) # # Formula: difficulty = (shares_per_second * 2^32) / hashrate # target_shares_per_second = 4 / 60 # 4 shares per minute # assumed_hashrate = 680000 # H/s (conservative estimate) # calculated_difficulty = (target_shares_per_second * (2**32)) / assumed_hashrate # # Scale based on network difficulty to maintain relative percentage # target_percentage = calculated_difficulty / network_diff if network_diff > 0 else 0.32 # scaled_difficulty = network_diff * target_percentage # # Use the calculated difficulty (should be around 421 for 680kH/s) # initial_difficulty = calculated_difficulty # # Ensure reasonable bounds # min_difficulty = 0.0001 # Absolute minimum # # IMPORTANT: DO NOT CHANGE THIS VALUE. our pool may grow big in the future, so we need to be able to handle it. # max_difficulty = network_diff * 0.1 # Maximum 10% of network # initial_difficulty = max(min_difficulty, min(max_difficulty, initial_difficulty)) # percentage = (initial_difficulty / network_diff) * 100 # print(f" ๐Ÿ“Š NEW CLIENT {addr}: Network diff {network_diff:.6f}") # print(f" ๐ŸŽฏ Starting difficulty: {initial_difficulty:.6f} ({percentage:.4f}% of network)") # miner_hashrate = self.clients.get(addr, {}).get('estimated_hashrate', 0) # if miner_hashrate > 0: # print(f" Target: 1 share every {self.target_share_interval}s @ {miner_hashrate:.0f} H/s") # else: # print(f" Target: 1 share every {self.target_share_interval}s (miner hashrate unknown)") # print(f" ๐Ÿ”ง Per-miner adjustment: Difficulty will adapt to each device's actual hashrate") # return initial_difficulty # # For existing clients, adjust based on actual hashrate performance # client_data = self.clients.get(addr, {}) # if not client_data: # return self.calculate_optimal_difficulty(addr, is_new_client=True) # current_time = time.time() # connect_time = client_data.get('connect_time', current_time) # share_count = client_data.get('share_count', 0) # current_difficulty = client_data.get('stratum_difficulty', network_diff * 0.005) # estimated_hashrate = client_data.get('estimated_hashrate', 0) # # Need at least 3 shares to make adjustments # if share_count < 3: # return current_difficulty # # Calculate target difficulty based on hashrate and desired share interval # # Target: 1 share every 15 seconds (self.target_share_interval) # if estimated_hashrate > 0: # # Formula: difficulty = (hashrate * target_time) / (2^32) # diff1_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 # target_difficulty = (estimated_hashrate * self.target_share_interval) / (2**32) # # Bounds checking # min_difficulty = network_diff * 0.0001 # 0.01% of network # max_difficulty = network_diff * 0.02 # 2% of network # target_difficulty = max(min_difficulty, min(max_difficulty, target_difficulty)) # # Conservative adjustment - move only 50% towards target each time # adjustment_factor = 0.5 # new_difficulty = current_difficulty + (target_difficulty - current_difficulty) * adjustment_factor # print(f" ๐Ÿ“Š {addr}: {estimated_hashrate:.0f} H/s, {share_count} shares, adjusting {current_difficulty:.6f} โ†’ {new_difficulty:.6f}") # return new_difficulty # return current_difficulty # except Exception as e: # print(f"Difficulty calculation error: {e}") # # Fallback to conservative difficulty # network_diff = self.calculate_network_difficulty(self.current_job['target']) if self.current_job else 1.0 # return max(network_diff * 0.001, 0.0001) # def adjust_global_difficulty_if_needed(self): # """Adjust difficulty globally if share rate is too high/low""" # try: # current_rate = self.stats['current_share_rate'] # # Target: 0.5-2 shares per second (30-120 second intervals) # target_min_rate = 0.008 # ~1 share per 2 minutes # target_max_rate = 0.033 # ~1 share per 30 seconds # if current_rate > target_max_rate: # # Too many shares - increase difficulty for all clients # multiplier = min(2.0, current_rate / target_max_rate) # print(f" ๐Ÿšจ HIGH SHARE RATE: {current_rate:.3f}/s > {target_max_rate:.3f}/s - increasing difficulty by {multiplier:.2f}x") # for addr in self.clients: # current_diff = self.clients[addr].get('stratum_difficulty', 0.001) # new_diff = min(0.001, current_diff * multiplier) # self.clients[addr]['stratum_difficulty'] = new_diff # print(f" ๐Ÿ“ˆ {addr}: {current_diff:.6f} โ†’ {new_diff:.6f}") # elif current_rate < target_min_rate and current_rate > 0: # # Too few shares - decrease difficulty for all clients # multiplier = max(0.5, target_min_rate / current_rate) # print(f" ๐ŸŒ LOW SHARE RATE: {current_rate:.3f}/s < {target_min_rate:.3f}/s - decreasing difficulty by {multiplier:.2f}x") # for addr in self.clients: # current_diff = self.clients[addr].get('stratum_difficulty', 0.001) # new_diff = max(0.000001, current_diff / multiplier) # self.clients[addr]['stratum_difficulty'] = new_diff # print(f" ๐Ÿ“‰ {addr}: {current_diff:.6f} โ†’ {new_diff:.6f}") # except Exception as e: # print(f"Global difficulty adjustment error: {e}") # def adjust_client_difficulty(self, addr): # """Adjust difficulty for a specific client if needed""" # try: # current_time = time.time() # # Only adjust every 2 minutes minimum # last_adjustment = self.clients[addr].get('last_difficulty_adjustment', 0) # if current_time - last_adjustment < 120: # return # new_difficulty = self.calculate_optimal_difficulty(addr, is_new_client=False) # current_difficulty = self.clients[addr].get('stratum_difficulty', 0.001) # # Only send update if difficulty changed significantly (>20%) # if abs(new_difficulty - current_difficulty) / current_difficulty > 0.2: # self.clients[addr]['stratum_difficulty'] = new_difficulty # self.clients[addr]['last_difficulty_adjustment'] = current_time # # Send new difficulty to client # if 'socket' in self.clients[addr]: # self.send_stratum_notification(self.clients[addr]['socket'], "mining.set_difficulty", [new_difficulty]) # print(f" ๐ŸŽฏ Updated difficulty for {addr}: {current_difficulty:.6f} โ†’ {new_difficulty:.6f}") # except Exception as e: # print(f"Difficulty adjustment error for {addr}: {e}") # def submit_share(self, job, extranonce1, extranonce2, ntime, nonce, addr=None, target_address=None): # """Validate share and submit block if valid - FIXED VERSION""" # try: # # Use provided address or default # address = target_address or self.target_address # # Build coinbase (with and without witness) # coinbase_wit, coinbase_nowit = self.build_coinbase_transaction_for_address( # job['template'], extranonce1, extranonce2, address) # if not coinbase_wit or not coinbase_nowit: # return False, "Coinbase construction failed" # # Calculate coinbase txid (non-witness serialization) # coinbase_txid = hashlib.sha256(hashlib.sha256(coinbase_nowit).digest()).digest()[::-1] # # Calculate merkle root # merkle_root = self.calculate_merkle_root(coinbase_txid, job['transactions']) # # Build block header - FIXED ENDIANNESS # header = b'' # header += struct.pack('= client_stratum_diff # meets_network_difficulty = share_difficulty >= network_difficulty # # Track share rate # current_time = time.time() # self.stats['shares_last_minute'].append(current_time) # # Remove shares older than 60 seconds # self.stats['shares_last_minute'] = [t for t in self.stats['shares_last_minute'] if current_time - t <= 60] # self.stats['current_share_rate'] = len(self.stats['shares_last_minute']) / 60.0 # # Enhanced logging # timestamp = time.strftime("%Y-%m-%d %H:%M:%S") # network_percentage = (share_difficulty / network_difficulty) * 100 if network_difficulty > 0 else 0 # stratum_percentage = (share_difficulty / client_stratum_diff) * 100 if client_stratum_diff > 0 else 0 # # Progress indicator based on difficulty comparison # if meets_network_difficulty: # progress_icon = "๐ŸŽ‰" # Block found! # elif meets_stratum_difficulty: # progress_icon = "โœ…" # Valid share for stratum # elif network_percentage >= 50: # progress_icon = "๐Ÿ”ฅ" # Very close to network # elif network_percentage >= 10: # progress_icon = "โšก" # Getting warm # elif network_percentage >= 1: # progress_icon = "๐Ÿ’ซ" # Some progress # else: # progress_icon = "๐Ÿ“Š" # Low progress # # Calculate network and pool hashrates # block_time = 60 # RinCoin 1-minute blocks # network_hashrate = (network_difficulty * (2**32)) / block_time # network_mhs = network_hashrate / 1e6 # # Calculate pool hashrate (sum of all connected miners) # pool_hashrate = 0 # for client_addr, client_data in self.clients.items(): # pool_hashrate += client_data.get('estimated_hashrate', 0) # pool_mhs = pool_hashrate / 1e6 # # Calculate percentages # pool_network_percentage = (pool_mhs / network_mhs) * 100 if network_mhs > 0 else 0 # miner_pool_percentage = (0.87 / pool_mhs) * 100 if pool_mhs > 0 else 0 # Assuming 870kH/s miner # print(f"[{timestamp}] {progress_icon} SHARE: job={job['job_id']} | nonce={nonce} | hash={block_hash_hex[:16]}...") # print(f" ๐ŸŽฏ Share Diff: {share_difficulty:.2e} | Stratum Diff: {client_stratum_diff:.6f} | Network Diff: {network_difficulty:.6f}") # print(f" ๐Ÿ“ˆ Progress: {network_percentage:.4f}% of network, {stratum_percentage:.1f}% of stratum") # print(f" ๐Ÿ“Š Share Rate: {self.stats['current_share_rate']:.2f}/s | Total: {len(self.stats['shares_last_minute'])}/min") # print(f" ๐ŸŒ Network: {network_mhs:.1f} MH/s | Pool: {pool_mhs:.1f} MH/s ({pool_network_percentage:.2f}% of network)") # print(f" โ›๏ธ Miner: 0.87 MH/s ({miner_pool_percentage:.1f}% of pool) | Expected solo: {network_mhs/0.87:.0f}h") # print(f" ๐Ÿ“ Target: {job['target'][:16]}... | Height: {job['height']}") # print(f" โฐ Time: {ntime} | Extranonce: {extranonce1}:{extranonce2}") # print(f" ๐Ÿ” Difficulty Check: Share {share_difficulty:.2e} vs Stratum {client_stratum_diff:.6f} = {'โœ…' if meets_stratum_difficulty else 'โŒ'}") # print(f" ๐Ÿ” Difficulty Check: Share {share_difficulty:.2e} vs Network {network_difficulty:.6f} = {'โœ…' if meets_network_difficulty else 'โŒ'}") # # Initialize submission variables # submit_network_difficulty = meets_network_difficulty # submit_debug_threshold = share_difficulty >= (network_difficulty * self.submit_threshold) # if submit_network_difficulty: # submit_reason = "meets network difficulty" # elif submit_debug_threshold: # submit_reason = f"meets debug threshold ({self.submit_threshold*100:.0f}% of network)" # else: # submit_reason = "does not meet minimum requirements" # # Handle submit_all_blocks mode - bypass all difficulty checks # if self.submit_all_blocks: # print(f" ๐Ÿงช TEST MODE: Submitting ALL shares to node for validation") # # Update stats for test mode # self.stats['total_shares'] += 1 # self.stats['accepted_shares'] += 1 # # Always submit in test mode # should_submit = True # else: # # Normal mode - check stratum difficulty properly # if not meets_stratum_difficulty: # # Share doesn't meet minimum stratum difficulty - reject # print(f" โŒ Share rejected (difficulty too low: {share_difficulty:.2e} < {client_stratum_diff:.6f})") # self.stats['total_shares'] += 1 # self.stats['rejected_shares'] += 1 # return False, "Share does not meet stratum difficulty" # # Valid stratum share! Update client stats # if addr and addr in self.clients: # current_time = time.time() # self.clients[addr]['share_count'] = self.clients[addr].get('share_count', 0) + 1 # self.clients[addr]['last_share_time'] = current_time # # Calculate hashrate from this share # # Hashrate = difficulty * 2^32 / time_to_find_share # client_difficulty = self.clients[addr].get('stratum_difficulty', 0.001) # last_share_time = self.clients[addr].get('last_share_time', current_time - 60) # time_since_last = max(1, current_time - last_share_time) # Avoid division by zero # # Calculate instantaneous hashrate for this share # diff1_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 # instant_hashrate = (client_difficulty * (2**32)) / time_since_last # # Store hashrate sample (keep last 10 samples) # if 'hashrate_samples' not in self.clients[addr]: # self.clients[addr]['hashrate_samples'] = [] # self.clients[addr]['hashrate_samples'].append((current_time, instant_hashrate)) # # Keep only last 10 samples # if len(self.clients[addr]['hashrate_samples']) > 10: # self.clients[addr]['hashrate_samples'] = self.clients[addr]['hashrate_samples'][-10:] # # Calculate average hashrate from recent samples # if self.clients[addr]['hashrate_samples']: # total_hashrate = sum(hr for _, hr in self.clients[addr]['hashrate_samples']) # self.clients[addr]['estimated_hashrate'] = total_hashrate / len(self.clients[addr]['hashrate_samples']) # print(f" โ›๏ธ Miner {addr}: {self.clients[addr]['estimated_hashrate']:.0f} H/s (avg of {len(self.clients[addr]['hashrate_samples'])} samples)") # # Update global stats # self.stats['total_shares'] += 1 # self.stats['accepted_shares'] += 1 # # Check if we should adjust difficulty # self.adjust_client_difficulty(addr) # # Global difficulty adjustment based on share rate # self.adjust_global_difficulty_if_needed() # should_submit = submit_network_difficulty or submit_debug_threshold # # Submit block if conditions are met # if should_submit: # if submit_network_difficulty: # print(f" ๐ŸŽ‰ VALID BLOCK FOUND! Hash: {block_hash_hex}") # print(f" ๐Ÿ’ฐ Reward: {job['coinbasevalue']/100000000:.2f} RIN -> {address}") # print(f" ๐Ÿ“Š Block height: {job['height']}") # print(f" ๐Ÿ” Difficulty: {share_difficulty:.6f} (target: {network_difficulty:.6f})") # print(f" ๐Ÿ“‹ Submit reason: {submit_reason}") # # Log the found hash - ONLY for actual network-valid blocks # reward_rin = job['coinbasevalue'] / 100000000 # self.log_hash_found(block_hash_hex, share_difficulty, job['height'], reward_rin, nonce, ntime) # elif submit_debug_threshold: # print(f" ๐Ÿงช DEBUG SUBMISSION! Hash: {block_hash_hex} ({submit_reason})") # print(f" ๐Ÿ“Š Block height: {job['height']} | Difficulty: {share_difficulty:.6f}") # print(f" ๐Ÿ’ก This is a test submission - not a real block") # else: # print(f" ๐Ÿงช TEST BLOCK SUBMISSION! Hash: {block_hash_hex} (submit_all_blocks=True)") # print(f" ๐Ÿ“Š Block height: {job['height']} | Difficulty: {share_difficulty:.6f}") # print(f" ๐Ÿ’ก This is a test submission - not a real block") # # Build complete block # block = header # # Transaction count # tx_count = 1 + len(job['transactions']) # block += self.encode_varint(tx_count) # # Add coinbase transaction (witness variant for block body) # block += coinbase_wit # # Add other transactions # for tx in job['transactions']: # block += bytes.fromhex(tx['data']) # # Submit block # block_hex = block.hex() # print(f" ๐Ÿ“ฆ Submitting block of size {len(block_hex)//2} bytes...") # print(f" ๐Ÿš€ Calling RPC: submitblock([block_hex={len(block_hex)//2}_bytes])") # result = self.rpc_call("submitblock", [block_hex]) # print(f" ๐Ÿ“ก RPC RESPONSE: {result}") # Log the actual RPC response # if result is None: # print(f" โœ… Block accepted by network!") # print(f" ๐ŸŽŠ SUCCESS: Block {job['height']} submitted successfully!") # print(f" ๐Ÿ’ฐ Reward earned: {job['coinbasevalue']/100000000:.8f} RIN") # # Update block stats # self.stats['blocks_found'] += 1 # # Log wallet balance after successful block submission # self.log_wallet_balance() # return True, "Block found and submitted" # else: # print(f" โŒ Block rejected: {result}") # print(f" ๐Ÿ” Debug: Block size {len(block_hex)//2} bytes, {len(job['transactions'])} transactions") # print(f" ๐Ÿ“‹ Full RPC error details: {result}") # return False, f"Block rejected: {result}" # else: # # Valid stratum share but not network-valid block (normal mode only) # if not self.submit_all_blocks: # print(f" โœ… Valid stratum share accepted") # return True, "Valid stratum share" # else: # # This shouldn't happen in test mode since should_submit would be True # return True, "Test mode share processed" # except Exception as e: # print(f"Share submission error: {e}") # return False, f"Submission error: {e}" # def send_stratum_response(self, client, msg_id, result, error=None): # """Send Stratum response to client""" # try: # response = { # "id": msg_id, # "result": result, # "error": error # } # message = json.dumps(response) + "\n" # client.send(message.encode('utf-8')) # except Exception as e: # print(f"Send response error: {e}") # def send_stratum_notification(self, client, method, params): # """Send Stratum notification to client""" # try: # notification = { # "id": None, # "method": method, # "params": params # } # message = json.dumps(notification) + "\n" # client.send(message.encode('utf-8')) # except Exception as e: # print(f"Send notification error: {e}") # def handle_stratum_message(self, client, addr, message): # """Handle incoming Stratum message from miner""" # try: # # Debug: log raw message # print(f"[{addr}] Raw message: {repr(message)}") # data = json.loads(message.strip()) # method = data.get("method") # msg_id = data.get("id") # params = data.get("params", []) # print(f"[{addr}] Parsed: method={method}, id={msg_id}, params={params}") # if method == "mining.subscribe": # # Generate unique extranonce1 for this connection # self.extranonce1_counter += 1 # extranonce1 = f"{self.extranonce1_counter:08x}" # # Store extranonce1 for this client # if addr not in self.clients: # self.clients[addr] = {} # self.clients[addr]['extranonce1'] = extranonce1 # # Subscribe response # self.send_stratum_response(client, msg_id, [ # [["mining.set_difficulty", "subscription_id"], ["mining.notify", "subscription_id"]], # extranonce1, # 4 # extranonce2 size # ]) # # Calculate optimal stratum difficulty for this client # initial_difficulty = self.calculate_optimal_difficulty(addr, is_new_client=True) # # Store stratum difficulty for this client # if addr not in self.clients: # self.clients[addr] = {} # self.clients[addr]['stratum_difficulty'] = initial_difficulty # self.clients[addr]['last_share_time'] = time.time() # self.clients[addr]['share_count'] = 0 # self.clients[addr]['connect_time'] = time.time() # self.clients[addr]['hashrate_samples'] = [] # Store recent hashrate measurements # self.clients[addr]['estimated_hashrate'] = 0 # Current estimated hashrate # self.send_stratum_notification(client, "mining.set_difficulty", [initial_difficulty]) # print(f" ๐ŸŽฏ Set initial difficulty {initial_difficulty:.6f} for {addr}") # # Send initial job # if self.current_job: # self.send_job_to_client(client, self.current_job) # else: # if self.get_block_template(): # self.send_job_to_client(client, self.current_job) # elif method == "mining.authorize": # username = params[0] if params else "anonymous" # self.clients[addr]['username'] = username # self.send_stratum_response(client, msg_id, True) # timestamp = time.strftime("%Y-%m-%d %H:%M:%S") # print(f"[{timestamp}] ๐Ÿ” [{addr}] Authorized as {username}") # elif method == "login": # # Handle xmrig's login method (JSON-RPC format with object params) # if isinstance(params, dict): # # xmrig format: {"login": "username", "pass": "password", "agent": "...", "algo": [...]} # username = params.get('login', 'anonymous') # password = params.get('pass', 'x') # agent = params.get('agent', 'unknown') # algorithms = params.get('algo', []) # else: # # Standard stratum format: ["username", "password"] # username = params[0] if params else "anonymous" # password = params[1] if len(params) > 1 else "x" # agent = "unknown" # algorithms = [] # self.clients[addr]['username'] = username # self.clients[addr]['password'] = password # self.clients[addr]['agent'] = agent # self.clients[addr]['algorithms'] = algorithms # # Check if rinhash is supported # rinhash_supported = any('rinhash' in algo.lower() or 'rin' in algo.lower() for algo in algorithms) # if not rinhash_supported: # print(f"[{addr}] Warning: rinhash not in supported algorithms: {algorithms}") # self.send_stratum_response(client, msg_id, True) # timestamp = time.strftime("%Y-%m-%d %H:%M:%S") # print(f"[{timestamp}] ๐Ÿ” [{addr}] xmrig Login as {username} (agent: {agent})") # print(f"[{addr}] Supported algorithms: {algorithms}") # # Send initial job after login # if self.current_job: # self.send_job_to_client(client, self.current_job) # else: # if self.get_block_template(): # self.send_job_to_client(client, self.current_job) # elif method == "mining.extranonce.subscribe": # self.send_stratum_response(client, msg_id, True) # elif method == "mining.submit": # if len(params) >= 5: # username = params[0] # job_id = params[1] # extranonce2 = params[2] # ntime = params[3] # nonce = params[4] # print(f"[{addr}] Submit: {username} | job={job_id} | nonce={nonce}") # # Always validate against current job # if self.current_job: # extranonce1 = self.clients[addr].get('extranonce1', '00000000') # # Submit share # success, message = self.submit_share(self.current_job, extranonce1, extranonce2, ntime, nonce, addr) # # Always accept shares for debugging, even if they don't meet target # self.send_stratum_response(client, msg_id, True) # if success and "Block found" in message: # # Get new job after block found # threading.Thread(target=self.update_job_after_block, daemon=True).start() # else: # self.send_stratum_response(client, msg_id, True) # else: # self.send_stratum_response(client, msg_id, False, "Invalid parameters") # else: # print(f"[{addr}] Unknown method: {method}") # self.send_stratum_response(client, msg_id, None, "Unknown method") # except json.JSONDecodeError as e: # print(f"[{addr}] Invalid JSON: {message}") # print(f"[{addr}] JSON Error: {e}") # except Exception as e: # print(f"[{addr}] Message handling error: {e}") # print(f"[{addr}] Error type: {type(e).__name__}") # def send_job_to_client(self, client, job): # """Send mining job to specific client with proper stratum parameters""" # try: # # Get network difficulty for display # network_difficulty = self.calculate_network_difficulty(job['target']) # # Build proper coinbase transaction parts for stratum protocol # template = job.get('template', {}) # height = job.get('height', 0) # # Encode height as minimal push (BIP34 compliance) # if height == 0: # height_push = b'' # else: # height_bytes = struct.pack(' 1 and height_bytes[-1] == 0: # height_bytes = height_bytes[:-1] # height_push = bytes([len(height_bytes)]) + height_bytes # # Build coinbase input script: height + arbitrary data + extranonces # coinb1_script = height_push + b'/RinCoin/' # # Build coinbase transaction structure # # Version (4 bytes) # coinb1 = struct.pack('= self.max_connections: # print(f"[{addr}] Connection rejected - max connections ({self.max_connections}) reached") # client.close() # return # print(f"[{addr}] Connected (clients: {len(self.clients) + 1}/{self.max_connections})") # if addr not in self.clients: # self.clients[addr] = {} # self.clients[addr]['socket'] = client # self.stats['connections'] = len(self.clients) # try: # while self.running: # data = client.recv(4096) # if not data: # break # # Handle multiple messages in one packet # messages = data.decode('utf-8').strip().split('\n') # for message in messages: # if message: # self.handle_stratum_message(client, addr, message) # except Exception as e: # print(f"[{addr}] Client error: {e}") # finally: # client.close() # if addr in self.clients: # del self.clients[addr] # self.stats['connections'] = len(self.clients) # print(f"[{addr}] Disconnected (clients: {len(self.clients)}/{self.max_connections})") # def print_stats(self): # """Print pool statistics""" # try: # uptime = time.time() - self.stats['start_time'] # uptime_str = f"{int(uptime//3600)}h {int((uptime%3600)//60)}m" # accept_rate = 0 # if self.stats['total_shares'] > 0: # accept_rate = (self.stats['accepted_shares'] / self.stats['total_shares']) * 100 # print(f"\n๐Ÿ“Š POOL STATISTICS:") # print(f" โฐ Uptime: {uptime_str}") # print(f" ๐Ÿ‘ฅ Connected miners: {self.stats['connections']}") # print(f" ๐Ÿ“ˆ Shares: {self.stats['accepted_shares']}/{self.stats['total_shares']} ({accept_rate:.1f}% accepted)") # print(f" ๐ŸŽ‰ Blocks found: {self.stats['blocks_found']}") # print(f" ๐ŸŽฏ Network difficulty: {self.calculate_network_difficulty(self.current_job['target']) if self.current_job else 'unknown':.6f}") # # Calculate hashrate estimate from connected clients # total_hashrate = 0 # for addr, client_data in self.clients.items(): # share_count = client_data.get('share_count', 0) # connect_time = client_data.get('connect_time', time.time()) # mining_duration = time.time() - connect_time # if mining_duration > 60 and share_count > 0: # stratum_diff = client_data.get('stratum_difficulty', 0.001) # # Rough hashrate estimate: shares * difficulty * 2^32 / time # hashrate = (share_count * stratum_diff * 4294967296) / mining_duration # total_hashrate += hashrate # print(f" ๐Ÿ”ฅ {addr}: ~{hashrate/1000:.0f} kH/s") # if total_hashrate > 0: # print(f" ๐Ÿš€ Total pool hashrate: ~{total_hashrate/1000:.0f} kH/s") # print() # except Exception as e: # print(f"Stats error: {e}") # def job_updater(self): # """Periodically update mining jobs""" # balance_log_counter = 0 # stats_counter = 0 # while self.running: # try: # time.sleep(30) # Update every 30 seconds # old_height = self.current_job['height'] if self.current_job else 0 # if self.get_block_template(): # new_height = self.current_job['height'] # if new_height > old_height: # print(f"New block detected! Broadcasting new job...") # self.broadcast_new_job() # # Log wallet balance every 10 minutes (20 cycles of 30 seconds) # balance_log_counter += 1 # if balance_log_counter >= 20: # self.log_wallet_balance() # balance_log_counter = 0 # # Print stats every 5 minutes (10 cycles of 30 seconds) # stats_counter += 1 # if stats_counter >= 10: # self.print_stats() # stats_counter = 0 # except Exception as e: # print(f"Job updater error: {e}") # def validate_mining_capability(self): # """Validate that we can mine valid blocks against the RinCoin node""" # try: # print("๐Ÿ” Validating mining capability...") # # Get block template # template = self.rpc_call("getblocktemplate", [{"rules": ["segwit", "mweb"]}]) # if not template: # print("โŒ Cannot get block template") # return False # # Test address validation # result = self.rpc_call("validateaddress", [self.target_address]) # if not result or not result.get('isvalid'): # print(f"โŒ Target address {self.target_address} is invalid") # return False # print(f"โœ… Target address {self.target_address} is valid") # # Test coinbase construction # coinbase_wit, coinbase_nowit = self.build_coinbase_transaction_for_address( # template, "00000001", "01000000", self.target_address) # if not coinbase_wit or not coinbase_nowit: # print("โŒ Cannot construct coinbase transaction") # return False # print(f"โœ… Coinbase construction works") # # Test block header construction with dummy values # test_nonce = "12345678" # test_ntime = f"{int(time.time()):08x}" # # Calculate merkle root # coinbase_txid = hashlib.sha256(hashlib.sha256(coinbase_nowit).digest()).digest()[::-1] # merkle_root = self.calculate_merkle_root(coinbase_txid, template.get('transactions', [])) # # Build test header # header = b'' # header += struct.pack('