javascript-opentimestamps
Version:
JS Porting of OpenTimestamps
535 lines (489 loc) • 16.4 kB
JavaScript
'use strict'
/**
* Timestamp module.
* @module Timestamp
* @author EternityWall
* @license LPGL3
*/
const bitcore = require('bitcore-lib')
const Utils = require('./utils.js')
const Notary = require('./notary.js')
const Ops = require('./ops.js')
const Context = require('./context.js')
/**
* Class representing Timestamp interface
* Proof that one or more attestations commit to a message.
* The proof is in the form of a tree, with each node being a message, and the
* edges being operations acting on those messages. The leafs of the tree are
* attestations that attest to the time that messages in the tree existed prior.
*/
class Timestamp {
/**
* Create a Timestamp object.
* @param {string} msg - The server url.
*/
constructor (msg) {
if (!msg || !(msg instanceof Array)) {
throw new TypeError('Expected msg to be bytes; got ' + typeof (msg))
} else if (msg.length > (new Ops.Op())._MAX_MSG_LENGTH()) {
throw new TypeError('Message exceeds Op length limit; ' + msg.length + ' > ' + (new Ops.Op())._MAX_MSG_LENGTH())
}
this.msg = msg
this.attestations = []
this.ops = new Map()
}
getDigest () {
return this.msg
}
/**
* Deserialize a Timestamp.
* Because the serialization format doesn't include the message that the
* timestamp operates on, you have to provide it so that the correct
* operation results can be calculated.
* The message you provide is assumed to be correct; if it causes a op to
* raise MsgValueError when the results are being calculated (done
* immediately, not lazily) DeserializationError is raised instead.
* @param {StreamDeserializationContext} ctx - The stream deserialization context.
* @param {initialMsg} initialMsg - The initial message.
* @return {Timestamp} The generated Timestamp.
*/
static deserialize (ctx, initialMsg) {
// console.log('deserialize: ', Utils.bytesToHex(initialMsg));
const self = new Timestamp(initialMsg)
function doTagOrAttestation (tag, initialMsg) {
// console.log('doTagOrAttestation: ', tag);
if (tag === 0x00) {
const attestation = Notary.TimeAttestation.deserialize(ctx)
self.attestations.push(attestation)
// console.log('attestation ', attestation);
} else {
const op = Ops.Op.deserializeFromTag(ctx, tag)
const result = op.call(initialMsg)
// console.log('result: ', Utils.bytesToHex(result));
const stamp = Timestamp.deserialize(ctx, result)
self.ops.set(op, stamp)
}
}
let tag = ctx.readBytes(1)[0]
while (tag === 0xff) {
const current = ctx.readBytes(1)[0]
doTagOrAttestation(current, initialMsg)
tag = ctx.readBytes(1)[0]
}
doTagOrAttestation(tag, initialMsg)
return self
}
/**
* Create a Serialize object.
* @param {StreamSerializationContext} ctx - The stream serialization context.
*/
serialize (ctx) {
// console.log('SERIALIZE');
// console.log(ctx.toString());
if (!(this.attestations) && !(this.ops)) {
throw new Context.ValueError('An empty timestamp can\'t be serialized')
}
// sort
const sortedAttestations = this.attestations
sortedAttestations.sort((a, b) => {
return a.compareTo(b)
})
if (sortedAttestations.length > 1) {
for (let i = 0; i < sortedAttestations.length - 1; i++) {
ctx.writeBytes([0xff, 0x00])
sortedAttestations[i].serialize(ctx)
}
}
if (this.ops.size === 0) {
if (sortedAttestations.length > 0) {
ctx.writeByte(0x00)
sortedAttestations[sortedAttestations.length - 1].serialize(ctx)
}
} else if (this.ops.size > 0) {
if (sortedAttestations.length > 0) {
ctx.writeBytes([0xff, 0x00])
sortedAttestations[sortedAttestations.length - 1].serialize(ctx)
}
// all op/stamp
let index = 0
this.ops.forEach((stamp, op) => {
if (index < this.ops.size - 1) {
ctx.writeBytes([0xff])
index++
}
op.serialize(ctx)
stamp.serialize(ctx)
})
// last op/stamp
/* let lastOp;
let lastStamp;
for (const [op, stamp] of this.ops) {
lastOp = op;
lastStamp = stamp;
} */
// lastOp.serialize(ctx);
// lastStamp.serialize(ctx);
}
}
/**
* Add all operations and attestations from another timestamp to this one.
* @param {Timestamp} other - Initial other Timestamp to merge.
*/
merge (other) {
if (!(other instanceof Timestamp)) {
throw new Context.ValueError('Can only merge Timestamps together')
}
if (!Utils.arrEq(this.msg, other.msg)) {
throw new Context.ValueError('Can\'t merge timestamps for different messages together')
}
other.attestations.forEach(attestation => {
this.attestations.push(attestation)
})
other.ops.forEach((otherOpStamp, otherOp) => {
// ourOpStamp = self.ops.add(otherOp)
let ourOpStamp = this.ops.get(otherOp)
if (ourOpStamp === undefined) {
ourOpStamp = new Timestamp(otherOp.call(this.msg))
this.ops.set(otherOp, ourOpStamp)
}
ourOpStamp.merge(otherOpStamp)
})
}
/**
* Iterate over all attestations recursively
* @return {HashMap} Returns iterable of (msg, attestation)
*/
allAttestations () {
const map = new Map()
this.attestations.forEach(attestation => {
map.set(this.msg, attestation)
})
this.ops.forEach(opStamp => {
const subMap = opStamp.allAttestations()
subMap.forEach((b, a) => {
map.set(a, b)
})
})
return map
}
/**
* Print as memory hierarchical object.
* @param {int} indent - Initial hierarchical indention.
* @return {string} The output string.
*/
toString (indent = 0) {
let output = ''
output += Timestamp.indention(indent) + 'msg: ' + Utils.bytesToHex(this.msg) + '\n'
output += Timestamp.indention(indent) + this.attestations.length + ' attestations: \n'
let i = 0
this.attestations.forEach(attestation => {
output += Timestamp.indention(indent) + '[' + i + '] ' + attestation.toString() + '\n'
i++
})
i = 0
output += Timestamp.indention(indent) + this.ops.size + ' ops: \n'
this.ops.forEach((stamp, op) => {
output += Timestamp.indention(indent) + '[' + i + '] op: ' + op.toString() + '\n'
output += Timestamp.indention(indent) + '[' + i + '] timestamp: \n'
output += stamp.toString(indent + 1)
i++
})
output += '\n'
return output
}
/**
* Print as json hierarchical object.
* @return {string} The output json object.
*/
toJson (fork) {
const json = {}
if (!fork) {
fork = 0
}
if (this.attestations.length > 0) {
json.attestations = []
this.attestations.forEach(attestation => {
const item = {}
item.fork = fork
if (attestation instanceof Notary.PendingAttestation) {
item.type = 'PendingAttestation'
item.param = attestation.uri
} else if (attestation instanceof Notary.UnknownAttestation) {
item.type = 'UnknownAttestation'
item.param = attestation.payload
} else if (attestation instanceof Notary.BitcoinBlockHeaderAttestation) {
item.type = 'BitcoinBlockHeaderAttestation'
item.param = attestation.height
item.merkle = Utils.bytesToHex(this.msg.reverse())
} else if (attestation instanceof Notary.LitecoinBlockHeaderAttestation) {
item.type = 'LitecoinBlockHeaderAttestation'
item.param = attestation.height
item.merkle = Utils.bytesToHex(this.msg.reverse())
} else if (attestation instanceof Notary.EthereumBlockHeaderAttestation) {
item.type = 'EthereumBlockHeaderAttestation'
item.param = attestation.height
item.merkle = Utils.bytesToHex(this.msg.reverse())
}
json.attestations.push(item)
})
}
json.result = Utils.bytesToHex(this.msg)
try {
bitcore.Transaction(Utils.bytesToHex(this.msg))
json.tx = new Ops.OpSHA256().call(new Ops.OpSHA256().call(this.msg))
json.tx = Utils.bytesToHex(json.tx.reverse())
} catch (err) {
}
if (this.ops.size > 1) {
fork++
}
if (this.ops.size > 0) {
json.ops = []
let count = 0
this.ops.forEach((timestamp, op) => {
const item = {}
item.fork = fork + count
item.op = op._TAG_NAME()
item.arg = (op.arg === undefined) ? '' : Utils.bytesToHex(op.arg)
item.result = Utils.bytesToHex(timestamp.msg)
item.timestamp = timestamp.toJson(fork + count)
try {
bitcore.Transaction(Utils.bytesToHex(timestamp.msg))
item.tx = new Ops.OpSHA256().call(new Ops.OpSHA256().call(timestamp.msg))
item.tx = Utils.bytesToHex(item.tx.reverse())
} catch (err) {
}
json.ops.push(item)
count++
})
}
return json
}
/**
* Indention function for printing tree.
* @param {int} pos - Initial hierarchical indention.
* @return {string} The output space string.
*/
static indention (pos) {
let r = ''
for (let i = 0; i < pos; i++) {
r += ' '
}
return r
}
/**
* Print as tree extended hierarchical object.
* @param {int} indent - Initial hierarchical indention.
* @param {int} verbosity - Verbose option.
* @return {string} The output string.
*/
strTree (indent, verbosity) {
const bcolors = {}
bcolors.HEADER = '\x1b[95m'
bcolors.OKBLUE = '\x1b[94m'
bcolors.OKGREEN = '\x1b[92m'
bcolors.WARNING = '\x1b[93m'
bcolors.FAIL = '\x1b[91m'
bcolors.ENDC = '\x1b[0m'
bcolors.BOLD = '\x1b[1m'
bcolors.UNDERLINE = '\x1b[4m'
function strResult (verb, parameter, result) {
let rr = ''
if (verb > 0 && result !== undefined) {
rr += ' == '
const resultHex = Utils.bytesToHex(result)
if (parameter === undefined) {
rr += resultHex
} else {
const parameterHex = Utils.bytesToHex(parameter)
try {
const index = resultHex.indexOf(parameterHex)
const parameterHexHighlight = bcolors.BOLD + parameterHex + bcolors.ENDC
if (index === 0) {
rr += parameterHexHighlight + resultHex.substring(index + parameterHex.length, resultHex.length)
} else {
rr += resultHex.substring(0, index) + parameterHexHighlight
}
} catch (err) {
rr += resultHex
}
}
}
return rr
}
if (indent === undefined) {
indent = 0
}
if (verbosity === undefined) {
verbosity = 0
}
let r = ''
if (this.attestations.length > 0) {
this.attestations.forEach(attestation => {
r += Timestamp.indention(indent) + 'verify ' + attestation.toString() + strResult(verbosity, this.msg) + '\n'
if (attestation instanceof Notary.BitcoinBlockHeaderAttestation) {
const tx = Utils.bytesToHex(new Ops.OpReverse().call(this.msg))
r += Timestamp.indention(indent) + '# Bitcoin block merkle root ' + tx + '\n'
}
if (attestation instanceof Notary.LitecoinBlockHeaderAttestation) {
const tx = Utils.bytesToHex(new Ops.OpReverse().call(this.msg))
r += Timestamp.indention(indent) + '# Litecoin block merkle root ' + tx + '\n'
}
})
}
if (this.ops.size > 1) {
this.ops.forEach((timestamp, op) => {
try {
bitcore.Transaction(Utils.bytesToHex(this.msg))
let tx = new Ops.OpReverse().call(new Ops.OpSHA256().call(new Ops.OpSHA256().call(this.msg)))
tx = Utils.bytesToHex(tx)
r += Timestamp.indention(indent) + '# Bitcoin transaction id ' + tx + '\n'
} catch (err) {
}
const curRes = op.call(this.msg)
const curPar = op.arg
r += Timestamp.indention(indent) + ' -> ' + op.toString() + strResult(verbosity, curPar, curRes) + '\n'
r += timestamp.strTree(indent + 1, verbosity)
})
} else if (this.ops.size > 0) {
try {
bitcore.Transaction(Utils.bytesToHex(this.msg))
let tx = new Ops.OpReverse().call(new Ops.OpSHA256().call(new Ops.OpSHA256().call(this.msg)))
tx = Utils.bytesToHex(tx)
r += Timestamp.indention(indent) + '# transaction id ' + tx + '\n'
} catch (err) {
}
const op = this.ops.keys().next().value
const stamp = this.ops.values().next().value
const curRes = op.call(this.msg)
const curPar = op.arg
r += Timestamp.indention(indent) + op.toString() + strResult(verbosity, curPar, curRes) + '\n'
r += stamp.strTree(indent, verbosity)
}
return r
}
/** Set of al Attestations.
* @return {Array} Array of all sub timestamps with attestations.
*/
directlyVerified () {
if (this.attestations.length > 0) {
return new Array(this)
}
let array = []
this.ops.forEach(value => {
const result = value.directlyVerified()
array = array.concat(result)
})
return array
}
/** Set of al Attestations.
* @return {Set} Set of all timestamp attestations.
*/
getAttestations () {
const set = new Set()
this.allAttestations().forEach(attestation => {
set.add(attestation)
})
return set
}
/** Determine if timestamp is complete and can be verified.
* @return {boolean} True if the timestamp is complete, False otherwise.
*/
isTimestampComplete () {
let found = false
this.allAttestations().forEach(attestation => {
if (attestation instanceof Notary.BitcoinBlockHeaderAttestation) {
found = true
} else if (attestation instanceof Notary.EthereumBlockHeaderAttestation) {
found = true
} else if (attestation instanceof Notary.LitecoinBlockHeaderAttestation) {
found = true
} else if (attestation instanceof Notary.UnknownAttestation) {
found = true
}
})
return found
}
/** Compare timestamps
* @param timestamp the timestamp to compare with
* @return Returns true if timestamps are equals
*/
equals (another) {
if (!(another instanceof Timestamp)) {
return false
}
if (Utils.arrEq(this.getDigest(), another.getDigest()) === false) {
return false
}
// Check attestations
if (this.getAttestations().size !== another.getAttestations().size) {
return false
}
if (this.attestations.length !== another.attestations.length) {
return false
}
for (let i = 0; i < this.attestations.length; i++) {
const a1 = this.attestations[i]
const a2 = another.attestations[i]
if (!(a1.equals(a2))) {
return false
}
}
// Check operations
if (this.ops.size !== another.ops.size) {
return false
}
let it1 = this.ops.keys()
let it2 = this.ops.keys()
for (let i = 0; i < this.ops.size; i++) {
const op1 = it1.next().value
const op2 = it2.next().value
if (!(op1.equals(op2))) {
return false
}
}
it1 = this.ops.values()
it2 = this.ops.values()
for (let i = 0; i < this.ops.size; i++) {
const t1 = it1.next().value
const t2 = it2.next().value
if (!(t1.equals(t2))) {
return false
}
}
return true
}
/**
* Add Op to current timestamp and return the sub stamp
* @param op - The operation to insert
* @return Returns the sub timestamp
*/
add (op) {
// nonce_appended_stamp = timestamp.ops.add(com.eternitywall.ots.op.OpAppend(os.urandom(16)))
// Op opAppend = new OpAppend(bytes);
if (this.ops.has(op)) {
return this.ops.get(op)
}
const stamp = new Timestamp(op.call(this.msg))
this.ops.set(op, stamp)
return stamp
}
/**
* Iterate over all tips recursively
* @return Returns iterable of (msg, attestation)
*/
allTips () {
const set = new Set()
if (this.ops.size === 0) {
set.add(this.msg)
}
this.ops.forEach(stamp => {
const subSet = stamp.allTips()
subSet.forEach(msg => {
set.add(msg)
})
})
return set
}
}
module.exports = Timestamp