Hard fork 2.0 (#15581)

* add HARD_FORK_2_0 consensus_mode to run tests under hard-fork consensus rules

* hook up soft-fork and hard-fork changes in CLVM

* fix AGG_SIG_* garbage tests for the 2.0 hard-fork

* add test for unknown conditions with cost, which is a hard-fork feature

* add mempool tests for unknown conditions with cost

* add tests for the SOFTFORK condition

* fix benchmark to take LIMIT_ANNOUNCES into account
This commit is contained in:
Arvid Norberg
2023-07-07 13:58:43 -05:00
committed by GitHub
parent ca1a53e42f
commit ca1147584d
7 changed files with 231 additions and 21 deletions
+47 -1
View File
@@ -3,7 +3,20 @@ from __future__ import annotations
import logging
from typing import Dict, List, Optional
from chia_rs import ENABLE_ASSERT_BEFORE, MEMPOOL_MODE, NO_RELATIVE_CONDITIONS_ON_EPHEMERAL
from chia_rs import (
AGG_SIG_ARGS,
ALLOW_BACKREFS,
ENABLE_ASSERT_BEFORE,
ENABLE_BLS_OPS,
ENABLE_BLS_OPS_OUTSIDE_GUARD,
ENABLE_FIXED_DIV,
ENABLE_SECP_OPS,
ENABLE_SOFTFORK_CONDITION,
LIMIT_ANNOUNCES,
LIMIT_OBJECTS,
MEMPOOL_MODE,
NO_RELATIVE_CONDITIONS_ON_EPHEMERAL,
)
from chia_rs import get_puzzle_and_solution_for_coin as get_puzzle_and_solution_for_coin_rust
from chia_rs import run_block_generator, run_chia_program
from clvm.casts import int_from_bytes
@@ -45,6 +58,39 @@ def get_name_puzzle_conditions(
if height >= constants.SOFT_FORK2_HEIGHT:
flags = flags | ENABLE_ASSERT_BEFORE | NO_RELATIVE_CONDITIONS_ON_EPHEMERAL
if height >= constants.SOFT_FORK3_HEIGHT:
# the soft-fork initiated with 2.0. To activate end of October 2023
# * the number of announces created and asserted are limited per spend
# * the total number of CLVM objects (atoms or pairs) are limited
# * BLS operators enabled, behind the softfork op. This set of operators
# also includes coinid, % and modpow
# * secp operators enabled
flags = flags | LIMIT_ANNOUNCES | LIMIT_OBJECTS | ENABLE_BLS_OPS | ENABLE_SECP_OPS
if height >= constants.HARD_FORK_HEIGHT:
# the hard-fork initiated with 2.0. 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
)
try:
block_args = [bytes(gen) for gen in generator.generator_refs]
err, result = run_block_generator(bytes(generator.program), block_args, max_cost, flags)
+39 -2
View File
@@ -1725,7 +1725,35 @@ def get_full_block_and_block_record(
return full_block, block_record, block_time_residual
def compute_cost_test(generator: BlockGenerator, cost_per_byte: int) -> Tuple[Optional[uint16], uint64]:
# these are the costs of unknown conditions, as defined chia_rs here:
# https://github.com/Chia-Network/chia_rs/pull/181
def compute_cost_table() -> List[int]:
A = 17
B = 16
s = []
NUM = 100
DEN = 1
MAX = 1 << 59
for i in range(256):
v = str(NUM // DEN)
v1 = v[:3] + ("0" * (len(v) - 3))
s.append(int(v1))
NUM *= A
DEN *= B
assert NUM < 1 << 64
assert DEN < 1 << 64
if NUM > MAX:
NUM >>= 5
DEN >>= 5
return s
CONDITION_COSTS = compute_cost_table()
def compute_cost_test(
generator: BlockGenerator, cost_per_byte: int, hard_fork: bool = False
) -> Tuple[Optional[uint16], uint64]:
try:
block_program_args = Program.to([[bytes(g) for g in generator.generator_refs]])
clvm_cost, result = GENERATOR_MOD.run_mempool_with_cost(INFINITE_COST, generator.program, block_program_args)
@@ -1742,6 +1770,13 @@ def compute_cost_test(generator: BlockGenerator, cost_per_byte: int) -> Tuple[Op
condition_cost += ConditionCost.AGG_SIG.value
elif 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:
condition_cost += CONDITION_COSTS[condition[1]]
elif hard_fork and condition == ConditionOpcode.SOFTFORK.value:
arg = cond.rest().first().as_int()
condition_cost += arg * 10000
return None, uint64(clvm_cost + size_cost + condition_cost)
except Exception:
return uint16(Err.GENERATOR_RUNTIME_ERROR.value), uint64(0)
@@ -1839,7 +1874,9 @@ def create_test_foliage(
# Calculate the cost of transactions
if block_generator is not None:
generator_block_heights_list = block_generator.block_height_list
err, cost = compute_cost_test(block_generator, constants.COST_PER_BYTE)
err, cost = compute_cost_test(
block_generator, constants.COST_PER_BYTE, hard_fork=height >= constants.HARD_FORK_HEIGHT
)
assert err is None
removal_amount = 0
+11 -3
View File
@@ -2018,7 +2018,13 @@ class TestBodyValidation:
(False, (AddBlockResult.NEW_PEAK, None, 2)),
],
)
async def test_aggsig_garbage(self, empty_blockchain, opcode, with_garbage, expected, bt):
async def test_aggsig_garbage(self, empty_blockchain, opcode, with_garbage, expected, bt, consensus_mode: Mode):
# 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 == Mode.HARD_FORK_2_0 and with_garbage:
expected = (AddBlockResult.NEW_PEAK, None, 2)
b = empty_blockchain
blocks = bt.get_consecutive_blocks(
3,
@@ -2035,7 +2041,7 @@ class TestBodyValidation:
wt: WalletTool = bt.get_pool_wallet_tool()
tx1: SpendBundle = wt.generate_signed_transaction(
10, wt.get_new_puzzlehash(), list(blocks[-1].get_included_reward_coins())[0]
uint64(10), wt.get_new_puzzlehash(), list(blocks[-1].get_included_reward_coins())[0]
)
coin1: Coin = tx1.additions()[0]
secret_key = wt.get_private_key_for_puzzle_hash(coin1.puzzle_hash)
@@ -2045,7 +2051,9 @@ class TestBodyValidation:
args = [public_key, b"msg"] + ([b"garbage"] if with_garbage else [])
conditions = {opcode: [ConditionWithArgs(opcode, args)]}
tx2: SpendBundle = wt.generate_signed_transaction(10, wt.get_new_puzzlehash(), coin1, condition_dic=conditions)
tx2: SpendBundle = wt.generate_signed_transaction(
uint64(10), wt.get_new_puzzlehash(), coin1, condition_dic=conditions
)
assert coin1 in tx2.removals()
bundles = SpendBundle.aggregate([tx1, tx2])
+6 -1
View File
@@ -100,9 +100,10 @@ def get_keychain():
class Mode(Enum):
PLAIN = 0
HARD_FORK_2_0 = 1
@pytest.fixture(scope="session", params=[Mode.PLAIN])
@pytest.fixture(scope="session", params=[Mode.PLAIN, Mode.HARD_FORK_2_0])
def consensus_mode(request):
return request.param
@@ -111,6 +112,10 @@ def consensus_mode(request):
def blockchain_constants(consensus_mode) -> ConsensusConstants:
if consensus_mode == Mode.PLAIN:
return test_constants
if consensus_mode == Mode.HARD_FORK_2_0:
return test_constants.replace(
HARD_FORK_HEIGHT=2, PLOT_FILTER_128_HEIGHT=10, PLOT_FILTER_64_HEIGHT=15, PLOT_FILTER_32_HEIGHT=20
)
raise AssertionError("Invalid Blockchain mode in simulation")
+71 -4
View File
@@ -23,6 +23,7 @@ from chia.types.full_block import FullBlock
from chia.types.spend_bundle import SpendBundle
from chia.util.errors import Err
from chia.util.ints import uint32, uint64
from tests.conftest import Mode
from ...blockchain.blockchain_test_utils import _validate_and_add_block
from .ram_db import create_ram_blockchain
@@ -60,7 +61,7 @@ async def check_spend_bundle_validity(
spend_bundle: SpendBundle,
expected_err: Optional[Err] = None,
softfork2: bool = False,
) -> Tuple[List[CoinRecord], List[CoinRecord]]:
) -> Tuple[List[CoinRecord], List[CoinRecord], FullBlock]:
"""
This test helper create an extra block after the given blocks that contains the given
`SpendBundle`, and then invokes `add_block` to ensure that it's accepted (if `expected_err=None`)
@@ -95,7 +96,7 @@ async def check_spend_bundle_validity(
coins_added = []
coins_removed = []
return coins_added, coins_removed
return coins_added, coins_removed, newest_block
finally:
# if we don't close the db_wrapper, the test process doesn't exit cleanly
@@ -111,7 +112,7 @@ async def check_conditions(
expected_err: Optional[Err] = None,
spend_reward_index: int = -2,
softfork2: bool = False,
):
) -> Tuple[List[CoinRecord], List[CoinRecord], FullBlock]:
blocks = await initial_blocks(bt)
coin = list(blocks[spend_reward_index].get_included_reward_coins())[0]
@@ -120,13 +121,79 @@ async def check_conditions(
# now let's try to create a block with the spend bundle and ensure that it doesn't validate
await check_spend_bundle_validity(bt, blocks, spend_bundle, expected_err=expected_err, softfork2=softfork2)
return await check_spend_bundle_validity(bt, blocks, spend_bundle, expected_err=expected_err, softfork2=softfork2)
co = ConditionOpcode
class TestConditions:
@pytest.mark.asyncio
@pytest.mark.parametrize(
"opcode, expected_cost",
[
(0x100, 100),
(0x101, 106),
(0x102, 112),
(0x103, 119),
(0x107, 152),
(0x1F0, 208000000),
# the pattern repeats for every leading byte
(0x400, 100),
(0x401, 106),
(0x4F0, 208000000),
(0x4000, 100),
(0x4001, 106),
(0x40F0, 208000000),
],
)
async def test_unknown_conditions_with_cost(
self,
opcode: int,
expected_cost: int,
bt,
consensus_mode: Mode,
):
conditions = Program.to(assemble(f"(({opcode} 1337))"))
additions, removals, new_block = await check_conditions(bt, conditions)
if consensus_mode != Mode.HARD_FORK_2_0:
# before the hard fork, all unknown conditions have 0 cost
expected_cost = 0
block_base_cost = 761056
assert new_block.transactions_info is not None
assert new_block.transactions_info.cost - block_base_cost == expected_cost
@pytest.mark.asyncio
@pytest.mark.parametrize(
"condition, expected_cost",
[
("((90 1337))", 13370000),
("((90 30000))", 300000000),
],
)
async def test_softfork_condition(
self,
condition: str,
expected_cost: int,
bt,
consensus_mode: Mode,
):
conditions = Program.to(assemble(condition))
additions, removals, new_block = await check_conditions(bt, conditions)
if consensus_mode != Mode.HARD_FORK_2_0:
# the SOFTFORK condition is not recognized before the hard fork
expected_cost = 0
# this includes the cost of the bytes for the condition with 2 bytes
# argument. This test works as long as the conditions it's parameterized
# on has the same size
block_base_cost = 737056
assert new_block.transactions_info is not None
assert new_block.transactions_info.cost - block_base_cost == expected_cost
@pytest.mark.asyncio
@pytest.mark.parametrize("softfork2", [True, False])
@pytest.mark.parametrize(
+56 -9
View File
@@ -12,6 +12,7 @@ from clvm_tools import binutils
from chia.consensus.condition_costs import ConditionCost
from chia.consensus.cost_calculator import NPCResult
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.full_node.bitcoin_fee_estimator import create_bitcoin_fee_estimator
from chia.full_node.fee_estimation import EmptyMempoolInfo, MempoolInfo
from chia.full_node.full_node_api import FullNodeAPI
@@ -2004,6 +2005,11 @@ class TestGeneratorConditions:
mempool_mode=mempool,
height=softfork_height,
)
# with the 2.0 hard fork, division with negative numbers is allowed
if operand < 0 and softfork_height >= DEFAULT_CONSTANTS.HARD_FORK_HEIGHT:
expected = None
assert npc_result.error == expected
def test_invalid_condition_list_terminator(self, softfork_height):
@@ -2195,14 +2201,50 @@ class TestGeneratorConditions:
assert coins == [(puzzle_hash_1.encode("ascii"), 5, hint.encode("ascii"))]
@pytest.mark.parametrize("mempool", [True, False])
def test_unknown_condition(self, mempool: bool, softfork_height: uint32):
for c in ['(2 100 "foo" "bar")', "(100)", "(4 1) (2 2) (3 3)", '("foobar")']:
npc_result = generator_condition_tester(c, mempool_mode=mempool, height=softfork_height)
print(npc_result)
if mempool:
assert npc_result.error == Err.INVALID_CONDITION.value
else:
assert npc_result.error is None
@pytest.mark.parametrize(
"condition",
[
'(2 100 "foo" "bar")',
"(100)",
"(4 1) (2 2) (3 3)",
'("foobar")',
'(0x100 "foobar")',
'(0x1ff "foobar")',
],
)
def test_unknown_condition(self, mempool: bool, condition: str, softfork_height: uint32):
npc_result = generator_condition_tester(condition, mempool_mode=mempool, height=softfork_height)
print(npc_result)
if mempool:
assert npc_result.error == Err.INVALID_CONDITION.value
else:
assert npc_result.error is None
@pytest.mark.parametrize("mempool", [True, False])
@pytest.mark.parametrize(
"condition, expect_error",
[
# the softfork condition must include at least 1 argument to
# indicate its cost
("(90)", Err.INVALID_CONDITION.value),
("(90 1000000)", None),
],
)
def test_softfork_condition(
self, mempool: bool, condition: str, expect_error: Optional[int], softfork_height: uint32
):
npc_result = generator_condition_tester(condition, mempool_mode=mempool, height=softfork_height)
print(npc_result)
# 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 < DEFAULT_CONSTANTS.HARD_FORK_HEIGHT:
expect_error = None
assert npc_result.error == expect_error
# the tests below are malicious generator programs
@@ -2442,7 +2484,12 @@ class TestMaliciousGenerators:
)
@pytest.mark.benchmark
def test_duplicate_coin_announces(self, request, opcode, softfork_height):
condition = CREATE_ANNOUNCE_COND.format(opcode=opcode.value[0], num=5950000)
# with soft-fork3, we only allow 1024 create- or assert announcements
# per spend
if softfork_height >= DEFAULT_CONSTANTS.SOFT_FORK3_HEIGHT:
condition = CREATE_ANNOUNCE_COND.format(opcode=opcode.value[0], num=1024)
else:
condition = CREATE_ANNOUNCE_COND.format(opcode=opcode.value[0], num=5950000)
with assert_runtime(seconds=9, label=request.node.name):
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
+1 -1
View File
@@ -112,7 +112,7 @@ async def test_only_odd_coins_0(bt):
conditions = Program.to(condition_list)
coin_spend = CoinSpend(farmed_coin, ANYONE_CAN_SPEND_PUZZLE, conditions)
spend_bundle = SpendBundle.aggregate([launcher_spend_bundle, SpendBundle([coin_spend], G2Element())])
coins_added, coins_removed = await check_spend_bundle_validity(bt, blocks, spend_bundle)
coins_added, coins_removed, _ = await check_spend_bundle_validity(bt, blocks, spend_bundle)
coin_set_added = set([_.coin for _ in coins_added])
coin_set_removed = set([_.coin for _ in coins_removed])