Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CHIA-786] simplify hard-fork consensus rules #18208

Merged
merged 1 commit into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 5 additions & 30 deletions chia/_tests/blockchain/test_blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import time
from contextlib import asynccontextmanager
from dataclasses import replace
from typing import AsyncIterator, Dict, List, Optional, Tuple
from typing import AsyncIterator, Dict, List, Optional

import pytest
from chia_rs import AugSchemeMPL, G2Element, MerkleSet
Expand Down Expand Up @@ -2071,42 +2071,15 @@ async def test_timelock_conditions(
ConditionOpcode.AGG_SIG_PARENT_PUZZLE,
],
)
@pytest.mark.parametrize(
"with_garbage,expected",
[
(True, (AddBlockResult.INVALID_BLOCK, Err.INVALID_CONDITION, None)),
(False, (AddBlockResult.NEW_PEAK, None, 2)),
],
)
@pytest.mark.parametrize("with_garbage", [True, False])
async def test_aggsig_garbage(
self,
empty_blockchain: Blockchain,
opcode: ConditionOpcode,
with_garbage: bool,
expected: Tuple[AddBlockResult, Optional[Err], Optional[uint32]],
bt: BlockTools,
consensus_mode: ConsensusMode,
) -> None:
# in the 2.0 hard fork, we relax the strict 2-parameters rule of
# AGG_SIG_* conditions, in consensus mode. In mempool mode we always
# apply strict rules.
if consensus_mode >= ConsensusMode.HARD_FORK_2_0 and with_garbage:
expected = (AddBlockResult.NEW_PEAK, None, uint32(2))

# before the 2.0 hard fork, these conditions do not exist
# but WalletTool still lets us create them, and aggregate them into the
# block signature. When the pre-hard fork node sees them, the conditions
# are ignored, but the aggregate signature is corrupt.
if consensus_mode < ConsensusMode.HARD_FORK_2_0 and opcode in [
ConditionOpcode.AGG_SIG_PARENT,
ConditionOpcode.AGG_SIG_PUZZLE,
ConditionOpcode.AGG_SIG_AMOUNT,
ConditionOpcode.AGG_SIG_PUZZLE_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_PUZZLE,
]:
expected = (AddBlockResult.INVALID_BLOCK, Err.BAD_AGGREGATE_SIGNATURE, None)

b = empty_blockchain
blocks = bt.get_consecutive_blocks(
3,
Expand Down Expand Up @@ -2153,7 +2126,9 @@ async def test_aggsig_garbage(
# Ignore errors from pre-validation, we are testing block_body_validation
repl_preval_results = replace(pre_validation_results[0], error=None, required_iters=uint64(1))
res, error, state_change = await b.add_block(blocks[-1], repl_preval_results, None)
assert (res, error, state_change.fork_height if state_change else None) == expected
assert res == AddBlockResult.NEW_PEAK
assert error is None
assert state_change is not None and state_change.fork_height == uint32(2)

@pytest.mark.anyio
@pytest.mark.parametrize("with_garbage", [True, False])
Expand Down
18 changes: 2 additions & 16 deletions chia/_tests/core/full_node/test_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,6 @@ async def test_unknown_conditions_with_cost(
conditions = Program.to(assemble(f"(({opcode} 1337))"))
additions, removals, new_block = await check_conditions(bt, conditions)

if consensus_mode < ConsensusMode.HARD_FORK_2_0:
# before the hard fork, all unknown conditions have 0 cost
expected_cost = 0

# once the hard fork activates, blocks no longer pay the cost of the ROM
# generator (which includes hashing all puzzles).
if consensus_mode >= ConsensusMode.HARD_FORK_2_0:
Expand All @@ -172,8 +168,6 @@ async def test_softfork_condition(
additions, removals, new_block = await check_conditions(bt, conditions)

if consensus_mode < ConsensusMode.HARD_FORK_2_0:
# the SOFTFORK condition is not recognized before the hard fork
expected_cost = 0
block_base_cost = 737056
else:
# once the hard fork activates, blocks no longer pay the cost of the ROM
Expand Down Expand Up @@ -533,16 +527,7 @@ async def test_agg_sig_illegal_suffix(
assert c.AGG_SIG_PARENT_PUZZLE_ADDITIONAL_DATA == additional_data[ConditionOpcode.AGG_SIG_PARENT_PUZZLE]

blocks = await initial_blocks(bt)
if consensus_mode < ConsensusMode.HARD_FORK_2_0 and opcode in [
ConditionOpcode.AGG_SIG_PARENT,
ConditionOpcode.AGG_SIG_PUZZLE,
ConditionOpcode.AGG_SIG_AMOUNT,
ConditionOpcode.AGG_SIG_PUZZLE_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_PUZZLE,
]:
expected_error = Err.BAD_AGGREGATE_SIGNATURE
elif opcode == ConditionOpcode.AGG_SIG_UNSAFE:
if opcode == ConditionOpcode.AGG_SIG_UNSAFE:
expected_error = Err.INVALID_CONDITION
else:
expected_error = None
Expand All @@ -551,6 +536,7 @@ async def test_agg_sig_illegal_suffix(
pubkey = sk.get_g1()
coin = blocks[-2].get_included_reward_coins()[0]
for msg in [
c.AGG_SIG_ME_ADDITIONAL_DATA,
c.AGG_SIG_PARENT_ADDITIONAL_DATA,
c.AGG_SIG_PUZZLE_ADDITIONAL_DATA,
c.AGG_SIG_AMOUNT_ADDITIONAL_DATA,
Expand Down
66 changes: 10 additions & 56 deletions chia/_tests/core/mempool/test_mempool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2202,15 +2202,15 @@ def test_invalid_condition_args_terminator(self, softfork_height: uint32) -> Non
assert npc_result.conds.spends[0].seconds_relative == 50

@pytest.mark.parametrize(
"mempool,operand,expected",
"mempool,operand",
[
(True, -1, Err.GENERATOR_RUNTIME_ERROR.value),
(False, -1, Err.GENERATOR_RUNTIME_ERROR.value),
(True, 1, None),
(False, 1, None),
(True, -1),
(False, -1),
(True, 1),
(False, 1),
],
)
def test_div(self, mempool: bool, operand: int, expected: Optional[int], softfork_height: uint32) -> None:
def test_div(self, mempool: bool, operand: int, softfork_height: uint32) -> None:
# op_div is disallowed on negative numbers in the mempool, and after the
# softfork
npc_result = generator_condition_tester(
Expand All @@ -2220,11 +2220,8 @@ def test_div(self, mempool: bool, operand: int, expected: Optional[int], softfor
height=softfork_height,
)

# with the 2.0 hard fork, division with negative numbers is allowed
if operand < 0 and softfork_height >= test_constants.HARD_FORK_HEIGHT:
expected = None

assert npc_result.error == expected
# after the 2.0 hard fork, division with negative numbers is allowed
assert npc_result.error is None

def test_invalid_condition_list_terminator(self, softfork_height: uint32) -> None:
# note how the list of conditions isn't correctly terminated with a
Expand Down Expand Up @@ -2367,17 +2364,7 @@ def test_agg_sig_cost(self, condition: ConditionOpcode, softfork_height: uint32)
else:
generator_base_cost = 20512

if softfork_height < test_constants.HARD_FORK_HEIGHT and condition in [
ConditionOpcode.AGG_SIG_PARENT,
ConditionOpcode.AGG_SIG_PUZZLE,
ConditionOpcode.AGG_SIG_AMOUNT,
ConditionOpcode.AGG_SIG_PUZZLE_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_PUZZLE,
]:
expected_cost = 0
else:
expected_cost = ConditionCost.AGG_SIG.value
expected_cost = ConditionCost.AGG_SIG.value

# this max cost is exactly enough for the AGG_SIG condition
npc_result = generator_condition_tester(
Expand Down Expand Up @@ -2422,41 +2409,12 @@ def test_agg_sig_extra_arg(
) -> None:
pubkey = "0x" + bytes(G1Element.generator()).hex()

new_condition = condition in [
ConditionOpcode.AGG_SIG_PARENT,
ConditionOpcode.AGG_SIG_PUZZLE,
ConditionOpcode.AGG_SIG_AMOUNT,
ConditionOpcode.AGG_SIG_PUZZLE_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_AMOUNT,
ConditionOpcode.AGG_SIG_PARENT_PUZZLE,
]

hard_fork_activated = softfork_height >= test_constants.HARD_FORK_HEIGHT

expected_error = None

# in mempool mode, we don't allow extra arguments
if mempool and extra_arg != "":
expected_error = Err.INVALID_CONDITION.value

# the original AGG_SIG_* conditions had a quirk (fixed in the hard fork)
# where they always required exactly two arguments, regardless of
# mempool or not. After the hard fork, they behave like all other
# conditions
if not new_condition and not hard_fork_activated and extra_arg != "":
expected_error = Err.INVALID_CONDITION.value

# except before the hard fork has activated, new conditions are just
# unknown
if new_condition and not hard_fork_activated:
else:
expected_error = None

# before the hard fork activates, the new conditions are unknown and
# fail in mempool mode, regardless of whether they have extra arguments
# or not
if new_condition and not hard_fork_activated and mempool:
expected_error = Err.INVALID_CONDITION.value

# this max cost is exactly enough for the AGG_SIG condition
npc_result = generator_condition_tester(
f'({condition[0]} {pubkey} "foobar"{extra_arg}) ',
Expand Down Expand Up @@ -2569,10 +2527,6 @@ def test_softfork_condition(
# in mempool all unknown conditions are always a failure
if mempool:
expect_error = Err.INVALID_CONDITION.value
# the SOFTFORK condition is only activated with the hard fork, so
# before then there are no errors
elif softfork_height < test_constants.HARD_FORK_HEIGHT:
expect_error = None

assert npc_result.error == expect_error

Expand Down
29 changes: 3 additions & 26 deletions chia/full_node/mempool_check_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,38 +40,14 @@


def get_flags_for_height_and_constants(height: int, constants: ConsensusConstants) -> int:
flags = 0
flags = ENABLE_SOFTFORK_CONDITION | ENABLE_BLS_OPS_OUTSIDE_GUARD | ENABLE_FIXED_DIV | AGG_SIG_ARGS | ALLOW_BACKREFS

if height >= constants.SOFT_FORK4_HEIGHT:
flags = flags | ENABLE_MESSAGE_CONDITIONS

if height >= constants.SOFT_FORK5_HEIGHT:
flags = flags | DISALLOW_INFINITY_G1

if height >= constants.HARD_FORK_HEIGHT:
# the hard-fork initiated with 2.1. To activate June 2024
# * costs are ascribed to some unknown condition codes, to allow for
# soft-forking in new conditions with cost
# * a new condition, SOFTFORK, is added which takes a first parameter to
# specify its cost. This allows soft-forks similar to the softfork
# operator
# * BLS operators introduced in the soft-fork (behind the softfork
# guard) are made available outside of the guard.
# * division with negative numbers are allowed, and round toward
# negative infinity
# * AGG_SIG_* conditions are allowed to have unknown additional
# arguments
# * Allow the block generator to be serialized with the improved clvm
# serialization format (with back-references)
flags = (
flags
| ENABLE_SOFTFORK_CONDITION
| ENABLE_BLS_OPS_OUTSIDE_GUARD
| ENABLE_FIXED_DIV
| AGG_SIG_ARGS
| ALLOW_BACKREFS
)

return flags


Expand All @@ -83,14 +59,15 @@ def get_name_puzzle_conditions(
height: uint32,
constants: ConsensusConstants,
) -> NPCResult:
run_block = run_block_generator
flags = get_flags_for_height_and_constants(height, constants)

if mempool_mode:
flags = flags | MEMPOOL_MODE

if height >= constants.HARD_FORK_HEIGHT:
run_block = run_block_generator2
else:
run_block = run_block_generator

try:
block_args = [bytes(gen) for gen in generator.generator_refs]
Expand Down
18 changes: 9 additions & 9 deletions chia/simulator/block_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1911,22 +1911,22 @@ def compute_cost_table() -> List[int]:
CONDITION_COSTS = compute_cost_table()


def conditions_cost(conds: Program, hard_fork: bool) -> uint64:
def conditions_cost(conds: Program) -> uint64:
condition_cost = 0
for cond in conds.as_iter():
condition = cond.first().as_atom()
if condition in [ConditionOpcode.AGG_SIG_UNSAFE, ConditionOpcode.AGG_SIG_ME]:
condition_cost += ConditionCost.AGG_SIG.value
elif condition == ConditionOpcode.CREATE_COIN:
if condition == ConditionOpcode.CREATE_COIN:
condition_cost += ConditionCost.CREATE_COIN.value
# after the 2.0 hard fork, two byte conditions (with no leading 0)
# have costs. Account for that.
elif hard_fork and len(condition) == 2 and condition[0] != 0:
elif len(condition) == 2 and condition[0] != 0:
condition_cost += CONDITION_COSTS[condition[1]]
elif hard_fork and condition == ConditionOpcode.SOFTFORK.value:
elif condition == ConditionOpcode.SOFTFORK.value:
arg = cond.rest().first().as_int()
condition_cost += arg * 10000
elif hard_fork and condition in [
elif condition in [
ConditionOpcode.AGG_SIG_UNSAFE,
ConditionOpcode.AGG_SIG_ME,
ConditionOpcode.AGG_SIG_PARENT,
ConditionOpcode.AGG_SIG_PUZZLE,
ConditionOpcode.AGG_SIG_AMOUNT,
Expand Down Expand Up @@ -1974,7 +1974,7 @@ def compute_cost_test(generator: BlockGenerator, constants: ConsensusConstants,

cost, result = puzzle._run(INFINITE_COST, MEMPOOL_MODE, solution)
clvm_cost += cost
condition_cost += conditions_cost(result, height >= constants.HARD_FORK_HEIGHT)
condition_cost += conditions_cost(result)

else:
block_program_args = SerializedProgram.to([[bytes(g) for g in generator.generator_refs]])
Expand All @@ -1984,7 +1984,7 @@ def compute_cost_test(generator: BlockGenerator, constants: ConsensusConstants,
# each condition item is:
# (parent-coin-id puzzle-hash amount conditions)
conditions = res.at("rrrf")
condition_cost += conditions_cost(conditions, height >= constants.HARD_FORK_HEIGHT)
condition_cost += conditions_cost(conditions)

size_cost = len(bytes(generator.program)) * constants.COST_PER_BYTE

Expand Down
Loading