├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── tle ├── Cargo.toml ├── build.rs └── src │ ├── args.rs │ └── main.rs └── tlock ├── Cargo.toml ├── examples └── example1.rs └── src ├── client.rs ├── ibe.rs ├── lib.rs └── time.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | 17 | 18 | # Added by cargo 19 | # 20 | # already existing elements were commented out 21 | 22 | #/target 23 | /Cargo.lock 24 | 25 | .idea/ 26 | 27 | test.txt 28 | 29 | test_lock.pem 30 | 31 | *.pem 32 | 33 | test_unlock.txt 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "tlock", 5 | "tle" 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Timofey Luin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tlock-rs: Practical Timelock Encryption/Decryption in Rust 2 | 3 | This repo contains pure Rust implementation of [`drand/tlock`](https://github.com/drand/tlock) scheme. It provides time-based encryption and decryption capabilities by relying on a [drand](https://drand.love/) threshold network and identity-based encryption (IBE). The IBE scheme implemented here is [`Boneh-Franklin`](https://crypto.stanford.edu/~dabo/papers/bfibe.pdf). 4 | 5 | ## Usage 6 | The tlock system relies on an unchained drand network. Working endpoints to access it are, for now: 7 | - https://pl-us.testnet.drand.sh/ 8 | - https://testnet0-api.drand.cloudflare.com/ 9 | 10 | ### Lock file for given duration 11 | ```bash 12 | cargo run -- lock -o test_lock.pem -d 30s test.txt 13 | ``` 14 | 15 | ### Lock file for drand round 16 | ```bash 17 | cargo run -- lock -o test_lock.pem -r 1000 test.txt 18 | ``` 19 | 20 | ### Attempt unlocking file 21 | ```bash 22 | cargo run -- unlock -o test_unlock.txt test_lock.pem 23 | ``` 24 | 25 | Error `Too early` will appear, if one tries to unlock a file before the specified round is reached. 26 | 27 | ## Known issues 28 | - API currently supports 32 bytes of plaintext 29 | - Cross-library decryption hasn't been verified 30 | -------------------------------------------------------------------------------- /tle/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tle" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | rand = "0.8.5" 8 | anyhow = "1.0.56" 9 | eyre = "0.6.8" 10 | clap = "4.0.29" 11 | tracing = {version = "0.1" } 12 | cli-batteries = "0.4.1" 13 | futures = { version = "0.3.2" } 14 | tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread"] } 15 | async-std = { version = "1.10.0", features = ["tokio1"] } 16 | serde = {version = "1.0", features = ["derive"] } 17 | serde_json = "1" 18 | humantime = "2.1.0" 19 | 20 | tlock = {path = "../tlock"} 21 | 22 | [build-dependencies] 23 | cli-batteries = "0.4.1" 24 | -------------------------------------------------------------------------------- /tle/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | cli_batteries::build_rs().unwrap() 3 | } 4 | -------------------------------------------------------------------------------- /tle/src/args.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use clap::{Args, Parser}; 3 | // use clap_duration::duration_range_value_parse; 4 | 5 | #[derive(Clone, Parser)] 6 | pub struct Options { 7 | #[command(subcommand)] 8 | pub command: Option, 9 | } 10 | 11 | #[derive(Clone, clap::Subcommand)] 12 | pub enum Command { 13 | #[command(about = "Lock file until specified drand round")] 14 | Lock(LockArgs), 15 | 16 | #[command(about = "Try unlocking the file")] 17 | Unlock(UnlockArgs), 18 | } 19 | 20 | #[derive(Clone, Args)] 21 | pub struct LockArgs { 22 | #[clap(index = 1, help = "plaintext file path")] 23 | pub input_path: String, 24 | 25 | #[clap(short, long, default_value = "./locked.pem", help = "write the result to the file at path OUTPUT")] 26 | pub output_path: String, 27 | 28 | #[clap(short, long, help = "round number")] 29 | pub round_number: Option, 30 | 31 | #[clap(short, long, help = "lock file for duration (y/w/d/h/m/s/ms)")] 32 | pub duration: Option, 33 | 34 | #[clap(short, long, default_value = "https://pl-us.testnet.drand.sh", help = "drand network host url")] 35 | pub network_host: String, 36 | 37 | #[clap(short, long, default_value = "7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf", help = "drand chain hash")] 38 | pub chain_hash: String, 39 | } 40 | 41 | #[derive(Clone, Args)] 42 | pub struct UnlockArgs { 43 | #[clap(index = 1, help = "ciphertext file path")] 44 | pub input_path: String, 45 | 46 | #[clap(short, long, default_value = "./unlocked.pem", help = "write the result to the file at path OUTPUT")] 47 | pub output_path: String, 48 | 49 | #[clap(short, long, default_value = "https://pl-us.testnet.drand.sh", help = "drand network host url")] 50 | pub network_host: String, 51 | 52 | #[clap(short, long, default_value = "7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf", help = "drand chain hash")] 53 | pub chain_hash: String, 54 | } 55 | -------------------------------------------------------------------------------- /tle/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process; 2 | use anyhow::anyhow; 3 | use std::fs; 4 | use cli_batteries::version; 5 | use tracing::{info, info_span, Instrument}; 6 | use tlock::client::Network; 7 | use tlock::time; 8 | use crate::args::{Options, Command, LockArgs, UnlockArgs}; 9 | 10 | mod args; 11 | 12 | fn main() { 13 | cli_batteries::run(version!(), app); 14 | } 15 | 16 | async fn app(opts: Options) -> eyre::Result<()> { 17 | if let Some(command) = opts.command { 18 | match command { 19 | Command::Lock(args) => lock(args).await, 20 | Command::Unlock(args) => unlock(args).await, 21 | }.map_err(|e| eyre::anyhow!(e))? 22 | } 23 | 24 | Ok(()) 25 | } 26 | 27 | async fn lock(args: LockArgs) -> anyhow::Result<()> { 28 | let network = Network::new(args.network_host, args.chain_hash).unwrap(); 29 | let info = network.info().instrument(info_span!("getting network info")).await.unwrap(); 30 | 31 | let round_number = match args.round_number { 32 | None => { 33 | let d = args.duration.expect("duration is expected if round_number isn't specified").into(); 34 | time::round_after(&info, d) 35 | }, 36 | Some(n) => n, 37 | }; 38 | 39 | info!("locked until {round_number} round"); 40 | 41 | let src = fs::File::open(args.input_path).map_err(|e| anyhow!("error reading input file"))?; 42 | let dst = fs::File::create(args.output_path).map_err(|e| anyhow!("error creating output file"))?; 43 | 44 | tlock::encrypt(network, dst, src, round_number).await 45 | } 46 | 47 | 48 | async fn unlock(args: UnlockArgs) -> anyhow::Result<()> { 49 | let network = Network::new(args.network_host, args.chain_hash).unwrap(); 50 | 51 | let src = fs::File::open(args.input_path).map_err(|e| anyhow!("error reading input file"))?; 52 | let dst = fs::File::create(args.output_path).map_err(|e| anyhow!("error creating output file"))?; 53 | 54 | tlock::decrypt(network, dst, src).await 55 | } 56 | -------------------------------------------------------------------------------- /tlock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tlock" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1" 8 | url = "2.3.1" 9 | hex = { version = "0.4", features = ["serde"] } 10 | surf = "2.3.2" 11 | sha2 = "0.9" 12 | rand = "0.8.5" 13 | group = "0.12" 14 | async-std = { version = "1.10.0" } 15 | bls12_381_plus = { version = "0.7.0", features = ["nightly"] } 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1" 18 | tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread"] } 19 | unsigned-varint = { version = "0.7", features = [ 20 | "futures", 21 | "asynchronous_codec", 22 | ] } 23 | tracing = "0.1.36" 24 | itertools = "0.10.5" 25 | -------------------------------------------------------------------------------- /tlock/examples/example1.rs: -------------------------------------------------------------------------------- 1 | use tlock::client::Network; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | let client = Network::new("https://pl-us.testnet.drand.sh/", "7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf").unwrap(); 6 | let info = client.info().await.unwrap(); 7 | 8 | let msg = vec![8;32]; 9 | let ct = tlock::time_lock(info.public_key, 1000, &msg); 10 | 11 | let beacon = client.get(1000).await.unwrap(); 12 | 13 | let pt = tlock::time_unlock(beacon, &ct); 14 | 15 | assert_eq!(msg, pt); 16 | } 17 | -------------------------------------------------------------------------------- /tlock/src/client.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use anyhow::anyhow; 3 | use bls12_381_plus::G1Affine; 4 | use url::Url; 5 | use serde::{Serialize, Deserialize}; 6 | 7 | #[derive(Clone)] 8 | pub struct Network { 9 | client: surf::Client, 10 | chain_hash: String 11 | } 12 | 13 | #[derive(Clone, Debug, Deserialize)] 14 | struct ChainInfoResp { 15 | #[serde(with = "hex::serde")] 16 | pub public_key: Vec, 17 | pub hash: String, 18 | pub period: u64, 19 | pub genesis_time: u64, 20 | } 21 | 22 | #[derive(Clone, Debug, Serialize)] 23 | pub struct ChainInfo { 24 | pub public_key: G1Affine, 25 | pub hash: String, 26 | pub period: Duration, 27 | pub genesis_time: u64, 28 | } 29 | 30 | #[derive(Clone, Debug, Serialize, Deserialize)] 31 | pub struct Beacon { 32 | pub round: u64, 33 | #[serde(with = "hex::serde")] 34 | pub randomness: Vec, 35 | #[serde(with = "hex::serde")] 36 | pub signature: Vec, 37 | } 38 | 39 | impl Network { 40 | pub fn new>(host: S, chain_hash: impl AsRef) -> anyhow::Result { 41 | let url = { 42 | let base = Url::parse(host.as_ref()) 43 | .map_err(|e| anyhow!("error parsing network host: {e}"))?; 44 | base.join(&format!("{}/", chain_hash.as_ref())) 45 | .map_err(|e| anyhow!("error joining chain hash: {e}")) 46 | }?; 47 | let config = surf::Config::new().set_base_url(url).set_timeout(None); 48 | Ok(Self { 49 | client: config.try_into()?, 50 | chain_hash: chain_hash.as_ref().to_string() 51 | }) 52 | } 53 | 54 | pub async fn info(&self) -> anyhow::Result { 55 | let mut resp = self 56 | .client 57 | .get("info") 58 | .await 59 | .map_err(|e| anyhow!("error requesting info: {e}"))?; 60 | 61 | if resp.status() != 200 { 62 | return Err(anyhow!("{:?}", resp.body_string().await.unwrap())); 63 | } 64 | 65 | let res = resp 66 | .body_json::() 67 | .await 68 | .map_err(|e| anyhow!("error decoding info response: {e}"))?; 69 | 70 | let public_key = { 71 | let bytes = (&*res.public_key).try_into() 72 | .map_err(|_| anyhow!("invalid public key size"))?; 73 | G1Affine::from_compressed(bytes).unwrap() 74 | }; 75 | 76 | Ok(ChainInfo{ 77 | public_key, 78 | hash: res.hash, 79 | period: Duration::from_secs(res.period), 80 | genesis_time: res.genesis_time 81 | }) 82 | } 83 | 84 | pub async fn get(&self, round: u64) -> anyhow::Result { 85 | let uri = if round == 0 { 86 | "public/latest".to_string() 87 | } else { 88 | format!("public/{round}") 89 | }; 90 | 91 | let mut resp = self 92 | .client 93 | .get(uri) 94 | .await 95 | .map_err(|e| anyhow!("error requesting info: {e}"))?; 96 | 97 | if resp.status() != 200 { 98 | return Err(anyhow!("Too early")); 99 | } 100 | 101 | resp 102 | .body_json::() 103 | .await 104 | .map_err(|e| anyhow!("error decoding round response: {e}")) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tlock/src/ibe.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Mul; 2 | use bls12_381_plus::{ExpandMsgXmd, G1Affine, G1Projective, G2Affine, G2Projective, Gt, Scalar}; 3 | use rand::distributions::Uniform; 4 | use rand::{Rng, thread_rng}; 5 | use sha2::{Digest, Sha256}; 6 | use group::{Curve, GroupEncoding}; 7 | use itertools::Itertools; 8 | use serde::{Serialize, Deserialize}; 9 | 10 | #[derive(Clone, Debug, Serialize, Deserialize)] 11 | pub struct Ciphertext { 12 | pub u: G1Affine, 13 | pub v: Vec, 14 | pub w: Vec, 15 | } 16 | 17 | const BLOCK_SIZE: usize = 32; 18 | pub const H2C_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; 19 | 20 | pub fn encrypt, M: AsRef<[u8]>>(master: G1Affine, id: I, msg: M) -> Ciphertext { 21 | assert!(msg.as_ref().len() <= BLOCK_SIZE, "plaintext too long for the block size"); 22 | 23 | let mut rng = rand::thread_rng(); 24 | // 1. Compute Gid = e(master,Q_id) 25 | let gid = { 26 | let qid = G2Projective::hash::>(id.as_ref(), H2C_DST) 27 | .to_affine(); 28 | 29 | bls12_381_plus::pairing(&master, &qid) 30 | }; 31 | 32 | /// dirty fix: loop to sample randomness that won't mess up constant time operation. 33 | /// otherwise can `Scalar::from_bytes(r).unwrap()` panic from subtle crate 34 | let (sigma, r) = loop { 35 | // 2. Derive random sigma 36 | let sigma: [u8; BLOCK_SIZE] = (0..BLOCK_SIZE).map(|_| rng.sample(&Uniform::new(0u8, 8u8))).collect_vec().try_into().unwrap(); 37 | 38 | // 3. Derive r from sigma and msg 39 | let r = { 40 | let mut hash = Sha256::new(); 41 | hash.update(b"h3"); 42 | hash.update(&sigma[..]); 43 | hash.update(msg.as_ref()); 44 | let r = &hash.finalize().to_vec()[0..32].try_into().unwrap(); 45 | 46 | Scalar::from_bytes(r) 47 | }; 48 | 49 | if r.is_some().unwrap_u8() == 1u8 { 50 | break (sigma, r.unwrap()); 51 | } 52 | }; 53 | 54 | // 4. Compute U = G^r 55 | let g = G1Affine::generator(); 56 | let u = (G1Affine::generator().mul(r)).to_affine(); 57 | 58 | // 5. Compute V = sigma XOR H(rGid) 59 | let v = { 60 | let mut hash = sha2::Sha256::new(); 61 | let r_gid = gid.mul(r); 62 | hash.update(b"h2"); // dst 63 | hash.update(&r_gid.to_bytes()); 64 | let h_r_git = &hash.finalize().to_vec()[0..BLOCK_SIZE]; 65 | 66 | xor(&sigma, h_r_git) 67 | }; 68 | 69 | // 6. Compute W = M XOR H(sigma) 70 | let w = { 71 | let mut hash = sha2::Sha256::new(); 72 | hash.update(b"h4"); 73 | hash.update(&sigma[..]); 74 | let h_sigma = &hash.finalize().to_vec()[0..BLOCK_SIZE]; 75 | xor(msg.as_ref(), h_sigma) 76 | }; 77 | 78 | Ciphertext { 79 | u, 80 | v, 81 | w, 82 | } 83 | } 84 | 85 | pub fn decrypt(private: G2Affine, c: &Ciphertext) -> Vec { 86 | assert!(c.w.len() <= BLOCK_SIZE, "ciphertext too long for the block size"); 87 | 88 | // 1. Compute sigma = V XOR H2(e(rP,private)) 89 | let sigma = { 90 | let mut hash = sha2::Sha256::new(); 91 | let r_gid = bls12_381_plus::pairing(&c.u, &private); 92 | hash.update(b"h2"); 93 | hash.update(&r_gid.to_bytes()); 94 | let h_r_git = &hash.finalize().to_vec()[0..BLOCK_SIZE]; 95 | xor(h_r_git, &c.v) 96 | }; 97 | 98 | // 2. Compute Msg = W XOR H4(sigma) 99 | let msg = { 100 | let mut hash = sha2::Sha256::new(); 101 | hash.update(b"h4"); 102 | hash.update(&sigma); 103 | let h_sigma = &hash.finalize().to_vec()[0..BLOCK_SIZE]; 104 | xor(h_sigma, &c.w) 105 | }; 106 | 107 | // 3. Check U = G^r 108 | let r_g = { 109 | let mut hash = sha2::Sha256::new(); 110 | hash.update(b"h3"); 111 | hash.update(&sigma[..]); 112 | hash.update(&msg); 113 | let r = &hash.finalize().to_vec()[0..BLOCK_SIZE]; 114 | let r = Scalar::from_bytes(r.try_into().unwrap()).unwrap(); 115 | (G1Affine::generator() * r).to_affine() 116 | }; 117 | assert_eq!(c.u, r_g); 118 | 119 | msg 120 | } 121 | 122 | fn xor(a: &[u8], b: &[u8]) -> Vec { 123 | a.iter().zip(b.iter()).map(|(a, b)| a ^ b).collect() 124 | } 125 | -------------------------------------------------------------------------------- /tlock/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod ibe; 3 | pub mod time; 4 | 5 | use std::io; 6 | use anyhow::anyhow; 7 | use bls12_381_plus::{G1Affine, G2Affine}; 8 | use sha2::Digest; 9 | use tracing::info_span; 10 | use crate::ibe::Ciphertext; 11 | use crate::client::{Beacon, Network}; 12 | 13 | pub async fn encrypt(network: Network, mut dst: W, mut src: R, round_number: u64) -> anyhow::Result<()> { 14 | let info = network.info().await?; 15 | 16 | let mut message = [0; 32]; 17 | src.read(&mut message).map_err(|e| anyhow!("error reading {e}"))?; 18 | 19 | let ct = info_span!("ibe::encryption").in_scope(|| time_lock(info.public_key, round_number, message)); 20 | 21 | { 22 | let mut buffer = unsigned_varint::encode::u64_buffer(); 23 | dst.write_all(unsigned_varint::encode::u64(round_number, &mut buffer)).unwrap(); 24 | } 25 | 26 | dst.write_all(ct.u.to_compressed().as_ref()).unwrap(); 27 | dst.write_all(&ct.v).unwrap(); 28 | dst.write_all(&ct.w).unwrap(); 29 | 30 | Ok(()) 31 | } 32 | 33 | pub async fn decrypt(network: Network, mut dst: W, mut src: R) -> anyhow::Result<()> { 34 | let round = unsigned_varint::io::read_u64(&mut src).map_err(|e| anyhow!("error reading {e}"))?; 35 | 36 | let c = { 37 | let mut u = [0u8;48]; 38 | src.read_exact(&mut u).map_err(|e| anyhow!("error reading {e}"))?; 39 | let mut v = [0u8;32]; 40 | src.read_exact(&mut v).map_err(|e| anyhow!("error reading {e}"))?; 41 | let mut w = [0u8;32]; 42 | src.read_exact(&mut w).map_err(|e| anyhow!("error reading {e}"))?; 43 | 44 | Ciphertext{ 45 | u: G1Affine::from_compressed(&u).unwrap(), 46 | v: v.to_vec(), 47 | w: w.to_vec(), 48 | } 49 | }; 50 | 51 | let beacon = network.get(round).await?; 52 | let mut pt = time_unlock(beacon, &c); 53 | 54 | if let Some(i) = pt.iter().rposition(|x| *x != 0) { 55 | pt.truncate(i+1); 56 | } 57 | 58 | dst.write_all(&pt).map_err(|e| anyhow!("error write {e}")) 59 | } 60 | 61 | pub fn time_lock>(pub_key: G1Affine, round_number: u64, message: M) -> ibe::Ciphertext { 62 | let id = { 63 | let mut hash = sha2::Sha256::new(); 64 | hash.update(&round_number.to_be_bytes()); 65 | &hash.finalize().to_vec()[0..32] 66 | }; 67 | 68 | ibe::encrypt(pub_key, id, message) 69 | } 70 | 71 | pub fn time_unlock(beacon: Beacon, c: &Ciphertext) -> Vec { 72 | let private = G2Affine::from_compressed((&*beacon.signature).try_into().unwrap()).unwrap(); 73 | 74 | ibe::decrypt(private, c) 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use std::time::Duration; 80 | use crate::client::ChainInfo; 81 | use super::*; 82 | 83 | #[test] 84 | fn test_e2e() { 85 | let pk_bytes = hex::decode("8200fc249deb0148eb918d6e213980c5d01acd7fc251900d9260136da3b54836ce125172399ddc69c4e3e11429b62c11").unwrap(); 86 | let info = ChainInfo { 87 | public_key: G1Affine::from_compressed((&*pk_bytes).try_into().unwrap()).unwrap(), 88 | hash: "7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf".to_string(), 89 | period: Duration::from_secs(25), 90 | genesis_time: 0 91 | }; 92 | 93 | let msg = vec![8;32]; 94 | let ct = time_lock(info.public_key, 1000, msg.clone()); 95 | 96 | let beacon = Beacon { 97 | round: 1000, 98 | randomness: hex::decode("3467f5d3118af125fbe8ffa0272e9fd1df026702afd4da50d0a0c8b3ff2dbf21").unwrap(), 99 | signature: hex::decode("a4721e6c3eafcd823f138cd29c6c82e8c5149101d0bb4bafddbac1c2d1fe3738895e4e21dd4b8b41bf007046440220910bb1cdb91f50a84a0d7f33ff2e8577aa62ac64b35a291a728a9db5ac91e06d1312b48a376138d77b4d6ad27c24221afe").unwrap(), 100 | }; 101 | 102 | 103 | let pt = time_unlock(beacon, &ct); 104 | assert_eq!(pt, msg) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tlock/src/time.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Add; 2 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 3 | use crate::client::ChainInfo; 4 | 5 | pub fn round_at(chain_info: &ChainInfo, t: SystemTime) -> u64 { 6 | let since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); 7 | let t_unix = since_epoch.as_secs(); 8 | current_round(t_unix, chain_info.period, chain_info.genesis_time) 9 | } 10 | 11 | pub fn round_after(chain_info: &ChainInfo, d: Duration) -> u64 { 12 | let t = SystemTime::now().add(d); 13 | round_at(chain_info, t) 14 | } 15 | 16 | pub fn current_round(now: u64, period: Duration, genesis: u64) -> u64 { 17 | let (next_round, _) = next_round(now, period, genesis); 18 | 19 | if next_round <= 1 { 20 | next_round 21 | } else { 22 | next_round - 1 23 | } 24 | } 25 | 26 | pub fn next_round(now: u64, period: Duration, genesis: u64) -> (u64, u64) { 27 | if now < genesis { 28 | return (1, genesis) 29 | } 30 | 31 | let from_genesis = now - genesis; 32 | let next_round = (((from_genesis as f64)/ (period.as_secs() as f64)).floor() + 1f64) as u64; 33 | let next_time = genesis + next_round*period.as_secs(); 34 | 35 | (next_round, next_time) 36 | } 37 | 38 | pub fn dur_before(chain_info: &ChainInfo, round: u64) -> Duration { 39 | let t = SystemTime::now(); 40 | let since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); 41 | let t_unix = since_epoch.as_secs(); 42 | 43 | let current = current_round(t_unix, chain_info.period, chain_info.genesis_time); 44 | let rounds = (round - current) as u32; 45 | 46 | chain_info.period * rounds 47 | } 48 | --------------------------------------------------------------------------------