Last active
February 22, 2022 20:56
-
-
Save ojura/b21e7ffdc757a2598246d7bc3c416df8 to your computer and use it in GitHub Desktop.
Full POC for unwrapping doublewrapped Chia CATs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict | |
from chia.types.blockchain_format.coin import Coin | |
from chia.types.blockchain_format.program import Program | |
from chia.wallet.wallet import Wallet | |
from chia.types.blockchain_format.sized_bytes import bytes32 | |
from chia.cmds.wallet_funcs import get_wallet | |
from chia.rpc.wallet_rpc_client import WalletRpcClient | |
from chia.util.default_root import DEFAULT_ROOT_PATH | |
from chia.util.config import load_config | |
from chia.util.ints import uint16 | |
from typing import Optional, Tuple, Iterable, Union, List | |
import io | |
import asyncio | |
from blspy import G2Element | |
from chia.types.blockchain_format.program import INFINITE_COST | |
from chia.types.condition_opcodes import ConditionOpcode | |
from chia.types.spend_bundle import CoinSpend, SpendBundle | |
from chia.util.condition_tools import conditions_for_solution | |
from chia.wallet.lineage_proof import LineageProof | |
from chia.wallet.puzzles.cat_loader import CAT_MOD | |
from chia.wallet.sign_coin_spends import sign_coin_spends | |
NULL_SIGNATURE = G2Element() | |
from blspy import PrivateKey | |
from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened | |
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( | |
DEFAULT_HIDDEN_PUZZLE_HASH, | |
calculate_synthetic_secret_key, | |
) | |
from chia.consensus.default_constants import DEFAULT_CONSTANTS | |
MAX_BLOCK_COST_CLVM = DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM | |
AGG_SIG_ME_ADDITIONAL_DATA = DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA | |
from chia.types.coin_spend import CoinSpend | |
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_for_pk | |
from chia.util.hash import std_hash | |
from chia.types.announcement import Announcement | |
from chia.wallet.cat_wallet.cat_utils import ( | |
CAT_MOD, | |
SpendableCAT, | |
construct_cat_puzzle, | |
unsigned_spend_bundle_for_spendable_cats, | |
match_cat_puzzle, | |
) | |
from chia.wallet.lineage_proof import LineageProof | |
# Tail hash, aka the CAT asset id (here Spacebucks) | |
tail_hash = bytes32.fromhex('78ad32a8c9ea70f27d73e9306fc467bab2a6b15b30289791e37ab6e8612212b1') | |
# Your master private key goes here | |
sk = PrivateKey.from_bytes( | |
bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000")) | |
# Select the correct secret key corresponding to the inner standard wallet puzzlehash | |
# of the coins we're trying to spend | |
key = {} | |
key[0] = master_sk_to_wallet_sk_unhardened(sk, 0) | |
key[8] = master_sk_to_wallet_sk_unhardened(sk, 8) | |
def keyf(pk): | |
for wallet_key in key.values(): | |
synth_key = calculate_synthetic_secret_key(wallet_key, DEFAULT_HIDDEN_PUZZLE_HASH) | |
if synth_key.get_g1() == pk: | |
return synth_key | |
print("couldn't find the appropriate wallet key in the key dictionary") | |
assert False | |
#keyf = lambda _ = pk: calculate_synthetic_secret_key(key, DEFAULT_HIDDEN_PUZZLE_HASH) | |
# To recover the doubly wrapped coin, we send it to a standard wallet puzzlehash. | |
# The regular coin CAT logic will wrap it only once. | |
destination_puzzlehash = bytes32.fromhex("0000000000000000000000000000000000000000000000000000000000000000") | |
coin_spends = [] | |
inner_puzzle = {} | |
cat_puzzle = {} | |
cat_cat_puzzle = {} | |
cat_coin = {} | |
lineage_proof = {} | |
innersol = {} | |
cat_solution = {} | |
cat_cat_solution = {} | |
# Coin with index 0 - "coin 0"; regular CAT coin worth 2 mojos; our value extractor | |
# Coin with index 8 - "coin 8"; double wrapped CAT worth 1 mojo; we're trying to salvage it | |
inner_puzzle[0] = puzzle_for_pk(key[0].get_g1()) | |
cat_puzzle[0] = construct_cat_puzzle(CAT_MOD, tail_hash, inner_puzzle[0]) | |
inner_puzzle[8] = puzzle_for_pk(key[8].get_g1()) | |
cat_puzzle[8] = construct_cat_puzzle(CAT_MOD, tail_hash, inner_puzzle[8]) | |
cat_cat_puzzle[8] = construct_cat_puzzle(CAT_MOD, tail_hash, cat_puzzle[8]) | |
# Populate coin info: parent, puzzlehash, amount | |
cat_coin[0] = Coin( | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
2) | |
# Double check we got everything right | |
assert cat_coin[0].name() == bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000') | |
assert cat_coin[0].puzzle_hash == cat_puzzle[0].get_tree_hash() | |
# Populate the CAT lineage proof info: parent's parent coin id, parent inner puzzle, parent amount | |
lineage_proof[0] = LineageProof( | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
1) | |
# Populate coin info: parent, puzzlehash, amount | |
cat_coin[8] = Coin( | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
1) | |
# Double check we got everything right | |
assert cat_coin[8].name() == bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000') | |
assert cat_coin[8].puzzle_hash == cat_cat_puzzle[8].get_tree_hash() | |
# Populate the CAT lineage proof info: parent's parent coin id, parent inner puzzle, parent amount | |
lineage_proof[8] = LineageProof( | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
bytes32.fromhex('0000000000000000000000000000000000000000000000000000000000000000'), | |
1) | |
coin_spends = [] | |
# We're trying to create a regular CAT worth 2 + 1 = 3 mojos | |
primaries = [{"puzzlehash": destination_puzzlehash, "amount": 3, "memos": [destination_puzzlehash]}] | |
# Crucially, the regular CAT, coin 0, is the one which will create the 3 mojo coin and extract value | |
innersol[0] = Wallet().make_solution(primaries) | |
# The double wrapped CAT goes to the "great beyond", i.e. is spent without creating outputs | |
innersol[8] = Wallet().make_solution([]) | |
# We're not minting or melting anything, so these are all empty | |
extra_delta, limitations_solution = 0, Program.to([]) | |
limitations_program_reveal = Program.to([]) | |
# Coin subtotals: | |
# Coin 0 = 0 by definition (first coin in the ring must have subtotal 0) | |
# Coin 8: 1 (coin 0 has an output of 3 mojos and contributes 2 mojos; | |
# its debt is therefore 1, which is the subtotal for the next coin in the ring, coin 8 | |
# Coin 0 will be the first coin in the ring | |
cat_solution[0] = [innersol[0], | |
lineage_proof[0].to_program(), | |
cat_coin[8].name(), # prev_coin_id: previous coin in the ring is coin 8 | |
cat_coin[0].as_list(), # this_coin_info | |
# next_coin_proof: notice that coin 8 has a CAT PUZZLE as its inner hash! | |
[cat_coin[8].parent_coin_info, cat_puzzle[8].get_tree_hash(), cat_coin[8].amount], | |
1, # PREVIOUS coin's subtotal -- in this case, of coin 8 | |
0 # extra delta | |
] | |
# Add it to the spend bundle | |
coin_spends.append(CoinSpend(cat_coin[0], cat_puzzle[0], Program.to(cat_solution[0]))) | |
# Inner CAT solution for the wrapped CAT coin. | |
cat_solution[8] = [innersol[8], | |
lineage_proof[8].to_program(), | |
cat_coin[0].name(), # prev_coin_id | |
cat_coin[8].as_list(), # this_coin_info | |
# next_coin_proof: since coin 0 is a regular CAT, its inner puzzle is standard | |
[cat_coin[0].parent_coin_info, inner_puzzle[0].get_tree_hash(), cat_coin[0].amount], | |
0, # prev_subtotal: subtotal of coin 0, which is zero by definition | |
0] | |
# Now we do the same dance again, for the outer CAT layer! | |
cat_cat_solution[8] = [ | |
# The solution for the inner CAT layer becomes the inner solution for the outer cat layer! | |
Program.to(cat_solution[8]), | |
lineage_proof[8].to_program(), | |
# All of these are same as for the inner CAT solution | |
cat_coin[0].name(), | |
cat_coin[8].as_list(), | |
[cat_coin[0].parent_coin_info, inner_puzzle[0].get_tree_hash(), cat_coin[0].amount], | |
0, | |
0] | |
# Add the wrapped CAT spend to the unsigned spend bundle | |
coin_spends.append(CoinSpend(cat_coin[8], cat_cat_puzzle[8], Program.to(cat_cat_solution[8]))) | |
for spend in coin_spends: | |
error, conditions, cost = conditions_for_solution( | |
spend.puzzle_reveal.to_program(), | |
spend.solution.to_program(), | |
MAX_BLOCK_COST_CLVM) | |
print('conditions: ') | |
for c in conditions: | |
print(c.opcode, [v.hex() for v in c.vars]) | |
# Sign the coin spends | |
signed_spend_bundle = asyncio.get_event_loop().run_until_complete( | |
sign_coin_spends(coin_spends, keyf, AGG_SIG_ME_ADDITIONAL_DATA, MAX_BLOCK_COST_CLVM)) | |
f = io.BytesIO() | |
signed_spend_bundle.stream(f) | |
print('signed spendbundle: ', f.getvalue().hex()) | |
# now run `cdv rpc pushtx <signed spendbundle bytes printed above>` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment