In this tutorial, we're going to create a web application very similar to Proof of Existence and Origin Stamp using Node.js, Tierion, RethinkDB, PubNub, and jsreport-core.
In a few words, these sites allow you to store the hash of a file in Bitcoin's Blockchain so that anyone can certify that the file existed at a specific time.
This has the following advantages:
This page has a more thorough explanation of proof of existence and covers more sites dedicated to enabling it.
Using our application, we will be able to upload a file by dragging and dropping it to a marked area. Then, its SHA256 digest will be calculated and sent to the Blockchain using Tierion Hash API
Notice how the list of last documents registered is updated in real-time; we'll be using PubNub's Storage and Playback to gain real-time syncing.
The hash of the file and the Blockchain receipt given by Tierion is stored in RethinkDB, a NoSQL database with real-time capabilities. When Tierion alerts us that a block of hashes has been processed, we update the verification page (and the list of verified documents) in real-time, using RethinkDB's changefeed:
As you can see, once the hash is anchored to the Blockchain, we can check it using sites like Block Explorer or Blockchain.info and get the receipt in PDF format using jsreport-core.
You can also verify that a file has been certified in the Blockchain by selecting the Verify option:
In case you stumble or want to skip ahead, the tutorial source code is available on Github.
Before diving into the code, let's explain what is the Bitcoin's Blockchain (referred to as the Blockchain).
Wayne Vaughan, founder and CEO of Tierion, launched a challenge during the 2016 Consensus Hackathon: What is the Blockchain? The winner was Lilia Vershinina, who answered:
"The Blockchain is a trust layer for the Internet".
Interesting. Let's see if we can clarify this statement. By the way, you can see the whole story behind this (as well as other answers) here.
One of the main philosophies behind Bitcoin is decentralization. Rather than trusting a single party (like a government or an institution) to manage and produce currency, Bitcoin is a peer-to-peer system with no central point of failure. As such, the blockchain is a public database of Bitcoin transactions distributed across a global network of nodes.
The blockchain comprises all Bitcoin transactions, which are represented as a chain of blocks linked together, since 2009, the year of its inception. These transactions span from the genesis block, or the first block of the blockchain, to the latest block.
When Bitcoin transactions are broadcasted, miners pick them up locally to form a Bitcoin block. Miners verify this block using a designated mathematical formula.
A hash is used to represent the verification of the block. This hash is computed using this block and the hash of the previous block, thereby forming a chain of hashes. You can see the concept of trust here; if any transaction is modified or voided, the hash, as well as the rest of the chain, becomes invalid.
For this reason, as the chain grows and more copies of it are kept by miners, the more difficult it becomes to modify the blockchain. Moreover, the increasing mining difficulty causes blocks to be generated about every ten minutes, a rather slow pace.
This is a (very) high-level overview of the blockchain. There are countles articles and books that explain and discuss it. If you want to know more, try this search.
Now that we understand blocks, blockchain, Bitcoin, and mining, let's dive into our web application.
We'll need a Tierion free account. You can signup here.
In addition to its Hash API, Tierion offers datastores to record any data in the blockchain. Depending on your plan, you can use a certain number of datastores and records per month. However, we'll be using the Hash API (that just record hashes in the blockchain), which at the time of this writing, it's free to use with some limitations.
When your account is ready, go to your Dashboard and in the API tab, copy your client secret, we'll need it later:
You'll need to install RethinkDB server. The easier way to install it is with one of the official packages.
This tutorial won't cover the basics of RethinkDB, but you can consult RethinkDB's ten-minute guide or my getting started guide if you haven't work with it.
Go to https://admin.pubnub.com/#/register and sign in. Create a new account if necessary. When you log in, you will be presented with this screen:
You can either create a new app or use the demo app. When you click on it, you'll something like the following:
Save your publish and subscribe keys because we'll need them later. Now click on the keyset panel, and go to the Application add-ons section to enable the Storage and Playback. Pubnub's Storage and Playback will let us determine the time period we want the messages to be stored. Enable Stream Controller add-ons as well. Finally, save the changes:
We'll be using the brand-new PubNub SDK for Javascript version 4, which is a little different than version 3. Here's the Node.js documentation and the Javascript (Web) documentation.
Tierion will trigger a webhook (similar to a callback) when a block of hashes has been anchored in the blockchain. An HTTP request will be made to our server, so we'll need to deploy our application on the cloud or keep it locally and use a service like ngrok.
Ngrok proxies external requests to your local machine by creating a secure tunnel and giving you a public URL.
ngrok is a Go program, distributed as a single executable file (no additional dependencies needed). For now, just download it from https://ngrok.com/download and unzip the compressed file.
You'll also need Node.js and npm installed. You can download an installer for your platform here.
Requirements met, we can start on the app.
Create a new directory and cd
into it:
1mkdir blockchain-proof && cd blockchain-proof
Now create a package.json
file either with:
1npm init
Or to accept the default values:
1npm init -y
We will use the next dependencies:
Add them with the following npm command:
1npm install --save hashapi-lib-node rethinkdbdash pubnub jsreport-core jsreport-jsrender jsreport-wkhtmltopdf express ejs body-parser
The project will have the following directory structure:
1|— models
2| |— receipt.js
3| |— subscription.js
4|— public
5| |— css
6| | |— index.css
7| | |— verify.css
8| |— img
9| | |— cloud-upload.css
10| |— js
11| | |— index.js
12| | |— pubnub-index.js
13| | |— pubnub-verify.js
14| | |— qrcode.min.js
15|— routes
16| |— app.js
17|— templates
18| |— receipt.html
19|— views
20| | |— index.ejs
21| | |— verify.ejs
22| |— config.js
23| |— package.json
24| |— server.js
The config.js
file stores all the important configuration (use your own keys in place of the Xs):
1module.exports = {
2 tierion: {
3 client_secret: "XXXXXXXXXXXXXXXX",
4 username: "[email protected]",
5 password: "xxxxxxxxxx"
6 },
7
8 pubnub: {
9 ssl: false,
10 publish_key: "pub-c-xxxxx-xxx-xxxx-xxxx-xxxx",
11 subscribe_key: "sub-c-xxxxxx-xxxx-xxx-xxxx-xxx",
12 registered_channel: "registered_channel",
13 confirmed_channel: "confirmed_channel"
14 },
15
16 url: "http://xxxxxxxx.ngrok.io",
17
18 db: "existence",
19
20 port: process.env.APP_PORT || 3000
21};
Start RethinkDB with this command:
1rethinkdb
Go to the Data Explorer section of your RethinkDB's dashboard and execute the following commands:
1r.dbCreate("existence");
2r.db("existence").tableCreate("subscription");
3r
4 .db("existence")
5 .table("subscription")
6 .insert({ id: 1, "subscription-id": "" });
7r.db("existence").tableCreate("receipt");
8r
9 .db("existence")
10 .table("receipt")
11 .indexCreate("timestamp");
These commands will create the database and tables of the application. You can leave the server running in the background while we work on the rest of the application.
Now, in a new terminal window, navigate to the directory where you unzipped ngrok.
We'll start ngrok by telling it which port we want to expose to the Internet. In our example, the port 3000
:
1./ngrok http 3000
Alternatively, if you're on Windows:
1ngrok http 3000
Now, you should see something like this:
See that URL in the Forwarding row(s) with the ngrok.io
domain? That's your public URL. Yours will be different; ngrok generates a random URL every time you run it.
Change the url
property on the config.js
file to that URL.
This URL is not permanent. If you restart ngrok, it will give you another URL.
You can specify a subdomain. For example, to get the URL http://app.ngrok.io
use the command:
1ngrok http -subdomain=app 3000
However, this requires a paid plan. You can get more info and acquire the paid version on this page.
Nevertheless, as long as you don't stop or restart ngrok, your public URL won't change. Let's leave ngrok running for now.
Next, let's review the server-side code starting with the Tierion Hash API.
Tierion Hash API is simple. First, you send your email and password to get a token that you must include in all requests. Next, you submit the token to get a receipt ID, which you'll use to get the Blockchain Receipt about ten minutes later.
With Block Subscriptions there's no need to check constantly for the receipt, a callback will alert you when a block of hashes has been processed and the corresponding receipts have been generated.
This is a REST API, or a Representational State Transfer Application Program Interface. Luckily, Tierion offers a client library for Node.js that makes things easier.
There are only two tricky parts when using this Block Subscription:
Let's start by creating the server.js
file with a standard Express configuration with EJS as the template engine:
1var express = require("express");
2var app = express();
3var bodyParser = require("body-parser");
4var path = require("path");
5var config = require("./config");
6
7var app = express();
8
9app.use(bodyParser.json());
10app.use(
11 bodyParser.urlencoded({
12 extended: true
13 })
14);
15
16app.use(express.static(path.join(__dirname, "public")));
17app.set("views", path.join(__dirname, "views"));
18
19app.set("view engine", "ejs");
20
21app.listen(config.port, function() {
22 console.log("Server up and listening on port %d", config.port);
23});
To make the raw body available to in our request objects, we can configure body-parser
like this:
1app.use(
2 bodyParser.urlencoded({
3 extended: true,
4 verify: function(req, res, buf) {
5 req.rawBody = buf.toString();
6 }
7 })
8);
This way, rawBody
will contain the raw body of the request, for example:
1id=5796c41f8f5944de49bc0d1d&startTimestamp=1470758399&endTimestamp=1470758999
For the Block Subscriptions, we'll store the subscription in the subscription
table so we can do something like this:
Let's translate this into code using the following approach:
Create the models/subscription.js
file. This will contain the code for the Block Subscription set up. The first part is:
1var config = require("../config");
2var r = require("rethinkdbdash")({
3 db: config.db
4});
5var hashapi = require("hashapi-lib-node");
6
7var SUBSCRIPTION_TABLE = "subscription";
8
9var URL = config.url + "/tierion";
It creates the variables required to do the work.
For this project, instead of using the official RethinkDB driver for Node.js, we'll be using rethinkdbdash. Now, the connections are managed by the driver with a connection pool, so we don't need to call r.connect
or pass a connection to the run
function.
Let's create a function to create a block subscription. It looks like this:
1var createSubscription = function(callback) {
2 hashClient.createBlockSubscription(URL, function(err, subscription) {
3 console.log("Create subscription");
4 if (err) {
5 console.log(err);
6 } else {
7 r
8 .table(SUBSCRIPTION_TABLE)
9 .get(1)
10 .update({ "subscription-id": subscription.id })
11 .run()
12 .then(function(updateResult) {
13 callback(hashClient);
14 });
15 }
16 });
17};
The createBlockSubscription
function configures a new Block Subscription callback to our server (with the URL
variable). If it's created successfully, we update the table in our database with the returned ID of the subscription. When this operation is completed, a callback will be executed.
Now, the function to setup the block subscription:
1module.exports.setup = function(callback) {
2 console.log("Setting up Block Subscription...");
3
4 hashClient.authenticate(
5 config.tierion.username,
6 config.tierion.password,
7 function(err, authToken) {
8 if (err) {
9 console.log(err);
10 } else {
11 //console.log(authToken);
12
13 r
14 .table(SUBSCRIPTION_TABLE)
15 .get(1)
16 .run()
17 .then(function(result) {
18 var id = result["subscription-id"];
19
20 if (id === "") {
21 createSubscription(callback);
22 } else {
23 hashClient.getBlockSubscription(id, function(errGet, result) {
24 // The subscription doesn't exists
25 if (errGet) {
26 console.log("Subscription doesnt exists");
27 createSubscription(callback);
28 } else {
29 // Let's check the callback URL hasn't change. If it has changed, update the URL
30 if (result.callbackUrl === URL) {
31 callback(hashClient);
32 } else {
33 hashClient.updateBlockSubscription(id, URL, function(
34 errUpdate,
35 result
36 ) {
37 console.log("Update susbcription");
38 if (errUpdate) {
39 console.log(errUpdate);
40 } else {
41 callback(hashClient);
42 }
43 });
44 }
45 } // else subscription exists
46 });
47 } // else exists subscription ID
48 });
49 } // else authentication
50 }
51 );
52};
First, we authenticate to Tierion's API. Then, we get the ID of the subscription from the database. If no ID exists, we create a subscription. Otherwise, the ID's validated according to the plan we laid out before.
Now, if we update the last part of server.js
, it should look like this:
1var subscription = require("./models/subscription");
2
3subscription.setup(function(hashClient) {
4 app.listen(config.port, function() {
5 console.log("Server up and listening on port %d", config.port);
6 });
7});
But before starting the server, let's configure the routes of the application. This will the topic of the next section.
Our application will have five routes:
/
will render the main page/verify/:hash
will render the verification page for the hash passed as a parameter/hash
to register a new hash/tierion
will receive the block subscription callback (you already saw it in the previous section)/pdf/:id
will render the receipt in PDF formatThe code of these routes is simple, so it's better to keep them all in one file. They'll use the Hash API client, so let's pass it as a parameter. Modify the last part of server.js
as so:
1var subscription = require("./models/subscription");
2
3subscription.setup(function(hashClient) {
4 var routes = require("./routes/app")(app, hashClient);
5
6 app.listen(config.port, function() {
7 console.log("Server up and listening on port %d", config.port);
8 });
9});
In the routes/app.js
file, enter these definitions:
1var fs = require("fs");
2var url = require("url");
3var crypto = require("crypto");
4var jsreport = require("jsreport-core")();
5var model = require("../models/receipt");
6var config = require("../config");
7
8jsreport.use(require("jsreport-wkhtmltopdf")());
9jsreport.use(require("jsreport-jsrender")());
10jsreport.init();
11
12var receiptTemplate = fs.readFileSync(
13 __dirname + "/../templates/receipt.html",
14 "utf8"
15);
16
17var validateRequest = function(rawBody, providedSignature) {
18 var hmac = crypto
19 .createHmac("sha256", config.tierion.client_secret)
20 .update(rawBody, "utf8");
21 var calculatedSignature = hmac.digest("hex");
22
23 return providedSignature == calculatedSignature;
24};
First, we require()
the modules that we're going to use. We'll see the code of the models/receipt.js
file in the next section.
Then, we load the modules used by jsreport to render a PDF, (jsreport-wkhtmltopdf) and (jsreport-jsrender), so we can initialize the library.
The following line will read (in a synchronous way) the HTML of the file that will serve as the template for the PDF generated by jsreport:
1var receiptTemplate = fs.readFileSync(
2 __dirname + "/../templates/receipt.html",
3 "utf8"
4);
Notice the use of __dirname
to build the absolute path to the file. In Node.js, you should be very careful with relative paths. The only place where you can use relative paths reliably is in require()
statements.
The next function will validate that the request received by Tierion's block subscription really comes from Tierion:
1var validateRequest = function(rawBody, providedSignature) {
2 var hmac = crypto
3 .createHmac("sha256", config.tierion.client_secret)
4 .update(rawBody, "utf8");
5 var calculatedSignature = hmac.digest("hex");
6
7 return providedSignature == calculatedSignature;
8};
Every callback includes a header containing the HMAC-SHA256 signature of the request. We have to calculate the signature using the raw body of the request and our account's Client Secret. If the calculated and the provided signatures match, we have a valid request.
Now the routes. The first one simply shows the main page of the site:
1module.exports = function(app, hashClient) {
2 app.get("/", function(req, res) {
3 res.render("index", { config: config });
4 });
5};
The next one will get the hash from the database through the model object, and it will render the verification page on success
1module.exports = function (app, hashClient) {
2
3 ...
4
5 app.get('/verify/:hash', function (req, res) {
6 model.getReceiptByHash(req.params.hash, function (success, result) {
7 if(success) {
8 res.render('verify', {
9 data: result,
10 config: config
11 });
12 } else {
13 res.sendStatus(404); // Not found
14 }
15 });
16 });
17}
To register a new hash, we have to check if the hash the user is sending has already been registered. If not, we send it to Tierion and store a receipt object with the information we got at this point:
1module.exports = function (app, hashClient) {
2
3 ...
4
5 app.post('/hash', function (req, res) {
6 model.getReceiptByHash(req.body.hash, function (success, result) {
7 if(success) { // Exists, redirect to verify
8 res.json({
9 status: 'Exists'
10 });
11 } else { // Doesn't exist, send it to Tierion
12 hashClient.submitHashItem(req.body.hash, function(err, result){
13 if(err) {
14 console.log(err);
15 res.json({
16 status: 'Error'
17 });
18 } else {
19 console.log(result);
20
21 var receipt = {
22 id: result.receiptId,
23 timestamp: result.timestamp,
24 sent_to_blockchain: false,
25 blockchain_receipt: null,
26 created_at: new Date(result.timestamp * 1000)
27 .toISOString()
28 .replace(/T/, ' ')
29 .replace(/.000Z/, ' UTC'),
30 hash: req.body.hash
31 };
32
33 model.saveReceipt(receipt, function (success, result) {
34 if (success) res.json({
35 status: 'OK'
36 });
37 else res.json({
38 status: 'Error'
39 });
40 });
41 }
42 });
43 }
44 });
45 });
46}
To keep things simple, the application will only support time in UTC (Coordinated Universal Time). The timestamp returned by Tierion is a UNIX timestamp, which represents the number of seconds since January 1, 1970, 00:00:00 UTC. Since the argument of the Date
object in Javascript is the number of milliseconds since the Unix epoch, we have to multiply it by 1000.
The next route (for the Tierion callback) uses the validateRequest
function showed before and simply marks the receipts once they've been sent to the blockchain. Marking will trigger RethinkDB's changefeed to get the actual receipt objects and send the updates using Pubnub (more on this in the next section):
1module.exports = function (app, hashClient) {
2
3 ...
4
5 app.post('/tierion', function(req, res) {
6 var validRequest = validateRequest(req.rawBody, req.get('x-tierion-sig'));
7
8 if(validateRequest) {
9 model.markReceiptsAsSent(req.body.startTimestamp, req.body.endTimestamp);
10 } else {
11 return res.status(403).send('Request validation failed');
12 }
13
14 res.sendStatus(200); // OK
15 });
16}
Notice that we're using rawBody
to authenticate the request and a signature provided by Tierion. A request from Tierion looks like this:
1{
2 id: '5796c41f8f5944de49bc0d1d',
3 merkleRoot: '21e31466867ab47e079421a7eeadaf5fae300a3c9eba8436ddab8b5cb90bf04b',
4 transactionId: 'f42f4b074e2d46ab0f3883a907dac52b6bb373b97c1282f5b4111632df1d225f',
5 startTimestamp: '1470758399',
6 endTimestamp: '1470758999'
7}
Finally, the route to present the PDF receipt looks like this:
1module.exports = function (app, hashClient) {
2
3 ...
4
5 app.get('/pdf/:id', function (req, res) {
6 model.getReceipt(req.params.id, function(success, receipt) {
7 if(success) {
8 var requestUrl = url.format({
9 protocol: req.protocol,
10 host: req.get('host')
11 });
12
13 jsreport.render({
14 template: {
15 content: receiptTemplate,
16 engine: 'jsrender',
17 recipe: 'wkhtmltopdf'
18 },
19 data: {
20 r: receipt,
21 url: requestUrl
22 }
23 }).then(function(obj) {
24 res.set(obj.headers);
25 res.send(obj.content);
26 });
27 } else {
28 res.sendStatus(500);
29 }
30 });
31 });
32}
It gets the receipt object by its ID from the database and uses jsreport-core to generate the PDF file from the HTML template with jsrender.
In the next section, we'll review the receipt model.
The core of the application is the models/receipt.js
file. It contains the functions that interact with RethinkDB and PubNub to implement the functionality related to the blockchain receipts provided by Tierion.
The first part requires the modules used by the functions and creates the PubNub and RethinkDB objects:
1var Pubnub = require("pubnub");
2var config = require("../config");
3
4var pubnub = new Pubnub({
5 ssl: config.pubnub.ssl,
6 publishKey: config.pubnub.publish_key,
7 subscribeKey: config.pubnub.subscribe_key
8});
9
10var r = require("rethinkdbdash")({
11 db: config.db
12});
13
14var RECEIPT_TABLE = "receipt";
The first function sets up RethinkDB's changefeed, which will get the receipt object from Tierion, save it to the database, and publish the objects so the pages presented to the user can be updated in real-time:
1module.exports.setup = function(hashClient) {
2 // Setup changefeed
3 r
4 .table(RECEIPT_TABLE)
5 .filter(
6 r.and(
7 r.row("sent_to_blockchain").eq(true),
8 r.row("blockchain_receipt").eq(null)
9 )
10 )
11 .changes()
12 .run()
13 .then(function(cursor) {
14 cursor.each(function(error, row) {
15 if (row && row.new_val) {
16 hashClient.getReceipt(row.new_val.id, function(err, result) {
17 if (err) {
18 console.log(err);
19 } else {
20 var obj = JSON.parse(result.receipt);
21 // Save receipt to the database
22 r
23 .table(RECEIPT_TABLE)
24 .get(row.new_val.id)
25 .update({ blockchain_receipt: obj })
26 .run()
27 .then(function(updateResult) {
28 // Publish the object so the verification page is updated with the blockchain information
29 pubnub.publish(
30 {
31 channel: row.new_val.hash,
32 message: Object.assign({ id: row.new_val.id }, obj),
33 storeInHistory: false
34 },
35 function(status, response) {
36 console.log(status, response);
37 }
38 );
39
40 // Publish the object so the list of last confirmed files on the main page is update
41 var formattedTimestamp = new Date()
42 .toISOString()
43 .replace(/T/, " ")
44 .replace(/.\d{3}Z/, " UTC");
45 pubnub.publish(
46 {
47 channel: config.pubnub.confirmed_channel,
48 message: {
49 hash: row.new_val.hash,
50 timestamp: formattedTimestamp
51 }
52 },
53 function(status, response) {
54 console.log(status, response);
55 }
56 );
57 })
58 .error(function(error) {
59 console.log(error);
60 });
61 }
62 });
63 }
64 });
65 });
66};
If you're curious, this is how a receipt object (version 2.0) from Tierion looks like:
1{
2 "@context": "https://w3id.org/chainpoint/v2",
3 "anchors": [
4 {
5 "sourceId": "e8e92ff3efcb2660922e8a870a4ebcda54c41978f0db3374b4c5d31173f0d720" ,
6 "type": "BTCOpReturn"
7 }
8 ] ,
9 "merkleRoot": "a82c3e4b04bcdec045695129c50221b15eadd2bfff6b65533a17867c201bbde0" ,
10 "proof": [
11 {
12 "right": "8b6aafc21661aed7ff5a7e8fce857ca0d2f4cef576c1c47c454d58a58ce4c09c"
13 }
14 ] ,
15 "targetHash": "691b6e0b3db43e22466734f7ec4b15d17687d5d8a92212fb99691e454cb9d9c4" ,
16 "type": "ChainpointSHA256v2"
17}
Now that we have this function, let's update the last part of server.js
, so it can be called upon server initialization:
1var subscription = require("./models/subscription");
2var receipt = require("./models/receipt");
3
4subscription.setup(function(hashClient) {
5 var routes = require("./routes/app")(app, hashClient);
6
7 app.listen(config.port, function() {
8 console.log("Server up and listening on port %d", config.port);
9 receipt.setup(hashClient);
10 });
11});
Back to receipt.js
, this changefeed is triggered when sent_to_blockchain
is set to true
(and the record doesn't have a blockchain_receipt
). This is done by the markReceiptsAsSent
function, called when Tierion's callback is received:
1module.exports.markReceiptsAsSent = function(startTimestamp, endTimestamp) {
2 r
3 .table(RECEIPT_TABLE)
4 .between(
5 r.epochTime(parseInt(startTimestamp)),
6 r.epochTime(parseInt(endTimestamp)),
7 { index: "timestamp" }
8 )
9 .update({ sent_to_blockchain: true })
10 .run();
11};
Using RethinkDB's time and date features, we mark all the records between the timestamps of processed hashes provided by Tierion.
To save a receipt object, we first clone the original object with the Object.assign() function, so we can safely change the timestamp property to use RethinkDB's time type, insert it into the database, and publish an update to the registered document channel (more on this in the next section):
1module.exports.saveReceipt = function(receipt, callback) {
2 var formattedTimestamp = new Date(receipt.timestamp * 1000)
3 .toISOString()
4 .replace(/T/, " ")
5 .replace(/.000Z/, " UTC");
6 var clonedReceipt = Object.assign({}, receipt);
7 clonedReceipt.timestamp = r.epochTime(receipt.timestamp);
8
9 r
10 .table(RECEIPT_TABLE)
11 .insert(clonedReceipt)
12 .run()
13 .then(function(results) {
14 pubnub.publish(
15 {
16 channel: config.pubnub.registered_channel,
17 message: {
18 hash: receipt.hash,
19 timestamp: formattedTimestamp
20 }
21 },
22 function(status, response) {
23 console.log(status, response);
24 callback(true, clonedReceipt);
25 }
26 );
27 })
28 .error(function(error) {
29 console.log(error);
30 callback(false, error);
31 });
32};
The other two methods on this file just get a receipt object by its hash and ID respectively:
1module.exports.getReceiptByHash = function(hash, callback) {
2 r
3 .table(RECEIPT_TABLE)
4 .filter(r.row("hash").eq(hash))
5 .run()
6 .then(function(result) {
7 if (result.length > 0) {
8 callback(true, result[0]);
9 } else {
10 callback(false, result);
11 }
12 })
13 .error(function(error) {
14 console.log(error);
15 callback(false, error);
16 });
17};
18
19module.exports.getReceipt = function(id, callback) {
20 r
21 .table(RECEIPT_TABLE)
22 .get(id)
23 .run()
24 .then(function(result) {
25 callback(true, result);
26 })
27 .error(function(error) {
28 console.log(error);
29 callback(false, error);
30 });
31};
Now let's review the code of the HTML pages.
Here's the complete code for the index page template:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8">
5 <title>Tierion/RethinkDB/Twilio Sync Demo</title>
6 <meta name="viewport" content="width=device-width,initial-scale=1" />
7 <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,400" />
8 <link rel="stylesheet" href="css/index.css" />
9</head>
10
11<body>
12
13<div class="container" role="main">
14
15 <nav role="navigation">
16 <a id="create-link" href="#" class="is-selected">Create</a>
17 <a id="verify-link" href="#">Verify</a>
18 </nav>
19
20 <div id="drag-zone" class="box upload">
21
22
23 <div class="box-input">
24 <img class="icon" src="/img/cloud-upload.png" />
25 <input type="file" name="files" id="file" class="file" />
26 <label for="file"><strong>Choose a file</strong></label> or drag it here.
27 </div>
28
29
30 <div class="uploading">Reading…</div>
31 <div class="success">
32 Done!<br/>The hash of the file is:
33 <span id="hash"></span>
34 <a id="submit-link" href="#">Submit</a> or <a id="read-another-link" href="#">read another file</a>.
35 </div>
36 <div class="error">Error. Try again!</div>
37 </div>
38
39
40
41 <footer>
42 <h2>Last documents registered:</h2>
43 <table class="table" id="latest-registered">
44 <tr>
45 <th>File Hash</th><th>Timestamp</th>
46 </tr>
47 </table>
48
49
50 <h2>Last documents confirmed:</h2>
51 <table class="table" id="latest-confirmed">
52 <tr>
53 <th>File Hash</th><th>Timestamp</th>
54 </tr>
55 </table>
56 </footer>
57
58</div>
59
60<script id="item-template" type="text/x-jsrender">
61 <tr><td><a href="verify/{{:hash}}">{{:hash}}</a></td><td>{{:timestamp}}</td></tr>
62</script>
63
64<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
65<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js"></script>
66<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/0.9.80/jsrender.min.js"></script>
67<script src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.0.0.js"></script>
68<script>
69 var REGISTERED_CHANNEL = '<%= config.pubnub.registered_channel %>';
70 var CONFIRMED_CHANNEL = '<%= config.pubnub.confirmed_channel %>';
71
72 var pubnubConfig = {
73 subscribeKey: '<%= config.pubnub.subscribe_key %>'
74 };
75</script>
76<script src="js/index.js"></script>
77<script src="js/pubnub-index.js"></script>
78</body>
79
80</html>
It's a simple HTML page. Before the <script>
tags, we define a template that contains the HTML to add to the lists of the latest registered and confirmed documents:
1<script id="item-template" type="text/x-jsrender">
2 <tr><td><a href="verify/{{:hash}}">{{:hash}}</a></td><td>{{:timestamp}}</td></tr>
3</script>
About the Javascript files, we're going to focus only on the essential parts. You can see the complete code for the Javascript (and CSS files) on the Github repository for this application.
For the public/js/index.js
file, the important part is where the hash of the file is generated. We're going to use the FileReader and cryptographic libraries of Javascript.
First, we test if the browser supports them. If that's not the case, an alert is presented:
1if (
2 !(
3 window.File &&
4 window.FileReader &&
5 window.crypto &&
6 window.crypto.subtle &&
7 window.crypto.subtle.digest
8 )
9) {
10 alert(
11 "No File API support. Please use the latest vesion of Firefox or Chrome."
12 );
13}
To read the file when it's dragged or chosen, we have the following code:
1dragZone.on( 'drop', function(evt)
2{
3 var files = evt.originalEvent.dataTransfer.files;
4
5 handleFile(files);
6});
7
8inputFile.on('change', function(evt) {
9 var files = evt.originalEvent.target.files;
10
11 handleFile(files);
12});
13
14function handleFile(files) {
15 if (!files.length) {
16 return;
17 }
18 var file = files[0];
19
20 var reader = new FileReader();
21 reader.readAsArrayBuffer(file);
22
23 reader.onload = function(e) {
24 var data = e.target.result;
25
26 ...
27 };
One downside is that this approach won't work with large files. Large generally means in the 200MB+ range, but the definition depends on your computer memory. A more robust implementation would read the file in blocks using workers. However, that's a little complicated and beyond the scope of this tutorial.
Then, to calculate the hash (as a SHA256 digest), we use the digest function, which returns the hash as an ArrayBuffer. Then we convert to its hexadecimal (HEX) representation:
1reader.onload = function(e) {
2 var data = e.target.result;
3
4 window.crypto.subtle.digest({name: 'SHA-256'}, data).then(function(hash) {
5 var hexString = '';
6 var bytes = new Uint8Array(hash);
7
8 for (var i = 0; i < bytes.length; i++) {
9 var hex_i = bytes[i].toString(16);
10 hexString += hex_i.length === 1 ? '0' + hex_i : hex_i;
11 }
12
13 $('#hash').text(hexString);
14 calculatedHash = hexString;
15
16 ...
17 }).catch(function(e) {
18 showError(e);
19 });
20 };
Now we have a HEX representation of the the original hash.
The next section will cover using PubNub's Storage and Playback functionality to get the list of the latest registered and confirmed documents.
PubNub's Storage and Playback feature allows you to store messages so they can be retrieved at a later time.
In the setup
and save
functions of the receive module, we saw how you can publish to channels using version 4 of PubNub SDK.
Notice how you can indicate not to storage a message with storeInHistory: false
(when the storage add-on is enabled, by default, all messages in all channels are stored).
setup
function:
1...
2pubnub.publish({
3 channel : row.new_val.hash,
4 message : Object.assign({id: row.new_val.id}, obj),
5 storeInHistory: false
6 },
7 function(status, response) {
8 console.log(status, response);
9 }
10);
11...
12pubnub.publish({
13 channel : config.pubnub.confirmed_channel,
14 message : {
15 hash: row.new_val.hash,
16 timestamp:formattedTimestamp
17 },
18 },
19 function(status, response) {
20 console.log(status, response);
21 }
22);
23...
save
function:
1...
2pubnub.publish({
3 channel : config.pubnub.registered_channel,
4 message : {
5 hash: receipt.hash,
6 timestamp:formattedTimestamp
7 }
8 },
9 function(status, response) {
10 console.log(status, response);
11 callback(true, clonedReceipt);
12 }
13);
14...
This way, on the public/js/pubnub-index.js
file, we create some variables and the PubNub object:
1$(document).ready(function() {
2 var DOCS_TO_SHOW = 5;
3 var listRegistered = '#latest-registered';
4 var listConfirmed = '#latest-confirmed';
5 var itemTemplate = $.templates('#item-template');
6
7 var pubnub = new PubNub(pubnubConfig);
8}
With multiplexing, we can subscribe to the channel for registered and confirmed documents:
1pubnub.addListener({
2 message: function(data) {
3 console.log(data);
4
5 var el = listRegistered;
6 if (data.subscribedChannel === CONFIRMED_CHANNEL) {
7 el = listConfirmed;
8 }
9
10 $(itemTemplate.render(data.message))
11 .hide()
12 .insertAfter($(el + " tr:first-child"))
13 .fadeIn();
14
15 var elements = $(el + " tr").length - 1;
16 if (elements > DOCS_TO_SHOW) {
17 $(el + " tr")
18 .last()
19 .remove();
20 }
21 }
22});
23
24pubnub.subscribe({
25 channels: [REGISTERED_CHANNEL, CONFIRMED_CHANNEL]
26});
Multiplexing means listening to multiple data streams on a single connection. Notice that our addListener
function must come before our subscribe
function.
Inside addListener
, we use the subscribedChannel
property to know from which channel the message comes from. The actual message is in the message
property.
This is for listening to new documents after the page is fully loaded. For the initial list when the page just loads, we call the history
function on both channels to retrieve the last five new documents on the channels (using reverse
and count
):
1pubnub.history(
2 {
3 channel: REGISTERED_CHANNEL,
4 reverse: false,
5 count: DOCS_TO_SHOW
6 },
7 function(status, response) {
8 console.log(response);
9
10 response.messages.forEach(function(item) {
11 $(listRegistered + " tr:first-child").after(
12 itemTemplate.render(item.entry)
13 );
14 });
15 }
16);
17
18pubnub.history(
19 {
20 channel: CONFIRMED_CHANNEL,
21 reverse: false,
22 count: DOCS_TO_SHOW
23 },
24 function(status, response) {
25 console.log(response);
26
27 response.messages.forEach(function(item) {
28 $(listConfirmed + " tr:first-child").after(
29 itemTemplate.render(item.entry)
30 );
31 });
32 }
33);
Next, we'll move onto the verification page.
The verification page is simple too:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8">
5 <title>Tierion/RethinkDB/Twilio Sync Demo</title>
6 <meta name="viewport" content="width=device-width,initial-scale=1" />
7 <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700" />
8 <link rel="stylesheet" href="/css/verify.css" />
9</head>
10<body>
11<div class="container">
12
13 <div id="notice">
14 <span class="title">The hash hasn't been submitted to <br/>the Blockchain yet.</span>
15
16 <p>Keep this page open and wait 10 minutes at most, it will be updated automatically<br/>OR<br/> Bookmark it an check back later.</p>
17 </div>
18
19 <div id="info">
20 <p>
21 <span class="subtitle">Hash:</span>
22 <small><%= data.hash %></small>
23 </p>
24
25 <p>
26 <span class="subtitle">Created at:</span>
27 <small><%= data.created_at %></small>
28 </p>
29
30 </div>
31
32 <div id="links"></div>
33</div>
34
35<script id="message-template" type="text/x-jsrender">
36 <span class="title">The hash was submitted to <br/>the Blockchain.</span>
37</script>
38
39<script id="blockchain-data-template" type="text/x-jsrender">
40 <p>
41 <span class="subtitle">Transaction ID:</span>
42 <small>{{:tx_id}}</small>
43 </p>
44
45 <p>
46 <span class="subtitle">Merkle Root:</span>
47 <small>{{:merkle_root}}</small>
48 </p>
49</script>
50
51<script id="links-template" type="text/x-jsrender">
52 <div class="verify">
53 <p>Verify the blockchain info at:<br/>
54 <small><a href="https://blockexplorer.com/tx/{{:tx_id}}" target="_blank">https://blockexplorer.com/tx/{{:tx_id}}</a></small></p>
55 </div>
56
57 <div id="pdf-link">
58 <p><a href="/pdf/{{:recepit_id}}" target="_blank">View PDF receipt</a></p>
59 </div>
60</script>
61
62<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
63<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/0.9.80/jsrender.min.js"></script>
64<script src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.0.0.js"></script>
65<script src="/js/pubnub-verify.js"></script>
66<script>
67 $(document).ready(function() {
68
69 var pubnubConfig = {
70 subscribeKey: '<%= config.pubnub.subscribe_key %>'
71 };
72
73 var addBlockchainInfo = function(params1, params2, params3) {
74 var notice = $('#notice');
75 var info = $('#info');
76 var links = $('#links');
77
78 var template = $.templates('#message-template');
79 notice.html(template.render(params1));
80
81 template = $.templates('#blockchain-data-template');
82 info.append(template.render(params2));
83
84 template = $.templates('#links-template');
85 links.append(template.render(params3));
86 }
87
88 <% if (data.sent_to_blockchain) { %>
89 var params1 = {};
90
91 var params2 = {
92 tx_id: '<%= data.blockchain_receipt.anchors[0].sourceId %>',
93 merkle_root: '<%= data.blockchain_receipt.merkleRoot %>'
94 };
95
96 var params3 = {
97 tx_id: '<%= data.blockchain_receipt.anchors[0].sourceId %>',
98 recepit_id: '<%= data.id %>'
99 };
100
101 addBlockchainInfo(params1, params2, params3);
102 <% } else { %>
103
104 setupPubNub(pubnubConfig, '<%= data.hash %>', addBlockchainInfo);
105
106 <% } %>
107 });
108</script>
109
110</body>
111
112</html>
After the .container
div
, you can find the templates to add the blockchain information. If a user comes to this page after the receipt is generated, the information is added on page load. Otherwise, PubNub is set up with the function setupPubNub
, which allows it to listen for updates.
The code of this function can be found on public/js/pubnub-verify.js
:
1var setupPubNub = function(pubnubConfig, hash, callback) {
2 var pubnub = new PubNub(pubnubConfig);
3
4 pubnub.addListener({
5 message: function(data) {
6 console.log(data);
7
8 var params1 = {};
9
10 var params2 = {
11 tx_id: data.message.anchors[0].sourceId,
12 merkle_root: data.message.merkleRoot
13 };
14
15 var params3 = {
16 tx_id: data.message.anchors[0].sourceId,
17 recepit_id: data.message.id
18 };
19
20 var container = $(".container");
21
22 container.css("opacity", "0");
23
24 callback(params1, params2, params3);
25
26 container.animate(
27 {
28 opacity: 1
29 },
30 1500
31 );
32 }
33 });
34
35 pubnub.subscribe({
36 channels: [hash]
37 });
38};
Here, we're just subscribing to the channel with name equals to the hash we're verifying to add the blockchain information when a message is received.
This way, the page of a verified hash looks like the following:
And the receipt in PDF format looks like this:
The template for the PDF receipt is similar to this page, the only significant difference is the QR code generated with QRCode.js. To create a QR code, use the JavaScript below:
1<div id="qrcode"></div>
2
3<script src="js/qrcode.min.js" type="text/javascript"></script>
4<script type="text/javascript">
5var qrcode = new QRCode(document.getElementById("qrcode"), {
6 text: "https://blockexplorer.com/tx/:{{r.blockchain_receipt.anchors[0].sourceId}}",
7 width: 128,
8 height: 128,
9 colorDark : "#000000",
10 colorLight : "#ffffff",
11 correctLevel : QRCode.CorrectLevel.H
12});
13</script>
And that's it, the whole application. Remember that the entire code is on Github in case you want to review the files not covered here or if you missed something.
I guess you may be thinking, but what are these merkle root, transaction id, and proof values that Tierion is returning? How can all this act as a proof of existence?.
Well, first of all, the file's SHA256 digest is unique. You need to have the exact same file to generate the same hash. (Just try checking the hash of a file, and then adding a space to see how the new hash is completely different). This proves that the file existed at least as early as the time the transaction was confirmed. Otherwise, the hash we used would not have been generated.
About Tierion's receipt:
targetHash
value is the hash of the record you wish to verify.merkleRoot
is calculated and inserted into the blockchain.With a Merkle tree, you can prove that something belongs to a set, without having to store the whole set.
To validate a receipt, you must confirm that targetHash
is part of a Merkle Tree, and the tree's Merkle root has been published to a Bitcoin transaction.
Receipts generated from Tierion conform to the Chainpoint 2.0 standard. You can know more about this standard and how a receipt can be verified in this whitepaper, but in summary here's the procedure:
targetHash
and the first hash in the proof
array. The right or left designation specifies which side of the concatenation that the proof hash value should be on.proof
array, using the same left and right rules.proof
array.merkleRoot
value if the proof is valid, otherwise, the proof is invalid.If your targetHash
was the only element in the Merkle Tree, it would be equal to merkleRoot
.
Let's see an example. To verify the hash of this receipt (691b6e0b3db43e22466734f7ec4b15d17687d5d8a92212fb99691e454cb9d9c4):
1{
2 "@context": https://w3id.org/chainpoint/v2,
3 "anchors": [
4 {
5 "sourceId": "e8e92ff3efcb2660922e8a870a4ebcda54c41978f0db3374b4c5d31173f0d720" ,
6 "type": "BTCOpReturn"
7 }
8 ] ,
9 "merkleRoot": "a82c3e4b04bcdec045695129c50221b15eadd2bfff6b65533a17867c201bbde0" ,
10 "proof": [
11 {
12 "right": "8b6aafc21661aed7ff5a7e8fce857ca0d2f4cef576c1c47c454d58a58ce4c09c"
13 }
14 ] ,
15 "targetHash": "691b6e0b3db43e22466734f7ec4b15d17687d5d8a92212fb99691e454cb9d9c4" ,
16 "type": "ChainpointSHA256v2"
17}
We concatenate the targetHash
value with the right
value of proof
in the following way:
1691b6e0b3db43e22466734f7ec4b15d17687d5d8a92212fb99691e454cb9d9c48b6aafc21661aed7ff5a7e8fce857ca0d2f4cef576c1c47c454d58a58ce4c09c
Then, we get the SHA-256 hash of that string to verify that is equal to the merkleRoot
value.
Since proof
has one element, we only have to perform this operation. Otherwise, we'd have to repeat the process for each value of proof
concatenating it either to the left or right of the previously calculated hash.
The catch is that the hash generation is done with the binary values (as raw binary data buffers) of targetHash
and proof
, rather than with their hexadecimal string representations (the ones shown), which means that we can't use an online SHA256 calculator to perform the operations.
Luckily, Tierion provides the following tools to validate your receipts:
And here's a tip. If you really want to see the code that does the validation, take a look at the validateProof(proof, targetHash, merkleRoot) function of Tierion's library for working with Merkle trees.
Now that you've proven the value of the merkleRoot
, take the Bitcoin transaction ID (sourceId
property from the anchors
array with type BTCOpReturn
) to review the transaction on an online site like https://blockchain.info/ or https://blockexplorer.com/.
The transaction must include the merkleRoot
value in the OP_RETURN
output. If the value is in the output, the receipt is valid:
As we have seen, we can access the blockchain in an easy way using Tierion. Furthermore, we can benefit from the real-time features of RethinkDB and Pubnub to improve user experience and sync data. This proves once again that by combining the right APIs, you can build simple but powerful applications.
I hope you found this tutorial informative and entertaining. Thank you for reading. Please leave comments and feedback in the discussion section below. Please favorite this guide if you found it useful!