Installation
npx skills add b-open-io/bsv-skills --skill stratum-v1 20
Installs
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 bytesIn 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) orderFor 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 bytesFor 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) <= 0Complete 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 + coinb2Where:
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 totalGo 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
References
Installs
Security Audit
View Source
b-open-io/bsv-skills
More from this source
Power your AI Agents with
the best open-source models.
Drop-in OpenAI-compatible API. No data leaves Europe.
Explore Inference APIGLM
GLM 5
$1.00 / $3.20
per M tokens
Kimi
Kimi K2.5
$0.60 / $2.80
per M tokens
MiniMax
MiniMax M2.5
$0.30 / $1.20
per M tokens
Qwen
Qwen3.5 122B
$0.40 / $3.00
per M tokens
How to use this skill
Install stratum-v1 by running npx skills add b-open-io/bsv-skills --skill stratum-v1 in your project directory. Run the install command above in your project directory. The skill file will be downloaded from GitHub and placed in your project.
No configuration needed. Your AI agent (Claude Code, Cursor, Windsurf, etc.) automatically detects installed skills and uses them as context when generating code.
The skill enhances your agent's understanding of stratum-v1, helping it follow established patterns, avoid common mistakes, and produce production-ready output.
What you get
Skills are plain-text instruction files — not executable code. They encode expert knowledge about frameworks, languages, or tools that your AI agent reads to improve its output. This means zero runtime overhead, no dependency conflicts, and full transparency: you can read and review every instruction before installing.
Compatibility
This skill works with any AI coding agent that supports the skills.sh format, including Claude Code (Anthropic), Cursor, Windsurf, Cline, Aider, and other tools that read project-level context files. Skills are framework-agnostic at the transport level — the content inside determines which language or framework it applies to.
Chat with 100+ AI Models in one App.
Use Claude, ChatGPT, Gemini alongside with EU-Hosted Models like Deepseek, GLM-5, Kimi K2.5 and many more.