mintBlue

Notary Express

An Express service that publishes a digest of a submitted document and finds the same digest again on a later submission, with the timestamp of the first record.

This service answers one question: has this exact content been recorded here before, and if so, when. It publishes a digest of what you submit rather than the content itself, so the record proves the content existed at that time without revealing it.

The name is the application's, not a capability claim. What the service gives you is a tamper-evident, independently checkable record of a digest and its time. Whether that has any legal weight in your setting is a question for your counsel, and nothing here answers it.

What it does

Submit a string through a form. The service computes its SHA-256 digest, looks for a record of that digest in the project, and either returns the earlier record with its timestamp or publishes a new one.

The program

const express = require('express');
const { Mintblue, utils } = require('@mintblue/sdk');

const app = express();
const port = 3000;
const projectId = process.env.MINTBLUE_PROJECT_ID;
let client;

app.use(express.urlencoded({ extended: false }));

async function records() {
  const transactions = await client.listTransactions({
    project_id: projectId,
    order: 'asc',
  });

  const found = [];
  for (const summary of transactions) {
    const full = await client.getTransaction({
      txid: summary.txid,
      parse: true,
    });
    const record = full.outputs.find((output) => output.type === 'data');
    if (record && typeof record.value === 'string') {
      found.push({
        txid: summary.txid,
        digest: record.value,
        recordedAt: summary.published_at,
      });
    }
  }
  return found;
}

app.get('/', (request, response) => {
  response.send(`
    <form method="POST" action="/">
      <input type="text" name="data" placeholder="content">
      <input type="submit">
    </form>
  `);
});

app.post('/', async (request, response) => {
  const digest = await utils.sha256s(request.body.data);
  const match = (await records()).find((entry) => entry.digest === digest);

  if (match) {
    response.send(
      `found<br>digest ${digest}<br>transaction ${match.txid}` +
        `<br>recorded ${match.recordedAt}`,
    );
    return;
  }

  const { txid } = await client.createTransaction({
    project_id: projectId,
    outputs: [{ type: 'data', value: digest, sign: true }],
  });

  response.send(`recorded<br>digest ${digest}<br>transaction ${txid}`);
});

app.listen(port, async () => {
  client = await Mintblue.create({ token: process.env.MINTBLUE_SDK_TOKEN });
  console.log(`listening on http://localhost:${port}`);
});

Install its two dependencies and start it:

npm install @mintblue/sdk express
MINTBLUE_SDK_TOKEN=... MINTBLUE_PROJECT_ID=... node notary.js

Submit mydocument once and the service records the digest and reports the new transaction id. Submit it again, once that transaction has been published, and the service reports found with the same transaction id and the time the record was published, because the digest matched something already in the project.

The three decisions worth copying

utils.sha256s rather than a hand-rolled digest. It ships with the SDK and returns a hex string, which is directly comparable and directly storable. Its sibling utils.sha256 returns the bytes if you would rather handle those.

sign: true and no encryption. A digest reveals nothing about the content that produced it, so there is nothing here to hide, and a record nobody else can read is useless for a check anybody should be able to make. The signature is still worth having: it says which account recorded the digest.

The timestamp comes from the transaction, not from the value. published_at is a property of the record. A timestamp written into the value would be a claim by the same party making the claim, which is exactly the thing the record exists to avoid.

What it is not

Production-shaped. It walks the whole project on every request, so response time grows with history, and it has no authentication in front of it. Two changes fix the first problem: keep an index of digests you have already recorded, or use transactionsAfterTxid to follow the project forward from the last one you saw rather than re-reading it.

It also digests a form field rather than a file. Digesting a file is the same call with the file's bytes, and the hash output type does the digest for you at submission time. See hash and Store a file.

Next

On this page