enable soft-fork2 conditions (incl. ASSERT_BEFORE) (#14733)

* enable soft-fork2

* add blockchain (consensus) test for time-lock conditions (non-ephemeral spend)

* introduce new soft-fork rule to compare ASSERT_SECONDS_* conditions against the previous transaction block's timestamp (to be consistent with ASSERT_HEIGHT_* conditions)

* bump chia_rs. This updates the mempool rules to disallow relative height- and time conditions on ephemeral coin spends

* implement assert_before in mempool_check_time_locks. Extend ephemeral coin test in blockchain with assert_before conditions

* implement support for assert_before conditions in compute_assert_height()

* support assert-before in mempool

* add timelock rule

* address review comments
This commit is contained in:
Arvid Norberg 2023-03-23 17:30:10 +01:00 committed by GitHub
parent 15d338976d
commit 8dbfc4840a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 786 additions and 106 deletions

View File

@ -65,7 +65,11 @@ class CostLogger:
def add_cost(self, descriptor: str, spend_bundle: SpendBundle) -> SpendBundle:
program: BlockGenerator = simple_solution_generator(spend_bundle)
npc_result: NPCResult = get_name_puzzle_conditions(
program, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=True
program,
INFINITE_COST,
cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE,
mempool_mode=True,
height=DEFAULT_CONSTANTS.SOFT_FORK2_HEIGHT,
)
self.cost_dict[descriptor] = npc_result.cost
cost_to_subtract: int = 0

View File

@ -59,6 +59,7 @@ async def validate_block_body(
if isinstance(block, FullBlock):
assert height == block.height
prev_transaction_block_height: uint32 = uint32(0)
prev_transaction_block_timestamp: uint64 = uint64(0)
# 1. For non transaction-blocs: foliage block, transaction filter, transactions info, and generator must
# be empty. If it is a block but not a transaction block, there is no body to validate. Check that all fields are
@ -103,6 +104,8 @@ async def validate_block_body(
# Add reward claims for all blocks from the prev prev block, until the prev block (including the latter)
prev_transaction_block = blocks.block_record(block.foliage_transaction_block.prev_transaction_block_hash)
prev_transaction_block_height = prev_transaction_block.height
assert prev_transaction_block.timestamp
prev_transaction_block_timestamp = prev_transaction_block.timestamp
assert prev_transaction_block.fees is not None
pool_coin = create_pool_coin(
prev_transaction_block_height,
@ -457,11 +460,18 @@ async def validate_block_body(
# verify absolute/relative height/time conditions
if npc_result is not None:
assert npc_result.conds is not None
block_timestamp: uint64
if height < constants.SOFT_FORK2_HEIGHT:
block_timestamp = block.foliage_transaction_block.timestamp
else:
block_timestamp = prev_transaction_block_timestamp
error = mempool_check_time_locks(
removal_coin_records,
npc_result.conds,
prev_transaction_block_height,
block.foliage_transaction_block.timestamp,
block_timestamp,
)
if error:
return error, None

View File

@ -135,6 +135,7 @@ def create_foliage(
constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=constants.COST_PER_BYTE,
mempool_mode=True,
height=height,
)
cost = result.cost

View File

@ -56,8 +56,7 @@ default_kwargs = {
"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": 3630000,
# the soft-fork 2 is disabled (for now)
"SOFT_FORK2_HEIGHT": 3830000,
"SOFT_FORK2_HEIGHT": 4000000,
}

View File

@ -29,6 +29,7 @@ class MempoolRemoveReason(Enum):
CONFLICT = 1
BLOCK_INCLUSION = 2
POOL_FULL = 3
EXPIRED = 4
@dataclass(frozen=True)
@ -61,12 +62,20 @@ class Mempool:
cost INT NOT NULL,
fee INT NOT NULL,
assert_height INT,
assert_before_height INT,
assert_before_seconds INT,
fee_per_cost REAL{generated})
"""
)
self._db_conn.execute("CREATE INDEX fee_sum ON tx(fee)")
self._db_conn.execute("CREATE INDEX cost_sum ON tx(cost)")
self._db_conn.execute("CREATE INDEX feerate ON tx(fee_per_cost)")
self._db_conn.execute(
"CREATE INDEX assert_before_height ON tx(assert_before_height) WHERE assert_before_height != NULL"
)
self._db_conn.execute(
"CREATE INDEX assert_before_seconds ON tx(assert_before_seconds) WHERE assert_before_seconds != NULL"
)
# This table maps coin IDs to spend bundles hashes
self._db_conn.execute(
@ -174,6 +183,21 @@ class Mempool:
else:
return 0
def new_tx_block(self, block_height: uint32, timestamp: uint64) -> None:
"""
Remove all items that became invalid because of this new height and
timestamp. (we don't know about which coins were spent in this new block
here, so those are handled separately)
"""
with self._db_conn:
cursor = self._db_conn.execute(
"SELECT name FROM tx WHERE assert_before_seconds <= ? OR assert_before_height <= ?",
(timestamp, block_height),
)
to_remove = [bytes32(row[0]) for row in cursor]
self.remove_from_pool(to_remove, MempoolRemoveReason.EXPIRED)
def remove_from_pool(self, items: List[bytes32], reason: MempoolRemoveReason) -> None:
"""
Removes an item from the mempool.
@ -232,12 +256,28 @@ class Mempool:
if SQLITE_NO_GENERATED_COLUMNS:
self._db_conn.execute(
"INSERT INTO tx VALUES(?, ?, ?, ?, ?)",
(item.name, item.cost, item.fee, item.assert_height, item.fee / item.cost),
"INSERT INTO tx VALUES(?, ?, ?, ?, ?, ?, ?)",
(
item.name,
item.cost,
item.fee,
item.assert_height,
item.assert_before_height,
item.assert_before_seconds,
item.fee / item.cost,
),
)
else:
self._db_conn.execute(
"INSERT INTO tx VALUES(?, ?, ?, ?)", (item.name, item.cost, item.fee, item.assert_height)
"INSERT INTO tx VALUES(?, ?, ?, ?, ?, ?)",
(
item.name,
item.cost,
item.fee,
item.assert_height,
item.assert_before_height,
item.assert_before_seconds,
),
)
all_coin_spends = [(s.coin_id, item.name) for s in item.npc_result.conds.spends]

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from typing import Dict, List, Optional, Tuple
from chia_rs import LIMIT_STACK, MEMPOOL_MODE
from chia_rs import ENABLE_ASSERT_BEFORE, LIMIT_STACK, 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
@ -39,23 +39,19 @@ def get_name_puzzle_conditions(
*,
cost_per_byte: int,
mempool_mode: bool,
height: Optional[uint32] = None,
height: uint32,
constants: ConsensusConstants = DEFAULT_CONSTANTS,
) -> NPCResult:
# 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
if mempool_mode:
flags = MEMPOOL_MODE
elif height is not None and height >= constants.SOFT_FORK_HEIGHT:
elif height >= constants.SOFT_FORK_HEIGHT:
flags = LIMIT_STACK
else:
flags = 0
# soft-fork2 is disabled (for now)
# if height is not None and height >= constants.SOFT_FORK2_HEIGHT:
# flags = flags | ENABLE_ASSERT_BEFORE
if height >= constants.SOFT_FORK2_HEIGHT:
flags = flags | ENABLE_ASSERT_BEFORE | NO_RELATIVE_CONDITIONS_ON_EPHEMERAL
try:
block_args = [bytes(gen) for gen in generator.generator_refs]
@ -134,6 +130,12 @@ def mempool_check_time_locks(
return Err.ASSERT_HEIGHT_ABSOLUTE_FAILED
if timestamp < bundle_conds.seconds_absolute:
return Err.ASSERT_SECONDS_ABSOLUTE_FAILED
if bundle_conds.before_height_absolute is not None:
if prev_transaction_block_height >= bundle_conds.before_height_absolute:
return Err.ASSERT_BEFORE_HEIGHT_ABSOLUTE_FAILED
if bundle_conds.before_seconds_absolute is not None:
if timestamp >= bundle_conds.before_seconds_absolute:
return Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED
for spend in bundle_conds.spends:
unspent = removal_coin_records[bytes32(spend.coin_id)]
@ -149,4 +151,11 @@ def mempool_check_time_locks(
if spend.seconds_relative is not None:
if timestamp < unspent.timestamp + spend.seconds_relative:
return Err.ASSERT_SECONDS_RELATIVE_FAILED
if spend.before_height_relative is not None:
if prev_transaction_block_height >= unspent.confirmed_block_index + spend.before_height_relative:
return Err.ASSERT_BEFORE_HEIGHT_RELATIVE_FAILED
if spend.before_seconds_relative is not None:
if timestamp >= unspent.timestamp + spend.before_seconds_relative:
return Err.ASSERT_BEFORE_SECONDS_RELATIVE_FAILED
return None

View File

@ -5,8 +5,9 @@ import logging
import time
from concurrent.futures import Executor
from concurrent.futures.process import ProcessPoolExecutor
from dataclasses import dataclass
from multiprocessing.context import BaseContext
from typing import Awaitable, Callable, Dict, List, Optional, Set, Tuple
from typing import Awaitable, Callable, Dict, List, Optional, Set, Tuple, TypeVar
from blspy import GTElement
from chiabip158 import PyBIP158
@ -93,25 +94,54 @@ def validate_clvm_and_signature(
return None, bytes(result), new_cache_entries
@dataclass
class TimelockConditions:
assert_height: uint32 = uint32(0)
assert_before_height: Optional[uint32] = None
assert_before_seconds: Optional[uint64] = None
def compute_assert_height(
removal_coin_records: Dict[bytes32, CoinRecord],
conds: SpendBundleConditions,
) -> uint32:
) -> TimelockConditions:
"""
Computes the most restrictive height assertion in the spend bundle. Relative
height assertions are resolved using the confirmed heights from the coin
records.
Computes the most restrictive height- and seconds assertion in the spend bundle.
Relative heights and times are resolved using the confirmed heights and
timestamps from the coin records.
"""
height: uint32 = uint32(conds.height_absolute)
ret = TimelockConditions()
ret.assert_height = uint32(conds.height_absolute)
ret.assert_before_height = (
uint32(conds.before_height_absolute) if conds.before_height_absolute is not None else None
)
ret.assert_before_seconds = (
uint64(conds.before_seconds_absolute) if conds.before_seconds_absolute is not None else None
)
for spend in conds.spends:
if spend.height_relative is None:
continue
h = uint32(removal_coin_records[bytes32(spend.coin_id)].confirmed_block_index + spend.height_relative)
height = max(height, h)
if spend.height_relative is not None:
h = uint32(removal_coin_records[bytes32(spend.coin_id)].confirmed_block_index + spend.height_relative)
ret.assert_height = max(ret.assert_height, h)
return height
if spend.before_height_relative is not None:
h = uint32(
removal_coin_records[bytes32(spend.coin_id)].confirmed_block_index + spend.before_height_relative
)
if ret.assert_before_height is not None:
ret.assert_before_height = min(ret.assert_before_height, h)
else:
ret.assert_before_height = h
if spend.before_seconds_relative is not None:
s = uint64(removal_coin_records[bytes32(spend.coin_id)].timestamp + spend.before_seconds_relative)
if ret.assert_before_seconds is not None:
ret.assert_before_seconds = min(ret.assert_before_seconds, s)
else:
ret.assert_before_seconds = s
return ret
class MempoolManager:
@ -472,23 +502,31 @@ class MempoolManager:
log.warning(f"{spend.puzzle_hash.hex()} != {coin_record.coin.puzzle_hash.hex()}")
return Err.WRONG_PUZZLE_HASH, None, []
chialisp_height = (
self.peak.prev_transaction_block_height if not self.peak.is_transaction_block else self.peak.height
)
# the height and time we pass in here represent the previous transaction
# block's height and timestamp. In the mempool, the most recent peak
# block we've received will be the previous transaction block, from the
# point-of-view of the next block to be farmed. Therefore we pass in the
# current peak's height and timestamp
assert self.peak.timestamp is not None
tl_error: Optional[Err] = mempool_check_time_locks(
removal_record_dict,
npc_result.conds,
uint32(chialisp_height),
self.peak.height,
self.peak.timestamp,
)
assert_height: Optional[uint32] = None
if tl_error:
assert_height = compute_assert_height(removal_record_dict, npc_result.conds)
timelocks: TimelockConditions = compute_assert_height(removal_record_dict, npc_result.conds)
potential = MempoolItem(new_spend, uint64(fees), npc_result, spend_name, first_added_height, assert_height)
potential = MempoolItem(
new_spend,
uint64(fees),
npc_result,
spend_name,
first_added_height,
timelocks.assert_height,
timelocks.assert_before_height,
timelocks.assert_before_seconds,
)
if tl_error:
if tl_error is Err.ASSERT_HEIGHT_ABSOLUTE_FAILED or tl_error is Err.ASSERT_HEIGHT_RELATIVE_FAILED:
@ -567,6 +605,7 @@ class MempoolManager:
"""
if new_peak is None:
return []
# we're only interested in transaction blocks
if new_peak.is_transaction_block is False:
return []
if self.peak == new_peak:
@ -575,6 +614,8 @@ class MempoolManager:
self.fee_estimator.new_block_height(new_peak.height)
included_items: List[MempoolItemInfo] = []
self.mempool.new_tx_block(new_peak.height, new_peak.timestamp)
use_optimization: bool = self.peak is not None and new_peak.prev_transaction_block_hash == self.peak.header_hash
self.peak = new_peak
@ -644,6 +685,17 @@ class MempoolManager:
return items
T = TypeVar("T", uint32, uint64)
def optional_min(a: Optional[T], b: Optional[T]) -> Optional[T]:
return min((v for v in [a, b] if v is not None), default=None)
def optional_max(a: Optional[T], b: Optional[T]) -> Optional[T]:
return max((v for v in [a, b] if v is not None), default=None)
def can_replace(
conflicting_items: Set[MempoolItem],
removal_names: Set[bytes32],
@ -659,6 +711,9 @@ def can_replace(
conflicting_fees = 0
conflicting_cost = 0
assert_height: Optional[uint32] = None
assert_before_height: Optional[uint32] = None
assert_before_seconds: Optional[uint64] = None
for item in conflicting_items:
conflicting_fees += item.fee
conflicting_cost += item.cost
@ -673,6 +728,10 @@ def can_replace(
log.debug(f"Rejecting conflicting tx as it does not spend conflicting coin {coin.name()}")
return False
assert_height = optional_max(assert_height, item.assert_height)
assert_before_height = optional_min(assert_before_height, item.assert_before_height)
assert_before_seconds = optional_min(assert_before_seconds, item.assert_before_seconds)
# New item must have higher fee per cost
conflicting_fees_per_cost = conflicting_fees / conflicting_cost
if new_item.fee_per_cost <= conflicting_fees_per_cost:
@ -688,5 +747,30 @@ def can_replace(
log.debug(f"Rejecting conflicting tx due to low fee increase ({fee_increase})")
return False
# New item may not have a different effective height/time lock (time-lock rule)
if new_item.assert_height != assert_height:
log.debug(
"Rejecting conflicting tx due to changing ASSERT_HEIGHT constraints %s -> %s",
assert_height,
new_item.assert_height,
)
return False
if new_item.assert_before_height != assert_before_height:
log.debug(
"Rejecting conflicting tx due to changing ASSERT_BEFORE_HEIGHT constraints %s -> %s",
assert_before_height,
new_item.assert_before_height,
)
return False
if new_item.assert_before_seconds != assert_before_seconds:
log.debug(
"Rejecting conflicting tx due to changing ASSERT_BEFORE_SECONDS constraints %s -> %s",
assert_before_seconds,
new_item.assert_before_seconds,
)
return False
log.info(f"Replacing conflicting tx in mempool. New tx fee: {new_item.fee}, old tx fees: {conflicting_fees}")
return True

View File

@ -23,6 +23,11 @@ class MempoolItem:
# If present, this SpendBundle is not valid at or before this height
assert_height: Optional[uint32] = None
# If presemt, this SpendBundle is not valid once the block height reaches
# the specified height
assert_before_height: Optional[uint32] = None
assert_before_seconds: Optional[uint64] = None
def __lt__(self, other: MempoolItem) -> bool:
return self.fee_per_cost < other.fee_per_cost

View File

@ -173,6 +173,9 @@ class Err(Enum):
ASSERT_MY_BIRTH_SECONDS_FAILED = 138
ASSERT_MY_BIRTH_HEIGHT_FAILED = 139
ASSERT_EPHEMERAL_FAILED = 140
EPHEMERAL_RELATIVE_CONDITION = 141
class ValidationError(Exception):
def __init__(self, code: Err, error_msg: str = ""):

View File

@ -289,11 +289,15 @@ class CATWallet:
assert txs[0].spend_bundle
program: BlockGenerator = simple_solution_generator(txs[0].spend_bundle)
# npc contains names of the coins removed, puzzle_hashes and their spend conditions
# we use height=0 here to not enable any soft-fork semantics. It
# will only matter once the wallet generates transactions relying on
# new conditions, and we can change this by then
result: NPCResult = get_name_puzzle_conditions(
program,
self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=self.wallet_state_manager.constants.COST_PER_BYTE,
mempool_mode=True,
height=uint32(0),
)
self.cost_of_single_tx = result.cost
self.log.info(f"Cost of a single tx for CAT wallet: {self.cost_of_single_tx}")

View File

@ -90,11 +90,15 @@ class Wallet:
assert tx.spend_bundle is not None
program: BlockGenerator = simple_solution_generator(tx.spend_bundle)
# npc contains names of the coins removed, puzzle_hashes and their spend conditions
# we use height=0 here to not enable any soft-fork semantics. It
# will only matter once the wallet generates transactions relying on
# new conditions, and we can change this by then
result: NPCResult = get_name_puzzle_conditions(
program,
self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=self.wallet_state_manager.constants.COST_PER_BYTE,
mempool_mode=True,
height=uint32(0),
)
self.cost_of_single_tx = result.cost
self.log.info(f"Cost of a single tx for standard wallet: {self.cost_of_single_tx}")

View File

@ -14,7 +14,7 @@ dependencies = [
"chiapos==1.0.11", # proof of space
"clvm==0.9.7",
"clvm_tools==0.4.6", # Currying, Program.to, other conveniences
"chia_rs==0.2.4",
"chia_rs==0.2.5",
"clvm-tools-rs==0.1.30", # Rust implementation of clvm_tools' compiler
"aiohttp==3.8.4", # HTTP server for full node rpc
"aiosqlite==0.17.0", # asyncio wrapper for sqlite, to store blocks

View File

@ -1831,6 +1831,150 @@ class TestBodyValidation:
assert err is None
assert state_change.fork_height == 2
@pytest.mark.asyncio
@pytest.mark.parametrize("with_softfork2", [False, True])
@pytest.mark.parametrize(
"opcode,lock_value,expected",
[
# the 3 blocks, starting at timestamp 10000 (and height 0).
# each block is 10 seconds apart.
# the 4th block (height 3, time 10030) spends a coin with the condition specified
# by the test case. The coin was born in height 2 at time 10020
# MY BIRHT HEIGHT
(co.ASSERT_MY_BIRTH_HEIGHT, -1, rbr.INVALID_BLOCK),
(co.ASSERT_MY_BIRTH_HEIGHT, 0x100000000, rbr.INVALID_BLOCK),
(co.ASSERT_MY_BIRTH_HEIGHT, 2, rbr.NEW_PEAK), # <- coin birth height
(co.ASSERT_MY_BIRTH_HEIGHT, 3, rbr.INVALID_BLOCK),
# MY BIRHT SECONDS
(co.ASSERT_MY_BIRTH_SECONDS, -1, rbr.INVALID_BLOCK),
(co.ASSERT_MY_BIRTH_SECONDS, 0x10000000000000000, rbr.INVALID_BLOCK),
(co.ASSERT_MY_BIRTH_SECONDS, 10019, rbr.INVALID_BLOCK),
(co.ASSERT_MY_BIRTH_SECONDS, 10020, rbr.NEW_PEAK), # <- coin birth time
(co.ASSERT_MY_BIRTH_SECONDS, 10021, rbr.INVALID_BLOCK),
# SECONDS RELATIVE
(co.ASSERT_SECONDS_RELATIVE, -2, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_RELATIVE, -1, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_RELATIVE, 0, rbr.NEW_PEAK), # <- birth time
(co.ASSERT_SECONDS_RELATIVE, 1, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_RELATIVE, 9, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_RELATIVE, 10, rbr.INVALID_BLOCK), # <- current block time
(co.ASSERT_SECONDS_RELATIVE, 11, rbr.INVALID_BLOCK),
# BEFORE SECONDS RELATIVE
(co.ASSERT_BEFORE_SECONDS_RELATIVE, -2, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, -1, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 0, rbr.INVALID_BLOCK), # <- birth time
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 1, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 9, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 10, rbr.NEW_PEAK), # <- current block time
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 11, rbr.NEW_PEAK),
# HEIGHT RELATIVE
(co.ASSERT_HEIGHT_RELATIVE, -2, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_RELATIVE, -1, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_RELATIVE, 0, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_RELATIVE, 1, rbr.INVALID_BLOCK),
# BEFORE HEIGHT RELATIVE
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, -2, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, -1, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 0, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 1, rbr.NEW_PEAK),
# HEIGHT ABSOLUTE
(co.ASSERT_HEIGHT_ABSOLUTE, 1, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_ABSOLUTE, 2, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_ABSOLUTE, 3, rbr.INVALID_BLOCK),
(co.ASSERT_HEIGHT_ABSOLUTE, 4, rbr.INVALID_BLOCK),
# BEFORE HEIGHT ABSOLUTE
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 1, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 2, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 3, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 4, rbr.NEW_PEAK),
# SECONDS ABSOLUTE
# genesis timestamp is 10000 and each block is 10 seconds
(co.ASSERT_SECONDS_ABSOLUTE, 10019, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_ABSOLUTE, 10020, rbr.NEW_PEAK), # <- previous tx-block
(co.ASSERT_SECONDS_ABSOLUTE, 10021, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_ABSOLUTE, 10029, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_ABSOLUTE, 10030, rbr.INVALID_BLOCK), # <- current block
(co.ASSERT_SECONDS_ABSOLUTE, 10031, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_ABSOLUTE, 10032, rbr.INVALID_BLOCK),
# BEFORE SECONDS ABSOLUTE
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10019, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10020, rbr.INVALID_BLOCK), # <- previous tx-block
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10021, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10029, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10030, rbr.NEW_PEAK), # <- current block
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10031, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10032, rbr.NEW_PEAK),
],
)
async def test_timelock_conditions(self, opcode, lock_value, expected, with_softfork2, bt):
if with_softfork2:
# enable softfork2 at height 0, to make it apply to this test
constants = test_constants.replace(SOFT_FORK2_HEIGHT=0)
else:
constants = test_constants
# if the softfork is not active in this test, fixup all the
# tests to instead expect NEW_PEAK unconditionally
if opcode in [
ConditionOpcode.ASSERT_MY_BIRTH_HEIGHT,
ConditionOpcode.ASSERT_MY_BIRTH_SECONDS,
ConditionOpcode.ASSERT_BEFORE_SECONDS_RELATIVE,
ConditionOpcode.ASSERT_BEFORE_SECONDS_ABSOLUTE,
ConditionOpcode.ASSERT_BEFORE_HEIGHT_RELATIVE,
ConditionOpcode.ASSERT_BEFORE_HEIGHT_ABSOLUTE,
]:
expected = ReceiveBlockResult.NEW_PEAK
# before soft-fork 2, the timestamp we compared against was the
# current block's timestamp as opposed to the previous tx-block's
# timestamp. These conditions used to be valid, before the soft-fork
if opcode == ConditionOpcode.ASSERT_SECONDS_RELATIVE and lock_value > 0 and lock_value <= 10:
expected = ReceiveBlockResult.NEW_PEAK
if opcode == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE and lock_value > 10020 and lock_value <= 10030:
expected = ReceiveBlockResult.NEW_PEAK
async with make_empty_blockchain(constants) as b:
blocks = bt.get_consecutive_blocks(
3,
guarantee_transaction_block=True,
farmer_reward_puzzle_hash=bt.pool_ph,
pool_reward_puzzle_hash=bt.pool_ph,
genesis_timestamp=10000,
time_per_block=10,
)
for bl in blocks:
await _validate_and_add_block(b, bl)
wt: WalletTool = bt.get_pool_wallet_tool()
conditions = {opcode: [ConditionWithArgs(opcode, [int_to_bytes(lock_value)])]}
coin = list(blocks[-1].get_included_reward_coins())[0]
tx: SpendBundle = wt.generate_signed_transaction(
10, wt.get_new_puzzlehash(), coin, condition_dic=conditions
)
blocks = bt.get_consecutive_blocks(
1,
block_list_input=blocks,
guarantee_transaction_block=True,
transaction_data=tx,
time_per_block=10,
)
pre_validation_results: List[PreValidationResult] = await b.pre_validate_blocks_multiprocessing(
[blocks[-1]], {}, validate_signatures=True
)
assert pre_validation_results is not None
assert (await b.receive_block(blocks[-1], pre_validation_results[0]))[0] == expected
if expected == ReceiveBlockResult.NEW_PEAK:
# ensure coin was in fact spent
c = await b.coin_store.get_coin_record(coin.name())
assert c is not None and c.spent
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", [ConditionOpcode.AGG_SIG_ME, ConditionOpcode.AGG_SIG_UNSAFE])
@pytest.mark.parametrize(
@ -1888,8 +2032,7 @@ class TestBodyValidation:
assert (res, error, state_change.fork_height if state_change else None) == expected
@pytest.mark.asyncio
# soft-fork 2 is disabled (for now)
@pytest.mark.parametrize("with_softfork2", [False])
@pytest.mark.parametrize("with_softfork2", [False, True])
@pytest.mark.parametrize("with_garbage", [True, False])
@pytest.mark.parametrize(
"opcode,lock_value,expected",
@ -1906,31 +2049,70 @@ class TestBodyValidation:
(co.ASSERT_MY_BIRTH_SECONDS, 10030, rbr.NEW_PEAK),
(co.ASSERT_MY_BIRTH_SECONDS, 10031, rbr.INVALID_BLOCK),
# SECONDS RELATIVE
# genesis timestamp is 10000 and each block is 10 seconds
(co.ASSERT_SECONDS_RELATIVE, -2, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_RELATIVE, -1, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_RELATIVE, 0, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_RELATIVE, 0, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_RELATIVE, 1, rbr.INVALID_BLOCK),
# BEFORE SECONDS RELATIVE
# relative conditions are not allowed on ephemeral spends
(co.ASSERT_BEFORE_SECONDS_RELATIVE, -2, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, -1, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 0, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 10, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 0x10000000000000000, rbr.INVALID_BLOCK),
# HEIGHT RELATIVE
(co.ASSERT_HEIGHT_RELATIVE, -2, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_RELATIVE, -1, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_RELATIVE, 0, rbr.INVALID_BLOCK),
(co.ASSERT_HEIGHT_RELATIVE, 1, rbr.INVALID_BLOCK),
# BEFORE HEIGHT RELATIVE
# relative conditions are not allowed on ephemeral spends
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, -2, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, -1, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 0, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 1, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 0x100000000, rbr.INVALID_BLOCK),
# HEIGHT ABSOLUTE
(co.ASSERT_HEIGHT_ABSOLUTE, 2, rbr.NEW_PEAK),
(co.ASSERT_HEIGHT_ABSOLUTE, 3, rbr.INVALID_BLOCK),
(co.ASSERT_HEIGHT_ABSOLUTE, 4, rbr.INVALID_BLOCK),
# BEFORE HEIGHT ABSOLUTE
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 2, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 3, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 4, rbr.NEW_PEAK),
# SECONDS ABSOLUTE
# genesis timestamp is 10000 and each block is 10 seconds
(co.ASSERT_SECONDS_ABSOLUTE, 10029, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_ABSOLUTE, 10030, rbr.NEW_PEAK),
(co.ASSERT_SECONDS_ABSOLUTE, 10020, rbr.NEW_PEAK), # <- previous tx-block
(co.ASSERT_SECONDS_ABSOLUTE, 10021, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_ABSOLUTE, 10029, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_ABSOLUTE, 10030, rbr.INVALID_BLOCK), # <- current tx-block
(co.ASSERT_SECONDS_ABSOLUTE, 10031, rbr.INVALID_BLOCK),
(co.ASSERT_SECONDS_ABSOLUTE, 10032, rbr.INVALID_BLOCK),
# BEFORE SECONDS ABSOLUTE
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10020, rbr.INVALID_BLOCK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10021, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10030, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10031, rbr.NEW_PEAK),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10032, rbr.NEW_PEAK),
],
)
async def test_ephemeral_timelock(self, opcode, lock_value, expected, with_garbage, with_softfork2, bt):
if with_softfork2:
# enable softfork2 at height 0, to make it apply to this test
constants = test_constants.replace(SOFT_FORK2_HEIGHT=0)
# after the softfork, we don't allow any birth assertions, not
# relative time locks on ephemeral coins. This test is only for
# ephemeral coins, so these cases should always fail
if opcode in [
ConditionOpcode.ASSERT_MY_BIRTH_HEIGHT,
ConditionOpcode.ASSERT_MY_BIRTH_SECONDS,
ConditionOpcode.ASSERT_SECONDS_RELATIVE,
ConditionOpcode.ASSERT_HEIGHT_RELATIVE,
]:
expected = ReceiveBlockResult.INVALID_BLOCK
else:
constants = test_constants
@ -1939,9 +2121,22 @@ class TestBodyValidation:
if opcode in [
ConditionOpcode.ASSERT_MY_BIRTH_HEIGHT,
ConditionOpcode.ASSERT_MY_BIRTH_SECONDS,
ConditionOpcode.ASSERT_BEFORE_HEIGHT_RELATIVE,
ConditionOpcode.ASSERT_BEFORE_HEIGHT_ABSOLUTE,
ConditionOpcode.ASSERT_BEFORE_SECONDS_RELATIVE,
ConditionOpcode.ASSERT_BEFORE_SECONDS_ABSOLUTE,
]:
expected = ReceiveBlockResult.NEW_PEAK
# before the softfork, we compared ASSERT_SECONDS_* conditions
# against the current block's timestamp, so we need to
# adjust these test cases
if opcode == co.ASSERT_SECONDS_ABSOLUTE and lock_value > 10020 and lock_value <= 10030:
expected = rbr.NEW_PEAK
if opcode == co.ASSERT_SECONDS_RELATIVE and lock_value > -10 and lock_value <= 0:
expected = rbr.NEW_PEAK
async with make_empty_blockchain(constants) as b:
blocks = bt.get_consecutive_blocks(

View File

@ -11,7 +11,13 @@ from chia.types.spend_bundle import SpendBundle
def cost_of_spend_bundle(spend_bundle: SpendBundle) -> int:
program: BlockGenerator = simple_solution_generator(spend_bundle)
# always use the post soft-fork2 semantics
npc_result: NPCResult = get_name_puzzle_conditions(
program, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=True
program,
INFINITE_COST,
cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE,
mempool_mode=True,
height=DEFAULT_CONSTANTS.SOFT_FORK2_HEIGHT,
)
return npc_result.cost

View File

@ -64,8 +64,9 @@ class CoinStore:
# this should use blockchain consensus code
program = simple_solution_generator(spend_bundle)
# always use the post soft-fork2 semantics
result: NPCResult = get_name_puzzle_conditions(
program, max_cost, cost_per_byte=cost_per_byte, mempool_mode=True
program, max_cost, cost_per_byte=cost_per_byte, mempool_mode=True, height=uint32(4000000)
)
if result.error is not None:
raise BadSpendBundleError(f"condition validation failure {Err(result.error)}")
@ -87,7 +88,11 @@ class CoinStore:
err = mempool_check_time_locks(
ephemeral_db,
result.conds,
# TODO: this is technically not right, it's supposed to be the
# previous transaction block's height
uint32(now.height),
# TODO: this is technically not right, it's supposed to be the
# previous transaction block's timestamp
uint64(now.seconds),
)

View File

@ -133,7 +133,7 @@ def db_version(request):
return request.param
@pytest.fixture(scope="function", params=[1000000, 3630000, 3830000])
@pytest.fixture(scope="function", params=[1000000, 3630000, 4000000])
def softfork_height(request):
return request.param
@ -457,8 +457,7 @@ async def one_node() -> AsyncIterator[Tuple[List[Service], List[FullNodeSimulato
yield _
# soft-fork 2 is disabled (for now)
@pytest.fixture(scope="function", params=[False])
@pytest.fixture(scope="function", params=[True, False])
def enable_softfork2(request):
return request.param

View File

@ -22,7 +22,7 @@ from chia.types.condition_opcodes import ConditionOpcode
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
from chia.util.ints import uint32, uint64
from ...blockchain.blockchain_test_utils import _validate_and_add_block
from .ram_db import create_ram_blockchain
@ -81,6 +81,8 @@ async def check_spend_bundle_validity(
block_list_input=blocks,
guarantee_transaction_block=True,
transaction_data=spend_bundle,
genesis_timestamp=uint64(10000),
time_per_block=10,
)
newest_block = additional_blocks[-1]
@ -126,8 +128,7 @@ co = ConditionOpcode
class TestConditions:
@pytest.mark.asyncio
# soft-fork 2 is disabled (for now)
@pytest.mark.parametrize("softfork2", [False])
@pytest.mark.parametrize("softfork2", [True, False])
@pytest.mark.parametrize(
"opcode,value,expected",
[
@ -135,13 +136,13 @@ class TestConditions:
# the coin being spent was created in the 3rd block (i.e. block 2)
# ensure invalid heights fail and pass correctly, depending on
# which end of the range they exceed
# genesis timestamp is 10000 and each block is 10 seconds
# MY BIRTH HEIGHT
(co.ASSERT_MY_BIRTH_HEIGHT, -1, Err.ASSERT_MY_BIRTH_HEIGHT_FAILED),
(co.ASSERT_MY_BIRTH_HEIGHT, 0x100000000, Err.ASSERT_MY_BIRTH_HEIGHT_FAILED),
(co.ASSERT_MY_BIRTH_HEIGHT, 3, Err.ASSERT_MY_BIRTH_HEIGHT_FAILED),
(co.ASSERT_MY_BIRTH_HEIGHT, 2, None),
# MY BIRTH SECONDS
# genesis timestamp is 10000 and each block is 10 seconds
(co.ASSERT_MY_BIRTH_SECONDS, -1, Err.ASSERT_MY_BIRTH_SECONDS_FAILED),
(co.ASSERT_MY_BIRTH_SECONDS, 0x10000000000000000, Err.ASSERT_MY_BIRTH_SECONDS_FAILED),
(co.ASSERT_MY_BIRTH_SECONDS, 10019, Err.ASSERT_MY_BIRTH_SECONDS_FAILED),
@ -153,23 +154,62 @@ class TestConditions:
(co.ASSERT_HEIGHT_RELATIVE, 1, None),
(co.ASSERT_HEIGHT_RELATIVE, 2, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 0x100000000, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
# BEFORE HEIGHT RELATIVE
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, -1, Err.ASSERT_BEFORE_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 0, Err.ASSERT_BEFORE_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 1, Err.ASSERT_BEFORE_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 2, None),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 0x100000000, None),
# HEIGHT ABSOLUTE
(co.ASSERT_HEIGHT_ABSOLUTE, -1, None),
(co.ASSERT_HEIGHT_ABSOLUTE, 0, None),
(co.ASSERT_HEIGHT_ABSOLUTE, 3, None),
(co.ASSERT_HEIGHT_ABSOLUTE, 4, Err.ASSERT_HEIGHT_ABSOLUTE_FAILED),
(co.ASSERT_HEIGHT_ABSOLUTE, 0x100000000, Err.ASSERT_HEIGHT_ABSOLUTE_FAILED),
# BEFORE HEIGHT ABSOLUTE
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, -1, Err.ASSERT_BEFORE_HEIGHT_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 0, Err.IMPOSSIBLE_HEIGHT_ABSOLUTE_CONSTRAINTS),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 3, Err.ASSERT_BEFORE_HEIGHT_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 4, None),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 0x100000000, None),
# SECONDS RELATIVE
(co.ASSERT_SECONDS_RELATIVE, -1, None),
(co.ASSERT_SECONDS_RELATIVE, 0, None),
(co.ASSERT_SECONDS_RELATIVE, 10, None),
(co.ASSERT_SECONDS_RELATIVE, 11, Err.ASSERT_SECONDS_RELATIVE_FAILED),
(co.ASSERT_SECONDS_RELATIVE, 20, Err.ASSERT_SECONDS_RELATIVE_FAILED),
(co.ASSERT_SECONDS_RELATIVE, 21, Err.ASSERT_SECONDS_RELATIVE_FAILED),
(co.ASSERT_SECONDS_RELATIVE, 30, Err.ASSERT_SECONDS_RELATIVE_FAILED),
(co.ASSERT_SECONDS_RELATIVE, 0x10000000000000000, Err.ASSERT_SECONDS_RELATIVE_FAILED),
# BEFORE SECONDS RELATIVE
(co.ASSERT_BEFORE_SECONDS_RELATIVE, -1, Err.ASSERT_BEFORE_SECONDS_RELATIVE_FAILED),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 0, Err.ASSERT_BEFORE_SECONDS_RELATIVE_FAILED),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 10, Err.ASSERT_BEFORE_SECONDS_RELATIVE_FAILED),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 11, None),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 20, None),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 21, None),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 30, None),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 0x100000000000000, None),
# SECONDS ABSOLUTE
(co.ASSERT_SECONDS_ABSOLUTE, -1, None),
(co.ASSERT_SECONDS_ABSOLUTE, 0, None),
(co.ASSERT_SECONDS_ABSOLUTE, 10000, None),
(co.ASSERT_SECONDS_ABSOLUTE, 10049, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_SECONDS_ABSOLUTE, 10030, None),
(co.ASSERT_SECONDS_ABSOLUTE, 10031, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_SECONDS_ABSOLUTE, 10039, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_SECONDS_ABSOLUTE, 10040, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_SECONDS_ABSOLUTE, 10041, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_SECONDS_ABSOLUTE, 0x10000000000000000, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
# BEFORE SECONDS ABSOLUTE
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, -1, Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 0, Err.IMPOSSIBLE_SECONDS_ABSOLUTE_CONSTRAINTS),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10000, Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10030, Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10031, None),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10039, None),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10040, None),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10041, None),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 0x100000000, None),
],
)
async def test_condition(self, opcode, value, expected, bt, softfork2):
@ -180,9 +220,23 @@ class TestConditions:
if not softfork2 and opcode in [
co.ASSERT_MY_BIRTH_HEIGHT,
co.ASSERT_MY_BIRTH_SECONDS,
co.ASSERT_BEFORE_SECONDS_RELATIVE,
co.ASSERT_BEFORE_SECONDS_ABSOLUTE,
co.ASSERT_BEFORE_HEIGHT_RELATIVE,
co.ASSERT_BEFORE_HEIGHT_ABSOLUTE,
]:
expected = None
if not softfork2:
# before soft-fork 2, the timestamp we compared against was the
# current block's timestamp as opposed to the previous tx-block's
# timestamp. These conditions used to be valid, before the soft-fork
if opcode == ConditionOpcode.ASSERT_SECONDS_RELATIVE and value > 10 and value <= 20:
expected = None
if opcode == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE and value > 10030 and value <= 10040:
expected = None
await check_conditions(bt, conditions, expected_err=expected, softfork2=softfork2)
@pytest.mark.asyncio

View File

@ -372,23 +372,25 @@ class TestMempoolManager:
@pytest.mark.parametrize(
"opcode,lock_value,expected",
[
# the mempool rules don't allow relative height- or time conditions on
# ephemeral spends
(co.ASSERT_MY_BIRTH_HEIGHT, -1, mis.FAILED),
(co.ASSERT_MY_BIRTH_HEIGHT, 0x100000000, mis.FAILED),
(co.ASSERT_MY_BIRTH_HEIGHT, 5, mis.FAILED),
(co.ASSERT_MY_BIRTH_HEIGHT, 6, mis.SUCCESS),
(co.ASSERT_MY_BIRTH_HEIGHT, 6, mis.FAILED),
(co.ASSERT_MY_BIRTH_SECONDS, -1, mis.FAILED),
(co.ASSERT_MY_BIRTH_SECONDS, 0x10000000000000000, mis.FAILED),
(co.ASSERT_MY_BIRTH_SECONDS, 10049, mis.FAILED),
(co.ASSERT_MY_BIRTH_SECONDS, 10050, mis.SUCCESS),
(co.ASSERT_MY_BIRTH_SECONDS, 10050, mis.FAILED),
(co.ASSERT_MY_BIRTH_SECONDS, 10051, mis.FAILED),
(co.ASSERT_SECONDS_RELATIVE, -2, mis.SUCCESS),
(co.ASSERT_SECONDS_RELATIVE, -1, mis.SUCCESS),
(co.ASSERT_SECONDS_RELATIVE, 0, mis.SUCCESS),
(co.ASSERT_SECONDS_RELATIVE, -2, mis.FAILED),
(co.ASSERT_SECONDS_RELATIVE, -1, mis.FAILED),
(co.ASSERT_SECONDS_RELATIVE, 0, mis.FAILED),
(co.ASSERT_SECONDS_RELATIVE, 1, mis.FAILED),
(co.ASSERT_HEIGHT_RELATIVE, -2, mis.SUCCESS),
(co.ASSERT_HEIGHT_RELATIVE, -1, mis.SUCCESS),
(co.ASSERT_HEIGHT_RELATIVE, 0, mis.PENDING),
(co.ASSERT_HEIGHT_RELATIVE, 1, mis.PENDING),
(co.ASSERT_HEIGHT_RELATIVE, -2, mis.FAILED),
(co.ASSERT_HEIGHT_RELATIVE, -1, mis.FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 0, mis.FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 1, mis.FAILED),
# the absolute height and seconds tests require fresh full nodes to
# run the test on. The fixture (one_node_one_block) creates a block,
# then condition_tester2 creates another 3 blocks
@ -2192,18 +2194,10 @@ class TestGeneratorConditions:
coins = npc_result.conds.spends[0].create_coin
assert coins == [(puzzle_hash_1.encode("ascii"), 5, hint.encode("ascii"))]
@pytest.mark.parametrize(
"mempool,height",
[
(True, None),
(False, 2300000),
(False, 3630000),
(False, 3830000),
],
)
def test_unknown_condition(self, mempool: bool, height: uint32):
@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=height)
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

View File

@ -11,7 +11,14 @@ from chia.consensus.constants import ConsensusConstants
from chia.consensus.cost_calculator import NPCResult
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.full_node.mempool_check_conditions import mempool_check_time_locks
from chia.full_node.mempool_manager import MempoolManager, can_replace, compute_assert_height
from chia.full_node.mempool_manager import (
MempoolManager,
TimelockConditions,
can_replace,
compute_assert_height,
optional_max,
optional_min,
)
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.serialized_program import SerializedProgram
@ -125,6 +132,10 @@ def make_test_conds(
height_absolute: int = 0,
seconds_relative: Optional[int] = None,
seconds_absolute: int = 0,
before_height_relative: Optional[int] = None,
before_height_absolute: Optional[int] = None,
before_seconds_relative: Optional[int] = None,
before_seconds_absolute: Optional[int] = None,
cost: int = 0,
) -> SpendBundleConditions:
return SpendBundleConditions(
@ -134,8 +145,8 @@ def make_test_conds(
IDENTITY_PUZZLE_HASH,
None if height_relative is None else uint32(height_relative),
None if seconds_relative is None else uint64(seconds_relative),
None,
None,
None if before_height_relative is None else uint32(before_height_relative),
None if before_seconds_relative is None else uint64(before_seconds_relative),
None if birth_height is None else uint32(birth_height),
None if birth_seconds is None else uint64(birth_seconds),
[],
@ -146,8 +157,8 @@ def make_test_conds(
0,
uint32(height_absolute),
uint64(seconds_absolute),
None,
None,
None if before_height_absolute is None else uint32(before_height_absolute),
None if before_seconds_absolute is None else uint64(before_seconds_absolute),
[],
cost,
0,
@ -189,6 +200,18 @@ class TestCheckTimeLocks:
(make_test_conds(birth_seconds=9999), Err.ASSERT_MY_BIRTH_SECONDS_FAILED),
(make_test_conds(birth_seconds=10000), None),
(make_test_conds(birth_seconds=10001), Err.ASSERT_MY_BIRTH_SECONDS_FAILED),
# the coin is 5 blocks old in this test
(make_test_conds(before_height_relative=5), Err.ASSERT_BEFORE_HEIGHT_RELATIVE_FAILED),
(make_test_conds(before_height_relative=6), None),
# The block height is 15
(make_test_conds(before_height_absolute=15), Err.ASSERT_BEFORE_HEIGHT_ABSOLUTE_FAILED),
(make_test_conds(before_height_absolute=16), None),
# the coin is 150 seconds old in this test
(make_test_conds(before_seconds_relative=150), Err.ASSERT_BEFORE_SECONDS_RELATIVE_FAILED),
(make_test_conds(before_seconds_relative=151), None),
# The block timestamp is 10150
(make_test_conds(before_seconds_absolute=10150), Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED),
(make_test_conds(before_seconds_absolute=10151), None),
],
)
def test_conditions(
@ -197,33 +220,78 @@ class TestCheckTimeLocks:
expected: Optional[Err],
) -> None:
assert (
mempool_check_time_locks(self.REMOVALS, conds, self.PREV_BLOCK_HEIGHT, self.PREV_BLOCK_TIMESTAMP)
mempool_check_time_locks(
self.REMOVALS,
conds,
self.PREV_BLOCK_HEIGHT,
self.PREV_BLOCK_TIMESTAMP,
)
== expected
)
def expect(*, height: int = 0) -> uint32:
return uint32(height)
def expect(
*, height: int = 0, before_height: Optional[int] = None, before_seconds: Optional[int] = None
) -> TimelockConditions:
ret = TimelockConditions(uint32(height))
if before_height is not None:
ret.assert_before_height = uint32(before_height)
if before_seconds is not None:
ret.assert_before_seconds = uint64(before_seconds)
return ret
@pytest.mark.parametrize(
"conds,expected",
[
# ASSERT_HEIGHT_*
# coin birth height is 12
(make_test_conds(), expect()),
(make_test_conds(height_absolute=42), expect(height=42)),
# 1 is a relative height, but that only amounts to 13, so the absolute
# height is more restrictive
(make_test_conds(height_relative=1), expect(height=13)),
# 100 is a relative height, and sinec the coin was confirmed at height 12,
# 100 is a relative height, and since the coin was confirmed at height 12,
# that's 112
(make_test_conds(height_absolute=42, height_relative=100), expect(height=112)),
# Same thing but without the absolute height
(make_test_conds(height_relative=100), expect(height=112)),
(make_test_conds(height_relative=0), expect(height=12)),
# 42 is more restrictive than 13
(make_test_conds(height_absolute=42, height_relative=1), expect(height=42)),
# ASSERT_BEFORE_HEIGHT_*
(make_test_conds(before_height_absolute=100), expect(before_height=100)),
# coin is created at 12 + 1 relative height = 13
(make_test_conds(before_height_relative=1), expect(before_height=13)),
# coin is created at 12 + 0 relative height = 12
(make_test_conds(before_height_relative=0), expect(before_height=12)),
# 13 is more restrictive than 42
(make_test_conds(before_height_absolute=42, before_height_relative=1), expect(before_height=13)),
# 100 is a relative height, and since the coin was confirmed at height 12,
# that's 112
(make_test_conds(before_height_absolute=200, before_height_relative=100), expect(before_height=112)),
# Same thing but without the absolute height
(make_test_conds(before_height_relative=100), expect(before_height=112)),
# ASSERT_BEFORE_SECONDS_*
# coin timestamp is 10000
# single absolute assert before seconds
(make_test_conds(before_seconds_absolute=20000), expect(before_seconds=20000)),
# coin is created at 10000 + 100 relative seconds = 10100
(make_test_conds(before_seconds_relative=100), expect(before_seconds=10100)),
# coin is created at 10000 + 0 relative seconds = 10000
(make_test_conds(before_seconds_relative=0), expect(before_seconds=10000)),
# 10100 is more restrictive than 20000
(make_test_conds(before_seconds_absolute=20000, before_seconds_relative=100), expect(before_seconds=10100)),
# 20000 is a relative seconds, and since the coin was confirmed at seconds
# 10000 that's 300000
(make_test_conds(before_seconds_absolute=20000, before_seconds_relative=20000), expect(before_seconds=20000)),
# Same thing but without the absolute seconds
(make_test_conds(before_seconds_relative=20000), expect(before_seconds=30000)),
],
)
def test_compute_assert_height(conds: SpendBundleConditions, expected: uint32) -> None:
def test_compute_assert_height(conds: SpendBundleConditions, expected: TimelockConditions) -> None:
coin_id = TEST_COIN.name()
confirmed_height = uint32(12)
coin_records = {coin_id: CoinRecord(TEST_COIN, confirmed_height, uint32(0), False, uint64(10000))}
@ -425,40 +493,58 @@ mis = MempoolInclusionStatus
@pytest.mark.asyncio
# soft-fork 2 is disabled (for now)
@pytest.mark.parametrize("softfork2", [False])
@pytest.mark.parametrize("softfork2", [False, True])
@pytest.mark.parametrize(
"opcode,lock_value,expected_status,expected_error",
[
# the mempool rules don't allow relative height- or time conditions on
# ephemeral spends
# SECONDS RELATIVE
(co.ASSERT_SECONDS_RELATIVE, -2, mis.SUCCESS, None),
(co.ASSERT_SECONDS_RELATIVE, -1, mis.SUCCESS, None),
# The rules allow spending an ephemeral coin with an ASSERT_SECONDS_RELATIVE 0 condition
(co.ASSERT_SECONDS_RELATIVE, 0, mis.SUCCESS, None),
(co.ASSERT_SECONDS_RELATIVE, 1, mis.FAILED, Err.ASSERT_SECONDS_RELATIVE_FAILED),
(co.ASSERT_SECONDS_RELATIVE, 9, mis.FAILED, Err.ASSERT_SECONDS_RELATIVE_FAILED),
(co.ASSERT_SECONDS_RELATIVE, 10, mis.FAILED, Err.ASSERT_SECONDS_RELATIVE_FAILED),
(co.ASSERT_SECONDS_RELATIVE, -2, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_SECONDS_RELATIVE, -1, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_SECONDS_RELATIVE, 0, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_SECONDS_RELATIVE, 1, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_SECONDS_RELATIVE, 9, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_SECONDS_RELATIVE, 10, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
# HEIGHT RELATIVE
(co.ASSERT_HEIGHT_RELATIVE, -2, mis.SUCCESS, None),
(co.ASSERT_HEIGHT_RELATIVE, -1, mis.SUCCESS, None),
(co.ASSERT_HEIGHT_RELATIVE, 0, mis.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 1, mis.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 5, mis.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 6, mis.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 7, mis.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 10, mis.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, 11, mis.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_HEIGHT_RELATIVE, -2, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, -1, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, 0, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, 1, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, 5, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, 6, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, 7, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, 10, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_HEIGHT_RELATIVE, 11, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
# BEFORE HEIGHT RELATIVE
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, -2, mis.FAILED, Err.ASSERT_BEFORE_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, -1, mis.FAILED, Err.ASSERT_BEFORE_HEIGHT_RELATIVE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 0, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 1, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 5, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 6, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 7, mis.FAILED, Err.EPHEMERAL_RELATIVE_CONDITION),
# HEIGHT ABSOLUTE
(co.ASSERT_HEIGHT_ABSOLUTE, 4, mis.SUCCESS, None),
(co.ASSERT_HEIGHT_ABSOLUTE, 5, mis.SUCCESS, None),
(co.ASSERT_HEIGHT_ABSOLUTE, 6, mis.PENDING, Err.ASSERT_HEIGHT_ABSOLUTE_FAILED),
(co.ASSERT_HEIGHT_ABSOLUTE, 7, mis.PENDING, Err.ASSERT_HEIGHT_ABSOLUTE_FAILED),
# BEFORE HEIGHT ABSOLUTE
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 4, mis.FAILED, Err.ASSERT_BEFORE_HEIGHT_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 5, mis.FAILED, Err.ASSERT_BEFORE_HEIGHT_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 6, mis.SUCCESS, None),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 7, mis.SUCCESS, None),
# SECONDS ABSOLUTE
# Current block timestamp is 10050
(co.ASSERT_SECONDS_ABSOLUTE, 10049, mis.SUCCESS, None),
(co.ASSERT_SECONDS_ABSOLUTE, 10050, mis.SUCCESS, None),
(co.ASSERT_SECONDS_ABSOLUTE, 10051, mis.FAILED, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_SECONDS_ABSOLUTE, 10052, mis.FAILED, Err.ASSERT_SECONDS_ABSOLUTE_FAILED),
# BEFORE SECONDS ABSOLUTE
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10049, mis.FAILED, Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10050, mis.FAILED, Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10051, mis.SUCCESS, None),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10052, mis.SUCCESS, None),
],
)
async def test_ephemeral_timelock(
@ -487,6 +573,7 @@ async def test_ephemeral_timelock(
co.ASSERT_BEFORE_SECONDS_RELATIVE,
]:
expected_error = Err.INVALID_CONDITION
expected_status = MempoolInclusionStatus.FAILED
conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1]]
created_coin = Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, 1)
@ -496,17 +583,52 @@ async def test_ephemeral_timelock(
sb = SpendBundle.aggregate([sb1, sb2])
# We shouldn't have a record of this ephemeral coin
assert await get_coin_record_for_test_coins(created_coin.name()) is None
_, status, error = await add_spendbundle(mempool_manager, sb, sb.name())
assert (status, error) == (expected_status, expected_error)
try:
_, status, error = await add_spendbundle(mempool_manager, sb, sb.name())
assert (status, error) == (expected_status, expected_error)
except ValidationError as e:
assert expected_status == mis.FAILED
assert expected_error == e.code
def mk_item(coins: List[Coin], *, cost: int = 1, fee: int = 0) -> MempoolItem:
def test_optional_min() -> None:
assert optional_min(uint32(100), None) == uint32(100)
assert optional_min(None, uint32(100)) == uint32(100)
assert optional_min(None, None) is None
assert optional_min(uint32(123), uint32(234)) == uint32(123)
def test_optional_max() -> None:
assert optional_max(uint32(100), None) == uint32(100)
assert optional_max(None, uint32(100)) == uint32(100)
assert optional_max(None, None) is None
assert optional_max(uint32(123), uint32(234)) == uint32(234)
def mk_item(
coins: List[Coin],
*,
cost: int = 1,
fee: int = 0,
assert_height: Optional[int] = None,
assert_before_height: Optional[int] = None,
assert_before_seconds: Optional[int] = None,
) -> MempoolItem:
# we don't actually care about the puzzle and solutions for the purpose of
# can_replace()
spends = [CoinSpend(c, SerializedProgram(), SerializedProgram()) for c in coins]
spend_bundle = SpendBundle(spends, G2Element())
npc_results = NPCResult(None, make_test_conds(cost=cost), uint64(cost))
return MempoolItem(spend_bundle, uint64(fee), npc_results, spend_bundle.name(), uint32(0))
return MempoolItem(
spend_bundle,
uint64(fee),
npc_results,
spend_bundle.name(),
uint32(0),
None if assert_height is None else uint32(assert_height),
None if assert_before_height is None else uint32(assert_before_height),
None if assert_before_seconds is None else uint64(assert_before_seconds),
)
def make_test_coins() -> List[Coin]:
@ -560,6 +682,95 @@ coins = make_test_coins()
mk_item(coins[0:2], fee=10000200, cost=10000200),
False,
),
# TIMELOCK RULE
# the new item must not have different time lock than the existing item(s)
# the assert height time lock condition was introduced in the new item
([mk_item(coins[0:1])], mk_item(coins[0:1], fee=10000000, assert_height=1000), False),
# the assert before height time lock condition was introduced in the new item
([mk_item(coins[0:1])], mk_item(coins[0:1], fee=10000000, assert_before_height=1000), False),
# the assert before seconds time lock condition was introduced in the new item
([mk_item(coins[0:1])], mk_item(coins[0:1], fee=10000000, assert_before_seconds=1000), False),
# if we don't alter any time locks, we are allowed to replace
([mk_item(coins[0:1])], mk_item(coins[0:1], fee=10000000), True),
# ASSERT_HEIGHT
# the assert height time lock condition was removed in the new item
([mk_item(coins[0:1], assert_height=1000)], mk_item(coins[0:1], fee=10000000), False),
# different assert height constraint
([mk_item(coins[0:1], assert_height=1000)], mk_item(coins[0:1], fee=10000000, assert_height=100), False),
([mk_item(coins[0:1], assert_height=1000)], mk_item(coins[0:1], fee=10000000, assert_height=2000), False),
# the same assert height is OK
([mk_item(coins[0:1], assert_height=1000)], mk_item(coins[0:1], fee=10000000, assert_height=1000), True),
# The new spend just have to match the most restrictive condition
(
[mk_item(coins[0:1], assert_height=200), mk_item(coins[1:2], assert_height=400)],
mk_item(coins[0:2], fee=10000000, assert_height=400),
True,
),
# ASSERT_BEFORE_HEIGHT
# the assert before height time lock condition was removed in the new item
([mk_item(coins[0:1], assert_before_height=1000)], mk_item(coins[0:1], fee=10000000), False),
# different assert before height constraint
(
[mk_item(coins[0:1], assert_before_height=1000)],
mk_item(coins[0:1], fee=10000000, assert_before_height=100),
False,
),
(
[mk_item(coins[0:1], assert_before_height=1000)],
mk_item(coins[0:1], fee=10000000, assert_before_height=2000),
False,
),
# The new spend just have to match the most restrictive condition
(
[mk_item(coins[0:1], assert_before_height=200), mk_item(coins[1:2], assert_before_height=400)],
mk_item(coins[0:2], fee=10000000, assert_before_height=200),
True,
),
# ASSERT_BEFORE_SECONDS
# the assert before height time lock condition was removed in the new item
([mk_item(coins[0:1], assert_before_seconds=1000)], mk_item(coins[0:1], fee=10000000), False),
# different assert before seconds constraint
(
[mk_item(coins[0:1], assert_before_seconds=1000)],
mk_item(coins[0:1], fee=10000000, assert_before_seconds=100),
False,
),
(
[mk_item(coins[0:1], assert_before_seconds=1000)],
mk_item(coins[0:1], fee=10000000, assert_before_seconds=2000),
False,
),
# the assert before height time lock condition was introduced in the new item
(
[mk_item(coins[0:1], assert_before_seconds=1000)],
mk_item(coins[0:1], fee=10000000, assert_before_seconds=1000),
True,
),
# The new spend just have to match the most restrictive condition
(
[mk_item(coins[0:1], assert_before_seconds=200), mk_item(coins[1:2], assert_before_seconds=400)],
mk_item(coins[0:2], fee=10000000, assert_before_seconds=200),
True,
),
# MIXED CONDITIONS
# we can't replace an assert_before_seconds with assert_before_height
(
[mk_item(coins[0:1], assert_before_seconds=1000)],
mk_item(coins[0:1], fee=10000000, assert_before_height=2000),
False,
),
# we added another condition
(
[mk_item(coins[0:1], assert_before_seconds=1000)],
mk_item(coins[0:1], fee=10000000, assert_before_seconds=1000, assert_height=200),
False,
),
# we removed assert before height
(
[mk_item(coins[0:1], assert_height=200, assert_before_height=1000)],
mk_item(coins[0:1], fee=10000000, assert_height=200),
False,
),
],
)
def test_can_replace(existing_items: List[MempoolItem], new_item: MempoolItem, expected: bool) -> None:
@ -713,3 +924,51 @@ async def test_create_bundle_from_mempool_on_max_cost() -> None:
# The first spend bundle hits the maximum block clvm cost and gets skipped
assert additions == [Coin(coins[1].name(), IDENTITY_PUZZLE_HASH, coins[1].amount - 2)]
assert removals == [coins[1]]
@pytest.mark.parametrize(
"opcode,arg,expect_eviction",
[
# current height: 10 current_time: 10000
# we step the chain forward 1 block and 19 seconds
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10001, True),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10019, True),
(co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 10020, False),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 11, True),
(co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 12, False),
# the coin was created at height: 5 timestamp: 9900
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 6, True),
(co.ASSERT_BEFORE_HEIGHT_RELATIVE, 7, False),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 119, True),
(co.ASSERT_BEFORE_SECONDS_RELATIVE, 120, False),
],
)
@pytest.mark.asyncio
async def test_assert_before_expiration(opcode: ConditionOpcode, arg: int, expect_eviction: bool) -> None:
async def get_coin_record(coin_id: bytes32) -> Optional[CoinRecord]:
return {TEST_COIN.name(): CoinRecord(TEST_COIN, uint32(5), uint32(0), False, uint64(9900))}.get(coin_id)
mempool_manager = await instantiate_mempool_manager(
get_coin_record,
block_height=uint32(10),
block_timestamp=uint64(10000),
constants=DEFAULT_CONSTANTS.replace(SOFT_FORK2_HEIGHT=0),
)
bundle = spend_bundle_from_conditions(
[
[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1],
[opcode, arg],
],
coin=TEST_COIN,
)
bundle_name = bundle.name()
assert (await add_spendbundle(mempool_manager, bundle, bundle_name))[1] == mis.SUCCESS
# make sure the spend was added correctly
assert mempool_manager.get_spendbundle(bundle_name) == bundle
block_record = create_test_block_record(height=uint32(11), timestamp=uint64(10019))
await mempool_manager.new_peak(block_record, None)
still_in_pool = mempool_manager.get_spendbundle(bundle_name) == bundle
assert still_in_pool != expect_eviction

View File

@ -186,6 +186,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(

View File

@ -42,7 +42,9 @@ class TestCATTrades:
"reuse_puzhash",
[True, False],
)
async def test_cat_trades(self, wallets_prefarm, forwards_compat: bool, reuse_puzhash: bool):
async def test_cat_trades(
self, wallets_prefarm, forwards_compat: bool, reuse_puzhash: bool, softfork_height: uint32
):
(
[wallet_node_maker, initial_maker_balance],
[wallet_node_taker, initial_taker_balance],
@ -575,7 +577,9 @@ class TestCATTrades:
# (and therefore are solved as a complete ring)
bundle = Offer.aggregate([first_offer, second_offer, third_offer, fourth_offer, fifth_offer]).to_valid_spend()
program = simple_solution_generator(bundle)
result: NPCResult = get_name_puzzle_conditions(program, INFINITE_COST, cost_per_byte=0, mempool_mode=True)
result: NPCResult = get_name_puzzle_conditions(
program, INFINITE_COST, cost_per_byte=0, mempool_mode=True, height=softfork_height
)
assert result.error is None
@pytest.mark.asyncio