So far, all of the Chialisp puzzles we see from the previous posts have one thing in common; the puzzle itself creates the conditions. However, the puzzle could also be provided the conditions as solution to output as well.
Here is the simplest example:
(mod conditions
conditions
)
❯ brun (run '(mod conditions conditions)') '((51 0xcafef00d 1000) (73 1000))'
((51 0xcafef00d 1000) (73 1000))
Better yet, Chialisp puzzle, treating code (as a 1st class citizen) like data, can be provided a puzzle and solution to execute and output the conditions!
We call the provided puzzle that can create the ouput conditions, delegated puzzle.
Here is the simplest example:
(mod (delegated_puzzle solution)
(a delegated_puzzle solution)
)
Let's pass the simplest puzzle above:
# the delegated puzzle
❯ run '(mod conditions conditions)'
1
# the delegaged puzzle is the 1st parameter
# the solution for the delegated puzzle is the 2nd parameter
❯ brun (run '(mod (delegated_puzzle solution) (a delegated_puzzle solution))') '(1 ((51 0xcafef00d 1000) (73 1000)))'
((51 0xcafef00d 1000) (73 1000))
But as we know, the puzzle is not secure if there is no AGG_SIG_ME
in the list of conditions. Also, we will need to have public key available too if we want to include AGG_SIG_ME
condition.
Here is the version with AGG_SIG_ME
:
(mod (PUB_KEY delegated_puzzle solution)
(include condition_codes.clib)
(c
; hard-coded message
(list AGG_SIG_ME PUB_KEY (sha256 "hello delegated puzzle"))
(a delegated_puzzle solution)
)
)
...
MOD = load_clvm("simple_pay_to_delegated.clsp", package_or_requirement=__name__, search_paths=["../include"])
# create a smart coin and curry in alice's pk
amt = 1_000_000
alice_mod = MOD.curry(alice.pk())
alice_coin = asyncio.run(
alice.launch_smart_coin(
alice_mod,
amt=amt
)
)
# (delegated_puzzle solution)
solution = Program.to([
1, # (mod conditions conditions)
[
[ConditionOpcode.CREATE_COIN, bob.puzzle_hash, amt],
[ConditionOpcode.ASSERT_MY_AMOUNT, alice_coin.amount]
]
])
# create a spend bundle with alice's signature
spend = CoinSpend(
alice_coin.as_coin(),
alice_mod,
solution
)
# hard-coded message
message: bytes = std_hash(bytes("hello delegated puzzle", "utf-8"))
alice_sk: PrivateKey = alice.pk_to_sk(alice.pk())
sig: G2Element = AugSchemeMPL.sign(
alice_sk,
message
+ alice_coin.name()
+ DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA,
)
spend_bundle = SpendBundle([spend], sig)
Running the simulation:
❯ python3 ./simple_pay_to_delegated.py
alice balance: 2000000000000
alice smart coin: {'amount': 1000000,
'parent_coin_info': '0x8d011a3236082916e08a2214379a063b38a8c7c2ed7fb6a708acf824e1d9b310',
'puzzle_hash': '0x1a54f0b830d632b8ccbbf8ce0202489ce9312aad6f407c1ddb21def66e763345'}
push spend bundle:
{'aggregated_signature': '0x8a1fe76f86f45975bad21b38ab759675300f04c02be911b572d66270e56d1c87ddb9255bbaeca646f72e97d94e1737d0169130ee2c9038a154fe15fdc6218f36e30deaf4a00914659156dc91f68e6ceae3cbdc9c9ee182e19b04f3f082d08c92',
'coin_spends': [{'coin': {'amount': 1000000,
'parent_coin_info': '0x8d011a3236082916e08a2214379a063b38a8c7c2ed7fb6a708acf824e1d9b310',
'puzzle_hash': '0x1a54f0b830d632b8ccbbf8ce0202489ce9312aad6f407c1ddb21def66e763345'},
'puzzle_reveal': '0xff02ffff01ff02ffff01ff04ffff04ff02ffff04ff05ffff01ffa011e82913276355e092ff40373677de4a87461938fb975e02b0ffe08fb3d88ba9808080ffff02ff0bff178080ffff04ffff0132ff018080ffff04ffff01b0ac2f40f6cb161f872f61910bdacd811534e5b5753242553d9022906cdfa479e172b1eac8e1f38a3743b7897e58942442ff018080',
'solution': '0xff01ffffff33ffa05abb5d5568b4a7411dd97b3356cfedfac09b5fb35621a7fa29ab9b59dc905fb6ff830f424080ffff49ff830f4240808080'}]}
alice balance: 1999999000000
bob balance: 1000000
The above example is not secure because the malicious actor can modify the delegated puzzle and solution. We could fix this by signing the hash of the delegated puzzle, so we are certain that nobody changes the delegated puzzle.
(c
; check if the delegated puzzle is tampered
(list AGG_SIG_ME PUB_KEY (sha256tree delegated_puzzle))
(a delegated_puzzle solution)
)
# message is a tree hash of `(mod conditions conditions)`
message: bytes = Program.to(1).get_tree_hash()
alice_sk: PrivateKey = alice.pk_to_sk(alice.pk())
sig: G2Element = AugSchemeMPL.sign(
alice_sk,
message
+ alice_coin.name()
+ DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA,
)
The python code above shows how we pre-committed the delegated puzzle, 1
or (mod conditions conditions)
without storing it inside the coin puzzle. We verify that the provided delegated_puzzle
matches the expected puzzle by verifying the puzzle hash using AGG_SIG_ME
.
We could also create a coin with pre-committed delegated puzzle and we just need solution to spend it.
(mod (PUZZLE PUB_KEY solution)
(include condition_codes.clib)
(include sha256tree.clib)
(c
(list AGG_SIG_ME PUB_KEY (sha256tree PUZZLE))
(a PUZZLE solution)
)
)
; password is hard-coded, "hello"
(mod (password new_puzhash amount)
(if (= (sha256 password) (q . 0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824))
(list (list 51 new_puzhash amount))
(x "wrong password")
)
)
password_locked_delegated = load_clvm("password-locked-delegated.clsp", package_or_requirement=__name__, search_paths=["../include"])
password_locked_curried = load_clvm(
"password-locked-curried.clsp", package_or_requirement=__name__, search_paths=["../include"]
).curry(password_locked_delegated, alice.pk_) # curry PUZZLE and PUB_KEY
print(password_locked_curried)
amt = 1_000_000_000
password_locked_curried_coin = launch_smart_coin(
alice,
password_locked_curried,
amt)
password_locked_curried_coin_solution = Program.to([
["hello", bob.puzzle_hash, amt]
])
# sign the tree hash of password-locked-delegated puzzle
sig = AugSchemeMPL.sign(alice.sk_,
(
password_locked_delegated.get_tree_hash()
+ password_locked_curried_coin.name()
+ DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA
)
)
spend_bundle = SpendBundle(
[
CoinSpend(
password_locked_curried_coin.as_coin(),
password_locked_curried,
password_locked_curried_coin_solution,
)
],
sig,
)
{
"aggregated_signature": "0xa46145ff038ae33d5776901c1a3dd36579ee6afda16cdb80a1ddaf4078b0a00703156c259bd1190dd321a25a7574946a057111aa4f0d9921dad7a3e6049e81bda1a8822439d885dfd7f9bb4e97a21489a7a9bf8b4a1e48919b4aa8c41ef6b6bd",
"coin_spends": [
{
"coin": {
"amount": 1000000000,
"parent_coin_info": "0x8d011a3236082916e08a2214379a063b38a8c7c2ed7fb6a708acf824e1d9b310",
"puzzle_hash": "0x1a45826b53cf0f3d759f664aad8e2303796284ecd59dbc78cb72cbc113dbd711"
},
"puzzle_reveal": "0xff02ffff01ff02ffff01ff04ffff04ff04ffff04ff0bffff04ffff02ff06ffff04ff02ffff04ff05ff80808080ff80808080ffff02ff05ff178080ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01ff02ffff03ffff09ffff0bff0280ffff01a02cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b982480ffff01ff04ffff04ffff0133ffff04ff05ffff04ff0bff80808080ff8080ffff01ff08ffff018e77726f6e672070617373776f72648080ff0180ffff04ffff01b0ac2f40f6cb161f872f61910bdacd811534e5b5753242553d9022906cdfa479e172b1eac8e1f38a3743b7897e58942442ff01808080",
"solution": "0xffff8568656c6c6fffa05abb5d5568b4a7411dd97b3356cfedfac09b5fb35621a7fa29ab9b59dc905fb6ff843b9aca008080"
}
]
}
Pay To Delegated Puzzle or Hidden Puzzle
Chia team provides and recommmends utilizing a standard transaction with Pay to "Delegated Puzzle" or "Hidden Puzzle" for most vanilla transactions. Coins with the puzzle can be unlocked by either signing a delegated puzzle and its solution with a synthetic private key OR by revealing the hidden puzzle and the underlying original key.
The puzzle of normal coins you see in your wallet are the standard puzzle. If you look at the signature of the puzzle, you will see that SYNTHETIC_PUBLIC_KEY
has to be curried in.
(SYNTHETIC_PUBLIC_KEY public_key delegated_puzzle solution)
The synthetic key is the key derived from:
- the hidden puzzle itself.
- the public key that can sign for the delegated spend case.
synthetic_offset == sha256(hidden_puzzle_hash + public_key)
synthentic_public_key == public_key + synthetic_offset_pubkey
The image is from Aggregated Signatures, Taproot, Graftroot, and Standard Transactions
There are two ways to spend the standard transaction:
- Signing a delegated puzzle and its solution with
synthentic_private_key
(SYNTHETIC_PUBLIC_KEY _public_key delegated_puzzle solution)
...
(c
(list AGG_SIG_ME SYNTHETIC_PUBLIC_KEY (sha256tree1 delegated_puzzle))
(a delegated_puzzle solution)
)
- Revealing BOTH the hidden puzzle and the public key, so the standard transaction puzzle can derive the synthetic public key and make sure that it matches the one that is curried in.
(SYNTHETIC_PUBLIC_KEY public_key hidden_puzzle solution)
...
(if (=
SYNTHETIC_PUBLIC_KEY
(point_add ; synthentic_public_key == public_key + synthetic_offset_pubkey
public_key
(pubkey_for_exp ( ; derive public key offset from synthetic_offset
; synthetic_offset == sha256(hidden_puzzle_hash + public_key)
sha256 public_key (sha256tree1 hidden_puzzle)
)
)
)
)
(a hidden_puzzle solution)
(x)
)
p2_delegated_puzzle_or_hidden_puzzle is essentially the "standard coin" in chia. DEFAULT_HIDDEN_PUZZLE_HASH from this puzzle is used with calculate_synthetic_secret_key in the wallet's standard pk_to_sk finder. *
The cool thing about the standard transaction is that we can control how the standard transaction coin is spent by providing our own delegated puzzle and solutions. Or we could also pre-commit the hidden puzzle and spend later as well.
Let's look at some scenarios that we can use the p2_delegated_puzzle_or_hidden_puzzle
.
-
DEFAULT_HIDDEN_PUZZLE_HASH
- alice is the coin owner and wants to send coin to bob
-
DEFAULT_HIDDEN_PUZZLE_HASH
- alice is the coin owner (i.e., coin's puzzle hash encoded to alice's wallet address & changes return to alice).
- alice wants to send xch to charlie.
- bob needs to approve the amount and recipient's address.
-
DEFAULT_HIDDEN_PUZZLE_HASH
- alice, bob, and charlie wants to contribute 1 XCH each (total of 3 XCH) and give to dan.
- everyone has to contribute or the spend won't happen.
-
Saving Coin
- alice and bob has a child named charlie.
- alice and bob wants to save 2 XCH for charlie.
- both alice and bob has to sign to spend the saving coin.
-
Others
alice sends xch to bob.
send random coin on testnet10
As you can see, the standard transaction puzzle is very flexible because we can provide delegated puzzle
or pre-committed puzzle
and solution
that returns any conditions
. It also introduces synthetic keys
which better hide how the puzzle run.
- Aggregated Signatures, Taproot, Graftroot, and Standard Transactions- 2 - Coins, Spends and Wallets | Chialisp.com
- 3 - Deeper into CLVM | Chialisp.com
- 6 - The Standard Transaction | Chialisp.com
- Signatures in Chia
- chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle
- What is Taproot? Technology to Enhance Bitcoin’s Privacy
- What is Bitcoin’s Graftroot? Complete Beginner’s Guide
- Multi-signature application examples