Skip to content

Commit

Permalink
Add Timelock restriction (#18838)
Browse files Browse the repository at this point in the history
This adds a restriction to be used with `ValidatorStackRestriction` that
enforces a timelock condition with a specific value be in the delegated
puzzle output.
  • Loading branch information
Quexington authored Nov 8, 2024
2 parents 00a7f1a + 9c41a49 commit adeb3ed
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 0 deletions.
65 changes: 65 additions & 0 deletions chia/_tests/clvm/test_restrictions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from chia.types.coin_spend import make_spend
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.util.errors import Err
from chia.util.ints import uint64
from chia.wallet.puzzles.custody.custody_architecture import DelegatedPuzzleAndSolution, PuzzleWithRestrictions
from chia.wallet.puzzles.custody.restriction_puzzles.restrictions import Timelock
from chia.wallet.puzzles.custody.restriction_utilities import ValidatorStackRestriction
from chia.wallet.wallet_spend_bundle import WalletSpendBundle

Expand Down Expand Up @@ -71,3 +73,66 @@ async def test_dpuz_validator_stack_restriction(cost_logger: CostLogger) -> None

# memo format assertion for coverage sake
assert restriction.memo(0) == Program.to([None])


@pytest.mark.anyio
async def test_timelock_wrapper(cost_logger: CostLogger) -> None:
async with sim_and_client() as (sim, client):
restriction = ValidatorStackRestriction([Timelock(uint64(100))])
pwr = PuzzleWithRestrictions(0, [restriction], ACSMember())

# Farm and find coin
await sim.farm_block(pwr.puzzle_hash())
coin = (await client.get_coin_records_by_puzzle_hashes([pwr.puzzle_hash()], include_spent_coins=False))[0].coin

# Attempt to just use any old dpuz
any_old_dpuz = DelegatedPuzzleAndSolution(puzzle=Program.to((1, [[1, "foo"]])), solution=Program.to(None))
wrapped_dpuz = restriction.modify_delegated_puzzle_and_solution(any_old_dpuz, [Program.to(None)])
not_timelocked_attempt = WalletSpendBundle(
[
make_spend(
coin,
pwr.puzzle_reveal(),
pwr.solve(
[], [Program.to([any_old_dpuz.puzzle.get_tree_hash()])], Program.to([[1, "bar"]]), any_old_dpuz
),
)
],
G2Element(),
)
result = await client.push_tx(not_timelocked_attempt)
assert result == (MempoolInclusionStatus.FAILED, Err.GENERATOR_RUNTIME_ERROR)

# Now actually put a timelock in the dpuz
timelocked_dpuz = DelegatedPuzzleAndSolution(
puzzle=Program.to((1, [[80, 100], [1, "foo"]])), solution=Program.to(None)
)
wrapped_dpuz = restriction.modify_delegated_puzzle_and_solution(timelocked_dpuz, [Program.to(None)])
sb = cost_logger.add_cost(
"Minimal puzzle with restrictions w/ timelock wrapper",
WalletSpendBundle(
[
make_spend(
coin,
pwr.puzzle_reveal(),
pwr.solve(
[],
[Program.to([timelocked_dpuz.puzzle.get_tree_hash()])],
Program.to([[1, "bar"]]),
wrapped_dpuz,
),
)
],
G2Element(),
),
)
result = await client.push_tx(sb)
assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_SECONDS_RELATIVE_FAILED)

sim.pass_time(uint64(100))
await sim.farm_block()
result = await client.push_tx(sb)
assert result == (MempoolInclusionStatus.SUCCESS, None)

# memo format assertion for coverage sake
assert restriction.memo(0) == Program.to([None])
30 changes: 30 additions & 0 deletions chia/wallet/puzzles/custody/restriction_puzzles/restrictions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from __future__ import annotations

from dataclasses import dataclass

from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.ints import uint64
from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile

TIMELOCK_WRAPPER = load_clvm_maybe_recompile(
"timelock.clsp", package_or_requirement="chia.wallet.puzzles.custody.restriction_puzzles.wrappers"
)


@dataclass(frozen=True)
class Timelock:
timelock: uint64

@property
def member_not_dpuz(self) -> bool:
return False

def memo(self, nonce: int) -> Program:
return Program.to(None)

def puzzle(self, nonce: int) -> Program:
return TIMELOCK_WRAPPER.curry(self.timelock)

def puzzle_hash(self, nonce: int) -> bytes32:
return self.puzzle(nonce).get_tree_hash()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
(mod
(
TIMELOCK
Conditions
)

(include condition_codes.clib)
(include utility_macros.clib)

(defun check_conditions (TIMELOCK conditions)
(if (and (= (f (f conditions)) ASSERT_SECONDS_RELATIVE) (= (f (r (f conditions))) TIMELOCK))
conditions
(check_conditions TIMELOCK (r conditions))
)
)

(check_conditions TIMELOCK Conditions)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ff02ffff01ff02ff06ffff04ff02ffff04ff05ffff04ff0bff8080808080ffff04ffff01ff50ff02ffff03ffff02ffff03ffff09ff23ff0480ffff01ff02ffff03ffff09ff53ff0580ffff01ff0101ff8080ff0180ff8080ff0180ffff010bffff01ff02ff06ffff04ff02ffff04ff05ffff04ff1bff808080808080ff0180ff018080
1 change: 1 addition & 0 deletions chia/wallet/puzzles/deployed_puzzle_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@
"std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6",
"test_generator_deserialize": "52add794fc76e89512e4a063c383418bda084c8a78c74055abe80179e4a7832c",
"test_multiple_generator_input_arguments": "156dafbddc3e1d3bfe1f2a84e48e5e46b287b8358bf65c3c091c93e855fbfc5b",
"timelock": "107c49e8c688883e65ec476b9568d90908f6fc0f9ef97017dfdcb3e4ae87766c",
"viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51"
}

0 comments on commit adeb3ed

Please sign in to comment.