introduce restrictions on generators at a specific height (#9957)

* introduce restrictions on generators at a specific height. disallow division on negative numbers and disallow redundant leading zeros on integer condition arguments (produced by a generator)

* use SOFT_FORK_HEIGHT constant

* there is no need to specify height when validating block in block_creation

* Update tests/core/full_node/test_mempool.py

Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com>

Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com>
This commit is contained in:
Arvid Norberg
2022-01-28 12:29:11 -08:00
committed by GitHub
co-authored by dustinface
parent 8b838731d9
commit b8ada1ceb7
19 changed files with 283 additions and 126 deletions
+1
View File
@@ -320,6 +320,7 @@ async def validate_block_body(
min(constants.MAX_BLOCK_COST_CLVM, curr.transactions_info.cost),
cost_per_byte=constants.COST_PER_BYTE,
mempool_mode=False,
height=curr.height,
)
removals_in_curr, additions_in_curr = tx_removals_and_additions(curr_npc_result.npc_list)
else:
+3 -1
View File
@@ -455,6 +455,7 @@ class Blockchain(BlockchainInterface):
self.constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=self.constants.COST_PER_BYTE,
mempool_mode=False,
height=block.height,
)
tx_removals, tx_additions = tx_removals_and_additions(npc_result.npc_list)
return tx_removals, tx_additions, npc_result
@@ -634,13 +635,14 @@ class Blockchain(BlockchainInterface):
validate_signatures=validate_signatures,
)
async def run_generator(self, unfinished_block: bytes, generator: BlockGenerator) -> NPCResult:
async def run_generator(self, unfinished_block: bytes, generator: BlockGenerator, height: uint32) -> NPCResult:
task = asyncio.get_running_loop().run_in_executor(
self.pool,
_run_generator,
self.constants_json,
unfinished_block,
bytes(generator),
height,
)
npc_result_bytes = await task
if npc_result_bytes is None:
+1
View File
@@ -59,6 +59,7 @@ class ConsensusConstants:
MAX_GENERATOR_SIZE: uint32
MAX_GENERATOR_REF_LIST_SIZE: uint32
POOL_SUB_SLOT_ITERS: uint64
SOFT_FORK_HEIGHT: uint32
def replace(self, **changes) -> "ConsensusConstants":
return dataclasses.replace(self, **changes)
+1
View File
@@ -54,6 +54,7 @@ testnet_kwargs = {
"MAX_GENERATOR_SIZE": 1000000,
"MAX_GENERATOR_REF_LIST_SIZE": 512, # Number of references allowed in the block generator ref list
"POOL_SUB_SLOT_ITERS": 37600000000, # iters limit * NUM_SPS
"SOFT_FORK_HEIGHT": 2000000,
}
@@ -90,6 +90,7 @@ def batch_pre_validate_blocks(
min(constants.MAX_BLOCK_COST_CLVM, block.transactions_info.cost),
cost_per_byte=constants.COST_PER_BYTE,
mempool_mode=False,
height=block.height,
)
removals, tx_additions = tx_removals_and_additions(npc_result.npc_list)
if npc_result is not None and npc_result.error is not None:
@@ -370,6 +371,7 @@ def _run_generator(
constants_dict: bytes,
unfinished_block_bytes: bytes,
block_generator_bytes: bytes,
height: uint32,
) -> Optional[bytes]:
"""
Runs the CLVM generator from bytes inputs. This is meant to be called under a ProcessPoolExecutor, in order to
@@ -386,6 +388,7 @@ def _run_generator(
min(constants.MAX_BLOCK_COST_CLVM, unfinished_block.transactions_info.cost),
cost_per_byte=constants.COST_PER_BYTE,
mempool_mode=False,
height=height,
)
return bytes(npc_result)
except ValidationError as e:
+2 -1
View File
@@ -1675,7 +1675,8 @@ class FullNode:
if block_bytes is None:
block_bytes = bytes(block)
npc_result = await self.blockchain.run_generator(block_bytes, block_generator)
height = uint32(0) if prev_b is None else uint32(prev_b.height + 1)
npc_result = await self.blockchain.run_generator(block_bytes, block_generator, height)
pre_validation_time = time.time() - pre_validation_start
pairs_pks, pairs_msgs = pkm_pairs(npc_result.npc_list, self.constants.AGG_SIG_ME_ADDITIONAL_DATA)
+26 -3
View File
@@ -1,10 +1,11 @@
import logging
import time
from typing import Dict, List, Optional
from clvm_rs import STRICT_MODE as MEMPOOL_MODE
from clvm_rs import MEMPOOL_MODE, COND_CANON_INTS, NO_NEG_DIV
from clvm.casts import int_from_bytes, int_to_bytes
from chia.consensus.cost_calculator import NPCResult
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.full_node.generator import create_generator_args, setup_generator_args
from chia.types.blockchain_format.program import NIL
from chia.types.coin_record import CoinRecord
@@ -107,8 +108,13 @@ def add_cond(
conds[op].append(ConditionWithArgs(op, args))
def unwrap(x: Optional[uint32]) -> uint32:
assert x is not None
return x
def get_name_puzzle_conditions(
generator: BlockGenerator, max_cost: int, *, cost_per_byte: int, mempool_mode: bool
generator: BlockGenerator, max_cost: int, *, cost_per_byte: int, mempool_mode: bool, height: Optional[uint32] = None
) -> NPCResult:
block_program, block_program_args = setup_generator_args(generator)
size_cost = len(bytes(generator.program)) * cost_per_byte
@@ -116,7 +122,24 @@ def get_name_puzzle_conditions(
if max_cost < 0:
return NPCResult(uint16(Err.INVALID_BLOCK_COST.value), [], uint64(0))
flags = MEMPOOL_MODE if mempool_mode else 0
# in mempool mode, the height doesn't matter, because it's always strict.
# But otherwise, height must be specified to know which rules to apply
assert mempool_mode or height is not None
# mempool mode also has these rules apply
assert (MEMPOOL_MODE & COND_CANON_INTS) != 0
assert (MEMPOOL_MODE & NO_NEG_DIV) != 0
if mempool_mode:
flags = MEMPOOL_MODE
elif unwrap(height) >= DEFAULT_CONSTANTS.SOFT_FORK_HEIGHT:
# conditions must use integers in canonical encoding (i.e. no redundant
# leading zeros)
# the division operator may not be used with negative operands
flags = COND_CANON_INTS | NO_NEG_DIV
else:
flags = 0
try:
err, result = GENERATOR_MOD.run_as_generator(max_cost, flags, block_program, block_program_args)
+1 -1
View File
@@ -7,7 +7,7 @@ from clvm.casts import int_from_bytes
from clvm.EvalError import EvalError
from clvm.operators import OPERATOR_LOOKUP
from clvm.serialize import sexp_from_stream, sexp_to_stream
from clvm_rs import STRICT_MODE as MEMPOOL_MODE, run_chia_program, serialized_length, run_generator2
from clvm_rs import MEMPOOL_MODE, run_chia_program, serialized_length, run_generator2
from clvm_tools.curry import curry, uncurry
from chia.types.blockchain_format.sized_bytes import bytes32
+5 -1
View File
@@ -17,7 +17,11 @@ def compute_coin_hints(cs: CoinSpend) -> List[bytes]:
generator = simple_solution_generator(bundle)
npc_result = get_name_puzzle_conditions(
generator, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=False
generator,
INFINITE_COST,
cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE,
mempool_mode=False,
height=DEFAULT_CONSTANTS.SOFT_FORK_HEIGHT,
)
h_list = []
for npc in npc_result.npc_list:
+1 -1
View File
@@ -8,7 +8,7 @@ dependencies = [
"chiabip158==1.1", # bip158-style wallet filters
"chiapos==1.0.8", # proof of space
"clvm==0.9.7",
"clvm_rs==0.1.17",
"clvm_rs==0.1.19",
"clvm_tools==0.4.3",
"aiohttp==3.7.4", # HTTP server for full node rpc
"aiosqlite==0.17.0", # asyncio wrapper for sqlite, to store blocks
+11 -7
View File
@@ -251,7 +251,7 @@ class TestBlockHeaderValidation:
assert empty_blockchain.get_peak().height == len(blocks) - 1
@pytest.mark.asyncio
async def test_unfinished_blocks(self, empty_blockchain):
async def test_unfinished_blocks(self, empty_blockchain, softfork_height):
blockchain = empty_blockchain
blocks = bt.get_consecutive_blocks(3)
for block in blocks[:-1]:
@@ -272,7 +272,7 @@ class TestBlockHeaderValidation:
if unf.transactions_generator is not None:
block_generator: BlockGenerator = await blockchain.get_block_generator(unf)
block_bytes = bytes(unf)
npc_result = await blockchain.run_generator(block_bytes, block_generator)
npc_result = await blockchain.run_generator(block_bytes, block_generator, height=softfork_height)
validate_res = await blockchain.validate_unfinished_block(unf, npc_result, False)
err = validate_res.error
@@ -296,7 +296,7 @@ class TestBlockHeaderValidation:
if unf.transactions_generator is not None:
block_generator: BlockGenerator = await blockchain.get_block_generator(unf)
block_bytes = bytes(unf)
npc_result = await blockchain.run_generator(block_bytes, block_generator)
npc_result = await blockchain.run_generator(block_bytes, block_generator, height=softfork_height)
validate_res = await blockchain.validate_unfinished_block(unf, npc_result, False)
assert validate_res.error is None
@@ -342,7 +342,7 @@ class TestBlockHeaderValidation:
assert blockchain.get_peak().height == num_blocks - 1
@pytest.mark.asyncio
async def test_unf_block_overflow(self, empty_blockchain):
async def test_unf_block_overflow(self, empty_blockchain, softfork_height):
blockchain = empty_blockchain
blocks = []
@@ -376,7 +376,7 @@ class TestBlockHeaderValidation:
if block.transactions_generator is not None:
block_generator: BlockGenerator = await blockchain.get_block_generator(unf)
block_bytes = bytes(unf)
npc_result = await blockchain.run_generator(block_bytes, block_generator)
npc_result = await blockchain.run_generator(block_bytes, block_generator, height=softfork_height)
validate_res = await blockchain.validate_unfinished_block(
unf, npc_result, skip_overflow_ss_validation=True
)
@@ -2165,7 +2165,7 @@ class TestBodyValidation:
)
@pytest.mark.asyncio
async def test_cost_exceeds_max(self, empty_blockchain):
async def test_cost_exceeds_max(self, empty_blockchain, softfork_height):
# 7
b = empty_blockchain
blocks = bt.get_consecutive_blocks(
@@ -2199,6 +2199,7 @@ class TestBodyValidation:
b.constants.MAX_BLOCK_COST_CLVM * 1000,
cost_per_byte=b.constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
err = (await b.receive_block(blocks[-1], PreValidationResult(None, uint64(1), npc_result, True)))[1]
assert err in [Err.BLOCK_COST_EXCEEDS_MAX]
@@ -2215,7 +2216,7 @@ class TestBodyValidation:
pass
@pytest.mark.asyncio
async def test_invalid_cost_in_block(self, empty_blockchain):
async def test_invalid_cost_in_block(self, empty_blockchain, softfork_height):
# 9
b = empty_blockchain
blocks = bt.get_consecutive_blocks(
@@ -2259,6 +2260,7 @@ class TestBodyValidation:
min(b.constants.MAX_BLOCK_COST_CLVM * 1000, block.transactions_info.cost),
cost_per_byte=b.constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
result, err, _, _ = await b.receive_block(block_2, PreValidationResult(None, uint64(1), npc_result, False))
assert err == Err.INVALID_BLOCK_COST
@@ -2283,6 +2285,7 @@ class TestBodyValidation:
min(b.constants.MAX_BLOCK_COST_CLVM * 1000, block.transactions_info.cost),
cost_per_byte=b.constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
result, err, _, _ = await b.receive_block(block_2, PreValidationResult(None, uint64(1), npc_result, False))
assert err == Err.INVALID_BLOCK_COST
@@ -2307,6 +2310,7 @@ class TestBodyValidation:
min(b.constants.MAX_BLOCK_COST_CLVM * 1000, block.transactions_info.cost),
cost_per_byte=b.constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
result, err, _, _ = await b.receive_block(block_2, PreValidationResult(None, uint64(1), npc_result, False))
@@ -281,7 +281,7 @@ class TestBlockchainTransactions:
await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(block))
@pytest.mark.asyncio
async def test_validate_blockchain_spend_reorg_coin(self, two_nodes):
async def test_validate_blockchain_spend_reorg_coin(self, two_nodes, softfork_height):
num_blocks = 10
wallet_a = WALLET_A
coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0]
@@ -320,7 +320,10 @@ class TestBlockchainTransactions:
coin_2 = None
for coin in run_and_get_removals_and_additions(
new_blocks[-1], test_constants.MAX_BLOCK_COST_CLVM, test_constants.COST_PER_BYTE
new_blocks[-1],
test_constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=test_constants.COST_PER_BYTE,
height=softfork_height,
)[1]:
if coin.puzzle_hash == receiver_1_puzzlehash:
coin_2 = coin
@@ -341,7 +344,10 @@ class TestBlockchainTransactions:
coin_3 = None
for coin in run_and_get_removals_and_additions(
new_blocks[-1], test_constants.MAX_BLOCK_COST_CLVM, test_constants.COST_PER_BYTE
new_blocks[-1],
test_constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=test_constants.COST_PER_BYTE,
height=softfork_height,
)[1]:
if coin.puzzle_hash == receiver_2_puzzlehash:
coin_3 = coin
+5
View File
@@ -35,6 +35,11 @@ def db_version(request):
return request.param
@pytest.fixture(scope="function", params=[1000000, 2000000])
def softfork_height(request):
return request.param
block_format_version = "rc4"
+2 -1
View File
@@ -58,7 +58,7 @@ def get_future_reward_coins(block: FullBlock) -> Tuple[Coin, Coin]:
class TestCoinStoreWithBlocks:
@pytest.mark.asyncio
@pytest.mark.parametrize("cache_size", [0])
async def test_basic_coin_store(self, cache_size: uint32, db_version):
async def test_basic_coin_store(self, cache_size: uint32, db_version, softfork_height):
wallet_a = WALLET_A
reward_ph = wallet_a.get_new_puzzlehash()
@@ -107,6 +107,7 @@ class TestCoinStoreWithBlocks:
bt.constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=bt.constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
tx_removals, tx_additions = tx_removals_and_additions(npc_result.npc_list)
else:
+161 -91
View File
@@ -26,7 +26,7 @@ from chia.types.mempool_item import MempoolItem
from chia.util.clvm import int_to_bytes
from chia.util.condition_tools import conditions_for_solution, pkm_pairs
from chia.util.errors import Err
from chia.util.ints import uint64
from chia.util.ints import uint64, uint32
from chia.util.hash import std_hash
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.util.api_decorators import api_request, peer_required, bytes_required
@@ -1762,6 +1762,7 @@ def generator_condition_tester(
mempool_mode: bool = False,
quote: bool = True,
max_cost: int = MAX_BLOCK_COST_CLVM,
height: uint32,
) -> NPCResult:
prg = f"(q ((0x0101010101010101010101010101010101010101010101010101010101010101 {'(q ' if quote else ''} {conditions} {')' if quote else ''} 123 (() (q . ())))))" # noqa
print(f"program: {prg}")
@@ -1769,18 +1770,18 @@ def generator_condition_tester(
generator = BlockGenerator(program, [])
print(f"len: {len(bytes(program))}")
npc_result: NPCResult = get_name_puzzle_conditions(
generator, max_cost, cost_per_byte=COST_PER_BYTE, mempool_mode=mempool_mode
generator, max_cost, cost_per_byte=COST_PER_BYTE, mempool_mode=mempool_mode, height=height
)
return npc_result
class TestGeneratorConditions:
def test_invalid_condition_args_terminator(self):
def test_invalid_condition_args_terminator(self, softfork_height):
# note how the condition argument list isn't correctly terminated with a
# NIL atom. This is allowed, and all arguments beyond the ones we look
# at are ignored, including the termination of the list
npc_result = generator_condition_tester("(80 50 . 1)")
npc_result = generator_condition_tester("(80 50 . 1)", height=softfork_height)
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
opcode = ConditionOpcode(bytes([80]))
@@ -1790,11 +1791,31 @@ class TestGeneratorConditions:
c = npc_result.npc_list[0].conditions[0][1][0]
assert c == ConditionWithArgs(opcode=ConditionOpcode.ASSERT_SECONDS_RELATIVE, vars=[bytes([50])])
def test_invalid_condition_list_terminator(self):
@pytest.mark.parametrize(
"mempool,height,operand,expected",
[
(True, 0, -1, Err.GENERATOR_RUNTIME_ERROR.value),
(False, 1000000, -1, None),
(False, 2000000, -1, Err.GENERATOR_RUNTIME_ERROR.value),
(True, 0, 1, None),
(False, 1000000, 1, None),
(False, 2000000, 1, None),
],
)
def test_div(self, mempool, height, operand, expected):
# op_div is disallowed on negative numbers in the mempool, and after the
# softfork
npc_result = generator_condition_tester(
f"(c (c (q . 80) (c (/ (q . 50) (q . {operand})) ())) ())", quote=False, mempool_mode=mempool, height=height
)
assert npc_result.error == expected
def test_invalid_condition_list_terminator(self, softfork_height):
# note how the list of conditions isn't correctly terminated with a
# NIL atom. This is a failure
npc_result = generator_condition_tester("(80 50) . 3")
npc_result = generator_condition_tester("(80 50) . 3", height=softfork_height)
assert npc_result.error in [Err.INVALID_CONDITION.value, Err.GENERATOR_RUNTIME_ERROR.value]
@pytest.mark.parametrize(
@@ -1806,10 +1827,12 @@ class TestGeneratorConditions:
ConditionOpcode.ASSERT_SECONDS_RELATIVE,
],
)
def test_duplicate_height_time_conditions(self, opcode):
def test_duplicate_height_time_conditions(self, opcode, softfork_height):
# even though the generator outputs multiple conditions, we only
# need to return the highest one (i.e. most strict)
npc_result = generator_condition_tester(" ".join([f"({opcode.value[0]} {i})" for i in range(50, 101)]))
npc_result = generator_condition_tester(
" ".join([f"({opcode.value[0]} {i})" for i in range(50, 101)]), height=softfork_height
)
print(npc_result)
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
@@ -1827,11 +1850,11 @@ class TestGeneratorConditions:
ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT,
],
)
def test_just_announcement(self, opcode):
def test_just_announcement(self, opcode, softfork_height):
message = "a" * 1024
# announcements are validated on the Rust side and never returned
# back. They are either satisified or cause an immediate failure
npc_result = generator_condition_tester(f'({opcode.value[0]} "{message}") ' * 50)
npc_result = generator_condition_tester(f'({opcode.value[0]} "{message}") ' * 50, height=softfork_height)
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
# create-announcements and assert-announcements are dropped once
@@ -1845,23 +1868,23 @@ class TestGeneratorConditions:
ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT,
],
)
def test_assert_announcement_fail(self, opcode):
def test_assert_announcement_fail(self, opcode, softfork_height):
message = "a" * 1024
# announcements are validated on the Rust side and never returned
# back. They ar either satisified or cause an immediate failure
# in this test we just assert announcements, we never make them, so
# these should fail
npc_result = generator_condition_tester(f'({opcode.value[0]} "{message}") ')
npc_result = generator_condition_tester(f'({opcode.value[0]} "{message}") ', height=softfork_height)
print(npc_result)
assert npc_result.error == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED.value
assert npc_result.npc_list == []
def test_multiple_reserve_fee(self):
def test_multiple_reserve_fee(self, softfork_height):
# RESERVE_FEE
cond = 52
# even though the generator outputs 3 conditions, we only need to return one copy
# with all the fees accumulated
npc_result = generator_condition_tester(f"({cond} 100) " * 3)
npc_result = generator_condition_tester(f"({cond} 100) " * 3, height=softfork_height)
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
opcode = ConditionOpcode(bytes([cond]))
@@ -1875,23 +1898,25 @@ class TestGeneratorConditions:
assert reserve_fee == 300
assert len(npc_result.npc_list[0].conditions[0][1]) == 1
def test_duplicate_outputs(self):
def test_duplicate_outputs(self, softfork_height):
# CREATE_COIN
# creating multiple coins with the same properties (same parent, same
# target puzzle hash and same amount) is not allowed. That's a consensus
# failure.
puzzle_hash = "abababababababababababababababab"
npc_result = generator_condition_tester(f'(51 "{puzzle_hash}" 10) ' * 2)
npc_result = generator_condition_tester(f'(51 "{puzzle_hash}" 10) ' * 2, height=softfork_height)
assert npc_result.error == Err.DUPLICATE_OUTPUT.value
assert npc_result.npc_list == []
def test_create_coin_cost(self):
def test_create_coin_cost(self, softfork_height):
# CREATE_COIN
puzzle_hash = "abababababababababababababababab"
# this max cost is exactly enough for the create coin condition
npc_result = generator_condition_tester(
f'(51 "{puzzle_hash}" 10) ', max_cost=20470 + 95 * COST_PER_BYTE + ConditionCost.CREATE_COIN.value
f'(51 "{puzzle_hash}" 10) ',
max_cost=20470 + 95 * COST_PER_BYTE + ConditionCost.CREATE_COIN.value,
height=softfork_height,
)
assert npc_result.error is None
assert npc_result.cost == 20470 + 95 * COST_PER_BYTE + ConditionCost.CREATE_COIN.value
@@ -1899,17 +1924,21 @@ class TestGeneratorConditions:
# if we subtract one from max cost, this should fail
npc_result = generator_condition_tester(
f'(51 "{puzzle_hash}" 10) ', max_cost=20470 + 95 * COST_PER_BYTE + ConditionCost.CREATE_COIN.value - 1
f'(51 "{puzzle_hash}" 10) ',
max_cost=20470 + 95 * COST_PER_BYTE + ConditionCost.CREATE_COIN.value - 1,
height=softfork_height,
)
assert npc_result.error in [Err.BLOCK_COST_EXCEEDS_MAX.value, Err.INVALID_BLOCK_COST.value]
def test_agg_sig_cost(self):
def test_agg_sig_cost(self, softfork_height):
# AGG_SIG_ME
pubkey = "abababababababababababababababababababababababab"
# this max cost is exactly enough for the AGG_SIG condition
npc_result = generator_condition_tester(
f'(49 "{pubkey}" "foobar") ', max_cost=20512 + 117 * COST_PER_BYTE + ConditionCost.AGG_SIG.value
f'(49 "{pubkey}" "foobar") ',
max_cost=20512 + 117 * COST_PER_BYTE + ConditionCost.AGG_SIG.value,
height=softfork_height,
)
assert npc_result.error is None
assert npc_result.cost == 20512 + 117 * COST_PER_BYTE + ConditionCost.AGG_SIG.value
@@ -1917,11 +1946,13 @@ class TestGeneratorConditions:
# if we subtract one from max cost, this should fail
npc_result = generator_condition_tester(
f'(49 "{pubkey}" "foobar") ', max_cost=20512 + 117 * COST_PER_BYTE + ConditionCost.AGG_SIG.value - 1
f'(49 "{pubkey}" "foobar") ',
max_cost=20512 + 117 * COST_PER_BYTE + ConditionCost.AGG_SIG.value - 1,
height=softfork_height,
)
assert npc_result.error in [Err.BLOCK_COST_EXCEEDS_MAX.value, Err.INVALID_BLOCK_COST.value]
def test_create_coin_different_parent(self):
def test_create_coin_different_parent(self, softfork_height):
# if the coins we create have different parents, they are never
# considered duplicate, even when they have the same puzzle hash and
@@ -1934,7 +1965,7 @@ class TestGeneratorConditions:
)
generator = BlockGenerator(program, [])
npc_result: NPCResult = get_name_puzzle_conditions(
generator, MAX_BLOCK_COST_CLVM, cost_per_byte=COST_PER_BYTE, mempool_mode=False
generator, MAX_BLOCK_COST_CLVM, cost_per_byte=COST_PER_BYTE, mempool_mode=False, height=softfork_height
)
assert npc_result.error is None
assert len(npc_result.npc_list) == 2
@@ -1947,12 +1978,14 @@ class TestGeneratorConditions:
)
]
def test_create_coin_different_puzzhash(self):
def test_create_coin_different_puzzhash(self, softfork_height):
# CREATE_COIN
# coins with different puzzle hashes are not considered duplicate
puzzle_hash_1 = "abababababababababababababababab"
puzzle_hash_2 = "cbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcb"
npc_result = generator_condition_tester(f'(51 "{puzzle_hash_1}" 5) (51 "{puzzle_hash_2}" 5)')
npc_result = generator_condition_tester(
f'(51 "{puzzle_hash_1}" 5) (51 "{puzzle_hash_2}" 5)', height=softfork_height
)
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
opcode = ConditionOpcode.CREATE_COIN
@@ -1965,11 +1998,13 @@ class TestGeneratorConditions:
in npc_result.npc_list[0].conditions[0][1]
)
def test_create_coin_different_amounts(self):
def test_create_coin_different_amounts(self, softfork_height):
# CREATE_COIN
# coins with different amounts are not considered duplicate
puzzle_hash = "abababababababababababababababab"
npc_result = generator_condition_tester(f'(51 "{puzzle_hash}" 5) (51 "{puzzle_hash}" 4)')
npc_result = generator_condition_tester(
f'(51 "{puzzle_hash}" 5) (51 "{puzzle_hash}" 4)', height=softfork_height
)
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
opcode = ConditionOpcode.CREATE_COIN
@@ -1982,11 +2017,11 @@ class TestGeneratorConditions:
in npc_result.npc_list[0].conditions[0][1]
)
def test_create_coin_with_hint(self):
def test_create_coin_with_hint(self, softfork_height):
# CREATE_COIN
puzzle_hash_1 = "abababababababababababababababab"
hint = "12341234123412341234213421341234"
npc_result = generator_condition_tester(f'(51 "{puzzle_hash_1}" 5 ("{hint}"))')
npc_result = generator_condition_tester(f'(51 "{puzzle_hash_1}" 5 ("{hint}"))', height=softfork_height)
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
opcode = ConditionOpcode.CREATE_COIN
@@ -1995,14 +2030,18 @@ class TestGeneratorConditions:
)
@pytest.mark.parametrize(
"mempool_mode",
[True, False],
"mempool,height",
[
(True, None),
(False, 1000000),
(False, 2000000),
],
)
def test_unknown_condition(self, mempool_mode):
def test_unknown_condition(self, mempool, height):
for c in ['(1 100 "foo" "bar")', "(100)", "(1 1) (2 2) (3 3)", '("foobar")']:
npc_result = generator_condition_tester(c, mempool_mode=mempool_mode)
npc_result = generator_condition_tester(c, mempool_mode=mempool, height=height)
print(npc_result)
if mempool_mode:
if mempool:
assert npc_result.error == Err.INVALID_CONDITION.value
assert npc_result.npc_list == []
else:
@@ -2104,6 +2143,22 @@ CREATE_COIN = '(a (q 2 2 (c 2 (c (q 51 "abababababababababababababababab" 1) (c
CREATE_UNIQUE_COINS = '(a (q 2 6 (c 2 (c (q 51 "abababababababababababababababab") (c 5 ())))) (c (q (a (i 5 (q 4 9 (a 4 (c 2 (c 13 (c 11 ()))))) (q 4 11 ())) 1) 2 (i 11 (q 4 (a 4 (c 2 (c 5 (c 11 ())))) (a 6 (c 2 (c 5 (c (- 11 (q . 1)) ()))))) ()) 1) (q {num})))' # noqa
# some of the malicious tests will fail post soft-fork, this function helps test
# the specific error to expect
def error_for_condition(cond: ConditionOpcode) -> int:
if cond == ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE:
return Err.ASSERT_HEIGHT_ABSOLUTE_FAILED.value
if cond == ConditionOpcode.ASSERT_HEIGHT_RELATIVE:
return Err.ASSERT_HEIGHT_RELATIVE_FAILED.value
if cond == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE:
return Err.ASSERT_SECONDS_ABSOLUTE_FAILED.value
if cond == ConditionOpcode.ASSERT_SECONDS_RELATIVE:
return Err.ASSERT_SECONDS_RELATIVE_FAILED.value
if cond == ConditionOpcode.RESERVE_FEE:
return Err.RESERVE_FEE_CONDITION_FAILED.value
assert False
class TestMaliciousGenerators:
# TODO: create a lot of announcements. The messages can be made different by
@@ -2120,19 +2175,22 @@ class TestMaliciousGenerators:
ConditionOpcode.ASSERT_SECONDS_RELATIVE,
],
)
def test_duplicate_large_integer_ladder(self, opcode):
def test_duplicate_large_integer_ladder(self, opcode, softfork_height):
condition = SINGLE_ARG_INT_LADDER_COND.format(opcode=opcode.value[0], num=28, filler="0x00")
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode,
[ConditionWithArgs(opcode, [int_to_bytes(28)])],
)
]
if softfork_height >= 2000000:
assert npc_result.error == error_for_condition(opcode)
else:
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode,
[ConditionWithArgs(opcode, [int_to_bytes(28)])],
)
]
assert run_time < 1.5
print(f"run time:{run_time}")
@@ -2145,19 +2203,22 @@ class TestMaliciousGenerators:
ConditionOpcode.ASSERT_SECONDS_RELATIVE,
],
)
def test_duplicate_large_integer(self, opcode):
def test_duplicate_large_integer(self, opcode, softfork_height):
condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0x00")
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode,
[ConditionWithArgs(opcode, [bytes([100])])],
)
]
if softfork_height >= 2000000:
assert npc_result.error == error_for_condition(opcode)
else:
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode,
[ConditionWithArgs(opcode, [bytes([100])])],
)
]
assert run_time < 2.5
print(f"run time:{run_time}")
@@ -2170,19 +2231,22 @@ class TestMaliciousGenerators:
ConditionOpcode.ASSERT_SECONDS_RELATIVE,
],
)
def test_duplicate_large_integer_substr(self, opcode):
def test_duplicate_large_integer_substr(self, opcode, softfork_height):
condition = SINGLE_ARG_INT_SUBSTR_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0x00")
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode,
[ConditionWithArgs(opcode, [bytes([100])])],
)
]
if softfork_height >= 2000000:
assert npc_result.error == error_for_condition(opcode)
else:
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode,
[ConditionWithArgs(opcode, [bytes([100])])],
)
]
assert run_time < 3
print(f"run time:{run_time}")
@@ -2195,18 +2259,21 @@ class TestMaliciousGenerators:
ConditionOpcode.ASSERT_SECONDS_RELATIVE,
],
)
def test_duplicate_large_integer_substr_tail(self, opcode):
def test_duplicate_large_integer_substr_tail(self, opcode, softfork_height):
condition = SINGLE_ARG_INT_SUBSTR_TAIL_COND.format(
opcode=opcode.value[0], num=280, val="0xffffffff", filler="0x00"
)
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
if softfork_height >= 2000000:
assert npc_result.error == error_for_condition(opcode)
else:
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
print(npc_result.npc_list[0].conditions[0][1])
assert ConditionWithArgs(opcode, [int_to_bytes(0xFFFFFFFF)]) in npc_result.npc_list[0].conditions[0][1]
print(npc_result.npc_list[0].conditions[0][1])
assert ConditionWithArgs(opcode, [int_to_bytes(0xFFFFFFFF)]) in npc_result.npc_list[0].conditions[0][1]
assert run_time < 1
print(f"run time:{run_time}")
@@ -2219,10 +2286,10 @@ class TestMaliciousGenerators:
ConditionOpcode.ASSERT_SECONDS_RELATIVE,
],
)
def test_duplicate_large_integer_negative(self, opcode):
def test_duplicate_large_integer_negative(self, opcode, softfork_height):
condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0xff")
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
@@ -2230,28 +2297,31 @@ class TestMaliciousGenerators:
assert run_time < 2
print(f"run time:{run_time}")
def test_duplicate_reserve_fee(self):
def test_duplicate_reserve_fee(self, softfork_height):
opcode = ConditionOpcode.RESERVE_FEE
condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0x00")
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode.value,
[ConditionWithArgs(opcode, [int_to_bytes(100 * 280000)])],
)
]
if softfork_height >= 2000000:
assert npc_result.error == error_for_condition(opcode)
else:
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
assert npc_result.npc_list[0].conditions == [
(
opcode.value,
[ConditionWithArgs(opcode, [int_to_bytes(100 * 280000)])],
)
]
assert run_time < 2
print(f"run time:{run_time}")
def test_duplicate_reserve_fee_negative(self):
def test_duplicate_reserve_fee_negative(self, softfork_height):
opcode = ConditionOpcode.RESERVE_FEE
condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=200000, val=100, filler="0xff")
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
# RESERVE_FEE conditions fail unconditionally if they have a negative
# amount
@@ -2263,10 +2333,10 @@ class TestMaliciousGenerators:
@pytest.mark.parametrize(
"opcode", [ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT]
)
def test_duplicate_coin_announces(self, opcode):
def test_duplicate_coin_announces(self, opcode, softfork_height):
condition = CREATE_ANNOUNCE_COND.format(opcode=opcode.value[0], num=5950000)
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
@@ -2276,28 +2346,28 @@ class TestMaliciousGenerators:
assert run_time < 21
print(f"run time:{run_time}")
def test_create_coin_duplicates(self):
def test_create_coin_duplicates(self, softfork_height):
# CREATE_COIN
# this program will emit 6000 identical CREATE_COIN conditions. However,
# we'll just end up looking at two of them, and fail at the first
# duplicate
condition = CREATE_COIN.format(num=600000)
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error == Err.DUPLICATE_OUTPUT.value
assert len(npc_result.npc_list) == 0
assert run_time < 2
print(f"run time:{run_time}")
def test_many_create_coin(self):
def test_many_create_coin(self, softfork_height):
# CREATE_COIN
# this program will emit many CREATE_COIN conditions, all with different
# amounts.
# the number 6095 was chosen carefully to not exceed the maximum cost
condition = CREATE_UNIQUE_COINS.format(num=6094)
start_time = time()
npc_result = generator_condition_tester(condition, quote=False)
npc_result = generator_condition_tester(condition, quote=False, height=softfork_height)
run_time = time() - start_time
assert npc_result.error is None
assert len(npc_result.npc_list) == 1
+20 -7
View File
@@ -54,7 +54,7 @@ def large_block_generator(size):
class TestCostCalculation:
@pytest.mark.asyncio
async def test_basics(self):
async def test_basics(self, softfork_height):
wallet_tool = bt.get_pool_wallet_tool()
ph = wallet_tool.get_new_puzzlehash()
num_blocks = 3
@@ -80,6 +80,7 @@ class TestCostCalculation:
test_constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=test_constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
cost = calculate_cost_of_program(program.program, npc_result, test_constants.COST_PER_BYTE)
@@ -111,7 +112,7 @@ class TestCostCalculation:
)
@pytest.mark.asyncio
async def test_mempool_mode(self):
async def test_mempool_mode(self, softfork_height):
wallet_tool = bt.get_pool_wallet_tool()
ph = wallet_tool.get_new_puzzlehash()
@@ -150,6 +151,7 @@ class TestCostCalculation:
test_constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=test_constants.COST_PER_BYTE,
mempool_mode=True,
height=softfork_height,
)
assert npc_result.error is not None
npc_result = get_name_puzzle_conditions(
@@ -157,6 +159,7 @@ class TestCostCalculation:
test_constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=test_constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
assert npc_result.error is None
@@ -167,7 +170,7 @@ class TestCostCalculation:
assert error is None
@pytest.mark.asyncio
async def test_clvm_mempool_mode(self):
async def test_clvm_mempool_mode(self, softfork_height):
block = Program.from_bytes(bytes(SMALL_BLOCK_GENERATOR.program))
disassembly = binutils.disassemble(block)
# this is a valid generator program except the first clvm
@@ -188,11 +191,12 @@ class TestCostCalculation:
test_constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=test_constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
assert npc_result.error is None
@pytest.mark.asyncio
async def test_tx_generator_speed(self):
async def test_tx_generator_speed(self, softfork_height):
LARGE_BLOCK_COIN_CONSUMED_COUNT = 687
generator_bytes = large_block_generator(LARGE_BLOCK_COIN_CONSUMED_COUNT)
program = SerializedProgram.from_bytes(generator_bytes)
@@ -204,6 +208,7 @@ class TestCostCalculation:
test_constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=test_constants.COST_PER_BYTE,
mempool_mode=False,
height=softfork_height,
)
end_time = time.time()
duration = end_time - start_time
@@ -214,7 +219,7 @@ class TestCostCalculation:
assert duration < 1
@pytest.mark.asyncio
async def test_clvm_max_cost(self):
async def test_clvm_max_cost(self, softfork_height):
block = Program.from_bytes(bytes(SMALL_BLOCK_GENERATOR.program))
disassembly = binutils.disassemble(block)
@@ -229,14 +234,22 @@ class TestCostCalculation:
# ensure we fail if the program exceeds the cost
generator = BlockGenerator(program, [])
npc_result: NPCResult = get_name_puzzle_conditions(generator, 10000000, cost_per_byte=0, mempool_mode=False)
npc_result: NPCResult = get_name_puzzle_conditions(
generator,
10000000,
cost_per_byte=0,
mempool_mode=False,
height=softfork_height,
)
assert npc_result.error is not None
assert npc_result.cost == 0
# raise the max cost to make sure this passes
# ensure we pass if the program does not exceeds the cost
npc_result = get_name_puzzle_conditions(generator, 20000000, cost_per_byte=0, mempool_mode=False)
npc_result = get_name_puzzle_conditions(
generator, 20000000, cost_per_byte=0, mempool_mode=False, height=softfork_height
)
assert npc_result.error is None
assert npc_result.cost > 10000000
+4 -2
View File
@@ -94,14 +94,16 @@ class TestROM:
assert cost == EXPECTED_ABBREVIATED_COST
assert r.as_bin().hex() == EXPECTED_OUTPUT
def test_get_name_puzzle_conditions(self):
def test_get_name_puzzle_conditions(self, softfork_height):
# this tests that extra block or coin data doesn't confuse `get_name_puzzle_conditions`
gen = block_generator()
cost, r = run_generator_unsafe(gen, max_cost=MAX_COST)
print(r)
npc_result = get_name_puzzle_conditions(gen, max_cost=MAX_COST, cost_per_byte=COST_PER_BYTE, mempool_mode=False)
npc_result = get_name_puzzle_conditions(
gen, max_cost=MAX_COST, cost_per_byte=COST_PER_BYTE, mempool_mode=False, height=softfork_height
)
assert npc_result.error is None
assert npc_result.cost == EXPECTED_COST + ConditionCost.CREATE_COIN.value + (
len(bytes(gen.program)) * COST_PER_BYTE
+3 -1
View File
@@ -6,10 +6,11 @@ from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.full_block import FullBlock
from chia.types.generator_types import BlockGenerator
from chia.util.generator_tools import additions_for_npc
from chia.util.ints import uint32
def run_and_get_removals_and_additions(
block: FullBlock, max_cost: int, cost_per_byte: int, mempool_mode=False
block: FullBlock, max_cost: int, *, cost_per_byte: int, height: uint32, mempool_mode=False
) -> Tuple[List[bytes32], List[Coin]]:
removals: List[bytes32] = []
additions: List[Coin] = []
@@ -24,6 +25,7 @@ def run_and_get_removals_and_additions(
max_cost,
cost_per_byte=cost_per_byte,
mempool_mode=mempool_mode,
height=height,
)
# build removals list
for npc in npc_result.npc_list:
+24 -6
View File
@@ -41,6 +41,8 @@ from typing import List, TextIO, Tuple, Dict
import click
from clvm_rs import COND_CANON_INTS, NO_NEG_DIV
from chia.consensus.constants import ConsensusConstants
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.types.blockchain_format.program import SerializedProgram
@@ -87,9 +89,18 @@ def npc_to_dict(npc: NPC):
}
def run_generator(block_generator: BlockGenerator, constants: ConsensusConstants, max_cost: int) -> List[CAT]:
def run_generator(
block_generator: BlockGenerator, constants: ConsensusConstants, max_cost: int, height: uint32
) -> List[CAT]:
if height >= DEFAULT_CONSTANTS.SOFT_FORK_HEIGHT:
# conditions must use integers in canonical encoding (i.e. no redundant
# leading zeros)
# the division operator may not be used with negative operands
flags = COND_CANON_INTS | NO_NEG_DIV
else:
flags = 0
flags = 0
_, result = block_generator.program.run_with_cost(max_cost, flags, block_generator.generator_refs())
coin_spends = result.first()
@@ -171,17 +182,23 @@ def run_full_block(block: FullBlock, constants: ConsensusConstants) -> List[CAT]
if block.transactions_generator is None or block.transactions_info is None:
raise RuntimeError("transactions_generator of FullBlock is null")
block_generator = BlockGenerator(block.transactions_generator, generator_args)
return run_generator(block_generator, constants, min(constants.MAX_BLOCK_COST_CLVM, block.transactions_info.cost))
return run_generator(
block_generator, constants, min(constants.MAX_BLOCK_COST_CLVM, block.transactions_info.cost), block.height
)
def run_generator_with_args(
generator_program_hex: str, generator_args: List[GeneratorArg], constants: ConsensusConstants, cost: uint64
generator_program_hex: str,
generator_args: List[GeneratorArg],
constants: ConsensusConstants,
cost: uint64,
height: uint32,
) -> List[CAT]:
if not generator_program_hex:
return []
generator_program = SerializedProgram.fromhex(generator_program_hex)
block_generator = BlockGenerator(generator_program, generator_args)
return run_generator(block_generator, constants, min(constants.MAX_BLOCK_COST_CLVM, cost))
return run_generator(block_generator, constants, min(constants.MAX_BLOCK_COST_CLVM, cost), height)
@click.command()
@@ -195,11 +212,12 @@ def run_json_block(full_block, constants: ConsensusConstants) -> List[CAT]:
ref_list = full_block["block"]["transactions_generator_ref_list"]
tx_info: dict = full_block["block"]["transactions_info"]
generator_program_hex: str = full_block["block"]["transactions_generator"]
height = full_block["block"]["reward_chain_block"]["height"]
cat_list: List[CAT] = []
if tx_info and generator_program_hex:
cost = tx_info["cost"]
args = ref_list_to_args(ref_list)
cat_list = run_generator_with_args(generator_program_hex, args, constants, cost)
cat_list = run_generator_with_args(generator_program_hex, args, constants, cost, height)
return cat_list