mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
3658 lines
179 KiB
Python
3658 lines
179 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import contextlib
|
||
import copy
|
||
import dataclasses
|
||
import logging
|
||
import multiprocessing
|
||
import random
|
||
import sqlite3
|
||
import time
|
||
import traceback
|
||
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
||
from multiprocessing.context import BaseContext
|
||
from pathlib import Path
|
||
from typing import TYPE_CHECKING, Any, ClassVar, TextIO, cast, final
|
||
|
||
from chia_rs import (
|
||
AugSchemeMPL,
|
||
BlockRecord,
|
||
BLSCache,
|
||
CoinRecord,
|
||
CoinState,
|
||
ConsensusConstants,
|
||
EndOfSubSlotBundle,
|
||
FullBlock,
|
||
HeaderBlock,
|
||
PoolTarget,
|
||
SpendBundle,
|
||
SpendBundleConditions,
|
||
SubEpochSummary,
|
||
UnfinishedBlock,
|
||
get_flags_for_height_and_constants,
|
||
run_block_generator,
|
||
run_block_generator2,
|
||
)
|
||
from chia_rs.sized_bytes import bytes32
|
||
from chia_rs.sized_ints import uint8, uint32, uint64, uint128
|
||
from packaging.version import Version
|
||
|
||
from chia.consensus.augmented_chain import AugmentedBlockchain
|
||
from chia.consensus.block_body_validation import ForkInfo
|
||
from chia.consensus.block_creation import unfinished_block_to_full_block_with_mmr
|
||
from chia.consensus.block_generator_info import (
|
||
block_has_transactions_generator,
|
||
get_transactions_generator_bytes,
|
||
)
|
||
from chia.consensus.block_height_map import BlockHeightMap
|
||
from chia.consensus.blockchain import AddBlockResult, Blockchain, BlockchainMutexPriority, StateChangeSummary
|
||
from chia.consensus.blockchain_interface import BlockchainInterface
|
||
from chia.consensus.condition_tools import pkm_pairs
|
||
from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty
|
||
from chia.consensus.get_block_challenge import post_hard_fork2, pre_sp_tx_block_height
|
||
from chia.consensus.make_sub_epoch_summary import next_sub_epoch_summary
|
||
from chia.consensus.multiprocess_validation import PreValidationResult, pre_validate_block
|
||
from chia.consensus.pot_iterations import calculate_sp_iters
|
||
from chia.consensus.signage_point import SignagePoint
|
||
from chia.full_node.block_store import BlockStore
|
||
from chia.full_node.check_fork_next_block import check_fork_next_block
|
||
from chia.full_node.coin_store import CoinStore
|
||
from chia.full_node.full_node_api import FullNodeAPI
|
||
from chia.full_node.full_node_store import FullNodeStore, FullNodeStorePeakResult, UnfinishedBlockEntry
|
||
from chia.full_node.hint_management import get_hints_and_subscription_coin_ids
|
||
from chia.full_node.hint_store import HintStore
|
||
from chia.full_node.mempool import MempoolRemoveInfo
|
||
from chia.full_node.mempool_manager import MempoolManager
|
||
from chia.full_node.subscriptions import PeerSubscriptions, peers_for_spend_bundle
|
||
from chia.full_node.sync_store import Peak, SyncStore
|
||
from chia.full_node.tx_processing_queue import PeerWithTx, TransactionQueue, TransactionQueueEntry
|
||
from chia.full_node.weight_proof import WeightProofHandler, _minimum_recent_chain_length
|
||
from chia.protocols import farmer_protocol, full_node_protocol, timelord_protocol, wallet_protocol
|
||
from chia.protocols.farmer_protocol import SignagePointSourceData, SPSubSlotSourceData, SPVDFSourceData
|
||
from chia.protocols.full_node_protocol import RequestBlocks, RespondBlock, RespondBlocks, RespondSignagePoint
|
||
from chia.protocols.outbound_message import Message, NodeType, make_msg
|
||
from chia.protocols.protocol_message_types import ProtocolMessageTypes
|
||
from chia.protocols.protocol_timing import CONSENSUS_ERROR_BAN_SECONDS
|
||
from chia.protocols.shared_protocol import Capability
|
||
from chia.protocols.wallet_protocol import CoinStateUpdate, RemovedMempoolItem
|
||
from chia.rpc.rpc_server import StateChangedProtocol
|
||
from chia.server.node_discovery import FullNodePeers
|
||
from chia.server.server import ChiaServer
|
||
from chia.server.ws_connection import WSChiaConnection
|
||
from chia.types.blockchain_format.classgroup import ClassgroupElement
|
||
from chia.types.blockchain_format.vdf import CompressibleVDFField, VDFInfo, VDFProof, validate_vdf
|
||
from chia.types.clvm_cost import QUOTE_BYTES, QUOTE_EXECUTION_COST
|
||
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
|
||
from chia.types.mempool_item import MempoolItem
|
||
from chia.types.peer_info import PeerInfo
|
||
from chia.types.validation_state import ValidationState
|
||
from chia.types.weight_proof import WeightProof
|
||
from chia.util.bech32m import encode_puzzle_hash
|
||
from chia.util.config import process_config_start_method
|
||
from chia.util.cpu import available_logical_cores
|
||
from chia.util.db_synchronous import db_synchronous_on
|
||
from chia.util.db_version import lookup_db_version, set_db_version_async
|
||
from chia.util.db_wrapper import DBWrapper2, manage_connection
|
||
from chia.util.errors import ConsensusError, Err, TimestampError, ValidationError
|
||
from chia.util.inline_executor import InlineExecutor
|
||
from chia.util.limited_semaphore import LimitedSemaphore
|
||
from chia.util.network import is_localhost
|
||
from chia.util.path import path_from_root
|
||
from chia.util.priority_thread_pool_executor import Executor, PriorityThreadPoolExecutor
|
||
from chia.util.profiler import enable_profiler, mem_profile_task, profile_task
|
||
from chia.util.safe_cancel_task import cancel_task_safe
|
||
from chia.util.task_pipeline import TaskPipeline
|
||
from chia.util.task_referencer import create_referenced_task
|
||
|
||
# Per-request timeout for block fetches inside short_sync_backtrack.
|
||
# Use 15s here instead of the default 60s call_api timeout to bound how
|
||
# long short_sync_backtrack can occupy a new_peak slot.
|
||
SHORT_SYNC_BACKTRACK_BLOCK_REQUEST_TIMEOUT_SEC: int = 15
|
||
|
||
|
||
async def respond_blocks_or_ban(
|
||
peer: WSChiaConnection,
|
||
response: RespondBlocks,
|
||
start_height: int,
|
||
end_height: int,
|
||
log: logging.Logger,
|
||
) -> bool:
|
||
"""
|
||
Return True if response covers the inclusive requested height range.
|
||
Otherwise ban the peer and return False.
|
||
|
||
Honest nodes return the full inclusive range or RejectBlocks. A
|
||
well-formed but incomplete/mismatched RespondBlocks cannot advance
|
||
sync, so the peer is banned rather than credited with a fetch.
|
||
"""
|
||
expected_heights = list(range(start_height, end_height + 1))
|
||
if (
|
||
response.start_height == start_height
|
||
and response.end_height == end_height
|
||
and [block.height for block in response.blocks] == expected_heights
|
||
):
|
||
return True
|
||
log.warning(
|
||
f"peer {peer.peer_info.host} responded to request_blocks "
|
||
f"{start_height}-{end_height} with {response.start_height}-"
|
||
f"{response.end_height} ({len(response.blocks)} blocks), banning"
|
||
)
|
||
await peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
return False
|
||
|
||
|
||
# This is the result of calling peak_post_processing, which is then fed into peak_post_processing_2
|
||
@dataclasses.dataclass
|
||
class PeakPostProcessingResult:
|
||
# The added transactions IDs from calling MempoolManager.new_peak
|
||
mempool_peak_added_tx_ids: list[bytes32]
|
||
mempool_removals: list[MempoolRemoveInfo] # The removed mempool items from calling MempoolManager.new_peak
|
||
fns_peak_result: FullNodeStorePeakResult # The result of calling FullNodeStore.new_peak
|
||
hints: list[tuple[bytes32, bytes]] # The hints added to the DB
|
||
lookup_coin_ids: list[bytes32] # The coin IDs that we need to look up to notify wallets of changes
|
||
signage_points: list[tuple[RespondSignagePoint, WSChiaConnection, EndOfSubSlotBundle | None]]
|
||
|
||
|
||
@dataclasses.dataclass(frozen=True)
|
||
class WalletUpdate:
|
||
fork_height: uint32
|
||
peak: Peak
|
||
coin_records: list[CoinRecord]
|
||
hints: dict[bytes32, bytes32]
|
||
|
||
|
||
@final
|
||
@dataclasses.dataclass
|
||
class FullNode:
|
||
if TYPE_CHECKING:
|
||
from chia.rpc.rpc_server import RpcServiceProtocol
|
||
|
||
_protocol_check: ClassVar[RpcServiceProtocol] = cast("FullNode", None)
|
||
|
||
root_path: Path
|
||
config: dict[str, Any]
|
||
constants: ConsensusConstants
|
||
signage_point_times: list[float]
|
||
full_node_store: FullNodeStore
|
||
log: logging.Logger
|
||
db_path: Path
|
||
wallet_sync_queue: asyncio.Queue[WalletUpdate]
|
||
_segment_task_list: list[asyncio.Task[None]] = dataclasses.field(default_factory=list)
|
||
initialized: bool = False
|
||
_server: ChiaServer | None = None
|
||
_shut_down: bool = False
|
||
pow_creation: dict[bytes32, asyncio.Event] = dataclasses.field(default_factory=dict)
|
||
state_changed_callback: StateChangedProtocol | None = None
|
||
full_node_peers: FullNodePeers | None = None
|
||
sync_store: SyncStore = dataclasses.field(default_factory=SyncStore)
|
||
uncompact_task: asyncio.Task[None] | None = None
|
||
compact_vdf_requests: set[bytes32] = dataclasses.field(default_factory=set)
|
||
# TODO: Logging isn't setup yet so the log entries related to parsing the
|
||
# config would end up on stdout if handled here.
|
||
multiprocessing_context: BaseContext | None = None
|
||
_ui_tasks: set[asyncio.Task[None]] = dataclasses.field(default_factory=set)
|
||
subscriptions: PeerSubscriptions = dataclasses.field(default_factory=PeerSubscriptions)
|
||
_transaction_queue_task: asyncio.Task[None] | None = None
|
||
simulator_transaction_callback: Callable[[bytes32], Awaitable[None]] | None = None
|
||
_sync_task_list: list[asyncio.Task[None]] = dataclasses.field(default_factory=list)
|
||
_transaction_queue: TransactionQueue | None = None
|
||
_tx_task_list: list[asyncio.Task[None]] = dataclasses.field(default_factory=list)
|
||
_compact_vdf_sem: LimitedSemaphore | None = None
|
||
_new_peak_sem: LimitedSemaphore | None = None
|
||
_sp_catchup_sem: LimitedSemaphore | None = None
|
||
_add_transaction_semaphore: asyncio.Semaphore | None = None
|
||
_db_wrapper: DBWrapper2 | None = None
|
||
_hint_store: HintStore | None = None
|
||
_block_store: BlockStore | None = None
|
||
_coin_store: CoinStore | None = None
|
||
_mempool_manager: MempoolManager | None = None
|
||
_init_weight_proof: asyncio.Task[None] | None = None
|
||
_blockchain: Blockchain | None = None
|
||
_pool: Executor | None = None
|
||
_timelord_lock: asyncio.Lock | None = None
|
||
weight_proof_handler: WeightProofHandler | None = None
|
||
# hashes of peaks that failed long sync on chip13 Validation
|
||
bad_peak_cache: dict[bytes32, uint32] = dataclasses.field(default_factory=dict)
|
||
wallet_sync_task: asyncio.Task[None] | None = None
|
||
_bls_cache: BLSCache = dataclasses.field(default_factory=lambda: BLSCache(50000))
|
||
|
||
@property
|
||
def server(self) -> ChiaServer:
|
||
# This is a stop gap until the class usage is refactored such the values of
|
||
# integral attributes are known at creation of the instance.
|
||
if self._server is None:
|
||
raise RuntimeError("server not assigned")
|
||
|
||
return self._server
|
||
|
||
@classmethod
|
||
async def create(
|
||
cls,
|
||
config: dict[str, Any],
|
||
root_path: Path,
|
||
consensus_constants: ConsensusConstants,
|
||
name: str = __name__,
|
||
) -> FullNode:
|
||
# NOTE: async to force the queue creation to occur when an event loop is available
|
||
db_path_replaced: str = config["database_path"].replace("CHALLENGE", config["selected_network"])
|
||
db_path = path_from_root(root_path, db_path_replaced)
|
||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
return cls(
|
||
root_path=root_path,
|
||
config=config,
|
||
constants=consensus_constants,
|
||
signage_point_times=[time.time() for _ in range(consensus_constants.NUM_SPS_SUB_SLOT)],
|
||
full_node_store=FullNodeStore(consensus_constants),
|
||
log=logging.getLogger(name),
|
||
db_path=db_path,
|
||
wallet_sync_queue=asyncio.Queue(),
|
||
)
|
||
|
||
@contextlib.asynccontextmanager
|
||
async def manage(self) -> AsyncIterator[None]:
|
||
self._timelord_lock = asyncio.Lock()
|
||
self._compact_vdf_sem = LimitedSemaphore.create(active_limit=4, waiting_limit=20)
|
||
|
||
# We don't want to run too many concurrent new_peak instances, because it would fetch the same block from
|
||
# multiple peers and re-validate.
|
||
self._new_peak_sem = LimitedSemaphore.create(active_limit=2, waiting_limit=20)
|
||
|
||
# Limit concurrent sub-slot catch-up loops (up to 30 iterations x 10s each) to prevent
|
||
# resource exhaustion from multiple peers triggering catch-up simultaneously.
|
||
self._sp_catchup_sem = LimitedSemaphore.create(active_limit=2, waiting_limit=6)
|
||
|
||
# These many respond_transaction tasks can be active at any point in time
|
||
self._add_transaction_semaphore = asyncio.Semaphore(200)
|
||
|
||
sql_log_path: Path | None = None
|
||
with contextlib.ExitStack() as exit_stack:
|
||
sql_log_file: TextIO | None = None
|
||
if self.config.get("log_sqlite_cmds", False):
|
||
sql_log_path = path_from_root(self.root_path, "log/sql.log")
|
||
self.log.info(f"logging SQL commands to {sql_log_path}")
|
||
sql_log_file = exit_stack.enter_context(sql_log_path.open("a", encoding="utf-8"))
|
||
|
||
# create the store (db) and full node instance
|
||
# TODO: is this standardized and thus able to be handled by DBWrapper2?
|
||
async with manage_connection(self.db_path, log_file=sql_log_file, name="version_check") as db_connection:
|
||
db_version = await lookup_db_version(db_connection)
|
||
|
||
self.log.info(f"using blockchain database {self.db_path}, which is version {db_version}")
|
||
|
||
db_sync = db_synchronous_on(self.config.get("db_sync", "auto"))
|
||
self.log.info(f"opening blockchain DB: synchronous={db_sync}")
|
||
|
||
async with DBWrapper2.managed(
|
||
self.db_path,
|
||
db_version=db_version,
|
||
reader_count=self.config.get("db_readers", 4),
|
||
log_path=sql_log_path,
|
||
synchronous=db_sync,
|
||
) as self._db_wrapper:
|
||
if self.db_wrapper.db_version != 2:
|
||
async with self.db_wrapper.reader_no_transaction() as conn:
|
||
async with conn.execute(
|
||
"SELECT name FROM sqlite_master WHERE type='table' AND name='full_blocks'"
|
||
) as cur:
|
||
if len(list(await cur.fetchall())) == 0:
|
||
try:
|
||
# this is a new DB file. Make it v2
|
||
async with self.db_wrapper.writer_maybe_transaction() as w_conn:
|
||
await set_db_version_async(w_conn, 2)
|
||
self.db_wrapper.db_version = 2
|
||
self.log.info("blockchain database is empty, configuring as v2")
|
||
except sqlite3.OperationalError:
|
||
# it could be a database created with "chia init", which is
|
||
# empty except it has the database_version table
|
||
pass
|
||
|
||
self._block_store = await BlockStore.create(self.db_wrapper)
|
||
self._hint_store = await HintStore.create(self.db_wrapper)
|
||
self._coin_store = await CoinStore.create(self.db_wrapper)
|
||
self.log.info("Initializing blockchain from disk")
|
||
start_time = time.monotonic()
|
||
single_threaded = self.config.get("single_threaded", False)
|
||
log_coins = self.config.get("log_coins", False)
|
||
multiprocessing_start_method = process_config_start_method(config=self.config, log=self.log)
|
||
self.multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method)
|
||
if single_threaded:
|
||
self._pool = InlineExecutor()
|
||
else:
|
||
reserved_cores = self.config.get("reserved_cores", 0)
|
||
cpu_count = available_logical_cores()
|
||
num_workers = max(cpu_count - reserved_cores, 1)
|
||
dedicated = 1 if num_workers > 1 else 0
|
||
self._pool = PriorityThreadPoolExecutor(
|
||
max_workers=num_workers,
|
||
dedicated=dedicated,
|
||
thread_name_prefix="validation-",
|
||
instrument=bool(self.config.get("instrument_thread_pool", 0)),
|
||
)
|
||
self.log.info(f"Started {num_workers} threads for validation ({dedicated} dedicated)")
|
||
selected_network = self.config.get("selected_network")
|
||
height_map = await BlockHeightMap.create(self.db_path.parent, self._db_wrapper, selected_network)
|
||
self._blockchain = await Blockchain.create(
|
||
coin_store=self.coin_store,
|
||
block_store=self.block_store,
|
||
consensus_constants=self.constants,
|
||
height_map=height_map,
|
||
pool=self.pool,
|
||
log_coins=log_coins,
|
||
)
|
||
|
||
async with MempoolManager.managed(
|
||
get_coin_records=self.coin_store.get_coin_records,
|
||
get_unspent_lineage_info_for_puzzle_hash=self.coin_store.get_unspent_lineage_info_for_puzzle_hash,
|
||
consensus_constants=self.constants,
|
||
pool=self.pool,
|
||
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(
|
||
1000, self.log, max_tx_clvm_cost=uint64(self.constants.MAX_BLOCK_COST_CLVM // 2)
|
||
)
|
||
self._transaction_queue_task: asyncio.Task[None] = create_referenced_task(self._handle_transactions())
|
||
|
||
self._init_weight_proof = create_referenced_task(self.initialize_weight_proof())
|
||
|
||
if self.config.get("enable_profiler", False):
|
||
create_referenced_task(profile_task(self.root_path, "node", self.log), known_unreferenced=True)
|
||
|
||
self.profile_block_validation = self.config.get("profile_block_validation", False)
|
||
if self.profile_block_validation: # pragma: no cover
|
||
# this is not covered by any unit tests as it's essentially test code
|
||
# itself. It's exercised manually when investigating performance issues
|
||
profile_dir = path_from_root(self.root_path, "block-validation-profile")
|
||
profile_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
if self.config.get("enable_memory_profiler", False):
|
||
create_referenced_task(mem_profile_task(self.root_path, "node", self.log), known_unreferenced=True)
|
||
|
||
time_taken = time.monotonic() - start_time
|
||
peak: BlockRecord | None = self.blockchain.get_peak()
|
||
if peak is None:
|
||
self.log.info(f"Initialized with empty blockchain time taken: {int(time_taken)}s")
|
||
if not await self.coin_store.is_empty():
|
||
self.log.error(
|
||
"Inconsistent blockchain DB file! Could not find peak block but found some coins! "
|
||
"This is a fatal error. The blockchain database may be corrupt"
|
||
)
|
||
raise RuntimeError("corrupt blockchain DB")
|
||
else:
|
||
self.log.info(
|
||
f"Blockchain initialized to peak {peak.header_hash} height"
|
||
f" {peak.height}, "
|
||
f"time taken: {int(time_taken)}s"
|
||
)
|
||
async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high):
|
||
pending_tx = await self.mempool_manager.new_peak(self.blockchain.get_tx_peak(), None)
|
||
# No pending transactions when starting up
|
||
assert len(pending_tx.spend_bundle_ids) == 0
|
||
|
||
full_peak: FullBlock | None = await self.blockchain.get_full_peak()
|
||
assert full_peak is not None
|
||
state_change_summary = StateChangeSummary(peak, uint32(max(peak.height - 1, 0)), [], [], [], [])
|
||
# Must be called under priority_mutex
|
||
ppp_result: PeakPostProcessingResult = await self.peak_post_processing(
|
||
full_peak, state_change_summary, None
|
||
)
|
||
# Can be called outside of priority_mutex
|
||
await self.peak_post_processing_2(full_peak, None, state_change_summary, ppp_result)
|
||
if self.config["send_uncompact_interval"] != 0:
|
||
sanitize_weight_proof_only = False
|
||
if "sanitize_weight_proof_only" in self.config:
|
||
sanitize_weight_proof_only = self.config["sanitize_weight_proof_only"]
|
||
assert self.config["target_uncompact_proofs"] != 0
|
||
self.uncompact_task = create_referenced_task(
|
||
self.broadcast_uncompact_blocks(
|
||
self.config["send_uncompact_interval"],
|
||
self.config["target_uncompact_proofs"],
|
||
sanitize_weight_proof_only,
|
||
)
|
||
)
|
||
if self.wallet_sync_task is None or self.wallet_sync_task.done():
|
||
self.wallet_sync_task = create_referenced_task(self._wallets_sync_task_handler())
|
||
|
||
self.initialized = True
|
||
|
||
try:
|
||
async with contextlib.AsyncExitStack() as aexit_stack:
|
||
aexit_stack.enter_context(self.pool)
|
||
if self.full_node_peers is not None:
|
||
await aexit_stack.enter_async_context(self.full_node_peers.manage())
|
||
yield
|
||
finally:
|
||
self._shut_down = True
|
||
if self._init_weight_proof is not None:
|
||
self._init_weight_proof.cancel()
|
||
|
||
# blockchain is created in _start and in certain cases it may not exist here during _close
|
||
if self._blockchain is not None:
|
||
self.blockchain.shut_down()
|
||
if self.uncompact_task is not None:
|
||
self.uncompact_task.cancel()
|
||
if self._transaction_queue_task is not None:
|
||
self._transaction_queue_task.cancel()
|
||
cancel_task_safe(task=self.wallet_sync_task, log=self.log)
|
||
for one_tx_task in self._tx_task_list:
|
||
if not one_tx_task.done():
|
||
cancel_task_safe(task=one_tx_task, log=self.log)
|
||
for one_sync_task in self._sync_task_list:
|
||
if not one_sync_task.done():
|
||
cancel_task_safe(task=one_sync_task, log=self.log)
|
||
for segment_task in self._segment_task_list:
|
||
cancel_task_safe(segment_task, self.log)
|
||
for task_id, task in list(self.full_node_store.tx_fetch_tasks.items()):
|
||
cancel_task_safe(task, self.log)
|
||
if self._init_weight_proof is not None:
|
||
await asyncio.wait([self._init_weight_proof])
|
||
for one_tx_task in self._tx_task_list:
|
||
if one_tx_task.done():
|
||
self.log.info(f"TX task {one_tx_task.get_name()} done")
|
||
else:
|
||
with contextlib.suppress(asyncio.CancelledError):
|
||
self.log.info(f"Awaiting TX task {one_tx_task.get_name()}")
|
||
await one_tx_task
|
||
for one_sync_task in self._sync_task_list:
|
||
if one_sync_task.done():
|
||
self.log.info(f"Long sync task {one_sync_task.get_name()} done")
|
||
else:
|
||
with contextlib.suppress(asyncio.CancelledError):
|
||
self.log.info(f"Awaiting long sync task {one_sync_task.get_name()}")
|
||
await one_sync_task
|
||
await asyncio.gather(*self._segment_task_list, return_exceptions=True)
|
||
|
||
@property
|
||
def block_store(self) -> BlockStore:
|
||
assert self._block_store is not None
|
||
return self._block_store
|
||
|
||
@property
|
||
def timelord_lock(self) -> asyncio.Lock:
|
||
assert self._timelord_lock is not None
|
||
return self._timelord_lock
|
||
|
||
@property
|
||
def mempool_manager(self) -> MempoolManager:
|
||
assert self._mempool_manager is not None
|
||
return self._mempool_manager
|
||
|
||
@property
|
||
def blockchain(self) -> Blockchain:
|
||
assert self._blockchain is not None
|
||
return self._blockchain
|
||
|
||
@property
|
||
def pool(self) -> Executor:
|
||
assert self._pool is not None
|
||
return self._pool
|
||
|
||
@property
|
||
def coin_store(self) -> CoinStore:
|
||
assert self._coin_store is not None
|
||
return self._coin_store
|
||
|
||
@property
|
||
def add_transaction_semaphore(self) -> asyncio.Semaphore:
|
||
assert self._add_transaction_semaphore is not None
|
||
return self._add_transaction_semaphore
|
||
|
||
@property
|
||
def transaction_queue(self) -> TransactionQueue:
|
||
assert self._transaction_queue is not None
|
||
return self._transaction_queue
|
||
|
||
@property
|
||
def db_wrapper(self) -> DBWrapper2:
|
||
assert self._db_wrapper is not None
|
||
return self._db_wrapper
|
||
|
||
@property
|
||
def hint_store(self) -> HintStore:
|
||
assert self._hint_store is not None
|
||
return self._hint_store
|
||
|
||
@property
|
||
def new_peak_sem(self) -> LimitedSemaphore:
|
||
assert self._new_peak_sem is not None
|
||
return self._new_peak_sem
|
||
|
||
@property
|
||
def compact_vdf_sem(self) -> LimitedSemaphore:
|
||
assert self._compact_vdf_sem is not None
|
||
return self._compact_vdf_sem
|
||
|
||
@property
|
||
def sp_catchup_sem(self) -> LimitedSemaphore:
|
||
assert self._sp_catchup_sem is not None
|
||
return self._sp_catchup_sem
|
||
|
||
def get_connections(self, request_node_type: NodeType | None) -> list[dict[str, Any]]:
|
||
connections = self.server.get_connections(request_node_type)
|
||
con_info: list[dict[str, Any]] = []
|
||
if self.sync_store is not None:
|
||
peak_store = self.sync_store.peer_to_peak
|
||
else:
|
||
peak_store = None
|
||
for con in connections:
|
||
if con.peer_node_id in peak_store:
|
||
peak = peak_store[con.peer_node_id]
|
||
peak_height = peak.height
|
||
peak_hash = peak.header_hash
|
||
peak_weight = peak.weight
|
||
else:
|
||
peak_height = None
|
||
peak_hash = None
|
||
peak_weight = None
|
||
con_dict: dict[str, Any] = {
|
||
"type": con.connection_type,
|
||
"local_port": con.local_port,
|
||
"peer_host": con.peer_info.host,
|
||
"peer_port": con.peer_info.port,
|
||
"peer_server_port": con.peer_server_port,
|
||
"node_id": con.peer_node_id,
|
||
"creation_time": con.creation_time,
|
||
"bytes_read": con.bytes_read,
|
||
"bytes_written": con.bytes_written,
|
||
"last_message_time": con.last_message_time,
|
||
"peak_height": peak_height,
|
||
"peak_weight": peak_weight,
|
||
"peak_hash": peak_hash,
|
||
}
|
||
con_info.append(con_dict)
|
||
|
||
return con_info
|
||
|
||
def _set_state_changed_callback(self, callback: StateChangedProtocol) -> None:
|
||
self.state_changed_callback = callback
|
||
|
||
async def _handle_one_transaction(self, entry: TransactionQueueEntry) -> None:
|
||
peer = entry.peer
|
||
try:
|
||
inc_status, err = await self.add_transaction(
|
||
entry.transaction, entry.spend_name, peer, entry.test, entry.peers_with_tx
|
||
)
|
||
entry.done.set((inc_status, err))
|
||
except asyncio.CancelledError:
|
||
error_stack = traceback.format_exc()
|
||
self.log.debug(f"Cancelling _handle_one_transaction, closing: {error_stack}")
|
||
except ValidationError as e:
|
||
self.log.exception("ValidationError in _handle_one_transaction, closing")
|
||
if peer is not None:
|
||
await peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
entry.done.set((MempoolInclusionStatus.FAILED, e.code))
|
||
except Exception:
|
||
self.log.exception("Error in _handle_one_transaction, closing")
|
||
if peer is not None:
|
||
await peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
entry.done.set((MempoolInclusionStatus.FAILED, Err.UNKNOWN))
|
||
finally:
|
||
self.add_transaction_semaphore.release()
|
||
|
||
async def _handle_transactions(self) -> None:
|
||
while not self._shut_down:
|
||
# We use a semaphore to make sure we don't send more than 200 concurrent calls of respond_transaction.
|
||
# However, doing them one at a time would be slow, because they get sent to other processes.
|
||
await self.add_transaction_semaphore.acquire()
|
||
|
||
# Clean up task reference list (used to prevent gc from killing running tasks)
|
||
for oldtask in self._tx_task_list[:]:
|
||
if oldtask.done():
|
||
self._tx_task_list.remove(oldtask)
|
||
|
||
item: TransactionQueueEntry = await self.transaction_queue.pop()
|
||
self._tx_task_list.append(create_referenced_task(self._handle_one_transaction(item)))
|
||
|
||
async def initialize_weight_proof(self) -> None:
|
||
self.weight_proof_handler = WeightProofHandler(
|
||
constants=self.constants,
|
||
blockchain=self.blockchain,
|
||
multiprocessing_context=self.multiprocessing_context,
|
||
)
|
||
peak = self.blockchain.get_peak()
|
||
if peak is not None:
|
||
await self.weight_proof_handler.create_sub_epoch_segments()
|
||
|
||
def set_server(self, server: ChiaServer) -> None:
|
||
self._server = server
|
||
dns_servers: list[str] = []
|
||
network_name = self.config["selected_network"]
|
||
try:
|
||
default_port = self.config["network_overrides"]["config"][network_name]["default_full_node_port"]
|
||
except Exception:
|
||
self.log.info("Default port field not found in config.")
|
||
default_port = None
|
||
if "dns_servers" in self.config:
|
||
dns_servers = self.config["dns_servers"]
|
||
elif network_name == "mainnet":
|
||
# If `dns_servers` is missing from the `config`, hardcode it if we're running mainnet.
|
||
dns_servers.append("dns-introducer.chia.net")
|
||
try:
|
||
self.full_node_peers = FullNodePeers(
|
||
server=self.server,
|
||
target_outbound_count=self.config["target_outbound_peer_count"],
|
||
peers_file_path=self.root_path / Path(self.config.get("peers_file_path", "db/peers.dat")),
|
||
introducer_info=self.config["introducer_peer"],
|
||
dns_servers=dns_servers,
|
||
peer_connect_interval=self.config["peer_connect_interval"],
|
||
selected_network=self.config["selected_network"],
|
||
default_port=default_port,
|
||
log=self.log,
|
||
)
|
||
except Exception as e:
|
||
error_stack = traceback.format_exc()
|
||
self.log.error(f"Exception: {e}")
|
||
self.log.error(f"Exception in peer discovery: {e}")
|
||
self.log.error(f"Exception Stack: {error_stack}")
|
||
|
||
def _state_changed(self, change: str, change_data: dict[str, Any] | None = None) -> None:
|
||
if self.state_changed_callback is not None:
|
||
self.state_changed_callback(change, change_data)
|
||
|
||
async def short_sync_batch(self, peer: WSChiaConnection, start_height: uint32, target_height: uint32) -> bool:
|
||
"""
|
||
Tries to sync to a chain which is not too far in the future, by downloading batches of blocks. If the first
|
||
block that we download is not connected to our chain, we return False and do an expensive long sync instead.
|
||
Long sync is not preferred because it requires downloading and validating a weight proof.
|
||
|
||
Args:
|
||
peer: peer to sync from
|
||
start_height: height that we should start downloading at. (Our peak is higher)
|
||
target_height: target to sync to
|
||
|
||
Returns:
|
||
False if the fork point was not found, and we need to do a long sync. True otherwise.
|
||
|
||
"""
|
||
# Don't trigger multiple batch syncs to the same peer
|
||
|
||
if self.sync_store.is_backtrack_syncing(node_id=peer.peer_node_id):
|
||
return True # Don't batch sync, we are already in progress of a backtrack sync
|
||
if peer.peer_node_id in self.sync_store.batch_syncing:
|
||
return True # Don't trigger a long sync
|
||
self.sync_store.batch_syncing.add(peer.peer_node_id)
|
||
try:
|
||
self.log.info(f"Starting batch short sync from {start_height} to height {target_height}")
|
||
if start_height > 0:
|
||
first = await peer.call_api(
|
||
FullNodeAPI.request_block, full_node_protocol.RequestBlock(uint32(start_height), False)
|
||
)
|
||
if first is None or not isinstance(first, full_node_protocol.RespondBlock):
|
||
self.log.error(f"Error short batch syncing, could not fetch block at height {start_height}")
|
||
return False
|
||
# The height comes from the peer's response and need not equal start_height. Only a
|
||
# height whose parent (height - 1) is in our height map can connect to our chain, so
|
||
# treat height 0 or a height past our peak as not connected.
|
||
hash = (
|
||
self.blockchain.height_to_hash(uint32(first.block.height - 1)) if first.block.height > 0 else None
|
||
)
|
||
if hash is None or hash != first.block.prev_header_hash:
|
||
# Not connected to our blockchain, so do a long sync instead. A deep chain is the
|
||
# usual cause, but the peer may also have answered with an unrelated height.
|
||
self.log.info(
|
||
f"Batch syncing stopped, block at height {first.block.height} does not connect to our "
|
||
f"chain (requested height {start_height})"
|
||
)
|
||
return False
|
||
|
||
batch_size = self.constants.MAX_BLOCK_COUNT_PER_REQUESTS
|
||
for task in self._segment_task_list[:]:
|
||
if task.done():
|
||
self._segment_task_list.remove(task)
|
||
else:
|
||
cancel_task_safe(task=task, log=self.log)
|
||
|
||
peer_info = peer.get_peer_logging()
|
||
if start_height > 0:
|
||
fork_hash = self.blockchain.height_to_hash(uint32(start_height - 1))
|
||
else:
|
||
fork_hash = self.constants.GENESIS_CHALLENGE
|
||
assert fork_hash
|
||
fork_info = ForkInfo(start_height - 1, start_height - 1, fork_hash)
|
||
blockchain = AugmentedBlockchain(self.blockchain)
|
||
for height in range(start_height, target_height, batch_size):
|
||
end_height = min(target_height, height + batch_size)
|
||
request = RequestBlocks(uint32(height), uint32(end_height), True)
|
||
response = await peer.call_api(FullNodeAPI.request_blocks, request)
|
||
if not isinstance(response, RespondBlocks):
|
||
raise ValueError(f"Error short batch syncing, invalid/no response for {height}-{end_height}")
|
||
if not await respond_blocks_or_ban(peer, response, height, end_height, self.log):
|
||
raise ConsensusError(
|
||
Err.INVALID_PROTOCOL_MESSAGE,
|
||
error_msg=(
|
||
f"Error short batch syncing, incomplete/mismatched blocks for {height}-{end_height}"
|
||
),
|
||
)
|
||
async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high):
|
||
state_change_summary: StateChangeSummary | None
|
||
prev_b = None
|
||
if response.blocks[0].height > 0:
|
||
prev_b = await self.blockchain.get_block_record_from_db(response.blocks[0].prev_header_hash)
|
||
if prev_b is None:
|
||
# First block of the batch is not connected to our chain; fall back to long sync.
|
||
return False
|
||
new_slot = len(response.blocks[0].finished_sub_slots) > 0
|
||
ssi, diff = get_next_sub_slot_iters_and_difficulty(
|
||
self.constants, new_slot, prev_b, self.blockchain
|
||
)
|
||
vs = ValidationState(ssi, diff, None)
|
||
success, state_change_summary = await self.add_block_batch(
|
||
response.blocks, peer_info, fork_info, vs, blockchain
|
||
)
|
||
if state_change_summary is not None:
|
||
try:
|
||
peak_fb: FullBlock | None = await self.blockchain.get_full_peak()
|
||
assert peak_fb is not None
|
||
ppp_result: PeakPostProcessingResult = await self.peak_post_processing(
|
||
peak_fb,
|
||
state_change_summary,
|
||
peer,
|
||
)
|
||
except Exception:
|
||
# Still do post processing after cancel (or exception)
|
||
peak_fb = await self.blockchain.get_full_peak()
|
||
assert peak_fb is not None
|
||
await self.peak_post_processing(peak_fb, state_change_summary, peer)
|
||
raise
|
||
finally:
|
||
# Log the committed prefix only; a failed suffix is not ingested.
|
||
self.log.info(f"Added blocks {height}-{state_change_summary.peak.height}")
|
||
if state_change_summary is not None and peak_fb is not None:
|
||
# Call outside of priority_mutex to encourage concurrency
|
||
await self.peak_post_processing_2(peak_fb, peer, state_change_summary, ppp_result)
|
||
if not success:
|
||
if state_change_summary is not None:
|
||
raise ValueError(
|
||
f"Error short batch syncing, failed to validate blocks after height "
|
||
f"{state_change_summary.peak.height} (batch {height}-{end_height})"
|
||
)
|
||
raise ValueError(f"Error short batch syncing, failed to validate blocks {height}-{end_height}")
|
||
finally:
|
||
self.sync_store.batch_syncing.discard(peer.peer_node_id)
|
||
return True
|
||
|
||
async def short_sync_backtrack(
|
||
self, peer: WSChiaConnection, peak_height: uint32, target_height: uint32, target_unf_hash: bytes32
|
||
) -> bool:
|
||
"""
|
||
Performs a backtrack sync, where blocks are downloaded one at a time from newest to oldest. If we do not
|
||
find the fork point 5 deeper than our peak, we return False and do a long sync instead.
|
||
|
||
Args:
|
||
peer: peer to sync from
|
||
peak_height: height of our peak
|
||
target_height: target height
|
||
target_unf_hash: partial hash of the unfinished block of the target
|
||
|
||
Returns:
|
||
True iff we found the fork point, and we do not need to long sync.
|
||
"""
|
||
try:
|
||
self.sync_store.increment_backtrack_syncing(node_id=peer.peer_node_id)
|
||
|
||
unfinished_block: UnfinishedBlock | None = self.full_node_store.get_unfinished_block(target_unf_hash)
|
||
curr_height: int = target_height
|
||
found_fork_point = False
|
||
blocks = []
|
||
while curr_height > peak_height - 5:
|
||
# If we already have the unfinished block, don't fetch the transactions. In the normal case, we will
|
||
# already have the unfinished block, from when it was broadcast, so we just need to download the header,
|
||
# but not the transactions
|
||
fetch_tx: bool = unfinished_block is None or curr_height != target_height
|
||
curr = await peer.call_api(
|
||
FullNodeAPI.request_block,
|
||
full_node_protocol.RequestBlock(uint32(curr_height), fetch_tx),
|
||
timeout=SHORT_SYNC_BACKTRACK_BLOCK_REQUEST_TIMEOUT_SEC,
|
||
)
|
||
if curr is None:
|
||
raise ValueError(f"Failed to fetch block {curr_height} from {peer.get_peer_logging()}, timed out")
|
||
if curr is None or not isinstance(curr, full_node_protocol.RespondBlock):
|
||
raise ValueError(
|
||
f"Failed to fetch block {curr_height} from {peer.get_peer_logging()}, wrong type {type(curr)}"
|
||
)
|
||
blocks.append(curr.block)
|
||
if curr_height == 0:
|
||
found_fork_point = True
|
||
break
|
||
hash_at_height = self.blockchain.height_to_hash(uint32(curr.block.height - 1))
|
||
if hash_at_height is not None and hash_at_height == curr.block.prev_header_hash:
|
||
found_fork_point = True
|
||
break
|
||
curr_height -= 1
|
||
if found_fork_point:
|
||
first_block = blocks[-1] # blocks are reveresd this is the lowest block to add
|
||
# we create the fork_info and pass it here so it would be updated on each call to add_block
|
||
fork_info = ForkInfo(first_block.height - 1, first_block.height - 1, first_block.prev_header_hash)
|
||
for block in reversed(blocks):
|
||
# when syncing, we won't share any signatures with the
|
||
# mempool, so there's no need to pass in the BLS cache.
|
||
await self.add_block(block, peer, fork_info=fork_info)
|
||
except (asyncio.CancelledError, Exception):
|
||
self.sync_store.decrement_backtrack_syncing(node_id=peer.peer_node_id)
|
||
raise
|
||
|
||
self.sync_store.decrement_backtrack_syncing(node_id=peer.peer_node_id)
|
||
return found_fork_point
|
||
|
||
async def _refresh_ui_connections(self, sleep_before: float = 0) -> None:
|
||
if sleep_before > 0:
|
||
await asyncio.sleep(sleep_before)
|
||
self._state_changed("peer_changed_peak")
|
||
|
||
async def new_peak(self, request: full_node_protocol.NewPeak, peer: WSChiaConnection) -> None:
|
||
"""
|
||
We have received a notification of a new peak from a peer. This happens either when we have just connected,
|
||
or when the peer has updated their peak.
|
||
|
||
Args:
|
||
request: information about the new peak
|
||
peer: peer that sent the message
|
||
|
||
"""
|
||
|
||
try:
|
||
seen_header_hash = self.sync_store.seen_header_hash(request.header_hash)
|
||
# Updates heights in the UI. Sleeps 1.5s before, so other peers have time to update their peaks as well.
|
||
# Limit to 3 refreshes.
|
||
if not seen_header_hash and len(self._ui_tasks) < 3:
|
||
self._ui_tasks.add(create_referenced_task(self._refresh_ui_connections(1.5)))
|
||
# Prune completed connect tasks
|
||
self._ui_tasks = set(filter(lambda t: not t.done(), self._ui_tasks))
|
||
except Exception as e:
|
||
self.log.warning(f"Exception UI refresh task: {e}")
|
||
|
||
# Store this peak/peer combination in case we want to sync to it, and to keep track of peers
|
||
self.sync_store.peer_has_block(request.header_hash, peer.peer_node_id, request.weight, request.height, True)
|
||
|
||
if self.blockchain.contains_block(request.header_hash, request.height):
|
||
return None
|
||
|
||
# Not interested in less heavy peaks
|
||
peak: BlockRecord | None = self.blockchain.get_peak()
|
||
curr_peak_height = uint32(0) if peak is None else peak.height
|
||
if peak is not None and peak.weight > request.weight:
|
||
return None
|
||
|
||
if self.sync_store.get_sync_mode():
|
||
# If peer connects while we are syncing, check if they have the block we are syncing towards
|
||
target_peak = self.sync_store.target_peak
|
||
if target_peak is not None and request.header_hash != target_peak.header_hash:
|
||
peak_peers: set[bytes32] = self.sync_store.get_peers_that_have_peak([target_peak.header_hash])
|
||
# Don't ask if we already know this peer has the peak
|
||
if peer.peer_node_id not in peak_peers:
|
||
target_peak_response = await peer.call_api(
|
||
FullNodeAPI.request_block,
|
||
full_node_protocol.RequestBlock(target_peak.height, False),
|
||
timeout=10,
|
||
)
|
||
if (
|
||
target_peak_response is not None
|
||
and isinstance(target_peak_response, RespondBlock)
|
||
and target_peak_response.block.header_hash == target_peak.header_hash
|
||
):
|
||
self.sync_store.peer_has_block(
|
||
target_peak.header_hash,
|
||
peer.peer_node_id,
|
||
target_peak_response.block.weight,
|
||
target_peak.height,
|
||
False,
|
||
)
|
||
else:
|
||
if (
|
||
curr_peak_height <= request.height
|
||
and request.height <= curr_peak_height + self.config["short_sync_blocks_behind_threshold"]
|
||
):
|
||
# This is the normal case of receiving the next block
|
||
if await self.short_sync_backtrack(
|
||
peer, curr_peak_height, request.height, request.unfinished_reward_block_hash
|
||
):
|
||
return None
|
||
|
||
if request.height < self.constants.WEIGHT_PROOF_RECENT_BLOCKS:
|
||
# This is the case of syncing up more than a few blocks, at the start of the chain
|
||
self.log.debug("Doing batch sync, no backup")
|
||
await self.short_sync_batch(peer, uint32(0), request.height)
|
||
return None
|
||
|
||
if (
|
||
curr_peak_height <= request.height
|
||
and request.height < curr_peak_height + self.config["sync_blocks_behind_threshold"]
|
||
):
|
||
# This case of being behind but not by so much
|
||
if await self.short_sync_batch(peer, uint32(max(curr_peak_height - 6, 0)), request.height):
|
||
return None
|
||
|
||
# Clean up task reference list (used to prevent gc from killing running tasks)
|
||
for oldtask in self._sync_task_list[:]:
|
||
if oldtask.done():
|
||
self._sync_task_list.remove(oldtask)
|
||
|
||
# This is the either the case where we were not able to sync successfully (for example, due to the fork
|
||
# point being in the past), or we are very far behind. Performs a long sync.
|
||
# Multiple tasks may be created here. If we don't save all handles, a task could enter a sync object
|
||
# and be cleaned up by the GC, corrupting the sync object and possibly not allowing anything else in.
|
||
self._sync_task_list.append(create_referenced_task(self._sync()))
|
||
|
||
async def send_peak_to_timelords(
|
||
self, peak_block: FullBlock | None = None, peer: WSChiaConnection | None = None
|
||
) -> None:
|
||
"""
|
||
Sends current peak to timelords
|
||
"""
|
||
if peak_block is None:
|
||
peak_block = await self.blockchain.get_full_peak()
|
||
if peak_block is not None:
|
||
peak = self.blockchain.block_record(peak_block.header_hash)
|
||
difficulty = self.blockchain.get_next_sub_slot_iters_and_difficulty(peak.header_hash, False)[1]
|
||
post_hard_fork = post_hard_fork2(
|
||
self.constants,
|
||
self.blockchain,
|
||
prev_b_hash=peak.header_hash,
|
||
sp_index=peak.signage_point_index,
|
||
finished_sub_slots=len(peak.finished_challenge_slot_hashes)
|
||
if peak.finished_challenge_slot_hashes is not None
|
||
else 0,
|
||
)
|
||
ses: SubEpochSummary | None = next_sub_epoch_summary(
|
||
self.constants,
|
||
self.blockchain,
|
||
peak.required_iters,
|
||
peak_block,
|
||
True,
|
||
post_hard_fork,
|
||
)
|
||
recent_rc = self.blockchain.get_recent_reward_challenges()
|
||
|
||
curr = peak
|
||
while not curr.is_challenge_block(self.constants) and not curr.first_in_sub_slot:
|
||
curr = self.blockchain.block_record(curr.prev_hash)
|
||
|
||
if curr.is_challenge_block(self.constants):
|
||
last_csb_or_eos = curr.total_iters
|
||
else:
|
||
last_csb_or_eos = curr.ip_sub_slot_total_iters(self.constants)
|
||
|
||
curr = peak
|
||
passed_ses_height_but_not_yet_included = True
|
||
while (curr.height % self.constants.SUB_EPOCH_BLOCKS) != 0:
|
||
if curr.sub_epoch_summary_included:
|
||
passed_ses_height_but_not_yet_included = False
|
||
curr = self.blockchain.block_record(curr.prev_hash)
|
||
if curr.sub_epoch_summary_included or curr.height == 0:
|
||
passed_ses_height_but_not_yet_included = False
|
||
|
||
timelord_new_peak: timelord_protocol.NewPeakTimelord = timelord_protocol.NewPeakTimelord(
|
||
peak_block.reward_chain_block,
|
||
difficulty,
|
||
peak.deficit,
|
||
peak.sub_slot_iters,
|
||
ses,
|
||
recent_rc,
|
||
last_csb_or_eos,
|
||
passed_ses_height_but_not_yet_included,
|
||
)
|
||
|
||
msg = make_msg(ProtocolMessageTypes.new_peak_timelord, timelord_new_peak)
|
||
if peer is None:
|
||
await self.server.send_to_all([msg], NodeType.TIMELORD)
|
||
else:
|
||
await self.server.send_to_specific([msg], peer.peer_node_id)
|
||
|
||
async def synced(self, block_is_current_at: uint64 | None = None) -> bool:
|
||
if block_is_current_at is None:
|
||
block_is_current_at = uint64(time.time() - 60 * 7)
|
||
if "simulator" in str(self.config.get("selected_network")):
|
||
return True # sim is always synced because it has no peers
|
||
curr: BlockRecord | None = self.blockchain.get_peak()
|
||
if curr is None:
|
||
return False
|
||
|
||
while curr is not None and not curr.is_transaction_block:
|
||
curr = self.blockchain.try_block_record(curr.prev_hash)
|
||
|
||
if (
|
||
curr is None
|
||
or curr.timestamp is None
|
||
or curr.timestamp < block_is_current_at
|
||
or self.sync_store.get_sync_mode()
|
||
):
|
||
return False
|
||
else:
|
||
return True
|
||
|
||
async def on_connect(self, connection: WSChiaConnection) -> None:
|
||
"""
|
||
Whenever we connect to another node / wallet, send them our current heads. Also send heads to farmers
|
||
and challenges to timelords.
|
||
"""
|
||
|
||
self._state_changed("add_connection")
|
||
self._state_changed("sync_mode")
|
||
# TODO: this can probably be improved
|
||
if self.full_node_peers is not None:
|
||
create_referenced_task(self.full_node_peers.on_connect(connection))
|
||
|
||
if self.initialized is False:
|
||
return None
|
||
|
||
if connection.connection_type is NodeType.FULL_NODE:
|
||
# Send filter to node and request mempool items that are not in it (Only if we are currently synced)
|
||
synced = await self.synced()
|
||
peak_height = self.blockchain.get_peak_height()
|
||
if synced and peak_height is not None:
|
||
my_filter = self.mempool_manager.get_filter()
|
||
mempool_request = full_node_protocol.RequestMempoolTransactions(my_filter)
|
||
|
||
msg = make_msg(ProtocolMessageTypes.request_mempool_transactions, mempool_request)
|
||
await connection.send_message(msg)
|
||
# Old peers (< 2.6.0) respond to RequestMempoolTransactions with
|
||
# RespondTransaction directly. New peers send NewTransaction instead.
|
||
try:
|
||
old_peer = Version(connection.version) < Version("2.6.0")
|
||
except Exception:
|
||
old_peer = True
|
||
if old_peer:
|
||
connection.expected_mempool_responses = 100
|
||
|
||
peak_full: FullBlock | None = await self.blockchain.get_full_peak()
|
||
|
||
if peak_full is not None:
|
||
peak: BlockRecord = self.blockchain.block_record(peak_full.header_hash)
|
||
if connection.connection_type is NodeType.FULL_NODE:
|
||
request_node = full_node_protocol.NewPeak(
|
||
peak.header_hash,
|
||
peak.height,
|
||
peak.weight,
|
||
peak.height,
|
||
peak_full.reward_chain_block.get_unfinished().get_hash(),
|
||
)
|
||
await connection.send_message(make_msg(ProtocolMessageTypes.new_peak, request_node))
|
||
|
||
elif connection.connection_type is NodeType.WALLET:
|
||
# If connected to a wallet, send the Peak
|
||
request_wallet = wallet_protocol.NewPeakWallet(
|
||
peak.header_hash,
|
||
peak.height,
|
||
peak.weight,
|
||
peak.height,
|
||
)
|
||
await connection.send_message(make_msg(ProtocolMessageTypes.new_peak_wallet, request_wallet))
|
||
elif connection.connection_type is NodeType.TIMELORD:
|
||
await self.send_peak_to_timelords()
|
||
|
||
async def on_disconnect(self, connection: WSChiaConnection) -> None:
|
||
self.log.info(f"peer disconnected {connection.get_peer_logging()}")
|
||
self._state_changed("close_connection")
|
||
self._state_changed("sync_mode")
|
||
if self.sync_store is not None:
|
||
self.sync_store.peer_disconnected(connection.peer_node_id)
|
||
# Remove all ph | coin id subscription for this peer
|
||
self.subscriptions.remove_peer(connection.peer_node_id)
|
||
|
||
async def _sync(self) -> None:
|
||
"""
|
||
Performs a full sync of the blockchain up to the peak.
|
||
- Wait a few seconds for peers to send us their peaks
|
||
- Select the heaviest peak, and request a weight proof from a peer with that peak
|
||
- Validate the weight proof, and disconnect from the peer if invalid
|
||
- Find the fork point to see where to start downloading blocks
|
||
- Download blocks in batch (and in parallel) and verify them one at a time
|
||
- Disconnect peers that provide invalid blocks or don't have the blocks
|
||
"""
|
||
# Ensure we are only syncing once and not double calling this method
|
||
fork_point: uint32 | None = None
|
||
if self.sync_store.get_sync_mode():
|
||
return None
|
||
|
||
if self.sync_store.get_long_sync():
|
||
self.log.debug("already in long sync")
|
||
return None
|
||
|
||
self.sync_store.set_long_sync(True)
|
||
self.log.debug("long sync started")
|
||
try:
|
||
self.log.info("Starting to perform sync.")
|
||
|
||
# Wait until we have 3 peaks or up to a max of 30 seconds
|
||
max_iterations = int(self.config.get("max_sync_wait", 30)) * 10
|
||
|
||
self.log.info(f"Waiting to receive peaks from peers. (timeout: {max_iterations / 10}s)")
|
||
peaks = []
|
||
for i in range(max_iterations):
|
||
peaks = [peak.header_hash for peak in self.sync_store.get_peak_of_each_peer().values()]
|
||
if len(self.sync_store.get_peers_that_have_peak(peaks)) < 3:
|
||
if self._shut_down:
|
||
return None
|
||
await asyncio.sleep(0.1)
|
||
continue
|
||
break
|
||
|
||
self.log.info(f"Collected a total of {len(peaks)} peaks.")
|
||
|
||
# Based on responses from peers about the current peaks, see which peak is the heaviest
|
||
# (similar to longest chain rule).
|
||
target_peak = self.sync_store.get_heaviest_peak()
|
||
|
||
if target_peak is None:
|
||
raise RuntimeError("Not performing sync, no peaks collected")
|
||
|
||
# Snapshot advertisers of the target peak before the first await. A
|
||
# peer that overwrites its peer_to_peak entry with a subsequent
|
||
# NewPeak during the peer-confirmation round-trip below would
|
||
# otherwise slip past a (header_hash, weight) lookup at ban time.
|
||
weight_liars = self.sync_store.get_advertisers_of_peak(target_peak)
|
||
|
||
self.sync_store.target_peak = target_peak
|
||
|
||
self.log.info(f"Selected peak {target_peak}")
|
||
# Check which peers are updated to this height
|
||
|
||
peers = self.server.get_connections(NodeType.FULL_NODE)
|
||
coroutines = []
|
||
for peer in peers:
|
||
coroutines.append(
|
||
peer.call_api(
|
||
FullNodeAPI.request_block,
|
||
full_node_protocol.RequestBlock(target_peak.height, True),
|
||
timeout=10,
|
||
)
|
||
)
|
||
any_peer_confirmed = False
|
||
for i, target_peak_response in enumerate(await asyncio.gather(*coroutines, return_exceptions=True)):
|
||
if isinstance(target_peak_response, BaseException):
|
||
self.log.warning(
|
||
"Failed to get target peak block from peer %s: %r",
|
||
peers[i].get_peer_logging(),
|
||
target_peak_response,
|
||
)
|
||
if not isinstance(target_peak_response, (TimeoutError, asyncio.CancelledError)):
|
||
await peers[i].close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
continue
|
||
if (
|
||
target_peak_response is not None
|
||
and isinstance(target_peak_response, RespondBlock)
|
||
and target_peak_response.block.header_hash == target_peak.header_hash
|
||
):
|
||
any_peer_confirmed = True
|
||
actual_weight = target_peak_response.block.reward_chain_block.weight
|
||
if actual_weight != target_peak.weight:
|
||
self.log.warning(
|
||
f"Peer-confirmed block weight {actual_weight} differs from "
|
||
f"advertised peak weight {target_peak.weight}, banning weight liars"
|
||
)
|
||
await self._ban_peak_weight_liars(weight_liars)
|
||
raise RuntimeError("Advertised peak weight was a lie, banned offending peers")
|
||
self.sync_store.peer_has_block(
|
||
target_peak.header_hash, peers[i].peer_node_id, actual_weight, target_peak.height, False
|
||
)
|
||
if not any_peer_confirmed:
|
||
self.log.warning(
|
||
f"No peer confirmed the advertised peak {target_peak.header_hash} "
|
||
f"via RequestBlock, banning the peak advertiser(s)"
|
||
)
|
||
await self._ban_peak_weight_liars(weight_liars)
|
||
raise RuntimeError("No peer confirmed the advertised peak")
|
||
fork_point, summaries = await self.request_validate_wp(
|
||
target_peak.header_hash, target_peak.height, target_peak.weight
|
||
)
|
||
# Ensures that the fork point does not change
|
||
async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high):
|
||
await self.blockchain.warmup(fork_point)
|
||
fork_point = await check_fork_next_block(
|
||
self.blockchain,
|
||
fork_point,
|
||
self.get_peers_with_peak(target_peak.header_hash),
|
||
node_next_block_check,
|
||
)
|
||
await self.sync_from_fork_point(fork_point, target_peak.height, target_peak.header_hash, summaries)
|
||
except asyncio.CancelledError:
|
||
self.log.warning("Syncing failed, CancelledError")
|
||
except Exception as e:
|
||
tb = traceback.format_exc()
|
||
self.log.error(f"Error with syncing: {type(e)}{tb}")
|
||
finally:
|
||
if not self._shut_down:
|
||
await self._finish_sync(fork_point)
|
||
|
||
async def request_validate_wp(
|
||
self, peak_header_hash: bytes32, peak_height: uint32, peak_weight: uint128
|
||
) -> tuple[uint32, list[SubEpochSummary]]:
|
||
if self.weight_proof_handler is None:
|
||
raise RuntimeError("Weight proof handler is None")
|
||
peers_with_peak = self.get_peers_with_peak(peak_header_hash)
|
||
# Request weight proof from a random peer
|
||
peers_with_peak_len = len(peers_with_peak)
|
||
self.log.info(f"Total of {peers_with_peak_len} peers with peak {peak_height}")
|
||
# We can't choose from an empty sequence
|
||
if peers_with_peak_len == 0:
|
||
raise RuntimeError(f"Not performing sync, no peers with peak {peak_height}")
|
||
weight_proof_peer: WSChiaConnection = random.choice(peers_with_peak)
|
||
self.log.info(
|
||
f"Requesting weight proof from peer {weight_proof_peer.peer_info.host} up to height {peak_height}"
|
||
)
|
||
cur_peak: BlockRecord | None = self.blockchain.get_peak()
|
||
if cur_peak is not None and peak_weight <= cur_peak.weight:
|
||
raise ValueError("Not performing sync, already caught up.")
|
||
wp_timeout = 360
|
||
if "weight_proof_timeout" in self.config:
|
||
wp_timeout = self.config["weight_proof_timeout"]
|
||
self.log.debug(f"weight proof timeout is {wp_timeout} sec")
|
||
request = full_node_protocol.RequestProofOfWeight(peak_height, peak_header_hash)
|
||
response = await weight_proof_peer.call_api(FullNodeAPI.request_proof_of_weight, request, timeout=wp_timeout)
|
||
# Disconnect from this peer, because they have not behaved properly
|
||
if response is None or not isinstance(response, full_node_protocol.RespondProofOfWeight):
|
||
await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
raise RuntimeError(f"Weight proof did not arrive in time from peer: {weight_proof_peer.peer_info.host}")
|
||
if len(response.wp.recent_chain_data) < _minimum_recent_chain_length(self.constants):
|
||
await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
raise RuntimeError(f"Weight proof recent chain was too short: {weight_proof_peer.peer_info.host}")
|
||
if response.wp.recent_chain_data[-1].reward_chain_block.height != peak_height:
|
||
await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
raise RuntimeError(f"Weight proof had the wrong height: {weight_proof_peer.peer_info.host}")
|
||
if response.wp.recent_chain_data[-1].reward_chain_block.weight != peak_weight:
|
||
await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
raise RuntimeError(f"Weight proof had the wrong weight: {weight_proof_peer.peer_info.host}")
|
||
if self.in_bad_peak_cache(response.wp):
|
||
raise ValueError("Weight proof failed bad peak cache validation")
|
||
# dont sync to wp if local peak is heavier,
|
||
# dont ban peer, we asked for this peak
|
||
current_peak = self.blockchain.get_peak()
|
||
if current_peak is not None:
|
||
if response.wp.recent_chain_data[-1].reward_chain_block.weight <= current_peak.weight:
|
||
raise RuntimeError(
|
||
f"current peak is heavier than Weight proof peek: {weight_proof_peer.peer_info.host}"
|
||
)
|
||
try:
|
||
validated, fork_point, summaries = await self.weight_proof_handler.validate_weight_proof(response.wp)
|
||
except Exception as e:
|
||
await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
raise ValueError(f"Weight proof validation threw an error {e}")
|
||
if not validated:
|
||
await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
raise ValueError("Weight proof validation failed")
|
||
self.log.info(f"Re-checked peers: total of {len(peers_with_peak)} peers with peak {peak_height}")
|
||
self.sync_store.set_sync_mode(True)
|
||
self._state_changed("sync_mode")
|
||
return fork_point, summaries
|
||
|
||
async def _ban_peak_weight_liars(self, weight_liars: set[bytes32]) -> None:
|
||
"""Ban and disconnect the snapshot of peers that advertised the suspect peak.
|
||
|
||
The snapshot is captured synchronously at peak-selection time in `_sync()`
|
||
so a peer cannot escape banning by overwriting its `peer_to_peak` entry
|
||
with a subsequent `NewPeak` during the intervening async work.
|
||
"""
|
||
for peer_id in weight_liars:
|
||
conn = self.server.all_connections.get(peer_id)
|
||
if conn is not None:
|
||
self.log.warning(f"Banning peer {conn.peer_info.host} for advertising inflated weight")
|
||
await conn.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
|
||
async def sync_from_fork_point(
|
||
self,
|
||
fork_point_height: uint32,
|
||
target_peak_sb_height: uint32,
|
||
peak_hash: bytes32,
|
||
summaries: list[SubEpochSummary],
|
||
) -> None:
|
||
self.log.info(f"Start syncing from fork point at {fork_point_height} up to {target_peak_sb_height}")
|
||
batch_size = self.constants.MAX_BLOCK_COUNT_PER_REQUESTS
|
||
counter = 0
|
||
if fork_point_height != 0:
|
||
# warmup the cache
|
||
curr = self.blockchain.height_to_block_record(fork_point_height)
|
||
while (
|
||
curr.sub_epoch_summary_included is None
|
||
or counter < 3 * self.constants.MAX_SUB_SLOT_BLOCKS + self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK + 3
|
||
):
|
||
res = await self.blockchain.get_block_record_from_db(curr.prev_hash)
|
||
if res is None:
|
||
break
|
||
curr = res
|
||
self.blockchain.add_block_record(curr)
|
||
counter += 1
|
||
|
||
# normally "fork_point" or "fork_height" refers to the first common
|
||
# block between the main chain and the fork. Here "fork_point_height"
|
||
# seems to refer to the first diverging block
|
||
# in case we're validating a reorg fork (i.e. not extending the
|
||
# main chain), we need to record the coin set from that fork in
|
||
# fork_info. Otherwise validation is very expensive, especially
|
||
# for deep reorgs
|
||
if fork_point_height > 0:
|
||
fork_hash = self.blockchain.height_to_hash(uint32(fork_point_height - 1))
|
||
assert fork_hash is not None
|
||
else:
|
||
fork_hash = self.constants.GENESIS_CHALLENGE
|
||
fork_info = ForkInfo(fork_point_height - 1, fork_point_height - 1, fork_hash)
|
||
|
||
if fork_point_height == 0:
|
||
ssi = self.constants.SUB_SLOT_ITERS_STARTING
|
||
diff = self.constants.DIFFICULTY_STARTING
|
||
prev_ses_block = None
|
||
else:
|
||
prev_b_hash = self.blockchain.height_to_hash(fork_point_height)
|
||
assert prev_b_hash is not None
|
||
prev_b = await self.blockchain.get_full_block(prev_b_hash)
|
||
assert prev_b is not None
|
||
ssi, diff, prev_ses_block = await self.get_sub_slot_iters_difficulty_ses_block(prev_b, None, None)
|
||
|
||
# we need an augmented blockchain to validate blocks in batches. The
|
||
# batch must be treated as if it's part of the chain to validate the
|
||
# blocks in it. We also need them to keep appearing as if they're part
|
||
# of the chain when pipelining the validation of blocks. We start
|
||
# validating the next batch while still adding the first batch to the
|
||
# chain.
|
||
blockchain = AugmentedBlockchain(self.blockchain)
|
||
peers_with_peak: list[WSChiaConnection] = self.get_peers_with_peak(peak_hash)
|
||
|
||
async def fetch_blocks() -> AsyncIterator[tuple[WSChiaConnection, list[FullBlock]]]:
|
||
# the rate limit for respond_blocks is 100 messages / 60 seconds.
|
||
# But the limit is scaled to 30% for outbound messages, so that's 30
|
||
# messages per 60 seconds.
|
||
# That's 2 seconds per request.
|
||
seconds_per_request = 2
|
||
|
||
# the timestamp of when the next request_block message is allowed to
|
||
# be sent. It's initialized to the current time, and bumped by the
|
||
# seconds_per_request every time we send a request. This ensures we
|
||
# won't exceed the 100 requests / 60 seconds rate limit.
|
||
# Whichever peer has the lowest timestamp is the one we request
|
||
# from. peers that take more than 5 seconds to respond are pushed to
|
||
# the end of the queue, to be less likely to request from.
|
||
|
||
# This should be cleaned up to not be a hard coded value, and maybe
|
||
# allow higher request rates (and align the request_blocks and
|
||
# respond_blocks rate limits).
|
||
now = time.monotonic()
|
||
new_peers_with_peak: list[tuple[WSChiaConnection, float]] = [(c, now) for c in peers_with_peak[:]]
|
||
self.log.info(f"peers with peak: {len(new_peers_with_peak)}")
|
||
random.shuffle(new_peers_with_peak)
|
||
# block request ranges are *inclusive*, this requires some
|
||
# gymnastics of this range (+1 to make it exclusive, like normal
|
||
# ranges) and then -1 when forming the request message
|
||
for start_height in range(fork_point_height, target_peak_sb_height + 1, batch_size):
|
||
end_height = min(target_peak_sb_height, start_height + batch_size - 1)
|
||
request = RequestBlocks(uint32(start_height), uint32(end_height), True)
|
||
new_peers_with_peak.sort(key=lambda pair: pair[1])
|
||
fetched = False
|
||
for idx, (peer, timestamp) in enumerate(new_peers_with_peak):
|
||
if peer.closed:
|
||
continue
|
||
|
||
start = time.monotonic()
|
||
if start < timestamp:
|
||
# rate limit ourselves, since we sent a message to
|
||
# this peer too recently
|
||
await asyncio.sleep(timestamp - start)
|
||
start = time.monotonic()
|
||
|
||
# update the timestamp, now that we're sending a request
|
||
# it's OK for the timestamp to fall behind wall-clock
|
||
# time. It just means we're allowed to send more
|
||
# requests to catch up
|
||
if is_localhost(peer.peer_info.host):
|
||
# we don't apply rate limits to localhost, and our
|
||
# tests depend on it
|
||
bump = 0.1
|
||
else:
|
||
bump = seconds_per_request
|
||
|
||
new_peers_with_peak[idx] = (
|
||
new_peers_with_peak[idx][0],
|
||
new_peers_with_peak[idx][1] + bump,
|
||
)
|
||
# the fewer peers we have, the more willing we should be
|
||
# to wait for them.
|
||
timeout = int(30 + 30 / len(new_peers_with_peak))
|
||
response = await peer.call_api(FullNodeAPI.request_blocks, request, timeout=timeout)
|
||
end = time.monotonic()
|
||
if response is None:
|
||
self.log.info(f"peer timed out after {end - start:.1f} s")
|
||
await peer.close()
|
||
elif isinstance(response, RespondBlocks):
|
||
if not await respond_blocks_or_ban(peer, response, start_height, end_height, self.log):
|
||
continue
|
||
if end - start > 5:
|
||
self.log.info(f"peer took {end - start:.1f} s to respond to request_blocks")
|
||
# this isn't a great peer, reduce its priority
|
||
# to prefer any peers that had to wait for it.
|
||
# By setting the next allowed timestamp to now,
|
||
# means that any other peer that has waited for
|
||
# this will have its next allowed timestamp in
|
||
# the passed, and be preferred multiple times
|
||
# over this peer.
|
||
new_peers_with_peak[idx] = (
|
||
new_peers_with_peak[idx][0],
|
||
end,
|
||
)
|
||
start = time.monotonic()
|
||
yield (peer, response.blocks)
|
||
end = time.monotonic()
|
||
if end - start > 1:
|
||
self.log.info(
|
||
f"sync pipeline back-pressure. stalled {end - start:0.2f} seconds on prevalidate block"
|
||
)
|
||
fetched = True
|
||
break
|
||
if fetched is False:
|
||
self.log.error(f"failed fetching {start_height} to {end_height} from peers")
|
||
return
|
||
if self.sync_store.peers_changed.is_set():
|
||
existing_peers = {id(c): timestamp for c, timestamp in new_peers_with_peak}
|
||
peers = self.get_peers_with_peak(peak_hash)
|
||
new_peers_with_peak = [(c, existing_peers.get(id(c), end)) for c in peers]
|
||
random.shuffle(new_peers_with_peak)
|
||
self.sync_store.peers_changed.clear()
|
||
self.log.info(f"peers with peak: {len(new_peers_with_peak)}")
|
||
|
||
first_batch = True
|
||
vs = ValidationState(ssi, diff, prev_ses_block)
|
||
|
||
async def validate_batch(
|
||
item: tuple[WSChiaConnection, list[FullBlock]],
|
||
) -> tuple[WSChiaConnection, ValidationState, list[Awaitable[PreValidationResult]], list[FullBlock]] | None:
|
||
nonlocal first_batch, blockchain, fork_info
|
||
peer, blocks = item
|
||
|
||
# Keep the augmented chain's MMR snapshot aligned with the
|
||
# underlying blockchain before validating each fetched batch.
|
||
blockchain.mmr_manager = self.blockchain.mmr_manager.copy()
|
||
|
||
# skip_blocks is only relevant at the start of the sync,
|
||
# to skip blocks we already have in the database (and have
|
||
# been validated). Once we start validating blocks, we
|
||
# shouldn't be skipping any.
|
||
blocks_to_validate = await self.skip_blocks(blockchain, blocks, fork_info, vs)
|
||
assert first_batch or len(blocks_to_validate) == len(blocks)
|
||
next_validation_state = copy.copy(vs)
|
||
|
||
if len(blocks_to_validate) == 0:
|
||
return None
|
||
|
||
first_batch = False
|
||
|
||
futures: list[Awaitable[PreValidationResult]] = []
|
||
for block in blocks_to_validate:
|
||
futures.extend(
|
||
await self.prevalidate_blocks(
|
||
blockchain,
|
||
[block],
|
||
vs,
|
||
summaries,
|
||
)
|
||
)
|
||
return (peer, next_validation_state, list(futures), blocks_to_validate)
|
||
|
||
block_rate = 0.0
|
||
block_rate_time = time.monotonic()
|
||
block_rate_height = -1
|
||
|
||
async def ingest_batch(
|
||
item: tuple[WSChiaConnection, ValidationState, list[Awaitable[PreValidationResult]], list[FullBlock]],
|
||
) -> None:
|
||
nonlocal fork_info, block_rate, block_rate_time, block_rate_height
|
||
peer, vs, futures, blocks = item
|
||
start_height = blocks[0].height
|
||
end_height = blocks[-1].height
|
||
|
||
if block_rate_height == -1:
|
||
block_rate_height = start_height
|
||
|
||
pre_validation_results = list(await asyncio.gather(*futures))
|
||
# The ValidationState object (vs) is an in-out parameter. the add_block_batch()
|
||
# call will update it
|
||
state_change_summary, err = await self.add_prevalidated_blocks(
|
||
blockchain,
|
||
blocks,
|
||
pre_validation_results,
|
||
fork_info,
|
||
peer.peer_info,
|
||
vs,
|
||
)
|
||
peak: BlockRecord | None = self.blockchain.get_peak()
|
||
if state_change_summary is not None:
|
||
assert peak is not None
|
||
# Hints must be added to the DB. The other post-processing tasks are not required when syncing.
|
||
# A failed batch can still have committed a valid prefix, whose hints must not be dropped.
|
||
hints_to_add, _ = get_hints_and_subscription_coin_ids(
|
||
state_change_summary,
|
||
self.subscriptions.has_coin_subscription,
|
||
self.subscriptions.has_puzzle_subscription,
|
||
)
|
||
await self.hint_store.add_hints(hints_to_add)
|
||
if err is not None:
|
||
await peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
if state_change_summary is not None:
|
||
committed_end = state_change_summary.peak.height
|
||
self.log.info(
|
||
f"Added blocks {start_height} to {committed_end} "
|
||
f"({block_rate:.3g} blocks/s) (from: {peer.peer_info.ip})"
|
||
)
|
||
raise ValueError(
|
||
f"Failed to validate block batch after height {committed_end} "
|
||
f"(batch {start_height}-{end_height}): {err}"
|
||
)
|
||
raise ValueError(f"Failed to validate block batch {start_height} to {end_height}: {err}")
|
||
if end_height - block_rate_height > 100:
|
||
now = time.monotonic()
|
||
block_rate = (end_height - block_rate_height) / (now - block_rate_time)
|
||
block_rate_time = now
|
||
block_rate_height = end_height
|
||
|
||
committed_end = state_change_summary.peak.height if state_change_summary is not None else end_height
|
||
self.log.info(
|
||
f"Added blocks {start_height} to {committed_end} "
|
||
f"({block_rate:.3g} blocks/s) (from: {peer.peer_info.ip})"
|
||
)
|
||
# Note that end_height is not necessarily the peak at this
|
||
# point. In case of a re-org, it may even be significantly
|
||
# higher than _peak_height, and still not be the peak.
|
||
# clean_block_record() will not necessarily honor this cut-off
|
||
# height, in that case.
|
||
self.blockchain.clean_block_record(end_height - self.constants.BLOCKS_CACHE_SIZE)
|
||
|
||
async def drain_pending_futures(p: TaskPipeline) -> None:
|
||
for item in p.drain(1):
|
||
_, _, futures, _ = item
|
||
await asyncio.gather(*futures)
|
||
|
||
pipeline = TaskPipeline(
|
||
source=fetch_blocks(),
|
||
stages=[validate_batch, ingest_batch],
|
||
queue_size=10,
|
||
names=["fetching", "validating", "ingesting"],
|
||
log=self.log,
|
||
cleanup=drain_pending_futures,
|
||
)
|
||
try:
|
||
await pipeline.run()
|
||
except Exception:
|
||
self.log.exception("sync from fork point failed")
|
||
|
||
def get_peers_with_peak(self, peak_hash: bytes32) -> list[WSChiaConnection]:
|
||
peer_ids: set[bytes32] = self.sync_store.get_peers_that_have_peak([peak_hash])
|
||
if len(peer_ids) == 0:
|
||
self.log.warning(f"Not syncing, no peers with header_hash {peak_hash} ")
|
||
return []
|
||
return [c for c in self.server.all_connections.values() if c.peer_node_id in peer_ids]
|
||
|
||
async def _wallets_sync_task_handler(self) -> None:
|
||
max_attempts = 3
|
||
retry_attempts: dict[tuple[uint32, bytes32], int] = {}
|
||
while not self._shut_down:
|
||
wallet_update: WalletUpdate | None = None
|
||
try:
|
||
wallet_update = await self.wallet_sync_queue.get()
|
||
await self.update_wallets(wallet_update)
|
||
retry_attempts.pop((wallet_update.fork_height, wallet_update.peak.header_hash), None)
|
||
except Exception:
|
||
if wallet_update is None:
|
||
self.log.exception("Wallet sync task failure before receiving update")
|
||
continue
|
||
|
||
update_key = (wallet_update.fork_height, wallet_update.peak.header_hash)
|
||
current_attempt = retry_attempts.get(update_key, 0) + 1
|
||
if current_attempt < max_attempts:
|
||
retry_attempts[update_key] = current_attempt
|
||
self.log.exception(
|
||
"Wallet sync task failure, retrying update %s/%s for peak %s",
|
||
current_attempt,
|
||
max_attempts - 1,
|
||
wallet_update.peak.header_hash.hex(),
|
||
)
|
||
# Bounded backoff reduces retry churn on transient failures.
|
||
await asyncio.sleep(0.1 * (2 ** (current_attempt - 1)))
|
||
await self.wallet_sync_queue.put(wallet_update)
|
||
else:
|
||
retry_attempts.pop(update_key, None)
|
||
self.log.exception(
|
||
"Wallet sync task failure, dropping update after %s attempts for peak %s",
|
||
max_attempts,
|
||
wallet_update.peak.header_hash.hex(),
|
||
)
|
||
continue
|
||
|
||
async def update_wallets(self, wallet_update: WalletUpdate) -> None:
|
||
self.log.debug(
|
||
f"update_wallets - fork_height: {wallet_update.fork_height}, peak_height: {wallet_update.peak.height}"
|
||
)
|
||
changes_for_peer: dict[bytes32, set[CoinState]] = {}
|
||
for coin_record in wallet_update.coin_records:
|
||
coin_id = coin_record.name
|
||
subscribed_peers = self.subscriptions.peers_for_coin_id(coin_id)
|
||
subscribed_peers.update(self.subscriptions.peers_for_puzzle_hash(coin_record.coin.puzzle_hash))
|
||
hint = wallet_update.hints.get(coin_id)
|
||
if hint is not None:
|
||
subscribed_peers.update(self.subscriptions.peers_for_puzzle_hash(hint))
|
||
for peer in subscribed_peers:
|
||
changes_for_peer.setdefault(peer, set()).add(coin_record.coin_state)
|
||
|
||
for peer, changes in changes_for_peer.items():
|
||
connection = self.server.all_connections.get(peer)
|
||
if connection is not None:
|
||
state = CoinStateUpdate(
|
||
wallet_update.peak.height,
|
||
wallet_update.fork_height,
|
||
wallet_update.peak.header_hash,
|
||
list(changes),
|
||
)
|
||
await connection.send_message(make_msg(ProtocolMessageTypes.coin_state_update, state))
|
||
|
||
# Tell wallets about the new peak
|
||
new_peak_message = make_msg(
|
||
ProtocolMessageTypes.new_peak_wallet,
|
||
wallet_protocol.NewPeakWallet(
|
||
wallet_update.peak.header_hash,
|
||
wallet_update.peak.height,
|
||
wallet_update.peak.weight,
|
||
wallet_update.fork_height,
|
||
),
|
||
)
|
||
await self.server.send_to_all([new_peak_message], NodeType.WALLET)
|
||
|
||
async def add_block_batch(
|
||
self,
|
||
all_blocks: list[FullBlock],
|
||
peer_info: PeerInfo,
|
||
fork_info: ForkInfo,
|
||
vs: ValidationState, # in-out parameter
|
||
blockchain: AugmentedBlockchain,
|
||
wp_summaries: list[SubEpochSummary] | None = None,
|
||
) -> tuple[bool, StateChangeSummary | None]:
|
||
# Precondition: All blocks must be contiguous blocks, index i+1 must be the parent of index i
|
||
# Returns a bool for success, as well as a StateChangeSummary if the peak was advanced
|
||
|
||
# Keep the augmented chain's MMR snapshot aligned with the underlying
|
||
# blockchain between batches.
|
||
blockchain.mmr_manager = self.blockchain.mmr_manager.copy()
|
||
|
||
pre_validate_start = time.monotonic()
|
||
blocks_to_validate = await self.skip_blocks(blockchain, all_blocks, fork_info, vs)
|
||
|
||
if len(blocks_to_validate) == 0:
|
||
return True, None
|
||
|
||
futures = await self.prevalidate_blocks(
|
||
blockchain,
|
||
blocks_to_validate,
|
||
copy.copy(vs),
|
||
wp_summaries,
|
||
)
|
||
pre_validation_results = list(await asyncio.gather(*futures))
|
||
|
||
agg_state_change_summary, err = await self.add_prevalidated_blocks(
|
||
blockchain,
|
||
blocks_to_validate,
|
||
pre_validation_results,
|
||
fork_info,
|
||
peer_info,
|
||
vs,
|
||
)
|
||
|
||
if agg_state_change_summary is not None:
|
||
self._state_changed("new_peak")
|
||
self.log.debug(
|
||
f"Total time for {len(blocks_to_validate)} blocks: {time.monotonic() - pre_validate_start}, "
|
||
f"advanced: True"
|
||
)
|
||
return err is None, agg_state_change_summary
|
||
|
||
async def skip_blocks(
|
||
self,
|
||
blockchain: AugmentedBlockchain,
|
||
all_blocks: list[FullBlock],
|
||
fork_info: ForkInfo,
|
||
vs: ValidationState, # in-out parameter
|
||
) -> list[FullBlock]:
|
||
blocks_to_validate: list[FullBlock] = []
|
||
for i, block in enumerate(all_blocks):
|
||
header_hash = block.header_hash
|
||
block_rec = await blockchain.get_block_record_from_db(header_hash)
|
||
if block_rec is None:
|
||
blocks_to_validate = all_blocks[i:]
|
||
break
|
||
else:
|
||
blockchain.add_block_record(block_rec)
|
||
if block_rec.sub_epoch_summary_included:
|
||
# already validated block, update sub slot iters, difficulty and prev sub epoch summary
|
||
vs.prev_ses_block = block_rec
|
||
if block_rec.sub_epoch_summary_included.new_sub_slot_iters is not None:
|
||
vs.ssi = block_rec.sub_epoch_summary_included.new_sub_slot_iters
|
||
if block_rec.sub_epoch_summary_included.new_difficulty is not None:
|
||
vs.difficulty = block_rec.sub_epoch_summary_included.new_difficulty
|
||
|
||
# the below section updates the fork_info object, if
|
||
# there is one.
|
||
if block.height <= fork_info.peak_height:
|
||
continue
|
||
# we have already validated this block once, no need to do it again.
|
||
# however, if this block is not part of the main chain, we need to
|
||
# update the fork context with its additions and removals
|
||
if self.blockchain.height_to_hash(block.height) == header_hash:
|
||
# we're on the main chain, just fast-forward the fork height
|
||
fork_info.reset(block.height, header_hash)
|
||
else:
|
||
# We have already validated the block, but if it's not part of the
|
||
# main chain, we still need to re-run it to update the additions and
|
||
# removals in fork_info.
|
||
await self.blockchain.advance_fork_info(block, fork_info)
|
||
await self.blockchain.run_single_block(block, fork_info)
|
||
return blocks_to_validate
|
||
|
||
async def prevalidate_blocks(
|
||
self,
|
||
blockchain: AugmentedBlockchain,
|
||
blocks_to_validate: list[FullBlock],
|
||
vs: ValidationState,
|
||
wp_summaries: list[SubEpochSummary] | None = None,
|
||
) -> Sequence[Awaitable[PreValidationResult]]:
|
||
"""
|
||
This is a thin wrapper over pre_validate_block().
|
||
|
||
Args:
|
||
blockchain:
|
||
blocks_to_validate:
|
||
vs: The ValidationState for the first block in the batch. This is an in-out
|
||
parameter. It will be updated to be the validation state for the next
|
||
batch of blocks.
|
||
wp_summaries:
|
||
"""
|
||
# Validates signatures in multiprocessing since they take a while, and we don't have cached transactions
|
||
# for these blocks (unlike during normal operation where we validate one at a time)
|
||
# We have to copy the ValidationState object to preserve it for the add_block()
|
||
# call below. pre_validate_block() will update the
|
||
# object we pass in.
|
||
ret: list[Awaitable[PreValidationResult]] = []
|
||
for block in blocks_to_validate:
|
||
ret.append(
|
||
await pre_validate_block(
|
||
self.constants,
|
||
blockchain,
|
||
block,
|
||
self.pool,
|
||
None,
|
||
vs,
|
||
wp_summaries=wp_summaries,
|
||
nice=(20,),
|
||
)
|
||
)
|
||
return ret
|
||
|
||
async def add_prevalidated_blocks(
|
||
self,
|
||
blockchain: AugmentedBlockchain,
|
||
blocks_to_validate: list[FullBlock],
|
||
pre_validation_results: list[PreValidationResult],
|
||
fork_info: ForkInfo,
|
||
peer_info: PeerInfo,
|
||
vs: ValidationState, # in-out parameter
|
||
) -> tuple[StateChangeSummary | None, Err | None]:
|
||
agg_state_change_summary: StateChangeSummary | None = None
|
||
for i, block in enumerate(blocks_to_validate):
|
||
header_hash = block.header_hash
|
||
assert vs.prev_ses_block is None or vs.prev_ses_block.height < block.height
|
||
if pre_validation_results[i].error is not None:
|
||
self.log.error(
|
||
f"prevalidation failed for block {header_hash.hex()} height {block.height} "
|
||
f"from peer {peer_info}: {Err(pre_validation_results[i].error).name}"
|
||
f" {pre_validation_results[i].error_msg or ''}"
|
||
)
|
||
return agg_state_change_summary, Err(pre_validation_results[i].error)
|
||
if pre_validation_results[i].required_iters is None:
|
||
self.log.error(
|
||
f"required_iters is None for block {header_hash.hex()} height {block.height} from peer {peer_info}"
|
||
)
|
||
return agg_state_change_summary, Err.UNKNOWN
|
||
state_change_summary: StateChangeSummary | None
|
||
# when adding blocks in batches, we won't have any overlapping
|
||
# signatures with the mempool. There won't be any cache hits, so
|
||
# there's no need to pass the BLS cache in
|
||
|
||
if len(block.finished_sub_slots) > 0:
|
||
cc_sub_slot = block.finished_sub_slots[0].challenge_chain
|
||
if cc_sub_slot.new_sub_slot_iters is not None or cc_sub_slot.new_difficulty is not None:
|
||
assert cc_sub_slot.new_sub_slot_iters is not None
|
||
vs.ssi = cc_sub_slot.new_sub_slot_iters
|
||
assert cc_sub_slot.new_difficulty is not None
|
||
vs.difficulty = cc_sub_slot.new_difficulty
|
||
block_rec = blockchain.block_record(block.header_hash)
|
||
result, error, state_change_summary = await self.blockchain.add_block(
|
||
block,
|
||
pre_validation_results[i],
|
||
vs.ssi,
|
||
fork_info,
|
||
prev_ses_block=vs.prev_ses_block,
|
||
block_record=block_rec,
|
||
)
|
||
if error is None:
|
||
blockchain.remove_extra_block(header_hash)
|
||
|
||
if result == AddBlockResult.NEW_PEAK:
|
||
# since this block just added a new peak, we've don't need any
|
||
# fork history from fork_info anymore
|
||
fork_info.reset(block.height, header_hash)
|
||
assert state_change_summary is not None
|
||
# Since all blocks are contiguous, we can simply append the rollback changes and npc results
|
||
if agg_state_change_summary is None:
|
||
agg_state_change_summary = state_change_summary
|
||
else:
|
||
# Keeps the old, original fork_height, since the next blocks will have fork height h-1
|
||
# Groups up all state changes into one
|
||
agg_state_change_summary = StateChangeSummary(
|
||
state_change_summary.peak,
|
||
agg_state_change_summary.fork_height,
|
||
agg_state_change_summary.rolled_back_records + state_change_summary.rolled_back_records,
|
||
agg_state_change_summary.removals + state_change_summary.removals,
|
||
agg_state_change_summary.additions + state_change_summary.additions,
|
||
agg_state_change_summary.new_rewards + state_change_summary.new_rewards,
|
||
)
|
||
elif result in {AddBlockResult.INVALID_BLOCK, AddBlockResult.DISCONNECTED_BLOCK}:
|
||
if error is not None:
|
||
self.log.error(f"Error: {error}, Invalid block from peer: {peer_info} ")
|
||
return agg_state_change_summary, error
|
||
block_record = block_rec
|
||
assert block_record is not None
|
||
if block_record.sub_epoch_summary_included is not None:
|
||
vs.prev_ses_block = block_record
|
||
if self.weight_proof_handler is not None:
|
||
await self.weight_proof_handler.create_prev_sub_epoch_segments()
|
||
if agg_state_change_summary is not None:
|
||
self._state_changed("new_peak")
|
||
return agg_state_change_summary, None
|
||
|
||
async def get_sub_slot_iters_difficulty_ses_block(
|
||
self, block: FullBlock, ssi: uint64 | None, diff: uint64 | None
|
||
) -> tuple[uint64, uint64, BlockRecord | None]:
|
||
prev_ses_block = None
|
||
if ssi is None or diff is None:
|
||
if block.height == 0:
|
||
ssi = self.constants.SUB_SLOT_ITERS_STARTING
|
||
diff = self.constants.DIFFICULTY_STARTING
|
||
if ssi is None or diff is None:
|
||
if len(block.finished_sub_slots) > 0:
|
||
if block.finished_sub_slots[0].challenge_chain.new_difficulty is not None:
|
||
diff = block.finished_sub_slots[0].challenge_chain.new_difficulty
|
||
if block.finished_sub_slots[0].challenge_chain.new_sub_slot_iters is not None:
|
||
ssi = block.finished_sub_slots[0].challenge_chain.new_sub_slot_iters
|
||
|
||
if block.height > 0:
|
||
prev_b = await self.blockchain.get_block_record_from_db(block.prev_header_hash)
|
||
curr = prev_b
|
||
while prev_ses_block is None or ssi is None or diff is None:
|
||
assert curr is not None
|
||
if curr.height == 0:
|
||
if ssi is None or diff is None:
|
||
ssi = self.constants.SUB_SLOT_ITERS_STARTING
|
||
diff = self.constants.DIFFICULTY_STARTING
|
||
if prev_ses_block is None:
|
||
prev_ses_block = curr
|
||
if curr.sub_epoch_summary_included is not None:
|
||
if prev_ses_block is None:
|
||
prev_ses_block = curr
|
||
if ssi is None or diff is None:
|
||
if curr.sub_epoch_summary_included.new_difficulty is not None:
|
||
diff = curr.sub_epoch_summary_included.new_difficulty
|
||
if curr.sub_epoch_summary_included.new_sub_slot_iters is not None:
|
||
ssi = curr.sub_epoch_summary_included.new_sub_slot_iters
|
||
curr = await self.blockchain.get_block_record_from_db(curr.prev_hash)
|
||
assert ssi is not None
|
||
assert diff is not None
|
||
return ssi, diff, prev_ses_block
|
||
|
||
async def _finish_sync(self, fork_point: uint32 | None) -> None:
|
||
"""
|
||
Finalize sync by setting sync mode to False, clearing all sync information, and adding any final
|
||
blocks that we have finalized recently.
|
||
"""
|
||
self.log.info("long sync done")
|
||
self.sync_store.set_long_sync(False)
|
||
self.sync_store.set_sync_mode(False)
|
||
self._state_changed("sync_mode")
|
||
if self._server is None:
|
||
return None
|
||
|
||
async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high):
|
||
peak: BlockRecord | None = self.blockchain.get_peak()
|
||
peak_fb: FullBlock | None = await self.blockchain.get_full_peak()
|
||
if peak_fb is not None:
|
||
if fork_point is None:
|
||
fork_point = uint32(max(peak_fb.height - 1, 0))
|
||
assert peak is not None
|
||
state_change_summary = StateChangeSummary(peak, fork_point, [], [], [], [])
|
||
ppp_result: PeakPostProcessingResult = await self.peak_post_processing(
|
||
peak_fb, state_change_summary, None
|
||
)
|
||
|
||
if peak_fb is not None:
|
||
# Call outside of priority_mutex to encourage concurrency
|
||
await self.peak_post_processing_2(peak_fb, None, state_change_summary, ppp_result)
|
||
|
||
if peak is not None and self.weight_proof_handler is not None:
|
||
await self.weight_proof_handler.get_proof_of_weight(peak.header_hash)
|
||
self._state_changed("block")
|
||
|
||
def has_valid_pool_sig(self, block: UnfinishedBlock | FullBlock) -> bool:
|
||
if (
|
||
block.foliage.foliage_block_data.pool_target
|
||
== PoolTarget(self.constants.GENESIS_PRE_FARM_POOL_PUZZLE_HASH, uint32(0))
|
||
and block.foliage.prev_block_hash != self.constants.GENESIS_CHALLENGE
|
||
and block.reward_chain_block.proof_of_space.pool_public_key is not None
|
||
):
|
||
assert block.foliage.foliage_block_data.pool_signature is not None
|
||
if not AugSchemeMPL.verify(
|
||
block.reward_chain_block.proof_of_space.pool_public_key,
|
||
bytes(block.foliage.foliage_block_data.pool_target),
|
||
block.foliage.foliage_block_data.pool_signature,
|
||
):
|
||
return False
|
||
return True
|
||
|
||
async def signage_point_post_processing(
|
||
self,
|
||
request: full_node_protocol.RespondSignagePoint,
|
||
peer: WSChiaConnection,
|
||
ip_sub_slot: EndOfSubSlotBundle | None,
|
||
) -> None:
|
||
self.log.info(
|
||
f"⏲️ Finished signage point {request.index_from_challenge}/"
|
||
f"{self.constants.NUM_SPS_SUB_SLOT}: "
|
||
f"CC: {request.challenge_chain_vdf.output.get_hash().hex()} "
|
||
f"RC: {request.reward_chain_vdf.output.get_hash().hex()} "
|
||
)
|
||
self.signage_point_times[request.index_from_challenge] = time.time()
|
||
sub_slot_tuple = self.full_node_store.get_sub_slot(request.challenge_chain_vdf.challenge)
|
||
prev_challenge: bytes32 | None
|
||
if sub_slot_tuple is not None:
|
||
prev_challenge = sub_slot_tuple[0].challenge_chain.challenge_chain_end_of_slot_vdf.challenge
|
||
else:
|
||
prev_challenge = None
|
||
|
||
# Notify nodes of the new signage point
|
||
broadcast = full_node_protocol.NewSignagePointOrEndOfSubSlot(
|
||
prev_challenge,
|
||
request.challenge_chain_vdf.challenge,
|
||
request.index_from_challenge,
|
||
request.reward_chain_vdf.challenge,
|
||
)
|
||
msg = make_msg(ProtocolMessageTypes.new_signage_point_or_end_of_sub_slot, broadcast)
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE, peer.peer_node_id)
|
||
|
||
peak = self.blockchain.get_peak()
|
||
if peak is not None and peak.height > self.constants.MAX_SUB_SLOT_BLOCKS:
|
||
sub_slot_iters = peak.sub_slot_iters
|
||
difficulty = uint64(peak.weight - self.blockchain.block_record(peak.prev_hash).weight)
|
||
# Makes sure to potentially update the difficulty if we are past the peak (into a new sub-slot)
|
||
assert ip_sub_slot is not None
|
||
if request.challenge_chain_vdf.challenge != ip_sub_slot.challenge_chain.get_hash():
|
||
sub_slot_iters, difficulty = self.blockchain.get_next_sub_slot_iters_and_difficulty(
|
||
peak.header_hash, True
|
||
)
|
||
else:
|
||
difficulty = self.constants.DIFFICULTY_STARTING
|
||
sub_slot_iters = self.constants.SUB_SLOT_ITERS_STARTING
|
||
|
||
tx_peak = self.blockchain.get_tx_peak()
|
||
# Get filter_challenge for V2 plot filter (challenge hash of a previously
|
||
# completed sub-slot
|
||
filter_challenge = self.full_node_store.get_filter_challenge(
|
||
request.challenge_chain_vdf.challenge,
|
||
request.index_from_challenge,
|
||
)
|
||
# Notify farmers of the new signage point
|
||
broadcast_farmer = farmer_protocol.NewSignagePoint(
|
||
request.challenge_chain_vdf.challenge,
|
||
request.challenge_chain_vdf.output.get_hash(),
|
||
request.reward_chain_vdf.output.get_hash(),
|
||
difficulty,
|
||
sub_slot_iters,
|
||
request.index_from_challenge,
|
||
uint32(0) if peak is None else peak.height,
|
||
tx_peak.height if tx_peak is not None else uint32(0),
|
||
sp_source_data=SignagePointSourceData(
|
||
vdf_data=SPVDFSourceData(request.challenge_chain_vdf.output, request.reward_chain_vdf.output)
|
||
),
|
||
filter_challenge=filter_challenge,
|
||
)
|
||
msg = make_msg(ProtocolMessageTypes.new_signage_point, broadcast_farmer)
|
||
await self.server.send_to_all([msg], NodeType.FARMER)
|
||
|
||
self._state_changed("signage_point", {"broadcast_farmer": broadcast_farmer})
|
||
|
||
async def peak_post_processing(
|
||
self,
|
||
block: FullBlock,
|
||
state_change_summary: StateChangeSummary,
|
||
peer: WSChiaConnection | None,
|
||
) -> PeakPostProcessingResult:
|
||
"""
|
||
Must be called under self.blockchain.priority_mutex. This updates the internal state of the full node with the
|
||
latest peak information. It also notifies peers about the new peak.
|
||
"""
|
||
|
||
record = state_change_summary.peak
|
||
sub_slot_iters, difficulty = self.blockchain.get_next_sub_slot_iters_and_difficulty(record.header_hash, False)
|
||
|
||
generator_bytes = get_transactions_generator_bytes(block)
|
||
generator_size: int | str = len(generator_bytes) if generator_bytes is not None else "No tx"
|
||
generator_refs_size: int | str = (
|
||
len(block.transactions_generator_ref_list) if generator_bytes is not None else "No tx"
|
||
)
|
||
self.log.info(
|
||
f"🌱 Updated peak to height {record.height}, weight {record.weight}, "
|
||
f"hh {record.header_hash.hex()}, "
|
||
f"ph {record.prev_hash.hex()}, "
|
||
f"forked at {state_change_summary.fork_height}, rh: {record.reward_infusion_new_challenge.hex()}, "
|
||
f"total iters: {record.total_iters}, "
|
||
f"overflow: {record.overflow}, "
|
||
f"deficit: {record.deficit}, "
|
||
f"difficulty: {difficulty}, "
|
||
f"sub slot iters: {sub_slot_iters}, "
|
||
f"Generator size: {generator_size}, "
|
||
f"Generator ref list size: {generator_refs_size}"
|
||
)
|
||
|
||
hints_to_add, lookup_coin_ids = get_hints_and_subscription_coin_ids(
|
||
state_change_summary,
|
||
self.subscriptions.has_coin_subscription,
|
||
self.subscriptions.has_puzzle_subscription,
|
||
)
|
||
await self.hint_store.add_hints(hints_to_add)
|
||
|
||
sub_slots = await self.blockchain.get_sp_and_ip_sub_slots(record.header_hash)
|
||
assert sub_slots is not None
|
||
|
||
if not self.sync_store.get_sync_mode():
|
||
self.blockchain.clean_block_records()
|
||
|
||
fork_block: BlockRecord | None = None
|
||
if state_change_summary.fork_height != block.height - 1 and block.height != 0:
|
||
# This is a reorg
|
||
fork_hash: bytes32 | None = self.blockchain.height_to_hash(state_change_summary.fork_height)
|
||
assert fork_hash is not None
|
||
fork_block = await self.blockchain.get_block_record_from_db(fork_hash)
|
||
|
||
fns_peak_result: FullNodeStorePeakResult = self.full_node_store.new_peak(
|
||
record,
|
||
block,
|
||
sub_slots[0],
|
||
sub_slots[1],
|
||
fork_block,
|
||
self.blockchain,
|
||
sub_slot_iters,
|
||
difficulty,
|
||
)
|
||
|
||
signage_points: list[tuple[RespondSignagePoint, WSChiaConnection, EndOfSubSlotBundle | None]] = []
|
||
if peer is not None:
|
||
for index, sp in fns_peak_result.new_signage_points:
|
||
assert (
|
||
sp.cc_vdf is not None
|
||
and sp.cc_proof is not None
|
||
and sp.rc_vdf is not None
|
||
and sp.rc_proof is not None
|
||
)
|
||
# Collect the data for networking outside the mutex
|
||
signage_points.append(
|
||
(
|
||
RespondSignagePoint(index, sp.cc_vdf, sp.cc_proof, sp.rc_vdf, sp.rc_proof),
|
||
peer,
|
||
sub_slots[1],
|
||
)
|
||
)
|
||
|
||
if sub_slots[1] is None:
|
||
assert record.ip_sub_slot_total_iters(self.constants) == 0
|
||
# Ensure the signage point is also in the store, for consistency
|
||
self.full_node_store.new_signage_point(
|
||
record.signage_point_index,
|
||
self.blockchain,
|
||
record,
|
||
record.sub_slot_iters,
|
||
SignagePoint(
|
||
block.reward_chain_block.challenge_chain_sp_vdf,
|
||
block.challenge_chain_sp_proof,
|
||
block.reward_chain_block.reward_chain_sp_vdf,
|
||
block.reward_chain_sp_proof,
|
||
),
|
||
skip_vdf_validation=True,
|
||
)
|
||
|
||
# Update the mempool (returns successful pending transactions added to the mempool)
|
||
spent_coins: list[bytes32] = [coin_id for coin_id, _ in state_change_summary.removals]
|
||
mempool_new_peak_result = await self.mempool_manager.new_peak(self.blockchain.get_tx_peak(), spent_coins)
|
||
|
||
return PeakPostProcessingResult(
|
||
mempool_new_peak_result.spend_bundle_ids,
|
||
mempool_new_peak_result.removals,
|
||
fns_peak_result,
|
||
hints_to_add,
|
||
lookup_coin_ids,
|
||
signage_points=signage_points,
|
||
)
|
||
|
||
async def peak_post_processing_2(
|
||
self,
|
||
block: FullBlock,
|
||
peer: WSChiaConnection | None,
|
||
state_change_summary: StateChangeSummary,
|
||
ppp_result: PeakPostProcessingResult,
|
||
) -> None:
|
||
"""
|
||
Does NOT need to be called under the blockchain lock. Handle other parts of post processing like communicating
|
||
with peers
|
||
"""
|
||
record = state_change_summary.peak
|
||
for signage_point in ppp_result.signage_points:
|
||
await self.signage_point_post_processing(*signage_point)
|
||
for transaction_id in ppp_result.mempool_peak_added_tx_ids:
|
||
self.log.debug(f"Added transaction to mempool: {transaction_id}")
|
||
mempool_item = self.mempool_manager.get_mempool_item(transaction_id)
|
||
assert mempool_item is not None
|
||
await self.broadcast_added_tx(mempool_item)
|
||
|
||
# If there were pending end of slots that happen after this peak, broadcast them if they are added
|
||
if ppp_result.fns_peak_result.added_eos is not None:
|
||
broadcast = full_node_protocol.NewSignagePointOrEndOfSubSlot(
|
||
ppp_result.fns_peak_result.added_eos.challenge_chain.challenge_chain_end_of_slot_vdf.challenge,
|
||
ppp_result.fns_peak_result.added_eos.challenge_chain.get_hash(),
|
||
uint8(0),
|
||
ppp_result.fns_peak_result.added_eos.reward_chain.end_of_slot_vdf.challenge,
|
||
)
|
||
msg = make_msg(ProtocolMessageTypes.new_signage_point_or_end_of_sub_slot, broadcast)
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE)
|
||
|
||
# TODO: maybe add and broadcast new IPs as well
|
||
|
||
# Flush any expired future-cache entries that weren't already cleaned
|
||
# up during append (safety net for quiescent periods).
|
||
self.full_node_store.clear_old_cache_entries()
|
||
|
||
if self.sync_store.get_sync_mode() is False:
|
||
await self.send_peak_to_timelords(block)
|
||
await self.broadcast_removed_tx(ppp_result.mempool_removals)
|
||
|
||
# Tell full nodes about the new peak
|
||
msg = make_msg(
|
||
ProtocolMessageTypes.new_peak,
|
||
full_node_protocol.NewPeak(
|
||
record.header_hash,
|
||
record.height,
|
||
record.weight,
|
||
state_change_summary.fork_height,
|
||
block.reward_chain_block.get_unfinished().get_hash(),
|
||
),
|
||
)
|
||
if peer is not None:
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE, peer.peer_node_id)
|
||
else:
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE)
|
||
|
||
coin_hints: dict[bytes32, bytes32] = {
|
||
coin_id: bytes32(hint) for coin_id, hint in ppp_result.hints if len(hint) == 32
|
||
}
|
||
|
||
peak = Peak(
|
||
state_change_summary.peak.header_hash, state_change_summary.peak.height, state_change_summary.peak.weight
|
||
)
|
||
|
||
# Looks up coin records in DB for the coins that wallets are interested in
|
||
new_states = await self.coin_store.get_coin_records(ppp_result.lookup_coin_ids)
|
||
|
||
await self.wallet_sync_queue.put(
|
||
WalletUpdate(
|
||
state_change_summary.fork_height,
|
||
peak,
|
||
state_change_summary.rolled_back_records + new_states,
|
||
coin_hints,
|
||
)
|
||
)
|
||
|
||
self._state_changed("new_peak")
|
||
|
||
async def add_block(
|
||
self,
|
||
block: FullBlock,
|
||
peer: WSChiaConnection | None = None,
|
||
bls_cache: BLSCache | None = None,
|
||
raise_on_disconnected: bool = False,
|
||
fork_info: ForkInfo | None = None,
|
||
) -> Message | None:
|
||
"""
|
||
Add a full block from a peer full node (or ourselves).
|
||
"""
|
||
if self.sync_store.get_sync_mode():
|
||
return None
|
||
|
||
# Adds the block to seen, and check if it's seen before (which means header is in memory)
|
||
header_hash = block.header_hash
|
||
if self.blockchain.contains_block(header_hash, block.height):
|
||
if fork_info is not None:
|
||
await self.blockchain.run_single_block(block, fork_info)
|
||
return None
|
||
|
||
pre_validation_result: PreValidationResult | None = None
|
||
if (
|
||
block.is_transaction_block()
|
||
and block.transactions_info is not None
|
||
and block.transactions_info.generator_root != bytes([0] * 32)
|
||
and not block_has_transactions_generator(block)
|
||
):
|
||
# This is the case where we already had the unfinished block, and asked for this block without
|
||
# the transactions (since we already had them). Therefore, here we add the transactions.
|
||
pos = block.reward_chain_block.proof_of_space
|
||
if pos.version == 1 and pos.quality_string() is None:
|
||
raise ConsensusError(Err.INVALID_POSPACE)
|
||
unfinished_rh: bytes32 = block.reward_chain_block.get_unfinished().get_hash()
|
||
foliage_hash: bytes32 | None = block.foliage.foliage_transaction_block_hash
|
||
assert foliage_hash is not None
|
||
unf_entry: UnfinishedBlockEntry | None = self.full_node_store.get_unfinished_block_result(
|
||
unfinished_rh, foliage_hash
|
||
)
|
||
assert unf_entry is None or unf_entry.result is None or unf_entry.result.validated_signature is True
|
||
if (
|
||
unf_entry is not None
|
||
and unf_entry.unfinished_block is not None
|
||
and block_has_transactions_generator(unf_entry.unfinished_block)
|
||
and unf_entry.unfinished_block.foliage_transaction_block == block.foliage_transaction_block
|
||
):
|
||
# We checked that the transaction block is the same, therefore all transactions and the signature
|
||
# must be identical in the unfinished and finished blocks. We can therefore use the cache.
|
||
|
||
# this is a transaction block, the foliage hash should be set
|
||
assert foliage_hash is not None
|
||
pre_validation_result = unf_entry.result
|
||
assert pre_validation_result is not None
|
||
block = block.replace(
|
||
transactions_generator=unf_entry.unfinished_block.transactions_generator,
|
||
transactions_generator_ref_list=unf_entry.unfinished_block.transactions_generator_ref_list,
|
||
transactions_generator_buffer=unf_entry.unfinished_block.transactions_generator_buffer,
|
||
version=unf_entry.unfinished_block.version,
|
||
)
|
||
else:
|
||
# We still do not have the correct information for this block, perhaps there is a duplicate block
|
||
# with the same unfinished block hash in the cache, so we need to fetch the correct one
|
||
if peer is None:
|
||
return None
|
||
|
||
block_response: Any | None = await peer.call_api(
|
||
FullNodeAPI.request_block, full_node_protocol.RequestBlock(block.height, True)
|
||
)
|
||
if block_response is None or not isinstance(block_response, full_node_protocol.RespondBlock):
|
||
self.log.warning(
|
||
f"Was not able to fetch the correct block for height {block.height} {block_response}"
|
||
)
|
||
return None
|
||
new_block: FullBlock = block_response.block
|
||
if new_block.foliage_transaction_block != block.foliage_transaction_block:
|
||
self.log.warning(
|
||
f"Received the wrong block for height {block.height} {new_block.header_hash.hex()}"
|
||
)
|
||
return None
|
||
assert block_has_transactions_generator(new_block)
|
||
|
||
self.log.debug(
|
||
f"Wrong info in the cache for bh {new_block.header_hash.hex()}, "
|
||
f"there might be multiple blocks from the "
|
||
f"same farmer with the same pospace."
|
||
)
|
||
# This recursion ends here, we cannot recurse again because the generator is present
|
||
return await self.add_block(new_block, peer, bls_cache)
|
||
state_change_summary: StateChangeSummary | None = None
|
||
ppp_result: PeakPostProcessingResult | None = None
|
||
async with (
|
||
self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high),
|
||
enable_profiler(self.profile_block_validation) as pr,
|
||
):
|
||
# After acquiring the lock, check again, because another asyncio thread might have added it
|
||
if self.blockchain.contains_block(header_hash, block.height):
|
||
if fork_info is not None:
|
||
await self.blockchain.run_single_block(block, fork_info)
|
||
return None
|
||
validation_start = time.monotonic()
|
||
# Tries to add the block to the blockchain, if we already validated transactions, don't do it again
|
||
conds = None
|
||
if pre_validation_result is not None and pre_validation_result.conds is not None:
|
||
conds = pre_validation_result.conds
|
||
|
||
# Don't validate signatures because we want to validate them in the main thread later, since we have a
|
||
# cache available
|
||
prev_b = None
|
||
prev_ses_block = None
|
||
if block.height > 0:
|
||
prev_b = await self.blockchain.get_block_record_from_db(block.prev_header_hash)
|
||
if prev_b is None:
|
||
raise ValueError(f"Previous block record not found for {block.prev_header_hash}")
|
||
curr = prev_b
|
||
while curr.height > 0 and curr.sub_epoch_summary_included is None:
|
||
curr = self.blockchain.block_record(curr.prev_hash)
|
||
prev_ses_block = curr
|
||
new_slot = len(block.finished_sub_slots) > 0
|
||
ssi, diff = get_next_sub_slot_iters_and_difficulty(self.constants, new_slot, prev_b, self.blockchain)
|
||
future = await pre_validate_block(
|
||
self.blockchain.constants,
|
||
AugmentedBlockchain(self.blockchain),
|
||
block,
|
||
self.pool,
|
||
conds,
|
||
ValidationState(ssi, diff, prev_ses_block),
|
||
)
|
||
pre_validation_result = await future
|
||
added: AddBlockResult | None = None
|
||
add_block_start = time.monotonic()
|
||
pre_validation_time = add_block_start - validation_start
|
||
try:
|
||
if pre_validation_result.error is not None:
|
||
if Err(pre_validation_result.error) == Err.INVALID_PREV_BLOCK_HASH:
|
||
added = AddBlockResult.DISCONNECTED_BLOCK
|
||
error_code: Err | None = Err.INVALID_PREV_BLOCK_HASH
|
||
elif Err(pre_validation_result.error) == Err.TIMESTAMP_TOO_FAR_IN_FUTURE:
|
||
raise TimestampError
|
||
else:
|
||
raise ValueError(
|
||
f"Failed to validate block {header_hash} height "
|
||
f"{block.height}: {Err(pre_validation_result.error).name}"
|
||
f" {pre_validation_result.error_msg or ''}"
|
||
)
|
||
else:
|
||
if fork_info is None:
|
||
fork_info = ForkInfo(block.height - 1, block.height - 1, block.prev_header_hash)
|
||
(added, error_code, state_change_summary) = await self.blockchain.add_block(
|
||
block, pre_validation_result, ssi, fork_info
|
||
)
|
||
add_block_time = time.monotonic() - add_block_start
|
||
if added == AddBlockResult.ALREADY_HAVE_BLOCK:
|
||
return None
|
||
elif added == AddBlockResult.INVALID_BLOCK:
|
||
assert error_code is not None
|
||
self.log.error(f"Block {header_hash} at height {block.height} is invalid with code {error_code}.")
|
||
raise ConsensusError(error_code, [header_hash])
|
||
elif added == AddBlockResult.DISCONNECTED_BLOCK:
|
||
self.log.info(f"Disconnected block {header_hash} at height {block.height}")
|
||
if raise_on_disconnected:
|
||
raise RuntimeError("Expected block to be added, received disconnected block.")
|
||
return None
|
||
elif added == AddBlockResult.NEW_PEAK:
|
||
# Evict any related BLS cache entries as we no longer need them
|
||
if bls_cache is not None and pre_validation_result.conds is not None:
|
||
pairs_pks, pairs_msgs = pkm_pairs(
|
||
pre_validation_result.conds, self.constants.AGG_SIG_ME_ADDITIONAL_DATA
|
||
)
|
||
bls_cache.evict(pairs_pks, pairs_msgs)
|
||
# Only propagate blocks which extend the blockchain (becomes one of the heads)
|
||
assert state_change_summary is not None
|
||
post_process_time = time.monotonic()
|
||
ppp_result = await self.peak_post_processing(block, state_change_summary, peer)
|
||
post_process_time = time.monotonic() - post_process_time
|
||
|
||
elif added == AddBlockResult.ADDED_AS_ORPHAN:
|
||
self.log.info(
|
||
f"Received orphan block of height {block.height} rh {block.reward_chain_block.get_hash()}"
|
||
)
|
||
post_process_time = 0.0
|
||
else:
|
||
# Should never reach here, all the cases are covered
|
||
raise RuntimeError(f"Invalid result from add_block {added}")
|
||
except asyncio.CancelledError:
|
||
# We need to make sure to always call this method even when we get a cancel exception, to make sure
|
||
# the node stays in sync
|
||
if added == AddBlockResult.NEW_PEAK:
|
||
assert state_change_summary is not None
|
||
await self.peak_post_processing(block, state_change_summary, peer)
|
||
raise
|
||
|
||
validation_time = time.monotonic() - validation_start
|
||
|
||
if ppp_result is not None:
|
||
assert state_change_summary is not None
|
||
post_process_time2 = time.monotonic()
|
||
await self.peak_post_processing_2(block, peer, state_change_summary, ppp_result)
|
||
post_process_time2 = time.monotonic() - post_process_time2
|
||
else:
|
||
post_process_time2 = 0.0
|
||
|
||
percent_full_str = (
|
||
(
|
||
", percent full: "
|
||
+ str(round(100.0 * float(block.transactions_info.cost) / self.constants.MAX_BLOCK_COST_CLVM, 3))
|
||
+ "%"
|
||
)
|
||
if block.transactions_info is not None
|
||
else ""
|
||
)
|
||
self.log.log(
|
||
logging.WARNING if validation_time > 2 else logging.DEBUG,
|
||
f"Block validation: {validation_time:0.2f}s, "
|
||
f"pre_validation: {pre_validation_time:0.2f}s, "
|
||
f"CLVM: {pre_validation_result.timing / 1000.0:0.2f}s, "
|
||
f"add-block: {add_block_time:0.2f}s, "
|
||
f"post-process: {post_process_time:0.2f}s, "
|
||
f"post-process2: {post_process_time2:0.2f}s, "
|
||
f"cost: {block.transactions_info.cost if block.transactions_info is not None else 'None'}"
|
||
f"{percent_full_str} header_hash: {header_hash.hex()} height: {block.height}",
|
||
)
|
||
|
||
# this is not covered by any unit tests as it's essentially test code
|
||
# itself. It's exercised manually when investigating performance issues
|
||
if validation_time > 2 and pr is not None: # pragma: no cover
|
||
pr.create_stats()
|
||
profile_dir = path_from_root(self.root_path, "block-validation-profile")
|
||
pr.dump_stats(profile_dir / f"{block.height}-{validation_time:0.1f}.profile")
|
||
|
||
# This code path is reached if added == ADDED_AS_ORPHAN or NEW_TIP
|
||
peak = self.blockchain.get_peak()
|
||
assert peak is not None
|
||
|
||
# Removes all temporary data for old blocks
|
||
clear_height = uint32(max(0, peak.height - 50))
|
||
self.full_node_store.clear_candidate_blocks_below(clear_height)
|
||
self.full_node_store.clear_unfinished_blocks_below(clear_height)
|
||
|
||
state_changed_data: dict[str, Any] = {
|
||
"transaction_block": False,
|
||
"k_size": block.reward_chain_block.proof_of_space.param().size_v1,
|
||
"strength": block.reward_chain_block.proof_of_space.param().strength_v2,
|
||
"header_hash": block.header_hash,
|
||
"fork_height": None,
|
||
"rolled_back_records": None,
|
||
"height": block.height,
|
||
"validation_time": validation_time,
|
||
"pre_validation_time": pre_validation_time,
|
||
}
|
||
|
||
if state_change_summary is not None:
|
||
state_changed_data["fork_height"] = state_change_summary.fork_height
|
||
state_changed_data["rolled_back_records"] = len(state_change_summary.rolled_back_records)
|
||
|
||
if block.transactions_info is not None:
|
||
state_changed_data["transaction_block"] = True
|
||
state_changed_data["block_cost"] = block.transactions_info.cost
|
||
state_changed_data["block_fees"] = block.transactions_info.fees
|
||
|
||
if block.foliage_transaction_block is not None:
|
||
state_changed_data["timestamp"] = block.foliage_transaction_block.timestamp
|
||
|
||
generator_bytes = get_transactions_generator_bytes(block)
|
||
if generator_bytes is not None:
|
||
state_changed_data["transaction_generator_size_bytes"] = len(generator_bytes)
|
||
|
||
state_changed_data["transaction_generator_ref_list"] = block.transactions_generator_ref_list
|
||
if added is not None:
|
||
state_changed_data["receive_block_result"] = added.value
|
||
|
||
self._state_changed("block", state_changed_data)
|
||
|
||
record = self.blockchain.block_record(block.header_hash)
|
||
if self.weight_proof_handler is not None and record.sub_epoch_summary_included is not None:
|
||
self._segment_task_list.append(
|
||
create_referenced_task(self.weight_proof_handler.create_prev_sub_epoch_segments())
|
||
)
|
||
for task in self._segment_task_list[:]:
|
||
if task.done():
|
||
self._segment_task_list.remove(task)
|
||
return None
|
||
|
||
async def add_unfinished_block(
|
||
self,
|
||
block: UnfinishedBlock,
|
||
peer: WSChiaConnection | None,
|
||
farmed_block: bool = False,
|
||
) -> None:
|
||
"""
|
||
We have received an unfinished block, either created by us, or from another peer.
|
||
We can validate and add it and if it's a good block, propagate it to other peers and
|
||
timelords.
|
||
"""
|
||
receive_time = time.time()
|
||
|
||
if (
|
||
block.prev_header_hash != self.constants.GENESIS_CHALLENGE
|
||
and self.blockchain.try_block_record(block.prev_header_hash) is None
|
||
):
|
||
# No need to request the parent, since the peer will send it to us anyway, via NewPeak
|
||
self.log.debug("Received a disconnected unfinished block")
|
||
return None
|
||
|
||
pos = block.reward_chain_block.proof_of_space
|
||
if pos.version == 1 and pos.quality_string() is None:
|
||
raise ConsensusError(Err.INVALID_POSPACE)
|
||
|
||
# Adds the unfinished block to seen, and check if it's seen before, to prevent
|
||
# processing it twice. This searches for the exact version of the unfinished block (there can be many different
|
||
# foliages for the same trunk). This is intentional, to prevent DOS attacks.
|
||
# Note that it does not require that this block was successfully processed
|
||
if self.full_node_store.seen_unfinished_block(block.get_hash()):
|
||
return None
|
||
|
||
block_hash = bytes32(block.reward_chain_block.get_hash())
|
||
foliage_tx_hash = block.foliage.foliage_transaction_block_hash
|
||
|
||
# If we have already added the block with this reward block hash and
|
||
# foliage hash, return
|
||
existing, unfinished_count, has_better = self.full_node_store.get_unfinished_block2(block_hash, foliage_tx_hash)
|
||
if existing is not None or has_better:
|
||
return None
|
||
|
||
peak: BlockRecord | None = self.blockchain.get_peak()
|
||
if peak is not None:
|
||
if block.total_iters < peak.total_iters:
|
||
# This unfinished block would be infused before the current
|
||
# finished head so it's already too late.
|
||
self.log.log(
|
||
logging.WARNING if farmed_block else logging.INFO,
|
||
f"Dropping{' farmed' if farmed_block else ''} unfinished block as it would be "
|
||
"infused before the current head. "
|
||
f"Signage point index: {block.reward_chain_block.signage_point_index}, "
|
||
f"block total iters: {block.total_iters}, "
|
||
f"current head total iters: {peak.total_iters}, "
|
||
f"peak height: {peak.height}.",
|
||
)
|
||
return None
|
||
|
||
if block.prev_header_hash == self.constants.GENESIS_CHALLENGE:
|
||
prev_b = None
|
||
else:
|
||
prev_b = self.blockchain.block_record(block.prev_header_hash)
|
||
|
||
# Count the blocks in sub slot, and check if it's a new epoch
|
||
if len(block.finished_sub_slots) > 0:
|
||
num_blocks_in_ss = 1 # Curr
|
||
else:
|
||
curr = self.blockchain.try_block_record(block.prev_header_hash)
|
||
num_blocks_in_ss = 2 # Curr and prev
|
||
while (curr is not None) and not curr.first_in_sub_slot:
|
||
curr = self.blockchain.try_block_record(curr.prev_hash)
|
||
num_blocks_in_ss += 1
|
||
|
||
if num_blocks_in_ss > self.constants.MAX_SUB_SLOT_BLOCKS:
|
||
# TODO: potentially allow overflow blocks here, which count for the next slot
|
||
self.log.warning("Too many blocks added, not adding block")
|
||
return None
|
||
|
||
# The clvm generator and aggregate signature are validated outside of the lock, to allow other blocks and
|
||
# transactions to get validated
|
||
conds: SpendBundleConditions | None = None
|
||
pre_validation_time = None
|
||
|
||
async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high):
|
||
start_header_time = time.monotonic()
|
||
_, header_error = await self.blockchain.validate_unfinished_block_header(block)
|
||
if header_error is not None:
|
||
if header_error == Err.TIMESTAMP_TOO_FAR_IN_FUTURE:
|
||
raise TimestampError
|
||
else:
|
||
raise ConsensusError(header_error)
|
||
validate_time = time.monotonic() - start_header_time
|
||
self.log.log(
|
||
logging.WARNING if validate_time > 2 else logging.DEBUG,
|
||
f"Time for header validate: {validate_time:0.3f}s",
|
||
)
|
||
|
||
if block_has_transactions_generator(block):
|
||
pre_validation_start = time.monotonic()
|
||
assert block.transactions_info is not None
|
||
if len(block.transactions_generator_ref_list) > 0:
|
||
generator_refs = set(block.transactions_generator_ref_list)
|
||
generators: dict[uint32, bytes] = await self.blockchain.lookup_block_generators(
|
||
block.prev_header_hash, generator_refs
|
||
)
|
||
generator_args = [generators[height] for height in block.transactions_generator_ref_list]
|
||
else:
|
||
generator_args = []
|
||
|
||
height = uint32(0) if prev_b is None else uint32(prev_b.height + 1)
|
||
# Match finished-block prevalidation: CLVM flags are gated on the
|
||
# latest tx block infused before this block's signage point.
|
||
prev_tx_height = pre_sp_tx_block_height(
|
||
constants=self.constants,
|
||
blocks=self.blockchain,
|
||
prev_b_hash=block.prev_header_hash,
|
||
sp_index=block.reward_chain_block.signage_point_index,
|
||
finished_sub_slots=len(block.finished_sub_slots),
|
||
)
|
||
flags = get_flags_for_height_and_constants(prev_tx_height, self.constants)
|
||
|
||
# on mainnet we won't receive unfinished blocks for heights
|
||
# below the hard fork activation, but we have tests where we do
|
||
if height >= self.constants.HARD_FORK_HEIGHT:
|
||
run_block = run_block_generator2
|
||
else:
|
||
run_block = run_block_generator
|
||
|
||
generator_bytes = get_transactions_generator_bytes(block)
|
||
assert generator_bytes is not None
|
||
|
||
# run_block() also validates the signature
|
||
# bump nice for each unfinished block we already have for this
|
||
# reward hash, so a burst of duplicates doesn't starve mempool
|
||
# transaction validation
|
||
err, err_msg, conditions = await self.pool.run_in_loop(
|
||
run_block,
|
||
generator_bytes,
|
||
generator_args,
|
||
min(self.constants.MAX_BLOCK_COST_CLVM, block.transactions_info.cost),
|
||
flags,
|
||
block.transactions_info.aggregated_signature,
|
||
self._bls_cache,
|
||
self.constants,
|
||
nice=(5 + unfinished_count,),
|
||
)
|
||
|
||
if err is not None:
|
||
raise ConsensusError(Err(err), error_msg=err_msg)
|
||
assert conditions is not None
|
||
assert conditions.validated_signature
|
||
conds = conditions
|
||
pre_validation_time = time.monotonic() - pre_validation_start
|
||
|
||
async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high):
|
||
# TODO: pre-validate VDFs outside of lock
|
||
validation_start = time.monotonic()
|
||
validate_result = await self.blockchain.validate_unfinished_block(block, conds)
|
||
if validate_result.error is not None:
|
||
raise ConsensusError(Err(validate_result.error), error_msg=validate_result.error_msg)
|
||
validation_time = time.monotonic() - validation_start
|
||
|
||
assert validate_result.required_iters is not None
|
||
|
||
# Perform another check, in case we have already concurrently added the same unfinished block
|
||
existing, _, has_better = self.full_node_store.get_unfinished_block2(block_hash, foliage_tx_hash)
|
||
if existing is not None or has_better:
|
||
return None
|
||
|
||
if block.prev_header_hash == self.constants.GENESIS_CHALLENGE:
|
||
height = uint32(0)
|
||
else:
|
||
height = uint32(self.blockchain.block_record(block.prev_header_hash).height + 1)
|
||
|
||
post_hard_fork = post_hard_fork2(
|
||
self.constants,
|
||
self.blockchain,
|
||
prev_b_hash=block.prev_header_hash,
|
||
sp_index=block.reward_chain_block.signage_point_index,
|
||
finished_sub_slots=len(block.finished_sub_slots),
|
||
)
|
||
ses: SubEpochSummary | None = next_sub_epoch_summary(
|
||
self.constants,
|
||
self.blockchain,
|
||
validate_result.required_iters,
|
||
block,
|
||
True,
|
||
post_hard_fork,
|
||
)
|
||
|
||
self.full_node_store.add_unfinished_block(height, block, validate_result)
|
||
pre_validation_log = (
|
||
f"pre_validation time {pre_validation_time:0.4f}, " if pre_validation_time is not None else ""
|
||
)
|
||
block_duration_in_seconds = (
|
||
receive_time - self.signage_point_times[block.reward_chain_block.signage_point_index]
|
||
)
|
||
if farmed_block is True:
|
||
self.log.info(
|
||
f"🍀 ️Farmed unfinished_block {block_hash}, SP: {block.reward_chain_block.signage_point_index}, "
|
||
f"validation time: {validation_time:0.4f} seconds, {pre_validation_log}"
|
||
f"cost: {block.transactions_info.cost if block.transactions_info else 'None'} "
|
||
)
|
||
else:
|
||
percent_full_str = (
|
||
(
|
||
", percent full: "
|
||
+ str(round(100.0 * float(block.transactions_info.cost) / self.constants.MAX_BLOCK_COST_CLVM, 3))
|
||
+ "%"
|
||
)
|
||
if block.transactions_info is not None
|
||
else ""
|
||
)
|
||
self.log.info(
|
||
f"Added unfinished_block {block_hash}, not farmed by us,"
|
||
f" SP: {block.reward_chain_block.signage_point_index} farmer response time: "
|
||
f"{block_duration_in_seconds:0.4f}, "
|
||
f"Pool pk {encode_puzzle_hash(block.foliage.foliage_block_data.pool_target.puzzle_hash, 'xch')}, "
|
||
f"validation time: {validation_time:0.4f} seconds, {pre_validation_log}"
|
||
f"cost: {block.transactions_info.cost if block.transactions_info else 'None'}"
|
||
f"{percent_full_str}"
|
||
)
|
||
|
||
sub_slot_iters, difficulty = get_next_sub_slot_iters_and_difficulty(
|
||
self.constants,
|
||
len(block.finished_sub_slots) > 0,
|
||
prev_b,
|
||
self.blockchain,
|
||
)
|
||
|
||
if block.reward_chain_block.signage_point_index == 0:
|
||
res = self.full_node_store.get_sub_slot(block.reward_chain_block.pos_ss_cc_challenge_hash)
|
||
if res is None:
|
||
if block.reward_chain_block.pos_ss_cc_challenge_hash == self.constants.GENESIS_CHALLENGE:
|
||
rc_prev = self.constants.GENESIS_CHALLENGE
|
||
else:
|
||
self.log.warning(f"Do not have sub slot {block.reward_chain_block.pos_ss_cc_challenge_hash}")
|
||
return None
|
||
else:
|
||
rc_prev = res[0].reward_chain.get_hash()
|
||
else:
|
||
assert block.reward_chain_block.reward_chain_sp_vdf is not None
|
||
rc_prev = block.reward_chain_block.reward_chain_sp_vdf.challenge
|
||
|
||
# MMR might be diffrent from the peak mmr depending on whether we have a new sub slot or new SP
|
||
header_mmr_root = self.blockchain.get_mmr_root_for_block(
|
||
block.prev_header_hash,
|
||
block.reward_chain_block.signage_point_index,
|
||
len(block.finished_sub_slots) > 0,
|
||
)
|
||
|
||
timelord_request = timelord_protocol.NewUnfinishedBlockTimelord(
|
||
block.reward_chain_block,
|
||
difficulty,
|
||
sub_slot_iters,
|
||
block.foliage,
|
||
ses,
|
||
rc_prev,
|
||
header_mmr_root,
|
||
)
|
||
|
||
timelord_msg = make_msg(ProtocolMessageTypes.new_unfinished_block_timelord, timelord_request)
|
||
await self.server.send_to_all([timelord_msg], NodeType.TIMELORD)
|
||
|
||
# create two versions of the NewUnfinishedBlock message, one to be sent
|
||
# to newer clients and one for older clients
|
||
full_node_request = full_node_protocol.NewUnfinishedBlock(block.reward_chain_block.get_hash())
|
||
msg = make_msg(ProtocolMessageTypes.new_unfinished_block, full_node_request)
|
||
|
||
full_node_request2 = full_node_protocol.NewUnfinishedBlock2(
|
||
block.reward_chain_block.get_hash(), block.foliage.foliage_transaction_block_hash
|
||
)
|
||
msg2 = make_msg(ProtocolMessageTypes.new_unfinished_block2, full_node_request2)
|
||
|
||
def old_clients(conn: WSChiaConnection) -> bool:
|
||
# don't send this to peers with new clients
|
||
return conn.protocol_version <= Version("0.0.35")
|
||
|
||
def new_clients(conn: WSChiaConnection) -> bool:
|
||
# don't send this to peers with old clients
|
||
return conn.protocol_version > Version("0.0.35")
|
||
|
||
peer_id: bytes32 | None = None if peer is None else peer.peer_node_id
|
||
await self.server.send_to_all_if([msg], NodeType.FULL_NODE, old_clients, peer_id)
|
||
await self.server.send_to_all_if([msg2], NodeType.FULL_NODE, new_clients, peer_id)
|
||
|
||
self._state_changed(
|
||
"unfinished_block",
|
||
{
|
||
"block_duration_in_seconds": block_duration_in_seconds,
|
||
"validation_time_in_seconds": validation_time,
|
||
"pre_validation_time_in_seconds": pre_validation_time,
|
||
"unfinished_block": block.to_json_dict(),
|
||
},
|
||
)
|
||
|
||
async def new_infusion_point_vdf(
|
||
self, request: timelord_protocol.NewInfusionPointVDF, timelord_peer: WSChiaConnection | None = None
|
||
) -> Message | None:
|
||
# Lookup unfinished blocks
|
||
unfinished_block: UnfinishedBlock | None = self.full_node_store.get_unfinished_block(
|
||
request.unfinished_reward_hash
|
||
)
|
||
|
||
if unfinished_block is None:
|
||
self.log.warning(
|
||
f"Do not have unfinished reward chain block {request.unfinished_reward_hash}, cannot finish."
|
||
)
|
||
return None
|
||
|
||
prev_b: BlockRecord | None = None
|
||
|
||
target_rc_hash = request.reward_chain_ip_vdf.challenge
|
||
last_slot_cc_hash = request.challenge_chain_ip_vdf.challenge
|
||
|
||
# Backtracks through end of slot objects, should work for multiple empty sub slots
|
||
for eos, _, _ in reversed(self.full_node_store.finished_sub_slots):
|
||
if eos is not None and eos.reward_chain.get_hash() == target_rc_hash:
|
||
target_rc_hash = eos.reward_chain.end_of_slot_vdf.challenge
|
||
if target_rc_hash == self.constants.GENESIS_CHALLENGE:
|
||
prev_b = None
|
||
else:
|
||
# Find the prev block, starts looking backwards from the peak. target_rc_hash must be the hash of a block
|
||
# and not an end of slot (since we just looked through the slots and backtracked)
|
||
curr: BlockRecord | None = self.blockchain.get_peak()
|
||
|
||
for _ in range(10):
|
||
if curr is None:
|
||
break
|
||
if curr.reward_infusion_new_challenge == target_rc_hash:
|
||
# Found our prev block
|
||
prev_b = curr
|
||
break
|
||
curr = self.blockchain.try_block_record(curr.prev_hash)
|
||
|
||
# If not found, cache keyed on prev block
|
||
if prev_b is None:
|
||
self.full_node_store.add_to_future_ip(request)
|
||
self.log.warning(
|
||
f"Previous block is None, infusion point {request.reward_chain_ip_vdf.challenge.hex()}"
|
||
)
|
||
return None
|
||
|
||
finished_sub_slots: list[EndOfSubSlotBundle] | None = self.full_node_store.get_finished_sub_slots(
|
||
self.blockchain,
|
||
prev_b,
|
||
last_slot_cc_hash,
|
||
)
|
||
if finished_sub_slots is None:
|
||
return None
|
||
|
||
sub_slot_iters, difficulty = get_next_sub_slot_iters_and_difficulty(
|
||
self.constants,
|
||
len(finished_sub_slots) > 0,
|
||
prev_b,
|
||
self.blockchain,
|
||
)
|
||
|
||
if unfinished_block.reward_chain_block.pos_ss_cc_challenge_hash == self.constants.GENESIS_CHALLENGE:
|
||
sub_slot_start_iters = uint128(0)
|
||
else:
|
||
ss_res = self.full_node_store.get_sub_slot(unfinished_block.reward_chain_block.pos_ss_cc_challenge_hash)
|
||
if ss_res is None:
|
||
self.log.warning(f"Do not have sub slot {unfinished_block.reward_chain_block.pos_ss_cc_challenge_hash}")
|
||
return None
|
||
_, _, sub_slot_start_iters = ss_res
|
||
sp_total_iters = uint128(
|
||
sub_slot_start_iters
|
||
+ calculate_sp_iters(
|
||
self.constants,
|
||
sub_slot_iters,
|
||
unfinished_block.reward_chain_block.signage_point_index,
|
||
)
|
||
)
|
||
|
||
block: FullBlock = unfinished_block_to_full_block_with_mmr(
|
||
unfinished_block,
|
||
request.challenge_chain_ip_vdf,
|
||
request.challenge_chain_ip_proof,
|
||
request.reward_chain_ip_vdf,
|
||
request.reward_chain_ip_proof,
|
||
request.infused_challenge_chain_ip_vdf,
|
||
request.infused_challenge_chain_ip_proof,
|
||
finished_sub_slots,
|
||
prev_b,
|
||
self.blockchain,
|
||
sp_total_iters,
|
||
difficulty,
|
||
self.constants,
|
||
)
|
||
if not self.has_valid_pool_sig(block):
|
||
self.log.warning("Trying to make a pre-farm block but height is not 0")
|
||
return None
|
||
try:
|
||
await self.add_block(block, None, self._bls_cache, raise_on_disconnected=True)
|
||
except Exception as e:
|
||
self.log.warning(f"Consensus error validating block: {e}")
|
||
if timelord_peer is not None:
|
||
# Only sends to the timelord who sent us this VDF, to reset them to the correct peak
|
||
await self.send_peak_to_timelords(peer=timelord_peer)
|
||
return None
|
||
|
||
async def add_end_of_sub_slot(
|
||
self, end_of_slot_bundle: EndOfSubSlotBundle, peer: WSChiaConnection
|
||
) -> tuple[Message | None, bool]:
|
||
fetched_ss = self.full_node_store.get_sub_slot(end_of_slot_bundle.challenge_chain.get_hash())
|
||
|
||
# We are not interested in sub-slots which have the same challenge chain but different reward chain. If there
|
||
# is a reorg, we will find out through the broadcast of blocks instead.
|
||
if fetched_ss is not None:
|
||
# Already have the sub-slot
|
||
return None, True
|
||
|
||
async with self.timelord_lock:
|
||
fetched_ss = self.full_node_store.get_sub_slot(
|
||
end_of_slot_bundle.challenge_chain.challenge_chain_end_of_slot_vdf.challenge
|
||
)
|
||
if (
|
||
(fetched_ss is None)
|
||
and end_of_slot_bundle.challenge_chain.challenge_chain_end_of_slot_vdf.challenge
|
||
!= self.constants.GENESIS_CHALLENGE
|
||
):
|
||
# If we don't have the prev, request the prev instead
|
||
full_node_request = full_node_protocol.RequestSignagePointOrEndOfSubSlot(
|
||
end_of_slot_bundle.challenge_chain.challenge_chain_end_of_slot_vdf.challenge,
|
||
uint8(0),
|
||
bytes32.zeros,
|
||
)
|
||
return (
|
||
make_msg(ProtocolMessageTypes.request_signage_point_or_end_of_sub_slot, full_node_request),
|
||
False,
|
||
)
|
||
|
||
peak = self.blockchain.get_peak()
|
||
if peak is not None and peak.height > 2:
|
||
next_sub_slot_iters, next_difficulty = self.blockchain.get_next_sub_slot_iters_and_difficulty(
|
||
peak.header_hash, True
|
||
)
|
||
else:
|
||
next_sub_slot_iters = self.constants.SUB_SLOT_ITERS_STARTING
|
||
next_difficulty = self.constants.DIFFICULTY_STARTING
|
||
|
||
# Adds the sub slot and potentially get new infusions
|
||
new_infusions = self.full_node_store.new_finished_sub_slot(
|
||
end_of_slot_bundle,
|
||
self.blockchain,
|
||
peak,
|
||
next_sub_slot_iters,
|
||
next_difficulty,
|
||
await self.blockchain.get_full_peak(),
|
||
)
|
||
# It may be an empty list, even if it's not None. Not None means added successfully
|
||
if new_infusions is not None:
|
||
self.log.info(
|
||
f"⏲️ Finished sub slot, SP {self.constants.NUM_SPS_SUB_SLOT}/{self.constants.NUM_SPS_SUB_SLOT}, "
|
||
f"{end_of_slot_bundle.challenge_chain.get_hash().hex()}, "
|
||
f"number of sub-slots: {len(self.full_node_store.finished_sub_slots)}, "
|
||
f"RC hash: {end_of_slot_bundle.reward_chain.get_hash().hex()}, "
|
||
f"Deficit {end_of_slot_bundle.reward_chain.deficit}"
|
||
)
|
||
# Reset farmer response timer for sub slot (SP 0)
|
||
self.signage_point_times[0] = time.time()
|
||
# Notify full nodes of the new sub-slot
|
||
broadcast = full_node_protocol.NewSignagePointOrEndOfSubSlot(
|
||
end_of_slot_bundle.challenge_chain.challenge_chain_end_of_slot_vdf.challenge,
|
||
end_of_slot_bundle.challenge_chain.get_hash(),
|
||
uint8(0),
|
||
end_of_slot_bundle.reward_chain.end_of_slot_vdf.challenge,
|
||
)
|
||
msg = make_msg(ProtocolMessageTypes.new_signage_point_or_end_of_sub_slot, broadcast)
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE, peer.peer_node_id)
|
||
|
||
for infusion in new_infusions:
|
||
await self.new_infusion_point_vdf(infusion)
|
||
tx_peak = self.blockchain.get_tx_peak()
|
||
# Get filter_challenge for V2 plot filter (challenge hash of a previously
|
||
# completed sub-slot; SP index 0 is in the first window, so SS(n-2))
|
||
filter_challenge = self.full_node_store.get_filter_challenge(
|
||
end_of_slot_bundle.challenge_chain.get_hash(),
|
||
uint8(0),
|
||
)
|
||
# Notify farmers of the new sub-slot
|
||
broadcast_farmer = farmer_protocol.NewSignagePoint(
|
||
end_of_slot_bundle.challenge_chain.get_hash(),
|
||
end_of_slot_bundle.challenge_chain.get_hash(),
|
||
end_of_slot_bundle.reward_chain.get_hash(),
|
||
next_difficulty,
|
||
next_sub_slot_iters,
|
||
uint8(0),
|
||
uint32(0) if peak is None else peak.height,
|
||
tx_peak.height if tx_peak is not None else uint32(0),
|
||
sp_source_data=SignagePointSourceData(
|
||
sub_slot_data=SPSubSlotSourceData(
|
||
end_of_slot_bundle.challenge_chain, end_of_slot_bundle.reward_chain
|
||
)
|
||
),
|
||
filter_challenge=filter_challenge,
|
||
)
|
||
msg = make_msg(ProtocolMessageTypes.new_signage_point, broadcast_farmer)
|
||
await self.server.send_to_all([msg], NodeType.FARMER)
|
||
return None, True
|
||
else:
|
||
self.log.info(
|
||
f"End of slot not added CC challenge "
|
||
f"{end_of_slot_bundle.challenge_chain.challenge_chain_end_of_slot_vdf.challenge.hex()}"
|
||
)
|
||
return None, False
|
||
|
||
async def add_transaction(
|
||
self,
|
||
transaction: SpendBundle,
|
||
spend_name: bytes32,
|
||
peer: WSChiaConnection | None = None,
|
||
test: bool = False,
|
||
# Map of peer ID to its hostname, the fee and the cost it advertised
|
||
# for this transaction.
|
||
peers_with_tx: dict[bytes32, PeerWithTx] = {},
|
||
) -> tuple[MempoolInclusionStatus, Err | None]:
|
||
if self.sync_store.get_sync_mode():
|
||
return MempoolInclusionStatus.FAILED, Err.NO_TRANSACTIONS_WHILE_SYNCING
|
||
if not test and not (await self.synced()):
|
||
return MempoolInclusionStatus.FAILED, Err.NO_TRANSACTIONS_WHILE_SYNCING
|
||
|
||
if self.mempool_manager.get_spendbundle(spend_name) is not None:
|
||
self.mempool_manager.remove_seen(spend_name)
|
||
return MempoolInclusionStatus.SUCCESS, None
|
||
if self.mempool_manager.seen(spend_name) or self.mempool_manager.in_flight(spend_name):
|
||
return MempoolInclusionStatus.FAILED, Err.ALREADY_INCLUDING_TRANSACTION
|
||
self.log.debug(f"Processing transaction: {spend_name}")
|
||
# Ignore if syncing or if we have not yet received a block
|
||
# the mempool must have a peak to validate transactions
|
||
if self.sync_store.get_sync_mode() or self.mempool_manager.peak is None:
|
||
return MempoolInclusionStatus.FAILED, Err.NO_TRANSACTIONS_WHILE_SYNCING
|
||
|
||
fee_per_cost = 0.0
|
||
for peer_info in peers_with_tx.values():
|
||
if peer_info.advertised_cost > 0:
|
||
fee_per_cost = max(fee_per_cost, peer_info.advertised_fee / peer_info.advertised_cost)
|
||
|
||
# Mark as in-flight before the expensive pre_validate call so
|
||
# concurrent workers processing the same tx from different peers are
|
||
# deduplicated without churning the bounded seen cache.
|
||
self.mempool_manager.add_in_flight(spend_name)
|
||
try:
|
||
cost_result = await self.mempool_manager.pre_validate_spendbundle(
|
||
transaction, spend_name, self._bls_cache, fee_per_cost=fee_per_cost
|
||
)
|
||
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
|
||
except ValidationError as e:
|
||
# Keep known-invalid bundles in seen-cache to prevent re-validation.
|
||
self.mempool_manager.add_and_maybe_pop_seen(spend_name)
|
||
self.log.info(f"Rejecting transaction {spend_name}: {e}")
|
||
return MempoolInclusionStatus.FAILED, e.code
|
||
finally:
|
||
self.mempool_manager.remove_in_flight(spend_name)
|
||
|
||
self.mempool_manager.add_and_maybe_pop_seen(spend_name)
|
||
|
||
if self.config.get("log_mempool", False): # pragma: no cover
|
||
try:
|
||
mempool_dir = path_from_root(self.root_path, "mempool-log") / f"{self.blockchain.get_peak_height()}"
|
||
mempool_dir.mkdir(parents=True, exist_ok=True)
|
||
with open(mempool_dir / f"{spend_name}.bundle", "wb+") as f:
|
||
f.write(bytes(transaction))
|
||
except Exception:
|
||
self.log.exception(f"Failed to log mempool item: {spend_name}")
|
||
|
||
async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.low):
|
||
if self.mempool_manager.get_spendbundle(spend_name) is not None:
|
||
self.mempool_manager.remove_seen(spend_name)
|
||
return MempoolInclusionStatus.SUCCESS, None
|
||
if self.mempool_manager.peak is None:
|
||
return MempoolInclusionStatus.FAILED, Err.MEMPOOL_NOT_INITIALIZED
|
||
info = await self.mempool_manager.add_spend_bundle(
|
||
transaction, cost_result, spend_name, self.mempool_manager.peak.height
|
||
)
|
||
status = info.status
|
||
error = info.error
|
||
if status == MempoolInclusionStatus.SUCCESS:
|
||
self.log.debug(
|
||
f"Added transaction to mempool: {spend_name} mempool size: "
|
||
f"{self.mempool_manager.mempool.total_mempool_cost()} normalized "
|
||
f"{self.mempool_manager.mempool.total_mempool_cost() / 5000000}"
|
||
)
|
||
|
||
mempool_item = self.mempool_manager.get_mempool_item(spend_name)
|
||
assert mempool_item is not None
|
||
# Now that we validated this transaction, check what fees and
|
||
# costs the peers have advertised for it.
|
||
for peer_id, entry in peers_with_tx.items():
|
||
# Older nodes (2.4.3 and earlier) compute the cost slightly
|
||
# differently. They include the byte cost and execution cost of
|
||
# the quote for the puzzle.
|
||
tolerated_diff = QUOTE_BYTES * self.constants.COST_PER_BYTE + QUOTE_EXECUTION_COST
|
||
if entry.advertised_fee == mempool_item.fee and (
|
||
entry.advertised_cost == mempool_item.cost
|
||
or entry.advertised_cost == mempool_item.cost + tolerated_diff
|
||
):
|
||
continue
|
||
self.log.warning(
|
||
f"Banning peer {peer_id}. Sent us a new tx {spend_name} with mismatch "
|
||
f"on cost {entry.advertised_cost} vs validation cost {mempool_item.cost} and/or "
|
||
f"fee {entry.advertised_fee} vs {mempool_item.fee}."
|
||
)
|
||
peer = self.server.all_connections.get(peer_id)
|
||
if peer is None:
|
||
self.server.ban_peer(entry.peer_host, CONSENSUS_ERROR_BAN_SECONDS)
|
||
else:
|
||
await peer.close(CONSENSUS_ERROR_BAN_SECONDS)
|
||
# Only broadcast successful transactions, not pending ones. Otherwise it's a DOS
|
||
# vector.
|
||
await self.broadcast_removed_tx(info.removals)
|
||
await self.broadcast_added_tx(mempool_item, current_peer=peer)
|
||
|
||
if self.simulator_transaction_callback is not None: # callback
|
||
await self.simulator_transaction_callback(spend_name)
|
||
|
||
else:
|
||
self.mempool_manager.remove_seen(spend_name)
|
||
self.log.debug(f"Wasn't able to add transaction with id {spend_name}, status {status} error: {error}")
|
||
return status, error
|
||
|
||
async def broadcast_added_tx(self, mempool_item: MempoolItem, current_peer: WSChiaConnection | None = None) -> None:
|
||
assert mempool_item.fee >= 0
|
||
assert mempool_item.cost is not None
|
||
|
||
new_tx = full_node_protocol.NewTransaction(
|
||
mempool_item.name,
|
||
mempool_item.cost,
|
||
mempool_item.fee,
|
||
)
|
||
msg = make_msg(ProtocolMessageTypes.new_transaction, new_tx)
|
||
if current_peer is None:
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE)
|
||
else:
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE, current_peer.peer_node_id)
|
||
|
||
conds = mempool_item.conds
|
||
|
||
all_peers = {
|
||
peer_id
|
||
for peer_id, peer in self.server.all_connections.items()
|
||
if peer.has_capability(Capability.MEMPOOL_UPDATES)
|
||
}
|
||
|
||
if len(all_peers) == 0:
|
||
return
|
||
|
||
start_time = time.monotonic()
|
||
|
||
hints_for_removals = await self.hint_store.get_hints([bytes32(spend.coin_id) for spend in conds.spends])
|
||
peer_ids = all_peers.intersection(peers_for_spend_bundle(self.subscriptions, conds, set(hints_for_removals)))
|
||
|
||
for peer_id in peer_ids:
|
||
peer = self.server.all_connections.get(peer_id)
|
||
|
||
if peer is None:
|
||
continue
|
||
|
||
msg = make_msg(
|
||
ProtocolMessageTypes.mempool_items_added, wallet_protocol.MempoolItemsAdded([mempool_item.name])
|
||
)
|
||
await peer.send_message(msg)
|
||
|
||
total_time = time.monotonic() - start_time
|
||
|
||
if len(peer_ids) == 0:
|
||
self.log.log(
|
||
logging.DEBUG if total_time < 0.5 else logging.WARNING,
|
||
f"Looking up hints for {len(conds.spends)} spends took {total_time:.4f}s",
|
||
)
|
||
else:
|
||
self.log.log(
|
||
logging.DEBUG if total_time < 0.5 else logging.WARNING,
|
||
f"Broadcasting added transaction {mempool_item.name} to {len(peer_ids)} peers took {total_time:.4f}s",
|
||
)
|
||
|
||
async def broadcast_removed_tx(self, mempool_removals: list[MempoolRemoveInfo]) -> None:
|
||
total_removals = sum(len(r.items) for r in mempool_removals)
|
||
if total_removals == 0:
|
||
return
|
||
|
||
start_time = time.monotonic()
|
||
|
||
self.log.debug(f"Broadcasting {total_removals} removed transactions to peers")
|
||
|
||
all_peers = {
|
||
peer_id
|
||
for peer_id, peer in self.server.all_connections.items()
|
||
if peer.has_capability(Capability.MEMPOOL_UPDATES)
|
||
}
|
||
|
||
if len(all_peers) == 0:
|
||
return
|
||
|
||
removals_to_send: dict[bytes32, list[RemovedMempoolItem]] = dict()
|
||
|
||
for removal_info in mempool_removals:
|
||
for transaction_id, internal_mempool_item in removal_info.items.items():
|
||
conds = internal_mempool_item.conds
|
||
assert conds is not None
|
||
|
||
hints_for_removals = await self.hint_store.get_hints([bytes32(spend.coin_id) for spend in conds.spends])
|
||
peer_ids = all_peers.intersection(
|
||
peers_for_spend_bundle(self.subscriptions, conds, set(hints_for_removals))
|
||
)
|
||
|
||
if len(peer_ids) == 0:
|
||
continue
|
||
|
||
self.log.debug(f"Broadcasting removed transaction {transaction_id} to wallet peers {peer_ids}")
|
||
|
||
for peer_id in peer_ids:
|
||
peer = self.server.all_connections.get(peer_id)
|
||
|
||
if peer is None:
|
||
continue
|
||
|
||
removal = wallet_protocol.RemovedMempoolItem(transaction_id, uint8(removal_info.reason.value))
|
||
removals_to_send.setdefault(peer.peer_node_id, []).append(removal)
|
||
|
||
for peer_id, removals in removals_to_send.items():
|
||
peer = self.server.all_connections.get(peer_id)
|
||
|
||
if peer is None:
|
||
continue
|
||
|
||
msg = make_msg(
|
||
ProtocolMessageTypes.mempool_items_removed,
|
||
wallet_protocol.MempoolItemsRemoved(removals),
|
||
)
|
||
await peer.send_message(msg)
|
||
|
||
total_time = time.monotonic() - start_time
|
||
|
||
self.log.log(
|
||
logging.DEBUG if total_time < 0.5 else logging.WARNING,
|
||
f"Broadcasting {total_removals} removed transactions "
|
||
f"to {len(removals_to_send)} peers took {total_time:.4f}s",
|
||
)
|
||
|
||
async def _needs_compact_proof(
|
||
self, vdf_info: VDFInfo, header_block: HeaderBlock, field_vdf: CompressibleVDFField
|
||
) -> bool:
|
||
if field_vdf == CompressibleVDFField.CC_EOS_VDF:
|
||
for sub_slot in header_block.finished_sub_slots:
|
||
if sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf == vdf_info:
|
||
if (
|
||
sub_slot.proofs.challenge_chain_slot_proof.witness_type == 0
|
||
and sub_slot.proofs.challenge_chain_slot_proof.normalized_to_identity
|
||
):
|
||
return False
|
||
return True
|
||
if field_vdf == CompressibleVDFField.ICC_EOS_VDF:
|
||
for sub_slot in header_block.finished_sub_slots:
|
||
if (
|
||
sub_slot.infused_challenge_chain is not None
|
||
and sub_slot.infused_challenge_chain.infused_challenge_chain_end_of_slot_vdf == vdf_info
|
||
):
|
||
assert sub_slot.proofs.infused_challenge_chain_slot_proof is not None
|
||
if (
|
||
sub_slot.proofs.infused_challenge_chain_slot_proof.witness_type == 0
|
||
and sub_slot.proofs.infused_challenge_chain_slot_proof.normalized_to_identity
|
||
):
|
||
return False
|
||
return True
|
||
if field_vdf == CompressibleVDFField.CC_SP_VDF:
|
||
if header_block.reward_chain_block.challenge_chain_sp_vdf is None:
|
||
return False
|
||
if vdf_info == header_block.reward_chain_block.challenge_chain_sp_vdf:
|
||
assert header_block.challenge_chain_sp_proof is not None
|
||
if (
|
||
header_block.challenge_chain_sp_proof.witness_type == 0
|
||
and header_block.challenge_chain_sp_proof.normalized_to_identity
|
||
):
|
||
return False
|
||
return True
|
||
if field_vdf == CompressibleVDFField.CC_IP_VDF:
|
||
if vdf_info == header_block.reward_chain_block.challenge_chain_ip_vdf:
|
||
if (
|
||
header_block.challenge_chain_ip_proof.witness_type == 0
|
||
and header_block.challenge_chain_ip_proof.normalized_to_identity
|
||
):
|
||
return False
|
||
return True
|
||
return False
|
||
|
||
async def _can_accept_compact_proof(
|
||
self,
|
||
vdf_info: VDFInfo,
|
||
vdf_proof: VDFProof,
|
||
height: uint32,
|
||
header_hash: bytes32,
|
||
field_vdf: CompressibleVDFField,
|
||
) -> bool:
|
||
"""
|
||
- Checks if the provided proof is indeed compact.
|
||
- Checks if proof verifies given the vdf_info from the start of sub-slot.
|
||
- Checks if the provided vdf_info is correct, assuming it refers to the start of sub-slot.
|
||
- Checks if the existing proof was non-compact. Ignore this proof if we already have a compact proof.
|
||
"""
|
||
is_fully_compactified = await self.block_store.is_fully_compactified(header_hash)
|
||
if is_fully_compactified is None or is_fully_compactified:
|
||
self.log.info(f"Already compactified block: {header_hash}. Ignoring.")
|
||
return False
|
||
peak = self.blockchain.get_peak()
|
||
if peak is None or peak.height - height < 5:
|
||
self.log.debug("Will not compactify recent block")
|
||
return False
|
||
if vdf_proof.witness_type > 0 or not vdf_proof.normalized_to_identity:
|
||
self.log.error(f"Received vdf proof is not compact: {vdf_proof}.")
|
||
return False
|
||
if not validate_vdf(vdf_proof, self.constants, ClassgroupElement.get_default_element(), vdf_info):
|
||
self.log.error(f"Received compact vdf proof is not valid: {vdf_proof}.")
|
||
return False
|
||
header_block = await self.blockchain.get_header_block_by_height(height, header_hash, tx_filter=False)
|
||
if header_block is None:
|
||
self.log.error(f"Can't find block for given compact vdf. Height: {height} Header hash: {header_hash}")
|
||
return False
|
||
is_new_proof = await self._needs_compact_proof(vdf_info, header_block, field_vdf)
|
||
if not is_new_proof:
|
||
self.log.info(f"Duplicate compact proof. Height: {height}. Header hash: {header_hash}.")
|
||
return is_new_proof
|
||
|
||
# returns True if we ended up replacing the proof, and False otherwise
|
||
async def _replace_proof(
|
||
self,
|
||
vdf_info: VDFInfo,
|
||
vdf_proof: VDFProof,
|
||
header_hash: bytes32,
|
||
field_vdf: CompressibleVDFField,
|
||
) -> bool:
|
||
block = await self.block_store.get_full_block(header_hash)
|
||
if block is None:
|
||
return False
|
||
|
||
new_block = None
|
||
|
||
if field_vdf == CompressibleVDFField.CC_EOS_VDF:
|
||
for index, sub_slot in enumerate(block.finished_sub_slots):
|
||
if sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf == vdf_info:
|
||
new_proofs = sub_slot.proofs.replace(challenge_chain_slot_proof=vdf_proof)
|
||
new_subslot = sub_slot.replace(proofs=new_proofs)
|
||
new_finished_subslots = block.finished_sub_slots
|
||
new_finished_subslots[index] = new_subslot
|
||
new_block = block.replace(finished_sub_slots=new_finished_subslots)
|
||
break
|
||
if field_vdf == CompressibleVDFField.ICC_EOS_VDF:
|
||
for index, sub_slot in enumerate(block.finished_sub_slots):
|
||
if (
|
||
sub_slot.infused_challenge_chain is not None
|
||
and sub_slot.infused_challenge_chain.infused_challenge_chain_end_of_slot_vdf == vdf_info
|
||
):
|
||
new_proofs = sub_slot.proofs.replace(infused_challenge_chain_slot_proof=vdf_proof)
|
||
new_subslot = sub_slot.replace(proofs=new_proofs)
|
||
new_finished_subslots = block.finished_sub_slots
|
||
new_finished_subslots[index] = new_subslot
|
||
new_block = block.replace(finished_sub_slots=new_finished_subslots)
|
||
break
|
||
if field_vdf == CompressibleVDFField.CC_SP_VDF:
|
||
if block.reward_chain_block.challenge_chain_sp_vdf == vdf_info:
|
||
assert block.challenge_chain_sp_proof is not None
|
||
new_block = block.replace(challenge_chain_sp_proof=vdf_proof)
|
||
if field_vdf == CompressibleVDFField.CC_IP_VDF:
|
||
if block.reward_chain_block.challenge_chain_ip_vdf == vdf_info:
|
||
new_block = block.replace(challenge_chain_ip_proof=vdf_proof)
|
||
if new_block is None:
|
||
return False
|
||
try:
|
||
await self.block_store.replace_proof(header_hash, new_block)
|
||
return True
|
||
except BaseException as e:
|
||
self.log.error(
|
||
f"_replace_proof error replacing proof for block {block.header_hash} height {block.height}:"
|
||
f" {e} {traceback.format_exc()}"
|
||
)
|
||
raise
|
||
|
||
async def add_compact_proof_of_time(self, request: timelord_protocol.RespondCompactProofOfTime) -> None:
|
||
peak = self.blockchain.get_peak()
|
||
if peak is None or peak.height - request.height < 5:
|
||
self.log.info(f"Ignoring add_compact_proof_of_time, height {request.height} too recent.")
|
||
return None
|
||
|
||
field_vdf = CompressibleVDFField(int(request.field_vdf))
|
||
if not await self._can_accept_compact_proof(
|
||
request.vdf_info, request.vdf_proof, request.height, request.header_hash, field_vdf
|
||
):
|
||
return None
|
||
async with self.blockchain.compact_proof_lock:
|
||
replaced = await self._replace_proof(request.vdf_info, request.vdf_proof, request.header_hash, field_vdf)
|
||
if not replaced:
|
||
self.log.error(f"Could not replace compact proof: {request.height}")
|
||
return None
|
||
self.log.info(f"Replaced compact proof at height {request.height}")
|
||
msg = make_msg(
|
||
ProtocolMessageTypes.new_compact_vdf,
|
||
full_node_protocol.NewCompactVDF(request.height, request.header_hash, request.field_vdf, request.vdf_info),
|
||
)
|
||
if self._server is not None:
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE)
|
||
|
||
async def new_compact_vdf(self, request: full_node_protocol.NewCompactVDF, peer: WSChiaConnection) -> None:
|
||
peak = self.blockchain.get_peak()
|
||
if peak is None or peak.height - request.height < 5:
|
||
self.log.info(f"Ignoring new_compact_vdf, height {request.height} too recent.")
|
||
return None
|
||
is_fully_compactified = await self.block_store.is_fully_compactified(request.header_hash)
|
||
if is_fully_compactified is None or is_fully_compactified:
|
||
return None
|
||
header_block = await self.blockchain.get_header_block_by_height(
|
||
request.height, request.header_hash, tx_filter=False
|
||
)
|
||
if header_block is None:
|
||
return None
|
||
field_vdf = CompressibleVDFField(int(request.field_vdf))
|
||
if await self._needs_compact_proof(request.vdf_info, header_block, field_vdf):
|
||
peer_request = full_node_protocol.RequestCompactVDF(
|
||
request.height, request.header_hash, request.field_vdf, request.vdf_info
|
||
)
|
||
vdf_req_key = peer_request.get_hash()
|
||
peer.pending_compact_vdfs.put(vdf_req_key)
|
||
response = await peer.call_api(FullNodeAPI.request_compact_vdf, peer_request, timeout=10)
|
||
if response is not None and isinstance(response, full_node_protocol.RespondCompactVDF):
|
||
# if we fail to receive a response within 10 seconds, we give up
|
||
# here, but it's stil possible it will arrive later. We leave it
|
||
# in pending_compact_vdfs
|
||
peer.pending_compact_vdfs.remove(vdf_req_key)
|
||
await self.add_compact_vdf(response, peer)
|
||
|
||
async def request_compact_vdf(
|
||
self, request: full_node_protocol.RequestCompactVDF, peer_id: bytes32
|
||
) -> VDFProof | None:
|
||
header_block = await self.blockchain.get_header_block_by_height(
|
||
request.height, request.header_hash, tx_filter=False
|
||
)
|
||
if header_block is None:
|
||
return None
|
||
vdf_proof: VDFProof | None = None
|
||
field_vdf = CompressibleVDFField(int(request.field_vdf))
|
||
if field_vdf == CompressibleVDFField.CC_EOS_VDF:
|
||
for sub_slot in header_block.finished_sub_slots:
|
||
if sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf == request.vdf_info:
|
||
vdf_proof = sub_slot.proofs.challenge_chain_slot_proof
|
||
break
|
||
if field_vdf == CompressibleVDFField.ICC_EOS_VDF:
|
||
for sub_slot in header_block.finished_sub_slots:
|
||
if (
|
||
sub_slot.infused_challenge_chain is not None
|
||
and sub_slot.infused_challenge_chain.infused_challenge_chain_end_of_slot_vdf == request.vdf_info
|
||
):
|
||
vdf_proof = sub_slot.proofs.infused_challenge_chain_slot_proof
|
||
break
|
||
if (
|
||
field_vdf == CompressibleVDFField.CC_SP_VDF
|
||
and header_block.reward_chain_block.challenge_chain_sp_vdf == request.vdf_info
|
||
):
|
||
vdf_proof = header_block.challenge_chain_sp_proof
|
||
if (
|
||
field_vdf == CompressibleVDFField.CC_IP_VDF
|
||
and header_block.reward_chain_block.challenge_chain_ip_vdf == request.vdf_info
|
||
):
|
||
vdf_proof = header_block.challenge_chain_ip_proof
|
||
if vdf_proof is None or vdf_proof.witness_type > 0 or not vdf_proof.normalized_to_identity:
|
||
self.log.info(f"{peer_id} requested compact vdf we don't have, height: {request.height}.")
|
||
return None
|
||
return vdf_proof
|
||
|
||
async def add_compact_vdf(self, request: full_node_protocol.RespondCompactVDF, peer: WSChiaConnection) -> None:
|
||
field_vdf = CompressibleVDFField(int(request.field_vdf))
|
||
if not await self._can_accept_compact_proof(
|
||
request.vdf_info, request.vdf_proof, request.height, request.header_hash, field_vdf
|
||
):
|
||
return None
|
||
async with self.blockchain.compact_proof_lock:
|
||
if self.blockchain.seen_compact_proofs(request.vdf_info, request.height):
|
||
return None
|
||
replaced = await self._replace_proof(request.vdf_info, request.vdf_proof, request.header_hash, field_vdf)
|
||
if not replaced:
|
||
self.log.error(f"Could not replace compact proof: {request.height}")
|
||
return None
|
||
msg = make_msg(
|
||
ProtocolMessageTypes.new_compact_vdf,
|
||
full_node_protocol.NewCompactVDF(request.height, request.header_hash, request.field_vdf, request.vdf_info),
|
||
)
|
||
if self._server is not None:
|
||
await self.server.send_to_all([msg], NodeType.FULL_NODE, peer.peer_node_id)
|
||
|
||
def in_bad_peak_cache(self, wp: WeightProof) -> bool:
|
||
for block in wp.recent_chain_data:
|
||
if block.header_hash in self.bad_peak_cache.keys():
|
||
return True
|
||
return False
|
||
|
||
def add_to_bad_peak_cache(self, peak_header_hash: bytes32, peak_height: uint32) -> None:
|
||
curr_height = self.blockchain.get_peak_height()
|
||
|
||
if curr_height is None:
|
||
self.log.debug(f"add bad peak {peak_header_hash} to cache")
|
||
self.bad_peak_cache[peak_header_hash] = peak_height
|
||
return
|
||
minimum_cache_height = curr_height - (2 * self.constants.SUB_EPOCH_BLOCKS)
|
||
if peak_height < minimum_cache_height:
|
||
return
|
||
|
||
new_cache = {}
|
||
self.log.info(f"add bad peak {peak_header_hash} to cache")
|
||
new_cache[peak_header_hash] = peak_height
|
||
min_height = peak_height
|
||
min_block = peak_header_hash
|
||
for header_hash, height in self.bad_peak_cache.items():
|
||
if height < minimum_cache_height:
|
||
self.log.debug(f"remove bad peak {peak_header_hash} from cache")
|
||
continue
|
||
if height < min_height:
|
||
min_block = header_hash
|
||
new_cache[header_hash] = height
|
||
|
||
if len(new_cache) > self.config.get("bad_peak_cache_size", 100):
|
||
del new_cache[min_block]
|
||
|
||
self.bad_peak_cache = new_cache
|
||
|
||
async def broadcast_uncompact_blocks(
|
||
self, uncompact_interval_scan: int, target_uncompact_proofs: int, sanitize_weight_proof_only: bool
|
||
) -> None:
|
||
try:
|
||
while not self._shut_down:
|
||
while self.sync_store.get_sync_mode() or self.sync_store.get_long_sync():
|
||
if self._shut_down:
|
||
return None
|
||
await asyncio.sleep(30)
|
||
|
||
broadcast_list: list[timelord_protocol.RequestCompactProofOfTime] = []
|
||
|
||
self.log.info("Getting random heights for bluebox to compact")
|
||
|
||
if self._server is None:
|
||
self.log.info("Not broadcasting uncompact blocks, no server found")
|
||
await asyncio.sleep(uncompact_interval_scan)
|
||
continue
|
||
connected_timelords = self.server.get_connections(NodeType.TIMELORD)
|
||
|
||
total_target_uncompact_proofs = target_uncompact_proofs * max(1, len(connected_timelords))
|
||
heights = await self.block_store.get_random_not_compactified(total_target_uncompact_proofs)
|
||
self.log.info("Heights found for bluebox to compact: [%s]", ", ".join(map(str, heights)))
|
||
|
||
for h in heights:
|
||
headers = await self.blockchain.get_header_blocks_in_range(h, h, tx_filter=False)
|
||
records: dict[bytes32, BlockRecord] = {}
|
||
if sanitize_weight_proof_only:
|
||
records = await self.blockchain.get_block_records_in_range(h, h)
|
||
for header in headers.values():
|
||
expected_header_hash = self.blockchain.height_to_hash(header.height)
|
||
if header.header_hash != expected_header_hash:
|
||
continue
|
||
if sanitize_weight_proof_only:
|
||
assert header.header_hash in records
|
||
record = records[header.header_hash]
|
||
for sub_slot in header.finished_sub_slots:
|
||
if (
|
||
sub_slot.proofs.challenge_chain_slot_proof.witness_type > 0
|
||
or not sub_slot.proofs.challenge_chain_slot_proof.normalized_to_identity
|
||
):
|
||
broadcast_list.append(
|
||
timelord_protocol.RequestCompactProofOfTime(
|
||
sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf,
|
||
header.header_hash,
|
||
header.height,
|
||
uint8(CompressibleVDFField.CC_EOS_VDF),
|
||
)
|
||
)
|
||
if sub_slot.proofs.infused_challenge_chain_slot_proof is not None and (
|
||
sub_slot.proofs.infused_challenge_chain_slot_proof.witness_type > 0
|
||
or not sub_slot.proofs.infused_challenge_chain_slot_proof.normalized_to_identity
|
||
):
|
||
assert sub_slot.infused_challenge_chain is not None
|
||
broadcast_list.append(
|
||
timelord_protocol.RequestCompactProofOfTime(
|
||
sub_slot.infused_challenge_chain.infused_challenge_chain_end_of_slot_vdf,
|
||
header.header_hash,
|
||
header.height,
|
||
uint8(CompressibleVDFField.ICC_EOS_VDF),
|
||
)
|
||
)
|
||
# Running in 'sanitize_weight_proof_only' ignores CC_SP_VDF and CC_IP_VDF
|
||
# unless this is a challenge block.
|
||
if sanitize_weight_proof_only:
|
||
if not record.is_challenge_block(self.constants):
|
||
continue
|
||
if header.challenge_chain_sp_proof is not None and (
|
||
header.challenge_chain_sp_proof.witness_type > 0
|
||
or not header.challenge_chain_sp_proof.normalized_to_identity
|
||
):
|
||
assert header.reward_chain_block.challenge_chain_sp_vdf is not None
|
||
broadcast_list.append(
|
||
timelord_protocol.RequestCompactProofOfTime(
|
||
header.reward_chain_block.challenge_chain_sp_vdf,
|
||
header.header_hash,
|
||
header.height,
|
||
uint8(CompressibleVDFField.CC_SP_VDF),
|
||
)
|
||
)
|
||
|
||
if (
|
||
header.challenge_chain_ip_proof.witness_type > 0
|
||
or not header.challenge_chain_ip_proof.normalized_to_identity
|
||
):
|
||
broadcast_list.append(
|
||
timelord_protocol.RequestCompactProofOfTime(
|
||
header.reward_chain_block.challenge_chain_ip_vdf,
|
||
header.header_hash,
|
||
header.height,
|
||
uint8(CompressibleVDFField.CC_IP_VDF),
|
||
)
|
||
)
|
||
|
||
broadcast_list_chunks: list[list[timelord_protocol.RequestCompactProofOfTime]] = []
|
||
for index in range(0, len(broadcast_list), target_uncompact_proofs):
|
||
broadcast_list_chunks.append(broadcast_list[index : index + target_uncompact_proofs])
|
||
if len(broadcast_list_chunks) == 0:
|
||
self.log.info("Did not find any uncompact blocks.")
|
||
await asyncio.sleep(uncompact_interval_scan)
|
||
continue
|
||
if self.sync_store.get_sync_mode() or self.sync_store.get_long_sync():
|
||
await asyncio.sleep(uncompact_interval_scan)
|
||
continue
|
||
if self._server is not None:
|
||
self.log.info(f"Broadcasting {len(broadcast_list)} items to the bluebox")
|
||
connected_timelords = self.server.get_connections(NodeType.TIMELORD)
|
||
chunk_index = 0
|
||
for connection in connected_timelords:
|
||
peer_node_id = connection.peer_node_id
|
||
msgs = []
|
||
broadcast_list = broadcast_list_chunks[chunk_index]
|
||
chunk_index = (chunk_index + 1) % len(broadcast_list_chunks)
|
||
for new_pot in broadcast_list:
|
||
msg = make_msg(ProtocolMessageTypes.request_compact_proof_of_time, new_pot)
|
||
msgs.append(msg)
|
||
await self.server.send_to_specific(msgs, peer_node_id)
|
||
await asyncio.sleep(uncompact_interval_scan)
|
||
except Exception as e:
|
||
error_stack = traceback.format_exc()
|
||
self.log.error(f"Exception in broadcast_uncompact_blocks: {e}")
|
||
self.log.error(f"Exception Stack: {error_stack}")
|
||
|
||
|
||
async def node_next_block_check(
|
||
peer: WSChiaConnection, potential_peek: uint32, blockchain: BlockchainInterface
|
||
) -> bool:
|
||
block_response: Any | None = await peer.call_api(
|
||
FullNodeAPI.request_block, full_node_protocol.RequestBlock(potential_peek, True)
|
||
)
|
||
if block_response is not None and isinstance(block_response, full_node_protocol.RespondBlock):
|
||
peak = blockchain.get_peak()
|
||
if peak is not None and block_response.block.prev_header_hash == peak.header_hash:
|
||
return True
|
||
return False
|