extend the mempool tests for timelocks, and improve error codes (#17105)

This commit is contained in:
Arvid Norberg 2023-12-21 21:34:11 +01:00 committed by GitHub
parent eb445d3a68
commit 9599f40219
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 194 additions and 2 deletions

View File

@ -95,6 +95,7 @@ def validate_clvm_and_signature(
@dataclass
class TimelockConditions:
assert_height: uint32 = uint32(0)
assert_seconds: uint64 = uint64(0)
assert_before_height: Optional[uint32] = None
assert_before_seconds: Optional[uint64] = None
@ -111,6 +112,7 @@ def compute_assert_height(
ret = TimelockConditions()
ret.assert_height = uint32(conds.height_absolute)
ret.assert_seconds = uint64(conds.seconds_absolute)
ret.assert_before_height = (
uint32(conds.before_height_absolute) if conds.before_height_absolute is not None else None
)
@ -123,6 +125,10 @@ def compute_assert_height(
h = uint32(removal_coin_records[bytes32(spend.coin_id)].confirmed_block_index + spend.height_relative)
ret.assert_height = max(ret.assert_height, h)
if spend.seconds_relative is not None:
s = uint64(removal_coin_records[bytes32(spend.coin_id)].timestamp + spend.seconds_relative)
ret.assert_seconds = max(ret.assert_seconds, s)
if spend.before_height_relative is not None:
h = uint32(
removal_coin_records[bytes32(spend.coin_id)].confirmed_block_index + spend.before_height_relative
@ -504,6 +510,13 @@ class MempoolManager:
timelocks: TimelockConditions = compute_assert_height(removal_record_dict, npc_result.conds)
if timelocks.assert_before_height is not None and timelocks.assert_before_height <= timelocks.assert_height:
# returning None as the "potential" means it failed. We won't store it
# in the pending cache
return Err.IMPOSSIBLE_HEIGHT_ABSOLUTE_CONSTRAINTS, None, [] # MempoolInclusionStatus.FAILED
if timelocks.assert_before_seconds is not None and timelocks.assert_before_seconds <= timelocks.assert_seconds:
return Err.IMPOSSIBLE_SECONDS_ABSOLUTE_CONSTRAINTS, None, [] # MempoolInclusionStatus.FAILED
potential = MempoolItem(
new_spend,
uint64(fees),

View File

@ -256,9 +256,9 @@ class TestCheckTimeLocks:
def expect(
*, height: int = 0, before_height: Optional[int] = None, before_seconds: Optional[int] = None
*, height: int = 0, seconds: int = 0, before_height: Optional[int] = None, before_seconds: Optional[int] = None
) -> TimelockConditions:
ret = TimelockConditions(uint32(height))
ret = TimelockConditions(uint32(height), uint64(seconds))
if before_height is not None:
ret.assert_before_height = uint32(before_height)
if before_seconds is not None:
@ -312,6 +312,21 @@ def expect(
(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)),
# ASSERT_SECONDS_*
# coin timestamp is 10000
# single absolute assert seconds
(make_test_conds(seconds_absolute=20000), expect(seconds=20000)),
# coin is created at 10000 + 100 relative seconds = 10100
(make_test_conds(seconds_relative=100), expect(seconds=10100)),
# coin is created at 10000 + 0 relative seconds = 10000
(make_test_conds(seconds_relative=0), expect(seconds=10000)),
# 20000 is more restrictive than 10100
(make_test_conds(seconds_absolute=20000, seconds_relative=100), expect(seconds=20000)),
# 20000 is a relative seconds, and since the coin was confirmed at seconds
# 10000 that's 300000
(make_test_conds(seconds_absolute=20000, seconds_relative=20000), expect(seconds=30000)),
# Same thing but without the absolute seconds
(make_test_conds(seconds_relative=20000), expect(seconds=30000)),
],
)
def test_compute_assert_height(conds: SpendBundleConditions, expected: TimelockConditions) -> None:
@ -1568,3 +1583,167 @@ async def test_identical_spend_aggregation_e2e(
)
assert len(eligible_coins) == 1
assert eligible_coins[0].coin.amount == 42
# we have two coins in this test. They have different birth heights (and
# timestamps)
# coin1: amount=1, confirmed_height=10, timestamp=1000
# coin2: amount=2, confirmed_height=20, timestamp=2000
# the mempool is at height 21 and timestamp 2010
@pytest.mark.anyio
@pytest.mark.parametrize(
"cond1,cond2,expected",
[
# ASSERT HEIGHT ABSOLUTE
(
[co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 30],
[co.ASSERT_HEIGHT_ABSOLUTE, 30],
Err.IMPOSSIBLE_HEIGHT_ABSOLUTE_CONSTRAINTS,
),
(
[co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 31],
[co.ASSERT_HEIGHT_ABSOLUTE, 30],
None,
),
(
[co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 21],
[co.ASSERT_HEIGHT_ABSOLUTE, 20],
Err.ASSERT_BEFORE_HEIGHT_ABSOLUTE_FAILED,
),
# ASSERT SECONDS ABSOLUTE
(
[co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 3000],
[co.ASSERT_SECONDS_ABSOLUTE, 3000],
Err.IMPOSSIBLE_SECONDS_ABSOLUTE_CONSTRAINTS,
),
(
[co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 3001],
[co.ASSERT_SECONDS_ABSOLUTE, 3000],
Err.ASSERT_SECONDS_ABSOLUTE_FAILED,
),
(
[co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 2001],
[co.ASSERT_SECONDS_ABSOLUTE, 2000],
Err.ASSERT_BEFORE_SECONDS_ABSOLUTE_FAILED,
),
# ASSERT HEIGHT RELATIVE
# coin1: height=10
# coin2: height=20
(
[co.ASSERT_BEFORE_HEIGHT_RELATIVE, 15],
[co.ASSERT_HEIGHT_RELATIVE, 5],
Err.IMPOSSIBLE_HEIGHT_ABSOLUTE_CONSTRAINTS,
),
(
[co.ASSERT_BEFORE_HEIGHT_RELATIVE, 26],
[co.ASSERT_HEIGHT_RELATIVE, 15],
None,
),
(
[co.ASSERT_BEFORE_HEIGHT_RELATIVE, 16],
[co.ASSERT_HEIGHT_RELATIVE, 5],
None,
),
# ASSERT SECONDS RELATIVE
# coin1: timestamp=1000
# coin2: timestamp=2000
(
[co.ASSERT_BEFORE_SECONDS_RELATIVE, 1500],
[co.ASSERT_SECONDS_RELATIVE, 500],
Err.IMPOSSIBLE_SECONDS_ABSOLUTE_CONSTRAINTS,
),
# we don't have a pending cache for seconds timelocks, so these fail
# immediately
(
[co.ASSERT_BEFORE_SECONDS_RELATIVE, 2501],
[co.ASSERT_SECONDS_RELATIVE, 1500],
Err.ASSERT_SECONDS_RELATIVE_FAILED,
),
(
[co.ASSERT_BEFORE_SECONDS_RELATIVE, 1501],
[co.ASSERT_SECONDS_RELATIVE, 500],
Err.ASSERT_SECONDS_RELATIVE_FAILED,
),
# ASSERT HEIGHT RELATIVE and ASSERT HEIGHT ABSOLUTE
# coin1: height=10
# coin2: height=20
(
[co.ASSERT_BEFORE_HEIGHT_RELATIVE, 20],
[co.ASSERT_HEIGHT_ABSOLUTE, 30],
Err.IMPOSSIBLE_HEIGHT_ABSOLUTE_CONSTRAINTS,
),
(
[co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 30],
[co.ASSERT_HEIGHT_RELATIVE, 10],
Err.IMPOSSIBLE_HEIGHT_ABSOLUTE_CONSTRAINTS,
),
(
[co.ASSERT_BEFORE_HEIGHT_RELATIVE, 21],
[co.ASSERT_HEIGHT_ABSOLUTE, 30],
None,
),
(
[co.ASSERT_BEFORE_HEIGHT_ABSOLUTE, 31],
[co.ASSERT_HEIGHT_RELATIVE, 10],
None,
),
# ASSERT SECONDS ABSOLUTE and ASSERT SECONDS RELATIVE
(
[co.ASSERT_BEFORE_SECONDS_RELATIVE, 2000],
[co.ASSERT_SECONDS_ABSOLUTE, 3000],
Err.IMPOSSIBLE_SECONDS_ABSOLUTE_CONSTRAINTS,
),
(
[co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 3000],
[co.ASSERT_SECONDS_RELATIVE, 1000],
Err.IMPOSSIBLE_SECONDS_ABSOLUTE_CONSTRAINTS,
),
# we don't have a pending cache for seconds timelocks, so these fail
# immediately
(
[co.ASSERT_BEFORE_SECONDS_RELATIVE, 2001],
[co.ASSERT_SECONDS_ABSOLUTE, 3000],
Err.ASSERT_SECONDS_ABSOLUTE_FAILED,
),
(
[co.ASSERT_BEFORE_SECONDS_ABSOLUTE, 3001],
[co.ASSERT_SECONDS_RELATIVE, 1000],
Err.ASSERT_SECONDS_RELATIVE_FAILED,
),
],
)
async def test_mempool_timelocks(cond1: List[object], cond2: List[object], expected: Optional[Err]) -> None:
coins = []
test_coin_records = {}
coin = Coin(IDENTITY_PUZZLE_HASH, IDENTITY_PUZZLE_HASH, uint64(1))
coins.append(coin)
test_coin_records[coin.name()] = CoinRecord(coin, uint32(10), uint32(0), False, uint64(1000))
coin = Coin(IDENTITY_PUZZLE_HASH, IDENTITY_PUZZLE_HASH, uint64(2))
coins.append(coin)
test_coin_records[coin.name()] = CoinRecord(coin, uint32(20), uint32(0), False, uint64(2000))
async def get_coin_record(coin_id: bytes32) -> Optional[CoinRecord]:
return test_coin_records.get(coin_id)
mempool_manager = await instantiate_mempool_manager(
get_coin_record, block_height=uint32(21), block_timestamp=uint64(2010)
)
coin_spends = [
make_spend(coins[0], IDENTITY_PUZZLE, Program.to([cond1])),
make_spend(coins[1], IDENTITY_PUZZLE, Program.to([cond2])),
]
bundle = SpendBundle(coin_spends, G2Element())
bundle_name = bundle.name()
try:
result = await add_spendbundle(mempool_manager, bundle, bundle_name)
print(result)
if expected is not None:
assert result == (None, MempoolInclusionStatus.FAILED, expected)
else:
assert result[0] is not None
assert result[1] != MempoolInclusionStatus.FAILED
except ValidationError as e:
assert e.code == expected