Skip to content

mining.notify and mining.submit: The Core Work Loop

Once the handshake is done — subscribe, configure, authorize — the real work begins. From this point on, your miner’s life becomes a tight loop: receive a job, grind hashes, submit results. Over and over, thousands of times per day.

This loop is powered by two methods: mining.notify (pool tells the miner what to work on) and mining.submit (miner sends back a result). Let’s tear them apart.

mining.notify is a server-initiated message. The pool pushes it to your miner whenever there’s a new job to work on. Your miner doesn’t ask for it — it arrives automatically.

Here’s what a real mining.notify message looks like:

{
"id": null,
"method": "mining.notify",
"params": [
"bf",
"4d16b6f85af6e2198f44ae2a6de67f78487ae5611b32aba40000000000000000",
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2c0342fe0c",
"ffffffff0100f2052a010000001976a914904a49878c0adfc3aa05de7afad2cc15f483a56a88ac00000000",
[
"f0dbfe210e0e2e7ef4a6c8e58b3e4e6e3f3e2e1d1c1b1a191817161514131211"
],
"20000000",
"1903a30c",
"649c3a21",
true
]
}

That’s a lot of hex. Let’s decode every single parameter.

The params array has 9 elements, in this exact order:

1. job_id ("bf") A short string that identifies this specific job. When your miner finds a share, it needs to tell the pool which job it was working on. The pool uses this to look up the corresponding block template.

2. prevhash (64 hex chars) The hash of the previous block in the blockchain. This is what you’re building on top of. When a new block is found on the network, this value changes and the pool sends a new mining.notify with clean_jobs = true.

3. coinbase1 (hex string) The first part of the coinbase transaction. This is everything up to where the extraNonce values go.

4. coinbase2 (hex string) The second part of the coinbase transaction. This is everything after the extraNonce values.

5. merkle_branches (array of hex strings) A list of transaction hashes needed to compute the Merkle root. The pool has already arranged all the block’s transactions into a Merkle tree — these are the “sibling” hashes your miner needs to climb from the coinbase transaction up to the root.

6. version (hex, 4 bytes) The block version field. With version rolling enabled (via mining.configure), your miner can modify certain bits of this field as additional nonce space.

7. nbits (hex, 4 bytes) The encoded target difficulty. This is the compact representation of the 256-bit target value. Your miner uses this to know what difficulty the network requires.

8. ntime (hex, 4 bytes) The current timestamp for the block. Your miner can increment this slightly (rolling ntime) to get more nonce space, but most of the heavy lifting is done by iterating the nonce and extraNonce2.

9. clean_jobs (boolean) The most important flag. When true, it means: stop everything you’re doing and switch to this job immediately. This happens when a new block has been found on the network, making all previous work invalid. When false, the miner can finish its current work before switching.

This is the fascinating part. Your miner receives all those hex strings and must assemble them into an 80-byte block header that it can hash. Here’s the step-by-step process:

coinbase_tx = coinbase1 + extraNonce1 + extraNonce2 + coinbase2
  • extraNonce1 was assigned by the pool during mining.subscribe
  • extraNonce2 is the value your miner iterates (this is the main search space your miner controls)
coinbase_hash = SHA256d(coinbase_tx)

(SHA256d means double SHA-256: hash it once, then hash the result again.)

Starting with coinbase_hash, combine it with each hash in merkle_branches in sequence:

current = coinbase_hash
for branch in merkle_branches:
current = SHA256d(current + branch)
merkle_root = current

Each step concatenates the current hash with the next branch hash and double-SHA256s the result. After processing all branches, you have the Merkle root.

header = version (4 bytes)
+ prevhash (32 bytes)
+ merkle_root (32 bytes)
+ ntime (4 bytes)
+ nbits (4 bytes)
+ nonce (4 bytes)

That’s exactly 80 bytes. This is what your ASIC chips actually hash.

block_hash = SHA256d(header)

If the resulting hash is below the pool’s target (share difficulty), you’ve found a valid share — submit it. If it’s also below the network’s target, congratulations: you just found a block.

The miner iterates through this process billions of times per second, changing the nonce (and extraNonce2 and version bits) each time, looking for a hash that meets the target.

When your miner finds a hash that meets the pool’s share difficulty, it sends a mining.submit message:

{
"id": 4,
"method": "mining.submit",
"params": [
"worker1.rig01",
"bf",
"00000002",
"649c3a25",
"6a909d70"
]
}

1. worker_name ("worker1.rig01") The authorized worker name, so the pool knows which miner submitted this share.

2. job_id ("bf") Must match the job_id from the mining.notify that this share was based on. If the pool no longer has this job (because a new block was found), you’ll get an error 21 (job not found).

3. extranonce2 ("00000002") The extraNonce2 value your miner used when it found this share. The pool combines this with its extraNonce1 and the coinbase parts to reconstruct and verify the coinbase transaction.

4. ntime ("649c3a25") The timestamp value used. This might differ slightly from the original ntime in mining.notify if the miner rolled the timestamp.

5. nonce ("6a909d70") The 4-byte nonce value that produced the valid hash.

If version rolling was negotiated via mining.configure, there’s an additional 6th parameter:

{
"id": 4,
"method": "mining.submit",
"params": [
"worker1.rig01",
"bf",
"00000002",
"649c3a25",
"6a909d70",
"20000000"
]
}

The 6th parameter is the version_bits mask that was applied to the block version field. The pool needs this to reconstruct the exact header your miner hashed.

On success:

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

Your share was accepted. The pool verified that the hash meets the share difficulty target.

On error:

{
"id": 4,
"result": null,
"error": [21, "Job not found", null]
}

This share was rejected. Common reasons are covered in the next article about error handling.

Here’s what happens continuously during mining:

  1. Pool sends mining.notify with a new job
  2. Miner constructs coinbase → Merkle root → block header
  3. ASIC chips iterate nonce at billions of hashes per second
  4. When nonce space is exhausted, miner increments extraNonce2 and rebuilds from step 2
  5. If version rolling is active, version bits are varied in parallel with nonce
  6. When a hash meets the share target → mining.submit
  7. Pool verifies and responds with true or an error
  8. Repeat until a new mining.notify arrives

This loop runs 24/7 for the entire life of your miner. Every share submitted earns you a fraction of a fraction of a Bitcoin, but millions of shares per day add up to your daily mining revenue.