diff --git a/chia/_tests/clvm/test_restrictions.py b/chia/_tests/clvm/test_restrictions.py index 87ad68a444a9..8242a78160bf 100644 --- a/chia/_tests/clvm/test_restrictions.py +++ b/chia/_tests/clvm/test_restrictions.py @@ -11,8 +11,9 @@ from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.errors import Err from chia.util.ints import uint64 +from chia.wallet.conditions import CreateCoinAnnouncement 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_puzzles.restrictions import ForceCoinAnnouncement, Timelock from chia.wallet.puzzles.custody.restriction_utilities import ValidatorStackRestriction from chia.wallet.wallet_spend_bundle import WalletSpendBundle @@ -136,3 +137,65 @@ async def test_timelock_wrapper(cost_logger: CostLogger) -> None: # memo format assertion for coverage sake assert restriction.memo(0) == Program.to([None]) + + +@pytest.mark.anyio +async def test_force_coin_announcement_wrapper(cost_logger: CostLogger) -> None: + async with sim_and_client() as (sim, client): + restriction = ValidatorStackRestriction([ForceCoinAnnouncement()]) + 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 coin announcement in the dpuz + announcement = CreateCoinAnnouncement(bytes32.zeros, coin_id=coin.name()) + announcement_dpuz = DelegatedPuzzleAndSolution( + puzzle=Program.to( + (1, [announcement.to_program(), announcement.corresponding_assertion().to_program(), [1, "foo"]]) + ), + solution=Program.to(None), + ) + wrapped_dpuz = restriction.modify_delegated_puzzle_and_solution(announcement_dpuz, [Program.to(None)]) + sb = cost_logger.add_cost( + "Minimal puzzle with restrictions w/ coin announcement forcing wrapper", + WalletSpendBundle( + [ + make_spend( + coin, + pwr.puzzle_reveal(), + pwr.solve( + [], + [Program.to([announcement_dpuz.puzzle.get_tree_hash()])], + Program.to([[1, "bar"]]), + wrapped_dpuz, + ), + ) + ], + G2Element(), + ), + ) + result = await client.push_tx(sb) + assert result == (MempoolInclusionStatus.SUCCESS, None) + + # memo format assertion for coverage sake + assert restriction.memo(0) == Program.to([None]) diff --git a/chia/wallet/puzzles/custody/restriction_puzzles/restrictions.py b/chia/wallet/puzzles/custody/restriction_puzzles/restrictions.py index 1c14fea64858..2edf9c70068b 100644 --- a/chia/wallet/puzzles/custody/restriction_puzzles/restrictions.py +++ b/chia/wallet/puzzles/custody/restriction_puzzles/restrictions.py @@ -10,6 +10,11 @@ TIMELOCK_WRAPPER = load_clvm_maybe_recompile( "timelock.clsp", package_or_requirement="chia.wallet.puzzles.custody.restriction_puzzles.wrappers" ) +FORCE_COIN_ANNOUNCEMENT_WRAPPER = load_clvm_maybe_recompile( + "force_assert_coin_announcement.clsp", + package_or_requirement="chia.wallet.puzzles.custody.restriction_puzzles.wrappers", +) +FORCE_COIN_ANNOUNCEMENT_WRAPPER_HASH = FORCE_COIN_ANNOUNCEMENT_WRAPPER.get_tree_hash() @dataclass(frozen=True) @@ -28,3 +33,19 @@ def puzzle(self, nonce: int) -> Program: def puzzle_hash(self, nonce: int) -> bytes32: return self.puzzle(nonce).get_tree_hash() + + +@dataclass(frozen=True) +class ForceCoinAnnouncement: + @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 FORCE_COIN_ANNOUNCEMENT_WRAPPER + + def puzzle_hash(self, nonce: int) -> bytes32: + return FORCE_COIN_ANNOUNCEMENT_WRAPPER_HASH diff --git a/chia/wallet/puzzles/custody/restriction_puzzles/wrappers/force_assert_coin_announcement.clsp b/chia/wallet/puzzles/custody/restriction_puzzles/wrappers/force_assert_coin_announcement.clsp new file mode 100644 index 000000000000..6f1555b516f8 --- /dev/null +++ b/chia/wallet/puzzles/custody/restriction_puzzles/wrappers/force_assert_coin_announcement.clsp @@ -0,0 +1,21 @@ +; This is a restriction on delegated puzzles intended to force a coin announcement +; +; The idea behind enforcing this is that it makes the current coin spend non-replayable since it will be implictly +; asserting that a specific coin ID is spent in tandem. +(mod + ( + Conditions + ) + + (include condition_codes.clib) + + (defun check_conditions (conditions) + ; If we run out of conditions without finding an assertion, this will raise with "path into atom" + (if (= (f (f conditions)) ASSERT_COIN_ANNOUNCEMENT) + conditions + (c (f conditions) (check_conditions (r conditions))) + ) + ) + + (check_conditions Conditions) +) diff --git a/chia/wallet/puzzles/custody/restriction_puzzles/wrappers/force_assert_coin_announcement.clsp.hex b/chia/wallet/puzzles/custody/restriction_puzzles/wrappers/force_assert_coin_announcement.clsp.hex new file mode 100644 index 000000000000..3599cc8138d8 --- /dev/null +++ b/chia/wallet/puzzles/custody/restriction_puzzles/wrappers/force_assert_coin_announcement.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff02ff06ffff04ff02ffff04ff05ff80808080ffff04ffff01ff3dff02ffff03ffff09ff11ff0480ffff0105ffff01ff04ff09ffff02ff06ffff04ff02ffff04ff0dff808080808080ff0180ff018080 diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 036a8c37ec57..07073f4aae51 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -33,6 +33,7 @@ "everything_with_signature": "1720d13250a7c16988eaf530331cefa9dd57a76b2c82236bec8bbbff91499b89", "exigent_metadata_layer": "d5fd32e069fda83e230ccd8f6a7c4f652231aed5c755514b3d996cbeff4182b8", "flag_proofs_checker": "fe2e3c631562fbb9be095297f762bf573705a0197164e9361ad5d50e045ba241", + "force_assert_coin_announcement": "ca0daca027e5ebd4a61fad7e32cfe1e984ad5b561c2fc08dea30accf3a191fab", "genesis_by_coin_id": "493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150", "genesis_by_coin_id_or_singleton": "40170305e3a71c3e7523f37fbcfc3188f9f949da0818a6331f28251e76e8c56f", "genesis_by_puzzle_hash": "de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36",