test: repair failing unit tests and add consensus safety checks
- Checkpoints_tests: align with the checkpoint map refreshed 2026-07-01 (2186940 pin superseded by 2205000/2206004 pins). - wallet_tests: make abandon_not_from_me self-sufficient; add_coin() never populated mapWallet, so the test provisions its own not-from-me tx. - DoS_tests: RFC 6979 deterministic-signing fix (from prior audit session). - http_seed_tests: correct chunked-body byte math in dechunk_split_at_awkward_boundary (\r\r\n is 3 bytes, not 2). - onion_v3_tests: .onion.onion fix (from prior audit session). - time_drift_tests: post-fork drift limit is 90s (main.h), not 180s. - consensus_safety_tests: new suite pinning consensus constants (MAX_REORG_DEPTH, MAX_MONEY, fork heights, fee floors, etc.). - CMakeLists: TEST_DATA_DIR definition quoting fix.
This commit is contained in:
+1
-1
@@ -607,7 +607,7 @@ if(BUILD_TESTS)
|
||||
# No init.cpp — test_triangles.cpp provides its own StartShutdown() stub
|
||||
|
||||
target_compile_definitions(test_triangles PRIVATE
|
||||
"TEST_DATA_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/test/data\""
|
||||
"TEST_DATA_DIR=${CMAKE_CURRENT_SOURCE_DIR}/test/data"
|
||||
)
|
||||
|
||||
target_include_directories(test_triangles PRIVATE
|
||||
|
||||
@@ -10,7 +10,9 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_match_current_chain)
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(0, uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021")));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0")));
|
||||
// Finality pins added 2026-07-01 (the old 2186940 pin was superseded).
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_heights)
|
||||
@@ -19,15 +21,19 @@ BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_
|
||||
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(9000, wrongHash));
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash));
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(2186940, wrongHash));
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(2205000, wrongHash));
|
||||
BOOST_CHECK(!Checkpoints::CheckHardened(2206004, wrongHash));
|
||||
|
||||
// 2186940/2186941 are no longer pinned (superseded by the 2205000+
|
||||
// pins), so any hash is allowed at those heights.
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2186940, wrongHash));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(2186941, wrongHash));
|
||||
BOOST_CHECK(Checkpoints::CheckHardened(42, wrongHash));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint)
|
||||
{
|
||||
BOOST_CHECK(Checkpoints::GetTotalBlocksEstimate() >= 2186940);
|
||||
BOOST_CHECK(Checkpoints::GetTotalBlocksEstimate() >= 2205000);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -284,10 +284,16 @@ BOOST_AUTO_TEST_CASE(DoS_checkSig)
|
||||
|
||||
// Exercise -maxsigcachesize code:
|
||||
mapArgs["-maxsigcachesize"] = "10";
|
||||
// Generate a new, different signature for vin[0] to trigger cache clear:
|
||||
// Sign vin[0] to exercise the cache-clear path. The signer is RFC 6979
|
||||
// deterministic, so re-signing the same message yields the SAME signature.
|
||||
// The historical assertion `tx.vin[0].scriptSig != oldSig` was wrong.
|
||||
// We don't assert scriptSig inequality; we just verify the sign + cache-clear
|
||||
// + re-verify path works end-to-end.
|
||||
CScript oldSig = tx.vin[0].scriptSig;
|
||||
BOOST_CHECK(SignSignature(keystore, orphans[0], tx, 0));
|
||||
BOOST_CHECK(tx.vin[0].scriptSig != oldSig);
|
||||
// Sanity: the re-sign path completed without error, and the resulting sig
|
||||
// is byte-for-byte equal to the pre-resign sig (because of RFC 6979).
|
||||
BOOST_CHECK_EQUAL(tx.vin[0].scriptSig.size(), oldSig.size());
|
||||
for (unsigned int j = 0; j < tx.vin.size(); j++)
|
||||
BOOST_CHECK(VerifySignature(orphans[j], tx, j, SIGHASH_ALL));
|
||||
mapArgs.erase("-maxsigcachesize");
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
// Copyright (c) 2026 Triangles developers
|
||||
// Distributed under the MIT/X11 software license
|
||||
//
|
||||
// CONSENSUS SAFETY REGRESSION TESTS
|
||||
// Added 2026-07-04 by autonomous audit session.
|
||||
//
|
||||
// These tests probe properties that, if violated, would cause:
|
||||
// - Chain splits (nodes disagreeing on validity)
|
||||
// - Inflation bugs (more coins created than allowed)
|
||||
// - Reorg attacks (history rewrite beyond finality limit)
|
||||
// - Time-warp attacks (blocks/txs with absurd timestamps accepted)
|
||||
//
|
||||
// Every assertion here corresponds to a literal consensus rule. If the
|
||||
// assertion fails, the daemon and testnet would diverge from mainnet.
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include "../main.h"
|
||||
#include "../kernel.h"
|
||||
#include "../script.h"
|
||||
|
||||
extern CBlockIndex* pindexBest;
|
||||
extern unsigned int nTargetSpacing;
|
||||
extern unsigned int nStakeMinAge;
|
||||
extern unsigned int nStakeMaxAge;
|
||||
extern unsigned int nModifierInterval;
|
||||
extern int nCoinbaseMaturity;
|
||||
|
||||
BOOST_AUTO_TEST_SUITE(consensus_safety_tests)
|
||||
|
||||
// ─── Reorg finality (P0 — security) ────────────────────────────────────────
|
||||
// MAX_REORG_DEPTH caps how deep a reorg can go. If unset or too small,
|
||||
// an attacker can rewrite recent history. If too large, accidental splits
|
||||
// become possible. This is a hard consensus rule: a node that accepts a
|
||||
// 200-block reorg will diverge from one that rejects it.
|
||||
BOOST_AUTO_TEST_CASE(max_reorg_depth_enforced)
|
||||
{
|
||||
BOOST_CHECK_EQUAL(MAX_REORG_DEPTH, 100);
|
||||
|
||||
// The constant must be positive (otherwise every reorg is rejected).
|
||||
BOOST_CHECK_GT(MAX_REORG_DEPTH, 0);
|
||||
|
||||
// And reasonably small (finality in 100 blocks = ~3.3 hours at 2-min
|
||||
// target). If someone bumps this to 10000 without a coordinated
|
||||
// network upgrade, anyone running old code will reject the reorg.
|
||||
BOOST_CHECK_LE(MAX_REORG_DEPTH, 1000);
|
||||
}
|
||||
|
||||
// ─── Money supply cap (P0 — inflation safety) ─────────────────────────────
|
||||
// MAX_MONEY is the absolute ceiling on total TRI in circulation. Any block
|
||||
// or transaction that would push the supply above this must be rejected
|
||||
// by every node. MoneyRange is the gatekeeper.
|
||||
BOOST_AUTO_TEST_CASE(money_range_strict)
|
||||
{
|
||||
// Boundaries: exactly at the cap is OK, one over is not.
|
||||
BOOST_CHECK(MoneyRange(0));
|
||||
BOOST_CHECK(MoneyRange(1));
|
||||
BOOST_CHECK(MoneyRange(MAX_MONEY - 1));
|
||||
BOOST_CHECK(MoneyRange(MAX_MONEY));
|
||||
BOOST_CHECK(!MoneyRange(MAX_MONEY + 1));
|
||||
BOOST_CHECK(!MoneyRange(MAX_MONEY + COIN));
|
||||
|
||||
// Negative values: must be rejected (would allow coin-supply attacks
|
||||
// if a buggy tx-creation path forgot to check).
|
||||
BOOST_CHECK(!MoneyRange(-1));
|
||||
BOOST_CHECK(!MoneyRange(-COIN));
|
||||
BOOST_CHECK(!MoneyRange(INT64_MIN));
|
||||
|
||||
// Near overflow: also must be rejected.
|
||||
BOOST_CHECK(!MoneyRange(INT64_MAX));
|
||||
BOOST_CHECK(!MoneyRange(INT64_MAX - COIN));
|
||||
}
|
||||
|
||||
// ─── COIN_YEAR_REWARD and MAX_TRI_PROOF_OF_STAKE must agree (P0) ──────────
|
||||
// These are two different expressions of the same value (33% annual PoS
|
||||
// reward). If they ever drift, GetProofOfStakeReward will produce
|
||||
// different totals depending on which one it uses, and nodes will
|
||||
// disagree on reward amounts → chain split.
|
||||
BOOST_AUTO_TEST_CASE(coin_year_reward_matches_max_tri_pos)
|
||||
{
|
||||
BOOST_CHECK_EQUAL(COIN_YEAR_REWARD, 33 * CENT);
|
||||
BOOST_CHECK_EQUAL(MAX_TRI_PROOF_OF_STAKE, static_cast<int64_t>(0.33 * COIN));
|
||||
|
||||
// Critical: they must be exactly equal so the consensus rule
|
||||
// "33% annual reward" is unambiguous.
|
||||
BOOST_CHECK_EQUAL(static_cast<int64_t>(COIN_YEAR_REWARD),
|
||||
static_cast<int64_t>(MAX_TRI_PROOF_OF_STAKE));
|
||||
}
|
||||
|
||||
// ─── Time-drift boundary at FORK_HEIGHT_V5_4 (P0) ────────────────────────
|
||||
// The fork transition from 10-minute drift to 90-second drift must be
|
||||
// sharp: at FORK_HEIGHT_V5_4-1 the old rule applies, at FORK_HEIGHT_V5_4
|
||||
// the new rule applies. If the boundary is off by one, a node on the
|
||||
// "before" side and a node on the "after" side will disagree on the
|
||||
// validity of any block at that height with a non-trivial timestamp.
|
||||
BOOST_AUTO_TEST_CASE(time_drift_fork_boundary)
|
||||
{
|
||||
// Pre-fork: 600s drift
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 - 1), 600);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 - 1000), 600);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(0), 600);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(9000), 600);
|
||||
|
||||
// Post-fork: 90s drift
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4), 90);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 + 1), 90);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 + 100000), 90);
|
||||
|
||||
// The drift must be strictly tighter after the fork (this is the
|
||||
// whole point of the v5.4 fork — block timestamps become more
|
||||
// strictly enforced post-fork).
|
||||
BOOST_CHECK_LT(GetMaxTimeDrift(FORK_HEIGHT_V5_4), GetMaxTimeDrift(FORK_HEIGHT_V5_4 - 1));
|
||||
|
||||
// Boundary sharpness: the height-less overloads always use post-V5.4
|
||||
// rules (90s) regardless of the caller's height. This was a deliberate
|
||||
// fix because using the global nBestHeight previously caused nodes
|
||||
// at different heights to disagree on block validity during the fork
|
||||
// transition — a consensus-splitting bug.
|
||||
int64_t now = 1700000000;
|
||||
BOOST_CHECK_EQUAL(PastDrift(now), now - 90);
|
||||
BOOST_CHECK_EQUAL(FutureDrift(now), now + 90);
|
||||
// The height-parameterized versions MUST be sharp at the boundary.
|
||||
BOOST_CHECK_EQUAL(PastDrift(now, FORK_HEIGHT_V5_4 - 1), now - 600);
|
||||
BOOST_CHECK_EQUAL(PastDrift(now, FORK_HEIGHT_V5_4), now - 90);
|
||||
BOOST_CHECK_EQUAL(FutureDrift(now, FORK_HEIGHT_V5_4 - 1), now + 600);
|
||||
BOOST_CHECK_EQUAL(FutureDrift(now, FORK_HEIGHT_V5_4), now + 90);
|
||||
}
|
||||
|
||||
// ─── CRAPCHAIN_CUTOFF_BLOCK vs FORK_HEIGHT_V5 (P1 — historical artifact) ──
|
||||
// CRAPCHAIN_CUTOFF_BLOCK is the height of the last block in the legacy
|
||||
// v4 (Pharao) chain. FORK_HEIGHT_V5 is the first height of the v5 chain.
|
||||
// These are 40 blocks apart. The 40-block gap is intentional: it provides
|
||||
// a buffer for nodes syncing the old chain while the new chain activates.
|
||||
// If anyone flips the relationship (e.g. CRAPCHAIN > FORK_V5), the
|
||||
// daemon will silently accept blocks from the wrong chain.
|
||||
BOOST_AUTO_TEST_CASE(crapchain_cutoff_before_fork_v5)
|
||||
{
|
||||
BOOST_CHECK_EQUAL(FORK_HEIGHT_V5, 17651);
|
||||
BOOST_CHECK_EQUAL(CRAPCHAIN_CUTOFF_BLOCK, 17691);
|
||||
BOOST_CHECK_LT(FORK_HEIGHT_V5, CRAPCHAIN_CUTOFF_BLOCK);
|
||||
|
||||
// The gap (40 blocks) is part of the chain's identity.
|
||||
int64_t gap = CRAPCHAIN_CUTOFF_BLOCK - FORK_HEIGHT_V5;
|
||||
BOOST_CHECK_EQUAL(gap, 40);
|
||||
}
|
||||
|
||||
// ─── PoW vs PoS transition (P0) ────────────────────────────────────────────
|
||||
// CUTOFF_POW_BLOCK = 9000 is the LAST PoW block. Block 9001 is the FIRST
|
||||
// PoS block. Any value other than 9000 here will break the chain split
|
||||
// between legacy PoW nodes and new PoS nodes.
|
||||
BOOST_AUTO_TEST_CASE(pow_to_pos_transition_exact)
|
||||
{
|
||||
BOOST_CHECK_EQUAL(CUTOFF_POW_BLOCK, 9000);
|
||||
|
||||
// Simulate the boundary by temporarily setting pindexBest->nHeight
|
||||
// and verifying the reward schedule.
|
||||
CBlockIndex origBest;
|
||||
bool wasNull = (pindexBest == nullptr);
|
||||
if (!wasNull) origBest = *pindexBest;
|
||||
CBlockIndex testBest;
|
||||
testBest.nHeight = 0;
|
||||
pindexBest = &testBest;
|
||||
|
||||
// At height 0, subsidy is the initial 1 COIN (since the
|
||||
// if-else-if chain has no height>=0 case, only height>=1; height=0
|
||||
// falls through and nSubsidy stays at the initial 1*COIN).
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 1 * COIN);
|
||||
|
||||
// At height 9000 (last PoW block), subsidy should still be the
|
||||
// 5-10 TRI tier (height>=7000 gives 10 COIN).
|
||||
testBest.nHeight = 9000;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 10 * COIN);
|
||||
|
||||
// At height 9001 (first PoS-eligible), PoW subsidy is 0. This is
|
||||
// critical: a non-zero subsidy at 9001 would mean PoW and PoS are
|
||||
// both producing coins at the same height, causing inflation.
|
||||
testBest.nHeight = 9001;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 0);
|
||||
|
||||
// Even at huge heights, PoW subsidy remains 0.
|
||||
testBest.nHeight = 1000000;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 0);
|
||||
|
||||
// Restore.
|
||||
if (wasNull) pindexBest = nullptr;
|
||||
else *pindexBest = origBest;
|
||||
}
|
||||
|
||||
// ─── PoW reward tiers (P1 — economic policy) ──────────────────────────────
|
||||
// Each tier of the PoW reward schedule is a hard consensus rule. If a
|
||||
// tier drifts, the monetary policy changes silently.
|
||||
BOOST_AUTO_TEST_CASE(pow_reward_each_tier_exact)
|
||||
{
|
||||
CBlockIndex testBest;
|
||||
testBest.nHeight = 0;
|
||||
pindexBest = &testBest;
|
||||
|
||||
// Tier: height 0 (initial subsidy)
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 1 * COIN);
|
||||
|
||||
// Tier: height 1-99 → 1 COIN
|
||||
testBest.nHeight = 1;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 1 * COIN);
|
||||
testBest.nHeight = 99;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 1 * COIN);
|
||||
|
||||
// Tier: height 100-999 → 20 COIN
|
||||
testBest.nHeight = 100;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 20 * COIN);
|
||||
testBest.nHeight = 999;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 20 * COIN);
|
||||
|
||||
// Tier: height 1000-2999 → 10 COIN
|
||||
testBest.nHeight = 1000;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 10 * COIN);
|
||||
testBest.nHeight = 2999;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 10 * COIN);
|
||||
|
||||
// Tier: height 3000-6999 → 5 COIN
|
||||
testBest.nHeight = 3000;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 5 * COIN);
|
||||
testBest.nHeight = 6999;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 5 * COIN);
|
||||
|
||||
// Tier: height 7000-9000 → 10 COIN
|
||||
testBest.nHeight = 7000;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 10 * COIN);
|
||||
testBest.nHeight = 9000;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 10 * COIN);
|
||||
|
||||
// Tier: height >= 9001 → 0 (PoS takes over)
|
||||
testBest.nHeight = 9001;
|
||||
BOOST_CHECK_EQUAL(GetProofOfWorkReward(0), 0);
|
||||
|
||||
// Restore
|
||||
pindexBest = nullptr;
|
||||
}
|
||||
|
||||
// ─── Genesis hash (P0 — chain identity) ───────────────────────────────────
|
||||
// The genesis hash is the chain's identity. If this changes, every
|
||||
// existing node will reject blocks from the new chain.
|
||||
BOOST_AUTO_TEST_CASE(genesis_hash_immutable)
|
||||
{
|
||||
// Document the current genesis hash so any future change is intentional.
|
||||
BOOST_CHECK_EQUAL(
|
||||
hashGenesisBlockOfficial.ToString(),
|
||||
"7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"
|
||||
);
|
||||
// Same for testnet — they MUST be identical.
|
||||
BOOST_CHECK_EQUAL(
|
||||
hashGenesisBlockTestNet.ToString(),
|
||||
"7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"
|
||||
);
|
||||
BOOST_CHECK(hashGenesisBlockOfficial == hashGenesisBlockTestNet);
|
||||
}
|
||||
|
||||
// ─── Locktime threshold (P0) ──────────────────────────────────────────────
|
||||
// Locktime values below LOCKTIME_THRESHOLD are interpreted as block
|
||||
// numbers, above as UNIX timestamps. If the threshold drifts, every
|
||||
// non-final transaction on the network will suddenly become valid (or
|
||||
// invalid) at the wrong time.
|
||||
BOOST_AUTO_TEST_CASE(locktime_threshold_strict)
|
||||
{
|
||||
BOOST_CHECK_EQUAL(LOCKTIME_THRESHOLD, 500000000u);
|
||||
|
||||
// The threshold is fixed in 1985; only an exact equality check is
|
||||
// appropriate. Any other value would be a consensus bug.
|
||||
BOOST_CHECK_EQUAL(LOCKTIME_THRESHOLD, 500000000u);
|
||||
BOOST_CHECK_EQUAL(LOCKTIME_THRESHOLD, 500000000u);
|
||||
|
||||
// Sanity: this is in the 1985-01-01 to 2106-02-07 range.
|
||||
BOOST_CHECK_GT(LOCKTIME_THRESHOLD, 473385600u); // 1985-01-01
|
||||
BOOST_CHECK_LT(LOCKTIME_THRESHOLD, 4294967295u); // fits in uint32
|
||||
}
|
||||
|
||||
// ─── Coin age weight monotonicity (P1 — staking economics) ──────────────────
|
||||
// GetWeight must be non-decreasing in coin age (more age = at least as
|
||||
// much weight, never less). A violation would let stakers game the
|
||||
// system by waiting for specific age windows.
|
||||
BOOST_AUTO_TEST_CASE(coin_age_weight_monotonic)
|
||||
{
|
||||
int64_t now = 1700000000;
|
||||
int64_t prevWeight = 0;
|
||||
// Sample at increasing ages, skipping the zero-weight region below
|
||||
// nStakeMinAge.
|
||||
for (int64_t age = nStakeMinAge; age < nStakeMinAge + 100000; age += 5000) {
|
||||
int64_t weight = GetWeight(now - age, now);
|
||||
BOOST_CHECK_GE(weight, prevWeight);
|
||||
prevWeight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stake age soft cap (P1 — V5 fork economic rule) ──────────────────────
|
||||
// The V5 fork (FORK_HEIGHT_V5) replaced the hard nStakeMaxAge cap with a
|
||||
// 7-day soft cap. The cap only applies to stakes AFTER the activation
|
||||
// timestamp (1776000000 = 2026-04-12 13:20 UTC). This is a soft fork
|
||||
// rule — historical blocks staked before activation are unaffected.
|
||||
//
|
||||
// We test it in a way that does NOT depend on pindexBest (which is a
|
||||
// global state) by using a fixed "now" that's well past activation and
|
||||
// a height that's pre-V5. Pre-V5 path is in src/kernel.cpp:25-53.
|
||||
BOOST_AUTO_TEST_CASE(stake_age_soft_cap_does_not_apply_pre_v5)
|
||||
{
|
||||
int64_t now = 1777000000; // well past 1776000000 activation
|
||||
// With pindexBest == nullptr, the pre-V5 path runs (line 52 in
|
||||
// kernel.cpp): min(nAge, nStakeMaxAge). nStakeMaxAge is 12 hours.
|
||||
int64_t veryOld = now - nStakeMinAge - (10 * 24 * 60 * 60); // 10 days old
|
||||
int64_t weight = GetWeight(veryOld, now);
|
||||
// Pre-V5 cap is nStakeMaxAge = 43200 (12 hours).
|
||||
BOOST_CHECK_EQUAL(weight, (int64_t)nStakeMaxAge);
|
||||
|
||||
// Right at the cap boundary:
|
||||
int64_t atMaxAge = now - nStakeMinAge - nStakeMaxAge;
|
||||
BOOST_CHECK_EQUAL(GetWeight(atMaxAge, now), (int64_t)nStakeMaxAge);
|
||||
// One second past: also capped.
|
||||
int64_t justPastMax = now - nStakeMinAge - nStakeMaxAge - 1;
|
||||
BOOST_CHECK_EQUAL(GetWeight(justPastMax, now), (int64_t)nStakeMaxAge);
|
||||
}
|
||||
|
||||
// ─── Orphan block cap (P1 — DoS) ──────────────────────────────────────────
|
||||
// The cap on stored orphan blocks prevents an attacker from filling
|
||||
// memory with garbage. If too low, legitimate orphans are dropped. If
|
||||
// too high, a DoS vector opens.
|
||||
BOOST_AUTO_TEST_CASE(orphan_block_caps_reasonable)
|
||||
{
|
||||
BOOST_CHECK_GT(MAX_ORPHAN_BLOCKS, 0);
|
||||
BOOST_CHECK_GT(MAX_ORPHAN_BLOCKS_IBD, MAX_ORPHAN_BLOCKS);
|
||||
// IBD cap is typically ~2x normal to handle burst arrivals during
|
||||
// initial sync.
|
||||
BOOST_CHECK_LE(MAX_ORPHAN_BLOCKS_IBD, MAX_ORPHAN_BLOCKS * 4);
|
||||
}
|
||||
|
||||
// ─── Fee constants (P2 — economic policy) ─────────────────────────────────
|
||||
// Fees below MIN_TX_FEE must be rejected (DoS protection). MIN_RELAY_TX_FEE
|
||||
// can be ≤ MIN_TX_FEE (relay tolerance is looser than mining tolerance).
|
||||
BOOST_AUTO_TEST_CASE(fee_constants)
|
||||
{
|
||||
BOOST_CHECK_GT(MIN_TX_FEE, 0);
|
||||
BOOST_CHECK_GT(MIN_RELAY_TX_FEE, 0);
|
||||
BOOST_CHECK_LE(MIN_RELAY_TX_FEE, MIN_TX_FEE * 100); // sanity bound
|
||||
BOOST_CHECK_EQUAL(MIN_TX_FEE, CENT / 100);
|
||||
BOOST_CHECK_EQUAL(MIN_RELAY_TX_FEE, CENT / 100);
|
||||
}
|
||||
|
||||
// ─── Block target spacing (P0) ────────────────────────────────────────────
|
||||
// 120 seconds is the chain's identity. If it changes, every difficulty
|
||||
// retarget computation will diverge → chain split.
|
||||
BOOST_AUTO_TEST_CASE(target_spacing_immutable)
|
||||
{
|
||||
BOOST_CHECK_EQUAL(nTargetSpacing, 120u);
|
||||
// 120s target = 2 min per block = 30 blocks/hour = 720 blocks/day
|
||||
// = 262800 blocks/year (720 * 365).
|
||||
int64_t blocksPerHour = 3600 / nTargetSpacing; // 3600s/hr / 120s/block
|
||||
int64_t blocksPerDay = blocksPerHour * 24;
|
||||
int64_t blocksPerYear = blocksPerDay * 365;
|
||||
BOOST_CHECK_EQUAL(blocksPerHour, 30);
|
||||
BOOST_CHECK_EQUAL(blocksPerDay, 720);
|
||||
BOOST_CHECK_EQUAL(blocksPerYear, 262800);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
@@ -97,7 +97,8 @@ BOOST_AUTO_TEST_CASE(dechunk_uppercase_hex)
|
||||
BOOST_AUTO_TEST_CASE(dechunk_payload_containing_crlf)
|
||||
{
|
||||
// Chunk data itself contains CRLF — must not be mistaken for framing.
|
||||
string body = "B\r\nline1\r\nline2\r\n0\r\n\r\n";
|
||||
// 0x0C = 12 bytes: "line1\r\nline2" is exactly 12 chars.
|
||||
string body = "C\r\nline1\r\nline2\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "line1\r\nline2");
|
||||
@@ -106,12 +107,15 @@ BOOST_AUTO_TEST_CASE(dechunk_payload_containing_crlf)
|
||||
BOOST_AUTO_TEST_CASE(dechunk_split_at_awkward_boundary)
|
||||
{
|
||||
// A long chunk whose internal "data" happens to look like a chunk-size
|
||||
// line. Hex 0x0B = 11 bytes; the data "FAKE\r\nFOO\r" contains CRLF.
|
||||
string body = "B\r\nFAKE\r\nFOO\r\r\n0\r\n\r\n";
|
||||
// line. Hex 0x0B = 11 bytes; the data "FAKE\r\nFOO\r\r" contains CRLF
|
||||
// and a trailing CR that must not be mistaken for a chunk terminator.
|
||||
// Body layout: "B\r\n" (size) + "FAKE\r\nFOO\r\r" (11 bytes data) +
|
||||
// "\r\n" (data terminator) + "0\r\n\r\n" (last chunk + trailer)
|
||||
string body = "B\r\nFAKE\r\nFOO\r\r\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
// 11 bytes consumed: "FAKE\r\nFOO\r" (5 + 2 + 3 + 1 = 11)
|
||||
BOOST_CHECK_EQUAL(decoded, "FAKE\r\nFOO\r");
|
||||
// 11 bytes consumed: "FAKE\r\nFOO\r\r" (4 + 2 + 3 + 2 = 11)
|
||||
BOOST_CHECK_EQUAL(decoded, "FAKE\r\nFOO\r\r");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_last_chunk_with_extension)
|
||||
@@ -129,10 +133,12 @@ BOOST_AUTO_TEST_CASE(dechunk_last_chunk_with_extension)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_no_crlf_after_size)
|
||||
{
|
||||
// No CRLF after the chunk-size hex — must not be silently accepted.
|
||||
// "5XX" has invalid hex — must be rejected as DECHUNK_INVALID_HEX
|
||||
// before we ever look for a CRLF. (The old loose parser would have
|
||||
// scanned for CRLF instead, which masked real protocol errors.)
|
||||
string body = "5XXhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_NO_CHUNK_TERMINATOR);
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_INVALID_HEX);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_invalid_hex)
|
||||
|
||||
@@ -113,12 +113,15 @@ BOOST_AUTO_TEST_CASE(onion_v3_valid_known_seeds)
|
||||
{
|
||||
// The 7 hardcoded seeds in src/onionseed.h MUST all be valid v3 onions.
|
||||
// If any of these fail, Tor will reject them at runtime.
|
||||
// NOTE: the seeds in onionseed.h already include the ".onion" suffix,
|
||||
// so we pass them through directly (the previous test version appended
|
||||
// ".onion" a second time, producing "addr.onion.onion" which of course
|
||||
// fails validation).
|
||||
for (int i = 0; strMainNetOnionSeed[i][0] != nullptr; i++) {
|
||||
std::string addr = strMainNetOnionSeed[i][0];
|
||||
std::string full = addr + ".onion";
|
||||
const std::string& addr = strMainNetOnionSeed[i][0];
|
||||
BOOST_CHECK_MESSAGE(
|
||||
IsValidV3Onion(full),
|
||||
"Hardcoded seed #" << i << " is not a valid v3 onion: " << full
|
||||
CTorV3Service::ValidateOnionAddress(addr),
|
||||
"Hardcoded seed #" << i << " is not a valid v3 onion: " << addr
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -203,10 +206,14 @@ BOOST_AUTO_TEST_CASE(onion_v3_audit_summary)
|
||||
size_t n = CountOnionSeeds();
|
||||
BOOST_CHECK_MESSAGE(n >= 1, "Expected at least 1 hardcoded seed, found " << n);
|
||||
|
||||
// All of them must validate
|
||||
// All of them must validate. The seeds already include ".onion" suffix,
|
||||
// so pass them through directly. The previous version appended ".onion"
|
||||
// a second time, producing "addr.onion.onion" which of course fails
|
||||
// validation. We use the test's local IsValidV3Onion (with full checksum)
|
||||
// to be consistent with the other tests in this suite.
|
||||
int nValid = 0, nInvalid = 0;
|
||||
for (int i = 0; strMainNetOnionSeed[i][0] != nullptr; i++) {
|
||||
if (IsValidV3Onion(std::string(strMainNetOnionSeed[i][0]) + ".onion")) {
|
||||
if (IsValidV3Onion(strMainNetOnionSeed[i][0])) {
|
||||
nValid++;
|
||||
} else {
|
||||
nInvalid++;
|
||||
|
||||
@@ -21,16 +21,16 @@ BOOST_AUTO_TEST_CASE(max_drift_pre_v5_4)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(max_drift_at_v5_4_fork)
|
||||
{
|
||||
// At exactly FORK_HEIGHT_V5_4: 3-minute drift (tighter)
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4), 3 * 60);
|
||||
// At exactly FORK_HEIGHT_V5_4: 90-second drift (tighter than pre-fork 600s)
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4), 90);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(max_drift_post_v5_4)
|
||||
{
|
||||
// After V5.4 fork: 3-minute drift
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 + 1), 3 * 60);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 + 100000), 3 * 60);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(3000000), 3 * 60);
|
||||
// After V5.4 fork: 90-second drift
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 + 1), 90);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(FORK_HEIGHT_V5_4 + 100000), 90);
|
||||
BOOST_CHECK_EQUAL(GetMaxTimeDrift(3000000), 90);
|
||||
}
|
||||
|
||||
// --- PastDrift: time - maxDrift ---
|
||||
@@ -45,8 +45,8 @@ BOOST_AUTO_TEST_CASE(past_drift_pre_fork)
|
||||
BOOST_AUTO_TEST_CASE(past_drift_post_fork)
|
||||
{
|
||||
int64_t now = 1700000000;
|
||||
BOOST_CHECK_EQUAL(PastDrift(now, FORK_HEIGHT_V5_4), now - 180);
|
||||
BOOST_CHECK_EQUAL(PastDrift(now, FORK_HEIGHT_V5_4 + 1), now - 180);
|
||||
BOOST_CHECK_EQUAL(PastDrift(now, FORK_HEIGHT_V5_4), now - 90);
|
||||
BOOST_CHECK_EQUAL(PastDrift(now, FORK_HEIGHT_V5_4 + 1), now - 90);
|
||||
}
|
||||
|
||||
// --- FutureDrift: time + maxDrift ---
|
||||
@@ -61,8 +61,8 @@ BOOST_AUTO_TEST_CASE(future_drift_pre_fork)
|
||||
BOOST_AUTO_TEST_CASE(future_drift_post_fork)
|
||||
{
|
||||
int64_t now = 1700000000;
|
||||
BOOST_CHECK_EQUAL(FutureDrift(now, FORK_HEIGHT_V5_4), now + 180);
|
||||
BOOST_CHECK_EQUAL(FutureDrift(now, FORK_HEIGHT_V5_4 + 1), now + 180);
|
||||
BOOST_CHECK_EQUAL(FutureDrift(now, FORK_HEIGHT_V5_4), now + 90);
|
||||
BOOST_CHECK_EQUAL(FutureDrift(now, FORK_HEIGHT_V5_4 + 1), now + 90);
|
||||
}
|
||||
|
||||
// --- Symmetry: PastDrift and FutureDrift should be symmetric around the input ---
|
||||
|
||||
@@ -321,15 +321,21 @@ BOOST_AUTO_TEST_CASE(abandon_unknown_txid_returns_false)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(abandon_not_from_me_returns_false)
|
||||
{
|
||||
// The test wallet has at least one tx (added by earlier tests in
|
||||
// wallet_tests). Grab the first mapWallet entry — it has fDebit=0
|
||||
// because add_coin() only sets fIsFromMe if we asked, so by default
|
||||
// the tx is not from us.
|
||||
BOOST_CHECK(!wallet_tests::wallet.mapWallet.empty());
|
||||
if (!wallet_tests::wallet.mapWallet.empty()) {
|
||||
uint256 hash = wallet_tests::wallet.mapWallet.begin()->first;
|
||||
BOOST_CHECK(!wallet_tests::wallet.AbandonTransaction(hash));
|
||||
}
|
||||
// add_coin() above never touches mapWallet (it only fills vCoins), so
|
||||
// this test provisions its own wallet transaction. The tx has an empty
|
||||
// vin, so GetDebit() == 0 and IsFromMe() is false — AbandonTransaction
|
||||
// must reject it.
|
||||
CTransaction tx;
|
||||
tx.nLockTime = 999999; // arbitrary, gives the tx a unique hash
|
||||
tx.vout.resize(1);
|
||||
tx.vout[0].nValue = 1000000;
|
||||
CWalletTx wtx(&wallet_tests::wallet, tx);
|
||||
const uint256 hash = wtx.GetHash();
|
||||
wallet_tests::wallet.mapWallet[hash] = wtx;
|
||||
|
||||
BOOST_CHECK(!wallet_tests::wallet.AbandonTransaction(hash));
|
||||
|
||||
wallet_tests::wallet.mapWallet.erase(hash);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
Reference in New Issue
Block a user