mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
Deletes five functions/classes with zero references (verified repo-wide including tests) and the test-only FullNodeStore.remove_unfinished_block, whose own TODO asked for its removal. The one test caller now exercises the production cleanup method clear_unfinished_blocks_below instead. Co-authored-by: Cursor <cursoragent@cursor.com>
1142 lines
51 KiB
Python
1142 lines
51 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import dataclasses
|
|
import enum
|
|
import logging
|
|
import time
|
|
|
|
from chia_rs import BlockRecord, ConsensusConstants, EndOfSubSlotBundle, FullBlock, UnfinishedBlock
|
|
from chia_rs.sized_bytes import bytes32
|
|
from chia_rs.sized_ints import uint8, uint32, uint64, uint128
|
|
|
|
from chia.consensus.blockchain_interface import BlockRecordsProtocol
|
|
from chia.consensus.difficulty_adjustment import can_finish_sub_and_full_epoch
|
|
from chia.consensus.get_block_challenge import post_hard_fork2
|
|
from chia.consensus.make_sub_epoch_summary import make_sub_epoch_summary
|
|
from chia.consensus.multiprocess_validation import PreValidationResult
|
|
from chia.consensus.pot_iterations import calculate_sp_interval_iters
|
|
from chia.consensus.signage_point import SignagePoint
|
|
from chia.full_node.tx_processing_queue import PeerWithTx
|
|
from chia.protocols import timelord_protocol
|
|
from chia.protocols.outbound_message import Message
|
|
from chia.types.blockchain_format.classgroup import ClassgroupElement
|
|
from chia.types.blockchain_format.proof_of_space import FILTER_WINDOW_SIZE
|
|
from chia.types.blockchain_format.vdf import VDFInfo, validate_vdf
|
|
from chia.util.lru_cache import LRUCache, LRUKeyedListCache, LRUSet
|
|
from chia.util.streamable import Streamable, streamable
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
MAX_UNFINISHED_BLOCKS_PER_REWARD_HASH = 20
|
|
|
|
# Future caches store untrusted timelord/full-node gossip and must stay small
|
|
# even when peers send max-size protocol messages with unique challenges.
|
|
FUTURE_CACHE_ENTRY_TTL_SECONDS = 300
|
|
|
|
FUTURE_SP_CACHE_MAX_KEYS = 128
|
|
FUTURE_SP_CACHE_MAX_ENTRIES_PER_KEY = 64
|
|
|
|
FUTURE_EOS_CACHE_MAX_KEYS = 128
|
|
FUTURE_EOS_CACHE_MAX_ENTRIES_PER_KEY = 4
|
|
|
|
FUTURE_IP_CACHE_MAX_KEYS = 128
|
|
FUTURE_IP_CACHE_MAX_ENTRIES_PER_KEY = 8
|
|
|
|
|
|
class SignagePointAddResult(enum.Enum):
|
|
ADDED = "added"
|
|
NOT_ADDED = "not_added"
|
|
INVALID_VDF = "invalid_vdf"
|
|
|
|
|
|
@streamable
|
|
@dataclasses.dataclass(frozen=True)
|
|
class FullNodeStorePeakResult(Streamable):
|
|
added_eos: EndOfSubSlotBundle | None
|
|
new_signage_points: list[tuple[uint8, SignagePoint]]
|
|
new_infusion_points: list[timelord_protocol.NewInfusionPointVDF]
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class UnfinishedBlockEntry:
|
|
# if this is None, it means we've requested this block but not yet received
|
|
# it
|
|
unfinished_block: UnfinishedBlock | None
|
|
# If this is None, it means we've initiated validation of this block, but it
|
|
# hasn't completed yet
|
|
result: PreValidationResult | None
|
|
height: uint32
|
|
|
|
|
|
def find_best_block(
|
|
result: dict[bytes32 | None, UnfinishedBlockEntry],
|
|
) -> tuple[bytes32 | None, UnfinishedBlock | None]:
|
|
"""
|
|
Given a collection of UnfinishedBlocks (all with the same reward block
|
|
hash), return the "best" one. i.e. the one with the smallest foliage hash.
|
|
"""
|
|
if len(result) == 0:
|
|
return None, None
|
|
|
|
all_blocks = list(result.items())
|
|
if len(all_blocks) == 1:
|
|
foliage_hash, entry = all_blocks[0]
|
|
# this means we don't have the block yet
|
|
if entry.unfinished_block is None:
|
|
return None, None
|
|
else:
|
|
return foliage_hash, entry.unfinished_block
|
|
|
|
def include_block(item: tuple[bytes32 | None, UnfinishedBlockEntry]) -> bool:
|
|
foliage_hash, entry = item
|
|
return foliage_hash is not None and entry.unfinished_block is not None
|
|
|
|
# if there are unfinished blocks with foliage (i.e. not None) we prefer
|
|
# those, so drop the first element
|
|
all_blocks = [e for e in all_blocks if include_block(e)]
|
|
all_blocks = sorted(all_blocks)
|
|
|
|
# we may have filtered out some blocks that we have only requested, but not
|
|
# yet received.
|
|
if len(all_blocks) == 0:
|
|
return None, None
|
|
|
|
return all_blocks[0][0], all_blocks[0][1].unfinished_block
|
|
|
|
|
|
def _maybe_evict_worst_unfinished_block(inner: dict[bytes32 | None, UnfinishedBlockEntry]) -> None:
|
|
if len(inner) <= MAX_UNFINISHED_BLOCKS_PER_REWARD_HASH:
|
|
return
|
|
# None foliage hash is considered worst since the block quality is unknown
|
|
if None in inner:
|
|
del inner[None]
|
|
return
|
|
# we've already checked for None keys. At this point we know there won't be
|
|
# any, but max() doesn't like that the type is still Optional[Bytes32]
|
|
worst_key = max(inner.keys()) # type: ignore[type-var]
|
|
del inner[worst_key]
|
|
|
|
|
|
class FullNodeStore:
|
|
constants: ConsensusConstants
|
|
|
|
# Blocks which we have created, but don't have plot signatures yet, so not yet "unfinished blocks"
|
|
candidate_blocks: dict[bytes32, tuple[uint32, UnfinishedBlock]]
|
|
candidate_backup_blocks: dict[bytes32, tuple[uint32, UnfinishedBlock]]
|
|
|
|
# Block hashes of unfinished blocks that we have seen recently. This is
|
|
# effectively a set[bytes32] but in order to evict the oldest items first,
|
|
# we use a Dict that preserves insertion order, and remove from the
|
|
# beginning
|
|
seen_unfinished_blocks: LRUSet[bytes32]
|
|
|
|
# Unfinished blocks, keyed from reward hash
|
|
# There may be multiple different unfinished blocks with the same partial
|
|
# hash (reward chain block hash). They are stored under their partial hash
|
|
# though. The inner dictionary uses the foliage hash as the key
|
|
# The UnfinishedBlockEntry is a placeholder for UnfinishedBlocks we have
|
|
# requested (but don't have yet) or that we have but haven't completed
|
|
# validation of (yet).
|
|
# The inner key (the foliage hash) is Optional, where None either means
|
|
# it's not a transaction block, or it's a block we learned about via the old
|
|
# protocol, where all we get is the reward block hash.
|
|
_unfinished_blocks: dict[bytes32, dict[bytes32 | None, UnfinishedBlockEntry]]
|
|
|
|
# Finished slots and sps from the peak's slot onwards
|
|
# We store all 32 SPs for each slot, starting as 32 Nones and filling them as we go
|
|
# Also stores the total iters at the end of slot
|
|
# For the first sub-slot, EndOfSlotBundle is None
|
|
finished_sub_slots: list[tuple[EndOfSubSlotBundle | None, list[SignagePoint | None], uint128]]
|
|
|
|
# These caches maintain objects which depend on infused blocks in the reward chain, that we
|
|
# might receive before the blocks themselves. The dict keys are the reward chain challenge hashes.
|
|
|
|
# End of slots which depend on infusions that we don't have
|
|
future_eos_cache: LRUKeyedListCache[bytes32, EndOfSubSlotBundle]
|
|
|
|
# Signage points which depend on infusions that we don't have
|
|
future_sp_cache: LRUKeyedListCache[bytes32, tuple[uint8, SignagePoint]]
|
|
|
|
# Infusion point VDFs which depend on infusions that we don't have
|
|
future_ip_cache: LRUKeyedListCache[bytes32, timelord_protocol.NewInfusionPointVDF]
|
|
|
|
# These recent caches are for pooling support
|
|
recent_signage_points: LRUCache[bytes32, tuple[SignagePoint, float]]
|
|
recent_eos: LRUCache[bytes32, tuple[EndOfSubSlotBundle, float]]
|
|
|
|
pending_tx_request: dict[bytes32, bytes32] # tx_id: peer_id
|
|
# Map of transaction ID to the map of peer ID to its hostname, fee and cost
|
|
# it advertised for that transaction.
|
|
peers_with_tx: dict[bytes32, dict[bytes32, PeerWithTx]]
|
|
tx_fetch_tasks: dict[bytes32, asyncio.Task[None]] # Task id: task
|
|
serialized_wp_message: Message | None
|
|
serialized_wp_message_tip: bytes32 | None
|
|
|
|
def __init__(self, constants: ConsensusConstants):
|
|
self.candidate_blocks = {}
|
|
self.candidate_backup_blocks = {}
|
|
self.seen_unfinished_blocks = LRUSet(1000)
|
|
self._unfinished_blocks = {}
|
|
self.finished_sub_slots = []
|
|
self.future_eos_cache = LRUKeyedListCache(
|
|
FUTURE_EOS_CACHE_MAX_KEYS, FUTURE_EOS_CACHE_MAX_ENTRIES_PER_KEY, ttl_seconds=FUTURE_CACHE_ENTRY_TTL_SECONDS
|
|
)
|
|
self.future_sp_cache = LRUKeyedListCache(
|
|
FUTURE_SP_CACHE_MAX_KEYS, FUTURE_SP_CACHE_MAX_ENTRIES_PER_KEY, ttl_seconds=FUTURE_CACHE_ENTRY_TTL_SECONDS
|
|
)
|
|
self.future_ip_cache = LRUKeyedListCache(
|
|
FUTURE_IP_CACHE_MAX_KEYS, FUTURE_IP_CACHE_MAX_ENTRIES_PER_KEY, ttl_seconds=FUTURE_CACHE_ENTRY_TTL_SECONDS
|
|
)
|
|
self.recent_signage_points = LRUCache(500)
|
|
self.recent_eos = LRUCache(50)
|
|
self.constants = constants
|
|
self.clear_slots()
|
|
self.initialize_genesis_sub_slot()
|
|
self.pending_tx_request = {}
|
|
self.peers_with_tx = {}
|
|
self.tx_fetch_tasks = {}
|
|
self.serialized_wp_message = None
|
|
self.serialized_wp_message_tip = None
|
|
|
|
def is_requesting_unfinished_block(
|
|
self, reward_block_hash: bytes32, foliage_hash: bytes32 | None
|
|
) -> tuple[bool, int]:
|
|
"""
|
|
Asks if we are already requesting this specific unfinished block (given
|
|
the reward block hash and foliage hash). The returned bool is true if we
|
|
are and false otherwise. The function also returns the number of
|
|
variants of an unfinished block with this reward block hash we are
|
|
currently requesting. This is useful to ensure we limit the number of
|
|
variants we request.
|
|
"""
|
|
ents = self._unfinished_blocks.get(reward_block_hash)
|
|
if ents is None:
|
|
return (False, 0)
|
|
elif foliage_hash is None:
|
|
return (len(ents) > 0, len(ents))
|
|
else:
|
|
return (foliage_hash in ents, len(ents))
|
|
|
|
def mark_requesting_unfinished_block(self, reward_block_hash: bytes32, foliage_hash: bytes32 | None) -> None:
|
|
ents = self._unfinished_blocks.setdefault(reward_block_hash, {})
|
|
ents.setdefault(foliage_hash, UnfinishedBlockEntry(None, None, uint32(0)))
|
|
_maybe_evict_worst_unfinished_block(ents)
|
|
|
|
def remove_requesting_unfinished_block(self, reward_block_hash: bytes32, foliage_hash: bytes32 | None) -> None:
|
|
reward_ents = self._unfinished_blocks.get(reward_block_hash)
|
|
if reward_ents is None:
|
|
return
|
|
foliage_ent = reward_ents.get(foliage_hash)
|
|
if foliage_ent is None:
|
|
return
|
|
if foliage_ent.unfinished_block is not None:
|
|
# in this case we've successfully received the unfinished block,
|
|
# it's already considered "not requesting", but actually downloaded
|
|
return
|
|
del reward_ents[foliage_hash]
|
|
if len(reward_ents) == 0:
|
|
del self._unfinished_blocks[reward_block_hash]
|
|
|
|
def add_candidate_block(
|
|
self, quality_string: bytes32, height: uint32, unfinished_block: UnfinishedBlock, backup: bool = False
|
|
) -> None:
|
|
if backup:
|
|
self.candidate_backup_blocks[quality_string] = (height, unfinished_block)
|
|
else:
|
|
self.candidate_blocks[quality_string] = (height, unfinished_block)
|
|
|
|
def get_candidate_block(
|
|
self, quality_string: bytes32, backup: bool = False
|
|
) -> tuple[uint32, UnfinishedBlock] | None:
|
|
if backup:
|
|
return self.candidate_backup_blocks.get(quality_string, None)
|
|
else:
|
|
return self.candidate_blocks.get(quality_string, None)
|
|
|
|
def clear_candidate_blocks_below(self, height: uint32) -> None:
|
|
del_keys = []
|
|
for key, value in self.candidate_blocks.items():
|
|
if value[0] < height:
|
|
del_keys.append(key)
|
|
for key in del_keys:
|
|
try:
|
|
del self.candidate_blocks[key]
|
|
except KeyError:
|
|
pass
|
|
del_keys = []
|
|
for key, value in self.candidate_backup_blocks.items():
|
|
if value[0] < height:
|
|
del_keys.append(key)
|
|
for key in del_keys:
|
|
try:
|
|
del self.candidate_backup_blocks[key]
|
|
except KeyError:
|
|
pass
|
|
|
|
def seen_unfinished_block(self, object_hash: bytes32) -> bool:
|
|
if object_hash in self.seen_unfinished_blocks:
|
|
return True
|
|
self.seen_unfinished_blocks.put(object_hash)
|
|
return False
|
|
|
|
def add_unfinished_block(
|
|
self, height: uint32, unfinished_block: UnfinishedBlock, result: PreValidationResult
|
|
) -> None:
|
|
partial_hash = unfinished_block.partial_hash
|
|
entry = self._unfinished_blocks.setdefault(partial_hash, {})
|
|
entry[unfinished_block.foliage.foliage_transaction_block_hash] = UnfinishedBlockEntry(
|
|
unfinished_block, result, height
|
|
)
|
|
_maybe_evict_worst_unfinished_block(entry)
|
|
|
|
def get_unfinished_block(self, unfinished_reward_hash: bytes32) -> UnfinishedBlock | None:
|
|
result = self._unfinished_blocks.get(unfinished_reward_hash, None)
|
|
if result is None:
|
|
return None
|
|
# The old API doesn't distinguish between duplicate UnfinishedBlocks,
|
|
# return the *best* UnfinishedBlock. This is the path taken when the
|
|
# timelord sends us an infusion point with this specific reward block
|
|
# hash. We pick one of the unfinished blocks based on an arbitrary but
|
|
# deterministic property.
|
|
# this sorts the UnfinishedBlocks by the foliage hash, and picks the
|
|
# smallest hash
|
|
_foliage_hash, block = find_best_block(result)
|
|
return block
|
|
|
|
def get_unfinished_block2(
|
|
self, unfinished_reward_hash: bytes32, unfinished_foliage_hash: bytes32 | None
|
|
) -> tuple[UnfinishedBlock | None, int, bool]:
|
|
"""
|
|
Looks up an UnfinishedBlock by its reward block hash and foliage hash.
|
|
If the foliage hash is None (e.g. it's not a transaction block), we fall
|
|
back to the original function that looks up unfinished blocks just by
|
|
their reward block hash.
|
|
Returns:
|
|
1. the (optional) UnfinishedBlock
|
|
2. the number of other candidate blocks we know of with the same
|
|
reward block hash
|
|
3. whether we already have a "better" UnfinishedBlock candidate than
|
|
this
|
|
"""
|
|
result = self._unfinished_blocks.get(unfinished_reward_hash, None)
|
|
if result is None:
|
|
return None, 0, False
|
|
if unfinished_foliage_hash is None:
|
|
foliage_hash, block = find_best_block(result)
|
|
return block, len(result), False
|
|
|
|
foliage_hash, block = find_best_block(result)
|
|
has_better: bool = foliage_hash is not None and foliage_hash < unfinished_foliage_hash
|
|
|
|
entry = result.get(unfinished_foliage_hash)
|
|
|
|
if entry is None:
|
|
return None, len(result), has_better
|
|
else:
|
|
return entry.unfinished_block, len(result), has_better
|
|
|
|
# we only have PreValidationResults for transaction blocks, and they all
|
|
# have a foliage hash. That's why unfinished_foliage_hash is not Optional.
|
|
def get_unfinished_block_result(
|
|
self, unfinished_reward_hash: bytes32, unfinished_foliage_hash: bytes32
|
|
) -> UnfinishedBlockEntry | None:
|
|
result = self._unfinished_blocks.get(unfinished_reward_hash, None)
|
|
if result is None:
|
|
return None
|
|
else:
|
|
return result.get(unfinished_foliage_hash)
|
|
|
|
# returns all unfinished blocks for the specified height
|
|
def get_unfinished_blocks(self, height: uint32) -> list[UnfinishedBlock]:
|
|
ret: list[UnfinishedBlock] = []
|
|
for entry in self._unfinished_blocks.values():
|
|
for ube in entry.values():
|
|
if ube.height == height and ube.unfinished_block is not None:
|
|
ret.append(ube.unfinished_block)
|
|
return ret
|
|
|
|
def clear_unfinished_blocks_below(self, height: uint32) -> None:
|
|
del_partial: list[bytes32] = []
|
|
for partial_hash, entry in self._unfinished_blocks.items():
|
|
del_foliage: list[bytes32 | None] = []
|
|
for foliage_hash, ube in entry.items():
|
|
if ube.height < height:
|
|
del_foliage.append(foliage_hash)
|
|
for fh in del_foliage:
|
|
del entry[fh]
|
|
if len(entry) == 0:
|
|
del_partial.append(partial_hash)
|
|
for ph in del_partial:
|
|
del self._unfinished_blocks[ph]
|
|
|
|
def add_to_future_ip(self, infusion_point: timelord_protocol.NewInfusionPointVDF) -> None:
|
|
ch: bytes32 = infusion_point.reward_chain_ip_vdf.challenge
|
|
self.future_ip_cache.append(ch, infusion_point)
|
|
|
|
def in_future_sp_cache(self, signage_point: SignagePoint, index: uint8) -> bool:
|
|
if signage_point.rc_vdf is None:
|
|
return False
|
|
|
|
if signage_point.rc_vdf.challenge not in self.future_sp_cache:
|
|
return False
|
|
for cache_index, cache_sp in self.future_sp_cache[signage_point.rc_vdf.challenge]:
|
|
if cache_index == index and cache_sp.rc_vdf == signage_point.rc_vdf:
|
|
return True
|
|
return False
|
|
|
|
def add_to_future_sp(self, signage_point: SignagePoint, index: uint8) -> None:
|
|
if (
|
|
signage_point.cc_vdf is None
|
|
or signage_point.rc_vdf is None
|
|
or signage_point.cc_proof is None
|
|
or signage_point.rc_proof is None
|
|
):
|
|
return None
|
|
challenge = signage_point.rc_vdf.challenge
|
|
if self.in_future_sp_cache(signage_point, index):
|
|
return None
|
|
if not self.future_sp_cache.append(challenge, (index, signage_point)):
|
|
return None
|
|
log.info(f"Don't have rc hash {challenge.hex()}. caching signage point {index}.")
|
|
|
|
def get_future_ip(self, rc_challenge_hash: bytes32) -> list[timelord_protocol.NewInfusionPointVDF]:
|
|
return self.future_ip_cache.get(rc_challenge_hash, [])
|
|
|
|
def clear_old_cache_entries(self) -> None:
|
|
self.future_ip_cache.evict_expired()
|
|
self.future_eos_cache.evict_expired()
|
|
self.future_sp_cache.evict_expired()
|
|
|
|
def clear_slots(self) -> None:
|
|
self.finished_sub_slots.clear()
|
|
|
|
def get_sub_slot(self, challenge_hash: bytes32) -> tuple[EndOfSubSlotBundle, int, uint128] | None:
|
|
assert len(self.finished_sub_slots) >= 1
|
|
for index, (sub_slot, _, total_iters) in enumerate(self.finished_sub_slots):
|
|
if sub_slot is not None and sub_slot.challenge_chain.get_hash() == challenge_hash:
|
|
return sub_slot, index, total_iters
|
|
return None
|
|
|
|
def initialize_genesis_sub_slot(self) -> None:
|
|
self.clear_slots()
|
|
self.finished_sub_slots = [(None, [None] * self.constants.NUM_SPS_SUB_SLOT, uint128(0))]
|
|
|
|
def new_finished_sub_slot(
|
|
self,
|
|
eos: EndOfSubSlotBundle,
|
|
blocks: BlockRecordsProtocol,
|
|
peak: BlockRecord | None,
|
|
next_sub_slot_iters: uint64,
|
|
next_difficulty: uint64,
|
|
peak_full_block: FullBlock | None,
|
|
) -> list[timelord_protocol.NewInfusionPointVDF] | None:
|
|
"""
|
|
Returns false if not added. Returns a list if added. The list contains all infusion points that depended
|
|
on this sub slot
|
|
"""
|
|
assert len(self.finished_sub_slots) >= 1
|
|
assert (peak is None) == (peak_full_block is None)
|
|
|
|
last_slot, _, last_slot_iters = self.finished_sub_slots[-1]
|
|
|
|
cc_challenge: bytes32 = (
|
|
last_slot.challenge_chain.get_hash() if last_slot is not None else self.constants.GENESIS_CHALLENGE
|
|
)
|
|
rc_challenge: bytes32 = (
|
|
last_slot.reward_chain.get_hash() if last_slot is not None else self.constants.GENESIS_CHALLENGE
|
|
)
|
|
icc_challenge: bytes32 | None = None
|
|
icc_iters: uint64 | None = None
|
|
|
|
# Skip if already present
|
|
for slot, _, _ in self.finished_sub_slots:
|
|
if slot == eos:
|
|
return []
|
|
|
|
if eos.challenge_chain.challenge_chain_end_of_slot_vdf.challenge != cc_challenge:
|
|
# This slot does not append to our next slot
|
|
# This prevent other peers from appending fake VDFs to our cache
|
|
log.error(
|
|
f"bad cc_challenge in new_finished_sub_slot, "
|
|
f"got {eos.challenge_chain.challenge_chain_end_of_slot_vdf.challenge.hex()}"
|
|
f"expected {cc_challenge}"
|
|
)
|
|
return None
|
|
|
|
if peak is None:
|
|
sub_slot_iters = self.constants.SUB_SLOT_ITERS_STARTING
|
|
else:
|
|
sub_slot_iters = peak.sub_slot_iters
|
|
|
|
total_iters = uint128(last_slot_iters + sub_slot_iters)
|
|
|
|
if peak is not None and peak.total_iters > last_slot_iters:
|
|
# Peak is in this slot
|
|
|
|
# Note: Adding an end of subslot does not lock the blockchain, for performance reasons. Only the
|
|
# timelord_lock is used. Therefore, it's possible that we add a new peak at the same time as seeing
|
|
# the finished subslot, and the peak is not fully added yet, so it looks like we still need the subslot.
|
|
# In that case, we will exit here and let the new_peak code add the subslot.
|
|
if total_iters < peak.total_iters:
|
|
log.debug("dont add slot, total_iters < peak.total_iters")
|
|
return None
|
|
|
|
rc_challenge = bytes32(eos.reward_chain.end_of_slot_vdf.challenge)
|
|
cc_start_element = peak.challenge_vdf_output
|
|
iters = uint64(total_iters - peak.total_iters)
|
|
if peak.reward_infusion_new_challenge != rc_challenge:
|
|
if not self.future_eos_cache.append(rc_challenge, eos):
|
|
return None
|
|
log.info(f"Don't have challenge hash {rc_challenge}, caching EOS")
|
|
return None
|
|
|
|
if peak.deficit == 0:
|
|
if eos.reward_chain.deficit != self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK:
|
|
log.error(
|
|
f"eos reward_chain deficit got {eos.reward_chain.deficit} "
|
|
f"expected {self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK}"
|
|
)
|
|
return None
|
|
elif eos.reward_chain.deficit != peak.deficit:
|
|
log.error(f"wrong eos reward_chain deficit got {eos.reward_chain.deficit} expected {peak.deficit}")
|
|
return None
|
|
|
|
if peak.deficit == self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK:
|
|
icc_start_element = None
|
|
elif peak.deficit == self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK - 1:
|
|
icc_start_element = ClassgroupElement.get_default_element()
|
|
else:
|
|
icc_start_element = peak.infused_challenge_vdf_output
|
|
|
|
if peak.deficit < self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK:
|
|
curr = peak
|
|
while not curr.first_in_sub_slot and not curr.is_challenge_block(self.constants):
|
|
curr = blocks.block_record(curr.prev_hash)
|
|
if curr.is_challenge_block(self.constants):
|
|
icc_challenge = curr.challenge_block_info_hash
|
|
icc_iters = uint64(total_iters - curr.total_iters)
|
|
else:
|
|
assert curr.finished_infused_challenge_slot_hashes is not None
|
|
icc_challenge = curr.finished_infused_challenge_slot_hashes[-1]
|
|
icc_iters = sub_slot_iters
|
|
assert icc_challenge is not None
|
|
|
|
finish_se, finish_epoch = can_finish_sub_and_full_epoch(
|
|
self.constants,
|
|
blocks,
|
|
peak.height,
|
|
peak.prev_hash,
|
|
peak.deficit,
|
|
peak.sub_epoch_summary_included is not None,
|
|
)
|
|
if finish_se:
|
|
# this is the first slot in a new sub epoch, should include SES
|
|
post_hard_fork = post_hard_fork2(
|
|
constants=self.constants,
|
|
blocks=blocks,
|
|
prev_b_hash=peak.prev_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
|
|
),
|
|
)
|
|
expected_sub_epoch_summary = make_sub_epoch_summary(
|
|
self.constants,
|
|
blocks,
|
|
peak.height,
|
|
blocks.block_record(blocks.block_record(peak.prev_hash).prev_hash),
|
|
next_difficulty if finish_epoch else None,
|
|
next_sub_slot_iters if finish_epoch else None,
|
|
make_challenge_root=post_hard_fork,
|
|
)
|
|
|
|
if eos.challenge_chain.subepoch_summary_hash is None:
|
|
log.warning("SES should not be None")
|
|
return None
|
|
|
|
if eos.challenge_chain.subepoch_summary_hash != expected_sub_epoch_summary.get_hash():
|
|
log.warning(
|
|
f"Bad SES, expected {expected_sub_epoch_summary} "
|
|
f"expected hash {expected_sub_epoch_summary.get_hash()}, got {eos.challenge_chain}"
|
|
)
|
|
return None
|
|
|
|
if finish_epoch:
|
|
# this is the first slot in a new epoch check diff and iterations
|
|
if (
|
|
eos.challenge_chain.new_sub_slot_iters is None
|
|
or eos.challenge_chain.new_sub_slot_iters != next_sub_slot_iters
|
|
):
|
|
log.error("wrong new iterations at end of slot bundle")
|
|
return None
|
|
|
|
if (
|
|
eos.challenge_chain.new_difficulty is None
|
|
or eos.challenge_chain.new_difficulty != next_difficulty
|
|
):
|
|
log.info("wrong new difficulty at end of slot bundle")
|
|
return None
|
|
|
|
else:
|
|
if eos.challenge_chain.new_sub_slot_iters is not None:
|
|
log.error("got new iterations at end of slot bundle when it should be None")
|
|
return None
|
|
|
|
if eos.challenge_chain.new_difficulty is not None:
|
|
log.info("got new difficulty at end of slot bundle when it should be None")
|
|
return None
|
|
|
|
else:
|
|
# empty slots dont have sub_epoch_summary
|
|
if eos.challenge_chain.subepoch_summary_hash is not None:
|
|
log.warning("SES not correct, should be None in an empty slot")
|
|
return None
|
|
|
|
# This is on an empty slot
|
|
cc_start_element = ClassgroupElement.get_default_element()
|
|
icc_start_element = ClassgroupElement.get_default_element()
|
|
iters = sub_slot_iters
|
|
icc_iters = sub_slot_iters
|
|
|
|
# The icc should only be present if the previous slot had an icc too, and not deficit 0 (just finished slot)
|
|
icc_challenge = (
|
|
last_slot.infused_challenge_chain.get_hash()
|
|
if last_slot is not None
|
|
and last_slot.infused_challenge_chain is not None
|
|
and last_slot.reward_chain.deficit != self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK
|
|
else None
|
|
)
|
|
|
|
# Validate cc VDF
|
|
partial_cc_vdf_info = VDFInfo(
|
|
cc_challenge,
|
|
iters,
|
|
eos.challenge_chain.challenge_chain_end_of_slot_vdf.output,
|
|
)
|
|
# The EOS will have the whole sub-slot iters, but the proof is only the delta, from the last peak
|
|
if eos.challenge_chain.challenge_chain_end_of_slot_vdf != partial_cc_vdf_info.replace(
|
|
number_of_iterations=sub_slot_iters
|
|
):
|
|
return None
|
|
if not eos.proofs.challenge_chain_slot_proof.normalized_to_identity and not validate_vdf(
|
|
eos.proofs.challenge_chain_slot_proof,
|
|
self.constants,
|
|
cc_start_element,
|
|
partial_cc_vdf_info,
|
|
):
|
|
return None
|
|
if eos.proofs.challenge_chain_slot_proof.normalized_to_identity and not validate_vdf(
|
|
eos.proofs.challenge_chain_slot_proof,
|
|
self.constants,
|
|
ClassgroupElement.get_default_element(),
|
|
eos.challenge_chain.challenge_chain_end_of_slot_vdf,
|
|
):
|
|
return None
|
|
|
|
# Validate reward chain VDF
|
|
if not validate_vdf(
|
|
eos.proofs.reward_chain_slot_proof,
|
|
self.constants,
|
|
ClassgroupElement.get_default_element(),
|
|
eos.reward_chain.end_of_slot_vdf,
|
|
VDFInfo(rc_challenge, iters, eos.reward_chain.end_of_slot_vdf.output),
|
|
):
|
|
return None
|
|
|
|
if icc_challenge is not None:
|
|
assert icc_start_element is not None
|
|
assert icc_iters is not None
|
|
assert eos.infused_challenge_chain is not None
|
|
assert eos.infused_challenge_chain is not None
|
|
assert eos.proofs.infused_challenge_chain_slot_proof is not None
|
|
if eos.reward_chain.deficit == self.constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK:
|
|
# only at the end of a challenge slot
|
|
if eos.infused_challenge_chain.get_hash() != eos.challenge_chain.infused_challenge_chain_sub_slot_hash:
|
|
log.error("infused_challenge_chain mismatch in challenge_chain")
|
|
return None
|
|
else:
|
|
assert eos.challenge_chain.infused_challenge_chain_sub_slot_hash is None
|
|
assert eos.infused_challenge_chain.get_hash() == eos.reward_chain.infused_challenge_chain_sub_slot_hash
|
|
|
|
partial_icc_vdf_info = VDFInfo(
|
|
icc_challenge,
|
|
iters,
|
|
eos.infused_challenge_chain.infused_challenge_chain_end_of_slot_vdf.output,
|
|
)
|
|
# The EOS will have the whole sub-slot iters, but the proof is only the delta, from the last peak
|
|
if eos.infused_challenge_chain.infused_challenge_chain_end_of_slot_vdf != partial_icc_vdf_info.replace(
|
|
number_of_iterations=icc_iters
|
|
):
|
|
return None
|
|
if not eos.proofs.infused_challenge_chain_slot_proof.normalized_to_identity and not validate_vdf(
|
|
eos.proofs.infused_challenge_chain_slot_proof, self.constants, icc_start_element, partial_icc_vdf_info
|
|
):
|
|
return None
|
|
if eos.proofs.infused_challenge_chain_slot_proof.normalized_to_identity and not validate_vdf(
|
|
eos.proofs.infused_challenge_chain_slot_proof,
|
|
self.constants,
|
|
ClassgroupElement.get_default_element(),
|
|
eos.infused_challenge_chain.infused_challenge_chain_end_of_slot_vdf,
|
|
):
|
|
return None
|
|
else:
|
|
# This is the first sub slot and it's empty, therefore there is no ICC
|
|
if eos.infused_challenge_chain is not None or eos.proofs.infused_challenge_chain_slot_proof is not None:
|
|
return None
|
|
if eos.challenge_chain.infused_challenge_chain_sub_slot_hash is not None:
|
|
return None
|
|
if eos.reward_chain.infused_challenge_chain_sub_slot_hash is not None:
|
|
return None
|
|
|
|
self.finished_sub_slots.append((eos, [None] * self.constants.NUM_SPS_SUB_SLOT, total_iters))
|
|
|
|
new_cc_hash = eos.challenge_chain.get_hash()
|
|
self.recent_eos.put(new_cc_hash, (eos, time.time()))
|
|
|
|
new_ips: list[timelord_protocol.NewInfusionPointVDF] = []
|
|
for ip in self.future_ip_cache.get(eos.reward_chain.get_hash(), []):
|
|
new_ips.append(ip)
|
|
|
|
return new_ips
|
|
|
|
def new_signage_point(
|
|
self,
|
|
index: uint8,
|
|
blocks: BlockRecordsProtocol,
|
|
peak: BlockRecord | None,
|
|
next_sub_slot_iters: uint64,
|
|
signage_point: SignagePoint,
|
|
skip_vdf_validation: bool = False,
|
|
) -> SignagePointAddResult:
|
|
"""
|
|
Returns:
|
|
ADDED: SP was stored successfully.
|
|
NOT_ADDED: SP was rejected for structural reasons (wrong sub-slot, future SP,
|
|
info mismatch). May be cached for later retry via add_to_future_sp.
|
|
INVALID_VDF: Challenge hash and VDF info matched expectations but the
|
|
cryptographic proof failed verification. Caller should ban the peer.
|
|
"""
|
|
assert len(self.finished_sub_slots) >= 1
|
|
|
|
if peak is None or peak.height < 2:
|
|
sub_slot_iters = self.constants.SUB_SLOT_ITERS_STARTING
|
|
else:
|
|
sub_slot_iters = peak.sub_slot_iters
|
|
|
|
if index == 0 or index >= self.constants.NUM_SPS_SUB_SLOT:
|
|
return SignagePointAddResult.NOT_ADDED
|
|
assert (
|
|
signage_point.cc_vdf is not None
|
|
and signage_point.cc_proof is not None
|
|
and signage_point.rc_vdf is not None
|
|
and signage_point.rc_proof is not None
|
|
)
|
|
for sub_slot, sp_arr, start_ss_total_iters in self.finished_sub_slots:
|
|
if sub_slot is None:
|
|
assert start_ss_total_iters == 0
|
|
ss_challenge_hash = self.constants.GENESIS_CHALLENGE
|
|
ss_reward_hash = self.constants.GENESIS_CHALLENGE
|
|
else:
|
|
ss_challenge_hash = sub_slot.challenge_chain.get_hash()
|
|
ss_reward_hash = sub_slot.reward_chain.get_hash()
|
|
if ss_challenge_hash == signage_point.cc_vdf.challenge:
|
|
# If we do have this slot, find the Prev block from SP and validate SP
|
|
if peak is not None and start_ss_total_iters > peak.total_iters:
|
|
# We are in a future sub slot from the peak, so maybe there is a new SSI
|
|
checkpoint_size: uint64 = uint64(next_sub_slot_iters // self.constants.NUM_SPS_SUB_SLOT)
|
|
delta_iters: uint64 = uint64(checkpoint_size * index)
|
|
future_sub_slot: bool = True
|
|
else:
|
|
# We are not in a future sub slot from the peak, so there is no new SSI
|
|
checkpoint_size = uint64(sub_slot_iters // self.constants.NUM_SPS_SUB_SLOT)
|
|
delta_iters = uint64(checkpoint_size * index)
|
|
future_sub_slot = False
|
|
sp_total_iters = start_ss_total_iters + delta_iters
|
|
|
|
curr = peak
|
|
if peak is None or future_sub_slot:
|
|
check_from_start_of_ss = True
|
|
else:
|
|
check_from_start_of_ss = False
|
|
while (
|
|
curr is not None
|
|
and curr.total_iters > start_ss_total_iters
|
|
and curr.total_iters > sp_total_iters
|
|
):
|
|
if curr.first_in_sub_slot:
|
|
# Did not find a block where it's iters are before our sp_total_iters, in this ss
|
|
check_from_start_of_ss = True
|
|
break
|
|
curr = blocks.block_record(curr.prev_hash)
|
|
|
|
if check_from_start_of_ss:
|
|
# Check VDFs from start of sub slot
|
|
cc_vdf_info_expected = VDFInfo(
|
|
ss_challenge_hash,
|
|
delta_iters,
|
|
signage_point.cc_vdf.output,
|
|
)
|
|
|
|
rc_vdf_info_expected = VDFInfo(
|
|
ss_reward_hash,
|
|
delta_iters,
|
|
signage_point.rc_vdf.output,
|
|
)
|
|
else:
|
|
# Check VDFs from curr
|
|
assert curr is not None
|
|
cc_vdf_info_expected = VDFInfo(
|
|
ss_challenge_hash,
|
|
uint64(sp_total_iters - curr.total_iters),
|
|
signage_point.cc_vdf.output,
|
|
)
|
|
rc_vdf_info_expected = VDFInfo(
|
|
curr.reward_infusion_new_challenge,
|
|
uint64(sp_total_iters - curr.total_iters),
|
|
signage_point.rc_vdf.output,
|
|
)
|
|
if not signage_point.cc_vdf == cc_vdf_info_expected.replace(number_of_iterations=delta_iters):
|
|
self.add_to_future_sp(signage_point, index)
|
|
return SignagePointAddResult.NOT_ADDED
|
|
if check_from_start_of_ss:
|
|
start_ele = ClassgroupElement.get_default_element()
|
|
else:
|
|
assert curr is not None
|
|
start_ele = curr.challenge_vdf_output
|
|
if not skip_vdf_validation:
|
|
# Non-normalized CC proofs are for a VDF segment. The SP's CC challenge/output can match while
|
|
# an honest peer proves from a different previous in-slot block than our current start element.
|
|
if not signage_point.cc_proof.normalized_to_identity and not validate_vdf(
|
|
signage_point.cc_proof,
|
|
self.constants,
|
|
start_ele,
|
|
cc_vdf_info_expected,
|
|
):
|
|
self.add_to_future_sp(signage_point, index)
|
|
return SignagePointAddResult.NOT_ADDED
|
|
if signage_point.cc_proof.normalized_to_identity and not validate_vdf(
|
|
signage_point.cc_proof,
|
|
self.constants,
|
|
ClassgroupElement.get_default_element(),
|
|
signage_point.cc_vdf,
|
|
):
|
|
return SignagePointAddResult.INVALID_VDF
|
|
|
|
if rc_vdf_info_expected != signage_point.rc_vdf:
|
|
self.add_to_future_sp(signage_point, index)
|
|
return SignagePointAddResult.NOT_ADDED
|
|
|
|
if not skip_vdf_validation:
|
|
if not validate_vdf(
|
|
signage_point.rc_proof,
|
|
self.constants,
|
|
ClassgroupElement.get_default_element(),
|
|
signage_point.rc_vdf,
|
|
rc_vdf_info_expected,
|
|
):
|
|
return SignagePointAddResult.INVALID_VDF
|
|
|
|
sp_arr[index] = signage_point
|
|
self.recent_signage_points.put(signage_point.cc_vdf.output.get_hash(), (signage_point, time.time()))
|
|
return SignagePointAddResult.ADDED
|
|
self.add_to_future_sp(signage_point, index)
|
|
return SignagePointAddResult.NOT_ADDED
|
|
|
|
def get_signage_point(self, cc_signage_point: bytes32) -> SignagePoint | None:
|
|
assert len(self.finished_sub_slots) >= 1
|
|
if cc_signage_point == self.constants.GENESIS_CHALLENGE:
|
|
return SignagePoint(None, None, None, None)
|
|
|
|
for sub_slot, sps, _ in self.finished_sub_slots:
|
|
if sub_slot is not None and sub_slot.challenge_chain.get_hash() == cc_signage_point:
|
|
return SignagePoint(None, None, None, None)
|
|
for sp in sps:
|
|
if sp is not None:
|
|
assert sp.cc_vdf is not None
|
|
if sp.cc_vdf.output.get_hash() == cc_signage_point:
|
|
return sp
|
|
return None
|
|
|
|
def get_filter_challenge(self, challenge: bytes32, index: uint8) -> bytes32 | None:
|
|
"""
|
|
Get the filter_challenge for V2 plot filter.
|
|
|
|
The filter_challenge is the cc sub-slot challenge hash of a previously
|
|
completed sub-slot. All SPs in the same window share the same value.
|
|
"""
|
|
assert len(self.finished_sub_slots) >= 1
|
|
|
|
def previous_sub_slot_challenge(sub_slot: EndOfSubSlotBundle) -> bytes32:
|
|
return sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf.challenge
|
|
|
|
def get_active_or_recent_sub_slot(slot_challenge: bytes32) -> EndOfSubSlotBundle | None:
|
|
for active_sub_slot, _, _ in self.finished_sub_slots:
|
|
if active_sub_slot is not None and active_sub_slot.challenge_chain.get_hash() == slot_challenge:
|
|
return active_sub_slot
|
|
|
|
recent_eos = self.recent_eos.get(slot_challenge)
|
|
if recent_eos is None:
|
|
return None
|
|
|
|
return recent_eos[0]
|
|
|
|
window_start = (index // FILTER_WINDOW_SIZE) * FILTER_WINDOW_SIZE
|
|
|
|
for sub_slot, _, _ in self.finished_sub_slots:
|
|
slot_challenge = (
|
|
sub_slot.challenge_chain.get_hash() if sub_slot is not None else self.constants.GENESIS_CHALLENGE
|
|
)
|
|
if slot_challenge != challenge:
|
|
continue
|
|
|
|
if window_start == 0:
|
|
# Window [0-15]: use SS(n-2) challenge hash
|
|
if sub_slot is None:
|
|
log.debug("filter_challenge unavailable: not enough sub-slot history for window 0")
|
|
return None
|
|
|
|
previous_challenge = previous_sub_slot_challenge(sub_slot)
|
|
if previous_challenge == self.constants.GENESIS_CHALLENGE:
|
|
log.debug("filter_challenge unavailable: not enough sub-slot history for window 0")
|
|
return None
|
|
|
|
previous_sub_slot = get_active_or_recent_sub_slot(previous_challenge)
|
|
if previous_sub_slot is None:
|
|
log.debug("filter_challenge unavailable: missing previous sub-slot for window 0")
|
|
return None
|
|
|
|
return previous_sub_slot_challenge(previous_sub_slot)
|
|
else:
|
|
# Windows [16-31], [32-47], [48-63]: use SS(n-1) challenge hash
|
|
if sub_slot is None:
|
|
log.debug("filter_challenge unavailable: no previous sub-slot")
|
|
return None
|
|
return previous_sub_slot_challenge(sub_slot)
|
|
|
|
log.debug("filter_challenge unavailable: challenge %s not found", challenge.hex()[:16])
|
|
return None
|
|
|
|
def get_signage_point_by_index_and_cc_output(
|
|
self, cc_signage_point: bytes32, challenge: bytes32, index: uint8
|
|
) -> SignagePoint | None:
|
|
assert len(self.finished_sub_slots) >= 1
|
|
for sub_slot, sps, _ in self.finished_sub_slots:
|
|
if sub_slot is not None:
|
|
cc_hash = sub_slot.challenge_chain.get_hash()
|
|
else:
|
|
cc_hash = self.constants.GENESIS_CHALLENGE
|
|
if cc_hash == challenge:
|
|
if index == 0:
|
|
# first SP in the sub slot
|
|
return SignagePoint(None, None, None, None)
|
|
sp: SignagePoint | None = sps[index]
|
|
if sp is None:
|
|
return None
|
|
assert sp.cc_vdf is not None
|
|
if sp.cc_vdf.output.get_hash() == cc_signage_point:
|
|
return sp
|
|
return None
|
|
|
|
def get_signage_point_by_index(
|
|
self, challenge_hash: bytes32, index: uint8, last_rc_infusion: bytes32
|
|
) -> SignagePoint | None:
|
|
assert len(self.finished_sub_slots) >= 1
|
|
for sub_slot, sps, _ in self.finished_sub_slots:
|
|
if sub_slot is not None:
|
|
cc_hash = sub_slot.challenge_chain.get_hash()
|
|
else:
|
|
cc_hash = self.constants.GENESIS_CHALLENGE
|
|
|
|
if cc_hash == challenge_hash:
|
|
if index == 0:
|
|
return SignagePoint(None, None, None, None)
|
|
sp: SignagePoint | None = sps[index]
|
|
if sp is not None:
|
|
assert sp.rc_vdf is not None
|
|
if sp.rc_vdf.challenge == last_rc_infusion:
|
|
return sp
|
|
return None
|
|
return None
|
|
|
|
def have_newer_signage_point(self, challenge_hash: bytes32, index: uint8, last_rc_infusion: bytes32) -> bool:
|
|
"""
|
|
Returns true if we have a signage point at this index which is based on a newer infusion.
|
|
"""
|
|
assert len(self.finished_sub_slots) >= 1
|
|
for sub_slot, sps, _ in self.finished_sub_slots:
|
|
if sub_slot is not None:
|
|
cc_hash = sub_slot.challenge_chain.get_hash()
|
|
else:
|
|
cc_hash = self.constants.GENESIS_CHALLENGE
|
|
|
|
if cc_hash == challenge_hash:
|
|
found_rc_hash = False
|
|
for i in range(index):
|
|
sp: SignagePoint | None = sps[i]
|
|
if sp is not None and sp.rc_vdf is not None and sp.rc_vdf.challenge == last_rc_infusion:
|
|
found_rc_hash = True
|
|
sp = sps[index]
|
|
if (
|
|
found_rc_hash
|
|
and sp is not None
|
|
and sp.rc_vdf is not None
|
|
and sp.rc_vdf.challenge != last_rc_infusion
|
|
):
|
|
return True
|
|
return False
|
|
|
|
def new_peak(
|
|
self,
|
|
peak: BlockRecord,
|
|
peak_full_block: FullBlock,
|
|
sp_sub_slot: EndOfSubSlotBundle | None, # None if not overflow, or in first/second slot
|
|
ip_sub_slot: EndOfSubSlotBundle | None, # None if in first slot
|
|
fork_block: BlockRecord | None,
|
|
blocks: BlockRecordsProtocol,
|
|
next_sub_slot_iters: uint64,
|
|
next_difficulty: uint64,
|
|
) -> FullNodeStorePeakResult:
|
|
"""
|
|
If the peak is an overflow block, must provide two sub-slots: one for the current sub-slot and one for
|
|
the prev sub-slot (since we still might get more blocks with an sp in the previous sub-slot)
|
|
|
|
Results in either one or two sub-slots in finished_sub_slots.
|
|
"""
|
|
assert len(self.finished_sub_slots) >= 1
|
|
|
|
if ip_sub_slot is None:
|
|
# We are still in the first sub-slot, no new sub slots ey
|
|
self.initialize_genesis_sub_slot()
|
|
else:
|
|
# This is not the first sub-slot in the chain
|
|
sp_sub_slot_sps: list[SignagePoint | None] = [None] * self.constants.NUM_SPS_SUB_SLOT
|
|
ip_sub_slot_sps: list[SignagePoint | None] = [None] * self.constants.NUM_SPS_SUB_SLOT
|
|
|
|
if fork_block is not None and fork_block.sub_slot_iters != peak.sub_slot_iters:
|
|
# If there was a reorg and a difficulty adjustment, just clear all the slots
|
|
self.clear_slots()
|
|
else:
|
|
interval_iters = calculate_sp_interval_iters(self.constants, peak.sub_slot_iters)
|
|
# If it's not a reorg, or there is a reorg on the same difficulty, we can keep signage points
|
|
# that we had before, in the cache
|
|
for index, (sub_slot, sps, total_iters) in enumerate(self.finished_sub_slots):
|
|
if sub_slot is None:
|
|
continue
|
|
|
|
if fork_block is None:
|
|
# If this is not a reorg, we still want to remove signage points after the new peak
|
|
fork_block = peak
|
|
replaced_sps: list[SignagePoint | None] = [] # index 0 is the end of sub slot
|
|
for i, sp in enumerate(sps):
|
|
if (total_iters + i * interval_iters) < fork_block.total_iters:
|
|
# Sps before the fork point as still valid
|
|
replaced_sps.append(sp)
|
|
else:
|
|
if sp is not None:
|
|
log.debug(
|
|
f"Reverting {i} {(total_iters + i * interval_iters)} {fork_block.total_iters}"
|
|
)
|
|
# Sps after the fork point should be removed
|
|
replaced_sps.append(None)
|
|
assert len(sps) == len(replaced_sps)
|
|
|
|
if sub_slot == sp_sub_slot:
|
|
sp_sub_slot_sps = replaced_sps
|
|
if sub_slot == ip_sub_slot:
|
|
ip_sub_slot_sps = replaced_sps
|
|
|
|
self.clear_slots()
|
|
|
|
prev_sub_slot_total_iters = peak.sp_sub_slot_total_iters(self.constants)
|
|
if sp_sub_slot is not None or prev_sub_slot_total_iters == 0:
|
|
assert peak.overflow or prev_sub_slot_total_iters
|
|
self.finished_sub_slots.append((sp_sub_slot, sp_sub_slot_sps, prev_sub_slot_total_iters))
|
|
|
|
ip_sub_slot_total_iters = peak.ip_sub_slot_total_iters(self.constants)
|
|
self.finished_sub_slots.append((ip_sub_slot, ip_sub_slot_sps, ip_sub_slot_total_iters))
|
|
|
|
new_eos: EndOfSubSlotBundle | None = None
|
|
new_sps: list[tuple[uint8, SignagePoint]] = []
|
|
new_ips: list[timelord_protocol.NewInfusionPointVDF] = []
|
|
|
|
future_eos: list[EndOfSubSlotBundle] = self.future_eos_cache.get(peak.reward_infusion_new_challenge, []).copy()
|
|
for eos in future_eos:
|
|
if (
|
|
self.new_finished_sub_slot(eos, blocks, peak, next_sub_slot_iters, next_difficulty, peak_full_block)
|
|
is not None
|
|
):
|
|
new_eos = eos
|
|
break
|
|
|
|
future_sps: list[tuple[uint8, SignagePoint]] = self.future_sp_cache.get(
|
|
peak.reward_infusion_new_challenge, []
|
|
).copy()
|
|
for index, sp in future_sps:
|
|
assert sp.cc_vdf is not None
|
|
if self.new_signage_point(index, blocks, peak, peak.sub_slot_iters, sp) == SignagePointAddResult.ADDED:
|
|
new_sps.append((index, sp))
|
|
|
|
for ip in self.future_ip_cache.get(peak.reward_infusion_new_challenge, []):
|
|
new_ips.append(ip)
|
|
|
|
self.future_eos_cache.pop(peak.reward_infusion_new_challenge)
|
|
self.future_sp_cache.pop(peak.reward_infusion_new_challenge)
|
|
self.future_ip_cache.pop(peak.reward_infusion_new_challenge)
|
|
|
|
for eos_op, _, _ in self.finished_sub_slots:
|
|
if eos_op is not None:
|
|
self.recent_eos.put(eos_op.challenge_chain.get_hash(), (eos_op, time.time()))
|
|
|
|
# Only forward the last 4 SPs that we have cached, as others will be too old
|
|
return FullNodeStorePeakResult(new_eos, sorted(new_sps)[-4:], new_ips)
|
|
|
|
def get_finished_sub_slots(
|
|
self,
|
|
block_records: BlockRecordsProtocol,
|
|
prev_b: BlockRecord | None,
|
|
last_challenge_to_add: bytes32,
|
|
) -> list[EndOfSubSlotBundle] | None:
|
|
"""
|
|
Retrieves the EndOfSubSlotBundles that are in the store either:
|
|
1. From the starting challenge if prev_b is None
|
|
2. That are not included in the blockchain with peak of prev_b if prev_b is not None
|
|
|
|
Stops at last_challenge
|
|
"""
|
|
|
|
if prev_b is None:
|
|
# The first sub slot must be None
|
|
assert self.finished_sub_slots[0][0] is None
|
|
challenge_in_chain: bytes32 = self.constants.GENESIS_CHALLENGE
|
|
else:
|
|
curr: BlockRecord = prev_b
|
|
while not curr.first_in_sub_slot:
|
|
curr = block_records.block_record(curr.prev_hash)
|
|
assert curr is not None
|
|
assert curr.finished_challenge_slot_hashes is not None
|
|
challenge_in_chain = curr.finished_challenge_slot_hashes[-1]
|
|
|
|
if last_challenge_to_add == challenge_in_chain:
|
|
# No additional slots to add
|
|
return []
|
|
|
|
collected_sub_slots: list[EndOfSubSlotBundle] = []
|
|
found_last_challenge = False
|
|
found_connecting_challenge = False
|
|
for sub_slot, sps, total_iters in self.finished_sub_slots[1:]:
|
|
assert sub_slot is not None
|
|
if sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf.challenge == challenge_in_chain:
|
|
found_connecting_challenge = True
|
|
if found_connecting_challenge:
|
|
collected_sub_slots.append(sub_slot)
|
|
if found_connecting_challenge and sub_slot.challenge_chain.get_hash() == last_challenge_to_add:
|
|
found_last_challenge = True
|
|
break
|
|
if not found_last_challenge:
|
|
log.warning(f"Did not find hash {last_challenge_to_add} connected to {challenge_in_chain}")
|
|
return None
|
|
return collected_sub_slots
|