upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-04 15:14:46 -0300
committerGitHub <noreply@github.com>2026-08-04 15:14:46 -0300
commit7aaaa7a684e33591125963df1eb4da6da2fa6495 (patch)
tree2f7424565e2422ed80dfc76232ed056fa14cf5f6 /src
parenta2592156207ed04671d6532cec4997fb1dab1595 (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')
-rw-r--r--src/rpc.cpp61
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),