Skip to content

fix(blockassembly): keep a mining candidate's coinbase on its job, so a solution without one can be mined - #2027

Merged
freemans13 merged 10 commits into
bsv-blockchain:mainfrom
freemans13:stu/mining-candidate-keeps-coinbase
Oct 28, 2026
Merged

freemans13 merged 10 commits into
bsv-blockchain:mainfrom
freemans13:stu/mining-candidate-keeps-coinbase

Conversation

@freemans13

Copy link
Copy Markdown
Collaborator

What happened

A submitminingsolution call with no coinbase could never produce a block. Block assembly rebuilt the coinbase on submit with CreateCoinbase, which appends 12 random extranonce bytes, so the merkle root never matched the one the miner hashed. Every such solution failed the proof-of-work check, deleted the job and reset block assembly. icellan found this reviewing #2017.

Three places each built their own coinbase for the same job, and none of them agreed:

  • submit rebuilt one at random;
  • GetCandidateBlock built another;
  • getminingcandidate with coinbase=true built a third in the RPC service, a version 2 pay-to-pubkey transaction that block assembly never saw.

bitcoin-sv keeps the whole candidate block. When a solution omits its coinbase it falls back to the candidate's own (result->GetBlockCoinbase() in src/rpc/mining-fbb.cpp), and a solution's version changes only the header.

Changes

  • The job owns the coinbase. GetMiningCandidate builds the coinbase once and stores it on the job. It also returns it in a new coinbase_tx field on the MiningCandidate proto, so the RPC service can hand out the same bytes.
  • One coinbase per candidate id. Candidate ids repeat. The main path hashes subtree root, parent and time, so polls in the same second share an id. The empty-block path derives its id from the parent alone. Building per poll would overwrite a coinbase an earlier miner was already hashing. So a held job's coinbase is reused when the new candidate pays the same value at the same height, and a mutex makes that read and the job store one step.
  • Submit uses it unaltered. A coinbase-less solution is mined with the job's coinbase. The old copies of the solution's version, time and nonce into the coinbase are gone; they belong in the header, as in bitcoin-sv.
  • The other two readers agree. GetCandidateBlock and getminingcandidate coinbase=true return the same coinbase. The RPC now returns the job's version 1 P2PKH coinbase instead of building a pay-to-pubkey one. If block assembly sends no coinbase, the RPC returns an error rather than inventing one.
  • CHANGELOG: under behaviour changes on upgrade, covering the new proto field, the RPC output shape and the upgrade order (block assembly no later than RPC).

Testing

  • TestSubmitMiningSolution_CoinbaseLessMinesTheIssuedCoinbase issues a candidate over a sqlitememory blockchain store holding a real chain. It mines the header over the issued coinbase with a version override, submits once with no coinbase and no retries, then reads the stored block back. The block carries the issued coinbase byte for byte, and the version is in the header.
  • TestGetCandidateBlock_CarriesTheIssuedCoinbase checks that the served candidate block has the same coinbase.
  • TestStoreJobWithCoinbase_OncePerCandidateID covers three cases: a repeated id reuses the coinbase, 16 concurrent polls of one id all get the held coinbase (run under -race), and a changed payout gets a new coinbase.
  • The RPC test now asserts the returned coinbase equals the candidate's bytes, and that a candidate without one is an error.
  • Mutations: restoring the random rebuild on submit, dropping the reuse, or dropping the mutex each turns a test red. Without the mutex the concurrent case fails on 3 of 3 runs.
  • go test -race passes for services/blockassembly/..., services/rpc/... and model/.... golangci-lint --new-from-rev=upstream/main reports no issues.

Limits and trade-offs

🤖 Generated with Claude Code

https://claude.ai/code/session_0191Q6pyDA2snkaJ2JRGJXLz

freemans13 and others added 3 commits October 22, 2026 11:01
… mine a coinbase-less solution with it

A submitminingsolution call without a coinbase could never be mined. Block
assembly rebuilt the coinbase with CreateCoinbase, which appends 12 random
extranonce bytes, so the merkle root was never the one the miner hashed. The
solution failed the proof-of-work check, the job was deleted and block
assembly was reset. getminingcandidate with coinbase=true built yet another
coinbase in the RPC service that block assembly never saw, and
GetCandidateBlock built a third.

bitcoin-sv keeps the candidate block, coinbase included, and falls back to
that coinbase when a solution omits one; a solution's version goes into the
header only. This does the same:

- GetMiningCandidate builds the coinbase once per candidate id, stores it on
  the job and returns it in a new coinbase_tx field of MiningCandidate. Ids
  repeat across polls, so a held job's coinbase is reused when it pays the
  same value at the same height, and a mutex makes the read and the store one
  step so concurrent polls cannot hand out different coinbases.
- submitMiningSolution mines a coinbase-less solution with the job's
  coinbase, unaltered. The version, time and nonce copies into the coinbase
  are gone.
- GetCandidateBlock and getminingcandidate with coinbase=true return that
  same coinbase.

TestSubmitMiningSolution_CoinbaseLessMinesTheIssuedCoinbase mines over a
sqlitememory blockchain store in one submit and reads the stored block back.
Restoring the rebuild, the reuse or the mutex each turns a test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191Q6pyDA2snkaJ2JRGJXLz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191Q6pyDA2snkaJ2JRGJXLz
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-2027 (673054d)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 169
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.717µ 1.707µ ~ 0.400
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/memory-4 11.93m 11.88m ~ 1.000
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_1-4 12.12m 11.88m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_2-4 12.96m 12.98m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/memo... 27.15m 26.32m ~ 0.400
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 31.43m 28.85m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 31.51m 29.05m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/mem... 123.1m 115.5m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/dis... 202.5m 195.5m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=131072/dis... 206.6m 200.1m ~ 0.100
BlockTxMap/txs=16384/nodeindex-4 2.504m 2.500m ~ 0.200
BlockTxMap/txs=16384/swiss-4 16.71m 14.31m ~ 0.700
BlockTxMap/txs=262144/nodeindex-4 18.11m 18.12m ~ 1.000
BlockTxMap/txs=262144/swiss-4 50.81m 50.55m ~ 0.400
BlockTxMap/txs=4194304/nodeindex-4 447.4m 417.2m ~ 0.700
BlockTxMap/txs=4194304/swiss-4 880.9m 861.7m ~ 0.100
TxMapPut_MemoryVsDisk/txs=100000/memory-4 7.961m 7.848m ~ 0.700
TxMapPut_MemoryVsDisk/txs=100000/disk_1-4 6.636m 7.804m ~ 0.700
TxMapPut_MemoryVsDisk/txs=100000/disk_2-4 6.139m 7.506m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/memory/hit-4 4.674m 5.146m ~ 0.100
TxMapGetFrozen_MemoryVsDisk/txs=100000/memory/miss-4 3.540m 3.587m ~ 1.000
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_1/hit-4 1.975m 2.007m ~ 0.700
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_1/miss-4 1.932m 1.942m ~ 0.700
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_2/hit-4 1.938m 1.985m ~ 0.200
TxMapGetFrozen_MemoryVsDisk/txs=100000/disk_2/miss-4 1.962m 1.990m ~ 0.400
ParentSpendsSetIfNotExists_MemoryVsDisk/inputs=200000/dis... 15.85m 16.47m ~ 0.100
ParentSpendsSetIfNotExists_MemoryVsDisk/inputs=200000/dis... 15.56m 16.01m ~ 0.700
InpointDedup/txs=16K-4 5.051m 4.917m ~ 0.400
InpointDedup/txs=128K-4 19.87m 22.27m ~ 0.200
InpointDedup/txs=1M-4 138.4m 136.3m ~ 0.400
InpointDedup_TinyRuns-4 50.45m 49.32m ~ 0.700
MiningCandidate_Stringify_Short-4 237.5n 237.3n ~ 0.700
MiningCandidate_Stringify_Long-4 1.664µ 1.675µ ~ 1.000
MiningSolution_Stringify-4 847.6n 848.9n ~ 0.400
BlockInfo_MarshalJSON-4 2.257µ 2.269µ ~ 0.400
DecodeSubtreeMeta/go-subtree-4 218.4m 218.5m ~ 1.000
DecodeSubtreeMeta/packed-4 60.42m 58.66m ~ 1.000
DecodeSubtreeMeta/go-subtree/parallel-4 107.1m 107.9m ~ 0.700
DecodeSubtreeMeta/packed/parallel-4 41.96m 43.91m ~ 0.700
ValidateSubtreeNodesMatchKey/leaves=1024-4 263.6µ 261.0µ ~ 0.100
ValidateSubtreeNodesMatchKey/leaves=1048576-4 81.21m 80.29m ~ 0.200
NewFromBytes-4 126.1n 125.1n ~ 0.200
AddTxBatchColumnar_Validation-4 2.176µ 2.194µ ~ 0.700
OffsetValidationLoop-4 645.9n 640.2n ~ 0.400
BuildSubtreeMeta/serialized-4 15.02m 13.21m ~ 0.400
BuildSubtreeMeta/decoded-4 26.35m 24.08m ~ 0.100
Mine_EasyDifficulty-4 52.43µ 52.44µ ~ 1.000
Mine_WithAddress-4 5.619µ 5.641µ ~ 0.500
DirectSubtreeAdd/4_per_subtree-4 80.17n 79.41n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 35.59n 35.28n ~ 0.400
DirectSubtreeAdd/256_per_subtree-4 32.92n 34.09n ~ 0.400
DirectSubtreeAdd/1024_per_subtree-4 31.45n 31.53n ~ 1.000
DirectSubtreeAdd/2048_per_subtree-4 31.00n 30.96n ~ 1.000
SubtreeProcessorAdd/4_per_subtree-4 269.2n 254.9n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 282.3n 288.8n ~ 0.700
SubtreeProcessorAdd/256_per_subtree-4 302.8n 307.1n ~ 0.700
SubtreeProcessorAdd/1024_per_subtree-4 302.1n 302.7n ~ 0.400
SubtreeProcessorAdd/2048_per_subtree-4 299.0n 302.9n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 303.8n 303.4n ~ 1.000
SubtreeProcessorRotate/64_per_subtree-4 302.2n 306.1n ~ 0.200
SubtreeProcessorRotate/256_per_subtree-4 302.9n 304.5n ~ 0.400
SubtreeProcessorRotate/1024_per_subtree-4 300.2n 304.7n ~ 0.700
SubtreeNodeAddOnly/4_per_subtree-4 68.12n 67.11n ~ 1.000
SubtreeNodeAddOnly/64_per_subtree-4 40.37n 40.61n ~ 0.700
SubtreeNodeAddOnly/256_per_subtree-4 39.67n 39.94n ~ 0.200
SubtreeNodeAddOnly/1024_per_subtree-4 38.98n 39.40n ~ 0.700
SubtreeCreationOnly/4_per_subtree-4 168.8n 164.9n ~ 1.000
SubtreeCreationOnly/64_per_subtree-4 470.5n 416.2n ~ 0.400
SubtreeCreationOnly/256_per_subtree-4 1.386µ 1.386µ ~ 0.800
SubtreeCreationOnly/1024_per_subtree-4 4.827µ 4.949µ ~ 0.400
SubtreeCreationOnly/2048_per_subtree-4 10.474µ 9.958µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 242.5n 242.2n ~ 1.000
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 293.9n 298.4n ~ 0.400
ParallelGetAndSetIfNotExists/1k_nodes-4 766.7µ 778.3µ ~ 1.000
ParallelGetAndSetIfNotExists/10k_nodes-4 5.646m 5.324m ~ 1.000
ParallelGetAndSetIfNotExists/50k_nodes-4 7.816m 7.739m ~ 1.000
ParallelGetAndSetIfNotExists/100k_nodes-4 10.56m 10.62m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 747.5µ 768.7µ ~ 1.000
SequentialGetAndSetIfNotExists/10k_nodes-4 6.420m 7.025m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 20.92m 23.72m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 36.16m 39.01m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 843.2µ 1040.2µ ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 7.418m 7.981m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 12.74m 13.73m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 750.1µ 809.5µ ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 9.187m 10.680m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 53.90m 59.56m ~ 0.100
BlockAssembler_AddTx-4 0.01571n 0.01602n ~ 1.000
AddNode-4 9.435 9.316 ~ 1.000
AddNodeWithMap-4 9.398 9.005 ~ 0.100
DiskTxMap_SetIfNotExists-4 509.2n 495.1n ~ 0.300
DiskTxMap_SetIfNotExists_Parallel-4 194.4n 191.2n ~ 0.400
DiskTxMap_ExistenceOnly-4 231.3n 242.1n ~ 0.600
Queue-4 131.8n 131.8n ~ 1.000
AtomicPointer-4 2.797n 2.779n ~ 0.200
TxMapSetIfNotExists-4 39.16n 39.35n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 33.06n 32.96n ~ 0.600
ChannelSendReceive-4 434.3n 432.7n ~ 0.400
CalcBlockWork-4 543.0n 549.0n ~ 1.000
CalculateWork-4 711.7n 708.8n ~ 0.400
CheckOldBlockIDs/on-chain-prefetch/1000-4 35.16µ 35.45µ ~ 0.200
CheckOldBlockIDs/in-memory-chain-check/1000-4 870.5µ 945.5µ ~ 0.100
CheckOldBlockIDs/on-chain-prefetch/10000-4 281.7µ 258.2µ ~ 0.700
CheckOldBlockIDs/in-memory-chain-check/10000-4 1.312m 1.321m ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 818.2n 839.8n ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 7.648µ 7.949µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 74.24µ 77.51µ ~ 0.100
CatchupWithHeaderCache-4 105.1m 104.9m ~ 0.700
_BufferPoolAllocation/16KB-4 4.627µ 4.987µ ~ 0.100
_BufferPoolAllocation/32KB-4 13.03µ 13.58µ ~ 0.700
_BufferPoolAllocation/64KB-4 17.67µ 21.68µ ~ 0.400
_BufferPoolAllocation/128KB-4 36.04µ 44.39µ ~ 0.100
_BufferPoolAllocation/512KB-4 148.7µ 152.2µ ~ 0.400
_BufferPoolConcurrent/32KB-4 22.47µ 22.59µ ~ 0.400
_BufferPoolConcurrent/64KB-4 36.22µ 36.58µ ~ 0.400
_BufferPoolConcurrent/512KB-4 196.4µ 196.6µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/16KB-4 711.8µ 707.6µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/32KB-4 730.5µ 709.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 737.6µ 701.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 737.5µ 700.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 789.8µ 715.7µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.20m 38.52m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.49m 38.17m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.22m 37.45m ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/128KB-4 37.00m 37.66m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 37.01m 38.09m ~ 0.100
_PooledVsNonPooled/Pooled-4 735.4n 738.0n ~ 0.400
_PooledVsNonPooled/NonPooled-4 9.071µ 9.812µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 7.730µ 7.856µ ~ 1.000
_MemoryFootprint/Proposed_32KB_32concurrent-4 10.54µ 10.61µ ~ 0.700
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.742µ 10.263µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.441m 1.408m ~ 0.400
SubtreeSizes/10k_tx_16_per_subtree-4 337.8µ 330.4µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 82.34µ 78.81µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 20.44µ 19.70µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 10.502µ 9.649µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 5.120µ 4.927µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.577µ 2.507µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 82.67µ 83.01µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 20.75µ 19.87µ ~ 0.200
BlockSizeScaling/10k_tx_1024_per_subtree-4 5.362µ 5.149µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 429.5µ 417.5µ ~ 0.200
BlockSizeScaling/50k_tx_256_per_subtree-4 105.1µ 103.1µ ~ 0.400
BlockSizeScaling/50k_tx_1024_per_subtree-4 27.19µ 26.89µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 183.9µ 181.2µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 205.8µ 193.6µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 387.9µ 378.5µ ~ 0.100
SubtreeAllocations/medium_subtrees_exists_check-4 11.32µ 10.90µ ~ 0.100
SubtreeAllocations/medium_subtrees_data_fetch-4 12.77µ 11.76µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 25.62µ 24.48µ ~ 0.200
SubtreeAllocations/large_subtrees_exists_check-4 2.823µ 2.880µ ~ 0.100
SubtreeAllocations/large_subtrees_data_fetch-4 3.332µ 3.113µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 6.384µ 6.147µ ~ 0.100
_prepareTxsPerLevel-4 362.2m 359.2m ~ 0.200
_prepareTxsPerLevelOrdered-4 3.404m 3.790m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 353.3m 357.6m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 3.274m 3.265m ~ 0.200
StoreBlockForkPath/inMemoryChainCheck=false-4 621.8µ 622.3µ ~ 1.000
StoreBlockForkPath/inMemoryChainCheck=true-4 1.294m 1.286m ~ 1.000
StoreBlock_Sequential/BelowCSVHeight-4 197.8µ 197.5µ ~ 1.000
StoreBlock_Sequential/AboveCSVHeight-4 200.1µ 197.0µ ~ 0.200
CheckSpendAndCreateMultiList_FanIn/parents=80000-4 47.69m 51.45m ~ 0.400
CheckSpendAndCreateMultiList_FanIn/parents=160000-4 129.6m 118.7m ~ 0.700
GetUtxoHashes-4 297.6n 291.9n ~ 0.700
GetUtxoHashes_ManyOutputs-4 41.62µ 41.30µ ~ 0.400
MetaBytes-4 67.22n 68.59n ~ 0.100
_NewMetaDataFromBytes-4 222.6n 224.7n ~ 1.000
_Bytes-4 338.1n 336.8n ~ 0.700
_MetaBytes-4 104.2n 107.9n ~ 0.100

Threshold: >10% with p < 0.05 | Generated: 2026-10-28 12:43 UTC

@freemans13
freemans13 marked this pull request as ready for review October 22, 2026 10:40
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:

No issues found. The only new commit, 15a33b9, merges upstream/main again. On the paths this PR touches, it changes just two files. In services/blockassembly/blockassembly_system_test.go, upstream's reorg tests replace fixed sleeps with a waitForMiningCandidate poll, which doesn't interact with this PR's coinbase handling. In CHANGELOG.md, upstream adds its own entries. This PR's entry is still under ## [Unreleased] → "Breaking — behaviour changes on upgrade" and is unchanged. Server.go, rpc/handlers.go, model/ and the docs were not touched by the merge.

The earlier changes still look correct. A missed share is rejected without a reset, ErrBlockInvalid keeps the job and its coinbase, a node without miner keys still serves candidates, and unusable configured keys log a rate-limited Warn. All inline threads have been answered and confirmed.

History:

  • ✅ Fixed: the requirePastFloorGuard doc comment is attached to the right function again (27f4704).
  • ✅ Fixed: GetMiningCandidate no longer fails without miner_wallet_private_keys.
  • ✅ Fixed: the docs now mark the submitminingsolution coinbase as optional and describe the fallback.
  • ✅ Addressed: the coinbase is no longer swapped after an invalid or low-difficulty solution, and bad configured keys log a warning.

Still not flagged: model.pb.go was regenerated with a newer protoc, which only changes the header comments.

Comment thread services/blockassembly/Server.go Resolved
…w round 1)

Building a candidate's coinbase needs miner_wallet_private_keys, and the
previous commit made every getminingcandidate build one, so a node with
no keys could no longer serve candidates. A pool that submits its own
coinbase and generatetoaddress never needed the keys. A coinbase that
cannot be built now leaves the job without one; a coinbase-less
solution, GetCandidateBlock and coinbase=true refuse such a job with a
message naming the setting, and the refusal does not drop the job.

Docs: submitminingsolution's coinbase is optional, and provideCoinbaseTx
returns the coinbase kept on the job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XzzaR3W5oM3wemXLEeNFc
@freemans13

Copy link
Copy Markdown
Collaborator Author

Reply to the Claude Code Review of 6ab204a, both points fixed in ecb0026.

  • Major, candidates need miner wallet keys: a coinbase that cannot be built now leaves the job without one instead of failing getminingcandidate. Only a coinbase-less solution, GetCandidateBlock and coinbase=true refuse, with a message naming miner_wallet_private_keys. Details and the test are on the inline thread.
  • Minor, docs: docs/howto/miners/minersHowToInteractWithRPCServer.md and docs/references/services/rpc_reference.md now list submitminingsolution's coinbase as optional, say a solution without one is mined with the coinbase kept on the job, and say provideCoinbaseTx returns that coinbase. I checked the handler first: an omitted coinbase decodes to empty bytes and takes the fallback path.

freemans13 and others added 2 commits October 22, 2026 12:04
GenerateBlocks with an explicit P2PKH address and
miner_wallet_private_keys cleared must mine a block: the best height
advances by one and the stored block's coinbase pays that address. The
test fails if storeJobWithCoinbase goes back to failing the candidate
when the job's own coinbase cannot be built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XzzaR3W5oM3wemXLEeNFc
@freemans13
freemans13 requested review from icellan and ordishs October 22, 2026 13:40

@ordishs ordishs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. The coinbase is built once per candidate, stored under a mutex, and mined unaltered on submit. This matches bitcoin-sv. The tests assert exact bytes against a real store and cover the race.

Two non-blocking notes:

  1. services/blockassembly/Server.go (storeJobWithCoinbase): a failure to build the coinbase logs at Debug level only. An invalid WIF in miner_wallet_private_keys then looks the same as "no keys". Consider a Warn when keys are configured but the build fails.
  2. MiningCandidate gains coinbase_tx (field 13). Does the Rust BlockAssembly port build and return it? If not, getminingcandidate coinbase=true errors against that service.

@icellan icellan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues, neither blocking. Details inline.

Comment thread services/blockassembly/Server.go Resolved
Comment thread services/blockassembly/Server.go Resolved
…nd warn on unusable miner keys

A consensus-invalid submitminingsolution deleted the job. With the coinbase
now kept on the job and candidate ids repeating (the empty-block id is derived
from the parent alone), the next poll built a coinbase with a fresh extranonce,
so every other coinbase-less miner on that id failed proof of work and deleted
the job again. The ErrBlockInvalid branch now keeps the job, as bitcoin-sv
leaves a candidate alone after an invalid solution; the TTL and the
mined-block DeleteAll still clear it. That delete came from e787b23 (upstream
PR 1748); this PR's coinbase reuse is what made it harmful.

A share that misses the target never reached that branch: Block.Valid reports
a miss as a processing error (HasMetTargetDifficulty returns one with the
miss, so the BlockInvalid line after it is unreachable), which took the reset
path, reset block assembly and dropped the job. submitMiningSolution now checks
the target itself before Block.Valid and rejects a miss without a reset,
keeping the job. The reset path and its delete are unchanged for faults not
attributable to the submitter; Block.Valid is untouched, so peer block
validation is unaffected.

storeJobWithCoinbase logged every coinbase build failure at Debug, so a bad WIF
in miner_wallet_private_keys looked like no keys. With a non-blank key
configured it now warns with the cause, at most once a minute; with no keys it
stays at Debug. The getminingcandidate coinbase=true error now also names an
implementation that does not set coinbase_tx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XzzaR3W5oM3wemXLEeNFc
@freemans13

Copy link
Copy Markdown
Collaborator Author

@ordishs, thanks for the review. Answers to your two notes:

1. Bad WIF logged at Debug. Fixed in b44c4b2, the same fix as icellan's inline comment on storeJobWithCoinbase. A non-blank configured key that cannot build a coinbase now logs a Warn with the cause, at most once a minute. A node with no keys stays at Debug. Test: TestStoreJobWithCoinbase_UnusableMinerKeysWarn. The details are in the reply on that thread.

2. The Rust BlockAssembly port and coinbase_tx. I cannot check this: there is no Rust code in this repo, and I could not find a public Rust BlockAssembly repo under bsv-blockchain, so I am not claiming it works. Here is what the change means for a service that does not set the field:

  • coinbase_tx is field 13 of MiningCandidate, added under proto3. An implementation that does not know it sends nothing, and this RPC service reads that as empty.
  • getminingcandidate with coinbase=true then returns an explicit RPC error instead of a coinbase: block assembly returned the candidate without its coinbase: it could not build one from miner_wallet_private_keys, or it is an older or other implementation that does not set coinbase_tx. Before this PR, the RPC service built that coinbase itself, so that call works against such a service today and would stop working.
  • A coinbase-less submitminingsolution goes to that service and follows its own logic, which this PR does not change. If it rebuilds the coinbase the way the Go service used to, it has the same random-extranonce mismatch this PR fixes.
  • coinbase=false, and solutions that carry their own coinbase, are unaffected.

Where does the Rust port live? If you point me at it, I will check whether it builds and returns the candidate's coinbase, or raise an issue there to add field 13.

@freemans13
freemans13 requested a review from icellan October 23, 2026 10:07
withIssuedCoinbase was inserted between requirePastFloorGuard and its doc
comment, so godoc attached that comment to the wrong function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XzzaR3W5oM3wemXLEeNFc
…e-keeps-coinbase

# Conflicts:
#	CHANGELOG.md
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants