Skip to content

Commit b93090b

Browse files
authored
Hash Time Lock Contracts (#168)
1 parent 1ad2343 commit b93090b

File tree

2 files changed

+388
-0
lines changed

2 files changed

+388
-0
lines changed

tuxedo-core/src/verifier.rs

+2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ use scale_info::TypeInfo;
1010
use serde::{Deserialize, Serialize};
1111
use sp_std::fmt::Debug;
1212

13+
mod htlc;
1314
mod multi_signature;
1415
mod simple_signature;
1516

17+
pub use htlc::{BlakeTwoHashLock, TimeLock};
1618
pub use multi_signature::ThresholdMultiSignature;
1719
pub use simple_signature::{Sr25519Signature, P2PKH};
1820

tuxedo-core/src/verifier/htlc.rs

+386
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
//! This module contains `Verifier` implementations related to Hash Time Lock Contracts.
2+
//! It contains a simple hash lock, a simple time lock, and a hash time lock.
3+
//!
4+
//! These could be used as the base of an atomic swap protocol with a similarly expressive
5+
//! utxo chain like Bitcoin. For atomic swaps with less expressive counter party chains,
6+
//! such as Monero, see the Farcaster protocol.
7+
8+
use super::Verifier;
9+
use parity_scale_codec::{Decode, Encode};
10+
use scale_info::TypeInfo;
11+
use serde::{Deserialize, Serialize};
12+
use sp_core::{
13+
sr25519::{Public, Signature},
14+
H256,
15+
};
16+
use sp_runtime::traits::{BlakeTwo256, Hash};
17+
use sp_std::vec::Vec;
18+
19+
/// Allows UTXOs to be spent after a certain block height has been reached.
20+
/// This is useful for locking up tokens as a future investment. Timelocking
21+
/// also form the basis of timeout paths in swapping protocols.
22+
///
23+
/// This verifier is unlike many others because it requires some environmental information,
24+
/// namely the current block number. So there is a decision to be made:
25+
/// * Allow the verifier to take come config and grab that info by calling a function given in the config.
26+
/// This is what we do with constraint checker.
27+
/// * Modify the `Verifier` trait to pass along the block number.
28+
///
29+
/// On the one hand the block number seems like a pretty fundamental and basic thing to add. On the other
30+
/// hand there could be many more things to pass. For example, the timestamp.
31+
/// However any more complex information would require coupling with Constraint Checkers and it is not
32+
/// easy to red state like in accounts.
33+
///
34+
/// Decision: I will add block number to the signature. And remain open to adding more blockchain-level
35+
/// fundamental things. Especially if they are available in bitcoin script.
36+
///
37+
/// Regarding the verifier constraint checker separation, perhaps the right line to be drawn is
38+
/// that verifiers are useful in a lot of places, but perhaps not expressive enough in others.
39+
/// When they are not expressive enough, just use `UpForGrabs` and rely on the constraint checker,
40+
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
41+
pub struct TimeLock {
42+
pub unlock_block_height: u32,
43+
}
44+
45+
impl Verifier for TimeLock {
46+
type Redeemer = ();
47+
fn verify(&self, _: &[u8], block_height: u32, _: &()) -> bool {
48+
block_height >= self.unlock_block_height
49+
}
50+
}
51+
52+
/// Allows UTXOs to be spent when a preimage to a recorded hash is provided.
53+
/// This could be used as a puzzle (although a partial preimage search would be better)
54+
/// or a means of sharing a password, or as part of a simple atomic swapping protocol.
55+
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
56+
pub struct BlakeTwoHashLock {
57+
pub hash_lock: H256,
58+
}
59+
60+
impl BlakeTwoHashLock {
61+
pub fn new_from_secret(secret: Vec<u8>) -> Self {
62+
Self {
63+
hash_lock: BlakeTwo256::hash(&secret),
64+
}
65+
}
66+
}
67+
68+
impl Verifier for BlakeTwoHashLock {
69+
type Redeemer = Vec<u8>;
70+
fn verify(&self, _: &[u8], _: u32, secret: &Self::Redeemer) -> bool {
71+
BlakeTwo256::hash(secret) == self.hash_lock
72+
}
73+
}
74+
75+
/// Allows a UTXO to be spent, and therefore acknowledged by an intended recipient by revealing
76+
/// a hash preimage. After an initial claim period elapses on chain, the UTXO can also be spent
77+
/// by the refunder. In practice, the refunder is often the same address initially funded the HTLC.
78+
///
79+
/// The receiver and refunder are specified as a simple public keys for simplicity. It would be
80+
/// interesting to use public key hash, or better yet, simply abstract this over some opaque
81+
/// inner verifier for maximum composability.
82+
///
83+
/// After the time refund path opens, the happy path remains open. This is for compatibility with
84+
/// bitcoin, but may not be desired in all cases.
85+
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
86+
pub struct HashTimeLockContract {
87+
/// The hash whose preimage must be revealed (along with the recipient's signature) to spend the UTXO.
88+
pub hash_lock: H256,
89+
/// The pubkey that is intended to receive and acknowledge receipt of the funds.
90+
pub recipient_pubkey: Public,
91+
/// The time (as a block height) when the refund path opens up.
92+
pub claim_period_end: u32,
93+
/// The address who can spend the coins without revealing the preimage after the claim period has ended.
94+
pub refunder_pubkey: Public,
95+
}
96+
97+
/// This is the redeemer information needed to spend a `HashTimeLockContract` verifier.
98+
///
99+
/// The `HashTimeLockContract` has two spend paths, and therefore this enum has two variants.
100+
/// The variant selects the spend path and contains the corresponding witness data.
101+
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
102+
pub enum HtlcSpendPath {
103+
/// The primary spend path is for the recipient to claim the UTXO by revealing the
104+
/// hash preimage as well as a signature.
105+
Claim {
106+
secret: Vec<u8>,
107+
signature: Signature,
108+
},
109+
/// The secondary spend path is for the original owner to refund their money to their private
110+
/// ownership. This path is not enabled until the enough time has elapsed. Once the time has
111+
/// elapsed, only the refunder's signature is required.
112+
Refund { signature: Signature },
113+
}
114+
115+
impl Verifier for HashTimeLockContract {
116+
type Redeemer = HtlcSpendPath;
117+
118+
fn verify(&self, simplified_tx: &[u8], block_height: u32, spend_path: &HtlcSpendPath) -> bool {
119+
match spend_path {
120+
HtlcSpendPath::Claim { secret, signature } => {
121+
// Claims are valid as long as the secret is correct and the receiver signature is correct.
122+
BlakeTwo256::hash(secret) == self.hash_lock
123+
&& sp_io::crypto::sr25519_verify(
124+
signature,
125+
simplified_tx,
126+
&self.recipient_pubkey,
127+
)
128+
}
129+
HtlcSpendPath::Refund { signature } => {
130+
// Check that the time has elapsed
131+
block_height >= self.claim_period_end
132+
133+
&&
134+
135+
// Check that the refunder has signed properly
136+
sp_io::crypto::sr25519_verify(
137+
signature,
138+
simplified_tx,
139+
&self.refunder_pubkey,
140+
)
141+
}
142+
}
143+
}
144+
}
145+
146+
#[cfg(test)]
147+
mod test {
148+
use super::*;
149+
use sp_core::{sr25519::Pair, Pair as _};
150+
151+
fn bad_sig() -> Signature {
152+
Signature::from_slice(
153+
b"bogus_signature_bogus_signature_bogus_signature_bogus_signature!".as_slice(),
154+
)
155+
.expect("Should be able to create a bogus signature.")
156+
}
157+
158+
#[test]
159+
fn time_lock_too_soon() {
160+
let time_lock = TimeLock {
161+
unlock_block_height: 100,
162+
};
163+
assert!(!time_lock.verify(&[], 10, &()));
164+
}
165+
166+
#[test]
167+
fn time_lock_exactly_on_time() {
168+
let time_lock = TimeLock {
169+
unlock_block_height: 100,
170+
};
171+
assert!(time_lock.verify(&[], 100, &()));
172+
}
173+
174+
#[test]
175+
fn time_lock_past_threshold() {
176+
let time_lock = TimeLock {
177+
unlock_block_height: 100,
178+
};
179+
assert!(time_lock.verify(&[], 200, &()));
180+
}
181+
182+
#[test]
183+
fn hash_lock_correct_secret() {
184+
let secret = "htlc ftw";
185+
186+
let hash_lock = BlakeTwoHashLock::new_from_secret(secret.encode());
187+
assert!(hash_lock.verify(&[], 0, &secret.encode()));
188+
}
189+
190+
#[test]
191+
fn hash_lock_wrong_secret() {
192+
let secret = "htlc ftw";
193+
let incorrect = "there is no second best";
194+
195+
let hash_lock = BlakeTwoHashLock::new_from_secret(secret.encode());
196+
assert!(!hash_lock.verify(&[], 0, &incorrect.encode()));
197+
}
198+
199+
#[test]
200+
fn htlc_claim_success() {
201+
const THRESHOLD: u32 = 100;
202+
let secret = "htlc ftw".encode();
203+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
204+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
205+
206+
let htlc = HashTimeLockContract {
207+
hash_lock: BlakeTwo256::hash(&secret),
208+
recipient_pubkey: recipient_pair.public(),
209+
claim_period_end: THRESHOLD,
210+
refunder_pubkey: refunder_pair.public(),
211+
};
212+
213+
let simplified_tx = b"hello world".as_slice();
214+
let recipient_sig = recipient_pair.sign(simplified_tx);
215+
let redeemer = HtlcSpendPath::Claim {
216+
secret,
217+
signature: recipient_sig,
218+
};
219+
220+
assert!(htlc.verify(&simplified_tx, 0, &redeemer));
221+
}
222+
223+
#[test]
224+
fn htlc_claim_wrong_secret() {
225+
const THRESHOLD: u32 = 100;
226+
let secret = "htlc ftw".encode();
227+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
228+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
229+
230+
let htlc = HashTimeLockContract {
231+
hash_lock: BlakeTwo256::hash(&secret),
232+
recipient_pubkey: recipient_pair.public(),
233+
claim_period_end: THRESHOLD,
234+
refunder_pubkey: refunder_pair.public(),
235+
};
236+
237+
let incorrect_secret = "there is no second best".encode();
238+
239+
let simplified_tx = b"hello world".as_slice();
240+
let recipient_sig = recipient_pair.sign(simplified_tx);
241+
let redeemer = HtlcSpendPath::Claim {
242+
secret: incorrect_secret,
243+
signature: recipient_sig,
244+
};
245+
246+
assert!(!htlc.verify(&simplified_tx, 0, &redeemer));
247+
}
248+
249+
#[test]
250+
fn htlc_claim_bogus_signature() {
251+
const THRESHOLD: u32 = 100;
252+
let secret = "htlc ftw".encode();
253+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
254+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
255+
256+
let htlc = HashTimeLockContract {
257+
hash_lock: BlakeTwo256::hash(&secret),
258+
recipient_pubkey: recipient_pair.public(),
259+
claim_period_end: THRESHOLD,
260+
refunder_pubkey: refunder_pair.public(),
261+
};
262+
263+
let simplified_tx = b"hello world".as_slice();
264+
let redeemer = HtlcSpendPath::Claim {
265+
secret,
266+
signature: bad_sig(),
267+
};
268+
269+
assert!(!htlc.verify(&simplified_tx, 0, &redeemer));
270+
}
271+
272+
#[test]
273+
fn htlc_claim_fails_when_signature_is_from_refunder() {
274+
const THRESHOLD: u32 = 100;
275+
let secret = "htlc ftw".encode();
276+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
277+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
278+
279+
let htlc = HashTimeLockContract {
280+
hash_lock: BlakeTwo256::hash(&secret),
281+
recipient_pubkey: recipient_pair.public(),
282+
claim_period_end: THRESHOLD,
283+
refunder_pubkey: refunder_pair.public(),
284+
};
285+
286+
let simplified_tx = b"hello world".as_slice();
287+
let refunder_sig = refunder_pair.sign(simplified_tx);
288+
let redeemer = HtlcSpendPath::Claim {
289+
secret,
290+
signature: refunder_sig,
291+
};
292+
293+
assert!(!htlc.verify(&simplified_tx, 0, &redeemer));
294+
}
295+
296+
#[test]
297+
fn htlc_refund_success() {
298+
const THRESHOLD: u32 = 100;
299+
let secret = "htlc ftw".encode();
300+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
301+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
302+
303+
let htlc = HashTimeLockContract {
304+
hash_lock: BlakeTwo256::hash(&secret),
305+
recipient_pubkey: recipient_pair.public(),
306+
claim_period_end: THRESHOLD,
307+
refunder_pubkey: refunder_pair.public(),
308+
};
309+
310+
let simplified_tx = b"hello world".as_slice();
311+
let refunder_sig = refunder_pair.sign(simplified_tx);
312+
let redeemer = HtlcSpendPath::Refund {
313+
signature: refunder_sig,
314+
};
315+
316+
assert!(htlc.verify(&simplified_tx, 2 * THRESHOLD, &redeemer));
317+
}
318+
319+
#[test]
320+
fn htlc_refund_too_early() {
321+
const THRESHOLD: u32 = 100;
322+
let secret = "htlc ftw".encode();
323+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
324+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
325+
326+
let htlc = HashTimeLockContract {
327+
hash_lock: BlakeTwo256::hash(&secret),
328+
recipient_pubkey: recipient_pair.public(),
329+
claim_period_end: THRESHOLD,
330+
refunder_pubkey: refunder_pair.public(),
331+
};
332+
333+
let simplified_tx = b"hello world".as_slice();
334+
let refunder_sig = refunder_pair.sign(simplified_tx);
335+
let redeemer = HtlcSpendPath::Refund {
336+
signature: refunder_sig,
337+
};
338+
339+
assert!(!htlc.verify(&simplified_tx, 0, &redeemer));
340+
}
341+
342+
#[test]
343+
fn htlc_refund_bogus_sig() {
344+
const THRESHOLD: u32 = 100;
345+
let secret = "htlc ftw".encode();
346+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
347+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
348+
349+
let htlc = HashTimeLockContract {
350+
hash_lock: BlakeTwo256::hash(&secret),
351+
recipient_pubkey: recipient_pair.public(),
352+
claim_period_end: THRESHOLD,
353+
refunder_pubkey: refunder_pair.public(),
354+
};
355+
356+
let simplified_tx = b"hello world".as_slice();
357+
let redeemer = HtlcSpendPath::Refund {
358+
signature: bad_sig(),
359+
};
360+
361+
assert!(!htlc.verify(&simplified_tx, 2 * THRESHOLD, &redeemer));
362+
}
363+
364+
#[test]
365+
fn htlc_refund_fails_when_signature_is_from_recipient() {
366+
const THRESHOLD: u32 = 100;
367+
let secret = "htlc ftw".encode();
368+
let recipient_pair = Pair::from_seed(&[0u8; 32]);
369+
let refunder_pair = Pair::from_seed(&[1u8; 32]);
370+
371+
let htlc = HashTimeLockContract {
372+
hash_lock: BlakeTwo256::hash(&secret),
373+
recipient_pubkey: recipient_pair.public(),
374+
claim_period_end: THRESHOLD,
375+
refunder_pubkey: refunder_pair.public(),
376+
};
377+
378+
let simplified_tx = b"hello world".as_slice();
379+
let recipient_sig = recipient_pair.sign(simplified_tx);
380+
let redeemer = HtlcSpendPath::Refund {
381+
signature: recipient_sig,
382+
};
383+
384+
assert!(!htlc.verify(&simplified_tx, 2 * THRESHOLD, &redeemer));
385+
}
386+
}

0 commit comments

Comments
 (0)