VDS挖矿算法概述

2025-10-15 · admin

V-Dimension(VOLLAR/VDS)采用了改造后的Equihash算法(参数为n=96, k=5)加上Scrypt后处理的混合PoW机制,被矿池统计网站称为“Equihash+Scrypt”算法。Equihash本身是一种基于广义生日问题的内存硬化算法,求解时需要生成大量数据并找出碰撞,对内存和并行性要求较高。VDS在找到Equihash解(69字节solution)后,还需对包含该解的区块头使用scrypt(参数N=1024, r=1)做一次散列,以获得最终的PoW哈希值。

Stratum挖矿协议流程

挖矿客户端通过Stratum协议与矿池通信。首先需要建立TCP连接并发送mining.subscribe消息,订阅矿池作业。根据Stratum规范,mining.subscribe通常包含客户端标识等参数。矿池返回的响应包括一个nonce1(示例中为27fff23b)。VDS协议规定整段Nonce长度为32字节,其中返回的nonce1作为前缀,矿工需自行生成剩余部分nonce2(长度=32−len(nonce1))。完成订阅后,客户端发送mining.authorize消息提交用户名和密码。授权成功后,矿池通常先通过mining.set_target通知当前目标难度,然后通过mining.notify下发新作业(job)。通知消息携带以下关键字段:作业ID、区块版本号、前一块哈希(prevHash)、Merkle树根(merkleRoot)、Sapling状态根(hashFinalSaplingRoot)、nTime、nBits等。客户端需将这些字段拼接到区块头中,以供求解Equihash。

构建区块头与Nonce处理

收到mining.notify后,客户端首先将接收到的字段(version、prevHash、merkleRoot、saplingRoot、nTime、nBits等)按顺序拼入一个缓冲区作为区块头的模板(示例详见)。在该模板末尾插入32字节的Nonce:前几字节使用矿池返回的nonce1,后续字节使用矿工生成的nonce2。例如本例中最终Nonce为27fff23b+b5080000000000000000000000000005000000000000000000000000。注意除了Nonce之外,也要插入与VDS协议相关的额外字段(如nvibpool等),以构造出完整的待解头数据。此时区块头(不含solution)长度约为212字节。加入解算出的Equihash解(solution,69字节)后,再对整个头数据调用scrypt_1024_1散列函数,即可得到最终的powHash。

Equihash求解与调用C/C++实现

由于Equihash-96_5计算复杂度极高,Python纯实现效率远低于C/C++。实际开发中通常编译现有的C/C++求解器(例如nheqminer提供的代码)为动态链接库,然后通过Python调用。Python的ctypes库允许加载DLL或共享库并调用其中的函数 。例如,可以用Visual Studio等工具将Equihash-96_5-VDS项目的solver编译为solver.dll,然后在Python中使用ctypes.CDLL("solver.dll")加载,配置函数参数和返回类型。调用求解函数时,将区块头字节串、nonce等数据传入,获取candidate solution。需要注意,调用C函数时可传入原始字节数据,避免Python进行昂贵的二进制处理。

提交结果与验证

当得到一个候选解(solution)时,客户端应将其附加到区块头模板中,构造出完整的headerWithSolution。然后对该头数据执行scrypt_1024_1计算,如果输出powHash满足目标难度(小于等于mining.set_target给定的值)则认为找到了一个有效的分享。随后,客户端通过mining.submit向矿池提交结果。提交内容包括用户名、jobId、当前时间、所用的nonce2和Equihash解。矿池返回的响应用来判断提交是否有效(true表示接受,false或error表示无效)。在整个过程中,需要持续监测新的mining.notifymining.set_target消息,一旦有新的作业或难度变化,应立即中断当前搜索并切换到最新任务。

多线程与性能优化

为充分利用多核CPU和隐藏网络延时,建议使用多线程方案。Python的threading模块可以在单进程内并发执行多个线程。一种常见做法是使用一个线程专门处理网络I/O(接收矿池消息、发送提交等),同时启动多个工作线程并行进行Equihash求解。每个工作线程可以尝试不同的nonce2范围以加快搜索。值得注意的是,CPython存在全局解释器锁(GIL),纯Python代码在多线程下并不能真正并行执行。但调用外部C库时(如通过ctypes)通常会释放GIL,从而可以在多个线程中并行执行算力密集的求解函数。若仍需进一步提升性能,可以考虑使用multiprocessingconcurrent.futures.ProcessPoolExecutor在多进程中运行求解器,彻底绕过GIL限制。同时,可使用线程安全的队列(queue)在网络线程与工作线程间传递作业数据。

实现要点总结

综上所述,一个完整的Equihash-96_5-VDS矿工客户端需要:通过TCP和JSON-RPC实现Stratum协议的消息交互(mining.subscribeauthorizenotifysubmit等);根据矿池下发的作业字段构造待解头数据;调用高效的C/C++ Equihash求解器并处理nonce;完成Scrypt哈希校验;最后将满足条件的解提交给矿池。建议采用Windows下的命令行程序形式,并利用多线程架构提升算力。Python调用ctypescffi加载已有Equihash求解库的能力,为在Python中实现高性能矿工提供了便利。以上设计和实现方案,可以指导开发一个完整的VDS挖矿客户端。

参考资料: 矿池Stratum协议说明;Zcash Equihash算法介绍;VDS矿池交互示例;Python ctypes文档;Python线程并发指南。

我们将采用以下方案:

  1. 核心算法: 我们将使用您提供的 jakeandtom/Equihash-96_5-VDS 项目中的C++代码作为Equihash求解器。这是实现高性能的关键,因为纯Python实现会非常慢。
  2. Python & C++ 交互: 我们将创建一个简单的C++包装器,将其编译成一个动态链接库(在Windows上是 .dll 文件),然后Python将使用 ctypes 库来调用这个DLL中的函数。这是在Python中利用C++性能的常用方法。
  3. 多线程: 程序将使用Python的 threading 模块。一个线程专门负责与矿池的Stratum协议通信,其他多个工作线程(数量可配置)将并行执行挖矿计算。
  4. Stratum协议: 我们将实现一个Stratum客户端,严格按照您提供的协议格式处理 mining.subscribe, mining.authorize, mining.notify, 和 mining.submit 等消息。
  5. 命令行界面: 程序将是一个功能齐全的命令行工具,通过参数接收矿池地址、端口、钱包地址和线程数。

第一步:创建C++求解器动态链接库 (DLL)

首先,我们需要从 jakeandtom/Equihash-96_5-VDS 项目中提取核心代码,并为其创建一个C语言风格的接口,以便Python的ctypes可以轻松调用。

  1. 下载源码: 从 https://github.com/jakeandtom/Equihash-96_5-VDS 下载或克隆项目。您需要以下文件:

    • equihash.h
    • equihash.cpp
    • blake2b.h
    • blake2b.cpp
    • uint256.h
  2. 创建C++包装器 (solver_wrapper.cpp): 在与上述文件相同的目录中,创建一个名为 solver_wrapper.cpp 的新文件。这个文件将包含一个 extern "C" 函数,这是我们将从Python调用的入口点。

    // solver_wrapper.cpp
    #include "equihash.h"
    #include <vector>
    #include <cstring>
    
    // 定义 Equihash 参数 n=96, k=5
    const int n = 96;
    const int k = 5;
    
    // extern "C" 告诉C++编译器以C语言的方式导出这个函数,防止名称修饰
    extern "C" {
        // __declspec(dllexport) 是Windows特有的,用于从DLL中导出函数
        __declspec(dllexport) bool solve_equihash_96_5(
            const unsigned char* header_input, // 输入:区块头 (不含nonce)
            unsigned char* nonce_input,        // 输入/输出:32字节的nonce
            unsigned char* solution_output     // 输出:找到的解决方案 (69字节)
        ) {
            // 初始化Equihash求解器
            Equihash equihash(n, k);
    
            // 将区块头和nonce合并为一个输入
            // VDS的区块头总长度为212字节,其中nonce为32字节
            // 这里的header_input是前 180 字节
            std::vector<unsigned char> input_data(180 + 32);
            memcpy(input_data.data(), header_input, 180);
            memcpy(input_data.data() + 180, nonce_input, 32);
    
            // 调用求解器
            std::vector<unsigned char> solution = equihash.solve(input_data);
    
            // 检查是否找到了一个有效的解
            // 有效解的长度是 2^(k-1) * (n/(k+1)+1) / 8 = 16 * (96/6+1) / 8 = 16 * 17 / 8 = 34
            // 加上solution size的varint,VDS的solution是69字节
            // equihash.solve() 返回的原始解是34字节,我们需要对其进行压缩
            if (!solution.empty()) {
                // VDS的解决方案压缩格式
                // 具体的压缩逻辑比较复杂,我们直接使用库中提供的压缩函数
                std::vector<unsigned char> compressed_solution = equihash.compress_solution(solution);
    
                // VDS的最终解决方案长度是69字节
                if (compressed_solution.size() == 69) {
                    memcpy(solution_output, compressed_solution.data(), 69);
                    return true;
                }
            }
    
            return false;
        }
    }
    

    注:equihash.solve 返回的是原始解,需要通过 compress_solution 才能得到提交给矿池的69字节格式。我们修改包装器以包含这一步。

  3. 编译DLL: 您需要一个C++编译器,例如 MinGW-w64。安装后,确保 g++ 在您的系统PATH中。然后打开命令行,进入存放这些源代码的目录,运行以下命令:

    g++ -shared -o vds_solver.dll solver_wrapper.cpp equihash.cpp blake2b.cpp -O3 -std=c++11
    

    如果一切顺利,您将在同一目录下看到一个名为 vds_solver.dll 的文件。这就是我们的高性能求解器。


第二步:编写Python挖矿客户端

现在我们可以编写Python主程序了。这个脚本将处理所有逻辑:连接矿池、管理任务、调用DLL进行挖矿以及提交结果。

依赖库: 在运行Python脚本之前,请确保安装了必要的库。pylibscrypt 用于最终的PoW验证,虽然主要工作是Equihash,但验证哈希是否低于目标值是必要的。

pip install pylibscrypt

完整的Python代码 (vds_miner.py):

将以下代码保存为 vds_miner.py,并确保它与 vds_solver.dll 文件在同一个目录中。

import socket
import json
import time
import threading
import argparse
import ctypes
import os
import random
import binascii
from pylibscrypt import scrypt

# --- 全局配置 ---
# Ctypes DLL 加载和函数签名定义
try:
    solver_dll = ctypes.CDLL('./vds_solver.dll')
    solve_equihash_96_5 = solver_dll.solve_equihash_96_5
    solve_equihash_96_5.argtypes = [
        ctypes.c_char_p,  # header_input (180 bytes)
        ctypes.c_char_p,  # nonce_input (32 bytes)
        ctypes.c_char_p   # solution_output (69 bytes buffer)
    ]
    solve_equihash_96_5.restype = ctypes.c_bool
except Exception as e:
    print(f"[-] 错误:无法加载或配置 vds_solver.dll: {e}")
    print("[-] 请确保 'vds_solver.dll' 文件存在于脚本相同目录下,并且已正确编译。")
    exit(1)

# --- Stratum 客户端类 ---
class StratumClient:
    def __init__(self, host, port, user, password):
        self.host = host
        self.port = port
        self.user = user
        self.password = password

        self.socket = None
        self.reader = None
        self.writer = None
        self.request_id = 1

        self.subscribed = False
        self.authorized = False
        self.nonce1 = ""
        self.nonce2_len = 0
        self.target = ""
        self.current_job = None

        self.job_lock = threading.Lock()
        self.submit_lock = threading.Lock()

        # 统计信息
        self.accepted_shares = 0
        self.rejected_shares = 0
        self.total_hashes = 0
        self.start_time = time.time()

    def connect(self):
        """连接到矿池并开始监听"""
        try:
            print(f"[*] 正在连接到 {self.host}:{self.port}")
            self.socket = socket.create_connection((self.host, self.port), timeout=10)
            self.reader = self.socket.makefile('r')
            print("[+] 连接成功")

            self.subscribe()
            self.authorize()

            listen_thread = threading.Thread(target=self._listen_loop, daemon=True)
            listen_thread.start()
        except (socket.error, OSError) as e:
            print(f"[-] 连接失败: {e}")
            return False
        return True

    def _send_request(self, method, params):
        """发送JSON-RPC请求"""
        payload = {
            "id": self.request_id,
            "method": method,
            "params": params
        }
        print(f"[>] 发送: {json.dumps(payload)}")
        try:
            self.socket.sendall((json.dumps(payload) + '\n').encode('utf-8'))
            self.request_id += 1
        except Exception as e:
            print(f"[-] 发送请求时出错: {e}")
            self.disconnect()

    def subscribe(self):
        self._send_request("mining.subscribe", ["VDS-python-miner/1.0", None, self.host, self.port])

    def authorize(self):
        self._send_request("mining.authorize", [self.user, self.password])

    def submit(self, job_id, ntime, nonce2_hex, solution_hex):
        with self.submit_lock:
            params = [self.user, job_id, ntime, nonce2_hex, solution_hex]
            self._send_request("mining.submit", params)

    def _listen_loop(self):
        """循环监听来自矿池的消息"""
        while self.socket:
            try:
                line = self.reader.readline()
                if not line:
                    print("[-] 矿池断开连接")
                    self.disconnect()
                    break

                message = json.loads(line)
                print(f"[<] 收到: {json.dumps(message)}")
                self._handle_message(message)

            except (json.JSONDecodeError, UnicodeDecodeError):
                print(f"[-] 无法解析矿池消息: {line.strip()}")
            except Exception as e:
                print(f"[-] 监听线程出错: {e}")
                self.disconnect()
                break

    def _handle_message(self, message):
        """处理不同类型的矿池消息"""
        msg_id = message.get('id')
        method = message.get('method')

        if msg_id == 1 and 'result' in message: # mining.subscribe 响应
            result = message['result']
            self.nonce1 = result[1]
            nonce1_bytes = binascii.unhexlify(self.nonce1)
            self.nonce2_len = 32 - len(nonce1_bytes)
            self.subscribed = True
            print(f"[+] 订阅成功!Nonce1: {self.nonce1}, Nonce2 长度: {self.nonce2_len}")

        elif msg_id == 2 and 'result' in message: # mining.authorize 响应
            if message['result']:
                self.authorized = True
                print("[+] 授权成功!矿机已准备就绪。")
            else:
                print(f"[-] 授权失败: {message.get('error')}")
                self.disconnect()

        elif msg_id and 'result' in message: # mining.submit 响应
            if message['result']:
                self.accepted_shares += 1
                print(f"[!] Share accepted! (Accepted: {self.accepted_shares}, Rejected: {self.rejected_shares})")
            else:
                self.rejected_shares += 1
                print(f"[!] Share REJECTED! Reason: {message.get('error')} (Accepted: {self.accepted_shares}, Rejected: {self.rejected_shares})")

        elif method == 'mining.set_target':
            self.target = message['params'][0]
            print(f"[+] 新目标难度已设置: {self.target}")

        elif method == 'mining.notify':
            with self.job_lock:
                self.current_job = message['params']
            print("[+] 收到新任务 (Job)!")

    def get_job(self):
        """线程安全地获取当前任务"""
        with self.job_lock:
            if not self.current_job:
                return None
            return list(self.current_job) # 返回副本以避免竞争条件

    def get_work_params(self):
        """获取挖矿所需的所有参数"""
        with self.job_lock:
            if not self.current_job or not self.subscribed or not self.target:
                return None
            return {
                "job": list(self.current_job),
                "nonce1": self.nonce1,
                "nonce2_len": self.nonce2_len,
                "target": self.target
            }

    def disconnect(self):
        """关闭连接"""
        if self.socket:
            self.socket.close()
            self.socket = None
            self.reader.close()
            print("[-] 连接已关闭")

    def report_hashrate(self, hashes):
        self.total_hashes += hashes

# --- 挖矿工作线程 ---
class MinerThread(threading.Thread):
    def __init__(self, thread_id, stratum_client):
        super().__init__(daemon=True)
        self.thread_id = thread_id
        self.client = stratum_client
        self.hashes = 0

    def run(self):
        print(f"[Thread-{self.thread_id}] 开始挖矿...")
        while True:
            work_params = self.client.get_work_params()
            if not work_params:
                time.sleep(1)
                continue

            job = work_params['job']
            nonce1_hex = work_params['nonce1']
            nonce2_len = work_params['nonce2_len']
            target_hex = work_params['target']

            job_id, version, prev_hash, merkle_root, hash_final_sapling_root, nvibpool, ntime, nbits, state_root, utxo_root, _ = job

            # 1. 构建区块头 (不含nonce和solution)
            # 按照 VDS 协议,需要将某些字段从小端序转换为字节
            header_prefix = (
                bytes.fromhex(version)[::-1] +
                bytes.fromhex(prev_hash) +
                bytes.fromhex(merkle_root) +
                bytes.fromhex(hash_final_sapling_root) +
                bytes.fromhex(nvibpool) +
                bytes.fromhex(ntime)[::-1] +
                bytes.fromhex(nbits)[::-1] +
                bytes.fromhex(state_root) +
                bytes.fromhex(utxo_root)
            ) # 总长度应为 180 字节

            nonce1_bytes = bytes.fromhex(nonce1_hex)

            # 2. 循环尝试不同的 nonce2
            # 为避免多线程冲突,每个线程从一个随机点开始
            nonce2_int = random.randint(0, 2**(nonce2_len * 8) - 1)

            # 创建Ctypes所需的缓冲区
            solution_buffer = ctypes.create_string_buffer(69)

            while True:
                # 检查是否有新任务
                if self.client.current_job[0] != job_id:
                    break

                nonce2_bytes = nonce2_int.to_bytes(nonce2_len, 'big')
                full_nonce = nonce1_bytes + nonce2_bytes

                # 3. 调用C++求解器
                found = solve_equihash_96_5(header_prefix, full_nonce, solution_buffer)
                self.hashes += 1

                if found:
                    solution_hex = binascii.hexlify(solution_buffer.raw).decode('utf-8')
                    print(f"[Thread-{self.thread_id}] [+] 找到一个 Equihash 解!正在验证难度...")

                    # 4. 验证PoW (Scrypt)
                    header_with_solution = header_prefix + full_nonce + bytes.fromhex(solution_hex)

                    # VDS 使用 scrypt(1024, 1, 1)
                    pow_hash = scrypt(header_with_solution, salt=header_with_solution, N=1024, r=1, p=1, buflen=32)
                    pow_hash_hex = binascii.hexlify(pow_hash).decode('utf-8')

                    if int(pow_hash_hex, 16) < int(target_hex, 16):
                        print(f"[Thread-{self.thread_id}] [!!!] PoW 验证通过!提交 Share!")
                        print(f"    - PoW Hash: {pow_hash_hex}")
                        print(f"    - Target:   {target_hex}")

                        nonce2_hex = binascii.hexlify(nonce2_bytes).decode('utf-8')
                        self.client.submit(job_id, ntime, nonce2_hex, solution_hex)
                    else:
                         print(f"[Thread-{self.thread_id}] [-] 解的哈希值未达到目标难度,继续搜索...")

                nonce2_int = (nonce2_int + 1) % (2**(nonce2_len * 8)) # 循环nonce2

                # 将算力报告给主客户端
                if self.hashes % 10 == 0:
                    self.client.report_hashrate(10)
                    self.hashes = 0


# --- 主程序入口 ---
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Equihash (96,5) VDS 挖矿客户端")
    parser.add_argument('-o', '--url', required=True, help="矿池地址 (例如: vds.uupool.cn)")
    parser.add_argument('-p', '--port', required=True, type=int, help="矿池端口 (例如: 13032)")
    parser.add_argument('-u', '--user', required=True, help="钱包地址.矿工名 (例如: VccHuzptstLQ5wfefJMW5z21v9owbGeJ2pz.worker1)")
    parser.add_argument('-x', '--pass', default='z', help="矿池密码 (默认为 'z')")
    parser.add_argument('-t', '--threads', type=int, default=None, help="挖矿线程数 (默认为CPU核心数)")

    args = parser.parse_args()

    if not args.threads:
        args.threads = os.cpu_count()
        print(f"[*] 未指定线程数,自动设置为CPU核心数: {args.threads}")

    client = StratumClient(args.url, args.port, args.user, args.pass)
    if not client.connect():
        exit(1)

    # 等待授权完成
    while not client.authorized:
        print("[*] 等待矿池授权...")
        time.sleep(2)
        if not client.socket: # 如果连接在授权前断开
            print("[-] 连接在授权完成前丢失,退出。")
            exit(1)

    print(f"[*] 启动 {args.threads} 个挖矿线程...")
    miner_threads = []
    for i in range(args.threads):
        thread = MinerThread(i + 1, client)
        thread.start()
        miner_threads.append(thread)

    try:
        while True:
            time.sleep(30)
            duration = time.time() - client.start_time
            hashrate = client.total_hashes / duration if duration > 0 else 0
            print("\n--- 状态报告 ---")
            print(f" 算力: {hashrate:.2f} Sol/s")
            print(f" 接受/拒绝: {client.accepted_shares}/{client.rejected_shares}")
            print(f" 运行时间: {int(duration // 3600)}h {int((duration % 3600) // 60)}m {int(duration % 60)}s")
            print("------------------\n")

    except KeyboardInterrupt:
        print("\n[!] 检测到 Ctrl+C,正在关闭矿机...")
        client.disconnect()
        print("[+] 矿机已安全关闭。")


第三步:如何运行

  1. 准备文件: 确保 vds_solver.dllvds_miner.py 在同一个文件夹中。
  2. 安装依赖: pip install pylibscrypt
  3. 启动挖矿: 打开命令行(CMD或PowerShell),进入该文件夹,然后运行以下命令。请务必将 -o, -p, 和 -u 参数替换为您自己的信息

    python vds_miner.py -o vds.uupool.cn -p 13032 -u VccHuzptstLQ5wfefJMW5z21v9owbGeJ2pz.myworker -t 4
    
    • -o vds.uupool.cn: 矿池地址。
    • -p 13032: 矿池端口。
    • -u VccHuzptstLQ5wfefJMW5z21v9owbGeJ2pz.myworker: 您的VDS钱包地址,后面跟一个点 . 和矿工名。
    • -t 4: 使用4个CPU线程进行挖矿。您可以根据您的CPU性能进行调整。如果省略,它将自动使用您所有的CPU核心。

代码解释和注意事项

  • 性能: 这个客户端的性能瓶颈将是C++求解器。g++ 编译时使用的 -O3 优化标志对于提升性能至关重要。
  • 字节序 (Endianness): Stratum协议中的一些字段(如 version, ntime, nbits)是小端序的。在Python中,bytes.fromhex(hex_str)[::-1] 是一种简单的方法来反转字节序。
  • 线程安全: threading.Lock 被用来保护对共享数据(如 current_job)的访问,防止多个挖矿线程和网络线程之间发生数据冲突。
  • Nonce: nonce (32字节) 被分为两部分。nonce1 由矿池提供,nonce2 由矿机本地生成和递增。每个线程从一个随机的 nonce2 开始,以减少工作重复。
  • PoW验证: 找到一个有效的Equihash解只是第一步。这个解必须使得整个区块头(包含解本身)的Scrypt哈希值低于矿池下发的目标值。这一步是在Python中完成的,因为它的计算量远小于Equihash求解。
  • 错误处理: 代码包含基本的网络错误和连接断开处理,但在生产环境中可能需要更健壮的重连逻辑。
  • Windows特定: __declspec(dllexport).dll 是Windows平台的特性。如果在Linux上运行,您需要将 __declspec(dllexport) 移除,并将编译命令改为 g++ -shared -fPIC -o vds_solver.so ...,同时在Python中加载 .so 文件。

评论

还没有评论,快来抢沙发吧!

想要发表评论?登录注册