upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/selftest.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 14:23:50 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 14:23:50 +0000
commitff6af901eaab61b6dbba41bb80f7bb2f40980880 (patch)
treef5d43b200dd7cfc165d29487c8a354fcd91fc85e /src/selftest.cpp
parent2ed8fae6088c99f1a5dfcfd760c1a24c2b2706db (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/selftest.cpp')
-rw-r--r--src/selftest.cpp157
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.
1241static 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
1252static 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.
1271static 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.
1289static 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
1238static int RunConsensusLimitsSelfTest() 1306static 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 {