mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-11 10:56:14 +03:00
Plot filter hard fork (#15336)
* update passed_plot_filter() to take the filter size rather than consensus constants. This allows the filter size to change by block height. make verify_and_get_quality_string() take either the height or filter size * Add some filter_prefix_bits tests on test blocks. * Add some filter_prefix_bits tests on simulated farmer and harvester. * Cache filter prefix bits by challenge chain signage point hash and use that for the the lookups. This allows us to perform plot filter validation. * Add more cases to verify_and_get_quality_string() unit tests. * Add some tests for Farmer's respond_signatures. * Apply Kevin's suggestion to simplify the check for passing plot filter. * Apply Kevin's suggestions to simplify some test checks and fix a couple typos. * Apply Kevin's suggestion to send peak height instead of filter prefix bits as part of NewSignagePoint. * Remove no longer needed filter prefix bit related logic and make height non optional in verify_and_get_quality_string(). --------- Co-authored-by: Amine Khaldi <amine.khaldi@reactos.org>
This commit is contained in:
parent
40a347f1a7
commit
cecda28e84
@ -486,7 +486,7 @@ def validate_unfinished_header_block(
|
||||
cc_sp_hash = header_block.reward_chain_block.challenge_chain_sp_vdf.output.get_hash()
|
||||
|
||||
q_str: Optional[bytes32] = verify_and_get_quality_string(
|
||||
header_block.reward_chain_block.proof_of_space, constants, challenge, cc_sp_hash
|
||||
header_block.reward_chain_block.proof_of_space, constants, challenge, cc_sp_hash, height=height
|
||||
)
|
||||
if q_str is None:
|
||||
return None, ValidationError(Err.INVALID_POSPACE)
|
||||
|
@ -236,7 +236,7 @@ async def pre_validate_blocks_multiprocessing(
|
||||
else:
|
||||
cc_sp_hash = block.reward_chain_block.challenge_chain_sp_vdf.output.get_hash()
|
||||
q_str: Optional[bytes32] = verify_and_get_quality_string(
|
||||
block.reward_chain_block.proof_of_space, constants, challenge, cc_sp_hash
|
||||
block.reward_chain_block.proof_of_space, constants, challenge, cc_sp_hash, height=block.height
|
||||
)
|
||||
if q_str is None:
|
||||
for i, block_i in enumerate(blocks):
|
||||
|
@ -33,6 +33,7 @@ from chia.server.ws_connection import WSChiaConnection
|
||||
from chia.ssl.create_ssl import get_mozilla_ca_crt
|
||||
from chia.types.blockchain_format.pool_target import PoolTarget
|
||||
from chia.types.blockchain_format.proof_of_space import (
|
||||
calculate_prefix_bits,
|
||||
generate_plot_public_key,
|
||||
generate_taproot_sk,
|
||||
get_plot_id,
|
||||
@ -40,7 +41,7 @@ from chia.types.blockchain_format.proof_of_space import (
|
||||
)
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.api_decorators import api_request
|
||||
from chia.util.ints import uint32, uint64
|
||||
from chia.util.ints import uint8, uint32, uint64
|
||||
|
||||
|
||||
def strip_old_entries(pairs: List[Tuple[float, Any]], before: float) -> List[Tuple[float, Any]]:
|
||||
@ -69,8 +70,9 @@ class FarmerAPI:
|
||||
self, new_proof_of_space: harvester_protocol.NewProofOfSpace, peer: WSChiaConnection
|
||||
) -> None:
|
||||
"""
|
||||
This is a response from the harvester, for a NewChallenge. Here we check if the proof
|
||||
of space is sufficiently good, and if so, we ask for the whole proof.
|
||||
This is a response from the harvester, for a NewSignagePointHarvester.
|
||||
Here we check if the proof of space is sufficiently good, and if so, we
|
||||
ask for the whole proof.
|
||||
"""
|
||||
if new_proof_of_space.sp_hash not in self.farmer.number_of_responses:
|
||||
self.farmer.number_of_responses[new_proof_of_space.sp_hash] = 0
|
||||
@ -100,6 +102,7 @@ class FarmerAPI:
|
||||
self.farmer.constants,
|
||||
new_proof_of_space.challenge_hash,
|
||||
new_proof_of_space.sp_hash,
|
||||
height=sp.peak_height,
|
||||
)
|
||||
if computed_quality_string is None:
|
||||
plotid: bytes32 = get_plot_id(new_proof_of_space.proof)
|
||||
@ -307,6 +310,7 @@ class FarmerAPI:
|
||||
return None
|
||||
is_sp_signatures: bool = False
|
||||
sps = self.farmer.sps[response.sp_hash]
|
||||
peak_height = sps[0].peak_height
|
||||
signage_point_index = sps[0].signage_point_index
|
||||
found_sp_hash_debug = False
|
||||
for sp_candidate in sps:
|
||||
@ -325,7 +329,7 @@ class FarmerAPI:
|
||||
include_taproot: bool = pospace.pool_contract_puzzle_hash is not None
|
||||
|
||||
computed_quality_string = verify_and_get_quality_string(
|
||||
pospace, self.farmer.constants, response.challenge_hash, response.sp_hash
|
||||
pospace, self.farmer.constants, response.challenge_hash, response.sp_hash, height=peak_height
|
||||
)
|
||||
if computed_quality_string is None:
|
||||
self.farmer.log.warning(f"Have invalid PoSpace {pospace}")
|
||||
@ -482,6 +486,7 @@ class FarmerAPI:
|
||||
new_signage_point.signage_point_index,
|
||||
new_signage_point.challenge_chain_sp,
|
||||
pool_difficulties,
|
||||
uint8(calculate_prefix_bits(self.farmer.constants, new_signage_point.peak_height)),
|
||||
)
|
||||
|
||||
msg = make_msg(ProtocolMessageTypes.new_signage_point_harvester, message)
|
||||
|
@ -1358,6 +1358,7 @@ class FullNode:
|
||||
difficulty,
|
||||
sub_slot_iters,
|
||||
request.index_from_challenge,
|
||||
uint32(0) if peak is None else peak.height,
|
||||
)
|
||||
msg = make_msg(ProtocolMessageTypes.new_signage_point, broadcast_farmer)
|
||||
await self.server.send_to_all([msg], NodeType.FARMER)
|
||||
@ -2148,6 +2149,7 @@ class FullNode:
|
||||
next_difficulty,
|
||||
next_sub_slot_iters,
|
||||
uint8(0),
|
||||
uint32(0) if peak is None else peak.height,
|
||||
)
|
||||
msg = make_msg(ProtocolMessageTypes.new_signage_point, broadcast_farmer)
|
||||
await self.server.send_to_all([msg], NodeType.FARMER)
|
||||
|
@ -726,12 +726,6 @@ class FullNodeAPI:
|
||||
# 2. In the same sub-slot as the peak
|
||||
# 3. In a future sub-slot that we already know of
|
||||
|
||||
# Checks that the proof of space is valid
|
||||
quality_string: Optional[bytes32] = verify_and_get_quality_string(
|
||||
request.proof_of_space, self.full_node.constants, cc_challenge_hash, request.challenge_chain_sp
|
||||
)
|
||||
assert quality_string is not None and len(quality_string) == 32
|
||||
|
||||
# Grab best transactions from Mempool for given tip target
|
||||
aggregate_signature: G2Element = G2Element()
|
||||
block_generator: Optional[BlockGenerator] = None
|
||||
@ -739,6 +733,22 @@ class FullNodeAPI:
|
||||
removals: Optional[List[Coin]] = []
|
||||
async with self.full_node.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high):
|
||||
peak: Optional[BlockRecord] = self.full_node.blockchain.get_peak()
|
||||
|
||||
# Checks that the proof of space is valid
|
||||
height: uint32
|
||||
if peak is None:
|
||||
height = uint32(0)
|
||||
else:
|
||||
height = peak.height
|
||||
quality_string: Optional[bytes32] = verify_and_get_quality_string(
|
||||
request.proof_of_space,
|
||||
self.full_node.constants,
|
||||
cc_challenge_hash,
|
||||
request.challenge_chain_sp,
|
||||
height=height,
|
||||
)
|
||||
assert quality_string is not None and len(quality_string) == 32
|
||||
|
||||
if peak is not None:
|
||||
# Finds the last transaction block before this one
|
||||
curr_l_tb: BlockRecord = peak
|
||||
@ -912,7 +922,7 @@ class FullNodeAPI:
|
||||
)
|
||||
self.log.info("Made the unfinished block")
|
||||
if prev_b is not None:
|
||||
height: uint32 = uint32(prev_b.height + 1)
|
||||
height = uint32(prev_b.height + 1)
|
||||
else:
|
||||
height = uint32(0)
|
||||
self.full_node.full_node_store.add_candidate_block(quality_string, height, unfinished_block)
|
||||
|
@ -580,7 +580,7 @@ class WeightProofHandler:
|
||||
log.error("failed weight proof sub epoch sample validation")
|
||||
return False, uint32(0)
|
||||
|
||||
if _validate_sub_epoch_segments(self.constants, rng, wp_segment_bytes, summary_bytes) is None:
|
||||
if _validate_sub_epoch_segments(self.constants, rng, wp_segment_bytes, summary_bytes, peak_height) is None:
|
||||
return False, uint32(0)
|
||||
log.info("validate weight proof recent blocks")
|
||||
success, _ = validate_recent_blocks(self.constants, wp_recent_chain_bytes, summary_bytes)
|
||||
@ -933,6 +933,7 @@ def _validate_sub_epoch_segments(
|
||||
rng: random.Random,
|
||||
weight_proof_bytes: bytes,
|
||||
summaries_bytes: List[bytes],
|
||||
height: uint32,
|
||||
validate_from: int = 0,
|
||||
) -> Optional[List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]]:
|
||||
summaries = summaries_from_bytes(summaries_bytes)
|
||||
@ -965,7 +966,15 @@ def _validate_sub_epoch_segments(
|
||||
|
||||
for idx, segment in enumerate(segments):
|
||||
valid_segment, ip_iters, slot_iters, slots, vdf_list = _validate_segment(
|
||||
constants, segment, curr_ssi, prev_ssi, curr_difficulty, prev_ses, idx == 0, sampled_seg_index == idx
|
||||
constants,
|
||||
segment,
|
||||
curr_ssi,
|
||||
prev_ssi,
|
||||
curr_difficulty,
|
||||
prev_ses,
|
||||
idx == 0,
|
||||
sampled_seg_index == idx,
|
||||
height,
|
||||
)
|
||||
vdfs_to_validate.extend(vdf_list)
|
||||
if not valid_segment:
|
||||
@ -988,6 +997,7 @@ def _validate_segment(
|
||||
ses: Optional[SubEpochSummary],
|
||||
first_segment_in_se: bool,
|
||||
sampled: bool,
|
||||
height: uint32,
|
||||
) -> Tuple[bool, int, int, int, List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]]:
|
||||
ip_iters, slot_iters, slots = 0, 0, 0
|
||||
after_challenge = False
|
||||
@ -995,7 +1005,9 @@ def _validate_segment(
|
||||
for idx, sub_slot_data in enumerate(segment.sub_slots):
|
||||
if sampled and sub_slot_data.is_challenge():
|
||||
after_challenge = True
|
||||
required_iters = __validate_pospace(constants, segment, idx, curr_difficulty, ses, first_segment_in_se)
|
||||
required_iters = __validate_pospace(
|
||||
constants, segment, idx, curr_difficulty, ses, first_segment_in_se, height
|
||||
)
|
||||
if required_iters is None:
|
||||
return False, uint64(0), uint64(0), uint64(0), []
|
||||
assert sub_slot_data.signage_point_index is not None
|
||||
@ -1305,6 +1317,7 @@ def _validate_pospace_recent_chain(
|
||||
constants,
|
||||
challenge if not overflow else prev_challenge,
|
||||
cc_sp_hash,
|
||||
height=block.height,
|
||||
)
|
||||
if q_str is None:
|
||||
log.error(f"could not verify proof of space block {block.height} {overflow}")
|
||||
@ -1326,6 +1339,7 @@ def __validate_pospace(
|
||||
curr_diff: uint64,
|
||||
ses: Optional[SubEpochSummary],
|
||||
first_in_sub_epoch: bool,
|
||||
height: uint32,
|
||||
) -> Optional[uint64]:
|
||||
if first_in_sub_epoch and segment.sub_epoch_n == 0 and idx == 0:
|
||||
cc_sub_slot_hash = constants.GENESIS_CHALLENGE
|
||||
@ -1353,6 +1367,7 @@ def __validate_pospace(
|
||||
constants,
|
||||
challenge,
|
||||
cc_sp_hash,
|
||||
height=height,
|
||||
)
|
||||
if q_str is None:
|
||||
log.error("could not verify proof of space")
|
||||
@ -1664,7 +1679,9 @@ async def validate_weight_proof_inner(
|
||||
)
|
||||
|
||||
if not skip_segment_validation:
|
||||
vdfs_to_validate = _validate_sub_epoch_segments(constants, rng, wp_segment_bytes, summary_bytes, validate_from)
|
||||
vdfs_to_validate = _validate_sub_epoch_segments(
|
||||
constants, rng, wp_segment_bytes, summary_bytes, peak_height, validate_from
|
||||
)
|
||||
await asyncio.sleep(0) # break up otherwise multi-second sync code
|
||||
|
||||
if vdfs_to_validate is None:
|
||||
|
@ -195,7 +195,7 @@ class HarvesterAPI:
|
||||
# This is being executed at the beginning of the slot
|
||||
total += 1
|
||||
if passes_plot_filter(
|
||||
self.harvester.constants,
|
||||
new_challenge.filter_prefix_bits,
|
||||
try_plot_info.prover.get_id(),
|
||||
new_challenge.challenge_hash,
|
||||
new_challenge.sp_hash,
|
||||
|
@ -26,6 +26,7 @@ class NewSignagePoint(Streamable):
|
||||
difficulty: uint64
|
||||
sub_slot_iters: uint64
|
||||
signage_point_index: uint8
|
||||
peak_height: uint32
|
||||
|
||||
|
||||
@streamable
|
||||
|
@ -40,6 +40,7 @@ class NewSignagePointHarvester(Streamable):
|
||||
signage_point_index: uint8
|
||||
sp_hash: bytes32
|
||||
pool_difficulties: List[PoolDifficulty]
|
||||
filter_prefix_bits: uint8
|
||||
|
||||
|
||||
@streamable
|
||||
|
@ -76,6 +76,7 @@ from chia.types.blockchain_format.program import INFINITE_COST, Program
|
||||
from chia.types.blockchain_format.proof_of_space import (
|
||||
ProofOfSpace,
|
||||
calculate_pos_challenge,
|
||||
calculate_prefix_bits,
|
||||
generate_plot_public_key,
|
||||
generate_taproot_sk,
|
||||
passes_plot_filter,
|
||||
@ -682,6 +683,7 @@ class BlockTools:
|
||||
seed,
|
||||
difficulty,
|
||||
sub_slot_iters,
|
||||
curr.height,
|
||||
force_plot_id=force_plot_id,
|
||||
)
|
||||
|
||||
@ -979,6 +981,7 @@ class BlockTools:
|
||||
seed,
|
||||
difficulty,
|
||||
sub_slot_iters,
|
||||
curr.height,
|
||||
force_plot_id=force_plot_id,
|
||||
)
|
||||
for required_iters, proof_of_space in sorted(qualified_proofs, key=lambda t: t[0]):
|
||||
@ -1130,6 +1133,7 @@ class BlockTools:
|
||||
assert signage_point.cc_vdf is not None
|
||||
cc_sp_output_hash = signage_point.cc_vdf.output.get_hash()
|
||||
# If did not reach the target slots to skip, don't make any proofs for this sub-slot
|
||||
# we're creating the genesis block, its height is always 0
|
||||
qualified_proofs: List[Tuple[uint64, ProofOfSpace]] = self.get_pospaces_for_challenge(
|
||||
constants,
|
||||
cc_challenge,
|
||||
@ -1137,6 +1141,7 @@ class BlockTools:
|
||||
seed,
|
||||
constants.DIFFICULTY_STARTING,
|
||||
constants.SUB_SLOT_ITERS_STARTING,
|
||||
uint32(0),
|
||||
)
|
||||
|
||||
# Try each of the proofs of space
|
||||
@ -1283,6 +1288,7 @@ class BlockTools:
|
||||
seed: bytes,
|
||||
difficulty: uint64,
|
||||
sub_slot_iters: uint64,
|
||||
height: uint32,
|
||||
force_plot_id: Optional[bytes32] = None,
|
||||
) -> List[Tuple[uint64, ProofOfSpace]]:
|
||||
found_proofs: List[Tuple[uint64, ProofOfSpace]] = []
|
||||
@ -1292,7 +1298,8 @@ class BlockTools:
|
||||
plot_id: bytes32 = plot_info.prover.get_id()
|
||||
if force_plot_id is not None and plot_id != force_plot_id:
|
||||
continue
|
||||
if passes_plot_filter(constants, plot_id, challenge_hash, signage_point):
|
||||
prefix_bits = calculate_prefix_bits(constants, height)
|
||||
if passes_plot_filter(prefix_bits, plot_id, challenge_hash, signage_point):
|
||||
new_challenge: bytes32 = calculate_pos_challenge(plot_id, challenge_hash, signage_point)
|
||||
qualities = plot_info.prover.get_qualities_for_challenge(new_challenge)
|
||||
|
||||
@ -1549,7 +1556,7 @@ def load_block_list(
|
||||
challenge = full_block.reward_chain_block.challenge_chain_sp_vdf.challenge
|
||||
sp_hash = full_block.reward_chain_block.challenge_chain_sp_vdf.output.get_hash()
|
||||
quality_str = verify_and_get_quality_string(
|
||||
full_block.reward_chain_block.proof_of_space, constants, challenge, sp_hash
|
||||
full_block.reward_chain_block.proof_of_space, constants, challenge, sp_hash, height=full_block.height
|
||||
)
|
||||
assert quality_str is not None
|
||||
required_iters: uint64 = calculate_iterations_quality(
|
||||
|
@ -7,7 +7,7 @@ from chia.consensus.pot_iterations import calculate_ip_iters, calculate_iteratio
|
||||
from chia.types.blockchain_format.proof_of_space import verify_and_get_quality_string
|
||||
from chia.types.blockchain_format.reward_chain_block import RewardChainBlock, RewardChainBlockUnfinished
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.ints import uint64
|
||||
from chia.util.ints import uint32, uint64
|
||||
|
||||
|
||||
def iters_from_block(
|
||||
@ -15,6 +15,7 @@ def iters_from_block(
|
||||
reward_chain_block: Union[RewardChainBlock, RewardChainBlockUnfinished],
|
||||
sub_slot_iters: uint64,
|
||||
difficulty: uint64,
|
||||
height: uint32,
|
||||
) -> Tuple[uint64, uint64]:
|
||||
if reward_chain_block.challenge_chain_sp_vdf is None:
|
||||
assert reward_chain_block.signage_point_index == 0
|
||||
@ -27,6 +28,7 @@ def iters_from_block(
|
||||
constants,
|
||||
reward_chain_block.pos_ss_cc_challenge_hash,
|
||||
cc_sp,
|
||||
height=height,
|
||||
)
|
||||
assert quality_string is not None
|
||||
|
||||
|
@ -227,6 +227,12 @@ class Timelord:
|
||||
except Exception as e:
|
||||
log.error(f"Exception in stop chain: {type(e)} {e}")
|
||||
|
||||
def get_height(self) -> uint32:
|
||||
if self.last_state.state_type == StateType.FIRST_SUB_SLOT:
|
||||
return uint32(0)
|
||||
else:
|
||||
return uint32(self.last_state.get_height() + 1)
|
||||
|
||||
def _can_infuse_unfinished_block(self, block: timelord_protocol.NewUnfinishedBlockTimelord) -> Optional[uint64]:
|
||||
assert self.last_state is not None
|
||||
sub_slot_iters = self.last_state.get_sub_slot_iters()
|
||||
@ -239,6 +245,7 @@ class Timelord:
|
||||
rc_block,
|
||||
sub_slot_iters,
|
||||
difficulty,
|
||||
self.get_height(),
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"Received invalid unfinished block: {e}.")
|
||||
@ -535,6 +542,7 @@ class Timelord:
|
||||
unfinished_block.reward_chain_block,
|
||||
self.last_state.get_sub_slot_iters(),
|
||||
self.last_state.get_difficulty(),
|
||||
self.get_height(),
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"Error {e}")
|
||||
|
@ -69,6 +69,7 @@ class TimelordAPI:
|
||||
new_unfinished_block.reward_chain_block,
|
||||
self.timelord.last_state.get_sub_slot_iters(),
|
||||
self.timelord.last_state.get_difficulty(),
|
||||
self.timelord.get_height(),
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
@ -59,6 +59,7 @@ class LastState:
|
||||
state.reward_chain_block,
|
||||
state.sub_slot_iters,
|
||||
state.difficulty,
|
||||
state.reward_chain_block.height,
|
||||
)
|
||||
self.deficit = state.deficit
|
||||
self.sub_epoch_summary = state.sub_epoch_summary
|
||||
|
@ -11,7 +11,7 @@ from chiapos import Verifier
|
||||
from chia.consensus.constants import ConsensusConstants
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.hash import std_hash
|
||||
from chia.util.ints import uint8
|
||||
from chia.util.ints import uint8, uint32
|
||||
from chia.util.streamable import Streamable, streamable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -41,6 +41,8 @@ def verify_and_get_quality_string(
|
||||
constants: ConsensusConstants,
|
||||
original_challenge_hash: bytes32,
|
||||
signage_point: bytes32,
|
||||
*,
|
||||
height: uint32,
|
||||
) -> Optional[bytes32]:
|
||||
# Exactly one of (pool_public_key, pool_contract_puzzle_hash) must not be None
|
||||
if (pos.pool_public_key is None) and (pos.pool_contract_puzzle_hash is None):
|
||||
@ -61,8 +63,8 @@ def verify_and_get_quality_string(
|
||||
if new_challenge != pos.challenge:
|
||||
log.error("Calculated pos challenge doesn't match the provided one")
|
||||
return None
|
||||
|
||||
if not passes_plot_filter(constants, plot_id, original_challenge_hash, signage_point):
|
||||
prefix_bits = calculate_prefix_bits(constants, height)
|
||||
if not passes_plot_filter(prefix_bits, plot_id, original_challenge_hash, signage_point):
|
||||
log.error("Did not pass the plot filter")
|
||||
return None
|
||||
|
||||
@ -77,14 +79,34 @@ def get_quality_string(pos: ProofOfSpace, plot_id: bytes32) -> Optional[bytes32]
|
||||
|
||||
|
||||
def passes_plot_filter(
|
||||
constants: ConsensusConstants,
|
||||
prefix_bits: int,
|
||||
plot_id: bytes32,
|
||||
challenge_hash: bytes32,
|
||||
signage_point: bytes32,
|
||||
) -> bool:
|
||||
# this is possible when using non-mainnet constants with a low
|
||||
# NUMBER_ZERO_BITS_PLOT_FILTER constant and activating sufficient plot
|
||||
# filter reductions
|
||||
if prefix_bits == 0:
|
||||
return True
|
||||
|
||||
plot_filter = BitArray(calculate_plot_filter_input(plot_id, challenge_hash, signage_point))
|
||||
# TODO: compensating for https://github.com/scott-griffiths/bitstring/issues/248
|
||||
return cast(bool, plot_filter[: constants.NUMBER_ZERO_BITS_PLOT_FILTER].uint == 0)
|
||||
return cast(bool, plot_filter[:prefix_bits].uint == 0)
|
||||
|
||||
|
||||
def calculate_prefix_bits(constants: ConsensusConstants, height: uint32) -> int:
|
||||
prefix_bits = constants.NUMBER_ZERO_BITS_PLOT_FILTER
|
||||
if height >= constants.PLOT_FILTER_32_HEIGHT:
|
||||
prefix_bits -= 4
|
||||
elif height >= constants.PLOT_FILTER_64_HEIGHT:
|
||||
prefix_bits -= 3
|
||||
elif height >= constants.PLOT_FILTER_128_HEIGHT:
|
||||
prefix_bits -= 2
|
||||
elif height >= constants.HARD_FORK_HEIGHT:
|
||||
prefix_bits -= 1
|
||||
|
||||
return max(0, prefix_bits)
|
||||
|
||||
|
||||
def calculate_plot_filter_input(plot_id: bytes32, challenge_hash: bytes32, signage_point: bytes32) -> bytes32:
|
||||
|
@ -10,7 +10,7 @@ from blspy import G1Element
|
||||
from chia.consensus.default_constants import DEFAULT_CONSTANTS
|
||||
from chia.types.blockchain_format.proof_of_space import ProofOfSpace, passes_plot_filter, verify_and_get_quality_string
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32, bytes48
|
||||
from chia.util.ints import uint8
|
||||
from chia.util.ints import uint8, uint32
|
||||
from tests.util.misc import Marks, datacases
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ class ProofOfSpaceCase:
|
||||
plot_public_key: G1Element
|
||||
pool_public_key: Optional[G1Element] = None
|
||||
pool_contract_puzzle_hash: Optional[bytes32] = None
|
||||
height: uint32 = uint32(0)
|
||||
expected_error: Optional[str] = None
|
||||
marks: Marks = ()
|
||||
|
||||
@ -68,7 +69,7 @@ class ProofOfSpaceCase:
|
||||
expected_error="Calculated pos challenge doesn't match the provided one",
|
||||
),
|
||||
ProofOfSpaceCase(
|
||||
id="Not passing the plot filter",
|
||||
id="Not passing the plot filter with size 9",
|
||||
pos_challenge=bytes32.from_hexstr("08b23cc2844dfb92d2eedaa705a1ce665d571ee753bd81cbb67b92caa6d34722"),
|
||||
plot_size=uint8(42),
|
||||
pool_public_key=G1Element.from_bytes_unchecked(
|
||||
@ -81,8 +82,25 @@ class ProofOfSpaceCase:
|
||||
"b17d368f5400230b2b01464807825bf4163c5c159bd7d4465f935912e538ac9fb996dd9a9c479bd8aa6256bdca1fed96"
|
||||
)
|
||||
),
|
||||
height=uint32(5495999),
|
||||
expected_error="Did not pass the plot filter",
|
||||
),
|
||||
ProofOfSpaceCase(
|
||||
id="Passing the plot filter with size 8",
|
||||
pos_challenge=bytes32.from_hexstr("08b23cc2844dfb92d2eedaa705a1ce665d571ee753bd81cbb67b92caa6d34722"),
|
||||
plot_size=uint8(42),
|
||||
pool_public_key=G1Element.from_bytes_unchecked(
|
||||
bytes48.from_hexstr(
|
||||
"b6449c2c68df97c19e884427e42ee7350982d4020571ead08732615ff39bd216bfd630b6460784982bec98b49fea79d0"
|
||||
)
|
||||
),
|
||||
plot_public_key=G1Element.from_bytes_unchecked(
|
||||
bytes48.from_hexstr(
|
||||
"b17d368f5400230b2b01464807825bf4163c5c159bd7d4465f935912e538ac9fb996dd9a9c479bd8aa6256bdca1fed96"
|
||||
)
|
||||
),
|
||||
height=uint32(5496000),
|
||||
),
|
||||
)
|
||||
def test_verify_and_get_quality_string(caplog: pytest.LogCaptureFixture, case: ProofOfSpaceCase) -> None:
|
||||
pos = ProofOfSpace(
|
||||
@ -100,25 +118,27 @@ def test_verify_and_get_quality_string(caplog: pytest.LogCaptureFixture, case: P
|
||||
"0x73490e166d0b88347c37d921660b216c27316aae9a3450933d3ff3b854e5831a"
|
||||
),
|
||||
signage_point=bytes32.from_hexstr("0x7b3e23dbd438f9aceefa9827e2c5538898189987f49b06eceb7a43067e77b531"),
|
||||
height=case.height,
|
||||
)
|
||||
assert quality_string is None
|
||||
assert len(caplog.text) == 0 if case.expected_error is None else case.expected_error in caplog.text
|
||||
|
||||
|
||||
class TestProofOfSpace:
|
||||
def test_can_create_proof(self) -> None:
|
||||
@pytest.mark.parametrize("prefix_bits", [DEFAULT_CONSTANTS.NUMBER_ZERO_BITS_PLOT_FILTER, 8, 7, 6, 5, 1, 0])
|
||||
def test_can_create_proof(self, prefix_bits: int) -> None:
|
||||
"""
|
||||
Tests that the change of getting a correct proof is exactly 1/target_filter.
|
||||
"""
|
||||
num_trials = 100000
|
||||
success_count = 0
|
||||
target_filter = 2**DEFAULT_CONSTANTS.NUMBER_ZERO_BITS_PLOT_FILTER
|
||||
target_filter = 2**prefix_bits
|
||||
for _ in range(num_trials):
|
||||
challenge_hash = bytes32(token_bytes(32))
|
||||
plot_id = bytes32(token_bytes(32))
|
||||
sp_output = bytes32(token_bytes(32))
|
||||
|
||||
if passes_plot_filter(DEFAULT_CONSTANTS, plot_id, challenge_hash, sp_output):
|
||||
if passes_plot_filter(prefix_bits, plot_id, challenge_hash, sp_output):
|
||||
success_count += 1
|
||||
|
||||
assert abs((success_count * target_filter / num_trials) - 1) < 0.35
|
||||
|
@ -131,7 +131,7 @@ async def test_farmer_signage_point_endpoints(harvester_farmer_environment: Harv
|
||||
return len(await farmer_rpc_client.get_signage_points()) > 0
|
||||
|
||||
sp = farmer_protocol.NewSignagePoint(
|
||||
std_hash(b"1"), std_hash(b"2"), std_hash(b"3"), uint64(1), uint64(1000000), uint8(2)
|
||||
std_hash(b"1"), std_hash(b"2"), std_hash(b"3"), uint64(1), uint64(1000000), uint8(2), uint32(1)
|
||||
)
|
||||
await farmer_api.new_signage_point(sp)
|
||||
|
||||
@ -241,7 +241,7 @@ async def test_farmer_get_pool_state(
|
||||
pool_dict[key].insert(0, before_24h)
|
||||
|
||||
sp = farmer_protocol.NewSignagePoint(
|
||||
std_hash(b"1"), std_hash(b"2"), std_hash(b"3"), uint64(1), uint64(1000000), uint8(2)
|
||||
std_hash(b"1"), std_hash(b"2"), std_hash(b"3"), uint64(1), uint64(1000000), uint8(2), uint32(1)
|
||||
)
|
||||
await farmer_api.new_signage_point(sp)
|
||||
client_pool_state = await farmer_rpc_client.get_pool_state()
|
||||
|
@ -4,16 +4,22 @@ import asyncio
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
from blspy import G1Element
|
||||
|
||||
from chia.farmer.farmer import Farmer
|
||||
from chia.farmer.farmer_api import FarmerAPI
|
||||
from chia.harvester.harvester import Harvester
|
||||
from chia.harvester.harvester_api import HarvesterAPI
|
||||
from chia.protocols import harvester_protocol
|
||||
from chia.protocols.protocol_message_types import ProtocolMessageTypes
|
||||
from chia.server.outbound_message import NodeType, make_msg
|
||||
from chia.server.start_service import Service
|
||||
from chia.simulator.block_tools import BlockTools
|
||||
from chia.simulator.time_out_assert import time_out_assert
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.peer_info import UnresolvedPeerInfo
|
||||
from chia.util.keychain import generate_mnemonic
|
||||
from tests.conftest import HarvesterFarmerEnvironment
|
||||
|
||||
|
||||
def farmer_is_started(farmer: Farmer) -> bool:
|
||||
@ -111,3 +117,36 @@ async def test_harvester_handshake(
|
||||
await time_out_assert(5, farmer_is_started, True, farmer)
|
||||
await time_out_assert(5, handshake_task_active, False)
|
||||
await time_out_assert(5, handshake_done, True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_farmer_respond_signatures(
|
||||
caplog: pytest.LogCaptureFixture, harvester_farmer_environment: HarvesterFarmerEnvironment
|
||||
) -> None:
|
||||
# This test ensures that the farmer correctly rejects invalid RespondSignatures
|
||||
# messages from the harvester.
|
||||
# In this test we're leveraging the fact that the farmer can handle RespondSignatures
|
||||
# messages even though it didn't request them, to cover when the farmer doesn't know
|
||||
# about an sp_hash, so it fails at the sp record check.
|
||||
|
||||
def log_is_ready() -> bool:
|
||||
return len(caplog.text) > 0
|
||||
|
||||
_, _, harvester_service, _, _ = harvester_farmer_environment
|
||||
# We won't have an sp record for this one
|
||||
challenge_hash = bytes32(b"1" * 32)
|
||||
sp_hash = bytes32(b"2" * 32)
|
||||
response: harvester_protocol.RespondSignatures = harvester_protocol.RespondSignatures(
|
||||
plot_identifier="test",
|
||||
challenge_hash=challenge_hash,
|
||||
sp_hash=sp_hash,
|
||||
local_pk=G1Element(),
|
||||
farmer_pk=G1Element(),
|
||||
message_signatures=[],
|
||||
)
|
||||
msg = make_msg(ProtocolMessageTypes.respond_signatures, response)
|
||||
await harvester_service._node.server.send_to_all([msg], NodeType.FARMER)
|
||||
await time_out_assert(5, log_is_ready)
|
||||
# We fail the sps record check
|
||||
expected_error = f"Do not have challenge hash {challenge_hash}"
|
||||
assert expected_error in caplog.text
|
||||
|
127
tests/farmer_harvester/test_filter_prefix_bits.py
Normal file
127
tests/farmer_harvester/test_filter_prefix_bits.py
Normal file
@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncIterator, Dict, List, Optional, Tuple
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from chia.farmer.farmer_api import FarmerAPI
|
||||
from chia.harvester.harvester import Harvester
|
||||
from chia.harvester.harvester_api import HarvesterAPI
|
||||
from chia.protocols import farmer_protocol
|
||||
from chia.rpc.farmer_rpc_client import FarmerRpcClient
|
||||
from chia.rpc.harvester_rpc_client import HarvesterRpcClient
|
||||
from chia.server.start_service import Service
|
||||
from chia.simulator.block_tools import create_block_tools_async, test_constants
|
||||
from chia.simulator.setup_nodes import setup_farmer_multi_harvester
|
||||
from chia.simulator.time_out_assert import time_out_assert
|
||||
from chia.types.blockchain_format.proof_of_space import get_plot_id, passes_plot_filter
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.full_block import FullBlock
|
||||
from chia.util.ints import uint8, uint32, uint64
|
||||
from chia.util.keychain import Keychain
|
||||
from tests.core.test_farmer_harvester_rpc import wait_for_plot_sync
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=["filter_prefix_bits", "should_pass"], argvalues=[(9, 33), (8, 66), (7, 138), (6, 265), (5, 607)]
|
||||
)
|
||||
def test_filter_prefix_bits_on_blocks(
|
||||
default_10000_blocks: List[FullBlock], filter_prefix_bits: uint8, should_pass: int
|
||||
) -> None:
|
||||
passed = 0
|
||||
for block in default_10000_blocks:
|
||||
plot_id = get_plot_id(block.reward_chain_block.proof_of_space)
|
||||
original_challenge_hash = block.reward_chain_block.pos_ss_cc_challenge_hash
|
||||
if block.reward_chain_block.challenge_chain_sp_vdf is None:
|
||||
assert block.reward_chain_block.signage_point_index == 0
|
||||
signage_point = original_challenge_hash
|
||||
else:
|
||||
signage_point = block.reward_chain_block.challenge_chain_sp_vdf.output.get_hash()
|
||||
if passes_plot_filter(filter_prefix_bits, plot_id, original_challenge_hash, signage_point):
|
||||
passed += 1
|
||||
assert passed == should_pass
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def farmer_harvester_with_filter_size_9(
|
||||
get_temp_keyring: Keychain, tmp_path: Path, self_hostname: str
|
||||
) -> AsyncIterator[Tuple[Service[Harvester, HarvesterAPI], FarmerAPI]]:
|
||||
async def have_connections() -> bool:
|
||||
return len(await farmer_rpc_cl.get_connections()) > 0
|
||||
|
||||
local_b_tools = await create_block_tools_async(
|
||||
constants=test_constants.replace(NUMBER_ZERO_BITS_PLOT_FILTER=9), keychain=get_temp_keyring
|
||||
)
|
||||
new_config = local_b_tools._config
|
||||
local_b_tools.change_config(new_config)
|
||||
async for harvesters, farmer_service, _ in setup_farmer_multi_harvester(
|
||||
local_b_tools, 1, tmp_path, local_b_tools.constants, start_services=True
|
||||
):
|
||||
harvester_service = harvesters[0]
|
||||
assert farmer_service.rpc_server is not None
|
||||
farmer_rpc_cl = await FarmerRpcClient.create(
|
||||
self_hostname, farmer_service.rpc_server.listen_port, farmer_service.root_path, farmer_service.config
|
||||
)
|
||||
assert harvester_service.rpc_server is not None
|
||||
harvester_rpc_cl = await HarvesterRpcClient.create(
|
||||
self_hostname,
|
||||
harvester_service.rpc_server.listen_port,
|
||||
harvester_service.root_path,
|
||||
harvester_service.config,
|
||||
)
|
||||
await time_out_assert(15, have_connections, True)
|
||||
yield harvester_service, farmer_service._api
|
||||
|
||||
farmer_rpc_cl.close()
|
||||
harvester_rpc_cl.close()
|
||||
await farmer_rpc_cl.await_closed()
|
||||
await harvester_rpc_cl.await_closed()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(argnames=["peak_height", "eligible_plots"], argvalues=[(5495999, 0), (5496000, 1)])
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_prefix_bits_with_farmer_harvester(
|
||||
farmer_harvester_with_filter_size_9: Tuple[Service[Harvester, HarvesterAPI], FarmerAPI],
|
||||
peak_height: uint32,
|
||||
eligible_plots: int,
|
||||
) -> None:
|
||||
state_change = None
|
||||
state_change_data = None
|
||||
|
||||
def state_changed_callback(change: str, change_data: Optional[Dict[str, Any]]) -> None:
|
||||
nonlocal state_change, state_change_data
|
||||
state_change = change
|
||||
state_change_data = change_data
|
||||
|
||||
def state_has_changed() -> bool:
|
||||
return state_change is not None and state_change_data is not None
|
||||
|
||||
# We need a custom block tools with constants that set the initial filter
|
||||
# size to 9 in order to test peak heights that cover sizes 9 and 8 respectively
|
||||
harvester_service, farmer_api = farmer_harvester_with_filter_size_9
|
||||
harvester_service._node.state_changed_callback = state_changed_callback
|
||||
harvester_id = harvester_service._server.node_id
|
||||
receiver = farmer_api.farmer.plot_sync_receivers[harvester_id]
|
||||
if receiver.initial_sync():
|
||||
await wait_for_plot_sync(receiver, receiver.last_sync().sync_id)
|
||||
# This allows us to pass the plot filter with prefix bits 8 but not 9
|
||||
challenge_hash = bytes32.from_hexstr("0x73490e166d0b88347c37d921660b216c27316aae9a3450933d3ff3b854e5831a")
|
||||
sp_hash = bytes32.from_hexstr("0x7b3e23dbd438f9aceefa9827e2c5538898189987f49b06eceb7a43067e77b531")
|
||||
sp = farmer_protocol.NewSignagePoint(
|
||||
challenge_hash=challenge_hash,
|
||||
challenge_chain_sp=sp_hash,
|
||||
reward_chain_sp=bytes32(b"1" * 32),
|
||||
difficulty=uint64(1),
|
||||
sub_slot_iters=uint64(1000000),
|
||||
signage_point_index=uint8(2),
|
||||
peak_height=peak_height,
|
||||
)
|
||||
await farmer_api.new_signage_point(sp)
|
||||
await time_out_assert(5, state_has_changed, True)
|
||||
# We're intercepting the harvester's state changes as we're expecting
|
||||
# a farming_info one.
|
||||
assert state_change == "farming_info"
|
||||
assert state_change_data is not None
|
||||
assert state_change_data.get("eligible_plots") == eligible_plots
|
@ -48,6 +48,7 @@ new_signage_point = farmer_protocol.NewSignagePoint(
|
||||
uint64(2329045448547720842),
|
||||
uint64(8265724497259558930),
|
||||
uint8(194),
|
||||
uint32(1),
|
||||
)
|
||||
|
||||
proof_of_space = ProofOfSpace(
|
||||
@ -698,6 +699,7 @@ new_signage_point_harvester = harvester_protocol.NewSignagePointHarvester(
|
||||
uint8(148),
|
||||
bytes32(bytes.fromhex("b78c9fca155e9742df835cbe84bb7e518bee70d78b6be6e39996c0a02e0cfe4c")),
|
||||
[pool_difficulty],
|
||||
uint8(9),
|
||||
)
|
||||
|
||||
new_proof_of_space = harvester_protocol.NewProofOfSpace(
|
||||
|
Binary file not shown.
@ -10,6 +10,7 @@ new_signage_point_json: Dict[str, Any] = {
|
||||
"difficulty": 2329045448547720842,
|
||||
"sub_slot_iters": 8265724497259558930,
|
||||
"signage_point_index": 194,
|
||||
"peak_height": 1,
|
||||
}
|
||||
|
||||
declare_proof_of_space_json: Dict[str, Any] = {
|
||||
@ -2023,6 +2024,7 @@ new_signage_point_harvester_json: Dict[str, Any] = {
|
||||
"pool_contract_puzzle_hash": "0xc9423123ea65e6923e973b95531b4874570dae942cb757a2daec4a6971753886",
|
||||
}
|
||||
],
|
||||
"filter_prefix_bits": 9,
|
||||
}
|
||||
|
||||
new_proof_of_space_json: Dict[str, Any] = {
|
||||
|
@ -13,7 +13,7 @@ from chia.consensus.full_block_to_block_record import block_to_block_record
|
||||
from chia.consensus.pot_iterations import calculate_iterations_quality
|
||||
from chia.full_node.block_store import BlockStore
|
||||
from chia.full_node.weight_proof import WeightProofHandler, _map_sub_epoch_summaries, _validate_summaries_weight
|
||||
from chia.types.blockchain_format.proof_of_space import verify_and_get_quality_string
|
||||
from chia.types.blockchain_format.proof_of_space import calculate_prefix_bits, verify_and_get_quality_string
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary
|
||||
from chia.types.full_block import FullBlock
|
||||
@ -51,6 +51,7 @@ async def load_blocks_dont_validate(
|
||||
constants,
|
||||
block.reward_chain_block.pos_ss_cc_challenge_hash,
|
||||
cc_sp,
|
||||
height=block.height,
|
||||
)
|
||||
assert quality_string is not None
|
||||
|
||||
@ -525,6 +526,40 @@ class TestWeightProof:
|
||||
print(f"size of proof is {get_size(wp)}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"height,expected",
|
||||
[
|
||||
(0, 3),
|
||||
(5496000, 2),
|
||||
(10542000, 1),
|
||||
(15592000, 0),
|
||||
(20643000, 0),
|
||||
],
|
||||
)
|
||||
def test_calculate_prefix_bits_clamp_zero(height: uint32, expected: int):
|
||||
constants = DEFAULT_CONSTANTS.replace(NUMBER_ZERO_BITS_PLOT_FILTER=3)
|
||||
assert calculate_prefix_bits(constants, height) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=["height", "expected"],
|
||||
argvalues=[
|
||||
(0, 9),
|
||||
(5495999, 9),
|
||||
(5496000, 8),
|
||||
(10541999, 8),
|
||||
(10542000, 7),
|
||||
(15591999, 7),
|
||||
(15592000, 6),
|
||||
(20642999, 6),
|
||||
(20643000, 5),
|
||||
],
|
||||
)
|
||||
def test_calculate_prefix_bits_default(height: uint32, expected: int):
|
||||
constants = DEFAULT_CONSTANTS
|
||||
assert calculate_prefix_bits(constants, height) == expected
|
||||
|
||||
|
||||
def get_size(obj, seen=None):
|
||||
"""Recursively finds size of objects"""
|
||||
size = sys.getsizeof(obj)
|
||||
|
Loading…
Reference in New Issue
Block a user