Skip to content

Instantly share code, notes, and snippets.

@ojura
Last active February 22, 2022 20:56
Show Gist options
  • Save ojura/b21e7ffdc757a2598246d7bc3c416df8 to your computer and use it in GitHub Desktop.
Save ojura/b21e7ffdc757a2598246d7bc3c416df8 to your computer and use it in GitHub Desktop.
Full POC for unwrapping doublewrapped Chia CATs
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