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 显示 │
└─────────────────────────────────────────────────────────────────┘
常见字节顺序错误
- 在头部中使用字反转的 prevhash - 必须使用简单字节反转
- 未反转版本/时间/位/随机数 - Stratum 发送 BE,头部需要 LE
- 重复反转默克尔分支 - 它们已由池预先反转
- 错误的哈希比较端序 - big.Int.SetBytes 期望 BE
- 显示哈希未反转 - 内部是 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
}
}
算法:
- 在循环缓冲区中跟踪共享之间的时间
- 每
retargetTime秒,计算平均共享时间 - 如果平均在
targetTime +/- variancePercent之外,调整:新难度 = 当前难度 * 目标时间 / 平均时间 - 应用
maxDelta限制并钳制到[minDiff, maxDiff]
版本滚动(BIP310)
允许矿工使用版本字段中的位作为额外的随机数空间。
掩码: 0x1fffe000(位 13-28,16 位 = 65536x 随机数空间)
协议流程:
- 矿工发送带有
version-rolling扩展的mining.configure - 池响应允许的掩码
- 矿工在
mining.submit中包含version_bits参数 - 池验证:
(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