mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-11-06 12:50:52 -04:00
[CHIA-4475] implement the schedule for maximum plot strength (#21645)
implement the schedule for maximum plot strength
This commit is contained in:
@@ -66,7 +66,7 @@ Public docs describe harvesters as checking plots for a farmer, but source corre
|
||||
- Pool-specific difficulty overrides are keyed by `pool_contract_puzzle_hash`. If no matching `PoolDifficulty` exists, the global signage-point difficulty/sub-slot iters remain in force.
|
||||
- V1 full proof retrieval can fail for decompressor timeouts, line point compression (`GRResult_NoProof received`), plot I/O/prover errors, or fork phase-out checks. These failures are logged and dropped per plot rather than aborting the whole challenge.
|
||||
- V2 partial proof lookup returns only partial proofs that pass the same required-iterations threshold. It must carry plot id, plot index, `meta_group`, strength, plot public key, and pool identifiers because the farmer/solver path needs those fields to build the final `ProofOfSpace`.
|
||||
- V2 filter eligibility uses the same `calculate_prefix_bits()` → `passes_plot_filter()` path as V1, but with `NUMBER_ZERO_BITS_PLOT_FILTER_V2` and height-based adjustments (`PLOT_FILTER_V2_FIRST/SECOND/THIRD_ADJUSTMENT_HEIGHT`). Plots with `strength_v2` below `MIN_PLOT_STRENGTH` or above `MAX_PLOT_STRENGTH` are rejected by `check_plot_param()`; harvester-side and validation-side filter math must stay identical or farmers produce proofs full nodes reject. See `chia/types/blockchain_format/proof_of_space.py` for exact constants.
|
||||
- V2 filter eligibility uses `calculate_plot_filter_bits()` and `passes_plot_filter_v2()` with the pre-signage-point transaction height. The base filter follows `PLOT_FILTER_V2_RELATIVE_HEIGHT`, and the effective filter also depends on plot strength. The allowed strength range is `MIN_PLOT_STRENGTH` through `MAX_PLOT_STRENGTH` minus the current base filter bits. When the pre-signage-point transaction height is unavailable during height-agnostic weight-proof validation, `check_plot_param()` uses the relaxed absolute `MAX_PLOT_STRENGTH` bound. Harvester-side and validation-side rules must stay aligned or farmers may produce proofs full nodes reject. See `chia/types/blockchain_format/proof_of_space.py` for exact constants and calculations.
|
||||
- The lookup warning threshold is operationally important: late proof lookup risks missed rewards. Avoid adding synchronous work, unbounded logging, RPC calls, or long lock holds to the signage-point path.
|
||||
|
||||
## Signature Authority
|
||||
|
||||
@@ -14,12 +14,14 @@ from chia.consensus.default_constants import DEFAULT_CONSTANTS
|
||||
from chia.types.blockchain_format.proof_of_space import (
|
||||
calculate_base_plot_filter_bits,
|
||||
calculate_plot_filter_bits,
|
||||
calculate_pos_challenge,
|
||||
calculate_prefix_bits,
|
||||
check_plot_param,
|
||||
compute_plot_group_id,
|
||||
is_v1_phased_out,
|
||||
is_v2_plot,
|
||||
make_pos,
|
||||
max_plot_strength,
|
||||
num_phase_out_epochs,
|
||||
passes_plot_filter,
|
||||
passes_plot_filter_v2,
|
||||
@@ -224,11 +226,17 @@ def test_is_v2_plot(plot_param: PlotParam, expected: bool) -> None:
|
||||
pool_contract_puzzle_hash=bytes32(b"1" * 32),
|
||||
expected_error="Calculated pos challenge doesn't match the provided one",
|
||||
),
|
||||
ProofOfSpaceCase(
|
||||
id="v2 plot above height-dependent maximum strength",
|
||||
plot_size=PlotParam.make_v2(0, 0, 9),
|
||||
pool_contract_puzzle_hash=bytes32(b"1" * 32),
|
||||
expected_error="Plot strength (9) is too high (max is 8)",
|
||||
),
|
||||
ProofOfSpaceCase(
|
||||
id="v2 plot strength 18",
|
||||
plot_size=PlotParam.make_v2(0, 0, 18),
|
||||
pool_contract_puzzle_hash=bytes32(b"1" * 32),
|
||||
expected_error="Plot strength (18) is too high (max is 17)",
|
||||
expected_error="Plot strength (18) is too high (max is 8)",
|
||||
),
|
||||
ProofOfSpaceCase(
|
||||
id="Not passing the plot filter v2 missing filter_challenge",
|
||||
@@ -425,12 +433,65 @@ def test_verify_and_get_quality_string_v2(caplog: pytest.LogCaptureFixture, case
|
||||
(PlotParam.make_v2(0, 0, 1), False), # strength too low
|
||||
(PlotParam.make_v2(0, 0, 2), True),
|
||||
(PlotParam.make_v2(0, 0, 3), True),
|
||||
(PlotParam.make_v2(0, 0, 17), True),
|
||||
(PlotParam.make_v2(0, 0, 18), False), # strength too high
|
||||
(PlotParam.make_v2(0, 0, 8), True),
|
||||
(PlotParam.make_v2(0, 0, 9), False), # strength too high at the initial base filter
|
||||
(PlotParam.make_v2(0, 0, 17), False),
|
||||
(PlotParam.make_v2(0, 0, 18), False),
|
||||
],
|
||||
)
|
||||
def test_check_plot_param(plot_param: PlotParam, valid: bool) -> None:
|
||||
assert check_plot_param(DEFAULT_CONSTANTS, plot_param) == valid
|
||||
assert check_plot_param(DEFAULT_CONSTANTS, plot_param, DEFAULT_CONSTANTS.HARD_FORK2_HEIGHT) == valid
|
||||
|
||||
|
||||
def test_height_agnostic_v2_strength_limit(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
constants = DEFAULT_CONSTANTS.replace(HARD_FORK2_HEIGHT=uint32(1_000_000))
|
||||
pool_hash = bytes32(b"1" * 32)
|
||||
plot_public_key = _test_pk()
|
||||
original_challenge = bytes32(b"2" * 32)
|
||||
signage_point = bytes32(b"3" * 32)
|
||||
|
||||
monkeypatch.setattr("chia.types.blockchain_format.proof_of_space.validate_proof_v2", lambda *args: bytes32.zeros)
|
||||
|
||||
for strength, expected_valid in [(9, True), (17, True), (18, False)]:
|
||||
param = PlotParam.make_v2(0, 0, strength)
|
||||
assert check_plot_param(constants, param, None) == expected_valid
|
||||
assert not check_plot_param(constants, param, constants.HARD_FORK2_HEIGHT)
|
||||
group_id = compute_plot_group_id(strength, plot_public_key, pool_hash)
|
||||
pos = make_pos(
|
||||
calculate_pos_challenge(group_id, original_challenge, signage_point),
|
||||
None,
|
||||
pool_hash,
|
||||
plot_public_key,
|
||||
param,
|
||||
b"proof",
|
||||
)
|
||||
quality = verify_and_get_quality_string(
|
||||
pos,
|
||||
constants,
|
||||
original_challenge,
|
||||
signage_point,
|
||||
height=constants.HARD_FORK2_HEIGHT,
|
||||
prev_transaction_block_height=uint32(0),
|
||||
height_agnostic=True,
|
||||
)
|
||||
assert (quality is not None) == expected_valid
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative_height,expected_maximum",
|
||||
[
|
||||
(10_100_999, 8),
|
||||
(10_101_000, 9),
|
||||
(50_493_999, 16),
|
||||
(50_494_000, 17),
|
||||
],
|
||||
)
|
||||
def test_max_plot_strength_follows_base_filter(relative_height: int, expected_maximum: int) -> None:
|
||||
constants = DEFAULT_CONSTANTS.replace(HARD_FORK2_HEIGHT=uint32(1_000_000))
|
||||
height = uint32(constants.HARD_FORK2_HEIGHT + relative_height)
|
||||
assert max_plot_strength(height, constants) == expected_maximum
|
||||
assert check_plot_param(constants, PlotParam.make_v2(0, 0, expected_maximum), height)
|
||||
assert not check_plot_param(constants, PlotParam.make_v2(0, 0, expected_maximum + 1), height)
|
||||
|
||||
|
||||
class TestProofOfSpace:
|
||||
|
||||
@@ -11,12 +11,13 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from chia_rs import ConsensusConstants, FullBlock, PartialProof, ProofOfSpace
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import uint8, uint16, uint64
|
||||
from chia_rs.sized_ints import uint8, uint16, uint32, uint64
|
||||
from packaging.version import Version
|
||||
|
||||
from chia._tests.conftest import ConsensusMode, HarvesterFarmerEnvironment
|
||||
from chia._tests.plotting.util import get_test_plots
|
||||
from chia._tests.util.time_out_assert import time_out_assert
|
||||
from chia.consensus.default_constants import DEFAULT_CONSTANTS
|
||||
from chia.harvester.harvester_api import HarvesterAPI
|
||||
from chia.plotting.prover import V1Prover, V2Prover, V2Quality
|
||||
from chia.plotting.util import PlotInfo
|
||||
@@ -112,6 +113,35 @@ def create_test_setup(
|
||||
return env, new_challenge, mock_peer
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative_height,strength,expected",
|
||||
[
|
||||
(0, 8, True),
|
||||
(0, 9, False),
|
||||
(10_101_000, 9, True),
|
||||
(10_101_000, 10, False),
|
||||
],
|
||||
)
|
||||
def test_v2_plot_strength_gate(relative_height: int, strength: int, expected: bool) -> None:
|
||||
constants = DEFAULT_CONSTANTS.replace(HARD_FORK2_HEIGHT=uint32(1_000_000))
|
||||
api = HarvesterAPI(MagicMock())
|
||||
api.harvester.constants = constants
|
||||
plot_info = MagicMock(spec=PlotInfo)
|
||||
inner_prover = MagicMock()
|
||||
inner_prover.get_strength.return_value = uint8(strength)
|
||||
inner_prover.get_meta_group.return_value = uint8(0)
|
||||
inner_prover.plot_group_id.return_value = bytes32.zeros
|
||||
plot_info.prover = V2Prover(inner_prover)
|
||||
challenge = MagicMock(spec=harvester_protocol.NewSignagePointHarvester2)
|
||||
challenge.last_tx_height = uint32(constants.HARD_FORK2_HEIGHT + relative_height)
|
||||
challenge.filter_challenge = bytes32.zeros
|
||||
challenge.signage_point_index = uint8(0)
|
||||
|
||||
with patch("chia.harvester.harvester_api.passes_plot_filter_v2", return_value=True) as plot_filter:
|
||||
assert api._plot_passes_filter(plot_info, challenge) == expected
|
||||
assert plot_filter.called == expected
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mock_successful_proof(plot_info: PlotInfo) -> Generator[None, None, None]:
|
||||
if isinstance(plot_info.prover, V1Prover):
|
||||
|
||||
@@ -32,6 +32,7 @@ from chia.types.blockchain_format.proof_of_space import (
|
||||
generate_plot_public_key,
|
||||
is_v1_phased_out,
|
||||
make_pos,
|
||||
max_plot_strength,
|
||||
passes_plot_filter,
|
||||
passes_plot_filter_v2,
|
||||
v1_cut_off_height,
|
||||
@@ -65,20 +66,24 @@ class HarvesterAPI:
|
||||
) -> bool:
|
||||
if plot_info.prover.get_version() == PlotVersion.V2:
|
||||
assert isinstance(plot_info.prover, V2Prover)
|
||||
strength = plot_info.prover.get_strength()
|
||||
constants = self.harvester.constants
|
||||
if strength < constants.MIN_PLOT_STRENGTH or strength > max_plot_strength(
|
||||
challenge.last_tx_height, constants
|
||||
):
|
||||
return False
|
||||
# V2 plots always use predictable filter — no fallback
|
||||
filter_challenge = getattr(challenge, "filter_challenge", None)
|
||||
if filter_challenge is None:
|
||||
return False
|
||||
group_strength = calculate_plot_filter_bits(
|
||||
challenge.last_tx_height, self.harvester.constants, plot_info.prover.get_strength()
|
||||
)
|
||||
group_strength = calculate_plot_filter_bits(challenge.last_tx_height, constants, strength)
|
||||
return passes_plot_filter_v2(
|
||||
plot_info.prover.get_id(),
|
||||
plot_info.prover.get_meta_group(),
|
||||
group_strength,
|
||||
filter_challenge,
|
||||
challenge.signage_point_index,
|
||||
self.harvester.constants.FILTER_WINDOW_SIZE,
|
||||
constants.FILTER_WINDOW_SIZE,
|
||||
)
|
||||
|
||||
# V1 plots use original prefix-bits filter
|
||||
|
||||
@@ -123,6 +123,7 @@ from chia.types.blockchain_format.proof_of_space import (
|
||||
generate_taproot_sk,
|
||||
is_v1_phased_out,
|
||||
make_pos,
|
||||
max_plot_strength,
|
||||
num_phase_out_epochs,
|
||||
passes_plot_filter,
|
||||
passes_plot_filter_v2,
|
||||
@@ -1885,10 +1886,11 @@ class BlockTools:
|
||||
f"cannot be used for farming: {plot_info.prover.get_filename()}"
|
||||
)
|
||||
continue
|
||||
if plot_info.prover.get_strength() > constants.MAX_PLOT_STRENGTH:
|
||||
max_strength = max_plot_strength(prev_tx_height, constants)
|
||||
if plot_info.prover.get_strength() > max_strength:
|
||||
self.log.warn(
|
||||
f"Plot strength ({plot_info.prover.get_strength()}) exceeds max "
|
||||
f"({constants.MAX_PLOT_STRENGTH}): {plot_info.prover.get_filename()}"
|
||||
f"({max_strength}): {plot_info.prover.get_filename()}"
|
||||
)
|
||||
continue
|
||||
if filter_challenge is None or signage_point_index is None:
|
||||
|
||||
@@ -55,15 +55,24 @@ def make_pos(
|
||||
)
|
||||
|
||||
|
||||
def check_plot_param(constants: ConsensusConstants, ps: PlotParam) -> bool:
|
||||
def check_plot_param(
|
||||
constants: ConsensusConstants,
|
||||
ps: PlotParam,
|
||||
pre_sp_tx_height: uint32 | None,
|
||||
) -> bool:
|
||||
size_v1 = ps.size_v1
|
||||
strength_v2 = ps.strength_v2
|
||||
if strength_v2 is not None:
|
||||
if strength_v2 < constants.MIN_PLOT_STRENGTH:
|
||||
log.error(f"Plot strength ({strength_v2}) is lower than the minimum ({constants.MIN_PLOT_STRENGTH})")
|
||||
return False
|
||||
if strength_v2 > constants.MAX_PLOT_STRENGTH:
|
||||
log.error(f"Plot strength ({strength_v2}) is too high (max is {constants.MAX_PLOT_STRENGTH})")
|
||||
# A missing pre-SP transaction height makes this check height agnostic. Use the
|
||||
# absolute maximum, a relaxed bound that accepts plots valid at later heights.
|
||||
max_strength = (
|
||||
constants.MAX_PLOT_STRENGTH if pre_sp_tx_height is None else max_plot_strength(pre_sp_tx_height, constants)
|
||||
)
|
||||
if strength_v2 > max_strength:
|
||||
log.error(f"Plot strength ({strength_v2}) is too high (max is {max_strength})")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -174,7 +183,7 @@ def verify_and_get_quality_string(
|
||||
log.error("Expected pool public key or pool contract puzzle hash but got both")
|
||||
return None
|
||||
|
||||
if not check_plot_param(constants, plot_param):
|
||||
if not check_plot_param(constants, plot_param, None if height_agnostic else prev_transaction_block_height):
|
||||
return None
|
||||
|
||||
plot_id: bytes32 = pos.compute_plot_id()
|
||||
@@ -279,9 +288,9 @@ def calculate_plot_filter_input(plot_id: bytes32, challenge_hash: bytes32, signa
|
||||
return std_hash(plot_id + challenge_hash + signage_point)
|
||||
|
||||
|
||||
def calculate_base_plot_filter_bits(height: uint32, constants: ConsensusConstants) -> int:
|
||||
def calculate_base_plot_filter_bits(pre_sp_tx_height: uint32, constants: ConsensusConstants) -> int:
|
||||
base_bits = int(constants.NUMBER_ZERO_BITS_PLOT_FILTER_V2)
|
||||
relative_height = int(height) - constants.HARD_FORK2_HEIGHT
|
||||
relative_height = int(pre_sp_tx_height) - constants.HARD_FORK2_HEIGHT
|
||||
if relative_height <= 0:
|
||||
return base_bits
|
||||
for index, offset in enumerate(constants.PLOT_FILTER_V2_RELATIVE_HEIGHT):
|
||||
@@ -291,13 +300,18 @@ def calculate_base_plot_filter_bits(height: uint32, constants: ConsensusConstant
|
||||
return base_bits
|
||||
|
||||
|
||||
def calculate_plot_filter_bits(height: uint32, constants: ConsensusConstants, plot_strength: int) -> int:
|
||||
def max_plot_strength(pre_sp_tx_height: uint32, constants: ConsensusConstants) -> int:
|
||||
return int(constants.MAX_PLOT_STRENGTH) - calculate_base_plot_filter_bits(pre_sp_tx_height, constants)
|
||||
|
||||
|
||||
def calculate_plot_filter_bits(pre_sp_tx_height: uint32, constants: ConsensusConstants, plot_strength: int) -> int:
|
||||
# min-strength schedule would change the zero point here.
|
||||
if plot_strength < constants.MIN_PLOT_STRENGTH:
|
||||
raise ValueError(f"Plot strength ({plot_strength}) is lower than the minimum ({constants.MIN_PLOT_STRENGTH})")
|
||||
strength_delta = int(plot_strength) - constants.MIN_PLOT_STRENGTH
|
||||
return min(
|
||||
calculate_base_plot_filter_bits(height, constants) + strength_delta, constants.MAX_EFFECTIVE_PLOT_FILTER_BITS
|
||||
calculate_base_plot_filter_bits(pre_sp_tx_height, constants) + strength_delta,
|
||||
constants.MAX_EFFECTIVE_PLOT_FILTER_BITS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user