├── Cargo.toml ├── README.md ├── src ├── lib.rs └── types │ ├── event.rs │ ├── graph.rs │ └── mod.rs ├── web_screenshot.png └── www ├── bootstrap.js ├── index.html ├── index.js ├── package.json └── webpack.config.js /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-hashgraph" 3 | version = "0.1.0" 4 | authors = ["Jay Butera "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "0.2" 12 | rust-crypto-wasm = "0.3" 13 | serde = { version = "1.0", features = ["derive"] } 14 | #libp2p = "0.4" 15 | serde_json = "1.0" 16 | #futures = "0.1" 17 | #names = "0.11" 18 | #tokio = "0.1" 19 | #rand = "0.6" 20 | #substrate-network-libp2p = { git = "https://github.com/paritytech/substrate.git"} 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An implementation of the SWIRLDS HashGraph Consensus Algorithm as specified in [the 2 | paper](https://www.swirlds.com/downloads/SWIRLDS-TR-2016-01.pdf). This is a 3 | library that provides all the functions needed to reach consensus. 4 | 5 | *This code uses iterators to traverse commitments, and although it is a pretty abstraction, it can be slow. I re-implemented the library using conventional graph search algorithms and matrix math [here](https://github.com/jaybutera/fast-hashgraph). Performance is improved dramatically.* 6 | ## Interact in the browser 7 | ![Force directed hashgraph](https://github.com/jaybutera/rust-hashgraph/blob/master/web_screenshot.png) 8 | 9 | 10 | The library compiles to [WebAssembly](https://webassembly.org/) and can be 11 | invoked like a normal ES6 module. The ```www``` directory contains a javascript 12 | webapp to interactively build and visualize a hashgraph as a force directed 13 | graph in [D3.js](https://d3js.org/). 14 | 15 | ## Tests 16 | *Right now tests aren't work as the interface has changed to support wasm* 17 | 18 | Run the tests with ```cargo test```. The unit tests in the graph module are the necessary steps to 19 | order all events topologically according to the HashGraph algorithm. 20 | 21 | ![Tests snapshot](https://i.imgur.com/eilv4Vk.png) 22 | 23 | --- 24 | The main.rs file contains the project binary. This is effectively an event loop that will 25 | 1. Connect and discover peers over the mDNS protocol 26 | 2. Upon receiving an event from a peer, update the in-memory graph, and pass the event onto another random peer 27 | 3. Host an HTTP service that a user can upload new transactions to, which will be encoded into an event and passed to a 28 | random peer 29 | 30 | ## TODO 31 | - Nodes send and receive events on gossip network with rust-libp2p 32 | - Graph traversal optimizations 33 | - Right now cloning strings is abundant to support the js/wasm cross-boundary. 34 | There should be workarounds for this, or at least a macro system to generate 35 | an optimized version for native. 36 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod types; 2 | -------------------------------------------------------------------------------- /src/types/event.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize,Deserialize}; 2 | use crypto::sha3::Sha3; 3 | use crypto::digest::Digest; 4 | use super::Transaction; 5 | 6 | #[derive(Serialize, Deserialize, Clone)] // Clone is temporary for graph unit tests 7 | pub enum Event { 8 | Update { 9 | creator: String, // TODO: Change to a signature 10 | self_parent: String, 11 | other_parent: Option, 12 | txs: Vec, 13 | }, 14 | Genesis{creator: String}, 15 | } 16 | 17 | impl Event { 18 | pub fn hash(&self) -> String { 19 | let mut hasher = Sha3::sha3_256(); 20 | let serialized = serde_json::to_string(self).unwrap(); 21 | hasher.input_str(&serialized[..]); 22 | hasher.result_str() 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/types/graph.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use crypto::sha3::Sha3; 3 | use crypto::digest::Digest; 4 | use std::collections::{HashMap,HashSet}; 5 | use wasm_bindgen::prelude::*; 6 | 7 | use super::event::Event::{self,*}; 8 | use super::{Transaction, RoundNum}; 9 | 10 | #[wasm_bindgen] 11 | #[derive(Serialize)] 12 | pub struct Graph { 13 | events: HashMap, 14 | #[serde(skip)] 15 | peer_id: String, 16 | #[serde(skip)] 17 | creators: HashSet, 18 | #[serde(skip)] 19 | round_index: Vec>, 20 | #[serde(skip)] 21 | is_famous: HashMap, // Some(false) means unfamous witness 22 | #[serde(skip)] 23 | latest_event: HashMap, // Creator id to event hash 24 | #[serde(skip)] 25 | round_of: HashMap, // Just testing a caching system for now 26 | } 27 | 28 | #[wasm_bindgen] 29 | impl Graph { 30 | pub fn new(peer_id: String) -> Self { 31 | let genesis = Event::Genesis { creator: peer_id.clone() }; 32 | 33 | let mut g = Graph { 34 | events: HashMap::new(), 35 | peer_id: peer_id, 36 | round_index: vec![HashSet::new()], 37 | creators: HashSet::new(), 38 | is_famous: HashMap::new(), 39 | latest_event: HashMap::new(), // Gets reset in add_event 40 | round_of: HashMap::new(), 41 | }; 42 | 43 | g.add_event(genesis); 44 | g 45 | } 46 | 47 | pub fn add( 48 | &mut self, 49 | other_parent: Option, // Events can be reactionary or independent of an "other" 50 | creator: String, 51 | js_txs: Box<[JsValue]>) -> Option 52 | { 53 | //let txs: Vec = Vec::from(js_txs); 54 | // TODO: This is complicated so ignoring txs for now 55 | let txs = vec![]; 56 | let creator_head_event = match self.latest_event.get(&creator) { 57 | Some(h) => h, 58 | None => { return None }, 59 | }; 60 | 61 | let event = Event::Update { 62 | creator: creator, // TODO: This is specifiable in the wasm api for now because the Event type can't be passed over JS 63 | self_parent: creator_head_event.clone(), 64 | other_parent: other_parent, 65 | txs: txs, 66 | }; 67 | 68 | let hash = event.hash(); 69 | if self.add_event(event).is_ok() { Some(hash) } 70 | else { None } 71 | } 72 | 73 | pub fn add_creator(&mut self, _creator: String) -> Option { 74 | let event = Genesis { creator: _creator }; 75 | let hash = event.hash(); 76 | 77 | if self.add_event(event).is_ok() { Some(hash) } 78 | else { None } 79 | } 80 | 81 | pub fn get_graph(&self) -> JsValue { 82 | let self_str = serde_json::to_string(self).expect("Graph should always be serializable"); 83 | wasm_bindgen::JsValue::from(self_str) 84 | } 85 | 86 | pub fn get_event(&self, hash: String) -> Option { 87 | self.events.get(&hash) 88 | .map(|event| serde_json::to_string(event).expect("Event should be serializable")) 89 | } 90 | } 91 | 92 | impl Graph { 93 | pub fn create_event( 94 | &self, 95 | other_parent: Option, // Events can be reactionary or independent of an "other" 96 | txs: Vec) -> Event 97 | { 98 | let latest_ev = self.latest_event.get( &self.peer_id ).expect("Peer id should always have an entry in latest_event"); 99 | Event::Update { 100 | creator: self.peer_id.clone(), 101 | self_parent: self.events.get( latest_ev ).expect("Should always have a self parent from genesis").hash(), 102 | other_parent: other_parent, 103 | txs: txs, 104 | } 105 | } 106 | 107 | pub fn add_event(&mut self, event: Event) -> Result<(),()> { 108 | // Only accept an event with a valid self_parent and other_parent 109 | // TODO: Make this prettier 110 | if let Update{ ref self_parent, ref other_parent, .. } = event { 111 | if self.events.get(self_parent).is_none() { 112 | return Err(()) 113 | } 114 | if let Some(ref op) = other_parent { 115 | if self.events.get(op).is_none() { 116 | return Err(()) 117 | } 118 | } 119 | } 120 | 121 | let event_hash = event.hash(); 122 | self.events.insert(event_hash.clone(), event); 123 | 124 | { // Add event to creators list if it isn't already there and update creator's latest event 125 | let event = self.events.get(&event_hash).unwrap(); 126 | match event { 127 | Genesis{ creator, .. } => { 128 | self.latest_event.insert(creator.clone(), event_hash.clone()); 129 | self.creators.insert(creator.clone()) 130 | }, 131 | Update { creator, .. } => { 132 | self.latest_event.insert(creator.clone(), event_hash.clone()); 133 | self.creators.insert(creator.clone()) 134 | }, 135 | }; 136 | } 137 | 138 | //-- Set event's round 139 | let last_idx = self.round_index.len()-1; 140 | let r = self.determine_round(&event_hash); 141 | // Cache result 142 | self.round_of.insert(event_hash.clone(), r); 143 | 144 | if r > last_idx { 145 | // Create a new round 146 | let mut hs = HashSet::new(); 147 | hs.insert(event_hash.clone()); 148 | self.round_index.push(hs); 149 | } 150 | else { 151 | // Otherwise push onto current round 152 | self.round_index[last_idx].insert(event_hash.clone()); 153 | } 154 | //-- 155 | 156 | // Set witness status 157 | if self.determine_witness(event_hash.clone()) { 158 | self.is_famous.insert(event_hash, false); 159 | } 160 | 161 | Ok(()) 162 | } 163 | 164 | pub fn iter(&self, event_hash: &String) -> EventIter { 165 | let event = self.events.get(event_hash).unwrap(); 166 | let mut e = EventIter { node_list: vec![], events: &self.events }; 167 | 168 | match *event { 169 | Update { ref self_parent, .. } => e.push_self_parents(event_hash), 170 | _ => (), 171 | } 172 | 173 | e 174 | } 175 | 176 | /// Determine the round an event belongs to, which is the max of its parents' rounds +1 if it 177 | /// is a witness. 178 | fn determine_round(&self, event_hash: &String) -> RoundNum { 179 | let event = self.events.get(event_hash).unwrap(); 180 | match event { 181 | Event::Genesis{ .. } => 0, 182 | Event::Update{ self_parent, other_parent, .. } => { 183 | // Check if it is cached 184 | if let Some(r) = self.round_of.get(event_hash) { 185 | return *r 186 | } else { 187 | let r = match other_parent { 188 | Some(op) => std::cmp::max( 189 | self.determine_round(self_parent), 190 | self.determine_round(op), 191 | ), 192 | None => self.determine_round(self_parent), 193 | }; 194 | 195 | // Get events from round r 196 | let round = self.round_index[r].iter() 197 | .filter(|eh| *eh != event_hash) 198 | .map(|e_hash| self.events.get(e_hash).unwrap()) 199 | .collect::>(); 200 | 201 | // Find out how many witnesses by unique members the event can strongly see 202 | let witnesses_strongly_seen = round.iter() 203 | .filter(|e| if let Some(_) = self.is_famous.get(&e.hash()) { true } else { false }) 204 | .fold(HashSet::new(), |mut set, e| { 205 | if self.strongly_see(event_hash.clone(), e.hash()) { 206 | let creator = match *e { 207 | Update{ ref creator, .. } => creator, 208 | Genesis{ ref creator } => creator, 209 | }; 210 | set.insert(creator.clone()); 211 | } 212 | set 213 | }); 214 | 215 | // n is number of members in hashgraph 216 | let n = self.creators.len(); 217 | 218 | if witnesses_strongly_seen.len() > (2*n/3) { r+1 } else { r } 219 | } 220 | }, 221 | } 222 | } 223 | } 224 | 225 | #[wasm_bindgen] 226 | impl Graph { 227 | pub fn round_of(&self, event_hash: String) -> RoundNum { 228 | match self.round_of.get(&event_hash) { 229 | Some(r) => *r, 230 | None => self.round_index.iter().enumerate() 231 | .find(|(_,round)| round.contains(&event_hash)) 232 | .expect("Failed to find a round for event").0 233 | } 234 | } 235 | 236 | /// Determines if the event is a witness 237 | pub fn determine_witness(&self, event_hash: String) -> bool { 238 | match self.events.get(&event_hash).unwrap() { 239 | Genesis{ .. } => true, 240 | Update{ self_parent, .. } => 241 | self.round_of(event_hash) > self.round_of(self_parent.clone()) 242 | } 243 | } 244 | 245 | /// Determine if the event is famous. 246 | /// An event is famous if 2/3 future witnesses strongly see it. 247 | fn is_famous(&self, event_hash: String) -> bool { 248 | let event = self.events.get(&event_hash).unwrap(); 249 | 250 | match event { 251 | Genesis{ .. } => true, 252 | Update{ .. } => { 253 | // Event must be a witness 254 | if !self.determine_witness(event_hash.clone()) { 255 | return false 256 | } 257 | 258 | let witnesses = self.events.values() 259 | .filter(|e| if let Some(_) = self.is_famous.get(&e.hash()) { true } else { false }) 260 | .fold(HashSet::new(), |mut set, e| { 261 | if self.strongly_see(e.hash(), event_hash.clone()) { 262 | let creator = match *e { 263 | Update{ ref creator, .. } => creator, 264 | Genesis{ ref creator } => creator, 265 | }; 266 | set.insert(creator.clone()); 267 | } 268 | set 269 | }); 270 | 271 | let n = self.creators.len(); 272 | witnesses.len() > (2*n/3) 273 | }, 274 | } 275 | } 276 | 277 | fn ancestor(&self, x_hash: String, y_hash: String) -> bool { 278 | let x = self.events.get(&x_hash).unwrap(); 279 | let y = self.events.get(&y_hash).unwrap(); 280 | 281 | match self.iter(&x_hash).find(|e| e.hash() == y_hash) { 282 | Some(_) => true, 283 | None => false, 284 | } 285 | } 286 | 287 | fn strongly_see(&self, x_hash: String, y_hash: String) -> bool { 288 | let mut creators_seen = self.iter(&x_hash) 289 | .filter(|e| self.ancestor(e.hash(), y_hash.clone())) 290 | .fold(HashSet::new(), |mut set, event| { 291 | let creator = match *event { 292 | Update{ ref creator, .. } => creator, 293 | Genesis{ ref creator } => creator, 294 | }; 295 | set.insert(creator.clone()); 296 | set 297 | }); 298 | 299 | // Add self to seen set incase it wasn't traversed above 300 | match self.events.get(&x_hash).unwrap() { 301 | Genesis{ .. } => true, 302 | Update{ .. } => creators_seen.insert(self.peer_id.clone()), 303 | }; 304 | 305 | let n = self.creators.len(); 306 | creators_seen.len() > (2*n/3) 307 | } 308 | } 309 | 310 | 311 | pub struct EventIter<'a> { 312 | node_list: Vec<&'a Event>, 313 | events: &'a HashMap, 314 | } 315 | 316 | impl<'a> EventIter<'a> { 317 | fn push_self_parents(&mut self, event_hash: &String) { 318 | let mut e = self.events.get(event_hash).unwrap(); 319 | 320 | loop { 321 | self.node_list.push(e); 322 | 323 | if let Update{ ref self_parent, .. } = *e { 324 | e = self.events.get(self_parent).unwrap(); 325 | } 326 | else { break; } 327 | } 328 | } 329 | } 330 | 331 | impl<'a> Iterator for EventIter<'a> { 332 | type Item = &'a Event; 333 | 334 | fn next(&mut self) -> Option { 335 | let event = match self.node_list.pop() { 336 | None => return None, 337 | Some(e) => e, 338 | }; 339 | 340 | if let Update{ ref other_parent, .. } = *event { 341 | if let Some(ref e) = *other_parent { 342 | self.push_self_parents(e); 343 | } 344 | } 345 | Some(event) 346 | } 347 | } 348 | 349 | #[cfg(test)] 350 | mod tests { 351 | use std::collections::HashMap; 352 | use super::*; 353 | 354 | fn generate() -> ((Graph,Graph,Graph), [String;10]) { 355 | /* Generates the following graph for each member (c1,c2,c3) 356 | * 357 | | o__| -- e7 358 | |__|__o -- e6 359 | o__| | -- e5 360 | | o__| -- e4 361 | | |__o -- e3 362 | |__o | -- e2 363 | o__| | -- e1 364 | o o o -- (g1,g2,g3) 365 | */ 366 | 367 | let c1 = "a".to_string(); 368 | let c2 = "b".to_string(); 369 | let c3 = "c".to_string(); 370 | 371 | let mut peer1 = Graph::new(c1); 372 | let mut peer2 = Graph::new(c2); 373 | let mut peer3 = Graph::new(c3); 374 | 375 | let p1_g: &String = peer1.latest_event.get(&peer1.peer_id).unwrap(); 376 | let p2_g: &String = peer2.latest_event.get(&peer2.peer_id).unwrap(); 377 | let p3_g: &String = peer3.latest_event.get(&peer3.peer_id).unwrap(); 378 | let g1_hash = peer1.events.get(p1_g).unwrap().hash(); 379 | let g2_hash = peer2.events.get(p2_g).unwrap().hash(); 380 | let g3_hash = peer3.events.get(p3_g).unwrap().hash(); 381 | 382 | // Share genesis events 383 | peer1.add_event(peer2.events.get(&g2_hash).unwrap().clone()); 384 | peer1.add_event(peer3.events.get(&g3_hash).unwrap().clone()); 385 | peer2.add_event(peer3.events.get(&g3_hash).unwrap().clone()); 386 | peer2.add_event(peer1.events.get(&g1_hash).unwrap().clone()); 387 | peer3.add_event(peer1.events.get(&g1_hash).unwrap().clone()); 388 | peer3.add_event(peer2.events.get(&g2_hash).unwrap().clone()); 389 | 390 | // Peer1 receives an update from peer2, and creates an event for it 391 | let e1 = peer1.create_event( 392 | Some(g2_hash.clone()), 393 | vec![]); 394 | let e1_hash = e1.hash(); 395 | peer1.add_event(e1.clone()); 396 | peer2.add_event(e1.clone()); 397 | peer3.add_event(e1.clone()); 398 | 399 | let e2 = peer2.create_event( 400 | Some(e1_hash.clone()), 401 | vec![]); 402 | let e2_hash = e2.hash(); 403 | peer1.add_event(e2.clone()); 404 | peer2.add_event(e2.clone()); 405 | peer3.add_event(e2.clone()); 406 | 407 | let e3 = peer3.create_event( 408 | Some(e2_hash.clone()), 409 | vec![]); 410 | let e3_hash = e3.hash(); 411 | peer1.add_event(e3.clone()); 412 | peer2.add_event(e3.clone()); 413 | peer3.add_event(e3.clone()); 414 | 415 | let e4 = peer2.create_event( 416 | Some(e3_hash.clone()), 417 | vec![]); 418 | let e4_hash = e4.hash(); 419 | peer1.add_event(e4.clone()); 420 | peer2.add_event(e4.clone()); 421 | peer3.add_event(e4.clone()); 422 | 423 | let e5 = peer1.create_event( 424 | Some(e4_hash.clone()), 425 | vec![]); 426 | let e5_hash = e5.hash(); 427 | peer1.add_event(e5.clone()); 428 | peer2.add_event(e5.clone()); 429 | peer3.add_event(e5.clone()); 430 | 431 | let e6 = peer3.create_event( 432 | Some(e5_hash.clone()), 433 | vec![]); 434 | let e6_hash = e6.hash(); 435 | peer1.add_event(e6.clone()); 436 | peer2.add_event(e6.clone()); 437 | peer3.add_event(e6.clone()); 438 | 439 | let e7 = peer2.create_event( 440 | Some(e6_hash.clone()), 441 | vec![]); 442 | let e7_hash = e7.hash(); 443 | peer1.add_event(e7.clone()); 444 | peer2.add_event(e7.clone()); 445 | peer3.add_event(e7.clone()); 446 | 447 | ((peer1, peer2, peer3), [g1_hash, g2_hash, g3_hash, e1_hash, e2_hash, e3_hash, e4_hash, e5_hash, e6_hash, e7_hash]) 448 | } 449 | 450 | #[test] 451 | fn test_ancestor() { 452 | let (graph, event_hashes) = generate(); 453 | 454 | assert_eq!( 455 | true, 456 | graph.0.ancestor( 457 | event_hashes[3].clone(), 458 | event_hashes[0].clone()) 459 | ) 460 | } 461 | 462 | #[test] 463 | fn test_strongly_see() { 464 | let (graph, event_hashes) = generate(); 465 | 466 | assert_eq!( 467 | false, 468 | graph.0.strongly_see( 469 | event_hashes[4].clone(), 470 | event_hashes[0].clone())); 471 | assert_eq!( 472 | true, 473 | graph.0.strongly_see( 474 | event_hashes[6].clone(), 475 | event_hashes[0].clone())); 476 | } 477 | 478 | #[test] 479 | fn test_determine_round() { 480 | let (graph, event_hashes) = generate(); 481 | 482 | assert_eq!( 483 | true, 484 | graph.0.round_index[0].contains(&event_hashes[5]) 485 | ); 486 | assert_eq!( 487 | true, 488 | graph.0.round_index[1].contains(&event_hashes[6]) 489 | ); 490 | } 491 | 492 | #[test] 493 | fn test_determine_witness() { 494 | let (graph, event_hashes) = generate(); 495 | 496 | assert_eq!( 497 | false, 498 | graph.0.determine_witness(event_hashes[5].clone())); 499 | assert_eq!( 500 | true, 501 | graph.0.determine_witness(event_hashes[6].clone())); 502 | assert_eq!( 503 | true, 504 | graph.0.determine_witness(event_hashes[7].clone())); 505 | } 506 | 507 | #[test] 508 | fn test_is_famous() { 509 | let (graph, event_hashes) = generate(); 510 | 511 | assert_eq!( 512 | true, 513 | graph.0.is_famous(event_hashes[0].clone())); 514 | assert_eq!( 515 | false, 516 | graph.0.is_famous(event_hashes[3].clone())); 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize,Deserialize}; 2 | 3 | pub mod graph; 4 | pub mod event; 5 | 6 | #[derive(Serialize, Deserialize, Clone)] // TODO: Does this need clone? 7 | pub struct Transaction; 8 | 9 | pub type RoundNum = usize; 10 | -------------------------------------------------------------------------------- /web_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaybutera/rust-hashgraph/5515c704cf326c13a46a943ea0f0cd0c84c34b97/web_screenshot.png -------------------------------------------------------------------------------- /www/bootstrap.js: -------------------------------------------------------------------------------- 1 | // A dependency graph that contains any wasm must all be imported 2 | // asynchronously. This `bootstrap.js` file does the single async import, so 3 | // that no one else needs to worry about it again. 4 | import("./index.js") 5 | .catch(e => console.error("Error importing `index.js`:", e)); 6 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lets see some hashgraphs! 6 | 7 | 8 | 9 | 10 | 11 |

From

12 |
13 |

By

14 |
15 | 16 | 17 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /www/index.js: -------------------------------------------------------------------------------- 1 | import * as wasm from "rust-hashgraph"; 2 | 3 | // Globals 4 | let 5 | nodes = [] 6 | , links = [] 7 | // Creator id -> event hash 8 | , latest_event = {} 9 | , latest_creator; 10 | 11 | /* 12 | document.getElementById('addEvent').addEventListener('click', () => { 13 | let other_parent = document.getElementById('inp_hash').value; 14 | let creator_id = document.getElementById('creator_id').value; 15 | 16 | if (!other_parent) other_parent = null; 17 | if (creator_id) 18 | add_event(other_parent, creator_id) 19 | }); 20 | */ 21 | 22 | document.getElementById('randomWalkEvent').addEventListener('click', () => { 23 | // Randomly select the self_parent 24 | const creators = Object.keys(latest_event); 25 | const rnd_idx = Math.floor( Math.random() * creators.length ); 26 | console.log('i: ' + rnd_idx); 27 | 28 | let other_parent = latest_event[latest_creator]; 29 | const creator_id = creators[rnd_idx]; 30 | console.log('creator id: ' + creator_id); 31 | console.log('op: ' + other_parent); 32 | //let self_parent = latest_event[ creators[rnd_idx] ]; 33 | 34 | // Normalize for rust api 35 | if (creator_id == latest_creator) 36 | other_parent = null; 37 | 38 | add_event(other_parent, creator_id); 39 | /* 40 | do { 41 | let rnd_idx = Math.random() * creators.length; 42 | self_parent = latest_event[ creators[rnd_idx] ]; 43 | } while (self_parent != other_parent); 44 | */ 45 | }); 46 | 47 | document.getElementById('addCreator').addEventListener('click', () => { 48 | let creator_id = document.getElementById('creator_id').value; 49 | if (creator_id) { new_creator(creator_id); } 50 | }); 51 | 52 | document.getElementById('createEvent').addEventListener('click', () => { 53 | const by_id = document.getElementById('by_creator').value; 54 | const from_id = document.getElementById('from_creator').value; 55 | 56 | let other_parent; 57 | if (!from_id) 58 | other_parent = null; 59 | else 60 | other_parent = latest_event[from_id]; 61 | 62 | if (by_id) 63 | add_event(other_parent, by_id) 64 | }); 65 | 66 | let drag = simulation => { 67 | function dragstarted(d) { 68 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 69 | d.fx = d.x; 70 | d.fy = d.y; 71 | } 72 | 73 | function dragged(d) { 74 | d.fx = d3.event.x; 75 | d.fy = d3.event.y; 76 | } 77 | 78 | function dragended(d) { 79 | if (!d3.event.active) simulation.alphaTarget(0); 80 | d.fx = null; 81 | d.fy = null; 82 | } 83 | 84 | return d3.drag() 85 | .on("start", dragstarted) 86 | .on("drag", dragged) 87 | .on("end", dragended); 88 | } 89 | 90 | const svg = d3.select("body") 91 | .append('svg') 92 | .attr('width', 800) 93 | .attr('height', 300); 94 | 95 | const width = svg.attr('width'), 96 | height = svg.attr('height'), 97 | color = d3.scaleOrdinal(d3.schemeCategory10); 98 | 99 | const sim = d3.forceSimulation() 100 | .nodes(nodes) 101 | .force('link', d3.forceLink(links).id(n => n.hash).distance(30)) 102 | .force('charge', d3.forceManyBody()) 103 | .force('center', d3.forceCenter(width/2, height/2)); 104 | 105 | let link = svg.append("g") 106 | .attr("stroke", "#999") 107 | .attr("stroke-opacity", 0.6) 108 | .selectAll("line") 109 | .data(links) 110 | .join("line") 111 | .attr("stroke-width", d => Math.sqrt(d.value)); 112 | 113 | var nodeGroup = svg.append('g') 114 | .classed('node', true); 115 | 116 | sim.on("tick", () => { 117 | link 118 | .attr("x1", d => d.source.x) 119 | .attr("y1", d => d.source.y) 120 | .attr("x2", d => d.target.x) 121 | .attr("y2", d => d.target.y); 122 | 123 | let n = nodeGroup.selectAll('g'); 124 | n.select('circle') 125 | .attr("cx", d => d.x) 126 | .attr("cy", d => d.y); 127 | n.select('text') 128 | .attr("x", d => d.x) 129 | .attr("y", d => d.y); 130 | }); 131 | 132 | function new_graph(creator_id) { 133 | let g = wasm.Graph.new(creator_id); 134 | 135 | const genesis_hash = Object.entries(JSON.parse(g.get_graph()).events)[0][0]; 136 | nodes.push( Object.create({ 137 | creator: creator_id, 138 | hash: genesis_hash 139 | })); 140 | 141 | restart(g); 142 | 143 | // Update latest event global record 144 | latest_event[creator_id] = genesis_hash; 145 | latest_creator = creator_id; 146 | 147 | //display_hash(genesis_hash, g); 148 | return g; 149 | } 150 | 151 | function new_creator(creator_id) { 152 | let h = g.add_creator(creator_id); 153 | 154 | if (h) { 155 | nodes.push( Object.create({ 156 | creator: creator_id, 157 | hash: h 158 | })); 159 | 160 | restart(g); 161 | 162 | // Update latest event global record 163 | latest_event[creator_id] = h; 164 | latest_creator = creator_id; 165 | 166 | //display_hash(h,g); 167 | } 168 | } 169 | 170 | function add_event(other_parent, creator_id) { 171 | let h = g.add(other_parent, creator_id, []); 172 | let self_parent = Object.values(JSON.parse( g.get_event(h) ))[0].self_parent; 173 | 174 | // Update d3 175 | if (h) { 176 | nodes.push( Object.create({ 177 | creator: creator_id, 178 | hash: h 179 | })); 180 | links.push({ 181 | source: self_parent, 182 | target: h 183 | }); 184 | if (other_parent) { 185 | links.push({ 186 | source: other_parent, 187 | target: h 188 | }); 189 | } 190 | 191 | restart(g); 192 | 193 | // Update latest event global record 194 | latest_event[creator_id] = h; 195 | latest_creator = creator_id; 196 | 197 | //display_hash(h,g); 198 | } 199 | 200 | 201 | console.log("NODES"); 202 | console.log(nodes); 203 | console.log("LINKS"); 204 | console.log(links); 205 | 206 | return h; 207 | }; 208 | 209 | function display_hash(hash, graph) { 210 | let p = document.createElement('p'); 211 | p.innerText = hash + ": round " + graph.round_of(hash); 212 | document.getElementById('hashes') 213 | .appendChild(p); 214 | } 215 | 216 | function restart(g) { 217 | // Add new circle/labels 218 | let n = nodeGroup.selectAll('g').data(nodes).enter().append('g');//.on('mouseover', console.log('hey')); 219 | n 220 | .append('circle') 221 | .attr('fill', d => { return color(d.creator) }) 222 | .attr('r', 8).call(drag(sim)); 223 | n 224 | .on('mouseover', console.log('hey')) 225 | .append('text') 226 | .text(d => 'round: ' + g.round_of(d.hash) ); 227 | 228 | // Apply the general update pattern to the links. 229 | link = link.data(links, function(d) { return d.source.hash + "-" + d.target.id; }); 230 | link.exit().remove(); 231 | link = link.enter().append("line").merge(link); 232 | 233 | // Update and restart the simulation. 234 | sim.nodes(nodes); 235 | sim.force("link").links(links); 236 | sim.alpha(1).restart(); 237 | } 238 | 239 | // Start a graph 240 | let creator = 'c1' 241 | var g = new_graph(creator); 242 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-wasm-app", 3 | "version": "0.1.0", 4 | "description": "create an app to consume rust-generated wasm packages", 5 | "main": "index.js", 6 | "bin": { 7 | "create-wasm-app": ".bin/create-wasm-app.js" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config webpack.config.js", 11 | "start": "webpack-dev-server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/rustwasm/create-wasm-app.git" 16 | }, 17 | "keywords": [ 18 | "webassembly", 19 | "wasm", 20 | "rust", 21 | "webpack" 22 | ], 23 | "author": "Ashley Williams ", 24 | "license": "(MIT OR Apache-2.0)", 25 | "bugs": { 26 | "url": "https://github.com/rustwasm/create-wasm-app/issues" 27 | }, 28 | "homepage": "https://github.com/rustwasm/create-wasm-app#readme", 29 | "devDependencies": { 30 | "rust-hashgraph": "file:../pkg", 31 | "webpack": "^4.29.3", 32 | "webpack-cli": "^3.1.0", 33 | "webpack-dev-server": "^3.1.5", 34 | "copy-webpack-plugin": "^5.0.0" 35 | }, 36 | "dependencies": {} 37 | } 38 | -------------------------------------------------------------------------------- /www/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: "./bootstrap.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "bootstrap.js", 9 | }, 10 | mode: "development", 11 | plugins: [ 12 | new CopyWebpackPlugin(['index.html']) 13 | ], 14 | }; 15 | --------------------------------------------------------------------------------