Skip to content

Commit 9c9e6c3

Browse files
committed
Signature puzzle
1 parent 34d35f3 commit 9c9e6c3

3 files changed

Lines changed: 347 additions & 2 deletions

File tree

docs/tutorials/password.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ Next, we need to add `sha2` as a dependency in our Rust project:
240240
cargo add sha2
241241
```
242242

243-
We should change the curried argument struct to match:
243+
We should update the Rust code to match. First, change the curried argument struct:
244244

245245
```rust
246246
#[derive(Debug, Clone, ToClvm, FromClvm)]
@@ -343,3 +343,7 @@ Additionally, the password would only be good for one use, since any other coins
343343
And lastly, a single sha256 hashed password is very easy to brute force and guess.
344344

345345
For these reasons, passwords are not a good way to secure your funds on the blockchain. But it's a good introduction into how coins work and the kinds of attacks you have to prepare for.
346+
347+
## Next Steps
348+
349+
In the [next tutorial](/docs/tutorials/signature.md), we'll create a puzzle that requires a signature from a specific public key to be spent. This fixes all of the security issues with the password puzzle and introduces the concept of BLS signatures and public keys.

docs/tutorials/signature.md

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
# Signature Puzzle
2+
3+
In this tutorial, we'll create a puzzle that locks coins with a public key and requires a signature from the corresponding secret key to be spent.
4+
5+
Before we get started, make sure you have [installed Rue](/docs/installation.md) and Rust.
6+
7+
This was last tested with the following versions:
8+
9+
- **Rue**: 0.6.0
10+
- **Wallet SDK**: 0.32.0
11+
12+
:::info
13+
This is for educational purposes only. While it's secure, there are standard puzzles that accomplish the same thing (with more interoperability with wallets).
14+
:::
15+
16+
## BLS Signatures
17+
18+
In the previous tutorial, we showed how to lock up a coin (albeit insecurely) with a password. If you think about it, what we really wanted to do is prove that we own the coin without giving away the password required to spend it.
19+
20+
This is what BLS signatures are for. You have a secret key that you don't share with anyone else, and a public key that's stored on the blockchain publicly. You can create signatures using the secret key, to prove ownership of the public key without revealing the secret key. This also lets you prevent anyone else from changing the output of the transaction, since you can sign a specific message (in this case, the hash of the conditions).
21+
22+
## Creating the Puzzle
23+
24+
First, we'll create a new project:
25+
26+
```bash
27+
rue init
28+
```
29+
30+
Open it in your editor of choice. You should see a `puzzles` directory with a `main.rue` file containing a simple "hello world" program.
31+
32+
You can replace the contents of `main.rue` with the following:
33+
34+
```rue
35+
fn main(
36+
public_key: PublicKey,
37+
conditions: List<Condition>,
38+
) -> List<Condition> {
39+
let agg_sig = AggSigMe {
40+
public_key,
41+
message: tree_hash(conditions),
42+
};
43+
44+
[agg_sig, ...conditions]
45+
}
46+
```
47+
48+
This program will prepend the list of conditions with an `AGG_SIG_ME` condition. The Chia blockchain's consensus rules will use this to verify that the curried public key has signed off on the hash of the list of conditions. If the conditions are altered, or if the spend doesn't include a valid signature, it will be invalid.
49+
50+
## Setup the Simulator
51+
52+
To test out our puzzle in a simulator, we can setup a Rust project and the [Chia Wallet SDK](https://crates.io/crates/chia-wallet-sdk).
53+
54+
Create a new Rust project in the same directory:
55+
56+
```bash
57+
cargo init --lib
58+
```
59+
60+
And install a couple dependencies:
61+
62+
```bash
63+
cargo add chia-wallet-sdk anyhow
64+
```
65+
66+
Clear the contents of `lib.rs` and define the curried argument type and solution type for the puzzle as structs:
67+
68+
```rust
69+
use chia_wallet_sdk::prelude::*;
70+
71+
#[derive(Debug, Clone, ToClvm, FromClvm)]
72+
#[clvm(curry)]
73+
pub struct SignatureArgs {
74+
pub public_key: PublicKey,
75+
}
76+
77+
#[derive(Debug, Clone, ToClvm, FromClvm)]
78+
#[clvm(list)]
79+
pub struct SignatureSolution<T> {
80+
pub conditions: Conditions<T>,
81+
}
82+
83+
compile_rue!(SignatureArgs = SIGNATURE_MOD, ".");
84+
```
85+
86+
This is going to automatically compile `SIGNATURE_MOD` every time we run the Rust code. In production, we'd usually hard code the serialized puzzle and its hash, but this is a shortcut to make development easier.
87+
88+
Now we can start writing a test below:
89+
90+
```rust
91+
#[cfg(test)]
92+
mod tests {
93+
use super::*;
94+
95+
use anyhow::Result;
96+
97+
#[test]
98+
fn test_signature() -> Result<()> {
99+
// Test will go here
100+
101+
Ok(())
102+
}
103+
}
104+
```
105+
106+
Let's fill this in. Start by creating a `SpendContext` and `Simulator` so that we can create and spend coins in a simulated environment:
107+
108+
```rust
109+
let mut ctx = SpendContext::new();
110+
let mut sim = Simulator::new();
111+
```
112+
113+
Then we want to create a standard coin that we can spend in order to create our custom signature coin:
114+
115+
```rust
116+
let alice = sim.bls(1);
117+
let alice_p2 = StandardLayer::new(alice.pk);
118+
```
119+
120+
Then we'll curry the puzzle with the public key we want to lock the coin with and create a coin with its puzzle hash:
121+
122+
```rust
123+
let puzzle = ctx.curry(SignatureArgs {
124+
public_key: alice.pk,
125+
})?;
126+
127+
let puzzle_hash = ctx.tree_hash(puzzle).into();
128+
129+
alice_p2.spend(
130+
&mut ctx,
131+
alice.coin,
132+
Conditions::new().create_coin(puzzle_hash, 1, Memos::None),
133+
)?;
134+
135+
let coin = Coin::new(alice.coin.coin_id(), puzzle_hash, 1);
136+
```
137+
138+
Now that we have a coin with the signature puzzle, we can construct a solution and spend it. In this case, let's just send a coin back to Alice:
139+
140+
```rust
141+
let solution = ctx.alloc(&SignatureSolution {
142+
conditions: Conditions::new().create_coin(alice.puzzle_hash, 1, Memos::None),
143+
})?;
144+
145+
ctx.spend(coin, Spend::new(puzzle, solution))?;
146+
```
147+
148+
To complete the test, we should actually execute the transactions on the simulator:
149+
150+
```rust
151+
sim.spend_coins(ctx.take(), &[alice.sk])?;
152+
```
153+
154+
If everything worked, you should be able to run `cargo test` and see the test pass! Conveniently, the generation of signatures is handled by `spend_coins` automatically. In an actual application, you will most likely have to handle this yourself.
155+
156+
<details>
157+
<summary>Checkpoint (lib.rs)</summary>
158+
159+
Putting it all together, we get:
160+
161+
```rust title="lib.rs"
162+
use chia_wallet_sdk::prelude::*;
163+
164+
#[derive(Debug, Clone, ToClvm, FromClvm)]
165+
#[clvm(curry)]
166+
pub struct SignatureArgs {
167+
pub public_key: PublicKey,
168+
}
169+
170+
#[derive(Debug, Clone, ToClvm, FromClvm)]
171+
#[clvm(list)]
172+
pub struct SignatureSolution<T> {
173+
pub conditions: Conditions<T>,
174+
}
175+
176+
compile_rue!(SignatureArgs = SIGNATURE_MOD, ".");
177+
178+
#[cfg(test)]
179+
mod tests {
180+
use super::*;
181+
182+
use anyhow::Result;
183+
184+
#[test]
185+
fn test_signature() -> Result<()> {
186+
let mut ctx = SpendContext::new();
187+
let mut sim = Simulator::new();
188+
189+
let alice = sim.bls(1);
190+
let alice_p2 = StandardLayer::new(alice.pk);
191+
192+
let puzzle = ctx.curry(SignatureArgs {
193+
public_key: alice.pk,
194+
})?;
195+
196+
let puzzle_hash = ctx.tree_hash(puzzle).into();
197+
198+
alice_p2.spend(
199+
&mut ctx,
200+
alice.coin,
201+
Conditions::new().create_coin(puzzle_hash, 1, Memos::None),
202+
)?;
203+
204+
let coin = Coin::new(alice.coin.coin_id(), puzzle_hash, 1);
205+
206+
let solution = ctx.alloc(&SignatureSolution {
207+
conditions: Conditions::new().create_coin(alice.puzzle_hash, 1, Memos::None),
208+
})?;
209+
210+
ctx.spend(coin, Spend::new(puzzle, solution))?;
211+
212+
sim.spend_coins(ctx.take(), &[alice.sk])?;
213+
214+
Ok(())
215+
}
216+
}
217+
```
218+
219+
</details>
220+
221+
## Delegated Puzzles
222+
223+
The previous example is perfectly secure, and is a variation of a standard Chialisp puzzle known as [p2_delegated_conditions](https://github.com/Chia-Network/chia_puzzles/blob/main/puzzles/p2_delegated_conditions.clsp). The term "delegated" means that you are delegating the output of the transaction to something else. In this case, a list of conditions.
224+
225+
In most cases this is sufficient. However, sometimes you want to sign an arbitrary puzzle (which could still just be a list of conditions if desired) which can be spent in multiple ways, to give more flexibility to whoever submits the final spend bundle. You can use a delegated puzzle to do this.
226+
227+
So, we can modify the puzzle in `main.rue` to use a delegated puzzle instead:
228+
229+
```rue
230+
fn main(
231+
public_key: PublicKey,
232+
delegated_puzzle: fn(...solution: Any) -> List<Condition>,
233+
delegated_solution: Any,
234+
) -> List<Condition> {
235+
let agg_sig = AggSigMe {
236+
public_key,
237+
message: tree_hash(delegated_puzzle),
238+
};
239+
240+
let conditions = delegated_puzzle(...delegated_solution);
241+
242+
[agg_sig, ...conditions]
243+
}
244+
```
245+
246+
You're essentially passing both a delegated puzzle and its solution into the solution of the main puzzle. The output of the delegated puzzle is the conditions, like before, but the tree hash of the delegated puzzle itself is what is being signed.
247+
248+
Next, let's update the Rust code to match. First, update the solution struct:
249+
250+
```rust
251+
#[derive(Debug, Clone, ToClvm, FromClvm)]
252+
#[clvm(list)]
253+
pub struct SignatureSolution<P, S> {
254+
pub delegated_puzzle: P,
255+
pub delegated_solution: S,
256+
}
257+
```
258+
259+
And you'll need to construct a delegated spend and pass it into the solution instead of the conditions themselves:
260+
261+
```rust
262+
let delegated_spend =
263+
ctx.delegated_spend(Conditions::new().create_coin(alice.puzzle_hash, 1, Memos::None))?;
264+
265+
let solution = ctx.alloc(&SignatureSolution {
266+
delegated_puzzle: delegated_spend.puzzle,
267+
delegated_solution: delegated_spend.solution,
268+
})?;
269+
```
270+
271+
If you run `cargo test`, the test should still pass. Now we have a variation of the standard Chialisp [p2_delegated_puzzle](https://github.com/Chia-Network/chia_puzzles/blob/55896b5c40de85e557871d526d56d366c6534dac/puzzles/p2_delegated_puzzle.clsp).
272+
273+
<details>
274+
<summary>Checkpoint (lib.rs)</summary>
275+
276+
Putting it all together, we get:
277+
278+
```rust title="lib.rs"
279+
use chia_wallet_sdk::prelude::*;
280+
281+
#[derive(Debug, Clone, ToClvm, FromClvm)]
282+
#[clvm(curry)]
283+
pub struct SignatureArgs {
284+
pub public_key: PublicKey,
285+
}
286+
287+
#[derive(Debug, Clone, ToClvm, FromClvm)]
288+
#[clvm(list)]
289+
pub struct SignatureSolution<P, S> {
290+
pub delegated_puzzle: P,
291+
pub delegated_solution: S,
292+
}
293+
294+
compile_rue!(SignatureArgs = SIGNATURE_MOD, ".");
295+
296+
#[cfg(test)]
297+
mod tests {
298+
use super::*;
299+
300+
use anyhow::Result;
301+
302+
#[test]
303+
fn test_signature() -> Result<()> {
304+
let mut ctx = SpendContext::new();
305+
let mut sim = Simulator::new();
306+
307+
let alice = sim.bls(1);
308+
let alice_p2 = StandardLayer::new(alice.pk);
309+
310+
let puzzle = ctx.curry(SignatureArgs {
311+
public_key: alice.pk,
312+
})?;
313+
314+
let puzzle_hash = ctx.tree_hash(puzzle).into();
315+
316+
alice_p2.spend(
317+
&mut ctx,
318+
alice.coin,
319+
Conditions::new().create_coin(puzzle_hash, 1, Memos::None),
320+
)?;
321+
322+
let coin = Coin::new(alice.coin.coin_id(), puzzle_hash, 1);
323+
324+
let delegated_spend =
325+
ctx.delegated_spend(Conditions::new().create_coin(alice.puzzle_hash, 1, Memos::None))?;
326+
327+
let solution = ctx.alloc(&SignatureSolution {
328+
delegated_puzzle: delegated_spend.puzzle,
329+
delegated_solution: delegated_spend.solution,
330+
})?;
331+
332+
ctx.spend(coin, Spend::new(puzzle, solution))?;
333+
334+
sim.spend_coins(ctx.take(), &[alice.sk])?;
335+
336+
Ok(())
337+
}
338+
}
339+
```
340+
341+
</details>

sidebars.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const sidebars: SidebarsConfig = {
66
{
77
type: "category",
88
label: "Tutorials",
9-
items: ["tutorials/password"],
9+
items: ["tutorials/password", "tutorials/signature"],
1010
},
1111
"functions",
1212
"control-flow",

0 commit comments

Comments
 (0)