├── .gitignore ├── example.js ├── schema.proto ├── package.json ├── LICENSE ├── index.js ├── README.md └── messages.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.db 3 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const hyperclock = require('./') 2 | 3 | const clock = hyperclock('./clock.db') 4 | 5 | clock.createReadStream({live: true}).on('data', console.log) 6 | -------------------------------------------------------------------------------- /schema.proto: -------------------------------------------------------------------------------- 1 | message Header { 2 | required string type = 1; 3 | } 4 | 5 | message Entry { 6 | required uint64 time = 1; 7 | required bytes random = 2; 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperclock", 3 | "version": "1.0.0", 4 | "description": "Distributed, secure clock over a hypercore", 5 | "main": "index.js", 6 | "dependencies": { 7 | "hypercore": "^6.17.1", 8 | "is-options": "^1.0.1", 9 | "protocol-buffers-encodings": "^1.1.0" 10 | }, 11 | "devDependencies": { 12 | "protocol-buffers": "^4.0.4", 13 | "standard": "^11.0.1" 14 | }, 15 | "scripts": { 16 | "test": "standard", 17 | "protobuf": "protocol-buffers schema.proto -o messages.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/mafintosh/hyperclock.git" 22 | }, 23 | "author": "Mathias Buus (@mafintosh)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/mafintosh/hyperclock/issues" 27 | }, 28 | "homepage": "https://github.com/mafintosh/hyperclock" 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const crypto = require('hypercore-crypto') 2 | const hypercore = require('hypercore') 3 | const isOptions = require('is-options') 4 | const messages = require('./messages') 5 | 6 | const valueEncoding = {decode, encode} 7 | 8 | module.exports = clock 9 | 10 | function clock (storage, key, opts) { 11 | if (isOptions(key)) { 12 | opts = key 13 | key = null 14 | } 15 | 16 | if (!opts) opts = {} 17 | 18 | opts.valueEncoding = valueEncoding 19 | 20 | const feed = hypercore(storage, key, opts) 21 | const interval = opts.interval || 1000 22 | 23 | feed.ready(function (err) { 24 | if (err) return 25 | if (!feed.length) feed.append({type: 'hyperclock'}) 26 | 27 | const id = setInterval(tick, interval || 1000) 28 | feed.on('close', destroy) 29 | 30 | function destroy () { 31 | clearInterval(id) 32 | } 33 | 34 | function tick () { 35 | feed.append({ 36 | time: Date.now(), 37 | random: crypto.randomBytes(32) 38 | }) 39 | } 40 | }) 41 | 42 | return feed 43 | } 44 | 45 | function decode (msg) { 46 | try { 47 | return messages.Entry.decode(msg) 48 | } catch (err) { 49 | return messages.Header.decode(msg) 50 | } 51 | } 52 | 53 | function encode (msg) { 54 | if (msg.type) return messages.Header.encode(msg) 55 | return messages.Entry.encode(msg) 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperclock 2 | 3 | Distributed, secure clock over a [hypercore](https://github.com/mafintosh/hypercore) 4 | 5 | ``` 6 | npm install hyperclock 7 | ``` 8 | 9 | Useful if you need a trusted clock in a distributed system. 10 | 11 | ## Usage 12 | 13 | ``` js 14 | const hyperclock = require('hyperclock') 15 | 16 | const clock = hyperclock('./clock') 17 | 18 | clock.createReadStream({live: true}).on('data', console.log) 19 | ``` 20 | 21 | Running the above prints stuff similar to 22 | 23 | ``` 24 | { type: 'hyperclock' } 25 | { time: 1530285479803, 26 | random: 27 | } 28 | { time: 1530285480804, 29 | random: 30 | } 31 | { time: 1530285481806, 32 | random: 33 | } 34 | { time: 1530285482808, 35 | random: 36 | } 37 | ``` 38 | 39 | ## API 40 | 41 | #### `feed = hyperclock(storage, [key], [options])` 42 | 43 | Create a new clock feed. The returned value is a [hypercore](https://github.com/mafintosh/hypercore) that 44 | represents a clock you can replicate elsewhere using normal hypercore replication. 45 | 46 | The first entry is a header identifying this as a hyperclock and the subsequent ones look like this 47 | 48 | ```js 49 | { 50 | time: unixTime, 51 | random: <32 random bytes> 52 | } 53 | ``` 54 | 55 | You can use the random values to prove that you've seen a specific time in a distributed system 56 | assuming you trust this clock. 57 | 58 | `storage`, `key`, and `options` are all forwarded to the hypercore constructor. 59 | 60 | Per default the time is added every `1000ms`. If you want to change this set the `{interval: ms}` option. 61 | 62 | ## License 63 | 64 | MIT 65 | -------------------------------------------------------------------------------- /messages.js: -------------------------------------------------------------------------------- 1 | // This file is auto generated by the protocol-buffers cli tool 2 | 3 | /* eslint-disable quotes */ 4 | /* eslint-disable indent */ 5 | /* eslint-disable no-redeclare */ 6 | /* eslint-disable camelcase */ 7 | 8 | // Remember to `npm install --save protocol-buffers-encodings` 9 | var encodings = require('protocol-buffers-encodings') 10 | var varint = encodings.varint 11 | var skip = encodings.skip 12 | 13 | var Header = exports.Header = { 14 | buffer: true, 15 | encodingLength: null, 16 | encode: null, 17 | decode: null 18 | } 19 | 20 | var Entry = exports.Entry = { 21 | buffer: true, 22 | encodingLength: null, 23 | encode: null, 24 | decode: null 25 | } 26 | 27 | defineHeader() 28 | defineEntry() 29 | 30 | function defineHeader () { 31 | var enc = [ 32 | encodings.string 33 | ] 34 | 35 | Header.encodingLength = encodingLength 36 | Header.encode = encode 37 | Header.decode = decode 38 | 39 | function encodingLength (obj) { 40 | var length = 0 41 | if (!defined(obj.type)) throw new Error("type is required") 42 | var len = enc[0].encodingLength(obj.type) 43 | length += 1 + len 44 | return length 45 | } 46 | 47 | function encode (obj, buf, offset) { 48 | if (!offset) offset = 0 49 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 50 | var oldOffset = offset 51 | if (!defined(obj.type)) throw new Error("type is required") 52 | buf[offset++] = 10 53 | enc[0].encode(obj.type, buf, offset) 54 | offset += enc[0].encode.bytes 55 | encode.bytes = offset - oldOffset 56 | return buf 57 | } 58 | 59 | function decode (buf, offset, end) { 60 | if (!offset) offset = 0 61 | if (!end) end = buf.length 62 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 63 | var oldOffset = offset 64 | var obj = { 65 | type: "" 66 | } 67 | var found0 = false 68 | while (true) { 69 | if (end <= offset) { 70 | if (!found0) throw new Error("Decoded message is not valid") 71 | decode.bytes = offset - oldOffset 72 | return obj 73 | } 74 | var prefix = varint.decode(buf, offset) 75 | offset += varint.decode.bytes 76 | var tag = prefix >> 3 77 | switch (tag) { 78 | case 1: 79 | obj.type = enc[0].decode(buf, offset) 80 | offset += enc[0].decode.bytes 81 | found0 = true 82 | break 83 | default: 84 | offset = skip(prefix & 7, buf, offset) 85 | } 86 | } 87 | } 88 | } 89 | 90 | function defineEntry () { 91 | var enc = [ 92 | encodings.varint, 93 | encodings.bytes 94 | ] 95 | 96 | Entry.encodingLength = encodingLength 97 | Entry.encode = encode 98 | Entry.decode = decode 99 | 100 | function encodingLength (obj) { 101 | var length = 0 102 | if (!defined(obj.time)) throw new Error("time is required") 103 | var len = enc[0].encodingLength(obj.time) 104 | length += 1 + len 105 | if (!defined(obj.random)) throw new Error("random is required") 106 | var len = enc[1].encodingLength(obj.random) 107 | length += 1 + len 108 | return length 109 | } 110 | 111 | function encode (obj, buf, offset) { 112 | if (!offset) offset = 0 113 | if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) 114 | var oldOffset = offset 115 | if (!defined(obj.time)) throw new Error("time is required") 116 | buf[offset++] = 8 117 | enc[0].encode(obj.time, buf, offset) 118 | offset += enc[0].encode.bytes 119 | if (!defined(obj.random)) throw new Error("random is required") 120 | buf[offset++] = 18 121 | enc[1].encode(obj.random, buf, offset) 122 | offset += enc[1].encode.bytes 123 | encode.bytes = offset - oldOffset 124 | return buf 125 | } 126 | 127 | function decode (buf, offset, end) { 128 | if (!offset) offset = 0 129 | if (!end) end = buf.length 130 | if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") 131 | var oldOffset = offset 132 | var obj = { 133 | time: 0, 134 | random: null 135 | } 136 | var found0 = false 137 | var found1 = false 138 | while (true) { 139 | if (end <= offset) { 140 | if (!found0 || !found1) throw new Error("Decoded message is not valid") 141 | decode.bytes = offset - oldOffset 142 | return obj 143 | } 144 | var prefix = varint.decode(buf, offset) 145 | offset += varint.decode.bytes 146 | var tag = prefix >> 3 147 | switch (tag) { 148 | case 1: 149 | obj.time = enc[0].decode(buf, offset) 150 | offset += enc[0].decode.bytes 151 | found0 = true 152 | break 153 | case 2: 154 | obj.random = enc[1].decode(buf, offset) 155 | offset += enc[1].decode.bytes 156 | found1 = true 157 | break 158 | default: 159 | offset = skip(prefix & 7, buf, offset) 160 | } 161 | } 162 | } 163 | } 164 | 165 | function defined (val) { 166 | return val !== null && val !== undefined && (typeof val !== 'number' || !isNaN(val)) 167 | } 168 | --------------------------------------------------------------------------------