mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-11 10:56:14 +03:00
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:
parent
15d338976d
commit
8dbfc4840a
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = ""):
|
||||
|
@ -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}")
|
||||
|
@ -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}")
|
||||
|
2
setup.py
2
setup.py
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user