#601

Global Rank · of 601 Skills

stratum-v1 AI Agent Skill

View Source: b-open-io/bsv-skills

Medium

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_ADDRESS is a valid BSV address (validated at connection)
  • workerName is 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

  1. Using word-reversed prevhash in header - Must use simple byte-reversed
  2. Not reversing version/time/bits/nonce - Stratum sends BE, header needs LE
  3. Reversing merkle branches twice - They're pre-reversed by pool
  4. Wrong hash comparison endianness - big.Int.SetBytes expects BE
  5. 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 connection
  • extranonce2: Miner-controlled value for nonce space expansion
  • coinb2: 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:

  1. Track time between shares in circular buffer
  2. Every retargetTime seconds, calculate average share time
  3. If average outside targetTime +/- variancePercent, adjust:
    newDiff = currentDiff * targetTime / averageTime
  4. Apply maxDelta limit 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:

  1. Miner sends mining.configure with version-rolling extension
  2. Pool responds with allowed mask
  3. Miner includes version_bits parameter in mining.submit
  4. 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 server
  • backend/internal/services/stratum/templates/gbt.go - Job construction
  • backend/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 testing
  • cgminer / bfgminer - Full-featured miners
  • Wireshark with Stratum dissector

References

Installs

Installs 20
Global Rank #601 of 601

Security Audit

ath Safe
socket Safe
Alerts: 0 Score: 90
snyk Medium
EU EU-Hosted Inference API

Power your AI Agents with the best open-source models.

Drop-in OpenAI-compatible API. No data leaves Europe.

Explore Inference API

GLM

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

1

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.

2

No configuration needed. Your AI agent (Claude Code, Cursor, Windsurf, etc.) automatically detects installed skills and uses them as context when generating code.

3

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.

Data sourced from the skills.sh registry and GitHub. Install counts and security audits are updated regularly.

EU Made in Europe

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.

Get the App:

Customer Support