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:
Arvid Norberg 2023-07-12 17:26:23 +02:00 committed by GitHub
parent 40a347f1a7
commit cecda28e84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 336 additions and 34 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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,

View File

@ -26,6 +26,7 @@ class NewSignagePoint(Streamable):
difficulty: uint64
sub_slot_iters: uint64
signage_point_index: uint8
peak_height: uint32
@streamable

View File

@ -40,6 +40,7 @@ class NewSignagePointHarvester(Streamable):
signage_point_index: uint8
sp_hash: bytes32
pool_difficulties: List[PoolDifficulty]
filter_prefix_bits: uint8
@streamable

View File

@ -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(

View File

@ -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

View File

@ -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}")

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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

View 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

View File

@ -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(

View File

@ -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] = {

View File

@ -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)