diff --git a/ots-cli.js b/ots-cli.js index 2d42277..b5958e0 100644 --- a/ots-cli.js +++ b/ots-cli.js @@ -44,6 +44,20 @@ const infoCommand = program info(file, options) }) +const pruneCommand = program + .command('prune [FILE_OTS]') + .alias('p') + .description('Prune timestamp.') + .action((file, options) => { + isExecuted = true + if (!file) { + console.log(pruneCommand.helpInformation()) + return + } + options = parseCommon(options) + prune(file, options) + }) + const stampCommand = program .command('stamp [FILE...]') .alias('s') @@ -185,6 +199,45 @@ function info (argsFileOts, options) { }) } +function prune (argsFileOts, options) { + const files = [] + files.push(Utils.readFilePromise(argsFileOts, null)) + Promise.all(files).then(values => { + const fileOts = values[0] + + // Read ots file and check hash function + let detachedOts + try { + detachedOts = DetachedTimestampFile.deserialize(fileOts) + } catch (err) { + if (err instanceof Context.BadMagicError) { + throw new Error('Error! ' + argsFileOts + ' is not a timestamp file.') + } else if (err instanceof Context.DeserializationError) { + throw new Error('Invalid timestamp file ' + argsFileOts) + } else { + throw err + } + } + // Opentimestamps prune + OpenTimestamps.prune(detachedOts, options) + .then(function (results) { + // Backup the file and create the new receipt + fs.renameSync(argsFileOts, argsFileOts + '.bak') + fs.writeFile(argsFileOts, Buffer.from(detachedOts.serializeToBytes()), 'binary', err => { if (err) { console.log('Error in serialization') } }) + console.log('Recepit Pruned') + }).catch(err => { + console.log('Is not possible to prune due to ' + err) + }) + }).catch(err => { + if (err.code === 'ENOENT') { + console.error('File not found \'' + err.path + '\'') + } else { + console.error(err.message) + } + process.exit(1) + }) +} + function stamp (argsFiles, options) { // check input params : file/hash const filePromises = [] diff --git a/src/calendar.js b/src/calendar.js index 8d771e8..f5e7461 100644 --- a/src/calendar.js +++ b/src/calendar.js @@ -20,6 +20,7 @@ const Message = require('bitcore-message') const Utils = require('./utils.js') const Context = require('./context.js') const Timestamp = require('./timestamp.js') +const { URL } = require('url') /* Errors */ const CommitmentNotFoundError = Error.extend('CommitmentNotFoundError') diff --git a/src/open-timestamps.js b/src/open-timestamps.js index 3b2687d..c86a6d4 100644 --- a/src/open-timestamps.js +++ b/src/open-timestamps.js @@ -97,6 +97,71 @@ module.exports = { return JSON.stringify(json) }, + /* + * Delete all the dead branches + */ + pruneTree (timestamp) { + let prun = timestamp.attestations.length === 0 + let changed = false + + for (const element of timestamp.ops) { + var result = this.pruneTree(element[1]) + var stampPrunable = result.prunable + var stampChanged = result.change + changed = changed || stampChanged || stampPrunable + if (stampPrunable) { + timestamp.ops.delete(element[0]) + } else { + prun = false + } + } + return { prunable: prun, change: changed } + }, + + /* + * Delete all the leaves which didn't match the height passed + */ + discard_attestation (timestamp, heightToKeep) { + timestamp.getAttestations().forEach(opStamp => { + if (!(opStamp instanceof Notary.BitcoinBlockHeaderAttestation && opStamp.height === heightToKeep)) { + timestamp.attestations.splice(timestamp.attestations.indexOf(opStamp), 1) + } + }) + var iter = timestamp.ops.values() + let res = iter.next() + while (!res.done) { + this.discard_attestation(res.value) + res = iter.next() + } + }, + /** + * Prune a timestamp dropping the redundant data included in the ots receipt, storing only the linear proof from the file hash to the best attestation. + * @exports OpenTimestamps/prune + * @param {DetachedTimestampFile[]} detaches - The array of detached file to stamp; input/output parameter. + * @param {Object} options - The option arguments. Not used yet + */ + prune (detaches, options = {}) { + let prunable = false + let maxHead = 0 + + if (detaches.timestamp.getAttestations().length <= 1) { + return new Promise((resolve, reject) => { reject(new Error('Just one attestation founded')) }) + } + detaches.timestamp.getAttestations().forEach(subStamp => { + if (subStamp instanceof Notary.BitcoinBlockHeaderAttestation) { + prunable = true + maxHead = (maxHead < subStamp.height) ? subStamp.height : maxHead + } + }) + if (prunable) { + this.discard_attestation(detaches.timestamp, maxHead) + this.pruneTree(detaches.timestamp) + return new Promise((resolve, reject) => { resolve(detaches) }) + } else { + return new Promise((resolve, reject) => { reject(new Error('There are no Bitcoin Attestation in the file')) }) + } + }, + /** * Create timestamp with the aid of a remote calendar for one or multiple files. * @exports OpenTimestamps/stamp diff --git a/test/calendar.js b/test/calendar.js index 9a34019..36e6c14 100644 --- a/test/calendar.js +++ b/test/calendar.js @@ -1,6 +1,7 @@ const test = require('tape') const Calendar = require('../src/calendar.js') const Utils = require('../src/utils.js') +const { URL } = require('url') test('Calendar.privateSubmit()', assert => { const digest = Array.from(Utils.randBytes(32))