diff options
| author | Bitflash <btf01@proton.me> | 2026-08-04 15:14:46 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-04 15:14:46 -0300 |
| commit | 7aaaa7a684e33591125963df1eb4da6da2fa6495 (patch) | |
| tree | 2f7424565e2422ed80dfc76232ed056fa14cf5f6 /src/rpc.cpp | |
| parent | a2592156207ed04671d6532cec4997fb1dab1595 (diff) | |
rpc: build the pool block template the way the solo miner does (#141)
Two bugs in RebuildJob(), the operator's Stratum template builder. It
is a separate path from BitcoinMiner() and had drifted from it.
A single pass over mapTransactions is not enough. That map is keyed by
hash and carries no dependency order, so a transaction spending another
still-unconfirmed transaction's output -- change from a recent send,
usually -- fails ConnectInputs whenever its parent sits later in map
order. One pass drops it, and map order does not change between
rebuilds, so every later rebuild drops it at the same point. That is
deterministic exclusion rather than bad luck: where pool operators mine
most blocks, a transaction of that shape can stay unconfirmed for as
long as its parent is. Now it retries until a pass adds nothing, which
is what the solo builder has always done.
And the fees reached nobody. nFees was declared inside the scan loop, so
each transaction reset it and the running total was discarded, while the
coinbase line passed a hardcoded 0. An operator collected fees into a
block and paid none of them out. The block stayed valid, since paying
less than allowed always is, so nothing ever complained.
Both the diagnosis and the fix are Vic-Nas's, from his fork of this
code. He deleted the fork, so this brings the work back rather than
letting it go with it. His description of the failure was precise enough
that finding the same two defects here took one read of our RebuildJob.
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 61 |
1 files changed, 48 insertions, 13 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index 41c0328..d73b09c 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -167,29 +167,64 @@ static bool RebuildJob() | |||
| 167 | CBlock block; | 167 | CBlock block; |
| 168 | block.vtx.push_back(txNew); | 168 | block.vtx.push_back(txNew); |
| 169 | 169 | ||
| 170 | // Fill the block the same way BitcoinMiner() does, because this is the | ||
| 171 | // same job: a block that gets mined and submitted for real. | ||
| 172 | // | ||
| 173 | // It used to differ in two ways, and both were bugs. | ||
| 174 | // | ||
| 175 | // One pass over mapTransactions is not enough. That map is keyed by | ||
| 176 | // hash, so it has no dependency order, and a transaction spending the | ||
| 177 | // still-unconfirmed output of another (change from a recent send, most | ||
| 178 | // commonly) fails ConnectInputs whenever its parent happens to sit | ||
| 179 | // later in map order. A single pass then drops it -- and because map | ||
| 180 | // order does not change between rebuilds, every later rebuild drops it | ||
| 181 | // again at the same point. Deterministic exclusion, not bad luck: if | ||
| 182 | // pool operators mine most of the blocks, a transaction of that shape | ||
| 183 | // can stay unconfirmed for as long as its parent does. | ||
| 184 | // | ||
| 185 | // Diagnosis and fix both come from Vic-Nas, who found it in his fork | ||
| 186 | // of this code and described it exactly. The fork has since been | ||
| 187 | // deleted, so this carries it back rather than losing it. | ||
| 188 | int64 nFees = 0; | ||
| 170 | { | 189 | { |
| 171 | CTxDB txdb("r"); | 190 | CTxDB txdb("r"); |
| 172 | CRITICAL_BLOCK(cs_mapTransactions) | 191 | CRITICAL_BLOCK(cs_mapTransactions) |
| 173 | { | 192 | { |
| 174 | map<uint256, CTxIndex> pool; | 193 | map<uint256, CTxIndex> pool; |
| 194 | vector<char> vfAlreadyAdded(mapTransactions.size()); | ||
| 195 | bool fFoundSomething = true; | ||
| 175 | unsigned int sz = 0; | 196 | unsigned int sz = 0; |
| 176 | for (auto& kv : mapTransactions) { | 197 | while (fFoundSomething && sz < MAX_SIZE / 2) |
| 177 | CTransaction& tx = kv.second; | 198 | { |
| 178 | if (tx.IsCoinBase() || !tx.IsFinal()) continue; | 199 | fFoundSomething = false; |
| 179 | int64 nFees = 0; | 200 | unsigned int n = 0; |
| 180 | map<uint256, CTxIndex> tmp(pool); | 201 | for (map<uint256, CTransaction>::iterator mi = mapTransactions.begin(); |
| 181 | if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, | 202 | mi != mapTransactions.end(); ++mi, ++n) |
| 182 | false, true, tx.GetMinFee(block.vtx.size() < 100))) | 203 | { |
| 183 | continue; | 204 | if (vfAlreadyAdded[n]) |
| 184 | pool = tmp; | 205 | continue; |
| 185 | block.vtx.push_back(tx); | 206 | CTransaction& tx = (*mi).second; |
| 186 | sz += ::GetSerializeSize(tx, SER_NETWORK); | 207 | if (tx.IsCoinBase() || !tx.IsFinal()) continue; |
| 187 | if (sz > MAX_SIZE / 2) break; | 208 | map<uint256, CTxIndex> tmp(pool); |
| 209 | if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, | ||
| 210 | false, true, tx.GetMinFee(block.vtx.size() < 100))) | ||
| 211 | continue; | ||
| 212 | swap(pool, tmp); | ||
| 213 | block.vtx.push_back(tx); | ||
| 214 | sz += ::GetSerializeSize(tx, SER_NETWORK); | ||
| 215 | vfAlreadyAdded[n] = true; | ||
| 216 | fFoundSomething = true; | ||
| 217 | } | ||
| 188 | } | 218 | } |
| 189 | } | 219 | } |
| 190 | } | 220 | } |
| 191 | 221 | ||
| 192 | block.vtx[0].vout[0].nValue = block.GetBlockValue(pindexPrev->nHeight + 1, 0); | 222 | // And the fees actually go to the coinbase now. nFees was declared |
| 223 | // inside the scan loop, so every transaction reset it and the total was | ||
| 224 | // thrown away, while this line passed a hardcoded 0. A pool operator | ||
| 225 | // collected fees into a block and paid none of them to itself. Solo | ||
| 226 | // mining has always passed nFees here; only this builder did not. | ||
| 227 | block.vtx[0].vout[0].nValue = block.GetBlockValue(pindexPrev->nHeight + 1, nFees); | ||
| 193 | block.hashPrevBlock = pindexPrev->GetBlockHash(); | 228 | block.hashPrevBlock = pindexPrev->GetBlockHash(); |
| 194 | block.hashMerkleRoot = block.BuildMerkleTree(); | 229 | block.hashMerkleRoot = block.BuildMerkleTree(); |
| 195 | block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1), | 230 | block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1), |