Stratumv1采矿协议Skill stratum-v1

Stratum v1 是采矿池与采矿硬件之间通信的标准协议,基于 JSON-RPC 2.0,用于实现挖矿基础设施、调试通信和验证共享。适用于构建 BSV 采矿池服务器、采矿代理软件等场景。关键词:Stratum v1、采矿协议、JSON-RPC、挖矿池、矿机通信、BSV 挖矿、区块链节点运维。

节点运维 0 次安装 0 次浏览 更新于 3/15/2026

name: stratum-v1 description: 当用户请求“实现 Stratum v1”、“采矿池协议”、“JSON-RPC 挖矿”、“池-矿工通信”、“mining.subscribe”或需要构建 Stratum v1 挖矿基础设施时,应使用此技能。

Stratum v1 采矿协议

Stratum v1 是采矿池与采矿硬件(如 ASIC)之间通信的标准协议。它使用基于 TCP 的 JSON-RPC 2.0,消息以换行符分隔。

何时使用

  • 实现 BSV 采矿池服务器
  • 构建采矿代理软件
  • 创建 ASIC 固件/软件
  • 调试矿工-池通信
  • 理解池共享验证

协议概述

传输层

  • 纯 TCP 套接字连接
  • JSON-RPC 消息以换行符 ( ) 终止
  • 持久连接(非 HTTP 请求/响应)
  • 可选 TLS 加密在单独端口

消息格式

请求:

{"id": 1, "method": "mining.subscribe", "params": ["UserAgent/1.0"]}

响应:

{"id": 1, "result": [...], "error": null}

通知(无需响应):

{"id": null, "method": "mining.notify", "params": [...]}

核心方法

1. mining.subscribe

矿工与池的初始握手。

请求:

{
  "id": 1,
  "method": "mining.subscribe",
  "params": ["UserAgent/1.0.0"]
}

响应:

{
  "id": 1,
  "result": [
    [["mining.set_difficulty", "subscription_id"], ["mining.notify", "subscription_id"]],
    "extranonce1",
    4
  ],
  "error": null
}

响应字段:

  • result[0]: 订阅元组数组 [method, subscription_id]
  • result[1]: Extranonce1(十六进制字符串,通常 8 个字符/4 字节)
  • result[2]: Extranonce2 大小(以字节为单位,通常 4)

2. mining.authorize

向池认证一个工作者。

请求:

{
  "id": 2,
  "method": "mining.authorize",
  "params": ["ADDRESS.workerName", "password"]
}

对于像 GorillaPool 这样的 BSV 池,用户名格式为 BSV_ADDRESS.workerName,其中:

  • BSV_ADDRESS 是有效的 BSV 地址(在连接时验证)
  • workerName 是采矿设备的可选标识符

响应:

{"id": 2, "result": true, "error": null}

3. mining.set_difficulty

服务器通知调整共享难度。

通知:

{
  "id": null,
  "method": "mining.set_difficulty",
  "params": [65536]
}

难度值表示池将接受的最小共享难度。低于此难度的共享将被拒绝。

4. mining.notify

服务器向矿工发送新作业。

通知:

{
  "id": null,
  "method": "mining.notify",
  "params": [
    "job_id",
    "prevhash",
    "coinb1",
    "coinb2",
    ["merkle_branch_1", "merkle_branch_2"],
    "version",
    "nbits",
    "ntime",
    true
  ]
}

参数:

索引 名称 描述
0 job_id 唯一作业标识符(8 字符十六进制)
1 prevhash 前一个区块哈希(字反转十六进制)
2 coinb1 币基交易的第一部分
3 coinb2 币基交易的第二部分
4 merkle_branch 默克尔树哈希数组
5 version 区块版本(大端十六进制)
6 nbits 编码的网络难度目标
7 ntime 区块时间戳(大端十六进制)
8 clean_jobs 如果为 true,则丢弃之前的作业

5. mining.submit

矿工提交共享(潜在的区块解决方案)。

请求:

{
  "id": 3,
  "method": "mining.submit",
  "params": [
    "ADDRESS.workerName",
    "job_id",
    "extranonce2",
    "ntime",
    "nonce",
    "version_bits"
  ]
}

参数:

索引 名称 描述
0 worker 工作者名称(ADDRESS.worker)
1 job_id 来自 mining.notify 的作业 ID
2 extranonce2 矿工的 extranonce2(十六进制,长度 = extranonce2_size * 2)
3 ntime 区块时间戳(8 字符十六进制)
4 nonce 32 位随机数(8 字符十六进制)
5 version_bits 版本滚动位(可选,8 字符十六进制)

响应:

{"id": 3, "result": true, "error": null}

6. mining.configure

扩展协商(BIP310 风格)。

请求:

{
  "id": 4,
  "method": "mining.configure",
  "params": [
    ["version-rolling", "minimum-difficulty"],
    {"version-rolling.mask": "1fffe000", "minimum-difficulty.value": 2048}
  ]
}

响应:

{
  "id": 4,
  "result": [true, {
    "version-rolling": true,
    "version-rolling.mask": "1fffe000",
    "minimum-difficulty": true
  }],
  "error": null
}

字节顺序参考(关键)

字节顺序是 Stratum 实现中 #1 的 bug 来源。本节记录每个阶段的精确字节顺序。

术语

  • BE(大端):最高有效字节在前(人类可读,“自然”顺序)
  • LE(小端):最低有效字节在前(比特币内部格式)
  • 字节反转:简单反转所有字节
  • 字反转:反转 4 字节块,然后反转整个内容(Stratum 特定)

mining.notify 字段字节顺序

字段 Stratum JSON 十六进制 转换为头部的变换
prevhash 字反转 使用单独的字节反转版本
version BE 十六进制字符串 反转为 LE 字节
nbits BE 十六进制字符串 反转为 LE 字节
ntime BE 十六进制字符串 反转为 LE 字节
merkle_branch[] LE(从节点字节反转) 直接使用
coinb1, coinb2 原始交易字节 直接使用

Prevhash 变换(最复杂)

Prevhash 经历两种不同的变换:

从节点(getminingcandidate):

原始: 000000000000000001a2b3c4d5e6f7...  (BE, 64 十六进制字符)

对于 Stratum 协议(mining.notify):

// 字反转: 分割成 8 个 4 字节字,反转每个字,然后反转字顺序
// 这是矿工在 mining.notify params[1] 中接收到的
stratumPrevhash := wordReverse(original)

func wordReverse(hash string) string {
    // 解码为字节
    bytes, _ := hex.DecodeString(hash)  // 32 字节

    // 分割成 8 个 4 字节字
    words := make([][]byte, 8)
    for i := 0; i < 8; i++ {
        words[i] = bytes[i*4 : (i+1)*4]
    }

    // 反转每个字
    for i := range words {
        reverse(words[i])
    }

    // 反转字顺序
    reverseSlice(words)

    // 连接回
    return hex.EncodeToString(flatten(words))
}

对于区块头部构建:

// 简单字节反转(非字反转)
// 这进入实际的 80 字节区块头部
headerPrevhash := reverseBytes(original)

示例:

节点返回:      00000000000000000452b3f2a1c4d5e6f7890abcdef1234567890abcdef12345
Stratum 发送:     e6d5c4a1f2b35204000000000000000045123fcdab0987654321fedcab0987...
头部使用:       4523f1cdab0987654321fedcab0987f6e5d4c1a2f3b25400000000000000...

版本、位、时间、随机数

在 Stratum JSON(mining.notify)中:

版本: "20000000"  <- BE 十六进制, 4 字节
位:   "1d00ffff"  <- BE 十六进制, 4 字节
时间:   "5f4a3b2c"  <- BE 十六进制, 4 字节

在区块头部(80 字节)中:

所有字段存储为 LE 字节

版本 "20000000" -> 字节 [0x00, 0x00, 0x00, 0x20]  (反转)
位   "1d00ffff" -> 字节 [0xff, 0xff, 0x00, 0x1d]  (反转)
时间   "5f4a3b2c" -> 字节 [0x2c, 0x3b, 0x4a, 0x5f]  (反转)
随机数   "12345678" -> 字节 [0x78, 0x56, 0x34, 0x12]  (反转)

Go 代码:

// Stratum 十六进制 -> 头部字节
func stratumHexToHeaderBytes(hexStr string) []byte {
    bytes, _ := hex.DecodeString(hexStr)  // 解码 BE 十六进制
    reverseInPlace(bytes)                  // 转换为 LE
    return bytes
}

默克尔分支字节顺序

从节点(getminingcandidate.merkleProof):

节点返回哈希值在 BE(自然)顺序中

对于 Stratum(mining.notify params[4]):

// 池必须在发送前字节反转每个默克尔证明元素
for i, proof := range node.MerkleProof {
    proofBytes, _ := hex.DecodeString(proof)
    reverseInPlace(proofBytes)  // 转换为 LE
    branches[i] = hex.EncodeToString(proofBytes)
}

当应用分支(共享验证)时:

// 分支已经是 LE,直接使用
func applyMerkleBranches(coinbaseHash []byte, branches []string) []byte {
    root := coinbaseHash  // 从 SHA256d 已经是 LE
    for _, branch := range branches {
        branchBytes, _ := hex.DecodeString(branch)  // 已经是 LE
        combined := append(root, branchBytes...)
        root = sha256d(combined)  // 结果是 LE
    }
    return root  // LE, 准备用于头部
}

区块哈希字节顺序

哈希头部后:

headerHash := sha256d(header80bytes)  // 返回 LE 字节

用于显示(区块浏览器、日志):

displayHash := reverseBytes(headerHash)  // 转换为 BE 用于显示
hashString := hex.EncodeToString(displayHash)

用于难度比较:

// 将 LE 哈希转换为 big.Int(SetBytes 期望 BE)
hashBE := reverseBytes(headerHash)
hashInt := new(big.Int).SetBytes(hashBE)

// 与目标比较
isBlock := hashInt.Cmp(networkTarget) <= 0

完整字节顺序流程

┌─────────────────────────────────────────────────────────────────┐
│                    节点(getminingcandidate)                     │
├─────────────────────────────────────────────────────────────────┤
│ prevhash:     BE(64 十六进制字符)                                 │
│ merkleProof:  BE(64 字符十六进制数组)                         │
│ version:      uint32                                            │
│ nBits:        BE 十六进制字符串                                     │
│ time:         uint32                                            │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    池(为 Stratum 变换)                 │
├─────────────────────────────────────────────────────────────────┤
│ prevhash:     为 mining.notify 字反转                    │
│               为头部验证字节反转(存储两者)   │
│ merkleProof:  字节反转每个元素                         │
│ version:      uint32 -> BE 十六进制字符串(8 字符)                 │
│ nBits:        已经是 BE 十六进制                                    │
│ ntime:        uint32 -> BE 十六进制字符串(8 字符)                 │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    STRATUM JSON(mining.notify)                  │
├─────────────────────────────────────────────────────────────────┤
│ params[1] prevhash:  字反转十六进制(64 字符)               │
│ params[4] branches:  LE 十六进制字符串                             │
│ params[5] version:   BE 十六进制(8 字符)                           │
│ params[6] nbits:     BE 十六进制(8 字符)                           │
│ params[7] ntime:     BE 十六进制(8 字符)                           │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    矿工(mining.submit)                         │
├─────────────────────────────────────────────────────────────────┤
│ extranonce2: 十六进制字符串(长度 = extranonce2_size * 2)         │
│ ntime:       BE 十六进制(8 字符) - 可能与作业不同             │
│ nonce:       BE 十六进制(8 字符)                                   │
│ versionBits: BE 十六进制(8 字符) - 可选                        │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    池(共享验证)                       │
├─────────────────────────────────────────────────────────────────┤
│ 1. 币基: concat(coinb1, en1, en2, coinb2) - 原始字节       │
│ 2. cbHash:   SHA256d(coinbase) -> LE 字节                      │
│ 3. 根:     应用 LE 分支 -> LE 字节                      │
│ 4. 头部:   [ver_LE, prev_LE, root_LE, time_LE, bits_LE, nonce_LE] │
│ 5. 哈希:     SHA256d(header) -> LE 字节                        │
│ 6. 显示:    反转哈希为 BE 显示                        │
└─────────────────────────────────────────────────────────────────┘

常见字节顺序错误

  1. 在头部中使用字反转的 prevhash - 必须使用简单字节反转
  2. 未反转版本/时间/位/随机数 - Stratum 发送 BE,头部需要 LE
  3. 重复反转默克尔分支 - 它们已由池预先反转
  4. 错误的哈希比较端序 - big.Int.SetBytes 期望 BE
  5. 显示哈希未反转 - 内部是 LE,显示是 BE

币基构建

币基交易通过连接以下部分构建:

币基 = coinb1 + extranonce1 + extranonce2 + coinb2

其中:

  • coinb1:版本 + 输入计数 + 前输出 + 脚本签名长度 + 脚本签名前缀(高度、时间戳)
  • extranonce1:池分配的每个连接唯一值
  • extranonce2:矿工控制的值,用于扩展随机数空间
  • coinb2:脚本签名后缀 + 序列 + 输出 + 锁定时间

所有币基部分都是原始交易字节 - 无需字节顺序变换。

区块头部构建

80 字节头部结构(所有字段在最终头部中为小端):

偏移  大小  字段       来源                    变换
------  ----  ----------  ------------------------  -------------------------
0       4     版本      mining.notify params[5]   解码 BE 十六进制,反转为 LE
4       32    前哈希    存储字节反转      使用字节反转(非字反转)
36      32    默克尔根  SHA256d 默克尔树    从哈希已经是 LE
68      4     时间      mining.submit params[3]   解码 BE 十六进制,反转为 LE
72      4     位        mining.notify params[6]   解码 BE 十六进制,反转为 LE
76      4     随机数    mining.submit params[4]   解码 BE 十六进制,反转为 LE
------  ----
        80 字节总计

Go 实现:

func buildHeader(job *Job, ntime, nonce string, versionMask *uint32, versionBits string) []byte {
    header := make([]byte, 80)

    // 版本: BE 十六进制 -> LE 字节
    version, _ := hex.DecodeString(job.VersionHex)
    reverseInPlace(version)
    copy(header[0:4], version)

    // 前哈希: 使用预计算的字节反转(非 Stratum 格式的字反转)
    prev, _ := hex.DecodeString(job.PrevHashForHeader)
    copy(header[4:36], prev)

    // 默克尔根: 从 ApplyMerkleBranches 已经是 LE
    copy(header[36:68], merkleRoot)

    // 时间: BE 十六进制 -> LE 字节
    time, _ := hex.DecodeString(ntime)
    reverseInPlace(time)
    copy(header[68:72], time)

    // 位: BE 十六进制 -> LE 字节
    bits, _ := hex.DecodeString(job.BitsHex)
    reverseInPlace(bits)
    copy(header[72:76], bits)

    // 随机数: BE 十六进制 -> LE 字节
    nonceBytes, _ := hex.DecodeString(nonce)
    reverseInPlace(nonceBytes)
    copy(header[76:80], nonceBytes)

    return header
}

共享验证

// 共享验证的伪代码
func validateShare(job, extranonce1, extranonce2, ntime, nonce, versionBits) bool {
    // 1. 构建币基
    coinbase := job.Coinb1 + extranonce1 + extranonce2 + job.Coinb2

    // 2. 哈希币基
    coinbaseHash := SHA256d(coinbase)

    // 3. 计算默克尔根
    merkleRoot := applyMerkleBranches(coinbaseHash, job.Branches)

    // 4. 构建 80 字节头部
    header := buildHeader(job.Version, job.PrevHash, merkleRoot, ntime, job.Bits, nonce)

    // 5. 如果启用,应用版本滚动
    if versionBits != "" {
        header.version = (header.version & ~mask) | (versionBits & mask)
    }

    // 6. 哈希头部
    blockHash := SHA256d(header)

    // 7. 计算共享难度
    shareDiff := diff1Target / hashToInt(blockHash)

    // 8. 检查与 Stratum 难度的比较
    return shareDiff >= session.difficulty
}

可变难度(VarDiff)

VarDiff 动态调整共享难度以维持目标共享率。

配置:

{
  "varDiff": {
    "minDiff": 512,
    "maxDiff": 1000000000,
    "targetTime": 15,
    "retargetTime": 90,
    "variancePercent": 30,
    "maxDelta": 500
  }
}

算法:

  1. 在循环缓冲区中跟踪共享之间的时间
  2. retargetTime 秒,计算平均共享时间
  3. 如果平均在 targetTime +/- variancePercent 之外,调整:
    新难度 = 当前难度 * 目标时间 / 平均时间
    
  4. 应用 maxDelta 限制并钳制到 [minDiff, maxDiff]

版本滚动(BIP310)

允许矿工使用版本字段中的位作为额外的随机数空间。

掩码: 0x1fffe000(位 13-28,16 位 = 65536x 随机数空间)

协议流程:

  1. 矿工发送带有 version-rolling 扩展的 mining.configure
  2. 池响应允许的掩码
  3. 矿工在 mining.submit 中包含 version_bits 参数
  4. 池验证:(version_bits & ~mask) == 0

错误码

消息 描述
20 其他/未知 通用错误
21 作业未找到 无效 job_id
22 重复共享 共享已提交
23 低难度 共享低于目标
24 未授权 工作者未授权
25 未订阅 未调用 mining.subscribe

实现示例(Go)

参考 GorillaNode 的实现:

  • backend/internal/services/stratum/server.go - Stratum 服务器
  • backend/internal/services/stratum/templates/gbt.go - 作业构建
  • backend/internal/services/vardiff/manager.go - VarDiff 逻辑

关键模式:

// 会话处理
type session struct {
    conn            net.Conn
    extranonce1     string      // 每个连接唯一
    extranonce2Size int         // 通常 4 字节
    difficulty      float64     // 当前共享难度
    authorized      bool        // mining.authorize 是否成功
    submits         map[string]struct{} // 重复检测
}

// 作业管理
type Job struct {
    Id       string   // 短 8 字符十六进制 ID
    Height   int64
    Coinb1   string
    Coinb2   string
    Branches []string
    // ... 区块头部字段
}

测试

使用 netcat 连接:

nc pool.example.com 3333
{"id":1,"method":"mining.subscribe","params":["test/1.0"]}
{"id":2,"method":"mining.authorize","params":["1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa.worker1",""]}

工具:

  • cpuminer-multi - 用于测试的 CPU 矿工
  • cgminer / bfgminer - 功能完整的矿工
  • 带有 Stratum 解析器的 Wireshark

参考文献