stratum-v1
npx skills add https://github.com/b-open-io/bsv-skills --skill stratum-v1
Agent 安装分布
Skill 文档
Stratum v1 Mining Protocol
Stratum v1 is the standard protocol for communication between mining pools and mining hardware (ASICs). It uses JSON-RPC 2.0 over TCP with newline-delimited messages.
When to Use
- Implementing a BSV mining pool server
- Building mining proxy software
- Creating ASIC firmware/software
- Debugging miner-pool communication
- Understanding pool share validation
Protocol Overview
Transport Layer
- Plain TCP socket connection
- JSON-RPC messages terminated by newline (
\n) - Persistent connection (not HTTP request/response)
- Optional TLS encryption on separate port
Message Format
Request:
{"id": 1, "method": "mining.subscribe", "params": ["UserAgent/1.0"]}
Response:
{"id": 1, "result": [...], "error": null}
Notification (no response expected):
{"id": null, "method": "mining.notify", "params": [...]}
Core Methods
1. mining.subscribe
Initial handshake from miner to pool.
Request:
{
"id": 1,
"method": "mining.subscribe",
"params": ["UserAgent/1.0.0"]
}
Response:
{
"id": 1,
"result": [
[["mining.set_difficulty", "subscription_id"], ["mining.notify", "subscription_id"]],
"extranonce1",
4
],
"error": null
}
Response fields:
result[0]: Array of subscription tuples[method, subscription_id]result[1]: Extranonce1 (hex string, typically 8 chars/4 bytes)result[2]: Extranonce2 size in bytes (typically 4)
2. mining.authorize
Authenticate a worker with the pool.
Request:
{
"id": 2,
"method": "mining.authorize",
"params": ["ADDRESS.workerName", "password"]
}
For BSV pools like GorillaPool, the username format is BSV_ADDRESS.workerName where:
BSV_ADDRESSis a valid BSV address (validated at connection)workerNameis an optional identifier for the specific mining device
Response:
{"id": 2, "result": true, "error": null}
3. mining.set_difficulty
Server notification to adjust share difficulty.
Notification:
{
"id": null,
"method": "mining.set_difficulty",
"params": [65536]
}
The difficulty value represents the minimum share difficulty the pool will accept. Shares below this difficulty are rejected.
4. mining.notify
Server sends a new job to miners.
Notification:
{
"id": null,
"method": "mining.notify",
"params": [
"job_id",
"prevhash",
"coinb1",
"coinb2",
["merkle_branch_1", "merkle_branch_2"],
"version",
"nbits",
"ntime",
true
]
}
Parameters:
| Index | Name | Description |
|---|---|---|
| 0 | job_id | Unique job identifier (8-char hex) |
| 1 | prevhash | Previous block hash (word-reversed hex) |
| 2 | coinb1 | First part of coinbase transaction |
| 3 | coinb2 | Second part of coinbase transaction |
| 4 | merkle_branch | Array of merkle tree hashes |
| 5 | version | Block version (big-endian hex) |
| 6 | nbits | Encoded network difficulty target |
| 7 | ntime | Block timestamp (big-endian hex) |
| 8 | clean_jobs | If true, discard previous jobs |
5. mining.submit
Miner submits a share (potential block solution).
Request:
{
"id": 3,
"method": "mining.submit",
"params": [
"ADDRESS.workerName",
"job_id",
"extranonce2",
"ntime",
"nonce",
"version_bits"
]
}
Parameters:
| Index | Name | Description |
|---|---|---|
| 0 | worker | Worker name (ADDRESS.worker) |
| 1 | job_id | Job ID from mining.notify |
| 2 | extranonce2 | Miner’s extranonce2 (hex, length = extranonce2_size * 2) |
| 3 | ntime | Block timestamp (8-char hex) |
| 4 | nonce | 32-bit nonce (8-char hex) |
| 5 | version_bits | Version rolling bits (optional, 8-char hex) |
Response:
{"id": 3, "result": true, "error": null}
6. mining.configure
Extension negotiation (BIP310-style).
Request:
{
"id": 4,
"method": "mining.configure",
"params": [
["version-rolling", "minimum-difficulty"],
{"version-rolling.mask": "1fffe000", "minimum-difficulty.value": 2048}
]
}
Response:
{
"id": 4,
"result": [true, {
"version-rolling": true,
"version-rolling.mask": "1fffe000",
"minimum-difficulty": true
}],
"error": null
}
Byte Order Reference (CRITICAL)
Byte order is the #1 source of bugs in Stratum implementations. This section documents the exact byte order at each stage.
Terminology
- BE (Big-Endian): Most significant byte first (human-readable, “natural” order)
- LE (Little-Endian): Least significant byte first (Bitcoin internal format)
- Byte-reversed: Simple reversal of all bytes
- Word-reversed: Reverse 4-byte chunks, then reverse the whole thing (Stratum-specific)
mining.notify Field Byte Orders
| Field | Stratum JSON Hex | Transformation for Header |
|---|---|---|
| prevhash | Word-reversed | Use separate byte-reversed version |
| version | BE hex string | Reverse to LE bytes |
| nbits | BE hex string | Reverse to LE bytes |
| ntime | BE hex string | Reverse to LE bytes |
| merkle_branch[] | LE (byte-reversed from node) | Use as-is |
| coinb1, coinb2 | Raw tx bytes | Use as-is |
Prevhash Transformation (Most Complex)
The prevhash undergoes TWO different transformations:
From Node (getminingcandidate):
Original: 000000000000000001a2b3c4d5e6f7... (BE, 64 hex chars)
For Stratum Protocol (mining.notify):
// Word-reverse: split into 8 4-byte words, reverse each word, then reverse word order
// This is what miners receive in mining.notify params[1]
stratumPrevhash := wordReverse(original)
func wordReverse(hash string) string {
// Decode to bytes
bytes, _ := hex.DecodeString(hash) // 32 bytes
// Split into 8 words of 4 bytes each
words := make([][]byte, 8)
for i := 0; i < 8; i++ {
words[i] = bytes[i*4 : (i+1)*4]
}
// Reverse each word
for i := range words {
reverse(words[i])
}
// Reverse word order
reverseSlice(words)
// Concatenate back
return hex.EncodeToString(flatten(words))
}
For Block Header Construction:
// Simple byte-reverse (NOT word-reverse)
// This goes into the actual 80-byte block header
headerPrevhash := reverseBytes(original)
Example:
Node returns: 00000000000000000452b3f2a1c4d5e6f7890abcdef1234567890abcdef12345
Stratum sends: e6d5c4a1f2b35204000000000000000045123fcdab0987654321fedcab0987...
Header uses: 4523f1cdab0987654321fedcab0987f6e5d4c1a2f3b25400000000000000...
Version, Bits, Time, Nonce
In Stratum JSON (mining.notify):
version: "20000000" <- BE hex, 4 bytes
nbits: "1d00ffff" <- BE hex, 4 bytes
ntime: "5f4a3b2c" <- BE hex, 4 bytes
In Block Header (80 bytes):
All fields stored as LE bytes
version "20000000" -> bytes [0x00, 0x00, 0x00, 0x20] (reversed)
nbits "1d00ffff" -> bytes [0xff, 0xff, 0x00, 0x1d] (reversed)
ntime "5f4a3b2c" -> bytes [0x2c, 0x3b, 0x4a, 0x5f] (reversed)
nonce "12345678" -> bytes [0x78, 0x56, 0x34, 0x12] (reversed)
Go code:
// Stratum hex -> header bytes
func stratumHexToHeaderBytes(hexStr string) []byte {
bytes, _ := hex.DecodeString(hexStr) // Decode BE hex
reverseInPlace(bytes) // Convert to LE
return bytes
}
Merkle Branch Byte Order
From Node (getminingcandidate.merkleProof):
Node returns hashes in BE (natural) order
For Stratum (mining.notify params[4]):
// Pool must byte-reverse each merkle proof element before sending
for i, proof := range node.MerkleProof {
proofBytes, _ := hex.DecodeString(proof)
reverseInPlace(proofBytes) // Convert to LE
branches[i] = hex.EncodeToString(proofBytes)
}
When applying branches (share validation):
// Branches are already LE, use directly
func applyMerkleBranches(coinbaseHash []byte, branches []string) []byte {
root := coinbaseHash // Already LE from SHA256d
for _, branch := range branches {
branchBytes, _ := hex.DecodeString(branch) // Already LE
combined := append(root, branchBytes...)
root = sha256d(combined) // Result is LE
}
return root // LE, ready for header
}
Block Hash Byte Order
After hashing header:
headerHash := sha256d(header80bytes) // Returns LE bytes
For display (block explorer, logs):
displayHash := reverseBytes(headerHash) // Convert to BE for display
hashString := hex.EncodeToString(displayHash)
For difficulty comparison:
// Convert LE hash to big.Int (SetBytes expects BE)
hashBE := reverseBytes(headerHash)
hashInt := new(big.Int).SetBytes(hashBE)
// Compare against target
isBlock := hashInt.Cmp(networkTarget) <= 0
Complete Byte Order Flow
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â NODE (getminingcandidate) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â prevhash: BE (64 hex chars) â
â merkleProof: BE (array of 64-char hex) â
â version: uint32 â
â nBits: BE hex string â
â time: uint32 â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â
â¼
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â POOL (transforms for Stratum) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â prevhash: Word-reverse for mining.notify â
â Byte-reverse for header validation (store both) â
â merkleProof: Byte-reverse each element â
â version: uint32 -> BE hex string (8 chars) â
â nBits: Already BE hex â
â ntime: uint32 -> BE hex string (8 chars) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â
â¼
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â STRATUM JSON (mining.notify) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â params[1] prevhash: Word-reversed hex (64 chars) â
â params[4] branches: LE hex strings â
â params[5] version: BE hex (8 chars) â
â params[6] nbits: BE hex (8 chars) â
â params[7] ntime: BE hex (8 chars) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â
â¼
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â MINER (mining.submit) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â extranonce2: Hex string (length = extranonce2_size * 2) â
â ntime: BE hex (8 chars) - may differ from job â
â nonce: BE hex (8 chars) â
â versionBits: BE hex (8 chars) - optional â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â
â¼
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â POOL (share validation) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â 1. Coinbase: concat(coinb1, en1, en2, coinb2) - raw bytes â
â 2. cbHash: SHA256d(coinbase) -> LE bytes â
â 3. Root: Apply LE branches -> LE bytes â
â 4. Header: [ver_LE, prev_LE, root_LE, time_LE, bits_LE, nonce_LE] â
â 5. Hash: SHA256d(header) -> LE bytes â
â 6. Display: Reverse hash for BE display â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Common Byte Order Bugs
- Using word-reversed prevhash in header – Must use simple byte-reversed
- Not reversing version/time/bits/nonce – Stratum sends BE, header needs LE
- Reversing merkle branches twice – They’re pre-reversed by pool
- Wrong hash comparison endianness – big.Int.SetBytes expects BE
- Displaying hash without reversal – Internal is LE, display is BE
Coinbase Construction
The coinbase transaction is built by concatenating:
coinbase = coinb1 + extranonce1 + extranonce2 + coinb2
Where:
coinb1: Version + input count + prevout + scriptSig length + scriptSig prefix (height, timestamp)extranonce1: Pool-assigned unique value per connectionextranonce2: Miner-controlled value for nonce space expansioncoinb2: ScriptSig suffix + sequence + outputs + locktime
All coinbase parts are raw transaction bytes – no byte order transformation needed.
Block Header Construction
80-byte header structure (all fields little-endian in final header):
Offset Size Field Source Transformation
------ ---- ---------- ------------------------ -------------------------
0 4 version mining.notify params[5] Decode BE hex, reverse to LE
4 32 prevhash Store byte-reversed Use byte-reversed (NOT word-reversed)
36 32 merkleroot SHA256d of merkle tree Already LE from hashing
68 4 time mining.submit params[3] Decode BE hex, reverse to LE
72 4 bits mining.notify params[6] Decode BE hex, reverse to LE
76 4 nonce mining.submit params[4] Decode BE hex, reverse to LE
------ ----
80 bytes total
Go implementation:
func buildHeader(job *Job, ntime, nonce string, versionMask *uint32, versionBits string) []byte {
header := make([]byte, 80)
// Version: BE hex -> LE bytes
version, _ := hex.DecodeString(job.VersionHex)
reverseInPlace(version)
copy(header[0:4], version)
// Prevhash: Use pre-computed byte-reversed (NOT the word-reversed Stratum format)
prev, _ := hex.DecodeString(job.PrevHashForHeader)
copy(header[4:36], prev)
// Merkle root: Already LE from ApplyMerkleBranches
copy(header[36:68], merkleRoot)
// Time: BE hex -> LE bytes
time, _ := hex.DecodeString(ntime)
reverseInPlace(time)
copy(header[68:72], time)
// Bits: BE hex -> LE bytes
bits, _ := hex.DecodeString(job.BitsHex)
reverseInPlace(bits)
copy(header[72:76], bits)
// Nonce: BE hex -> LE bytes
nonceBytes, _ := hex.DecodeString(nonce)
reverseInPlace(nonceBytes)
copy(header[76:80], nonceBytes)
return header
}
Share Validation
// Pseudocode for share validation
func validateShare(job, extranonce1, extranonce2, ntime, nonce, versionBits) bool {
// 1. Build coinbase
coinbase := job.Coinb1 + extranonce1 + extranonce2 + job.Coinb2
// 2. Hash coinbase
coinbaseHash := SHA256d(coinbase)
// 3. Calculate merkle root
merkleRoot := applyMerkleBranches(coinbaseHash, job.Branches)
// 4. Build 80-byte header
header := buildHeader(job.Version, job.PrevHash, merkleRoot, ntime, job.Bits, nonce)
// 5. Apply version rolling if enabled
if versionBits != "" {
header.version = (header.version & ~mask) | (versionBits & mask)
}
// 6. Hash header
blockHash := SHA256d(header)
// 7. Calculate share difficulty
shareDiff := diff1Target / hashToInt(blockHash)
// 8. Check against stratum difficulty
return shareDiff >= session.difficulty
}
Variable Difficulty (VarDiff)
VarDiff dynamically adjusts share difficulty to maintain target share rate.
Configuration:
{
"varDiff": {
"minDiff": 512,
"maxDiff": 1000000000,
"targetTime": 15,
"retargetTime": 90,
"variancePercent": 30,
"maxDelta": 500
}
}
Algorithm:
- Track time between shares in circular buffer
- Every
retargetTimeseconds, calculate average share time - If average outside
targetTime +/- variancePercent, adjust:newDiff = currentDiff * targetTime / averageTime - Apply
maxDeltalimit and clamp to[minDiff, maxDiff]
Version Rolling (BIP310)
Allows miners to use bits in the version field as additional nonce space.
Mask: 0x1fffe000 (bits 13-28, 16 bits = 65536x nonce space)
Protocol flow:
- Miner sends
mining.configurewithversion-rollingextension - Pool responds with allowed mask
- Miner includes
version_bitsparameter inmining.submit - Pool validates:
(version_bits & ~mask) == 0
Error Codes
| Code | Message | Description |
|---|---|---|
| 20 | Other/Unknown | Generic error |
| 21 | Job not found | Invalid job_id |
| 22 | Duplicate share | Share already submitted |
| 23 | Low difficulty | Share below target |
| 24 | Unauthorized | Worker not authorized |
| 25 | Not subscribed | mining.subscribe not called |
Implementation Example (Go)
See GorillaNode’s implementation at:
backend/internal/services/stratum/server.go– Stratum serverbackend/internal/services/stratum/templates/gbt.go– Job constructionbackend/internal/services/vardiff/manager.go– VarDiff logic
Key patterns:
// Session handling
type session struct {
conn net.Conn
extranonce1 string // Unique per connection
extranonce2Size int // Typically 4 bytes
difficulty float64 // Current share difficulty
authorized bool // Has mining.authorize succeeded
submits map[string]struct{} // Duplicate detection
}
// Job management
type Job struct {
Id string // Short 8-char hex ID
Height int64
Coinb1 string
Coinb2 string
Branches []string
// ... block header fields
}
Testing
Connect with netcat:
nc pool.example.com 3333
{"id":1,"method":"mining.subscribe","params":["test/1.0"]}
{"id":2,"method":"mining.authorize","params":["1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa.worker1",""]}
Tools:
cpuminer-multi– CPU miner for testingcgminer/bfgminer– Full-featured miners- Wireshark with Stratum dissector