Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,24 @@ Upgrading from v0.16.0 changes nothing for them; upgrading from an earlier relea

### Breaking — behaviour changes on upgrade

- **A mining candidate keeps its coinbase, and a solution without one is mined with it.**
Affects miners and pools that use `getminingcandidate` and `submitminingsolution`. As in
bitcoin-sv, block assembly now builds a candidate's coinbase once, keeps it on the job,
and returns it in the new `coinbase_tx` field of the `MiningCandidate` message. A
`submitminingsolution` call with no `coinbase` is mined with that coinbase. Before, block
assembly rebuilt one with a fresh random extranonce, so its merkle root was never the one
the miner hashed and every such solution failed the proof-of-work check, deleted the job
and reset block assembly. A solution's `version` now goes into the header only, no longer
into the coinbase as well. `getminingcandidate` with `coinbase=true` returns the kept
coinbase: a version 1 transaction paying the configured miner wallet addresses by P2PKH,
where it used to return a separately built version 2 transaction with one pay-to-pubkey
output that block assembly never saw. Miners that submit their own coinbase are not
affected, and neither is a node without `miner_wallet_private_keys`: it still serves
candidates, with no coinbase on the job, and refuses only a coinbase-less solution and
`coinbase=true`, as it did before. Upgrade block assembly no later than the RPC service: an RPC service on this
build answers `getminingcandidate` with `coinbase=true` with an error while it is talking
to an older block assembly, which sends no coinbase. (#2027)

- **A failed revert of an invalid block's mined tags now holds block assembly until it
succeeds.** When block validation reverts the mined tags of an invalid block and
`SetMinedMulti` fails, or a subtree read fails with anything other than not-found, the
Expand Down
4 changes: 2 additions & 2 deletions docs/howto/miners/minersHowToInteractWithRPCServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The Teranode RPC server provides a JSON-RPC interface for interacting with the n

- Optional object containing:

- `provideCoinbaseTx` (boolean, optional, default=false): If true, includes the coinbase transaction in the response
- `provideCoinbaseTx` (boolean, optional, default=false): If true, includes the coinbase transaction in the response. This is the coinbase block assembly keeps on the candidate's job, paying the configured `miner_wallet_private_keys` addresses, and the one a solution without its own coinbase is mined with. The call fails if block assembly could not build one, for example because no miner wallet keys are configured
- `verbosity` (numeric, optional, default=0): 0 for standard response, 1 to include subtree hashes

- Returns: Object containing candidate ID, previous block hash, coinbase value, and merkle proof
Expand Down Expand Up @@ -90,7 +90,7 @@ The Teranode RPC server provides a JSON-RPC interface for interacting with the n

- `id` (string, required): Mining candidate ID
- `nonce` (hexadecimal string, required): Nonce value found
- `coinbase` (hexadecimal string, required): Complete coinbase transaction
- `coinbase` (hexadecimal string, optional): Complete coinbase transaction. If omitted, the block is mined with the coinbase block assembly kept on the candidate's job, the one `getminingcandidate` returns with `provideCoinbaseTx`, as in bitcoin-sv. The miner must then have hashed that coinbase. Omitting it fails if block assembly could not build one, for example because no miner wallet keys are configured
- `time` (numeric, required): Block time
- `version` (numeric, optional): Block version

Expand Down
6 changes: 4 additions & 2 deletions docs/references/services/rpc_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ Returns information for creating a new block.

1. `parameters` (object, optional):

- `provideCoinbaseTx` (boolean, optional, default=false): If true, includes the coinbase transaction in the response
- `provideCoinbaseTx` (boolean, optional, default=false): If true, includes the coinbase transaction in the response. This is the coinbase block assembly keeps on the candidate's job, paying the configured `miner_wallet_private_keys` addresses, and the one a solution without its own coinbase is mined with. The call fails if block assembly could not build one, for example because no miner wallet keys are configured
- `verbosity` (numeric, optional, default=0): 0 for standard response, 1 to include subtree hashes

**Returns:**
Expand Down Expand Up @@ -947,12 +947,14 @@ Submits a solved block to the network.
{
"id": "string", // Mining candidate ID
"nonce": number, // Block nonce
"coinbase": "string", // Coinbase transaction
"coinbase": "string", // Coinbase transaction (optional, see below)
"time": number, // Block time
"version": number // Block version
}
```

If `coinbase` is omitted, the block is mined with the coinbase block assembly kept on the candidate's job, the one `getminingcandidate` returns with `provideCoinbaseTx`, as in bitcoin-sv. The miner must then have hashed that coinbase. `version` goes into the block header only. Omitting `coinbase` fails if block assembly could not build one, for example because no miner wallet keys are configured.

**Returns:**

- `boolean` - True if accepted, false if rejected
Expand Down
27 changes: 20 additions & 7 deletions model/model.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions model/model.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ message MiningCandidate {
uint32 num_txs = 10;
uint64 size_without_coinbase = 11;
repeated bytes subtree_hashes = 12;
// The coinbase block assembly built for this candidate and keeps on its job. A solution that
// omits its own coinbase is mined with this one, as in bitcoin-sv, so it is the coinbase a miner
// must hash when it does not supply one.
bytes coinbase_tx = 13;
}

// swagger:model MiningSolution
Expand Down
88 changes: 65 additions & 23 deletions services/blockassembly/Server.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ type BlockAssembly struct {
// jobStore caches mining jobs with TTL
jobStore *ttlcache.Cache[chainhash.Hash, *subtreeprocessor.Job]

// jobCoinbaseMu makes GetMiningCandidate's read of a job's coinbase and its store of the new
// job one step, so two polls that share a candidate id cannot each build a coinbase and leave
// one miner holding a coinbase the job no longer has.
jobCoinbaseMu sync.Mutex

// blockSubmissionChan handles block submission requests
blockSubmissionChan chan *BlockSubmissionRequest

Expand Down Expand Up @@ -1668,11 +1673,7 @@ func (ba *BlockAssembly) GetMiningCandidate(ctx context.Context, req *blockassem

id, _ := chainhash.NewHash(miningCandidate.Id)

ba.jobStore.Set(*id, &subtreeprocessor.Job{
ID: id,
Subtrees: subtrees,
MiningCandidate: miningCandidate,
}, jobTTL) // create a new job with a TTL, will be cleaned up automatically
ba.storeJobWithCoinbase(id, miningCandidate, subtrees)

if includeSubtreeHashes {
miningCandidate.SubtreeHashes = make([][]byte, len(subtrees))
Expand All @@ -1691,6 +1692,46 @@ func (ba *BlockAssembly) GetMiningCandidate(ctx context.Context, req *blockassem
return miningCandidate, nil
}

// storeJobWithCoinbase gives the candidate its coinbase and stores it as a job with a TTL, after
// which it is cleaned up automatically. The coinbase is the one a solution without its own is mined
// with, as bitcoin-sv keeps the candidate block's coinbase, so it must not change under a miner who
// has been handed it. Candidate ids repeat: the main path hashes the subtree root, parent and time,
// so polls in the same second share one, and the empty-block path derives its id from the parent
// alone. A poll for an id whose job is still held therefore reuses that job's coinbase rather than
// building a new one, provided it pays the same value at the same height.
//
// Building the coinbase needs miner_wallet_private_keys, which a pool that submits its own coinbase
// and generatetoaddress, which pays an explicit address, have never needed. So a coinbase that
// cannot be built leaves the job without one rather than failing the candidate: only a solution
// that omits its coinbase, GetCandidateBlock and getminingcandidate with coinbase=true need it,
// and each of those refuses a job without one.
func (ba *BlockAssembly) storeJobWithCoinbase(id *chainhash.Hash, miningCandidate *model.MiningCandidate, subtrees []*subtreepkg.Subtree) {
ba.jobCoinbaseMu.Lock()
defer ba.jobCoinbaseMu.Unlock()

if held := ba.jobStore.Get(*id); held != nil {
Comment thread
icellan marked this conversation as resolved.
Resolved
prev := held.Value().MiningCandidate
if len(prev.CoinbaseTx) > 0 && prev.CoinbaseValue == miningCandidate.CoinbaseValue && prev.Height == miningCandidate.Height {
miningCandidate.CoinbaseTx = prev.CoinbaseTx
}
}

if len(miningCandidate.CoinbaseTx) == 0 {
coinbaseTx, err := miningCandidate.CreateCoinbaseTxCandidate(ba.blockAssembler.settings)
Comment thread
freemans13 marked this conversation as resolved.
Resolved
if err != nil {
ba.logger.Debugf("[GetMiningCandidate][%s] storing the job without a coinbase, so only a solution with its own coinbase can be mined: %v", id.String(), err)
Comment thread
icellan marked this conversation as resolved.
Resolved
} else {
miningCandidate.CoinbaseTx = coinbaseTx.Bytes()
}
}

ba.jobStore.Set(*id, &subtreeprocessor.Job{
ID: id,
Subtrees: subtrees,
MiningCandidate: miningCandidate,
}, jobTTL)
}

// SubmitMiningSolution processes a mining solution submission.
// It validates the solution, creates a block, and adds it to the blockchain.
//
Expand Down Expand Up @@ -1934,23 +1975,19 @@ func (ba *BlockAssembly) submitMiningSolution(ctx context.Context, req *BlockSub
return nil, errors.NewProcessingError("[BlockAssembly][%s] submitted coinbase transaction is not a valid coinbase", jobID)
}
} else {
// recreate coinbase tx here, nothing was passed in
coinbaseTx, err = jobItem.Value().MiningCandidate.CreateCoinbaseTxCandidate(ba.blockAssembler.settings)
if err != nil {
return nil, errors.NewProcessingError("[BlockAssembly][%s] failed to create coinbase tx", jobID, err)
// No coinbase was passed in, so mine the one the job was issued with, which is the one
// GetMiningCandidate handed out, as bitcoin-sv falls back to the candidate block's coinbase.
// It used to be rebuilt here, with a fresh random extranonce, so its merkle root was never
// the one the miner hashed and every such solution failed the proof-of-work check. The
// solution's version, time and nonce belong in the header, built from req below, and are not
// copied into the coinbase, so it is used exactly as issued.
if len(job.MiningCandidate.CoinbaseTx) == 0 {
return nil, errors.NewProcessingError("[BlockAssembly][%s] solution has no coinbase and the job has none to mine with, because block assembly could not build one from miner_wallet_private_keys", jobID)
}

// set the new mining parameters on the coinbase (nonce)
if req.Version != nil {
coinbaseTx.Version = *req.Version
}

if req.Time != nil {
coinbaseTx.LockTime = *req.Time
}

if req.Nonce != 0 {
coinbaseTx.Inputs[0].SequenceNumber = req.Nonce
coinbaseTx, err = bt.NewTxFromBytes(job.MiningCandidate.CoinbaseTx)
if err != nil {
return nil, errors.NewProcessingError("[BlockAssembly][%s] job holds no usable coinbase tx", jobID, err)
}
}

Expand Down Expand Up @@ -2290,10 +2327,15 @@ func (ba *BlockAssembly) GetCandidateBlock(ctx context.Context, req *blockassemb

job := jobItem.Value()

// Create default coinbase transaction from the mining candidate
coinbaseTx, err := job.MiningCandidate.CreateCoinbaseTxCandidate(ba.blockAssembler.settings)
// The coinbase the job was issued with, so this block is the one a coinbase-less solution to the
// candidate would mine.
if len(job.MiningCandidate.CoinbaseTx) == 0 {
return nil, errors.WrapGRPC(errors.NewProcessingError("[GetCandidateBlock][%s] job has no coinbase, because block assembly could not build one from miner_wallet_private_keys", candidateID))
}

coinbaseTx, err := bt.NewTxFromBytes(job.MiningCandidate.CoinbaseTx)
if err != nil {
return nil, errors.WrapGRPC(errors.NewProcessingError("[GetCandidateBlock][%s] failed to create coinbase tx", candidateID, err))
return nil, errors.WrapGRPC(errors.NewProcessingError("[GetCandidateBlock][%s] job holds no usable coinbase tx", candidateID, err))
}

coinbaseTxIDHash := coinbaseTx.TxIDChainHash()
Expand Down
Loading
Loading