mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
make the mempool a bit more defensive on slow machines (#20594)
* make the mempool a bit more defensive on slow machines * add tests
This commit is contained in:
@@ -94,7 +94,11 @@ async def run_mempool_benchmark() -> None:
|
||||
timestamp = uint64(1631794488)
|
||||
|
||||
with MempoolManager(
|
||||
get_coin_record, get_unspent_lineage_info_for_puzzle_hash, DEFAULT_CONSTANTS, single_threaded=True
|
||||
get_coin_record,
|
||||
get_unspent_lineage_info_for_puzzle_hash,
|
||||
DEFAULT_CONSTANTS,
|
||||
validation_timeout=2,
|
||||
single_threaded=True,
|
||||
) as mempool:
|
||||
print("\nrunning add_spend_bundle() + new_peak()")
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ async def run_mempool_benchmark() -> None:
|
||||
get_unspent_lineage_info_for_puzzle_hash,
|
||||
DEFAULT_CONSTANTS,
|
||||
single_threaded=single_threaded,
|
||||
validation_timeout=2,
|
||||
) as mempool:
|
||||
height = start_height
|
||||
rec = fake_block_record(height, timestamp)
|
||||
@@ -204,6 +205,7 @@ async def run_mempool_benchmark() -> None:
|
||||
get_unspent_lineage_info_for_puzzle_hash,
|
||||
DEFAULT_CONSTANTS,
|
||||
single_threaded=single_threaded,
|
||||
validation_timeout=2,
|
||||
) as mempool:
|
||||
height = start_height
|
||||
rec = fake_block_record(height, timestamp)
|
||||
|
||||
@@ -1322,7 +1322,7 @@ async def farmer_harvester_2_simulators_zero_bits_plot_filter(
|
||||
)
|
||||
)
|
||||
|
||||
config_overrides: dict[str, int] = {"full_node.max_sync_wait": 0}
|
||||
config_overrides: dict[str, int] = {"full_node.max_sync_wait": 0, "full_node.block_creation_timeout": 10}
|
||||
|
||||
bts = [
|
||||
await async_exit_stack.enter_async_context(
|
||||
|
||||
@@ -63,7 +63,10 @@ async def test_fee_increase() -> None:
|
||||
async with DBConnection(db_version=2) as db_wrapper:
|
||||
coin_store = await CoinStore.create(db_wrapper)
|
||||
async with MempoolManager.managed(
|
||||
coin_store.get_coin_records, coin_store.get_unspent_lineage_info_for_puzzle_hash, test_constants
|
||||
coin_store.get_coin_records,
|
||||
coin_store.get_unspent_lineage_info_for_puzzle_hash,
|
||||
test_constants,
|
||||
validation_timeout=10,
|
||||
) as mempool_manager:
|
||||
assert test_constants.MAX_BLOCK_COST_CLVM == mempool_manager.constants.MAX_BLOCK_COST_CLVM
|
||||
btc_fee_estimator: BitcoinFeeEstimator = mempool_manager.mempool.fee_estimator # type: ignore
|
||||
|
||||
@@ -266,6 +266,7 @@ async def instantiate_mempool_manager(
|
||||
zero_calls_get_unspent_lineage_info_for_puzzle_hash,
|
||||
constants,
|
||||
max_tx_clvm_cost=max_tx_clvm_cost,
|
||||
validation_timeout=10,
|
||||
) as mempool_manager:
|
||||
test_block_record = create_test_block_record(height=block_height, timestamp=block_timestamp)
|
||||
await mempool_manager.new_peak(test_block_record, None)
|
||||
@@ -711,6 +712,33 @@ async def test_reserve_fee_condition(zero_mempool_manager: MempoolManager) -> No
|
||||
await zero_mempool_manager.pre_validate_spendbundle(sb)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_validation_timeout() -> None:
|
||||
async with MempoolManager.managed(
|
||||
zero_calls_get_coin_records,
|
||||
zero_calls_get_unspent_lineage_info_for_puzzle_hash,
|
||||
DEFAULT_CONSTANTS,
|
||||
validation_timeout=0,
|
||||
) as mempool_manager:
|
||||
await mempool_manager.new_peak(create_test_block_record(), None)
|
||||
conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1]]
|
||||
sb = spend_bundle_from_conditions(conditions)
|
||||
with pytest.raises(ValueError, match="timeout"):
|
||||
await mempool_manager.pre_validate_spendbundle(sb)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_too_many_atoms() -> None:
|
||||
# a very large MAX_BLOCK_COST_CLVM makes the per-cost atom/pair threshold
|
||||
# effectively 0, triggering the density check on any spend
|
||||
constants = DEFAULT_CONSTANTS.replace(MAX_BLOCK_COST_CLVM=uint64(10**18))
|
||||
async with instantiate_mempool_manager(zero_calls_get_coin_records, constants=constants) as mempool_manager:
|
||||
conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1]]
|
||||
sb = spend_bundle_from_conditions(conditions)
|
||||
with pytest.raises(ValueError, match="too many atoms"):
|
||||
await mempool_manager.pre_validate_spendbundle(sb)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_unknown_unspent() -> None:
|
||||
async def get_coin_records(_: Collection[bytes32]) -> list[CoinRecord]:
|
||||
@@ -2420,6 +2448,7 @@ async def setup_mempool(coins: TestCoins) -> AsyncGenerator[MempoolManager, None
|
||||
coins.get_coin_records,
|
||||
coins.get_unspent_lineage_info,
|
||||
DEFAULT_CONSTANTS,
|
||||
validation_timeout=10,
|
||||
) as mempool_manager:
|
||||
test_block_record = create_test_block_record(height=uint32(5000000), timestamp=uint64(12345678))
|
||||
await mempool_manager.new_peak(test_block_record, None)
|
||||
@@ -2673,6 +2702,7 @@ def test_no_peak(old: bool, transactions_1000: list[SpendBundle]) -> None:
|
||||
coins.get_coin_records,
|
||||
coins.get_unspent_lineage_info,
|
||||
DEFAULT_CONSTANTS,
|
||||
validation_timeout=10,
|
||||
) as mempool_manager:
|
||||
create_block = mempool_manager.create_block_generator if old else mempool_manager.create_block_generator2
|
||||
|
||||
|
||||
@@ -91,7 +91,11 @@ async def setup_two_nodes(
|
||||
Setup and teardown of two full nodes, with blockchains and separate DBs.
|
||||
"""
|
||||
|
||||
config_overrides = {"full_node.max_sync_wait": 0, "full_node.log_coins": True}
|
||||
config_overrides = {
|
||||
"full_node.max_sync_wait": 0,
|
||||
"full_node.log_coins": True,
|
||||
"full_node.block_creation_timeout": 10,
|
||||
}
|
||||
with TempKeyring(populate=True) as keychain1, TempKeyring(populate=True) as keychain2:
|
||||
async with (
|
||||
create_block_tools_async(
|
||||
@@ -132,7 +136,11 @@ async def setup_n_nodes(
|
||||
"""
|
||||
Setup and teardown of n full nodes, with blockchains and separate DBs.
|
||||
"""
|
||||
config_overrides = {"full_node.max_sync_wait": 0, "full_node.log_coins": True}
|
||||
config_overrides = {
|
||||
"full_node.max_sync_wait": 0,
|
||||
"full_node.log_coins": True,
|
||||
"full_node.block_creation_timeout": 10,
|
||||
}
|
||||
with ExitStack() as stack:
|
||||
keychains = [stack.enter_context(TempKeyring(populate=True)) for _ in range(n)]
|
||||
async with AsyncExitStack() as async_exit_stack:
|
||||
@@ -260,6 +268,7 @@ async def setup_simulators_and_wallets_inner(
|
||||
if config_overrides is not None and "full_node.max_sync_wait" not in config_overrides:
|
||||
config_overrides["full_node.max_sync_wait"] = 0
|
||||
config_overrides["full_node.log_coins"] = True
|
||||
config_overrides["full_node.block_creation_timeout"] = 10
|
||||
async with AsyncExitStack() as async_exit_stack:
|
||||
bt_tools: list[BlockTools] = [
|
||||
await async_exit_stack.enter_async_context(
|
||||
@@ -380,7 +389,11 @@ async def setup_full_system_inner(
|
||||
keychain2: Keychain,
|
||||
shared_b_tools: BlockTools,
|
||||
) -> AsyncIterator[FullSystem]:
|
||||
config_overrides = {"full_node.max_sync_wait": 0, "full_node.log_coins": True}
|
||||
config_overrides = {
|
||||
"full_node.max_sync_wait": 0,
|
||||
"full_node.log_coins": True,
|
||||
"full_node.block_creation_timeout": 10,
|
||||
}
|
||||
|
||||
self_hostname = shared_b_tools.config["self_hostname"]
|
||||
|
||||
|
||||
@@ -169,7 +169,10 @@ class SpendSim:
|
||||
self.defaults = defaults
|
||||
|
||||
async with MempoolManager.managed(
|
||||
self.coin_store.get_coin_records, self.coin_store.get_unspent_lineage_info_for_puzzle_hash, defaults
|
||||
self.coin_store.get_coin_records,
|
||||
self.coin_store.get_unspent_lineage_info_for_puzzle_hash,
|
||||
defaults,
|
||||
validation_timeout=10,
|
||||
) as self.mempool_manager:
|
||||
# Load the next data if there is any
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
|
||||
@@ -285,6 +285,7 @@ class FullNode:
|
||||
get_unspent_lineage_info_for_puzzle_hash=self.coin_store.get_unspent_lineage_info_for_puzzle_hash,
|
||||
consensus_constants=self.constants,
|
||||
single_threaded=single_threaded,
|
||||
validation_timeout=self.config.get("block_creation_timeout", 2.0),
|
||||
) as self._mempool_manager:
|
||||
# Transactions go into this queue from the server, and get sent to respond_transaction
|
||||
self._transaction_queue = TransactionQueue(
|
||||
@@ -2783,7 +2784,13 @@ class FullNode:
|
||||
if self.sync_store.get_sync_mode() or self.mempool_manager.peak is None:
|
||||
return MempoolInclusionStatus.FAILED, Err.NO_TRANSACTIONS_WHILE_SYNCING
|
||||
|
||||
cost_result = await self.mempool_manager.pre_validate_spendbundle(transaction, spend_name, self._bls_cache)
|
||||
try:
|
||||
cost_result = await self.mempool_manager.pre_validate_spendbundle(transaction, spend_name, self._bls_cache)
|
||||
except ValueError as e:
|
||||
# ValueError is used to indicate a soft failure. We don't want to
|
||||
# ban the peer
|
||||
self.log.info(f"Rejecting transaction {spend_name}: {e}")
|
||||
return MempoolInclusionStatus.FAILED, Err.INVALID_SPEND_BUNDLE
|
||||
|
||||
self.mempool_manager.add_and_maybe_pop_seen(spend_name)
|
||||
|
||||
|
||||
@@ -312,6 +312,7 @@ class MempoolManager:
|
||||
_worker_queue_size: int
|
||||
max_block_clvm_cost: uint64
|
||||
max_tx_clvm_cost: uint64
|
||||
validation_timeout: float
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -319,6 +320,7 @@ class MempoolManager:
|
||||
get_unspent_lineage_info_for_puzzle_hash: Callable[[bytes32], Awaitable[UnspentLineageInfo | None]],
|
||||
consensus_constants: ConsensusConstants,
|
||||
*,
|
||||
validation_timeout: float,
|
||||
single_threaded: bool = False,
|
||||
max_tx_clvm_cost: uint64 | None = None,
|
||||
):
|
||||
@@ -350,6 +352,7 @@ class MempoolManager:
|
||||
self._pending_cache = PendingTxCache(self.constants.MAX_BLOCK_COST_CLVM * 1, 1000)
|
||||
self.seen_cache_size = 10000
|
||||
self._worker_queue_size = 0
|
||||
self.validation_timeout = validation_timeout
|
||||
if single_threaded:
|
||||
self.pool = InlineExecutor()
|
||||
else:
|
||||
@@ -372,6 +375,8 @@ class MempoolManager:
|
||||
get_coin_records: Callable[[Collection[bytes32]], Awaitable[list[CoinRecord]]],
|
||||
get_unspent_lineage_info_for_puzzle_hash: Callable[[bytes32], Awaitable[UnspentLineageInfo | None]],
|
||||
consensus_constants: ConsensusConstants,
|
||||
*,
|
||||
validation_timeout: float,
|
||||
single_threaded: bool = False,
|
||||
max_tx_clvm_cost: uint64 | None = None,
|
||||
) -> AsyncIterator[Self]:
|
||||
@@ -381,6 +386,7 @@ class MempoolManager:
|
||||
consensus_constants,
|
||||
single_threaded=single_threaded,
|
||||
max_tx_clvm_cost=max_tx_clvm_cost,
|
||||
validation_timeout=validation_timeout,
|
||||
)
|
||||
try:
|
||||
yield self
|
||||
@@ -504,10 +510,17 @@ class MempoolManager:
|
||||
self._worker_queue_size -= 1
|
||||
|
||||
if sbc.num_atoms > sbc.cost * 60_000_000 / self.constants.MAX_BLOCK_COST_CLVM:
|
||||
raise ValidationError(Err.INVALID_SPEND_BUNDLE, "too many atoms")
|
||||
raise ValueError("too many atoms")
|
||||
|
||||
if sbc.num_pairs > sbc.cost * 60_000_000 / self.constants.MAX_BLOCK_COST_CLVM:
|
||||
raise ValidationError(Err.INVALID_SPEND_BUNDLE, "too many pairs")
|
||||
raise ValueError("too many pairs")
|
||||
|
||||
if duration > self.validation_timeout:
|
||||
raise ValueError(f"timeout {duration:0.4} s")
|
||||
|
||||
cost = sbc.execution_cost + sbc.condition_cost
|
||||
if cost == 0 or (duration > 0.1 and duration * 1e9 / cost > self.validation_timeout * 5.0):
|
||||
raise ValueError(f"timeout ({duration * 1e9 / cost:0.4} ns/cost)")
|
||||
|
||||
if bls_cache is not None:
|
||||
bls_cache.update(new_cache_entries)
|
||||
@@ -516,8 +529,8 @@ class MempoolManager:
|
||||
spend_bundle_id = spend_bundle.name()
|
||||
|
||||
log.log(
|
||||
logging.DEBUG if duration < 2 else logging.WARNING,
|
||||
f"pre_validate_spendbundle took {duration:0.4f} seconds "
|
||||
logging.DEBUG if duration < self.validation_timeout else logging.WARNING,
|
||||
f"pre_validate_spendbundle took {duration:0.4f} seconds {duration * 1e9 / cost:0.4} ns/cost "
|
||||
f"for {spend_bundle_id} (queue-size: {self._worker_queue_size})",
|
||||
)
|
||||
return sbc
|
||||
@@ -808,12 +821,12 @@ class MempoolManager:
|
||||
duration = time.monotonic() - start_time
|
||||
|
||||
log.log(
|
||||
logging.DEBUG if duration < 2 else logging.WARNING,
|
||||
logging.DEBUG if duration < self.validation_timeout else logging.WARNING,
|
||||
f"add_spendbundle {spend_name} took {duration:0.2f} seconds. "
|
||||
f"Cost: {cost} ({round(100.0 * cost / self.constants.MAX_BLOCK_COST_CLVM, 3)}% of max block cost)",
|
||||
)
|
||||
|
||||
if duration > 2:
|
||||
if duration > self.validation_timeout:
|
||||
log.warning("validating spend took too long, rejecting")
|
||||
return Err.INVALID_SPEND_BUNDLE, None, []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user