chia-blockchain/chia/wallet/puzzles/singleton_top_layer_v1_1.py
Matt Hauff e5b94d34a0
Chialisp file extensions (#14996)
* Rename chialisp file extensions

* Add pre-commit check for .clvm files

* Delete sha256tree files

* remove hash files in favor of central dictionary

* Add check for missing files like hex and hash entries

* Enhance clsp pre-commit check

* Actually check hash matches

* Update tools/manage_clvm.py

Co-authored-by: Kyle Altendorf <sda@fstab.net>

* Update tools/manage_clvm.py

Co-authored-by: Kyle Altendorf <sda@fstab.net>

* Fix Windows file writing

* Fix setup.py package_data fields

* Load hash dict at runtime

* Move away from exception pattern

* Bad equality check

* Minor fixes

* remove trailing whitespace fix

---------

Co-authored-by: Kyle Altendorf <sda@fstab.net>
2023-04-18 11:27:17 -05:00

350 lines
12 KiB
Python

from __future__ import annotations
from typing import Iterator, List, Optional, Tuple
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import CoinSpend
from chia.types.condition_opcodes import ConditionOpcode
from chia.util.hash import std_hash
from chia.util.ints import uint64
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile
from chia.wallet.uncurried_puzzle import UncurriedPuzzle
SINGLETON_MOD = load_clvm_maybe_recompile("singleton_top_layer_v1_1.clsp")
SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash()
P2_SINGLETON_MOD = load_clvm_maybe_recompile("p2_singleton.clsp")
P2_SINGLETON_OR_DELAYED_MOD = load_clvm_maybe_recompile("p2_singleton_or_delayed_puzhash.clsp")
SINGLETON_LAUNCHER = load_clvm_maybe_recompile("singleton_launcher.clsp")
SINGLETON_LAUNCHER_HASH = SINGLETON_LAUNCHER.get_tree_hash()
ESCAPE_VALUE = -113
MELT_CONDITION = [ConditionOpcode.CREATE_COIN, 0, ESCAPE_VALUE]
#
# An explanation of how this functions from a user's perspective
#
# Consider that you have some coin A that you want to create a singleton
# containing some inner puzzle I from with amount T. We'll call the Launcher
# coin, which is created from A "Launcher" and the first iteration of the
# singleton, called the "Eve" spend, Eve. When spent, I yields a coin
# running I' and so on in a singleton specific way described below.
#
# The structure of this on the blockchain when done looks like this
#
# ,------------.
# | Coin A |
# `------------'
# |
# ------------------ Atomic Transaction 1 -----------------
# v
# .------------. .-------------------------------.
# | Launcher |------>| Eve Coin Containing Program I |
# `------------' `-------------------------------'
# |
# -------------------- End Transaction 1 ------------------\
# | > The Eve coin
# --------------- (2) Transaction With I ------------------/ may also be
# | spent
# v simultaneously
# .-----------------------------------.
# | Running Singleton With Program I' |
# `-----------------------------------'
# |
# --------------------- End Transaction 2 ------------------
# |
# --------------- (3) Transaction With I' ------------------
# ...
#
#
# == Practical use of singleton_top_layer.py ==
#
# 1) Designate some coin as coin A
#
# 2) call puzzle_for_singleton with that coin's name (it is the Parent of the
# Launch coin), and the initial inner puzzle I, curried as appropriate for
# its own purpose. Adaptations of the program I and its descendants are
# required as below.
#
# 3) call launch_conditions_and_coinsol to get a set of "launch_conditions",
# which will be used to spend standard coin A, and a "spend", which spends
# the Launcher created by the application of "launch_conditions" to A in a
# spend. These actions must be done in the same spend bundle.
#
# One can create a SpendBundle containing the spend of A giving it the
# argument list (() (q . launch_conditions) ()) and then append "spend" onto
# its .coin_spends to create a combined spend bundle.
#
# 4) submit the combine spend bundle.
#
# 5) Remember the identity of the Launcher coin:
#
# Coin(A.name(), SINGLETON_LAUNCHER_HASH, amount)
#
# A singleton has been created like this:
#
# Coin(Launcher.name(), puzzle_for_singleton(Launcher.name(), I), amount)
#
#
# == To spend the singleton requires some host side setup ==
#
# The singleton adds an ASSERT_MY_COIN_ID to constrain it to the coin that
# matches its own conception of itself. It consumes a "LineageProof" object
# when spent that must be constructed so. We'll call the singleton we intend
# to spend "S".
#
# Specifically, the required puzzle is the Inner puzzle I for the parent of S
# unless S is the Eve coin, in which case it is None.
# So to spend S', the second singleton, I is used, and to spend S'', I' is used.
# We'll call this puzzle hash (or None) PH.
#
# If this is the Eve singleton:
#
# PH = None
# L = LineageProof(Launcher, PH, amount)
#
# - Note: the Eve singleton's .parent_coin_info should match Launcher here.
#
# Otherwise
#
# PH = ParentOf(S).inner_puzzle_hash
# L = LineageProof(ParentOf(S).name(), PH, amount)
#
# - Note: ParentOf(S).name is the .parent_coin_info member of the
# coin record for S.
#
# Now the coin S can be spent.
# The puzzle to use in the spend is given by
#
# puzzle_for_singleton(S.name(), I'.puzzle_hash())
#
# and the arguments are given by (with the argument list to I designated AI)
#
# solution_for_singleton(L, amount, AI)
#
# Note that AI contains dynamic arguments to puzzle I _after_ the singleton
# truths.
#
#
# Adapting puzzles to the singleton
#
# 1) For the puzzle to create a coin from inside the singleton it will need the
# following values to be added to its curried in arguments:
#
# - A way to compute its own puzzle has for each of I' and so on. This can
# be accomplished by giving it its uncurried puzzle hash and using
# puzzle-hash-of-curried-function to compute it. Although full_puzzle_hash
# is used for some arguments, the inputs to all singleton_top_layer
# functions is the inner puzzle.
#
# - the name() of the Launcher coin (which you can compute from a Coin
# object) if you're not already using it in I puzzle for some other
# reason.
#
# 2) A non-curried argument called "singleton_truths" will be passed to your
# program. It is not required to use anything inside.
#
# There is little value in not receiving this argument via the adaptations
# below as a standard puzzle can't be used anyway. To work the result must
# be itself a singleton, and the singleton does not change the puzzle hash
# in an outgoing CREATE_COIN to cause it to be one.
#
# With this modification of the program I done, I and descendants will
# continue to produce I', I'' etc.
#
# The actual CREATE_COIN puzzle hash will be the result of
# this. The Launcher ID referred to here is the name() of
# the Launcher coin as above.
#
def match_singleton_puzzle(puzzle: UncurriedPuzzle) -> Tuple[bool, Iterator[Program]]:
if puzzle.mod == SINGLETON_MOD:
return True, puzzle.args.as_iter()
else:
return False, iter(())
# Given the parent and amount of the launcher coin, return the launcher coin
def generate_launcher_coin(coin: Coin, amount: uint64) -> Coin:
return Coin(coin.name(), SINGLETON_LAUNCHER_HASH, amount)
def remove_singleton_truth_wrapper(puzzle: Program) -> Program:
inner_puzzle: Program = puzzle.rest().first().rest()
return inner_puzzle
# Take standard coin and amount -> launch conditions & launcher coin solution
def launch_conditions_and_coinsol(
coin: Coin,
inner_puzzle: Program,
comment: List[Tuple[str, str]],
amount: uint64,
) -> Tuple[List[Program], CoinSpend]:
if (amount % 2) == 0:
raise ValueError("Coin amount cannot be even. Subtract one mojo.")
launcher_coin: Coin = generate_launcher_coin(coin, amount)
curried_singleton: Program = SINGLETON_MOD.curry(
(SINGLETON_MOD_HASH, (launcher_coin.name(), SINGLETON_LAUNCHER_HASH)),
inner_puzzle,
)
launcher_solution = Program.to(
[
curried_singleton.get_tree_hash(),
amount,
comment,
]
)
create_launcher = Program.to(
[
ConditionOpcode.CREATE_COIN,
SINGLETON_LAUNCHER_HASH,
amount,
],
)
assert_launcher_announcement = Program.to(
[
ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT,
std_hash(launcher_coin.name() + launcher_solution.get_tree_hash()),
],
)
conditions = [create_launcher, assert_launcher_announcement]
launcher_coin_spend = CoinSpend(
launcher_coin,
SINGLETON_LAUNCHER,
launcher_solution,
)
return conditions, launcher_coin_spend
# Take a coin solution, return a lineage proof for their child to use in spends
def lineage_proof_for_coinsol(coin_spend: CoinSpend) -> LineageProof:
parent_name: bytes32 = coin_spend.coin.parent_coin_info
inner_puzzle_hash: Optional[bytes32] = None
if coin_spend.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH:
full_puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
r = full_puzzle.uncurry()
if r is not None:
_, args = r
_, inner_puzzle = list(args.as_iter())
inner_puzzle_hash = inner_puzzle.get_tree_hash()
amount: uint64 = uint64(coin_spend.coin.amount)
return LineageProof(
parent_name,
inner_puzzle_hash,
amount,
)
# Return the puzzle reveal of a singleton with specific ID and innerpuz
def puzzle_for_singleton(
launcher_id: bytes32, inner_puz: Program, launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH
) -> Program:
return SINGLETON_MOD.curry(
(SINGLETON_MOD_HASH, (launcher_id, launcher_hash)),
inner_puz,
)
# Return a solution to spend a singleton
def solution_for_singleton(
lineage_proof: LineageProof,
amount: uint64,
inner_solution: Program,
) -> Program:
if lineage_proof.inner_puzzle_hash is None:
parent_info = [
lineage_proof.parent_name,
lineage_proof.amount,
]
else:
parent_info = [
lineage_proof.parent_name,
lineage_proof.inner_puzzle_hash,
lineage_proof.amount,
]
solution: Program = Program.to([parent_info, amount, inner_solution])
return solution
# Create a coin that a singleton can claim
def pay_to_singleton_puzzle(launcher_id: bytes32) -> Program:
return P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, SINGLETON_LAUNCHER_HASH)
# Create a coin that a singleton can claim or that can be sent to another puzzle after a specified time
def pay_to_singleton_or_delay_puzzle(launcher_id: bytes32, delay_time: uint64, delay_ph: bytes32) -> Program:
return P2_SINGLETON_OR_DELAYED_MOD.curry(
SINGLETON_MOD_HASH,
launcher_id,
SINGLETON_LAUNCHER_HASH,
delay_time,
delay_ph,
)
# Solution for EITHER p2_singleton or the claiming spend case for p2_singleton_or_delayed_puzhash
def solution_for_p2_singleton(p2_singleton_coin: Coin, singleton_inner_puzhash: bytes32) -> Program:
solution: Program = Program.to([singleton_inner_puzhash, p2_singleton_coin.name()])
return solution
# Solution for the delayed spend case for p2_singleton_or_delayed_puzhash
def solution_for_p2_delayed_puzzle(output_amount: uint64) -> Program:
solution: Program = Program.to([output_amount, []])
return solution
# Get announcement conditions for singleton solution and full CoinSpend for the claimed coin
def claim_p2_singleton(
p2_singleton_coin: Coin,
singleton_inner_puzhash: bytes32,
launcher_id: bytes32,
delay_time: Optional[uint64] = None,
delay_ph: Optional[bytes32] = None,
) -> Tuple[Program, Program, CoinSpend]:
assertion = Program.to([ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, std_hash(p2_singleton_coin.name() + b"$")])
announcement = Program.to([ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, p2_singleton_coin.name()])
if delay_time is None or delay_ph is None:
puzzle: Program = pay_to_singleton_puzzle(launcher_id)
else:
puzzle = pay_to_singleton_or_delay_puzzle(
launcher_id,
delay_time,
delay_ph,
)
claim_coinsol = CoinSpend(
p2_singleton_coin,
puzzle,
solution_for_p2_singleton(p2_singleton_coin, singleton_inner_puzhash),
)
return assertion, announcement, claim_coinsol
# Get the CoinSpend for spending to a delayed puzzle
def spend_to_delayed_puzzle(
p2_singleton_coin: Coin,
output_amount: uint64,
launcher_id: bytes32,
delay_time: uint64,
delay_ph: bytes32,
) -> CoinSpend:
claim_coinsol = CoinSpend(
p2_singleton_coin,
pay_to_singleton_or_delay_puzzle(launcher_id, delay_time, delay_ph),
solution_for_p2_delayed_puzzle(output_amount),
)
return claim_coinsol