mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user