diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 14:14:48 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 14:14:48 +0000 |
| commit | 74c068829967bbff60da3506613844df4da1ae80 (patch) | |
| tree | 358400d4a170a5777138bd44d048a4cd7f4aaf6b | |
| parent | e4f898c4a6b9900553c64b9804889b5fdae3e55f (diff) | |
mine: ask whether a transaction fits before appending it
The node miner tested its size budget only on the outer while loop. The inner
for walked all of mapTransactions with no test, so a single pass could append
the entire mempool -- the overshoot was bounded by mempool size, not by one
transaction as the comment claimed.
The pool builder did test, but only after the push, which bounds the overshoot
at one transaction. Nothing caps how big one transaction may be: neither
CheckTransaction nor AcceptTransaction limits it, so the mempool will hold one
of any size the wire allows and "one transaction" is worth up to MAX_SIZE. A
lone 1.5 MB transaction offering a large fee is therefore enough to make every
template unmineable, and the fee is never paid because the block never
confirms.
Both now ask before appending and skip what does not fit, so the overshoot is
zero and an oversized transaction costs the template nothing rather than
invalidating it. The budget moves to MAX_BLOCK_SIZE_GEN so the two assemblers
cannot drift on it again -- drift is what produced this.
The test is ordered before ConnectInputs on purpose: ConnectInputs credits
nFees by reference on success, so declining a transaction after calling it
would leave the coinbase claiming a fee the block does not collect.
Assembly only. CheckBlock, CheckTransaction and AcceptTransaction are
untouched: this changes what we produce, not what the network accepts.
| -rw-r--r-- | src/main.cpp | 41 | ||||
| -rw-r--r-- | src/main.h | 20 | ||||
| -rw-r--r-- | src/rpc.cpp | 34 |
3 files changed, 81 insertions, 14 deletions
diff --git a/src/main.cpp b/src/main.cpp index 7b3e555..64eb290 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -4470,7 +4470,6 @@ bool BitcoinMiner(int nThreadId) | |||
| 4470 | map<uint256, CTxIndex> mapTestPool; | 4470 | map<uint256, CTxIndex> mapTestPool; |
| 4471 | vector<char> vfAlreadyAdded(mapTransactions.size()); | 4471 | vector<char> vfAlreadyAdded(mapTransactions.size()); |
| 4472 | bool fFoundSomething = true; | 4472 | bool fFoundSomething = true; |
| 4473 | unsigned int nBlockSize = 0; | ||
| 4474 | // Assembly must stop below the consensus cap that CheckBlock() | 4473 | // Assembly must stop below the consensus cap that CheckBlock() |
| 4475 | // enforces, or we mine a block our own node then rejects. | 4474 | // enforces, or we mine a block our own node then rejects. |
| 4476 | // | 4475 | // |
| @@ -4481,9 +4480,19 @@ bool BitcoinMiner(int nThreadId) | |||
| 4481 | // meant solving a block and throwing it away -- with this pool at | 4480 | // meant solving a block and throwing it away -- with this pool at |
| 4482 | // ~90% of hashrate, that is the network stalling, not just us. | 4481 | // ~90% of hashrate, that is the network stalling, not just us. |
| 4483 | // | 4482 | // |
| 4484 | // Half the cap, keeping the original margin: the test runs before | 4483 | // The budget test then sat only on this outer while, and the |
| 4485 | // a transaction is added, so the loop can overshoot by one tx. | 4484 | // comment claimed that bounded the overshoot to a single |
| 4486 | while (fFoundSomething && nBlockSize < MAX_BLOCK_SIZE/2) | 4485 | // transaction. It did not. The inner for walks all of |
| 4486 | // mapTransactions with no test of its own, so one pass could append | ||
| 4487 | // the entire mempool: the overshoot was bounded by mempool size, | ||
| 4488 | // which is to say not bounded at all. Ask per transaction, before | ||
| 4489 | // appending, and the overshoot is zero rather than small. | ||
| 4490 | // | ||
| 4491 | // Seeded with what the block already weighs -- header, tx count, | ||
| 4492 | // coinbase -- because CheckBlock() weighs the whole serialized | ||
| 4493 | // block and not just the transactions we chose to add to it. | ||
| 4494 | unsigned int nBlockSize = ::GetSerializeSize(*pblock, SER_DISK); | ||
| 4495 | while (fFoundSomething && nBlockSize < MAX_BLOCK_SIZE_GEN) | ||
| 4487 | { | 4496 | { |
| 4488 | fFoundSomething = false; | 4497 | fFoundSomething = false; |
| 4489 | unsigned int n = 0; | 4498 | unsigned int n = 0; |
| @@ -4495,6 +4504,28 @@ bool BitcoinMiner(int nThreadId) | |||
| 4495 | if (tx.IsCoinBase() || !tx.IsFinal()) | 4504 | if (tx.IsCoinBase() || !tx.IsFinal()) |
| 4496 | continue; | 4505 | continue; |
| 4497 | 4506 | ||
| 4507 | // Skip, not break: a transaction too big for the room left | ||
| 4508 | // says nothing about the next one, which may still fit. | ||
| 4509 | // | ||
| 4510 | // This is also the only ceiling on a single transaction's | ||
| 4511 | // size anywhere on the path into a block. CheckTransaction | ||
| 4512 | // does not cap it and neither does AcceptTransaction, so | ||
| 4513 | // the mempool will hold one of any size the wire allows; | ||
| 4514 | // without this test a lone 1.5 MB transaction offering a | ||
| 4515 | // fat fee is selected, and every template built from that | ||
| 4516 | // mempool is one CheckBlock() throws away. The fee is never | ||
| 4517 | // paid, because the block never confirms. | ||
| 4518 | // | ||
| 4519 | // Deliberately before ConnectInputs and not after: | ||
| 4520 | // ConnectInputs adds this transaction's fee to nFees by | ||
| 4521 | // reference on success. Passing it and then declining the | ||
| 4522 | // transaction would credit the coinbase for a fee no | ||
| 4523 | // transaction in the block pays, and the block would be | ||
| 4524 | // rejected for a wholly different reason. | ||
| 4525 | unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); | ||
| 4526 | if (!BlockTemplateFits(nBlockSize, nTxSize)) | ||
| 4527 | continue; | ||
| 4528 | |||
| 4498 | // Transaction fee requirements, mainly only needed for flood control | 4529 | // Transaction fee requirements, mainly only needed for flood control |
| 4499 | // Under 10K (about 80 inputs) is free for first 100 transactions | 4530 | // Under 10K (about 80 inputs) is free for first 100 transactions |
| 4500 | // Base rate is 0.01 per KB | 4531 | // Base rate is 0.01 per KB |
| @@ -4506,7 +4537,7 @@ bool BitcoinMiner(int nThreadId) | |||
| 4506 | swap(mapTestPool, mapTestPoolTmp); | 4537 | swap(mapTestPool, mapTestPoolTmp); |
| 4507 | 4538 | ||
| 4508 | pblock->vtx.push_back(tx); | 4539 | pblock->vtx.push_back(tx); |
| 4509 | nBlockSize += ::GetSerializeSize(tx, SER_NETWORK); | 4540 | nBlockSize += nTxSize; |
| 4510 | vfAlreadyAdded[n] = true; | 4541 | vfAlreadyAdded[n] = true; |
| 4511 | fFoundSomething = true; | 4542 | fFoundSomething = true; |
| 4512 | } | 4543 | } |
| @@ -33,6 +33,26 @@ static const unsigned int MAX_BLOCK_SIGOPS = 20000; | |||
| 33 | // limit (32 MB), not a block rule. Bitcoin 0.1.0 predated the later 1 MB block | 33 | // limit (32 MB), not a block rule. Bitcoin 0.1.0 predated the later 1 MB block |
| 34 | // cap, so this tree inherited no real block-size consensus limit. | 34 | // cap, so this tree inherited no real block-size consensus limit. |
| 35 | static const unsigned int MAX_BLOCK_SIZE = 1000000; | 35 | static const unsigned int MAX_BLOCK_SIZE = 1000000; |
| 36 | // How much of that cap a template we build may fill. Half, which is what both | ||
| 37 | // assemblers already aimed at -- this only gives the number a name and one | ||
| 38 | // place to live. They had already drifted on how it was enforced, which is the | ||
| 39 | // bug this exists to stop repeating. | ||
| 40 | // | ||
| 41 | // NOT a consensus rule. CheckBlock() decides what the network accepts; this | ||
| 42 | // decides what we are willing to produce, and it has to stay under that. | ||
| 43 | static const unsigned int MAX_BLOCK_SIZE_GEN = MAX_BLOCK_SIZE / 2; | ||
| 44 | |||
| 45 | // Room for one more transaction in a template that currently weighs | ||
| 46 | // nBlockSize bytes? | ||
| 47 | // | ||
| 48 | // Asked BEFORE the transaction is appended, never after. A template that has | ||
| 49 | // already overshot is one CheckBlock() rejects, and asking afterwards only | ||
| 50 | // measures the waste. Both assemblers call this, so neither can drift into a | ||
| 51 | // different idea of what fits. | ||
| 52 | inline bool BlockTemplateFits(unsigned int nBlockSize, unsigned int nTxSize) | ||
| 53 | { | ||
| 54 | return (nBlockSize + nTxSize <= MAX_BLOCK_SIZE_GEN); | ||
| 55 | } | ||
| 36 | static const int64 COIN = 100000000; | 56 | static const int64 COIN = 100000000; |
| 37 | // Total Bitflash emission: identical to Bitcoin (21 million). | 57 | // Total Bitflash emission: identical to Bitcoin (21 million). |
| 38 | // MAX_MONEY guards against the value overflow bug (CVE-2010-5139), which in | 58 | // MAX_MONEY guards against the value overflow bug (CVE-2010-5139), which in |
diff --git a/src/rpc.cpp b/src/rpc.cpp index a4b53ca..7c5ef6c 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -267,16 +267,24 @@ static bool RebuildJob() | |||
| 267 | map<uint256, CTxIndex> pool; | 267 | map<uint256, CTxIndex> pool; |
| 268 | vector<char> vfAlreadyAdded(mapTransactions.size()); | 268 | vector<char> vfAlreadyAdded(mapTransactions.size()); |
| 269 | bool fFoundSomething = true; | 269 | bool fFoundSomething = true; |
| 270 | unsigned int sz = 0; | 270 | // The cap is MAX_BLOCK_SIZE_GEN, not MAX_SIZE/2. MAX_SIZE is |
| 271 | // The cap is MAX_BLOCK_SIZE/2, not MAX_SIZE/2. MAX_SIZE is the | 271 | // the 32 MB serialisation ceiling, so half of it is 16 MB -- |
| 272 | // 32 MB serialisation ceiling, so half of it is 16 MB -- this | 272 | // this builder would happily assemble a template that its own |
| 273 | // builder would happily assemble a template that its own | ||
| 274 | // CheckBlock() rejects at the 1 MB consensus cap. This is the | 273 | // CheckBlock() rejects at the 1 MB consensus cap. This is the |
| 275 | // pool's job builder, so a block over the cap is not one | 274 | // pool's job builder, so a block over the cap is not one |
| 276 | // wasted solve, it is every miner on the pool working a | 275 | // wasted solve, it is every miner on the pool working a |
| 277 | // template the network will reject. The inner break bounds the | 276 | // template the network will reject. |
| 278 | // overshoot to a single transaction within a pass. | 277 | // |
| 279 | while (fFoundSomething && sz < MAX_BLOCK_SIZE / 2) | 278 | // The inner break used to bound the overshoot to one |
| 279 | // transaction per pass. One transaction is still too many: | ||
| 280 | // nothing caps how big a single transaction may be, so that | ||
| 281 | // bound was worth up to MAX_SIZE. Asking whether each one fits | ||
| 282 | // before appending it bounds the overshoot at zero instead. | ||
| 283 | // | ||
| 284 | // Seeded with the header, tx count and coinbase, because | ||
| 285 | // CheckBlock() weighs the whole serialized block. | ||
| 286 | unsigned int sz = ::GetSerializeSize(block, SER_DISK); | ||
| 287 | while (fFoundSomething && sz < MAX_BLOCK_SIZE_GEN) | ||
| 280 | { | 288 | { |
| 281 | fFoundSomething = false; | 289 | fFoundSomething = false; |
| 282 | unsigned int n = 0; | 290 | unsigned int n = 0; |
| @@ -287,16 +295,24 @@ static bool RebuildJob() | |||
| 287 | continue; | 295 | continue; |
| 288 | CTransaction& tx = (*mi).second; | 296 | CTransaction& tx = (*mi).second; |
| 289 | if (tx.IsCoinBase() || !tx.IsFinal()) continue; | 297 | if (tx.IsCoinBase() || !tx.IsFinal()) continue; |
| 298 | // Skip rather than stop: one transaction too big for | ||
| 299 | // the room left does not mean the next is. Ordered | ||
| 300 | // before ConnectInputs because ConnectInputs credits | ||
| 301 | // nFees by reference on success, and a transaction we | ||
| 302 | // then decline would leave the coinbase claiming a fee | ||
| 303 | // this block does not collect. | ||
| 304 | unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); | ||
| 305 | if (!BlockTemplateFits(sz, nTxSize)) | ||
| 306 | continue; | ||
| 290 | map<uint256, CTxIndex> tmp(pool); | 307 | map<uint256, CTxIndex> tmp(pool); |
| 291 | if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, | 308 | if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, |
| 292 | false, true, tx.GetMinFee(block.vtx.size() < 100))) | 309 | false, true, tx.GetMinFee(block.vtx.size() < 100))) |
| 293 | continue; | 310 | continue; |
| 294 | swap(pool, tmp); | 311 | swap(pool, tmp); |
| 295 | block.vtx.push_back(tx); | 312 | block.vtx.push_back(tx); |
| 296 | sz += ::GetSerializeSize(tx, SER_NETWORK); | 313 | sz += nTxSize; |
| 297 | vfAlreadyAdded[n] = true; | 314 | vfAlreadyAdded[n] = true; |
| 298 | fFoundSomething = true; | 315 | fFoundSomething = true; |
| 299 | if (sz > MAX_BLOCK_SIZE / 2) break; | ||
| 300 | } | 316 | } |
| 301 | } | 317 | } |
| 302 | } | 318 | } |