Convert PlotNFT drivers

This commit is contained in:
Matt
2026-08-19 20:40:03 +12:00
parent 5bdbbf3fc1
commit 0b6485d693
13 changed files with 620 additions and 520 deletions
+4 -4
View File
@@ -109,7 +109,7 @@ async def test_singleton_top_layer(cost_logger: CostLogger) -> None:
await push_bundle(
sim,
sim_client,
[starting_spend, launch_result.launcher_spend],
[starting_spend, *launch_result.necessary_spends],
cost_logger=cost_logger,
cost_log_msg="Singleton Launch + ACS",
)
@@ -126,7 +126,7 @@ async def test_singleton_top_layer(cost_logger: CostLogger) -> None:
cost_logger=cost_logger,
cost_log_msg="Singleton Eve Spend w/ ACS",
)
singleton.inner_puzzle = ACS
singleton = singleton.with_inner_puzzle(ACS)
await sim.farm_block(ACS_PH) # for non-FF spends
@@ -140,7 +140,7 @@ async def test_singleton_top_layer(cost_logger: CostLogger) -> None:
cost_logger=cost_logger,
cost_log_msg="Singleton Spend + ACS",
)
singleton.inner_puzzle = ACS
singleton = singleton.with_inner_puzzle(ACS)
# CLAIM A P2_SINGLETON
assert singleton.coin == await odd_singleton_coin(sim)
@@ -171,7 +171,7 @@ async def test_singleton_top_layer(cost_logger: CostLogger) -> None:
cost_logger=cost_logger,
cost_log_msg="Singleton w/ ACS claim p2_singleton",
)
singleton.inner_puzzle = ACS
singleton = singleton.with_inner_puzzle(ACS)
assert len(await sim_client.get_coin_records_by_puzzle_hash(bytes32.zeros, include_spent_coins=False)) == 1
# CREATE MULTIPLE ODD CHILDREN (Negative Test)
+91 -69
View File
@@ -13,12 +13,9 @@ from chia._tests.util.spend_sim import CostLogger, SimClient, SpendSim, sim_and_
from chia.pools.plotnft_drivers import (
GetNextPlotNFTError,
PlotNFT,
PlotNFTPuzzle,
PlotNFTInnerPuzzle,
PoolConfig,
PoolReward,
RewardPuzzle,
SingletonPuzzles,
SingletonStruct,
UserConfig,
)
from chia.types.blockchain_format.program import Program, run
@@ -34,11 +31,13 @@ from chia.wallet.conditions import (
SendMessage,
parse_conditions_non_consensus,
)
from chia.wallet.puzzles.custody.custody_architecture import DelegatedPuzzleAndSolution, PuzzleWithRestrictions
from chia.wallet.puzzles.custody.custody_architecture import PuzzleWithRestrictions
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
)
from chia.wallet.puzzles.puzzle_drivers import DelegatedPuzzleAndSolution, UnknownPuzzle, UnknownSolution
from chia.wallet.puzzles.singleton_drivers import P2SingletonPuzzle, SingletonCorePuzzles, SingletonStruct
from chia.wallet.uncurried_puzzle import UncurriedPuzzle
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
@@ -71,7 +70,7 @@ async def mint_plotnft(
fund_coin, _ = await sim_client.get_coin_records_by_puzzle_hash(ACS_PH, include_spent_coins=False)
conditions, spends, plotnft = PlotNFT.launch(
launch_result = PlotNFT.launch_plotnft(
origin_coins=[fund_coin.coin],
user_config=UserConfig(synthetic_pubkey=user_sk.get_g1()),
pool_config=None
@@ -81,23 +80,16 @@ async def mint_plotnft(
hint=bytes32.zeros,
exiting=desired_state == "waiting_room",
)
plotnft = launch_result.launched_singleton
result = await sim_client.push_tx(
WalletSpendBundle(
[
*spends,
*launch_result.necessary_spends,
make_spend(
coin=fund_coin.coin,
puzzle_reveal=ACS,
solution=Program.to(
[condition.to_program() for condition in conditions]
+ [
CreateCoin(
puzzle_hash=plotnft.singleton_struct.singleton_puzzles.singleton_launcher_hash,
amount=uint64(1),
).to_program()
]
),
solution=Program.to([condition.to_program() for condition in launch_result.necessary_conditions]),
),
],
G2Element(),
@@ -108,7 +100,9 @@ async def mint_plotnft(
# Test syncing from launcher
assert (
PlotNFT.get_next_from_coin_spend(coin_spend=spends[1], genesis_challenge=sim.defaults.GENESIS_CHALLENGE)
PlotNFT.get_next_from_coin_spend(
coin_spend=launch_result.necessary_spends[1], genesis_challenge=sim.defaults.GENESIS_CHALLENGE
)
== plotnft
)
return plotnft
@@ -123,14 +117,16 @@ async def test_plotnft_transitions(cost_logger: CostLogger) -> None:
with pytest.raises(ValueError, match=re.escape("Cannot exit to waiting room while self pooling.")):
plotnft.exit_to_waiting_room(
delegated_puzzle_and_solution=DelegatedPuzzleAndSolution(
puzzle=Program.to(None), solution=Program.to(None)
puzzle=UnknownPuzzle(known_puzzle=Program.to(None)),
solution=UnknownSolution(solution=Program.to(None)),
)
)
with pytest.raises(ValueError, match=re.escape("Cannot exit waiting room while self pooling.")):
plotnft.exit_waiting_room(
delegated_puzzle_and_solution=DelegatedPuzzleAndSolution(
puzzle=Program.to(None), solution=Program.to(None)
puzzle=UnknownPuzzle(known_puzzle=Program.to(None)),
solution=UnknownSolution(solution=Program.to(None)),
)
)
@@ -138,7 +134,7 @@ async def test_plotnft_transitions(cost_logger: CostLogger) -> None:
fee_hook = CreateCoinAnnouncement(msg=b"", coin_id=plotnft.coin.name())
url_remark = Remark(rest=Program.to("url"))
coin_spends = plotnft.join_pool(
user_config=plotnft.user_config,
user_config=plotnft.inner_puzzle.user_config,
pool_config=PoolConfig(
pool_puzzle_hash=POOL_PUZZLE_HASH, heightlock=uint32(5), pool_memoization=Program.to(["pool"])
),
@@ -167,21 +163,23 @@ async def test_plotnft_transitions(cost_logger: CostLogger) -> None:
with pytest.raises(ValueError, match=re.escape("Cannot exit waiting room while not in it")):
plotnft.exit_waiting_room(
delegated_puzzle_and_solution=DelegatedPuzzleAndSolution(
puzzle=Program.to(None), solution=Program.to(None)
puzzle=UnknownPuzzle(known_puzzle=Program.to(None)),
solution=UnknownSolution(solution=Program.to(None)),
)
)
# Attempt to leave without waiting room
quick_exit_dpuz_and_solution = DelegatedPuzzleAndSolution(
puzzle=ACS, solution=Program.to([CreateCoin(bytes32.zeros, uint64(1)).to_program()])
puzzle=UnknownPuzzle(known_puzzle=ACS),
solution=UnknownSolution(solution=Program.to([CreateCoin(bytes32.zeros, uint64(1)).to_program()])),
)
singing_info = plotnft.modify_delegated_puzzle_and_solution(quick_exit_dpuz_and_solution)
singing_info = plotnft.inner_puzzle.modify_delegated_puzzle_and_solution(quick_exit_dpuz_and_solution)
coin_spends = plotnft.exit_to_waiting_room(quick_exit_dpuz_and_solution)
result = await sim_client.push_tx(
WalletSpendBundle(
coin_spends,
user_sk.sign(
singing_info.puzzle.get_tree_hash() + plotnft.coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
singing_info.puzzle.puzzle_hash + plotnft.coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
),
)
)
@@ -189,25 +187,27 @@ async def test_plotnft_transitions(cost_logger: CostLogger) -> None:
# # Attempt to make a message while leaving
message_dpuz_and_solution = DelegatedPuzzleAndSolution(
puzzle=ACS,
solution=Program.to(
[
plotnft.exit_to_waiting_room_condition().to_program(),
SendMessage(
bytes32.zeros,
sender=MessageParticipant(parent_id_committed=bytes32.zeros),
receiver=MessageParticipant(parent_id_committed=bytes32.zeros),
).to_program(),
]
puzzle=UnknownPuzzle(known_puzzle=ACS),
solution=UnknownSolution(
solution=Program.to(
[
plotnft.inner_puzzle.exit_to_waiting_room_condition.to_program(),
SendMessage(
bytes32.zeros,
sender=MessageParticipant(parent_id_committed=bytes32.zeros),
receiver=MessageParticipant(parent_id_committed=bytes32.zeros),
).to_program(),
]
)
),
)
singing_info = plotnft.modify_delegated_puzzle_and_solution(message_dpuz_and_solution)
singing_info = plotnft.inner_puzzle.modify_delegated_puzzle_and_solution(message_dpuz_and_solution)
coin_spends = plotnft.exit_to_waiting_room(message_dpuz_and_solution)
result = await sim_client.push_tx(
WalletSpendBundle(
coin_spends,
user_sk.sign(
singing_info.puzzle.get_tree_hash() + plotnft.coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
singing_info.puzzle.puzzle_hash + plotnft.coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
),
)
)
@@ -215,10 +215,12 @@ async def test_plotnft_transitions(cost_logger: CostLogger) -> None:
# Leave honestly
honest_exit_dpuz_and_solution = DelegatedPuzzleAndSolution(
puzzle=ACS,
solution=Program.to([plotnft.exit_to_waiting_room_condition().to_program()]),
puzzle=UnknownPuzzle(known_puzzle=ACS),
solution=UnknownSolution(
solution=Program.to([plotnft.inner_puzzle.exit_to_waiting_room_condition.to_program()])
),
)
singing_info = plotnft.modify_delegated_puzzle_and_solution(honest_exit_dpuz_and_solution)
singing_info = plotnft.inner_puzzle.modify_delegated_puzzle_and_solution(honest_exit_dpuz_and_solution)
coin_spends = plotnft.exit_to_waiting_room(honest_exit_dpuz_and_solution)
result = await sim_client.push_tx(
cost_logger.add_cost(
@@ -226,54 +228,61 @@ async def test_plotnft_transitions(cost_logger: CostLogger) -> None:
WalletSpendBundle(
coin_spends,
user_sk.sign(
singing_info.puzzle.get_tree_hash()
+ plotnft.coin.name()
+ sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
singing_info.puzzle.puzzle_hash + plotnft.coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
),
),
)
)
assert result == (MempoolInclusionStatus.SUCCESS, None)
await sim.farm_block()
plotnft = PlotNFT.get_next_from_coin_spend(coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft)
plotnft = PlotNFT.get_next_from_coin_spend(
coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft.inner_puzzle
)
with pytest.raises(ValueError, match=re.escape("Already exiting to waiting room, cannot exit again")):
plotnft.exit_to_waiting_room(
delegated_puzzle_and_solution=DelegatedPuzzleAndSolution(
puzzle=Program.to(None), solution=Program.to(None)
puzzle=UnknownPuzzle(known_puzzle=Program.to(None)),
solution=UnknownSolution(solution=Program.to(None)),
)
)
# Return to self-pooling
exit_dpuz_and_solution = DelegatedPuzzleAndSolution(
puzzle=ACS,
solution=Program.to([cond.to_program() for cond in plotnft.exit_from_waiting_room_conditions()]),
puzzle=UnknownPuzzle(known_puzzle=ACS),
solution=UnknownSolution(
solution=Program.to(
[cond.to_program() for cond in plotnft.inner_puzzle.exit_from_waiting_room_conditions]
)
),
)
singing_info = plotnft.modify_delegated_puzzle_and_solution(exit_dpuz_and_solution)
singing_info = plotnft.inner_puzzle.modify_delegated_puzzle_and_solution(exit_dpuz_and_solution)
coin_spends = plotnft.exit_waiting_room(exit_dpuz_and_solution)
timelocked_spend = WalletSpendBundle(
coin_spends,
user_sk.sign(
singing_info.puzzle.get_tree_hash() + plotnft.coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
singing_info.puzzle.puzzle_hash + plotnft.coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA
),
)
result = await sim_client.push_tx(timelocked_spend)
assert result == (MempoolInclusionStatus.PENDING, Err.ASSERT_HEIGHT_RELATIVE_FAILED)
for _ in range(plotnft.guaranteed_pool_config.heightlock):
for _ in range(plotnft.inner_puzzle.guaranteed_pool_config.heightlock):
await sim.farm_block()
result = await sim_client.push_tx(cost_logger.add_cost("Waiting Room -> Self Custody", timelocked_spend))
assert result == (MempoolInclusionStatus.SUCCESS, None)
await sim.farm_block()
# Check that it's there
plotnft = PlotNFT.get_next_from_coin_spend(coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft)
plotnft = PlotNFT.get_next_from_coin_spend(
coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft.inner_puzzle
)
assert await sim_client.get_coin_record_by_name(plotnft.coin.name()) is not None
async def mint_reward(sim: SpendSim, sim_client: SimClient, singleton_id: bytes32) -> PoolReward:
reward_puzzle = RewardPuzzle(singleton_id=singleton_id)
await sim.farm_block(reward_puzzle.puzzle_hash())
coin_1, coin_2 = await sim_client.get_coin_records_by_puzzle_hash(reward_puzzle.puzzle_hash())
reward_puzzle = P2SingletonPuzzle(singleton_id=singleton_id)
await sim.farm_block(reward_puzzle.puzzle_hash)
coin_1, coin_2 = await sim_client.get_coin_records_by_puzzle_hash(reward_puzzle.puzzle_hash)
return PoolReward(
coin=coin_1.coin if coin_1.coin.amount > coin_2.coin.amount else coin_2.coin,
singleton_id=singleton_id,
@@ -299,7 +308,8 @@ async def test_plotnft_self_custody_claim(cost_logger: CostLogger) -> None:
plotnft.claim_pool_rewards(rewards_to_claim=[reward], reward_delegated_puzzles_and_solutions=[])
reward_dpuz_and_sol = DelegatedPuzzleAndSolution(
puzzle=ACS, solution=Program.to([CreateCoin(bytes32.zeros, uint64(1)).to_program()])
puzzle=UnknownPuzzle(known_puzzle=ACS),
solution=UnknownSolution(solution=Program.to([CreateCoin(bytes32.zeros, uint64(1)).to_program()])),
)
coin_spends = plotnft.claim_pool_rewards(
rewards_to_claim=[reward], reward_delegated_puzzles_and_solutions=[reward_dpuz_and_sol]
@@ -320,7 +330,9 @@ async def test_plotnft_self_custody_claim(cost_logger: CostLogger) -> None:
assert len(await sim_client.get_coin_records_by_puzzle_hash(bytes32.zeros)) == 1
# Make sure we can find the plotnft
plotnft = PlotNFT.get_next_from_coin_spend(coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft)
plotnft = PlotNFT.get_next_from_coin_spend(
coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft.inner_puzzle
)
# PlotNFT claims pooling rewards while pooling
@@ -340,7 +352,10 @@ async def test_plotnft_pooling_claim(
plotnft.claim_pool_rewards(
rewards_to_claim=[reward],
reward_delegated_puzzles_and_solutions=[
DelegatedPuzzleAndSolution(puzzle=Program.to(None), solution=Program.to(None))
DelegatedPuzzleAndSolution(
puzzle=UnknownPuzzle(known_puzzle=Program.to(None)),
solution=UnknownSolution(solution=Program.to(None)),
)
],
)
@@ -359,27 +374,34 @@ async def test_plotnft_pooling_claim(
# Make sure the pooling reward did what it was supposed to
assert (
len(await sim_client.get_coin_records_by_puzzle_hash(plotnft.guaranteed_pool_config.pool_puzzle_hash)) == 1
len(
await sim_client.get_coin_records_by_puzzle_hash(
plotnft.inner_puzzle.guaranteed_pool_config.pool_puzzle_hash
)
)
== 1
)
# Make sure we can find the plotnft
plotnft = PlotNFT.get_next_from_coin_spend(coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft)
plotnft = PlotNFT.get_next_from_coin_spend(
coin_spend=coin_spends[0], previous_plotnft_puzzle=plotnft.inner_puzzle
)
def test_plotnft_errors() -> None:
with pytest.raises(
ValueError, match=re.escape("Cannot initialize a PlotNFTPuzzle with an empty pool config and exiting=True")
):
PlotNFTPuzzle(
launcher_id=bytes32.zeros,
PlotNFTInnerPuzzle(
self_launcher_id=bytes32.zeros,
genesis_challenge=bytes32.zeros,
user_config=UserConfig(synthetic_pubkey=user_sk.get_g1()),
exiting=True,
)
with pytest.raises(ValueError, match=re.escape("Plot NFT is not pooling, cannot retrieve pool config")):
PlotNFTPuzzle(
launcher_id=bytes32.zeros,
PlotNFTInnerPuzzle(
self_launcher_id=bytes32.zeros,
genesis_challenge=bytes32.zeros,
user_config=UserConfig(synthetic_pubkey=user_sk.get_g1()),
exiting=False,
@@ -404,10 +426,10 @@ def test_plotnft_errors() -> None:
Program.to(PlotNFT.singleton_puzzles.singleton_mod).curry(
SingletonStruct(
launcher_id=bytes32.zeros,
singleton_puzzles=SingletonPuzzles(
singleton_puzzles=SingletonCorePuzzles(
singleton_launcher=Program.to("not the launcher"), singleton_launcher_hash_pre_computed=None
),
).to_program()
).program
),
Program.to(None),
),
@@ -417,7 +439,7 @@ def test_plotnft_errors() -> None:
def wrap_inner_puz(inner_puz: Program) -> UncurriedPuzzle:
return UncurriedPuzzle(
mod=PlotNFT.singleton_puzzles.singleton_mod,
args=Program.to([SingletonStruct(launcher_id=bytes32.zeros).to_program(), inner_puz]),
args=Program.to([SingletonStruct(launcher_id=bytes32.zeros).program, inner_puz]),
)
FAUX_SPEND = make_spend(default_coin, Program.to(None), Program.to([None, None, None]))
@@ -538,10 +560,10 @@ def test_plotnft_errors() -> None:
def test_singleton_constructs() -> None:
assert (
SingletonPuzzles(singleton_mod_hash_pre_computed=None).singleton_mod_hash
== SingletonPuzzles().singleton_mod_hash
SingletonCorePuzzles(singleton_mod_hash_pre_computed=None).singleton_mod_hash
== SingletonCorePuzzles().singleton_mod_hash
)
assert (
SingletonPuzzles(singleton_launcher_hash_pre_computed=None).singleton_launcher_hash
== SingletonPuzzles().singleton_launcher_hash
SingletonCorePuzzles(singleton_launcher_hash_pre_computed=None).singleton_launcher_hash
== SingletonCorePuzzles().singleton_launcher_hash
)
@@ -11,7 +11,7 @@ from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint32, uint64
from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework
from chia.pools.plotnft_drivers import PlotNFT, PoolConfig, UserConfig
from chia.pools.plotnft_drivers import PlotNFT, PlotNFTInnerPuzzle, PoolConfig, UserConfig
from chia.rpc.rpc_client import ResponseFailureError
from chia.simulator.simulator_protocol import ReorgProtocol
from chia.types.blockchain_format.program import Program
@@ -409,7 +409,10 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_
coin_spends += new_coin_spends
singleton_coin_spend = next(iter(spend for spend in new_coin_spends if spend.coin.amount == 1))
plotnft = PlotNFT.get_next_from_coin_spend(
coin_spend=singleton_coin_spend, genesis_challenge=None, pre_uncurry=None, previous_plotnft_puzzle=plotnft
coin_spend=singleton_coin_spend,
genesis_challenge=None,
pre_uncurry=None,
previous_plotnft_puzzle=plotnft.inner_puzzle,
)
NUM_CLAIMED = len(pool_rewards) - 1
@@ -499,7 +502,7 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_
# FINISH LEAVING (to new pool)
plotnft = await plotnft_wallet.get_current_plotnft()
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
count=plotnft.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
count=plotnft.inner_puzzle.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
)
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
@@ -648,7 +651,7 @@ async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_
# FINISH LEAVING
plotnft = await plotnft_wallet.get_current_plotnft()
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
count=plotnft.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
count=plotnft.inner_puzzle.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
)
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
@@ -901,7 +904,7 @@ async def test_plotnft_errors(wallet_environments: WalletTestFramework, self_hos
# farm to where completion should happen
plotnft = await plotnft_wallet.get_current_plotnft()
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
count=plotnft.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
count=plotnft.inner_puzzle.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
)
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
@@ -964,11 +967,14 @@ async def test_plotnft_errors(wallet_environments: WalletTestFramework, self_hos
peer=Mock(),
coin_data=PlotNFT(
launcher_id=bytes32.zeros,
genesis_challenge=bytes32.zeros,
user_config=UserConfig(synthetic_pubkey=G1Element()), # the important bit
exiting=False,
inner_puzzle=PlotNFTInnerPuzzle(
self_launcher_id=bytes32.zeros,
genesis_challenge=bytes32.zeros,
user_config=UserConfig(synthetic_pubkey=G1Element()), # the important bit
exiting=False,
),
coin=Mock(),
singleton_lineage_proof=Mock(),
lineage_proof=Mock(),
),
)
plotnft_after_raise = await plotnft_wallet.get_current_plotnft()
+267 -303
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import dataclasses
from dataclasses import dataclass, field, replace
from functools import cached_property
from typing import ClassVar
from typing import TYPE_CHECKING, ClassVar, cast
from chia_rs import G1Element
from chia_rs.chia_rs import Coin, CoinSpend
@@ -19,29 +19,42 @@ from chia.wallet.conditions import (
Condition,
CreateCoin,
CreateCoinAnnouncement,
MessageParticipant,
Remark,
SendMessage,
parse_conditions_non_consensus,
)
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.custody.custody_architecture import (
DelegatedPuzzleAndSolution,
DelegatedPuzzleAndSolution as MIPSDelegatedPuzzleAndSolution,
)
from chia.wallet.puzzles.custody.custody_architecture import (
MofN,
ProvenSpend,
PuzzleWithRestrictions,
)
from chia.wallet.puzzles.custody.member_puzzles import BLSWithTaprootMember, FixedPuzzleMember, SingletonMember
from chia.wallet.puzzles.custody.member_puzzles import (
BLSWithTaprootMember,
FixedPuzzleMember,
)
from chia.wallet.puzzles.custody.restriction_utilities import ValidatorStackRestriction
from chia.wallet.puzzles.custody.restrictions import FixedCreateCoinDestinations, Heightlock, SendMessageBanned
from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile
from chia.wallet.puzzles.singleton_top_layer_v1_1 import (
SINGLETON_LAUNCHER,
SINGLETON_LAUNCHER_HASH,
SINGLETON_MOD,
SINGLETON_MOD_HASH,
puzzle_for_singleton,
solution_for_singleton,
from chia.wallet.puzzles.puzzle_drivers import (
DelegatedPuzzleAndSolution,
InnerPuzzle,
PuzzleWithPuzzleHash,
UnknownPuzzle,
UnknownSolution,
)
from chia.wallet.puzzles.singleton_drivers import (
P2Singleton,
P2SingletonPuzzle,
Singleton,
SingletonCorePuzzles,
SingletonLaunchInfo,
SingletonLaunchResult,
SingletonPuzzle,
SingletonSolution,
SingletonStruct,
)
from chia.wallet.uncurried_puzzle import UncurriedPuzzle, uncurry_puzzle
@@ -57,45 +70,6 @@ def forward_to_pool_puzzle_hash_dpuz(pool_puzzle_hash: bytes32, pool_memoization
return FORWARD_TO_POOL_PUZZLE_HASH_DELEGATED_PUZZLE.curry(pool_puzzle_hash, pool_memoization)
@dataclass(kw_only=True, frozen=True)
class SingletonPuzzles:
singleton_mod: Program = field(default_factory=lambda: SINGLETON_MOD)
singleton_mod_hash_pre_computed: bytes32 | None = SINGLETON_MOD_HASH
singleton_launcher: Program = field(default_factory=lambda: SINGLETON_LAUNCHER)
singleton_launcher_hash_pre_computed: bytes32 | None = SINGLETON_LAUNCHER_HASH
@cached_property
def singleton_mod_hash(self) -> bytes32:
if self.singleton_mod_hash_pre_computed is not None:
return self.singleton_mod_hash_pre_computed
else:
return self.singleton_mod.get_tree_hash()
@cached_property
def singleton_launcher_hash(self) -> bytes32:
if self.singleton_launcher_hash_pre_computed is not None:
return self.singleton_launcher_hash_pre_computed
else:
return self.singleton_launcher.get_tree_hash()
@dataclass(kw_only=True, frozen=True)
class SingletonStruct:
launcher_id: bytes32
singleton_puzzles: SingletonPuzzles = SingletonPuzzles()
def to_program(self) -> Program:
return Program.to(
(
self.singleton_puzzles.singleton_mod_hash,
(self.launcher_id, self.singleton_puzzles.singleton_launcher_hash),
)
)
def struct_hash(self) -> bytes32:
return self.to_program().get_tree_hash()
@dataclass(kw_only=True, frozen=True)
class PoolConfig:
pool_puzzle_hash: bytes32
@@ -109,18 +83,31 @@ class UserConfig:
@dataclass(kw_only=True, frozen=True)
class PlotNFTPuzzle:
launcher_id: bytes32
genesis_challenge: bytes32
class PlotNFTInnerPuzzle(PuzzleWithPuzzleHash):
if TYPE_CHECKING:
_outer_puzzle_protocol_check: ClassVar[InnerPuzzle] = cast("PlotNFTInnerPuzzle", None)
user_config: UserConfig
exiting: bool
exiting: bool | None = None
self_launcher_id: bytes32 | None = None
genesis_challenge: bytes32 | None = None
pool_config: PoolConfig | None = None
singleton_puzzles: ClassVar[SingletonPuzzles] = SingletonPuzzles()
singleton_puzzles: ClassVar[SingletonCorePuzzles] = SingletonCorePuzzles()
def __post_init__(self) -> None:
if self.pool_config is not None and (
self.self_launcher_id is None or self.genesis_challenge is None or self.exiting is None
):
raise ValueError("Trying to initialize a pooling PlotNFT without required information")
if self.pool_config is None and self.exiting:
raise ValueError("Cannot initialize a PlotNFTPuzzle with an empty pool config and exiting=True")
@property
def launcher_id(self) -> bytes32:
if self.self_launcher_id is None:
raise ValueError("Launcher ID is not present because PlotNFT is not pooling")
return self.self_launcher_id
@property
def singleton_struct(self) -> SingletonStruct:
return SingletonStruct(launcher_id=self.launcher_id, singleton_puzzles=self.singleton_puzzles)
@@ -139,36 +126,38 @@ class PlotNFTPuzzle:
def bls_member(self) -> BLSWithTaprootMember:
return BLSWithTaprootMember(synthetic_key=self.user_config.synthetic_pubkey)
def reward_puzhash(self) -> bytes32:
return RewardPuzzle(singleton_id=self.launcher_id).puzzle_hash()
@cached_property
def forward_pool_reward_dpuz(self) -> Program:
return forward_to_pool_puzzle_hash_dpuz(
self.guaranteed_pool_config.pool_puzzle_hash, self.guaranteed_pool_config.pool_memoization
)
@property
def waiting_room_puzzle(self) -> Self:
return dataclasses.replace(self, exiting=True)
@cached_property
def claim_pool_reward_dpuz(self) -> Program:
assert self.genesis_challenge is not None
return CLAIM_POOL_REWARDS_DELEGATED_PUZZLE.curry(
self.genesis_challenge[:16],
self.singleton_struct.singleton_puzzles.singleton_mod_hash,
self.singleton_struct.struct_hash(),
self.reward_puzhash(),
self.forward_pool_reward_dpuz().get_tree_hash(),
self.singleton_puzzles.singleton_mod_hash,
self.singleton_struct.struct_hash,
P2SingletonPuzzle(singleton_id=self.launcher_id).puzzle_hash,
self.forward_pool_reward_dpuz.get_tree_hash(),
)
def claim_pool_reward_dpuz_and_solution(self, reward: PoolReward) -> DelegatedPuzzleAndSolution:
return DelegatedPuzzleAndSolution(
puzzle=self.claim_pool_reward_dpuz(),
solution=Program.to([self.inner_puzzle_hash(), reward.height, reward.coin.amount]),
puzzle=UnknownPuzzle(known_puzzle=self.claim_pool_reward_dpuz),
solution=UnknownSolution(Program.to([self.puzzle_hash, reward.height, reward.coin.amount])),
)
@property
def user_restriction(self) -> ValidatorStackRestriction:
return ValidatorStackRestriction(
required_wrappers=[
FixedCreateCoinDestinations(allowed_ph=self.waiting_room_puzzle().inner_puzzle().get_tree_hash()),
FixedCreateCoinDestinations(allowed_ph=self.waiting_room_puzzle.puzzle_hash),
SendMessageBanned(),
]
if not self.exiting
@@ -178,51 +167,63 @@ class PlotNFTPuzzle:
def modify_delegated_puzzle_and_solution(
self, delegated_puzzle_and_solution: DelegatedPuzzleAndSolution
) -> DelegatedPuzzleAndSolution:
return self.user_restriction().modify_delegated_puzzle_and_solution(
delegated_puzzle_and_solution, [Program.to(None), Program.to(None)]
modified_dpuz_and_solution = self.user_restriction.modify_delegated_puzzle_and_solution(
MIPSDelegatedPuzzleAndSolution(
puzzle=delegated_puzzle_and_solution.puzzle.puzzle,
solution=delegated_puzzle_and_solution.solution.as_program(),
),
[Program.to(None), Program.to(None)],
)
return DelegatedPuzzleAndSolution(
puzzle=UnknownPuzzle(known_puzzle=modified_dpuz_and_solution.puzzle),
solution=UnknownSolution(solution=modified_dpuz_and_solution.solution),
)
@property
def user_puzzle_with_restrictions(self) -> PuzzleWithRestrictions:
return PuzzleWithRestrictions(
nonce=0,
restrictions=[self.user_restriction()],
restrictions=[self.user_restriction],
puzzle=self.bls_member,
)
def user_proven_spend(self, premodified_dpuz: Program) -> dict[bytes32, ProvenSpend]:
return {
self.user_puzzle_with_restrictions().puzzle_hash(_top_level=False): ProvenSpend(
puzzle_reveal=self.user_puzzle_with_restrictions().puzzle_reveal(_top_level=False),
solution=self.user_puzzle_with_restrictions().solve(
self.user_puzzle_with_restrictions.puzzle_hash(_top_level=False): ProvenSpend(
puzzle_reveal=self.user_puzzle_with_restrictions.puzzle_reveal(_top_level=False),
solution=self.user_puzzle_with_restrictions.solve(
member_validator_solutions=[],
dpuz_validator_solutions=[self.user_restriction().solve(premodified_dpuz)],
dpuz_validator_solutions=[self.user_restriction.solve(premodified_dpuz)],
member_solution=self.bls_member.solve(),
),
)
}
@property
def fixed_puzzle_member(self) -> FixedPuzzleMember:
return FixedPuzzleMember(fixed_puzzle_hash=self.claim_pool_reward_dpuz().get_tree_hash())
return FixedPuzzleMember(fixed_puzzle_hash=self.claim_pool_reward_dpuz.get_tree_hash())
@property
def pool_puzzle_with_restrictions(self) -> PuzzleWithRestrictions:
return PuzzleWithRestrictions(
nonce=0,
restrictions=[],
puzzle=self.fixed_puzzle_member(),
puzzle=self.fixed_puzzle_member,
)
def pool_proven_spend(self) -> dict[bytes32, ProvenSpend]:
return {
self.pool_puzzle_with_restrictions().puzzle_hash(_top_level=False): ProvenSpend(
puzzle_reveal=self.pool_puzzle_with_restrictions().puzzle_reveal(_top_level=False),
solution=self.pool_puzzle_with_restrictions().solve(
self.pool_puzzle_with_restrictions.puzzle_hash(_top_level=False): ProvenSpend(
puzzle_reveal=self.pool_puzzle_with_restrictions.puzzle_reveal(_top_level=False),
solution=self.pool_puzzle_with_restrictions.solve(
member_validator_solutions=[],
dpuz_validator_solutions=[],
member_solution=self.fixed_puzzle_member().solve(),
member_solution=self.fixed_puzzle_member.solve(),
),
)
}
@property
def puzzle_with_restrictions(self) -> PuzzleWithRestrictions:
return PuzzleWithRestrictions(
nonce=0,
@@ -230,18 +231,20 @@ class PlotNFTPuzzle:
puzzle=MofN(
m=1,
members=[
self.user_puzzle_with_restrictions(),
self.pool_puzzle_with_restrictions(),
self.user_puzzle_with_restrictions,
self.pool_puzzle_with_restrictions,
],
)
if self.pooling
else self.bls_member,
additional_memos=self.additional_memos(),
additional_memos=self.additional_memos,
)
@property
def memo(self) -> Program:
return self.puzzle_with_restrictions().memo()
return self.puzzle_with_restrictions.memo()
@property
def additional_memos(self) -> Program:
if self.pooling:
return Program.to(
@@ -255,80 +258,106 @@ class PlotNFTPuzzle:
else:
return Program.to([self.bls_member.synthetic_key])
def inner_puzzle(self) -> Program:
return self.puzzle_with_restrictions().puzzle_reveal()
@property
def puzzle(self) -> Program:
return self.puzzle_with_restrictions.puzzle_reveal()
def inner_puzzle_hash(self) -> bytes32:
return self.inner_puzzle().get_tree_hash()
def puzzle(self, nonce: int) -> Program:
return puzzle_for_singleton(
launcher_id=self.singleton_struct.launcher_id,
launcher_hash=self.singleton_struct.singleton_puzzles.singleton_launcher_hash,
singleton_mod=self.singleton_struct.singleton_puzzles.singleton_mod,
singleton_mod_hash=self.singleton_struct.singleton_puzzles.singleton_mod_hash,
inner_puz=self.puzzle_with_restrictions().puzzle_reveal(),
)
def puzzle_hash(self, nonce: int) -> bytes32:
return self.puzzle(nonce).get_tree_hash()
@property
def puzzle_hash(self) -> bytes32:
return self.puzzle.get_tree_hash()
def forward_pool_reward_inner_solution(self, reward: PoolReward) -> Program:
custody_pwr = self.puzzle_with_restrictions()
custody_pwr = self.puzzle_with_restrictions
assert isinstance(custody_pwr.puzzle, MofN)
dp_and_sol = self.claim_pool_reward_dpuz_and_solution(reward)
return custody_pwr.solve(
member_validator_solutions=[],
dpuz_validator_solutions=[],
member_solution=custody_pwr.puzzle.solve(self.pool_proven_spend()),
delegated_puzzle_and_solution=self.claim_pool_reward_dpuz_and_solution(reward),
delegated_puzzle_and_solution=MIPSDelegatedPuzzleAndSolution(
puzzle=dp_and_sol.puzzle.puzzle, solution=dp_and_sol.solution.as_program()
),
)
def exit_to_from_waiting_room_inner_solution(
self, delegated_puzzle_and_solution: DelegatedPuzzleAndSolution
) -> Program:
custody_pwr = self.puzzle_with_restrictions()
custody_pwr = self.puzzle_with_restrictions
assert isinstance(custody_pwr.puzzle, MofN)
return custody_pwr.solve(
member_validator_solutions=[],
dpuz_validator_solutions=[],
member_solution=custody_pwr.puzzle.solve(self.user_proven_spend(delegated_puzzle_and_solution.puzzle)),
delegated_puzzle_and_solution=self.user_restriction().modify_delegated_puzzle_and_solution(
delegated_puzzle_and_solution, [Program.to([]), Program.to([])]
member_solution=custody_pwr.puzzle.solve(
self.user_proven_spend(delegated_puzzle_and_solution.puzzle.puzzle)
),
delegated_puzzle_and_solution=self.user_restriction.modify_delegated_puzzle_and_solution(
MIPSDelegatedPuzzleAndSolution(
puzzle=delegated_puzzle_and_solution.puzzle.puzzle,
solution=delegated_puzzle_and_solution.solution.as_program(),
),
[Program.to([]), Program.to([])],
),
)
@property
def exit_to_waiting_room_condition(self) -> CreateCoin:
return CreateCoin(
puzzle_hash=self.waiting_room_puzzle().inner_puzzle_hash(),
puzzle_hash=self.waiting_room_puzzle.puzzle_hash,
amount=uint64(1),
memos=[self.singleton_struct.struct_hash()],
memos=[self.singleton_struct.struct_hash],
)
@property
def exit_from_waiting_room_conditions(self) -> tuple[AssertHeightRelative, CreateCoin]:
next_plotnft_puzzle = replace(self, pool_config=None, exiting=False)
return (
AssertHeightRelative(height=self.guaranteed_pool_config.heightlock),
CreateCoin(
puzzle_hash=next_plotnft_puzzle.inner_puzzle_hash(),
puzzle_hash=next_plotnft_puzzle.puzzle_hash,
amount=uint64(1),
# maybe the full memo is not strictly necessary, but it's needed for robustness at the moment
memo_blob=Program.to((self.singleton_struct.struct_hash(), next_plotnft_puzzle.memo())),
memo_blob=Program.to((self.singleton_struct.struct_hash, next_plotnft_puzzle.memo)),
),
)
@classmethod
def match(cls, *, unknown_puzzle: UnknownPuzzle, solution: object | None = None) -> PlotNFTInnerPuzzle | None:
mips_match = PuzzleWithRestrictions.match(unknown_puzzle=unknown_puzzle, solution=solution)
if mips_match is None:
return None
assert isinstance(mips_match, PuzzleWithRestrictions)
assert isinstance(mips_match.puzzle, UnknownPuzzle)
potential_bls_member_match = BLSWithTaprootMember.match(unknown_puzzle=mips_match.puzzle)
if potential_bls_member_match is not None:
return PlotNFTInnerPuzzle(
user_config=UserConfig(synthetic_pubkey=potential_bls_member_match.guaranteed_synthetic_key)
)
potential_mofn_match = MofN.match(unknown_puzzle=mips_match.puzzle, solution=solution)
if potential_mofn_match is None:
return None
if MofN.m != 2:
return None
raise NotImplementedError("Currently unimplemented")
class GetNextPlotNFTError(Exception):
pass
@dataclass(kw_only=True, frozen=True)
class PlotNFT(PlotNFTPuzzle):
class PlotNFTLaunchResult(SingletonLaunchResult[PlotNFTInnerPuzzle]):
necessary_conditions: list[Condition]
necessary_spends: list[CoinSpend]
launched_singleton: PlotNFT
@dataclass(kw_only=True, frozen=True)
class PlotNFT(Singleton[PlotNFTInnerPuzzle]):
coin: Coin
singleton_lineage_proof: LineageProof
remarks: list[Remark] = field(default_factory=list)
@classmethod
def launch(
def launch_plotnft(
cls,
*,
origin_coins: list[Coin],
@@ -338,13 +367,13 @@ class PlotNFT(PlotNFTPuzzle):
pool_config: PoolConfig | None = None,
exiting: bool = False,
remark: Remark | None = None,
) -> tuple[tuple[AssertCoinAnnouncement, AssertCoinAnnouncement], list[CoinSpend], Self]:
) -> PlotNFTLaunchResult:
origin_coin = origin_coins[0]
launcher_coin = Coin(origin_coin.name(), cls.singleton_puzzles.singleton_launcher_hash, uint64(1))
launcher_id = launcher_coin.name()
plotnft_puzzle = PlotNFTPuzzle(
launcher_id=launcher_id,
plotnft_inner_puzzle = PlotNFTInnerPuzzle(
self_launcher_id=launcher_id,
user_config=user_config,
pool_config=pool_config,
exiting=exiting,
@@ -355,59 +384,49 @@ class PlotNFT(PlotNFTPuzzle):
1,
[
CreateCoin(
plotnft_puzzle.inner_puzzle_hash(),
plotnft_inner_puzzle.puzzle_hash,
uint64(1),
memo_blob=Program.to((hint, plotnft_puzzle.puzzle_with_restrictions().memo())),
memo_blob=Program.to((hint, plotnft_inner_puzzle.puzzle_with_restrictions.memo())),
).to_program(),
CreateCoinAnnouncement(msg=b"").to_program(),
*([] if remark is None else [remark.to_program()]),
],
)
)
full_rev_singleton_puzzle = puzzle_for_singleton(
launcher_id,
rev_puzzle,
singleton_mod=cls.singleton_puzzles.singleton_mod,
launcher_hash=cls.singleton_puzzles.singleton_launcher_hash,
singleton_mod_hash=cls.singleton_puzzles.singleton_mod_hash,
)
rev_coin = Coin(launcher_id, full_rev_singleton_puzzle.get_tree_hash(), uint64(1))
rev_coin_id = rev_coin.name()
launcher_solution = Program.to([full_rev_singleton_puzzle.get_tree_hash(), uint64(1), None])
conditions = (
AssertCoinAnnouncement(asserted_id=launcher_id, asserted_msg=launcher_solution.get_tree_hash()),
AssertCoinAnnouncement(asserted_id=rev_coin_id, asserted_msg=b""),
)
launcher_spend = make_spend(
launcher_coin,
cls.singleton_puzzles.singleton_launcher,
launcher_solution,
)
rev_spend = make_spend(
rev_coin,
full_rev_singleton_puzzle,
solution_for_singleton(
LineageProof(parent_name=launcher_coin.parent_coin_info, amount=launcher_coin.amount),
uint64(1),
Program.to(None),
pre_rev_launch_result = super().launch(
origin_coin=origin_coin,
launch_info=SingletonLaunchInfo(
desired_inner_puzzle=UnknownPuzzle(known_puzzle=rev_puzzle), key_value_hints={}
),
)
return (
conditions,
[launcher_spend, rev_spend],
cls(
coin=Coin(rev_coin_id, plotnft_puzzle.puzzle_hash(nonce=0), uint64(1)),
singleton_lineage_proof=LineageProof(
parent_name=rev_coin.parent_coin_info,
inner_puzzle_hash=rev_puzzle.get_tree_hash(),
amount=rev_coin.amount,
rev_coin_id = pre_rev_launch_result.launched_singleton.coin.name()
assert_rev_ca = AssertCoinAnnouncement(asserted_id=rev_coin_id, asserted_msg=b"")
rev_spend = make_spend(
pre_rev_launch_result.launched_singleton.coin,
pre_rev_launch_result.launched_singleton.puzzle,
SingletonSolution(
lineage_proof=LineageProof(parent_name=launcher_coin.parent_coin_info, amount=launcher_coin.amount),
coin_amount=uint64(1),
inner_solution=UnknownSolution(solution=Program.NIL),
).as_program(),
)
return PlotNFTLaunchResult(
necessary_conditions=[*pre_rev_launch_result.necessary_conditions, assert_rev_ca],
necessary_spends=[*pre_rev_launch_result.necessary_spends, rev_spend],
launched_singleton=cls(
coin=Coin(
rev_coin_id,
SingletonPuzzle(launcher_id=launcher_id, inner_puzzle=plotnft_inner_puzzle).puzzle_hash,
uint64(1),
),
launcher_id=launcher_id,
user_config=user_config,
pool_config=pool_config,
exiting=exiting,
genesis_challenge=genesis_challenge,
lineage_proof=LineageProof(
parent_name=pre_rev_launch_result.launched_singleton.coin.parent_coin_info,
inner_puzzle_hash=rev_puzzle.get_tree_hash(),
amount=pre_rev_launch_result.launched_singleton.coin.amount,
),
inner_puzzle=plotnft_inner_puzzle,
),
)
@@ -418,7 +437,7 @@ class PlotNFT(PlotNFTPuzzle):
coin_spend: CoinSpend,
genesis_challenge: bytes32 | None = None,
pre_uncurry: UncurriedPuzzle | None = None,
previous_plotnft_puzzle: PlotNFTPuzzle | None = None,
previous_plotnft_puzzle: PlotNFTInnerPuzzle | None = None,
) -> Self:
# some input validation
if genesis_challenge is None and previous_plotnft_puzzle is None:
@@ -463,13 +482,11 @@ class PlotNFT(PlotNFTPuzzle):
and previous_plotnft_puzzle.pool_config is not None
):
if (
replace(previous_plotnft_puzzle, pool_config=None, exiting=False).inner_puzzle_hash()
replace(previous_plotnft_puzzle, pool_config=None, exiting=False).puzzle_hash
== singleton_create_coin.puzzle_hash
):
plotnft_puzzle = replace(previous_plotnft_puzzle, pool_config=None, exiting=False)
elif (
replace(previous_plotnft_puzzle, exiting=True).inner_puzzle_hash() == singleton_create_coin.puzzle_hash
):
elif replace(previous_plotnft_puzzle, exiting=True).puzzle_hash == singleton_create_coin.puzzle_hash:
plotnft_puzzle = replace(previous_plotnft_puzzle, exiting=True)
# Finally, we try to look for the memos
@@ -500,217 +517,164 @@ class PlotNFT(PlotNFTPuzzle):
pool_config = None
exiting = False
plotnft_puzzle = PlotNFTPuzzle(
launcher_id=launcher_id,
plotnft_puzzle = PlotNFTInnerPuzzle(
self_launcher_id=launcher_id,
user_config=UserConfig(synthetic_pubkey=pubkey),
pool_config=pool_config,
exiting=exiting,
genesis_challenge=genesis_challenge,
)
if plotnft_puzzle.inner_puzzle_hash() != singleton_create_coin.puzzle_hash:
if plotnft_puzzle.puzzle_hash != singleton_create_coin.puzzle_hash:
raise GetNextPlotNFTError("Invalid memoization of PlotNFT")
return cls(
coin=Coin(
coin_spend.coin.name(),
plotnft_puzzle.puzzle(nonce=0).get_tree_hash(),
SingletonPuzzle(launcher_id=launcher_id, inner_puzzle=plotnft_puzzle).puzzle_hash,
coin_spend.coin.amount,
),
singleton_lineage_proof=LineageProof(
lineage_proof=LineageProof(
parent_name=coin_spend.coin.parent_coin_info,
inner_puzzle_hash=inner_puzzle.get_tree_hash(),
amount=coin_spend.coin.amount,
),
inner_puzzle=plotnft_puzzle,
launcher_id=launcher_id,
user_config=plotnft_puzzle.user_config,
pool_config=plotnft_puzzle.pool_config,
exiting=plotnft_puzzle.exiting,
genesis_challenge=genesis_challenge,
remarks=remarks,
)
def singleton_action_spend(self, inner_solution: Program) -> CoinSpend:
return make_spend(
coin=self.coin,
puzzle_reveal=puzzle_for_singleton(
launcher_id=self.singleton_struct.launcher_id,
inner_puz=self.inner_puzzle(),
singleton_mod=self.singleton_struct.singleton_puzzles.singleton_mod,
singleton_mod_hash=self.singleton_struct.singleton_puzzles.singleton_mod_hash,
launcher_hash=self.singleton_struct.singleton_puzzles.singleton_launcher_hash,
),
solution=solution_for_singleton(
lineage_proof=self.singleton_lineage_proof,
amount=self.coin.amount,
inner_solution=inner_solution,
),
)
def forward_pool_reward(self, reward: PoolReward) -> list[CoinSpend]:
if not self.pooling:
if not self.inner_puzzle.pooling:
raise ValueError("Cannot forward pool reward while self pooling. Try `claim_pool_rewards`")
return [
self.singleton_action_spend(inner_solution=self.forward_pool_reward_inner_solution(reward)),
make_spend(
coin=reward.coin,
puzzle_reveal=reward.puzzle(),
solution=reward.solve(
self.inner_puzzle_hash(),
delegated_puzzle_and_solution=DelegatedPuzzleAndSolution(
puzzle=self.forward_pool_reward_dpuz(),
solution=Program.to([reward.coin.amount]),
),
),
),
]
coin_spend = self.spend(
inner_solution=UnknownSolution(self.inner_puzzle.forward_pool_reward_inner_solution(reward))
)
reward_spends, _ = self.claim_p2_singletons(
rewards_to_claim=[reward],
reward_delegated_puzzles_and_solutions=[
DelegatedPuzzleAndSolution(
puzzle=UnknownPuzzle(known_puzzle=self.inner_puzzle.forward_pool_reward_dpuz),
solution=UnknownSolution(Program.to([reward.coin.amount])),
)
],
)
return [coin_spend, *reward_spends]
def exit_to_waiting_room(self, delegated_puzzle_and_solution: DelegatedPuzzleAndSolution) -> list[CoinSpend]:
if not self.pooling:
if not self.inner_puzzle.pooling:
raise ValueError("Cannot exit to waiting room while self pooling.")
if self.exiting:
if self.inner_puzzle.exiting:
raise ValueError("Already exiting to waiting room, cannot exit again")
return [
self.singleton_action_spend(
inner_solution=self.exit_to_from_waiting_room_inner_solution(delegated_puzzle_and_solution)
coin_spend = self.spend(
inner_solution=UnknownSolution(
solution=self.inner_puzzle.exit_to_from_waiting_room_inner_solution(delegated_puzzle_and_solution)
)
]
)
return [coin_spend]
def exit_waiting_room(self, delegated_puzzle_and_solution: DelegatedPuzzleAndSolution) -> list[CoinSpend]:
if not self.pooling:
if not self.inner_puzzle.pooling:
raise ValueError("Cannot exit waiting room while self pooling.")
if not self.exiting:
if not self.inner_puzzle.exiting:
raise ValueError("Cannot exit waiting room while not in it")
return [
self.singleton_action_spend(
inner_solution=self.exit_to_from_waiting_room_inner_solution(delegated_puzzle_and_solution)
coin_spend = self.spend(
inner_solution=UnknownSolution(
solution=self.inner_puzzle.exit_to_from_waiting_room_inner_solution(delegated_puzzle_and_solution)
)
]
)
return [coin_spend]
def claim_pool_rewards(
self,
rewards_to_claim: list[PoolReward],
reward_delegated_puzzles_and_solutions: list[DelegatedPuzzleAndSolution],
) -> list[CoinSpend]:
if self.pooling:
if self.inner_puzzle.pooling:
raise ValueError("Cannot claim rewards while pooling. If you're a pool, try `forward_pool_rewards`")
if len(rewards_to_claim) != len(reward_delegated_puzzles_and_solutions):
raise ValueError("Number of rewards and delegated puzzles and solutions must match")
reward_spends, messages = self.claim_p2_singletons(
rewards_to_claim=rewards_to_claim,
reward_delegated_puzzles_and_solutions=reward_delegated_puzzles_and_solutions,
)
dpuz_and_solution = DelegatedPuzzleAndSolution(
puzzle=Program.to(
(
1,
[
CreateCoin(
puzzle_hash=self.inner_puzzle_hash(),
amount=self.coin.amount,
memos=[self.singleton_struct.struct_hash()],
).to_program(),
*(
SendMessage(
msg=dpuz_and_sol.puzzle.get_tree_hash(),
sender=MessageParticipant(puzzle_hash_committed=self.puzzle_hash(nonce=0)),
receiver=MessageParticipant(coin_id_committed=reward.coin.name()),
).to_program()
for reward, dpuz_and_sol in zip(rewards_to_claim, reward_delegated_puzzles_and_solutions)
),
],
puzzle=UnknownPuzzle(
known_puzzle=Program.to(
(
1,
[
CreateCoin(
puzzle_hash=self.inner_puzzle.puzzle_hash,
amount=self.coin.amount,
memos=[self.singleton_struct.struct_hash],
).to_program(),
*[msg.to_program() for msg in messages],
],
)
)
),
solution=Program.to([]),
solution=UnknownSolution(solution=Program.to([])),
)
return [
self.singleton_action_spend(
inner_solution=self.puzzle_with_restrictions().solve(
coin_spend = self.spend(
inner_solution=UnknownSolution(
solution=self.inner_puzzle.puzzle_with_restrictions.solve(
member_validator_solutions=[],
dpuz_validator_solutions=[],
member_solution=self.bls_member.solve(),
delegated_puzzle_and_solution=dpuz_and_solution,
)
),
*(
make_spend(
coin=reward.coin,
puzzle_reveal=reward.puzzle(),
solution=reward.solve(
self.inner_puzzle_hash(),
delegated_puzzle_and_solution=dpuz_and_sol,
member_solution=self.inner_puzzle.bls_member.solve(),
delegated_puzzle_and_solution=MIPSDelegatedPuzzleAndSolution(
puzzle=dpuz_and_solution.puzzle.puzzle, solution=dpuz_and_solution.solution.as_program()
),
)
for reward, dpuz_and_sol in zip(rewards_to_claim, reward_delegated_puzzles_and_solutions)
),
]
)
)
return [coin_spend, *reward_spends]
def join_pool(
self, user_config: UserConfig, pool_config: PoolConfig, extra_conditions: tuple[Condition, ...] = tuple()
) -> list[CoinSpend]:
plotnft_puzzle = PlotNFTPuzzle(
launcher_id=self.launcher_id,
plotnft_puzzle = PlotNFTInnerPuzzle(
self_launcher_id=self.launcher_id,
user_config=user_config,
pool_config=pool_config,
exiting=False,
genesis_challenge=self.genesis_challenge,
genesis_challenge=self.inner_puzzle.genesis_challenge,
)
dpuz_and_solution = DelegatedPuzzleAndSolution(
puzzle=Program.to(
(
1,
[
CreateCoin(
plotnft_puzzle.inner_puzzle_hash(),
amount=self.coin.amount,
memo_blob=Program.to((self.singleton_struct.struct_hash(), plotnft_puzzle.memo())),
).to_program(),
*(cond.to_program() for cond in extra_conditions),
],
puzzle=UnknownPuzzle(
known_puzzle=Program.to(
(
1,
[
CreateCoin(
plotnft_puzzle.puzzle_hash,
amount=self.coin.amount,
memo_blob=Program.to((self.singleton_struct.struct_hash, plotnft_puzzle.memo)),
).to_program(),
*(cond.to_program() for cond in extra_conditions),
],
)
)
),
solution=Program.to([]),
solution=UnknownSolution(solution=Program.to([])),
)
return [
self.singleton_action_spend(
inner_solution=self.puzzle_with_restrictions().solve(
coin_spend = self.spend(
inner_solution=UnknownSolution(
self.inner_puzzle.puzzle_with_restrictions.solve(
member_validator_solutions=[],
dpuz_validator_solutions=[],
member_solution=self.bls_member.solve(),
delegated_puzzle_and_solution=dpuz_and_solution,
member_solution=self.inner_puzzle.bls_member.solve(),
delegated_puzzle_and_solution=MIPSDelegatedPuzzleAndSolution(
puzzle=dpuz_and_solution.puzzle.puzzle, solution=dpuz_and_solution.solution.as_program()
),
)
)
]
@dataclass(kw_only=True, frozen=True)
class RewardPuzzle:
singleton_id: bytes32
@property
def singleton_member(self) -> SingletonMember:
return SingletonMember(singleton_id=self.singleton_id)
def puzzle_with_restrictions(self) -> PuzzleWithRestrictions:
return PuzzleWithRestrictions(nonce=0, restrictions=[], puzzle=self.singleton_member)
def puzzle(self) -> Program:
return self.puzzle_with_restrictions().puzzle_reveal()
def puzzle_hash(self) -> bytes32:
return self.puzzle().get_tree_hash()
def solve(
self, singleton_inner_puzzle_hash: bytes32, delegated_puzzle_and_solution: DelegatedPuzzleAndSolution
) -> Program:
return self.puzzle_with_restrictions().solve(
[],
[],
self.singleton_member.solve(singleton_inner_puzzle_hash),
delegated_puzzle_and_solution,
)
return [coin_spend]
@dataclass(kw_only=True, frozen=True)
class PoolReward(RewardPuzzle):
coin: Coin
class PoolReward(P2Singleton):
@property
def height(self) -> uint32:
return uint32.from_bytes(self.coin.parent_coin_info[28:])
+27 -18
View File
@@ -7,7 +7,7 @@ from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint32, uint64
from typing_extensions import Self
from chia.pools.plotnft_drivers import PlotNFT, PoolConfig, PoolReward, UserConfig
from chia.pools.plotnft_drivers import PlotNFT, PlotNFTInnerPuzzle, PoolConfig, PoolReward, UserConfig
from chia.types.blockchain_format.program import Program
from chia.util.db_wrapper import DBWrapper2
from chia.wallet.conditions import Remark
@@ -20,18 +20,21 @@ DEFAULT_POOL_REWARDS_PER_CLAIM = 20
def _row_to_plotnft(row: Row, genesis_challenge: bytes32) -> PlotNFT:
return PlotNFT(
coin=Coin(parent_coin_info=bytes32(row[1]), puzzle_hash=bytes32(row[2]), amount=uint64.from_bytes(row[3])),
singleton_lineage_proof=LineageProof.from_bytes(row[4]),
lineage_proof=LineageProof.from_bytes(row[4]),
launcher_id=bytes32(row[5]),
user_config=UserConfig(synthetic_pubkey=G1Element.from_bytes(row[6])),
pool_config=PoolConfig(
pool_puzzle_hash=bytes32(row[7]),
heightlock=uint32.from_bytes(row[8]),
pool_memoization=Program.from_bytes(row[9]),
)
if row[7:10] != (b"", b"", b"")
else None,
genesis_challenge=genesis_challenge,
exiting=False if row[10] == 0 else True,
inner_puzzle=PlotNFTInnerPuzzle(
self_launcher_id=bytes32(row[5]),
user_config=UserConfig(synthetic_pubkey=G1Element.from_bytes(row[6])),
pool_config=PoolConfig(
pool_puzzle_hash=bytes32(row[7]),
heightlock=uint32.from_bytes(row[8]),
pool_memoization=Program.from_bytes(row[9]),
)
if row[7:10] != (b"", b"", b"")
else None,
genesis_challenge=genesis_challenge,
exiting=False if row[10] == 0 else True,
),
remarks=[Remark(Program.to(row[11]))] if row[11] is not None else [],
)
@@ -96,13 +99,19 @@ class PlotNFTStore:
plotnft.coin.parent_coin_info,
plotnft.coin.puzzle_hash,
bytes(plotnft.coin.amount),
bytes(plotnft.singleton_lineage_proof),
bytes(plotnft.lineage_proof),
plotnft.launcher_id,
bytes(plotnft.user_config.synthetic_pubkey),
plotnft.pool_config.pool_puzzle_hash if plotnft.pool_config is not None else b"",
bytes(plotnft.pool_config.heightlock) if plotnft.pool_config is not None else b"",
bytes(plotnft.pool_config.pool_memoization) if plotnft.pool_config is not None else b"",
plotnft.exiting,
bytes(plotnft.inner_puzzle.user_config.synthetic_pubkey),
plotnft.inner_puzzle.pool_config.pool_puzzle_hash
if plotnft.inner_puzzle.pool_config is not None
else b"",
bytes(plotnft.inner_puzzle.pool_config.heightlock)
if plotnft.inner_puzzle.pool_config is not None
else b"",
bytes(plotnft.inner_puzzle.pool_config.pool_memoization)
if plotnft.inner_puzzle.pool_config is not None
else b"",
plotnft.inner_puzzle.exiting,
str(plotnft.remarks[0].rest.atom, "utf8")
if len(plotnft.remarks) > 0 and plotnft.remarks[0].rest.atom is not None
else None,
+95 -76
View File
@@ -10,14 +10,15 @@ from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint8, uint32, uint64, uint128
from typing_extensions import Self, Unpack
from chia.pools.plotnft_drivers import PlotNFT, PoolConfig, PoolReward, RewardPuzzle, SingletonStruct, UserConfig
from chia.pools.plotnft_drivers import PlotNFT, PoolConfig, PoolReward, UserConfig
from chia.pools.pool_config import PoolingShareState
from chia.pools.pool_wallet_info import PoolSingletonState, PoolState, PoolWalletInfo
from chia.server.ws_connection import WSChiaConnection
from chia.types.blockchain_format.program import Program
from chia.wallet.conditions import AssertCoinAnnouncement, Condition, CreateCoin, CreateCoinAnnouncement, Remark
from chia.wallet.puzzles.custody.custody_architecture import DelegatedPuzzleAndSolution
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key
from chia.wallet.puzzles.puzzle_drivers import DelegatedPuzzleAndSolution, UnknownPuzzle, UnknownSolution
from chia.wallet.puzzles.singleton_drivers import P2SingletonPuzzle, SingletonStruct
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_action_scope import PlotNFTTargetStateInfo, WalletActionScope
@@ -67,7 +68,7 @@ class PlotNFT2Wallet:
@property
def hint(self) -> bytes32:
return SingletonStruct(launcher_id=self.plotnft_id).struct_hash()
return SingletonStruct(launcher_id=self.plotnft_id).struct_hash
@property
def plotnft_id(self) -> bytes32:
@@ -75,7 +76,7 @@ class PlotNFT2Wallet:
@property
def p2_singleton_puzzle_hash(self) -> bytes32:
return RewardPuzzle(singleton_id=self.plotnft_id).puzzle_hash()
return P2SingletonPuzzle(singleton_id=self.plotnft_id).puzzle_hash
@property
def rewards_claim_puzhash(self) -> bytes32:
@@ -114,7 +115,7 @@ class PlotNFT2Wallet:
target_puzzle_hash = await action_scope.get_puzzle_hash(wallet_state_manager)
target_pubkey = G1Element.from_bytes(await wallet_state_manager.get_public_key(target_puzzle_hash))
origin_coins = await xch_wallet.select_coins(amount=uint64(fee + 1), action_scope=action_scope)
announcement_assertions, coin_spends, new_plotnft = PlotNFT.launch(
launch_result = PlotNFT.launch_plotnft(
origin_coins=list(origin_coins),
user_config=UserConfig(synthetic_pubkey=xch_wallet.convert_public_key_to_synthetic(target_pubkey)),
genesis_challenge=wallet_state_manager.constants.GENESIS_CHALLENGE,
@@ -123,17 +124,19 @@ class PlotNFT2Wallet:
remark=Remark(rest=Program.to(pool_url)) if pool_url is not None else None,
)
async with action_scope.use() as interface:
interface.side_effects.extra_spends.append(WalletSpendBundle(coin_spends, G2Element()))
interface.side_effects.extra_spends.append(WalletSpendBundle(launch_result.necessary_spends, G2Element()))
create_launcher = next(cond for cond in launch_result.necessary_conditions if isinstance(cond, CreateCoin))
other_conditions = iter(cond for cond in launch_result.necessary_conditions if not isinstance(cond, CreateCoin))
await xch_wallet.generate_signed_transaction(
amounts=[uint64(1)],
puzzle_hashes=[new_plotnft.singleton_struct.singleton_puzzles.singleton_launcher_hash],
amounts=[create_launcher.amount],
puzzle_hashes=[create_launcher.puzzle_hash],
action_scope=action_scope,
fee=fee,
coins=origin_coins,
extra_conditions=(*announcement_assertions, *extra_conditions),
origin_id=coin_spends[0].coin.parent_coin_info,
extra_conditions=(*other_conditions, *extra_conditions),
origin_id=launch_result.necessary_spends[0].coin.parent_coin_info,
)
return new_plotnft
return launch_result.launched_singleton
async def claim_rewards(
self,
@@ -154,33 +157,37 @@ class PlotNFT2Wallet:
rewards_to_claim=rewards_to_claim,
reward_delegated_puzzles_and_solutions=[
DelegatedPuzzleAndSolution(
puzzle=self.xch_wallet.make_solution(
primaries=[
CreateCoin(
puzzle_hash=self.rewards_claim_puzhash,
amount=uint64(total_reward_amount - fee),
),
],
fee=fee,
conditions=(*extra_conditions, CreateCoinAnnouncement(b""))
if len(rewards_to_claim) > 1
else extra_conditions,
).at("rf"), # strips away to just the delegated puzzle (bit of a hack)
solution=Program.to(None),
puzzle=UnknownPuzzle(
known_puzzle=self.xch_wallet.make_solution(
primaries=[
CreateCoin(
puzzle_hash=self.rewards_claim_puzhash,
amount=uint64(total_reward_amount - fee),
),
],
fee=fee,
conditions=(*extra_conditions, CreateCoinAnnouncement(b""))
if len(rewards_to_claim) > 1
else extra_conditions,
).at("rf")
), # strips away to just the delegated puzzle (bit of a hack)
solution=UnknownSolution(solution=Program.to(None)),
)
if i == 0
else DelegatedPuzzleAndSolution(
puzzle=Program.to(
(
1,
[
AssertCoinAnnouncement(
asserted_id=rewards_to_claim[0].coin.name(), asserted_msg=b""
).to_program()
],
puzzle=UnknownPuzzle(
known_puzzle=Program.to(
(
1,
[
AssertCoinAnnouncement(
asserted_id=rewards_to_claim[0].coin.name(), asserted_msg=b""
).to_program()
],
)
)
),
solution=Program.to(None),
solution=UnknownSolution(solution=Program.to(None)),
)
for i, reward in enumerate(rewards_to_claim)
],
@@ -204,7 +211,7 @@ class PlotNFT2Wallet:
),
Coin(
parent_coin_info=plotnft.coin.name(),
puzzle_hash=plotnft.puzzle_hash(nonce=uint64(0)),
puzzle_hash=plotnft.puzzle_hash,
amount=uint64(1),
),
],
@@ -229,7 +236,7 @@ class PlotNFT2Wallet:
plotnft = await self.get_current_plotnft()
else:
plotnft = plotnft_override
if plotnft.pool_config is not None:
if plotnft.inner_puzzle.pool_config is not None:
await self.leave_pool(
action_scope=action_scope,
fee=fee,
@@ -244,7 +251,7 @@ class PlotNFT2Wallet:
fee_hook = CreateCoinAnnouncement(msg=b"", coin_id=plotnft.coin.name())
url_remark = Remark(rest=Program.to(pool_url))
coin_spends = plotnft.join_pool(
user_config=plotnft.user_config,
user_config=plotnft.inner_puzzle.user_config,
pool_config=pool_config,
extra_conditions=(*extra_conditions, fee_hook, url_remark),
)
@@ -268,7 +275,9 @@ class PlotNFT2Wallet:
additions=[
Coin(
parent_coin_info=plotnft.coin.name(),
puzzle_hash=dataclasses.replace(plotnft, pool_config=pool_config).puzzle_hash(nonce=0),
puzzle_hash=dataclasses.replace(
plotnft, inner_puzzle=dataclasses.replace(plotnft.inner_puzzle, pool_config=pool_config)
).puzzle_hash,
amount=uint64(1),
)
],
@@ -293,17 +302,21 @@ class PlotNFT2Wallet:
):
raise ValueError("Both new_pool_url or new_pool_config must be provided together")
plotnft = await self.get_current_plotnft()
if not plotnft.pooling or plotnft.exiting:
if not plotnft.inner_puzzle.pooling or plotnft.inner_puzzle.exiting:
raise ValueError("`leave_pool` called on a non-pooling or exiting PlotNFT")
next_plotnft = dataclasses.replace(plotnft, exiting=True)
next_plotnft = dataclasses.replace(
plotnft, inner_puzzle=dataclasses.replace(plotnft.inner_puzzle, exiting=True)
)
fee_hook = CreateCoinAnnouncement(msg=b"", coin_id=plotnft.coin.name())
exit_create_coin = plotnft.exit_to_waiting_room_condition()
exit_create_coin = plotnft.inner_puzzle.exit_to_waiting_room_condition
exit_to_waiting_room_dpuz_and_sol = DelegatedPuzzleAndSolution(
puzzle=self.xch_wallet.make_solution(
primaries=[exit_create_coin],
conditions=(*extra_conditions, fee_hook),
).at("rf"), # strips away to just the delegated puzzle (bit of a hack)
solution=Program.to(None),
puzzle=UnknownPuzzle(
known_puzzle=self.xch_wallet.make_solution(
primaries=[exit_create_coin],
conditions=(*extra_conditions, fee_hook),
).at("rf")
), # strips away to just the delegated puzzle (bit of a hack)
solution=UnknownSolution(solution=Program.to(None)),
)
coin_spends = plotnft.exit_to_waiting_room(exit_to_waiting_room_dpuz_and_sol)
if fee > 0:
@@ -334,7 +347,7 @@ class PlotNFT2Wallet:
additions=[
Coin(
parent_coin_info=plotnft.coin.name(),
puzzle_hash=next_plotnft.puzzle_hash(nonce=0),
puzzle_hash=next_plotnft.puzzle_hash,
amount=uint64(1),
)
],
@@ -353,19 +366,21 @@ class PlotNFT2Wallet:
) -> None:
plotnft = await self.get_current_plotnft()
fee_hook = CreateCoinAnnouncement(msg=b"", coin_id=plotnft.coin.name())
heightlock, exit_create_coin = plotnft.exit_from_waiting_room_conditions()
heightlock, exit_create_coin = plotnft.inner_puzzle.exit_from_waiting_room_conditions
exit_to_waiting_room_dpuz_and_sol = DelegatedPuzzleAndSolution(
puzzle=self.xch_wallet.make_solution(
primaries=[exit_create_coin],
conditions=(fee_hook, heightlock, *extra_conditions),
).at("rf"), # strips away to just the delegated puzzle (bit of a hack)
solution=Program.to(None),
puzzle=UnknownPuzzle(
known_puzzle=self.xch_wallet.make_solution(
primaries=[exit_create_coin],
conditions=(fee_hook, heightlock, *extra_conditions),
).at("rf")
), # strips away to just the delegated puzzle (bit of a hack)
solution=UnknownSolution(solution=Program.to(None)),
)
coin_spends = plotnft.exit_waiting_room(exit_to_waiting_room_dpuz_and_sol)
next_plotnft = PlotNFT.get_next_from_coin_spend(
coin_spend=coin_spends[0],
genesis_challenge=self.wallet_state_manager.constants.GENESIS_CHALLENGE,
previous_plotnft_puzzle=plotnft,
previous_plotnft_puzzle=plotnft.inner_puzzle,
)
if exiting_info.exiting_fee > 0:
await self.xch_wallet.create_tandem_xch_tx(
@@ -387,9 +402,10 @@ class PlotNFT2Wallet:
additions=[
Coin(
parent_coin_info=plotnft.coin.name(),
puzzle_hash=dataclasses.replace(plotnft, pool_config=None, exiting=False).puzzle_hash(
nonce=0
),
puzzle_hash=dataclasses.replace(
plotnft,
inner_puzzle=dataclasses.replace(plotnft.inner_puzzle, pool_config=None, exiting=False),
).puzzle_hash,
amount=uint64(1),
)
],
@@ -411,7 +427,7 @@ class PlotNFT2Wallet:
async def coin_added(self, coin: Coin, height: uint32, peer: WSChiaConnection, coin_data: object | None) -> None:
if isinstance(coin_data, PlotNFT):
index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash(
puzzle_hash_for_synthetic_public_key(coin_data.user_config.synthetic_pubkey)
puzzle_hash_for_synthetic_public_key(coin_data.inner_puzzle.user_config.synthetic_pubkey)
)
if index is None:
raise ValueError(f"No index found for synthetic pubkey for launcher_id: {coin_data.launcher_id}")
@@ -424,14 +440,14 @@ class PlotNFT2Wallet:
root_path=self.wallet_state_manager.root_path,
p2_singleton_puzzle_hash=self.p2_singleton_puzzle_hash,
) as pool_config:
pool_config.owner_public_key = coin_data.user_config.synthetic_pubkey
pool_config.owner_public_key = coin_data.inner_puzzle.user_config.synthetic_pubkey
pool_config.key_derivation_index = int(index)
if coin_data.pool_config is not None:
if coin_data.pool_config.pool_puzzle_hash != pool_config.target_puzzle_hash:
if coin_data.inner_puzzle.pool_config is not None:
if coin_data.inner_puzzle.pool_config.pool_puzzle_hash != pool_config.target_puzzle_hash:
pool_config.pool_url = await self.wallet_state_manager.plotnft2_store.get_latest_remark(
coin_data.launcher_id
)
pool_config.target_puzzle_hash = coin_data.pool_config.pool_puzzle_hash
pool_config.target_puzzle_hash = coin_data.inner_puzzle.pool_config.pool_puzzle_hash
else:
pool_config.target_puzzle_hash = bytes32.from_hexstr(pool_config.payout_instructions)
else:
@@ -442,11 +458,11 @@ class PlotNFT2Wallet:
PoolingShareState(
launcher_id=coin_data.launcher_id,
pool_url=await self.wallet_state_manager.plotnft2_store.get_latest_remark(coin_data.launcher_id)
if coin_data.pool_config is not None
if coin_data.inner_puzzle.pool_config is not None
else "",
owner_public_key=coin_data.user_config.synthetic_pubkey,
target_puzzle_hash=coin_data.pool_config.pool_puzzle_hash
if coin_data.pool_config is not None
owner_public_key=coin_data.inner_puzzle.user_config.synthetic_pubkey,
target_puzzle_hash=coin_data.inner_puzzle.pool_config.pool_puzzle_hash
if coin_data.inner_puzzle.pool_config is not None
else payout_puzzle_hash,
p2_singleton_puzzle_hash=self.p2_singleton_puzzle_hash,
payout_instructions=payout_puzzle_hash.hex(),
@@ -454,9 +470,10 @@ class PlotNFT2Wallet:
version=2,
).add(root_path=self.wallet_state_manager.root_path)
if coin_data.exiting:
if coin_data.inner_puzzle.exiting:
await self.wallet_state_manager.plotnft2_store.add_exiting_height(
wallet_id=self.id(), height=uint32(height + coin_data.guaranteed_pool_config.heightlock)
wallet_id=self.id(),
height=uint32(height + coin_data.inner_puzzle.guaranteed_pool_config.heightlock),
)
else:
finish_height = await self.wallet_state_manager.plotnft2_store.get_exiting_height(wallet_id=self.id())
@@ -516,12 +533,12 @@ class PlotNFT2Wallet:
self,
) -> PoolWalletInfo: # backwards compat with previous pool wallet
plotnft = await self.get_current_plotnft()
if plotnft.pool_config is None:
if plotnft.inner_puzzle.pool_config is None:
singleton_state = PoolSingletonState.SELF_POOLING
rewards_claim_ph = self.rewards_claim_puzhash
else:
rewards_claim_ph = plotnft.pool_config.pool_puzzle_hash
if plotnft.exiting:
rewards_claim_ph = plotnft.inner_puzzle.pool_config.pool_puzzle_hash
if plotnft.inner_puzzle.exiting:
singleton_state = PoolSingletonState.LEAVING_POOL
else:
singleton_state = PoolSingletonState.FARMING_TO_POOL
@@ -531,11 +548,13 @@ class PlotNFT2Wallet:
version=uint8(2),
state=uint8(singleton_state.value),
target_puzzle_hash=rewards_claim_ph,
owner_pubkey=plotnft.user_config.synthetic_pubkey,
owner_pubkey=plotnft.inner_puzzle.user_config.synthetic_pubkey,
pool_url=await self.wallet_state_manager.plotnft2_store.get_latest_remark(plotnft.launcher_id)
if plotnft.pool_config is not None
if plotnft.inner_puzzle.pool_config is not None
else None,
relative_lock_height=plotnft.pool_config.heightlock if plotnft.pool_config is not None else uint32(0),
relative_lock_height=plotnft.inner_puzzle.pool_config.heightlock
if plotnft.inner_puzzle.pool_config is not None
else uint32(0),
),
target=PoolState(
version=uint8(2),
@@ -545,17 +564,17 @@ class PlotNFT2Wallet:
target_puzzle_hash=self.rewards_claim_puzhash
if exiting_info.next_pool_puzzle_hash is None
else exiting_info.next_pool_puzzle_hash,
owner_pubkey=plotnft.user_config.synthetic_pubkey,
owner_pubkey=plotnft.inner_puzzle.user_config.synthetic_pubkey,
pool_url=None if exiting_info.next_pool_url is None else exiting_info.next_pool_url,
relative_lock_height=uint32(0)
if exiting_info.next_heightlock is None
else exiting_info.next_heightlock,
)
if plotnft.exiting
if plotnft.inner_puzzle.exiting
else None,
launcher_coin=Coin(bytes32.zeros, bytes32.zeros, uint64(0)),
launcher_id=plotnft.launcher_id,
p2_singleton_puzzle_hash=RewardPuzzle(singleton_id=plotnft.launcher_id).puzzle_hash(),
p2_singleton_puzzle_hash=P2SingletonPuzzle(singleton_id=plotnft.launcher_id).puzzle_hash,
tip_singleton_coin_id=plotnft.coin.name(),
singleton_block_height=await self.wallet_state_manager.plotnft2_store.get_plotnft_created_height(
coin_id=plotnft.coin.name()
@@ -9,7 +9,7 @@ from chia_rs.sized_bytes import bytes32
from typing_extensions import runtime_checkable
from chia.types.blockchain_format.program import Program
from chia.wallet.puzzles.puzzle_drivers import InnerPuzzle, UnknownPuzzle
from chia.wallet.puzzles.puzzle_drivers import UnknownPuzzle
from chia.wallet.util.merkle_tree import MerkleTree, hash_a_pair, hash_an_atom
MofN_MOD = Program.from_bytes(puzzle_mods.M_OF_N)
@@ -217,6 +217,27 @@ class MofN: # Technically matches Puzzle protocol but is a bespoke part of the
else:
return self.puzzle(nonce).get_tree_hash()
@classmethod
def match(cls, *, unknown_puzzle: UnknownPuzzle, solution: object | None = None) -> MofN | None:
if unknown_puzzle.mod not in [MofN_MOD, NofN_MOD, OneOfN_MOD] or unknown_puzzle.curried_args is None: # ruff: ignore[literal-membership]
return None
if unknown_puzzle.mod == NofN_MOD:
list_of_members = [_ for _ in unknown_puzzle.curried_args]
pwr_matches = [
PuzzleWithRestrictions.match(unknown_puzzle=UnknownPuzzle(known_puzzle=member))
for member in list_of_members
]
if None in pwr_matches:
return None
# come on mypy, be better
return MofN(m=len(list_of_members), members=pwr_matches) # type: ignore[arg-type]
elif unknown_puzzle.mod == MofN_MOD:
(m, _) = unknown_puzzle.curried_args
return MofN(m=m.as_int(), members=[])
else:
return MofN(m=1, members=[])
# A convenience object for hinting the two solution values that must always exist
@dataclass(kw_only=True, frozen=True)
@@ -432,7 +453,7 @@ class PuzzleWithRestrictions:
return solution
@classmethod
def match(cls, *, unknown_puzzle: UnknownPuzzle, solution: object | None = None) -> InnerPuzzle | None:
def match(cls, *, unknown_puzzle: UnknownPuzzle, solution: object | None = None) -> PuzzleWithRestrictions | None:
if unknown_puzzle.mod != INDEX_WRAPPER or unknown_puzzle.curried_args is None:
return None
@@ -465,4 +486,4 @@ class PuzzleWithRestrictions:
restrictions = []
inner_puzzle = potentially_restricted_puzzle
return cls(nonce=nonce, restrictions=restrictions, puzzle=inner_puzzle) # type: ignore[return-value, arg-type]
return cls(nonce=nonce, restrictions=restrictions, puzzle=inner_puzzle) # type: ignore[arg-type]
+53 -6
View File
@@ -11,6 +11,7 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
calculate_synthetic_public_key,
calculate_synthetic_secret_key,
)
from chia.wallet.puzzles.puzzle_drivers import UnknownPuzzle, UnknownSolution
from chia.wallet.singleton import SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_MOD_HASH
BLS_WITH_TAPROOT_MEMBER_MOD = Program.from_bytes(puzzle_mods.BLS_WITH_TAPROOT_MEMBER)
@@ -33,13 +34,16 @@ class BLSWithTaprootMember:
def memo(self, nonce: int) -> Program:
return Program.to(0)
def puzzle(self, nonce: int) -> Program:
if self.synthetic_key is None:
assert self.public_key is not None and self.hidden_puzzle is not None
synthetic_public_key = calculate_synthetic_public_key(self.public_key, self.hidden_puzzle.get_tree_hash())
return BLS_WITH_TAPROOT_MEMBER_MOD.curry(bytes(synthetic_public_key))
@property
def guaranteed_synthetic_key(self) -> G1Element:
if self.synthetic_key is not None:
return self.synthetic_key
else:
return BLS_WITH_TAPROOT_MEMBER_MOD.curry(bytes(self.synthetic_key))
assert self.hidden_puzzle is not None # guarded by __post_init__
return calculate_synthetic_public_key(self.public_key, self.hidden_puzzle.get_tree_hash())
def puzzle(self, nonce: int) -> Program:
return BLS_WITH_TAPROOT_MEMBER_MOD.curry(bytes(self.guaranteed_synthetic_key))
def puzzle_hash(self, nonce: int) -> bytes32:
return self.puzzle(nonce).get_tree_hash()
@@ -57,6 +61,49 @@ class BLSWithTaprootMember:
return Program.to([self.public_key, self.hidden_puzzle])
return Program.to([0])
@classmethod
def match(
cls, *, unknown_puzzle: UnknownPuzzle, unknown_solution: UnknownSolution | None = None
) -> BLSWithTaprootMember | None:
if unknown_puzzle.mod != BLS_WITH_TAPROOT_MEMBER_MOD or unknown_puzzle.curried_args is None:
return None
(synthetic_key_prog,) = unknown_puzzle.curried_args
synthetic_key = G1Element.from_bytes(synthetic_key_prog.as_atom())
original_key = None
hidden_puzzle = None
if unknown_solution is not None:
solution_match = BLSWithTaprootMemberSolution.match(unknown_solution=unknown_solution)
if solution_match is not None:
original_key = solution_match.original_public_key
hidden_puzzle = solution_match.hidden_puzzle
return BLSWithTaprootMember(synthetic_key=synthetic_key, public_key=original_key, hidden_puzzle=hidden_puzzle)
@dataclass(kw_only=True, frozen=True)
class BLSWithTaprootMemberSolution:
original_public_key: G1Element | None = None
hidden_puzzle: Program | None = None
def __post_init__(self) -> None:
if (self.original_public_key is not None and self.hidden_puzzle is None) or (
self.original_public_key is None and self.hidden_puzzle is not None
):
raise ValueError("Must specify both or neither of original_public_key and hidden_puzzle")
@classmethod
def match(cls, *, unknown_solution: UnknownSolution) -> BLSWithTaprootMemberSolution | None:
if unknown_solution.as_program().atom is not None:
return None
list_of_values = list(unknown_solution.as_program().as_iter())
if len(list_of_values) == 2:
return BLSWithTaprootMemberSolution(
original_public_key=G1Element.from_bytes(list_of_values[0].as_atom()), hidden_puzzle=list_of_values[1]
)
elif len(list_of_values) == 1:
return BLSWithTaprootMemberSolution()
else:
return None
@dataclass(kw_only=True, frozen=True)
class SingletonMember:
+6 -6
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass, field
from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING, ClassVar, Protocol, TypeVar, cast
@@ -50,25 +50,25 @@ class OptimizedPuzzleHashPuzzle(Protocol):
def puzzle_hash_optimized(self) -> bytes32: ...
@dataclass
class PuzzleWithPuzzleHash:
"""
This is designed to be a base class to `Inner/OuterPuzzle`s which provides caching on the puzzle hash generation
"""
pre_computed_puzzle_hash: bytes32 | None = field(default=None, kw_only=True)
pre_computed_puzzle_hash: bytes32 | None = None
@property
def puzzle_hash(self) -> bytes32:
if self.pre_computed_puzzle_hash is None:
if isinstance(self, OptimizedPuzzleHashPuzzle):
self.pre_computed_puzzle_hash = self.puzzle_hash_optimized
object.__setattr__(self, "pre_computed_puzzle_hash", self.puzzle_hash_optimized)
else:
self.pre_computed_puzzle_hash = self.puzzle.get_tree_hash() # type: ignore[attr-defined]
object.__setattr__(self, "pre_computed_puzzle_hash", self.puzzle.get_tree_hash()) # type: ignore[attr-defined]
assert self.pre_computed_puzzle_hash is not None
return self.pre_computed_puzzle_hash
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class UnknownPuzzle(PuzzleWithPuzzleHash):
if TYPE_CHECKING:
_protocol_check: ClassVar[InnerPuzzle] = cast("UnknownPuzzle", None)
+23 -16
View File
@@ -1,8 +1,9 @@
from __future__ import annotations
from dataclasses import dataclass, field
from collections.abc import Sequence
from dataclasses import dataclass, field, replace
from functools import cached_property
from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar, cast
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast
from chia_rs import Coin, CoinSpend
from chia_rs.sized_bytes import bytes32
@@ -95,7 +96,7 @@ class SingletonStruct:
_T_InnerPuzzle = TypeVar("_T_InnerPuzzle", bound=InnerPuzzle)
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class SingletonPuzzle(PuzzleWithPuzzleHash, Generic[_T_InnerPuzzle]):
if TYPE_CHECKING:
_outer_puzzle_protocol_check: ClassVar[OuterPuzzle[InnerPuzzle]] = cast("SingletonPuzzle[_T_InnerPuzzle]", None)
@@ -140,6 +141,11 @@ class SingletonPuzzle(PuzzleWithPuzzleHash, Generic[_T_InnerPuzzle]):
inner_puzzle=UnknownPuzzle(known_puzzle=inner_puzzle),
)
def with_inner_puzzle(self, inner_puzzle: InnerPuzzle) -> Any:
# should be used as little as possible because it doesn't seem possible to return a
# newly paramed instance of the current type so we have to resort to Any
return replace(self, inner_puzzle=inner_puzzle) # type: ignore[arg-type]
@dataclass(kw_only=True)
class SingletonSolution:
@@ -187,7 +193,7 @@ class SingletonLaunchInfo(Generic[_T_InnerPuzzle]):
@dataclass(kw_only=True, frozen=True)
class SingletonLaunchResult(Generic[_T_InnerPuzzle]):
necessary_conditions: list[Condition]
launcher_spend: CoinSpend
necessary_spends: list[CoinSpend]
launched_singleton: Singleton[_T_InnerPuzzle]
@@ -199,10 +205,7 @@ def _new_create_coin_from_inner_puzzle_and_solution(inner_puzzle: InnerPuzzle, s
)
_T_LaunchInnerPuzzle = TypeVar("_T_LaunchInnerPuzzle", bound=InnerPuzzle)
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class Singleton(SingletonPuzzle[_T_InnerPuzzle]):
if TYPE_CHECKING:
_smart_coin_protocol_check: ClassVar[SmartCoin] = cast("Singleton[_T_InnerPuzzle]", None)
@@ -210,6 +213,8 @@ class Singleton(SingletonPuzzle[_T_InnerPuzzle]):
coin: Coin
lineage_proof: LineageProof
_T_LaunchInnerPuzzle = TypeVar("_T_LaunchInnerPuzzle", bound=InnerPuzzle)
@classmethod
def launch(
cls,
@@ -240,11 +245,13 @@ class Singleton(SingletonPuzzle[_T_InnerPuzzle]):
return SingletonLaunchResult(
necessary_conditions=[create_launcher_condition, assert_launcher_announcement],
launcher_spend=make_spend(
launcher_coin,
cls.singleton_puzzles.singleton_launcher,
launcher_solution,
),
necessary_spends=[
make_spend(
launcher_coin,
cls.singleton_puzzles.singleton_launcher,
launcher_solution,
)
],
launched_singleton=Singleton(
coin=Coin(
parent_coin_info=launcher_id,
@@ -287,7 +294,7 @@ class Singleton(SingletonPuzzle[_T_InnerPuzzle]):
def claim_p2_singletons(
self,
*,
rewards_to_claim: list[P2Singleton],
rewards_to_claim: Sequence[P2Singleton],
reward_delegated_puzzles_and_solutions: list[DelegatedPuzzleAndSolution],
) -> tuple[list[CoinSpend], list[SendMessage]]:
if len(rewards_to_claim) != len(reward_delegated_puzzles_and_solutions):
@@ -313,7 +320,7 @@ class Singleton(SingletonPuzzle[_T_InnerPuzzle]):
], messages_to_send
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class P2SingletonPuzzle(PuzzleWithPuzzleHash):
if TYPE_CHECKING:
_protocol_check: ClassVar[InnerPuzzle] = cast("P2SingletonPuzzle", None)
@@ -359,6 +366,6 @@ class P2SingletonPuzzle(PuzzleWithPuzzleHash):
return cls(singleton_id=bytes32(singleton_struct_prog.at("rf").as_atom()))
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class P2Singleton(P2SingletonPuzzle):
coin: Coin
+11 -7
View File
@@ -28,13 +28,13 @@ from chia.wallet.puzzles.puzzle_drivers import (
from chia.wallet.util.curry_and_treehash import curry_and_treehash, shatree_atom
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class HiddenPuzzleInfo(PuzzleWithPuzzleHash):
puzzle: Program = field(default_factory=lambda: DEFAULT_HIDDEN_PUZZLE)
pre_computed_puzzle_hash: bytes32 | None = field(default=DEFAULT_HIDDEN_PUZZLE_HASH, kw_only=True)
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class StandardPuzzle(PuzzleWithPuzzleHash):
if TYPE_CHECKING:
_protocol_check: ClassVar[InnerPuzzle] = cast("StandardPuzzle", None)
@@ -51,9 +51,12 @@ class StandardPuzzle(PuzzleWithPuzzleHash):
def synthetic_public_key(self) -> G1Element:
if self.pre_known_synthetic_public_key is None:
assert self.pre_known_original_public_key is not None # guarded by __post_init__
self.pre_known_synthetic_public_key = calculate_synthetic_public_key(
self.pre_known_original_public_key, self.hidden_puzzle_info.puzzle_hash
object.__setattr__(
self,
"pre_known_synthetic_public_key",
calculate_synthetic_public_key(self.pre_known_original_public_key, self.hidden_puzzle_info.puzzle_hash),
)
assert self.pre_known_synthetic_public_key is not None
return self.pre_known_synthetic_public_key
@property
@@ -80,8 +83,9 @@ class StandardPuzzle(PuzzleWithPuzzleHash):
raise ValueError("Trying to match a standard puzzle without a standard puzzle solution")
if solution.original_public_key is not None:
original_public_key = solution.original_public_key
hidden_puzzle_info.puzzle = solution.delegated_puzzle
hidden_puzzle_info.pre_computed_puzzle_hash = None
hidden_puzzle_info = HiddenPuzzleInfo(
puzzle=solution.delegated_puzzle, pre_computed_puzzle_hash=None
)
return cls(
pre_known_synthetic_public_key=G1Element.from_bytes(list_of_args[0].as_atom()),
pre_known_original_public_key=original_public_key,
@@ -136,7 +140,7 @@ class StandardPuzzleSolution:
)
@dataclass(kw_only=True)
@dataclass(kw_only=True, frozen=True)
class StandardXCHCoin(StandardPuzzle):
if TYPE_CHECKING:
_protocol_check_2: ClassVar[SmartCoin] = cast("StandardXCHCoin", None)
+3 -2
View File
@@ -18,7 +18,7 @@ from clvm_tools.binutils import assemble
from chia.consensus.block_rewards import calculate_base_farmer_reward
from chia.data_layer.data_layer_util import DLProof, VerifyProofResponse, dl_verify_proof
from chia.data_layer.data_layer_wallet import Mirror
from chia.pools.plotnft_drivers import PoolConfig, RewardPuzzle
from chia.pools.plotnft_drivers import PoolConfig
from chia.pools.pool_wallet import PoolWallet
from chia.pools.pool_wallet_info import (
FARMING_TO_POOL,
@@ -64,6 +64,7 @@ from chia.wallet.outer_puzzles import AssetType
from chia.wallet.plotnft_wallet.plotnft_wallet import PlotNFT2Wallet
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings, ClawbackMetadata
from chia.wallet.puzzles.singleton_drivers import P2SingletonPuzzle
from chia.wallet.remote_wallet.remote_wallet import RemoteWallet
from chia.wallet.signer_protocol import SigningResponse
from chia.wallet.singleton import (
@@ -1214,7 +1215,7 @@ class WalletRpcApi:
transaction=REPLACEABLE_TRANSACTION_RECORD,
total_fee=uint64(request.fee),
launcher_id=plotnft.launcher_id,
p2_singleton_puzzle_hash=RewardPuzzle(singleton_id=plotnft.launcher_id).puzzle_hash(),
p2_singleton_puzzle_hash=P2SingletonPuzzle(singleton_id=plotnft.launcher_id).puzzle_hash,
# irrelevant, will be replace in serialization
type=WalletType.PLOTNFT_2.name,
wallet_id=uint32(0),
+1 -1
View File
@@ -1002,7 +1002,7 @@ class WalletStateManager:
coin_spend=coin_spend,
genesis_challenge=self.constants.GENESIS_CHALLENGE,
pre_uncurry=uncurried,
previous_plotnft_puzzle=previous_plotnft,
previous_plotnft_puzzle=previous_plotnft.inner_puzzle if previous_plotnft is not None else None,
)
for id, wallet in self.wallets.items():
if isinstance(wallet, PlotNFT2Wallet) and wallet.plotnft_id == next_plot_nft.launcher_id: