diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 14:23:50 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 14:23:50 +0000 |
| commit | ff6af901eaab61b6dbba41bb80f7bb2f40980880 (patch) | |
| tree | f5d43b200dd7cfc165d29487c8a354fcd91fc85e /src | |
| parent | 2ed8fae6088c99f1a5dfcfd760c1a24c2b2706db (diff) | |
selftest: cover the assembly half of the block limits
consensus-limits only ever asked what CheckBlock() refuses. Nothing asked what
the assemblers produce, which is why a miner able to overshoot the cap by an
entire mempool sat here unnoticed through both the deployed and candidate
builds.
Adds the other half: a template built from a 3 MB mempool stays inside the
budget and passes CheckSizeLimits(); a transaction too big for the budget is
skipped while the ones that fit are still taken; sigop-heavy transactions stop
selection at MAX_BLOCK_SIGOPS while the block is still small. Plus the rule's
edges, including that sigops exactly on the limit fit -- CheckBlock() fails on
`>`, so assembly must accept what validation accepts.
The test drives BlockTemplateFits the way the assemblers drive it rather than
calling them: BitcoinMiner needs a CTxDB, a wallet key and cs_main, and
RebuildJob is static in rpc.cpp. What that proves is the shared rule and the
property it exists to give, which is where the defect was -- the budget was
right and the enforcement was not.
Checked against the bug: with the fit test neutered, eight of these fail,
including both templates that CheckBlock() would have rejected. With the sigop
comparison stricter than CheckBlock's by one, the boundary case fails.
Diffstat (limited to 'src')
| -rw-r--r-- | src/selftest.cpp | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/src/selftest.cpp b/src/selftest.cpp index 9f7106f..d11fdd8 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -1235,6 +1235,74 @@ static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) | |||
| 1235 | return block; | 1235 | return block; |
| 1236 | } | 1236 | } |
| 1237 | 1237 | ||
| 1238 | // A distinct transaction of a chosen weight and verification cost. nUnique | ||
| 1239 | // only keeps the txids apart, so a mempool built from these is not a pile of | ||
| 1240 | // duplicates a block could never carry anyway. | ||
| 1241 | static CTransaction MakeSizedTx(size_t nPadBytes, unsigned int nSigOps, int nUnique) | ||
| 1242 | { | ||
| 1243 | CTransaction tx; | ||
| 1244 | tx.vin.push_back(CTxIn(COutPoint(), CScript() << (int64)nUnique << 1)); | ||
| 1245 | tx.vout.push_back(CTxOut(0, CScript())); | ||
| 1246 | for (unsigned int i = 0; i < nSigOps; i++) | ||
| 1247 | tx.vout[0].scriptPubKey << OP_CHECKSIG; | ||
| 1248 | tx.vout[0].scriptPubKey.insert(tx.vout[0].scriptPubKey.end(), nPadBytes, 0); | ||
| 1249 | return tx; | ||
| 1250 | } | ||
| 1251 | |||
| 1252 | static CBlock MakeCoinbaseOnlyTemplate() | ||
| 1253 | { | ||
| 1254 | CTransaction txNew; | ||
| 1255 | txNew.vin.resize(1); | ||
| 1256 | txNew.vin[0].prevout.SetNull(); | ||
| 1257 | txNew.vin[0].scriptSig << 1 << 1 << 1; | ||
| 1258 | txNew.vout.resize(1); | ||
| 1259 | txNew.vout[0].scriptPubKey << OP_CHECKSIG; | ||
| 1260 | |||
| 1261 | CBlock block; | ||
| 1262 | block.vtx.push_back(txNew); | ||
| 1263 | block.nVersion = 1; | ||
| 1264 | block.nTime = GetAdjustedTime(); | ||
| 1265 | block.nBits = bnProofOfWorkLimit.GetCompact(); | ||
| 1266 | block.nNonce = 0; | ||
| 1267 | return block; | ||
| 1268 | } | ||
| 1269 | |||
| 1270 | // The sum CheckBlock() takes, spelled the same way. | ||
| 1271 | static unsigned int BlockSigOpCount(const CBlock& block) | ||
| 1272 | { | ||
| 1273 | unsigned int n = 0; | ||
| 1274 | for (int i = 0; i < (int)block.vtx.size(); i++) | ||
| 1275 | n += block.vtx[i].GetSigOpCount(); | ||
| 1276 | return n; | ||
| 1277 | } | ||
| 1278 | |||
| 1279 | // Drive the selection rule the way both assemblers drive it: ask | ||
| 1280 | // BlockTemplateFits before appending, skip what does not fit, never stop early | ||
| 1281 | // on one that does not. | ||
| 1282 | // | ||
| 1283 | // This mirrors the loops in BitcoinMiner() and RebuildJob() rather than calling | ||
| 1284 | // them -- one needs a CTxDB, a wallet key and cs_main, the other is static in | ||
| 1285 | // rpc.cpp, so neither is reachable from here. What that leaves proven is the | ||
| 1286 | // rule the two of them share and the property it is meant to guarantee, which | ||
| 1287 | // is where the defect actually lived: the budget was right and the enforcement | ||
| 1288 | // was not. | ||
| 1289 | static void SelectIntoTemplate(CBlock& block, const vector<CTransaction>& vMempool) | ||
| 1290 | { | ||
| 1291 | unsigned int nBlockSize = ::GetSerializeSize(block, SER_DISK); | ||
| 1292 | unsigned int nBlockSigOps = block.vtx[0].GetSigOpCount(); | ||
| 1293 | for (int i = 0; i < (int)vMempool.size(); i++) | ||
| 1294 | { | ||
| 1295 | const CTransaction& tx = vMempool[i]; | ||
| 1296 | unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); | ||
| 1297 | unsigned int nTxSigOps = tx.GetSigOpCount(); | ||
| 1298 | if (!BlockTemplateFits(nBlockSize, nBlockSigOps, nTxSize, nTxSigOps)) | ||
| 1299 | continue; | ||
| 1300 | block.vtx.push_back(tx); | ||
| 1301 | nBlockSize += nTxSize; | ||
| 1302 | nBlockSigOps += nTxSigOps; | ||
| 1303 | } | ||
| 1304 | } | ||
| 1305 | |||
| 1238 | static int RunConsensusLimitsSelfTest() | 1306 | static int RunConsensusLimitsSelfTest() |
| 1239 | { | 1307 | { |
| 1240 | fflush(stdout); | 1308 | fflush(stdout); |
| @@ -1258,6 +1326,95 @@ static int RunConsensusLimitsSelfTest() | |||
| 1258 | "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; | 1326 | "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; |
| 1259 | nFail += Check(!oversized.CheckSizeLimits(), | 1327 | nFail += Check(!oversized.CheckSizeLimits(), |
| 1260 | "a block above 1 MB is outside the consensus size limit") ? 0 : 1; | 1328 | "a block above 1 MB is outside the consensus size limit") ? 0 : 1; |
| 1329 | |||
| 1330 | // The other half of the cap. Everything above asks what validation | ||
| 1331 | // refuses; nothing asked what assembly produces, which is why a miner | ||
| 1332 | // that could overshoot the cap by an entire mempool went unnoticed. | ||
| 1333 | nFail += Check(MAX_BLOCK_SIZE_GEN < MAX_BLOCK_SIZE, | ||
| 1334 | "the assembly budget stays under the consensus cap") ? 0 : 1; | ||
| 1335 | |||
| 1336 | // The rule itself, at its edges. The sigop comparison has to match | ||
| 1337 | // CheckBlock(), which fails on `> MAX_BLOCK_SIGOPS` and so accepts a | ||
| 1338 | // block sitting exactly on the limit. | ||
| 1339 | nFail += Check(BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 100, 0), | ||
| 1340 | "a transaction that exactly fills the size budget fits") ? 0 : 1; | ||
| 1341 | nFail += Check(!BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 101, 0), | ||
| 1342 | "one byte past the size budget does not fit") ? 0 : 1; | ||
| 1343 | nFail += Check(BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS), | ||
| 1344 | "sigops exactly on the limit fit, as CheckBlock() allows") ? 0 : 1; | ||
| 1345 | nFail += Check(!BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS + 1), | ||
| 1346 | "one sigop past the limit does not fit") ? 0 : 1; | ||
| 1347 | |||
| 1348 | // A mempool several times the consensus cap, in ordinary-sized | ||
| 1349 | // transactions. The old miner tested its budget only on the outer | ||
| 1350 | // loop, so a single pass appended all of this. | ||
| 1351 | { | ||
| 1352 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 1353 | vector<CTransaction> vMempool; | ||
| 1354 | for (int i = 0; i < 300; i++) | ||
| 1355 | vMempool.push_back(MakeSizedTx(10000, 1, i)); | ||
| 1356 | SelectIntoTemplate(block, vMempool); | ||
| 1357 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 1358 | |||
| 1359 | nFail += Check(block.vtx.size() > 1, | ||
| 1360 | "a full mempool still fills a template") ? 0 : 1; | ||
| 1361 | nFail += Check(::GetSerializeSize(block, SER_DISK) <= MAX_BLOCK_SIZE_GEN, | ||
| 1362 | "a template built from a 3 MB mempool respects the budget") ? 0 : 1; | ||
| 1363 | nFail += Check(block.CheckSizeLimits(), | ||
| 1364 | "that template is one CheckBlock() would accept on size") ? 0 : 1; | ||
| 1365 | } | ||
| 1366 | |||
| 1367 | // One transaction too big for the budget. Nothing on the path into the | ||
| 1368 | // mempool caps transaction size, so this is a transaction a peer can | ||
| 1369 | // simply send. It must be skipped and the rest still taken -- skipped, | ||
| 1370 | // not stopped on. | ||
| 1371 | { | ||
| 1372 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 1373 | vector<CTransaction> vMempool; | ||
| 1374 | vMempool.push_back(MakeSizedTx(1500000, 1, 9999)); | ||
| 1375 | for (int i = 0; i < 10; i++) | ||
| 1376 | vMempool.push_back(MakeSizedTx(1000, 1, i)); | ||
| 1377 | |||
| 1378 | unsigned int nHugeSize = ::GetSerializeSize(vMempool[0], SER_NETWORK); | ||
| 1379 | nFail += Check(nHugeSize > MAX_BLOCK_SIZE && nHugeSize <= MAX_SIZE, | ||
| 1380 | "the outsized transaction is one the mempool would hold") ? 0 : 1; | ||
| 1381 | |||
| 1382 | SelectIntoTemplate(block, vMempool); | ||
| 1383 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 1384 | |||
| 1385 | bool fTookHuge = false; | ||
| 1386 | for (int i = 0; i < (int)block.vtx.size(); i++) | ||
| 1387 | if (block.vtx[i].GetHash() == vMempool[0].GetHash()) | ||
| 1388 | fTookHuge = true; | ||
| 1389 | |||
| 1390 | nFail += Check(!fTookHuge, | ||
| 1391 | "a transaction larger than the budget is left out") ? 0 : 1; | ||
| 1392 | nFail += Check(block.vtx.size() == 11, | ||
| 1393 | "the ten that do fit are taken anyway") ? 0 : 1; | ||
| 1394 | nFail += Check(block.CheckSizeLimits(), | ||
| 1395 | "one outsized transaction cannot invalidate a template") ? 0 : 1; | ||
| 1396 | } | ||
| 1397 | |||
| 1398 | // Transactions that are cheap in bytes and expensive to verify. Size | ||
| 1399 | // alone does not bound what a block costs, and CheckBlock() refuses on | ||
| 1400 | // the count regardless of how small the block is. | ||
| 1401 | { | ||
| 1402 | CBlock block = MakeCoinbaseOnlyTemplate(); | ||
| 1403 | vector<CTransaction> vMempool; | ||
| 1404 | for (int i = 0; i < 100; i++) | ||
| 1405 | vMempool.push_back(MakeSizedTx(0, 1000, i)); | ||
| 1406 | SelectIntoTemplate(block, vMempool); | ||
| 1407 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 1408 | |||
| 1409 | nFail += Check(block.vtx.size() > 1, | ||
| 1410 | "sigop-heavy transactions are still selected") ? 0 : 1; | ||
| 1411 | nFail += Check(BlockSigOpCount(block) <= MAX_BLOCK_SIGOPS, | ||
| 1412 | "selection stops at the sigop cap CheckBlock() enforces") ? 0 : 1; | ||
| 1413 | nFail += Check(::GetSerializeSize(block, SER_DISK) < MAX_BLOCK_SIZE_GEN, | ||
| 1414 | "and stops on sigops while still well inside the size budget") ? 0 : 1; | ||
| 1415 | nFail += Check(block.CheckSizeLimits(), | ||
| 1416 | "the sigop-bounded template is within the size limit too") ? 0 : 1; | ||
| 1417 | } | ||
| 1261 | } | 1418 | } |
| 1262 | catch (const std::exception& e) | 1419 | catch (const std::exception& e) |
| 1263 | { | 1420 | { |