├── .gitignore
├── manage
├── .gitignore
├── contracts
│ ├── noop.tz
│ ├── contract.jsligo
│ └── metadata.json
├── .gitlab-ci.yml
├── package.json
├── .eslintrc.json
├── man
│ └── man1
│ │ └── tzstamp-manage.md
├── README.md
└── test
│ └── test.js
├── packages
├── helpers
│ ├── .gitignore
│ ├── dev_deps.ts
│ ├── mod.ts
│ ├── package-lock.json
│ ├── .gitlab-ci.yml
│ ├── readme.md
│ ├── hex.test.ts
│ ├── package.json
│ ├── license.txt
│ ├── bytes.ts
│ ├── hex.ts
│ ├── bytes.test.ts
│ ├── changelog.md
│ ├── sha256.test.ts
│ ├── _build.ts
│ ├── base58.test.ts
│ ├── blake2b.test.ts
│ ├── sha256.ts
│ ├── base58.ts
│ └── blake2b.ts
├── proof
│ ├── .gitignore
│ ├── mod.ts
│ ├── deps.ts
│ ├── dev_deps.ts
│ ├── .gitlab-ci.yml
│ ├── _build.ts
│ ├── package.json
│ ├── license.txt
│ ├── errors.test.ts
│ ├── errors.ts
│ ├── changelog.md
│ ├── schemas.ts
│ ├── readme.md
│ ├── operation.ts
│ └── operation.test.ts
├── tezos-merkle
│ ├── .gitignore
│ ├── mod.ts
│ ├── dev_deps.ts
│ ├── deps.ts
│ ├── _build.ts
│ ├── package.json
│ ├── license.txt
│ ├── _bench.ts
│ ├── changelog.md
│ ├── path.ts
│ ├── readme.md
│ ├── package-lock.json
│ ├── merkletree.test.ts
│ └── merkletree.ts
└── export-commonjs
│ ├── deps.ts
│ ├── readme.md
│ ├── license.txt
│ └── mod.ts
├── cli
├── .gitignore
├── src
│ ├── version.js
│ ├── resolve_module.js
│ ├── derive.js
│ ├── help.js
│ ├── manual_verify.js
│ ├── verify.js
│ ├── helpers.js
│ └── stamp.js
├── LICENSE.txt
├── .eslintrc.json
├── package.json
├── bin
│ └── index.js
├── README.md
├── man
│ └── man1
│ │ └── tzstamp.md
└── test
│ └── test.js
├── server
├── .gitignore
├── Dockerfile
├── .gitlab-ci.yml
├── package.json
├── tests
│ ├── tezos.test.js
│ ├── storage.test.js
│ └── aggregator.test.js
├── lib
│ ├── tezos.js
│ ├── storage.js
│ ├── aggregator.js
│ └── api.js
├── LICENSE.txt
├── .eslintrc.json
├── index.js
├── man
│ └── man1
│ │ └── tzstamp-server.md
└── README.md
├── website
├── Dockerfile
├── public
│ ├── favicon.ico
│ ├── logomark.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── mstile-150x150.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── tzstamp-chain-of-commitments.png
│ ├── browserconfig.xml
│ ├── site.webmanifest
│ ├── main.css
│ └── safari-pinned-tab.svg
├── README.md
├── CHANGELOG.md
└── LICENSE.txt
├── upgrade
├── readme.md
├── license.txt
└── tzstamp-upgrade.ts
├── .github
├── pull_request_template.md
└── workflows
│ ├── docker_build_server.yml
│ └── docker_build_web.yml
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 |
--------------------------------------------------------------------------------
/manage/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/packages/helpers/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
--------------------------------------------------------------------------------
/cli/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/packages/proof/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | proofs
3 | .env
4 | *key.json
5 |
--------------------------------------------------------------------------------
/website/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx
2 | COPY public /usr/share/nginx/html
--------------------------------------------------------------------------------
/packages/export-commonjs/deps.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/x/ts_morph@11.0.1/mod.ts";
2 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./merkletree.ts";
2 | export * from "./path.ts";
3 |
--------------------------------------------------------------------------------
/website/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/favicon.ico
--------------------------------------------------------------------------------
/website/public/logomark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/logomark.png
--------------------------------------------------------------------------------
/packages/proof/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./errors.ts";
2 | export * from "./operation.ts";
3 | export * from "./proof.ts";
4 |
--------------------------------------------------------------------------------
/website/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/favicon-16x16.png
--------------------------------------------------------------------------------
/website/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/favicon-32x32.png
--------------------------------------------------------------------------------
/website/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/mstile-150x150.png
--------------------------------------------------------------------------------
/website/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/website/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/website/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/website/public/tzstamp-chain-of-commitments.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marigold-dev/tzstamp/HEAD/website/public/tzstamp-chain-of-commitments.png
--------------------------------------------------------------------------------
/packages/helpers/dev_deps.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/std@0.97.0/testing/asserts.ts";
2 | export * from "https://deno.land/std@0.97.0/path/mod.ts";
3 |
--------------------------------------------------------------------------------
/manage/contracts/noop.tz:
--------------------------------------------------------------------------------
1 | { parameter bytes ;
2 | storage (pair (big_map %metadata string bytes) (address %owner)) ;
3 | code { CDR ; NIL operation ; PAIR } }
4 |
--------------------------------------------------------------------------------
/packages/proof/deps.ts:
--------------------------------------------------------------------------------
1 | export * from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/helpers/mod.ts";
2 | export * from "https://deno.land/x/jtd@v0.1.0/mod.ts";
3 |
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.13-alpine
2 | WORKDIR /app
3 | COPY ["package.json", "package-lock.json*", "./"]
4 | RUN npm install
5 | COPY . .
6 | EXPOSE 8000
7 | CMD ["node", "index.js"]
8 |
--------------------------------------------------------------------------------
/packages/helpers/mod.ts:
--------------------------------------------------------------------------------
1 | export * as Hex from "./hex.ts";
2 | export * as Base58 from "./base58.ts";
3 | export { Blake2b } from "./blake2b.ts";
4 | export { Sha256 } from "./sha256.ts";
5 | export { compare, concat } from "./bytes.ts";
6 |
--------------------------------------------------------------------------------
/packages/proof/dev_deps.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/std@0.97.0/testing/asserts.ts";
2 | export * from "https://deno.land/std@0.97.0/path/mod.ts";
3 | export * from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/export-commonjs/mod.ts";
4 |
--------------------------------------------------------------------------------
/server/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: node:16.3
2 |
3 | stages:
4 | - test
5 |
6 | test:
7 | stage: test
8 | before_script:
9 | - npm ci
10 | - mv $TESTNET_FAUCET_KEY key.json
11 | script:
12 | - npm run lint
13 | - npm run test
14 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # TzStamp Website
2 |
3 | This is the code for the static front end to the [tzstamp.io](https://tzstamp.io) website.
4 | It's a transitional application that is planned to be replaced with a progressive web app.
5 |
6 | ## License
7 |
8 | MIT
9 |
--------------------------------------------------------------------------------
/packages/helpers/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tzstamp/helpers",
3 | "version": "0.3.4",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@tzstamp/helpers",
9 | "version": "0.3.4",
10 | "license": "MIT"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/website/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/helpers/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: denoland/deno:1.10.3
2 |
3 | stages:
4 | - test
5 | - build
6 |
7 | test:
8 | stage: test
9 | script:
10 | - deno lint *.ts
11 | - deno test
12 |
13 | build:
14 | stage: build
15 | script:
16 | - deno run --unstable --allow-read --allow-write _build.ts
17 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/dev_deps.ts:
--------------------------------------------------------------------------------
1 | export * from "https://deno.land/std@0.99.0/path/mod.ts";
2 | export * from "https://deno.land/std@0.99.0/testing/asserts.ts";
3 | export * from "https://deno.land/std@0.99.0/testing/bench.ts";
4 | export * from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/export-commonjs/mod.ts";
5 |
--------------------------------------------------------------------------------
/cli/src/version.js:
--------------------------------------------------------------------------------
1 | const { version } = require('../package.json')
2 |
3 | async function handler () {
4 | console.log(`TzStamp CLI ${version}`)
5 | process.exit(2)
6 | }
7 |
8 | module.exports = {
9 | handler,
10 | title: 'Version',
11 | description: 'Prints the installed version.',
12 | usage: 'tzstamp version'
13 | }
14 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/deps.ts:
--------------------------------------------------------------------------------
1 | export {
2 | Blake2b,
3 | concat,
4 | Hex,
5 | } from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/helpers/mod.ts";
6 | export {
7 | Blake2bOperation,
8 | JoinOperation,
9 | Operation,
10 | Proof,
11 | } from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/proof/mod.ts";
12 |
--------------------------------------------------------------------------------
/manage/contracts/contract.jsligo:
--------------------------------------------------------------------------------
1 | type storage = {
2 | metadata: big_map,
3 | owner: address
4 | };
5 |
6 | type parameter = bytes;
7 |
8 | type ret = [list, storage];
9 |
10 | /* Main access point that dispatches to the entrypoints according to
11 | the smart contract parameter. */
12 | let main = ([action, storage]: [parameter, storage]) : ret => {
13 | return [
14 | (list([]) as list ), // No operations
15 | storage
16 | ]
17 | };
18 |
--------------------------------------------------------------------------------
/manage/contracts/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TzStamp",
3 | "description": "Trusted timestamping on the Tezos blockchain",
4 | "version": "1.0.0",
5 | "authors": ["Marigold"],
6 | "license": {
7 | "name": "MIT",
8 | "details": "MIT License"
9 | },
10 | "source": {
11 | "tools": ["Ligo"],
12 | "location": "https://github.com/marigold-dev/tzstamp/blob/main/manage/contracts"
13 | },
14 | "homepage": "https://tzstamp.io/",
15 | "interfaces": [
16 | "TZIP-16"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/website/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/website/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | This project adheres to [Calendar Versioning](https://calver.org), scheme
6 | `YY.0W.MICRO`
7 |
8 | ## [21.04.0] - 2021-01-27
9 |
10 | ### Added
11 |
12 | - Usage instructions
13 | - File submission (SHA-256 hashed before sending)
14 | - Direct SHA-256 hash submission
15 | - Outputting pending proof endpoints
16 | - RESTful API documentation page
17 |
18 | [21.04.0]: https://web.archive.org/web/20210127001029/https://tzstamp.io
19 |
--------------------------------------------------------------------------------
/manage/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: node:latest
2 |
3 | stages:
4 | - build
5 | - lint
6 | - test
7 |
8 | cache:
9 | paths:
10 | - .npm/
11 | - .server/
12 |
13 | build:
14 | stage: build
15 | script:
16 | - npm ci --cache .npm --prefer-offline
17 |
18 | lint:
19 | stage: lint
20 | script:
21 | - npx eslint index.js
22 | - npx eslint test/test.js
23 |
24 | test:
25 | stage: test
26 | before_script:
27 | - npm ci --cache .npm --prefer-offline
28 | - mv $TESTNET_FAUCET faucet.json
29 | script:
30 | - npm test
31 |
--------------------------------------------------------------------------------
/cli/src/resolve_module.js:
--------------------------------------------------------------------------------
1 | const modules = new Map([
2 | [ 'help', () => require('./help') ],
3 | [ 'version', () => require('./version') ],
4 | [ 'stamp', () => require('./stamp') ],
5 | [ 'verify', () => require('./verify') ],
6 | [ 'manual-verify', () => require('./manual_verify') ],
7 | [ 'derive', () => require('./derive') ]
8 | ])
9 |
10 | function resolveModule (subcommand) {
11 | const mod = modules.get(subcommand)
12 | if (!mod) {
13 | return null
14 | }
15 | return mod()
16 | }
17 |
18 | module.exports = {
19 | modules,
20 | resolveModule
21 | }
22 |
--------------------------------------------------------------------------------
/packages/proof/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: denoland/deno:1.11.0
2 |
3 | stages:
4 | - test
5 | - setup
6 | - build
7 |
8 | cache:
9 | paths:
10 | - .npm/
11 | - node_modules/
12 |
13 | setup:
14 | stage: setup
15 | image: node:16-alpine
16 | script:
17 | - npm ci --cache .npm --prefer-offline
18 |
19 | test:
20 | stage: test
21 | script:
22 | - deno lint --ignore=node_modules,dist
23 | - deno test --unstable --allow-net *.test.ts
24 |
25 | build:
26 | stage: build
27 | script:
28 | - deno run --unstable --allow-read=. --allow-write=dist _build.ts
29 |
--------------------------------------------------------------------------------
/upgrade/readme.md:
--------------------------------------------------------------------------------
1 | # TzStamp Proof Upgrade Utility
2 |
3 | Upgrade deprecated [TzStamp] proofs to a supported version.
4 |
5 | ## Usage
6 |
7 | Install with [Deno]:
8 |
9 | ```bash
10 | deno install --allow-read https://raw.githubusercontent.com/marigold-dev/tzstamp/main/upgrade/tzstamp-upgrade.ts
11 | ```
12 |
13 | Version 0 to Version 1:
14 |
15 | ```bash
16 | tzstamp-upgrade --version 0 --timestamp --proof [--file | --hash ] > upgraded.proof.json
17 | ```
18 |
19 | ## License
20 |
21 | [MIT](license.txt)
22 |
23 | [TzStamp]: http://tzstamp.io/
24 | [Deno]: https://deno.land/
25 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/_build.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S deno run --unstable --allow-read=. --allow-write=dist
2 |
3 | import { exportCommonJS } from "./dev_deps.ts";
4 |
5 | await exportCommonJS({
6 | filePaths: [
7 | "deps.ts",
8 | "mod.ts",
9 | "merkletree.ts",
10 | "path.ts",
11 | ],
12 | outDir: "dist",
13 | dependencyMap: new Map([
14 | [
15 | "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/helpers/mod.ts",
16 | "@tzstamp/helpers",
17 | ],
18 | [
19 | "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/proof/mod.ts",
20 | "@tzstamp/proof",
21 | ],
22 | ]),
23 | });
24 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | ## Depends
9 |
10 |
11 | - [ ] #100
12 |
13 | ## Problem
14 |
15 |
16 |
17 | ## Solution
18 |
19 |
20 |
21 |
22 | ## Related
23 |
24 |
25 | - #99
--------------------------------------------------------------------------------
/packages/proof/_build.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S deno run --unstable --allow-read=. --allow-write=dist
2 |
3 | import { exportCommonJS } from "./dev_deps.ts";
4 |
5 | await exportCommonJS({
6 | filePaths: [
7 | "deps.ts",
8 | "mod.ts",
9 | "operation.ts",
10 | "proof.ts",
11 | "errors.ts",
12 | "schemas.ts",
13 | ],
14 | dependencyMap: new Map([
15 | ["https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/helpers/mod.ts", "@tzstamp/helpers"],
16 | ["https://deno.land/x/jtd@v0.1.0/mod.ts", "jtd"],
17 | ]),
18 | shims: [{
19 | defaultImport: "fetch",
20 | moduleSpecifier: "node-fetch",
21 | }],
22 | outDir: "dist",
23 | });
24 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "0.3.4",
4 | "main": "index.js",
5 | "scripts": {
6 | "start": "node index.js",
7 | "test": "npx jest",
8 | "lint": "npx eslint index.js lib/**/*.js --fix"
9 | },
10 | "license": "MIT",
11 | "dependencies": {
12 | "@koa/cors": "^3.1.0",
13 | "@koa/router": "^10.0.0",
14 | "@taquito/signer": "^12.1.0",
15 | "@taquito/taquito": "^12.1.0",
16 | "@tzstamp/helpers": "^0.3.4",
17 | "@tzstamp/proof": "^0.3.4",
18 | "@tzstamp/tezos-merkle": "^0.3.4",
19 | "axios": "^0.27.2",
20 | "cron": "^1.8.2",
21 | "dotenv": "^10.0.0",
22 | "koa": "^2.13.1",
23 | "koa-bodyparser": "^4.3.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/manage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tzstamp-manage",
3 | "version": "0.3.4",
4 | "description": "Manages instances of the tzstamp tezos smart contract.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "FAUCET_PATH=faucet.json node test/test.js"
8 | },
9 | "author": "John David Pressman",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@taquito/michel-codec": "^15.0.0",
13 | "@taquito/signer": "^15.0.0",
14 | "@taquito/taquito": "^15.0.0",
15 | "@taquito/tzip16": "^15.0.1",
16 | "@tzstamp/helpers": "^0.3.4",
17 | "dotenv": "^8.2.0",
18 | "dotenv-defaults": "^2.0.1",
19 | "minimist": "^1.2.5",
20 | "node-fetch": "^2.6.1"
21 | },
22 | "devDependencies": {
23 | "eslint": "^7.25.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/export-commonjs/readme.md:
--------------------------------------------------------------------------------
1 | # Export Deno Project as CommonJS
2 |
3 | Example usage:
4 |
5 | ```js
6 | import { exportCommonJS } from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/export-commonjs/mod.ts";
7 |
8 | await exportCommonJS({
9 | filePaths: [
10 | "mod.ts",
11 | "deps.ts",
12 | "src/**/*.ts",
13 | ],
14 | outDir: "dist",
15 | clean: true, // Delete contents of "dist" before export
16 | dependencyMap: new Map([ // Substitute dependencies
17 | [
18 | "https://deno.land/std@0.100.0/fs/mod.ts",
19 | "fs",
20 | ],
21 | ]),
22 | shims: [ // Inject shims for Deno built-in
23 | {
24 | moduleSpecifier: "node-fetch",
25 | defaultImport: "fetch",
26 | },
27 | ],
28 | });
29 | ```
30 |
31 | ## License
32 |
33 | [MIT](license.txt)
34 |
--------------------------------------------------------------------------------
/packages/helpers/readme.md:
--------------------------------------------------------------------------------
1 | # TzStamp Helper Functions
2 |
3 | Helper functions for [TzStamp] tools.
4 |
5 | ## Usage
6 |
7 | ```js
8 | // Deno
9 | import { ... } from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/helpers/mod.ts";
10 |
11 | // Node w/ NPM
12 | const { ... } = require("@tzstamp/helpers");
13 |
14 | // Browser w/ UNPKG
15 | import { ... } from "https://unpkg.com/@tzstamp/helpers/dist/bundle.js"
16 | ```
17 |
18 | See the [reference documentation] for details on exported helpers.
19 |
20 | ## Build
21 |
22 | ```sh
23 | deno run --unstable --allow-read=. --allow-write=dist _build.ts
24 | ```
25 |
26 | ## License
27 |
28 | [MIT](license.txt)
29 |
30 | [TzStamp]: https://tzstamp.io
31 | [reference documentation]: https://doc.deno.land/https/raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/helpers/mod.ts
32 |
--------------------------------------------------------------------------------
/packages/helpers/hex.test.ts:
--------------------------------------------------------------------------------
1 | import * as Hex from "./hex.ts";
2 | import { assertEquals, assertThrows } from "./dev_deps.ts";
3 |
4 | Deno.test({
5 | name: "Encode byte array as hex string",
6 | fn() {
7 | // Encode filled byte array
8 | const bytes = new Uint8Array([0, 1, 2]);
9 | assertEquals(Hex.stringify(bytes), "000102");
10 |
11 | // Encode empty byte array
12 | const empty = new Uint8Array([]);
13 | assertEquals(Hex.stringify(empty), "");
14 | },
15 | });
16 |
17 | Deno.test({
18 | name: "Parse byte array from hex string",
19 | fn() {
20 | // Parse odd-length valid hex string
21 | assertEquals(Hex.parse("5f309"), new Uint8Array([5, 243, 9]));
22 |
23 | // Parse invalid hex string
24 | assertThrows(() => Hex.parse("not a hex string"), SyntaxError);
25 |
26 | // Parse empty string
27 | assertEquals(Hex.parse(""), new Uint8Array([]));
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/packages/helpers/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tzstamp/helpers",
3 | "version": "0.3.4",
4 | "description": "TzStamp helper functions",
5 | "files": [
6 | "dist"
7 | ],
8 | "main": "dist/index.js",
9 | "types": "dist/index.d.ts",
10 | "scripts": {
11 | "test": "deno test",
12 | "build": "deno run --unstable --allow-read=. --allow-write=dist _build.ts"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/marigold-dev/tzstamp.git",
17 | "directory": "helpers"
18 | },
19 | "contributors": [
20 | "John David Pressman (https://jdpressman.com)",
21 | "Benjamin Herman (https://metanomial.com)"
22 | ],
23 | "bugs": {
24 | "url": "https://github.com/marigold-dev/tzstamp/issues"
25 | },
26 | "homepage": "https://github.com/marigold-dev/tzstamp/tree/main/helpers#readme",
27 | "license": "MIT"
28 | }
29 |
--------------------------------------------------------------------------------
/server/tests/tezos.test.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const { configureTezosClient } = require('../lib/tezos')
3 | const { randomBytes } = require('crypto')
4 | const { Base58 } = require('@tzstamp/helpers')
5 |
6 | const {
7 | RPC_URL: rpcURL = 'https://testnet-tezos.giganode.io/',
8 | KEY_FILE: keyFile
9 | } = process.env
10 |
11 | describe('Configure a tezos client', () => {
12 | test('with a faucet key', async () => {
13 | const client = await configureTezosClient(
14 | undefined,
15 | keyFile,
16 | rpcURL
17 | )
18 | expect(client.rpc.url).toBe(rpcURL)
19 | })
20 | test('with a secret key', async () => {
21 | const key = Base58.encodeCheck(
22 | randomBytes(32),
23 | new Uint8Array([ 13, 15, 58, 7 ])
24 | )
25 | await configureTezosClient(key, undefined, rpcURL)
26 | })
27 | test('without any key', async () => {
28 | await expect(async () => {
29 | await configureTezosClient(undefined, undefined, rpcURL)
30 | }).rejects.toThrow()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tzstamp/tezos-merkle",
3 | "version": "0.3.4",
4 | "description": "Tezos-style Merkle trees",
5 | "main": "dist/mod.js",
6 | "types": "dist/mod.d.ts",
7 | "files": [
8 | "src",
9 | "dist"
10 | ],
11 | "scripts": {
12 | "test": "deno test --unstable *.test.ts",
13 | "build": "deno run --unstable --allow-read=. --allow-write=dist _build.ts"
14 | },
15 | "contributors": [
16 | "John David Pressman (https://jdpressman.com)",
17 | "Benjamin Herman (https://metanomial.com)"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/marigold-dev/tzstamp.git",
22 | "directory": "tezos-merkle"
23 | },
24 | "homepage": "https://github.com/marigold-dev/tzstamp/tree/main/tezos-merkle#readme",
25 | "license": "MIT",
26 | "dependencies": {
27 | "@tzstamp/helpers": "^0.3.4",
28 | "@tzstamp/proof": "^0.3.4"
29 | },
30 | "devDependencies": {
31 | "@types/node": "^15.0.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/proof/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tzstamp/proof",
3 | "version": "0.3.4",
4 | "description": "Cryptographic proofs for TzStamp tools",
5 | "files": [
6 | "dist"
7 | ],
8 | "main": "dist/mod.js",
9 | "types": "dist/mod.d.ts",
10 | "scripts": {
11 | "build": "deno run --unstable --allow-read=. --allow-write=dist _build.ts",
12 | "test": "deno test --unstable --allow-net *.test.ts"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/marigold-dev/tzstamp.git",
17 | "directory": "proof"
18 | },
19 | "contributors": [
20 | "John David Pressman (https://jdpressman.com)",
21 | "Benjamin Herman (https://metanomial.com)"
22 | ],
23 | "homepage": "https://github.com/marigold-dev/tzstamp/tree/main/proof#readme",
24 | "license": "MIT",
25 | "dependencies": {
26 | "@tzstamp/helpers": "^0.3.4",
27 | "jtd": "^0.1.1",
28 | "node-fetch": "^2.6.1"
29 | },
30 | "devDependencies": {
31 | "@types/node-fetch": "^2.5.10"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/tests/storage.test.js:
--------------------------------------------------------------------------------
1 | const { tmpdir } = require('os')
2 | const { join, sep } = require('path')
3 | const { ProofStorage } = require('../lib/storage')
4 | const { randomUUID } = require('crypto')
5 | const { Proof } = require('@tzstamp/proof')
6 | const { stat } = require('fs/promises')
7 |
8 | const dir = join(tmpdir(), 'tzstamp-' + randomUUID())
9 | const proof1 = new Proof({
10 | hash: new Uint8Array([ 1 ]),
11 | operations: []
12 | })
13 | let storage
14 |
15 | describe('Use proof storage', () => {
16 | test('Instantiate', () => {
17 | storage = new ProofStorage(dir)
18 | })
19 | test('Calculate a proof path', () => {
20 | expect(storage.path('foo')).toBe(`${dir}${sep}foo.proof.json`)
21 | })
22 | test('Store a proof', async () => {
23 | await storage.storeProof(proof1, 'foo')
24 | const fooStat = await stat(storage.path('foo'))
25 | expect(fooStat.isFile())
26 | })
27 | test('Get a proof', async () => {
28 | const proof2 = await storage.getProof('foo')
29 | expect(proof2).toEqual(proof1)
30 | await expect(() => storage.getProof('bar')).rejects.toThrow()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Marigold
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/upgrade/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Benjamin Herman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/server/lib/tezos.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises')
2 | const { TezosToolkit } = require('@taquito/taquito')
3 | const { InMemorySigner, importKey } = require('@taquito/signer')
4 |
5 | /**
6 | * Configures a Tezos client
7 | *
8 | * @param {string | undefined} secret
9 | * @param {string | undefined} keyFile
10 | * @param {string} rpcURL
11 | */
12 | exports.configureTezosClient = async function (secret, keyFile, rpcURL) {
13 | const client = new TezosToolkit(rpcURL)
14 | if (secret != undefined) {
15 | console.log('Configuring local signer')
16 | const signer = await InMemorySigner.fromSecretKey(secret)
17 | client.setProvider({ signer })
18 | } else if (keyFile != undefined) {
19 | console.log('Importing key file')
20 | const contents = await fs.readFile(keyFile, 'utf-8')
21 | const key = JSON.parse(contents)
22 | await importKey(
23 | client,
24 | key.email,
25 | key.password,
26 | key.mnemonic.join(' '),
27 | key.secret
28 | )
29 | } else {
30 | throw new Error('Either the KEY_FILE or SECRET environment variable must be set')
31 | }
32 | return client
33 | }
34 |
--------------------------------------------------------------------------------
/cli/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 John David Pressman, Benjamin Herman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/export-commonjs/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Benjamin Herman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/proof/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 John David Pressman, Benjamin Herman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/server/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020-2021 John David Pressman, Benjamin Herman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/website/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020-2021 John David Pressman, Benjamin Herman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 John David Pressman, Benjamin Herman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/cli/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es2021": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 2021
8 | },
9 | "extends": "eslint:recommended",
10 | "rules": {
11 | "indent": [ "error", 2, { "SwitchCase": 1 } ],
12 | "max-len": [ "error", { "code": 120 } ],
13 | "camelcase": "error",
14 | "new-cap": "error",
15 | "no-var": "error",
16 | "keyword-spacing": "error",
17 | "key-spacing": "error",
18 | "comma-style": "error",
19 | "comma-spacing": "error",
20 | "quotes": [ "error", "single" ],
21 | "block-spacing": "error",
22 | "array-bracket-spacing": [ "error", "always" ],
23 | "no-trailing-spaces": "error",
24 | "no-multiple-empty-lines": [ "error", { "max": 1 } ],
25 | "eol-last": "error",
26 | "function-paren-newline": [ "error", "multiline-arguments" ],
27 | "space-before-function-paren": "error",
28 | "brace-style": "error",
29 | "curly": "error",
30 | "comma-dangle": "error",
31 | "semi": [ "error", "never" ],
32 | "one-var": [ "error", "never" ],
33 | "max-statements-per-line": "error",
34 | "func-call-spacing": "error"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/manage/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es2021": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 2021
8 | },
9 | "extends": "eslint:recommended",
10 | "rules": {
11 | "indent": [ "error", 2, { "SwitchCase": 1 } ],
12 | "max-len": [ "error", { "code": 120 } ],
13 | "camelcase": "error",
14 | "new-cap": "error",
15 | "no-var": "error",
16 | "keyword-spacing": "error",
17 | "key-spacing": "error",
18 | "comma-style": "error",
19 | "comma-spacing": "error",
20 | "quotes": [ "error", "single" ],
21 | "block-spacing": "error",
22 | "array-bracket-spacing": [ "error", "always" ],
23 | "no-trailing-spaces": "error",
24 | "no-multiple-empty-lines": [ "error", { "max": 1 } ],
25 | "eol-last": "error",
26 | "function-paren-newline": [ "error", "multiline-arguments" ],
27 | "space-before-function-paren": "error",
28 | "brace-style": "error",
29 | "curly": "error",
30 | "comma-dangle": "error",
31 | "semi": [ "error", "never" ],
32 | "one-var": [ "error", "never" ],
33 | "max-statements-per-line": "error",
34 | "func-call-spacing": "error"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es2021": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 2021
8 | },
9 | "extends": "eslint:recommended",
10 | "rules": {
11 | "indent": [ "error", 2, { "SwitchCase": 1 } ],
12 | "max-len": [ "error", { "code": 120 } ],
13 | "camelcase": "error",
14 | "new-cap": "error",
15 | "no-var": "error",
16 | "keyword-spacing": "error",
17 | "key-spacing": "error",
18 | "comma-style": "error",
19 | "comma-spacing": "error",
20 | "quotes": [ "error", "single" ],
21 | "block-spacing": "error",
22 | "array-bracket-spacing": [ "error", "always" ],
23 | "no-trailing-spaces": "error",
24 | "no-multiple-empty-lines": [ "error", { "max": 1 } ],
25 | "eol-last": "error",
26 | "function-paren-newline": [ "error", "multiline-arguments" ],
27 | "space-before-function-paren": "error",
28 | "brace-style": "error",
29 | "curly": "error",
30 | "comma-dangle": "error",
31 | "semi": [ "error", "never" ],
32 | "one-var": [ "error", "never" ],
33 | "max-statements-per-line": "error",
34 | "func-call-spacing": "error"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/proof/errors.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FetchError,
3 | InvalidTezosNetworkError,
4 | MismatchedHashError,
5 | UnsupportedVersionError,
6 | } from "./errors.ts";
7 | import { assertEquals } from "./dev_deps.ts";
8 |
9 | Deno.test({
10 | name: "Unsupported version error",
11 | fn() {
12 | const error = new UnsupportedVersionError(14, "message");
13 | assertEquals(error.toString(), "UnsupportedVersionError: message");
14 | assertEquals(error.version, 14);
15 | },
16 | });
17 |
18 | Deno.test({
19 | name: "Mismatched hash error",
20 | fn() {
21 | const error = new MismatchedHashError("message");
22 | assertEquals(error.toString(), "MismatchedHashError: message");
23 | },
24 | });
25 |
26 | Deno.test({
27 | name: "Invalid Tezos network error",
28 | fn() {
29 | const error = new InvalidTezosNetworkError("message");
30 | assertEquals(error.toString(), "InvalidTezosNetworkError: message");
31 | },
32 | });
33 |
34 | Deno.test({
35 | name: "Fetch error",
36 | fn() {
37 | const error = new FetchError(404, "message");
38 | assertEquals(error.toString(), "FetchError: message");
39 | assertEquals(error.status, 404);
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/packages/helpers/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 John David Pressman, Benjamin Herman
2 | Modified work Copyright (c) 2017 Mathias Buus
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/packages/proof/errors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unsupported proof version error
3 | */
4 | export class UnsupportedVersionError extends Error {
5 | name = "UnsupportedVersionError";
6 |
7 | /**
8 | * Proof version
9 | */
10 | version: number;
11 |
12 | /**
13 | * @param version Proof version
14 | * @param message Optional error message
15 | */
16 | constructor(version: number, message?: string) {
17 | super(message);
18 | this.version = version;
19 | }
20 | }
21 |
22 | /**
23 | * Mismatched hash error
24 | */
25 | export class MismatchedHashError extends Error {
26 | name = "MismatchedHashError";
27 | }
28 |
29 | /**
30 | * Invalid Tezos network identifier error
31 | */
32 | export class InvalidTezosNetworkError extends Error {
33 | name = "InvalidTezosNetworkError";
34 | }
35 |
36 | /**
37 | * Fetch error
38 | */
39 | export class FetchError extends Error {
40 | name = "FetchError";
41 |
42 | /**
43 | * HTTP status code
44 | */
45 | status: number;
46 |
47 | /**
48 | * @param status HTTP status code
49 | * @param message Optional error message
50 | */
51 | constructor(status: number, message?: string) {
52 | super(message);
53 | this.status = status;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/.github/workflows/docker_build_server.yml:
--------------------------------------------------------------------------------
1 | name: Create Server Docker Images
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | required: true
8 | description: "Version tag"
9 |
10 | jobs:
11 | docker:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 |
17 | - name: Set up Docker Buildx
18 | uses: docker/setup-buildx-action@v2
19 |
20 | - name: Login to GitHub Container Registry
21 | uses: docker/login-action@v2
22 | with:
23 | registry: ghcr.io
24 | username: ${{ github.repository_owner }}
25 | password: ${{ secrets.GITHUB_TOKEN }}
26 |
27 | - name: Build and push
28 | id: docker_build_tzstamp_server
29 | uses: docker/build-push-action@v3
30 | with:
31 | context: ./server/
32 | platforms: linux/amd64
33 | cache-from: type=gha
34 | cache-to: type=gha,mode=max
35 | # Only push if on main branch
36 | push: ${{ github.ref == 'refs/heads/main' }}
37 | tags: |
38 | ghcr.io/marigold-dev/tzstamp_server:latest
39 | ghcr.io/marigold-dev/tzstamp_server:${{ github.event.inputs.version }}
40 |
--------------------------------------------------------------------------------
/.github/workflows/docker_build_web.yml:
--------------------------------------------------------------------------------
1 | name: Create Website Docker Images
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | required: true
8 | description: "Version tag"
9 |
10 | jobs:
11 | docker:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 |
17 | - name: Set up Docker Buildx
18 | uses: docker/setup-buildx-action@v2
19 |
20 | - name: Login to GitHub Container Registry
21 | uses: docker/login-action@v2
22 | with:
23 | registry: ghcr.io
24 | username: ${{ github.repository_owner }}
25 | password: ${{ secrets.GITHUB_TOKEN }}
26 |
27 | - name: Build and push
28 | id: docker_build_tzstamp_website
29 | uses: docker/build-push-action@v3
30 | with:
31 | context: ./website/
32 | platforms: linux/amd64
33 | cache-from: type=gha
34 | cache-to: type=gha,mode=max
35 | # Only push if on main branch
36 | push: ${{ github.ref == 'refs/heads/main' }}
37 | tags: |
38 | ghcr.io/marigold-dev/tzstamp_website:latest
39 | ghcr.io/marigold-dev/tzstamp_website:${{ github.event.inputs.version }}
40 |
--------------------------------------------------------------------------------
/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tzstamp/cli",
3 | "version": "0.3.4",
4 | "description": "Tezos timestamping utility",
5 | "main": "bin/index.js",
6 | "bin": {
7 | "tzstamp": "./bin/index.js"
8 | },
9 | "files": [
10 | "bin",
11 | "src",
12 | "man"
13 | ],
14 | "scripts": {
15 | "lint": "npx --yes eslint **/*.js --fix",
16 | "test": "node test/test.js",
17 | "build": "npx pkg . --bytecode false"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/marigold-dev/tzstamp.git",
22 | "directory": "cli"
23 | },
24 | "keywords": [
25 | "timestamp",
26 | "timestamping",
27 | "tezos",
28 | "cli"
29 | ],
30 | "contributors": [
31 | "John David Pressman",
32 | "Benjamin Herman (https://metanomial.com)"
33 | ],
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/marigold-dev/tzstamp/issues"
37 | },
38 | "homepage": "https://github.com/marigold-dev/tzstamp/tree/main/cli#readme",
39 | "dependencies": {
40 | "@tzstamp/helpers": "^0.3.4",
41 | "@tzstamp/proof": "^0.3.4",
42 | "chalk": "^4.1.1",
43 | "delay": "^5.0.0",
44 | "minimist": "^1.2.5",
45 | "node-fetch": "^2.6.1"
46 | },
47 | "pkg": {
48 | "targets": [
49 | "node16-linux-x64",
50 | "node16-macos-x64",
51 | "node16-win-x64"
52 | ],
53 | "outputPath": "dist"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/server/lib/storage.js:
--------------------------------------------------------------------------------
1 | const fsSync = require('fs')
2 | const fs = require('fs/promises')
3 | const { Proof } = require('@tzstamp/proof')
4 |
5 | /**
6 | * Proof storage
7 | */
8 | class ProofStorage {
9 |
10 | /**
11 | * @param {string} directory Proof directory path
12 | */
13 | constructor (directory) {
14 | this.directory = directory
15 | fsSync.mkdirSync(directory, { recursive: true })
16 | }
17 |
18 | /**
19 | * Gets the storage path for a given proof identifier.
20 | *
21 | * @param {string} proofId Proof identifier
22 | */
23 | path (proofId) {
24 | return `${this.directory}/${proofId}.proof.json`
25 | }
26 |
27 | /**
28 | * Reads a proof from storage.
29 | *
30 | * @param {string} proofId Proof identifier
31 | */
32 | async getProof (proofId) {
33 | const contents = await fs.readFile(
34 | this.path(proofId)
35 | )
36 | const template = JSON.parse(contents)
37 | return Proof.from(template)
38 | }
39 |
40 | /**
41 | * Serializes and writes a proof to storage.
42 | *
43 | * @param {Proof} proof Proof instance
44 | * @param {string} proofId Proof identifier
45 | */
46 | async storeProof (proof, proofId) {
47 | try {
48 | await fs.stat(this.path(proofId))
49 | } catch (error) {
50 | await fs.writeFile(
51 | this.path(proofId),
52 | JSON.stringify(proof)
53 | )
54 | }
55 | }
56 | }
57 |
58 | module.exports = {
59 | ProofStorage
60 | }
61 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/_bench.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S deno run
2 |
3 | import { MerkleTree } from "./mod.ts";
4 | import { Blake2b } from "./deps.ts";
5 | import { bench, BenchmarkTimer, runBenchmarks } from "./dev_deps.ts";
6 |
7 | bench({
8 | name: "Hash one hundred 32-byte blocks",
9 | func: nHashes(100),
10 | });
11 |
12 | bench({
13 | name: "Hash ten thousand 32-byte blocks",
14 | func: nHashes(10_000),
15 | });
16 |
17 | bench({
18 | name: "Hash one million 32-byte blocks",
19 | func: nHashes(1_000_000),
20 | });
21 |
22 | function nHashes(n: number) {
23 | return (timer: BenchmarkTimer) => {
24 | timer.start();
25 | for (let i = 0; i < n; ++i) {
26 | const block = crypto.getRandomValues(new Uint8Array(32));
27 | Blake2b.digest(block);
28 | }
29 | timer.stop();
30 | };
31 | }
32 |
33 | bench({
34 | name: "Append one hundred 32-byte blocks",
35 | func: nAppends(100),
36 | });
37 |
38 | bench({
39 | name: "Append ten thousand 32-byte blocks",
40 | func: nAppends(10_000),
41 | });
42 |
43 | bench({
44 | name: "Append one million 32-byte blocks",
45 | func: nAppends(1_000_000),
46 | });
47 |
48 | function nAppends(n: number) {
49 | const merkleTree = new MerkleTree();
50 | return (timer: BenchmarkTimer) => {
51 | timer.start();
52 | for (let i = 0; i < n; ++i) {
53 | const block = crypto.getRandomValues(new Uint8Array(32));
54 | merkleTree.append(block);
55 | }
56 | timer.stop();
57 | };
58 | }
59 |
60 | runBenchmarks();
61 |
--------------------------------------------------------------------------------
/packages/helpers/bytes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Compares two byte arrays.
3 | *
4 | * ```js
5 | * compare(
6 | * new Uint8Array([104, 101, 108, 108, 111]),
7 | * new TextEncoder().encode("hello")
8 | * );
9 | * // true
10 | * ```
11 | */
12 | export function compare(a: Uint8Array, b: Uint8Array): boolean {
13 | // Mismatched length
14 | if (a.length != b.length) {
15 | return false;
16 | }
17 |
18 | // Mismatched bytes
19 | for (const index in a) {
20 | if (a[index] != b[index]) {
21 | return false;
22 | }
23 | }
24 |
25 | return true;
26 | }
27 |
28 | /**
29 | * Concatenates numbers or byte arrays into a single byte array.
30 | * Numbers out of the range [0, 256) will wrap.
31 | *
32 | * ```js
33 | * concat(
34 | * new Uint8Array([1, 2, 3]),
35 | * 4,
36 | * new Uint8Array([5, 6]),
37 | * );
38 | * // Uint8Array (6) [ 1, 2, 3, 4, 5, 6 ]
39 | * ```
40 | */
41 | export function concat(...chunks: (number | Uint8Array)[]): Uint8Array {
42 | // Calculate size of resulting array
43 | let size = 0;
44 | for (const piece of chunks) {
45 | size += piece instanceof Uint8Array ? piece.length : 1;
46 | }
47 |
48 | // Populate resulting array
49 | const result = new Uint8Array(size);
50 | let cursor = 0;
51 | for (const piece of chunks) {
52 | if (piece instanceof Uint8Array) {
53 | result.set(piece, cursor);
54 | cursor += piece.length;
55 | } else {
56 | // Piece is number
57 | result[cursor] = piece;
58 | cursor++;
59 | }
60 | }
61 |
62 | return result;
63 | }
64 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | This project adheres to
6 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [0.3.1] - 2021-06-30
9 |
10 | ### Fixed
11 |
12 | - Path method correctly checks for out-of-range indices.
13 |
14 | ## [0.3.0] - 2021-06-29
15 |
16 | ### Fixed
17 |
18 | - Root of empty tree is the BLAKE2b digest of no input.
19 |
20 | ## [0.2.0] - 2021-05-31
21 |
22 | ### Added
23 |
24 | - Method to calculate specific path by index.
25 | - Path-to-proof conversion.
26 |
27 | ### Changed
28 |
29 | - The `Path` interface is more ergonomic.
30 | - The `Step` interface is renamed to `Sibling`.
31 | - Blocks are stored internally, rather than just their hashes. Keep this in mind
32 | when appending large blocks.
33 | - Block deduplication is configurable and disabled by default.
34 |
35 | ### Removed
36 |
37 | - Methods to access internal nodes and leaves.
38 |
39 | ## [0.1.0] - 2021-04-15
40 |
41 | ### Added
42 |
43 | - Minimal Tezos-style Merkle tree implementation
44 | - Fast element appends
45 | - Leaf-to-root paths generator
46 |
47 | [0.1.0]: https://gitlab.com/tzstamp/tezos-merkle/-/releases/0.1.0
48 | [0.2.0]: https://gitlab.com/tzstamp/tezos-merkle/-/releases/0.2.0
49 | [0.3.0]: https://gitlab.com/tzstamp/tezos-merkle/-/releases/0.3.0
50 | [0.3.1]: https://gitlab.com/tzstamp/tezos-merkle/-/releases/0.3.1
51 | [0.3.2]: https://github.com/marigold-dev/tzstamp/releases/tag/0.3.2
52 | [0.3.4]: https://github.com/marigold-dev/tzstamp/releases/tag/0.3.4
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('dotenv').config()
4 | const { ProofStorage } = require('./lib/storage')
5 | const { Aggregator } = require('./lib/aggregator')
6 | const {
7 | stampHandler,
8 | proofHandler,
9 | statusHandler,
10 | configureAPI
11 | } = require('./lib/api')
12 | const { configureTezosClient } = require('./lib/tezos')
13 | const { CronJob } = require('cron')
14 |
15 | const {
16 | PROOFS_DIR: proofsDirectory = 'proofs',
17 | PORT: port = '8000',
18 | BASE_URL: baseURL = 'http://localhost:8000',
19 | KEY_FILE: keyFile,
20 | SECRET: secret,
21 | CONTRACT_ADDRESS: contractAddress = 'KT1AtaeG5PhuFyyivbfPZRUBkVMiqyxpo2cH',
22 | RPC_URL: rpcURL = 'https://mainnet.tezos.marigold.dev',
23 | SCHEDULE: schedule = '*/5 * * * *'
24 | } = process.env
25 |
26 | /**
27 | * Server setup
28 | */
29 | void async function () {
30 | const storage = new ProofStorage(proofsDirectory)
31 | const tezosClient = await configureTezosClient(secret, keyFile, rpcURL)
32 | const contract = await tezosClient.contract.at(contractAddress)
33 | const aggregator = new Aggregator(storage, tezosClient.rpc, contract)
34 | const job = new CronJob(schedule, () => aggregator.publish())
35 | const network = await tezosClient.rpc.getChainId()
36 | const app = await configureAPI(
37 | stampHandler(baseURL, aggregator, schedule),
38 | proofHandler(baseURL, storage, aggregator),
39 | statusHandler(network, contractAddress, schedule)
40 | )
41 | app.listen(port, () => {
42 | console.log(`Listening on port ${port}`)
43 | })
44 | job.start()
45 | }()
46 |
--------------------------------------------------------------------------------
/cli/src/derive.js:
--------------------------------------------------------------------------------
1 | const { Hex } = require('@tzstamp/helpers')
2 | const Help = require('./help')
3 | const { getProof } = require('./helpers')
4 |
5 | function format (format, bytes) {
6 | const hex = Hex.stringify(bytes)
7 | switch (format) {
8 | case 'decimal':
9 | case 'dec':
10 | return BigInt('0x' + hex).toString()
11 | case 'binary':
12 | case 'bin':
13 | return BigInt('0x' + hex).toString(2)
14 | case 'hexadecimal':
15 | case 'hex':
16 | default:
17 | return hex
18 | }
19 | }
20 |
21 | async function handler (options) {
22 | // Early exits
23 | if (options._ == undefined || options._.length < 1) {
24 | return Help.handler({ _: [ 'derive' ] })
25 | }
26 |
27 | // Print formatted derivation
28 | const proofLocation = options._[0]
29 | const proof = await getProof(proofLocation)
30 | console.log(
31 | format(
32 | options.format,
33 | proof.derivation
34 | )
35 | )
36 | }
37 |
38 | const parseOptions = {
39 | string: [
40 | 'format'
41 | ],
42 | alias: {
43 | format: 'f'
44 | }
45 | }
46 |
47 | module.exports = {
48 | handler,
49 | parseOptions,
50 | title: 'Derive',
51 | description: 'Print a formatted derivation of a proof to be used in shell scripts.',
52 | usage: 'tzstamp derive ',
53 | remarks: [
54 | 'Default output is hexadecimal.'
55 | ],
56 | options: [
57 | [ '--format, -f', 'Sets output format. Values are bin(ary), dec(imal), and hex(adecimal).' ]
58 | ],
59 | examples: [
60 | 'tzstamp derive --format bin myFile.txt.proof.json',
61 | 'tzstamp derive https://example.com/myProof.json'
62 | ]
63 | }
64 |
--------------------------------------------------------------------------------
/cli/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const parseArgs = require('minimist')
4 | const { resolveModule } = require('../src/resolve_module')
5 | const Help = require('../src/help')
6 | const Version = require('../src/version')
7 |
8 | const globalParseOptions = {
9 | boolean: [
10 | 'help',
11 | 'version',
12 | 'verbose'
13 | ],
14 | alias: {
15 | help: 'h',
16 | version: 'v',
17 | verbose: 'V'
18 | },
19 | stopEarly: true
20 | }
21 |
22 | const { _: subArgv, ...options } = parseArgs(process.argv.slice(2), globalParseOptions)
23 |
24 | void async function () {
25 | // Early exits
26 | if (options.help) {
27 | await Help.handler({ _: [] }, true)
28 | }
29 | if (options.version) {
30 | await Version.handler({ _: [] })
31 | }
32 |
33 | // Subcommand delegation
34 | const subcommand = subArgv[0]
35 | const mod = resolveModule(subcommand)
36 | if (!mod) {
37 | await Help.handler({ _: [ subcommand ] }, true)
38 | }
39 | const parseOptions = mod.parseOptions || {}
40 | parseOptions.stopEarly = true
41 | if (parseOptions.boolean == undefined) {
42 | parseOptions.boolean = []
43 | }
44 | parseOptions.boolean.push('help', 'verbose')
45 | if (parseOptions.alias == undefined) {
46 | parseOptions.alias = {}
47 | }
48 | parseOptions.alias.help = 'h'
49 | parseOptions.alias.verbose = 'V'
50 | const subOptions = parseArgs(subArgv.slice(1), parseOptions)
51 | subOptions.verbose = subOptions.verbose || options.verbose
52 | if (subOptions.help) {
53 | await Help.handler({ _: [ subcommand ] })
54 | }
55 | await mod.handler(subOptions)
56 | }().catch((error) => {
57 | console.error(error.message)
58 | process.exit(1)
59 | })
60 |
--------------------------------------------------------------------------------
/packages/helpers/hex.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Hexadecimal string validation regular expression.
3 | * Matches strings comprised of only the 16 hexadecimal symbols,
4 | * case-insensitively.
5 | */
6 | export const validator = /^[0-9a-fA-F]+$/;
7 |
8 | /**
9 | * Creates a hexadecimal string from a byte array.
10 | *
11 | * ```js
12 | * Hex.stringify(new Uint8Array([49, 125, 7]));
13 | * // "317d07"
14 | * ```
15 | *
16 | * @param bytes Byte array
17 | */
18 | export function stringify(bytes: Uint8Array): string {
19 | return Array
20 | .from(bytes)
21 | .map((byte) =>
22 | // Map each byte to a 2-digit hex string
23 | byte
24 | .toString(16)
25 | .padStart(2, "0")
26 | )
27 | .join("");
28 | }
29 |
30 | /**
31 | * Parses a hexadecimal string to a byte array.
32 | * Throws `SyntaxError` if the hexadecimal string is invalid.
33 | *
34 | * ```js
35 | * Hex.parse("395f001");
36 | * // Uint8Array(4) [ 3, 149, 240, 1 ]
37 | * ```
38 | *
39 | * @param input Hexadecimal string
40 | */
41 | export function parse(input: string): Uint8Array {
42 | // Empty string
43 | if (input.length == 0) {
44 | return new Uint8Array([]);
45 | }
46 |
47 | // Validate hex string
48 | if (!validator.test(input)) {
49 | throw new SyntaxError("Invalid hexadecimal string");
50 | }
51 |
52 | // Setup byte array
53 | const byteCount = Math.ceil(input.length / 2);
54 | const bytes = new Uint8Array(byteCount);
55 |
56 | // Populate byte array
57 | for (let index = 0; index < input.length / 2; ++index) {
58 | const offset = index * 2 - input.length % 2;
59 | const hexByte = input.substring(offset, offset + 2);
60 | bytes[index] = parseInt(hexByte, 16);
61 | }
62 |
63 | return bytes;
64 | }
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TzStamp
2 |
3 |
4 |
5 |
6 |
7 | ## About
8 |
9 | TzStamp is a [cryptographic timestamping service](https://www.gwern.net/Timestamping)
10 | that uses the Tezos blockchain to prove a file existed at or before a certain time.
11 |
12 |
13 | ## Stamp and Verify
14 |
15 | To create a timestamp, choose a file to calculate its SHA-256 hash locally in your browser. Alternatively, hash the file yourself and paste the hexadecimal representation into the corresponding field. The stamp button will send the hash to the api.tzstamp.io aggregator server, which will include your file hash in its next publication. Your browser will be prompted to download a partial timestamp proof file. Once published to the blockchain, your timestamp proof will become verifiable.
16 |
17 | To verify a timestamp, choose a file (or enter its hash) and a corresponding timestamp proof. The verify button will contact the mainnet.tezos.marigold.dev public Tezos node to verify the proof and display the timestamp. If the timestamp proof is partial, your browser will be prompted to download a full proof.
18 |
19 | **The aggregator root is published every five minutes.**
20 |
21 | ## How to use
22 |
23 | We provide tools that integrate with our TzStamp Server:
24 |
25 | - [Website](https://tzstamp.io).
26 | - [CLI](https://github.com/marigold-dev/tzstamp/tree/main/cli).
27 | - [Proof Library](https://github.com/marigold-dev/tzstamp/tree/main/packages/proof).
28 |
29 | Furthermore you can set up your own [TzStamp Server](https://github.com/marigold-dev/tzstamp/tree/main/server) and [TzStamp Website](https://github.com/marigold-dev/tzstamp/tree/main/website).
30 |
31 |
--------------------------------------------------------------------------------
/packages/helpers/bytes.test.ts:
--------------------------------------------------------------------------------
1 | import { compare, concat } from "./bytes.ts";
2 | import { assert, assertEquals } from "./dev_deps.ts";
3 |
4 | Deno.test({
5 | name: "Concatenation",
6 | fn() {
7 | // Concatenate bytes arrays
8 | assertEquals(
9 | concat(
10 | new Uint8Array([1, 2]),
11 | new Uint8Array([3, 4, 5]),
12 | new Uint8Array([]),
13 | ),
14 | new Uint8Array([1, 2, 3, 4, 5]),
15 | );
16 |
17 | // Concatenate numbers
18 | assertEquals(
19 | concat(0, -0, 1, 2, 3, 277, -12),
20 | new Uint8Array([0, 0, 1, 2, 3, 21, 244]),
21 | );
22 |
23 | // Concatenate mix of numbers and byte arrays
24 | assertEquals(
25 | concat(
26 | new Uint8Array([6, 7, 8]),
27 | 1,
28 | new Uint8Array([54, 55, 56]),
29 | 255,
30 | ),
31 | new Uint8Array([6, 7, 8, 1, 54, 55, 56, 255]),
32 | );
33 | },
34 | });
35 |
36 | Deno.test({
37 | name: "Compare two byte arrays",
38 | fn() {
39 | // Compare two equal byte arrays
40 | assert(
41 | compare(
42 | new Uint8Array([1, 2, 3]),
43 | new Uint8Array([1, 2, 3]),
44 | ),
45 | );
46 |
47 | // Compare two same-length inequivalent byte arrays
48 | assert(
49 | !compare(
50 | new Uint8Array([1, 2, 3]),
51 | new Uint8Array([1, 3, 5]),
52 | ),
53 | );
54 |
55 | // Compare two different-length byte arrays
56 | assert(
57 | !compare(
58 | new Uint8Array([1, 2, 3]),
59 | new Uint8Array([1, 2, 3, 4]),
60 | ),
61 | );
62 |
63 | // Compare filled byte array to empty byte array
64 | assert(
65 | !compare(
66 | new Uint8Array([1, 2, 3]),
67 | new Uint8Array([]),
68 | ),
69 | );
70 |
71 | // Compare two empty byte arrays
72 | assert(
73 | compare(
74 | new Uint8Array([]),
75 | new Uint8Array([]),
76 | ),
77 | );
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/packages/helpers/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | This project adheres to
6 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [0.3.0] - 2021-06-17
9 |
10 | ### Added
11 |
12 | - SHA-256 helper
13 | - Base58check prefixing
14 |
15 | ### Changed
16 |
17 | - Moved `blake2b` convenience function to `Blake2b.digest`.
18 |
19 | ## [0.2.0] - 2021-05-14
20 |
21 | ### Added
22 |
23 | - `Blake2b` class underpinning the `blake2b` convenience function
24 | - Variable BLAKE2b digest lengths and keying
25 | - Node read-stream collection helper
26 | - Hexadecimal and Base58 string validators
27 |
28 | ### Changed
29 |
30 | - Better concatenation helper
31 | - Accepts any number of inputs
32 | - Accepts bare numbers
33 |
34 | ## Removed
35 |
36 | - The Base58 `ALPHABET` constant
37 |
38 | ## [0.1.1] - 2021-04-14
39 |
40 | ### Fixed
41 |
42 | - Parse empty hex string to empty byte array
43 | - Base-58 encode an empty byte array as an empty string
44 | - Check for invalid characters when base-58 encoding
45 | - Throw `SyntaxError` errors when parsing an invalid hex and base-58
46 | - Improve error messages
47 |
48 | ## [0.1.0] - 2021-03-24
49 |
50 | ### Added
51 |
52 | - Hex string helpers
53 | - Encode from Uint8Array byte array
54 | - Decode from string
55 | - Base-58 string helpers
56 | - Uses Bitcoin alphabet
57 | - Encode from Uint8Array byte array
58 | - Decode from string
59 | - Encode and decode with SHA-256 checksum
60 | - BLAKE2b 32-byte hashing helper
61 | - Concatenate Uint8Arrays helper
62 | - Compare Uint8Arrays helper
63 |
64 | [0.1.0]: https://gitlab.com/tzstamp/helpers/-/releases/0.1.0
65 | [0.1.1]: https://gitlab.com/tzstamp/helpers/-/releases/0.1.1
66 | [0.2.0]: https://gitlab.com/tzstamp/helpers/-/releases/0.2.0
67 | [0.3.0]: https://gitlab.com/tzstamp/helpers/-/releases/0.3.0
68 | [0.3.2]: https://github.com/marigold-dev/tzstamp/releases/tag/0.3.2
69 | [0.3.4]: https://github.com/marigold-dev/tzstamp/releases/tag/0.3.4
--------------------------------------------------------------------------------
/cli/README.md:
--------------------------------------------------------------------------------
1 | # TzStamp CLI
2 |
3 | **tzstamp-client** is the command line client for the TzStamp server software.
4 | It lets users stamp and verify files. A full description of its functionality can
5 | be found [in its manual page](man/man1/tzstamp.md).
6 |
7 | ## Installation
8 |
9 | Assuming a fresh install you'll need to `apt-get` some dependencies:
10 |
11 | sudo apt-get install git nodejs npm
12 |
13 | Upgrade the system installation of npm:
14 |
15 | sudo npm install --global --upgrade npm
16 |
17 | Upgrade the system installation of node:
18 |
19 | sudo npm install --global --upgrade node
20 |
21 | Then install the tzstamp package with npm:
22 |
23 | npm install -g @tzstamp/cli
24 |
25 | ## How To Verify An Inclusion Proof On Debian 10.6
26 |
27 | You need:
28 |
29 | * The original file you timestamped or its SHA-256 hash.
30 |
31 | * The proof JSON file returned by the tzstamp server.
32 |
33 | With these run the `verify` subcommand.
34 |
35 | tzstamp verify a.png 1ab1b7db1f4a533de4294166cd3df01403b11c84c16f178a4807b94aa858c3fb.json
36 |
37 | If your 'file' is actually a set of bytes, say a block of text, you can also pass
38 | the sha256 directly in place of a filename.
39 |
40 | tzstamp verify a4e9de2410c9e7c3ac4c57bbc18beedc5935d5c8118e345a72baee00a9820b67 1ab1b7db1f4a533de4294166cd3df01403b11c84c16f178a4807b94aa858c3fb.json
41 |
42 | You should get back a derived Tezos block and timestamp:
43 |
44 | ```
45 | Verified
46 | Target: /home/user/Downloads/Streisand_Estate_800_521.jpg
47 | Hash existed at 6/1/2021, 6:30:54 AM
48 | Block hash: BLQFwmjYJDTyT6wjEwKyot2RFC8wZf9Qyzo56fQ5x4nUYjnfKid
49 | Node queried: https://testnet-tezos.giganode.io/
50 | ```
51 |
52 | If the proof is high stakes you should also manually check this block hash against the [published blocks on mainnet](https://tzkt.io/).
53 | The block [should exist at some point in time](https://tzkt.io/BMTUxYz166GSjyKszmirbYubDLiGEBPRUiJrkjAz6ryVppvDiFX/), if you can't find it the proof may
54 | be invalid.
55 |
56 | ## Timestamping a file
57 |
58 | Files or their hashes can be timestamped like so:
59 |
60 | tzstamp stamp filename.txt
61 |
62 | tzstamp stamp 4a5be57589b4ddc42d87e4df775161e5bbcdf772058093d524b04dd88533a50a
63 |
64 |
--------------------------------------------------------------------------------
/server/tests/aggregator.test.js:
--------------------------------------------------------------------------------
1 | const { Aggregator } = require('../lib/aggregator')
2 | const { randomInt } = require('crypto')
3 | const { Proof } = require('@tzstamp/proof')
4 | const { Blake2b, Hex } = require('@tzstamp/helpers')
5 |
6 | // Mock data
7 | const storage = {
8 | storeProof: jest.fn()
9 | }
10 | const height = randomInt(1000)
11 | const blockData = {}
12 | const rpc = {
13 | getBlock: jest.fn(({ block }) => {
14 | expect(block).toBe(height)
15 | return blockData
16 | })
17 | }
18 | const opGroup = {
19 | confirmation: jest.fn().mockResolvedValue(height + 2)
20 | }
21 | const defaultMethod = {
22 | send: jest.fn().mockResolvedValue(opGroup)
23 | }
24 | const contract = {
25 | methods: {
26 | default: jest.fn().mockReturnValue(defaultMethod)
27 | }
28 | }
29 | let aggregator
30 |
31 | describe('Use an aggregator', () => {
32 | test('Instantiate', () => {
33 | aggregator = new Aggregator(storage, rpc, contract)
34 | })
35 | test('Cycle the active Merkle tree', () => {
36 | const m1 = aggregator.merkleTree
37 | const m2 = aggregator.cycle()
38 | expect(m1).toBe(m2)
39 | expect(m1).not.toBe(aggregator.merkleTree)
40 | })
41 | test('Invoke the publishing contract', async () => {
42 | const [ b, og ] = await aggregator._invoke('foo')
43 | expect(contract.methods.default.mock.calls.length).toBe(1)
44 | expect(defaultMethod.send.mock.calls.length).toBe(1)
45 | expect(opGroup.confirmation.mock.calls.length).toBe(1)
46 | expect(b).toEqual(blockData)
47 | expect(og).toEqual(opGroup)
48 | })
49 | test('Output proofs to storage', async () => {
50 | const data = [
51 | new Uint8Array([ 1 ]),
52 | new Uint8Array([ 2 ]),
53 | new Uint8Array([ 3 ])
54 | ]
55 | for (const block of data) {
56 | const proofId = Hex.stringify(Blake2b.digest(block))
57 | aggregator.pendingProofs.add(proofId)
58 | }
59 | const merkleTree = aggregator.cycle()
60 | merkleTree.append(...data)
61 | expect(aggregator.pendingProofs.size).toBe(3)
62 | const highProof = new Proof({
63 | hash: merkleTree.root,
64 | operations: []
65 | })
66 | await aggregator._output(merkleTree, highProof)
67 | expect(aggregator.pendingProofs.size).toBe(0)
68 | expect(storage.storeProof.mock.calls.length).toBe(3)
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/path.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Blake2b,
3 | Blake2bOperation,
4 | JoinOperation,
5 | Operation,
6 | Proof,
7 | } from "./deps.ts";
8 |
9 | /**
10 | * Sibling node
11 | */
12 | export interface Sibling {
13 | /**
14 | * Sibling node hash
15 | */
16 | hash: Uint8Array;
17 |
18 | /**
19 | * Relation of a sibling node
20 | *
21 | * The hash of a left sibling node should prepended to the current
22 | * hash to derive the parent node hash, whereas the hash of a right
23 | * sibling node should appended.
24 | */
25 | relation: "left" | "right";
26 | }
27 |
28 | /**
29 | * Path constructor options
30 | */
31 | export interface PathOptions {
32 | /**
33 | * Data block
34 | */
35 | block: Uint8Array;
36 |
37 | /**
38 | * Sibling nodes along the path from the block to the root hash.
39 | */
40 | siblings: Sibling[];
41 |
42 | /**
43 | * Root node hash
44 | */
45 | root: Uint8Array;
46 | }
47 |
48 | /**
49 | * Path from leaf to root
50 | */
51 | export class Path {
52 | /**
53 | * Data block
54 | */
55 | block: Uint8Array;
56 |
57 | /**
58 | * Sibling nodes along path from the block to the root hash
59 | */
60 | siblings: Sibling[];
61 |
62 | /**
63 | * Root node hash
64 | */
65 | root: Uint8Array;
66 |
67 | constructor({ block, siblings, root }: PathOptions) {
68 | this.block = block;
69 | this.siblings = siblings;
70 | this.root = root;
71 | }
72 |
73 | /**
74 | * Leaf node hash
75 | */
76 | get leaf(): Uint8Array {
77 | return Blake2b.digest(this.block);
78 | }
79 |
80 | /**
81 | * Creates a [timestamp proof](https://github.com/marigold-dev/tzstamp/tree/main/proof) from the path.
82 | */
83 | toProof(): Proof {
84 | const operations: Operation[] = [new Blake2bOperation()];
85 | for (const { hash, relation } of this.siblings) {
86 | operations.push(
87 | new JoinOperation({
88 | prepend: relation == "left" ? hash : undefined,
89 | append: relation == "right" ? hash : undefined,
90 | }),
91 | new Blake2bOperation(),
92 | );
93 | }
94 | return new Proof({
95 | hash: this.block,
96 | operations,
97 | });
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/packages/helpers/sha256.test.ts:
--------------------------------------------------------------------------------
1 | import { Sha256 } from "./sha256.ts";
2 | import { assert, assertEquals, assertThrows } from "./dev_deps.ts";
3 |
4 | Deno.test({
5 | name: "Sha256 class interface",
6 | fn() {
7 | const hash = new Sha256();
8 | assert(!hash.finalized);
9 | hash.update(new Uint8Array([]));
10 | assert(!hash.finalized);
11 | const digest = hash.digest();
12 | assert(hash.finalized);
13 | assertEquals(digest, hash.digest());
14 | assertThrows(() => hash.update(new Uint8Array([])));
15 | },
16 | });
17 |
18 | Deno.test({
19 | name: "SHA-256 test vectors",
20 | fn() {
21 | assertEquals(
22 | Sha256.digest(new Uint8Array()),
23 | // deno-fmt-ignore
24 | [
25 | 227, 176, 196, 66, 152, 252, 28, 20,
26 | 154, 251, 244, 200, 153, 111, 185, 36,
27 | 39, 174, 65, 228, 100, 155, 147, 76,
28 | 164, 149, 153, 27, 120, 82, 184, 85,
29 | ],
30 | );
31 | assertEquals(
32 | Sha256.digest(new TextEncoder().encode("abc")),
33 | // deno-fmt-ignore
34 | [
35 | 186, 120, 22, 191, 143, 1, 207, 234,
36 | 65, 65, 64, 222, 93, 174, 34, 35,
37 | 176, 3, 97, 163, 150, 23, 122, 156,
38 | 180, 16, 255, 97, 242, 0, 21, 173,
39 | ],
40 | );
41 | assertEquals(
42 | Sha256.digest(new Uint8Array(123).fill(1)),
43 | // deno-fmt-ignore
44 | [
45 | 62, 40, 216, 212, 230, 73, 221, 101,
46 | 223, 81, 245, 183, 250, 217, 78, 187,
47 | 145, 91, 61, 135, 31, 11, 76, 168,
48 | 203, 225, 97, 117, 112, 151, 107, 65,
49 | ],
50 | );
51 | assertEquals(
52 | Sha256.digest(new Uint8Array(63).fill(1)),
53 | // deno-fmt-ignore
54 | [
55 | 67, 110, 103, 172, 201, 29, 33, 167,
56 | 113, 226, 46, 226, 168, 139, 143, 108,
57 | 244, 16, 63, 152, 56, 65, 254, 84,
58 | 118, 34, 17, 20, 220, 138, 124, 88,
59 | ],
60 | );
61 | assertEquals(
62 | Sha256.digest(Uint8Array.from({ length: 255 }, (_, i) => +i)),
63 | // deno-fmt-ignore
64 | [
65 | 63, 133, 145, 17, 44, 107, 190, 92,
66 | 150, 57, 101, 149, 78, 41, 49, 8,
67 | 183, 32, 142, 210, 175, 137, 62, 80,
68 | 13, 133, 147, 104, 198, 84, 234, 190
69 | ],
70 | );
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/server/man/man1/tzstamp-server.md:
--------------------------------------------------------------------------------
1 | % TZSTAMP-SERVER(1)
2 | % John David Pressman
3 | % December 2020
4 |
5 | # NAME
6 |
7 | tzstamp-server - Tezos trusted timestamping service
8 |
9 | # SYNOPSIS
10 |
11 | tzstamp-server
12 |
13 | # DESCRIPTION
14 |
15 | **tzstamp-server** is a service that utilizes the Tezos blockchain to perform
16 | trusted timestamping of files. Files are hashed using the SHA-256 or blake2b
17 | algorithm and then submitted to the server. tzstamp-server aggregates the hashes
18 | in a merkle tree structure, or a tree of hashes. The root of that tree is
19 | published to a smart contract on the Tezos blockchain, allowing many hashes to
20 | be shown to have existed at a certain time on-chain for the cost of one. A
21 | further benefit of the merkle tree structure is that users can prove their hash
22 | was included in the merkle root without relying on tzstamp to store their
23 | submitted hash long term. Once the root is published, users download an
24 | inclusion proof consisting of the nodes necessary to derive a merkle root given
25 | the submitted hash. Because nodes are stored as a binary tree, the number of
26 | nodes necessary to derive the root grows logarithmically to the tree size,
27 | ensuring that the proof stays small even as the server begins to handle millions
28 | or billions of entries.
29 |
30 | See https://www.gwern.net/Timestamping for more background information on
31 | trusted timestamping services.
32 |
33 | # ENVIRONMENT VARIABLES
34 |
35 | The server configuration is stored in **.env.defaults**, a list of environment
36 | variables whose default values are set by the file. Values can be overridden
37 | by outside scripts and automation by setting the environment variables manually.
38 |
39 | **$PORT**
40 | : Port for the server to listen on
41 |
42 | **$INTERVAL**
43 | : Time between merkle root inclusions, expressed in seconds
44 |
45 | **$BASE_URL**
46 | : Base url of the server instance, e.g. "http://example.com"
47 |
48 | **$FAUCET_KEY_PATH**
49 | : Path to a Tezos testnet faucet key, **this is not for use on mainnet**
50 |
51 | **TEZOS_WALLET_SECRET**
52 | : Raw Tezos private key for use with local signing, **use this setting for mainnet instances**
53 |
54 | **$CONTRACT_ADDRESS**
55 | : KT1 of the Tezos tzstamp smart contract instance the server will use
56 |
57 | **$RPC_URL**
58 | : URL of the Tezos node to publish merkle roots with
59 |
60 |
61 |
--------------------------------------------------------------------------------
/cli/man/man1/tzstamp.md:
--------------------------------------------------------------------------------
1 | % TZSTAMP(1)
2 | % John David Pressman, Benjamin Herman
3 | % December 2020
4 |
5 | # NAME
6 |
7 | tzstamp - Submit and verify hashes with the tzstamp Tezos timestamping service
8 |
9 | # SYNOPSIS
10 |
11 | **tzstamp** **stamp** [*HASH* | *FILEPATH*]
12 |
13 | **tzstamp** **derive** [*HASH* | *FILEPATH*] [*MERKLE_PROOF_FILEPATH* | *MERKLE_PROOF_URL*]
14 |
15 | **tzstamp** **verify** [*HASH* | *FILEPATH*] [*MERKLE_PROOF_FILEPATH* | *MERKLE_PROOF_URL*]
16 |
17 | **tzstamp** **help**
18 |
19 | # DESCRIPTION
20 |
21 | **tzstamp** is a service that utilizes the Tezos blockchain to perform
22 | trusted timestamping of files. Files are hashed using the SHA-256 algorithm and
23 | then the hash is submitted to the tzstamp service. tzstamp aggregates the hashes
24 | in a merkle tree structure, or a tree of hashes. The root of that tree is
25 | published to a smart contract on the Tezos blockchain, allowing many hashes to
26 | be shown to have existed at a certain time on-chain for the cost of one. A
27 | further benefit of the merkle tree structure is that users can prove their hash
28 | was included in the merkle root without relying on tzstamp to store their
29 | submitted hash long term. Once the root is published, users download an
30 | inclusion proof consisting of the nodes necessary to derive a merkle root given
31 | the submitted hash. Because nodes are stored as a binary tree, the number of
32 | nodes necessary to derive the root grows logarithmically to the tree size,
33 | ensuring that the proof stays small even as the server begins to handle millions
34 | or billions of entries.
35 |
36 | See https://www.gwern.net/Timestamping for more background information on
37 | trusted timestamping services.
38 |
39 | # OPTIONS
40 |
41 | **\-\-server**
42 | : TzStamp server instance to publish hashes to. Default is "https://api.tzstamp.io"
43 |
44 | **\-\-root-format**
45 | : Set root display format. Values are: hex, binary, decimal.
46 |
47 | **\-w, \-\-wait**
48 | : If set, execution will hang until server publishes the proof.
49 |
50 |
51 | # EXAMPLES
52 |
53 | tzstamp stamp prediction.txt
54 |
55 | tzstamp derive 344f904b931e6033102e4235e592ea19f800ff3737ff3a18c47cfe63dbea2ed7 https://tzstamp.io/api/proof/bc46aa6337581b95201294a253f94c2ed3d51e11f17cabf84ad118d1d91ef080
56 |
57 | tzstamp verify 344f904b931e6033102e4235e592ea19f800ff3737ff3a18c47cfe63dbea2ed7 https://tzstamp.io/api/proof/bc46aa6337581b95201294a253f94c2ed3d51e11f17cabf84ad118d1d91ef080
58 |
--------------------------------------------------------------------------------
/packages/proof/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | This project adheres to
6 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [0.3.0] - 2021-06-28
9 |
10 | ### Added
11 |
12 | - `Proof.create` static method for automatic subclassing.
13 | - Type guards for affixed and unresolved proofs.
14 |
15 | ### Changed
16 |
17 | - The `PendingProof` class is renamed to `UnresolvedProof`.
18 | - Verifying an affixed proof returns a more informative result type.
19 |
20 | ### Fixed
21 |
22 | - Verify and resolve methods do not leak resources on early return.
23 |
24 | ## [0.2.0] - 2021-05-30
25 |
26 | ### Added
27 |
28 | - Proof serialization format version 1
29 | - JSON TypeDef schemas and validation.
30 | - Easier to read as JSON.
31 | - Three operations: "join", "blake2b", and "sha256".
32 | - BLAKE2b operation options
33 | - Variable length digest
34 | - Keying
35 | - Proof concatenation
36 | - Join two proofs with safety checks.
37 | - Allows storing proof segments for further construction later.
38 | - Affixed proofs
39 | - Verify against a Tezos node.
40 | - Asserts that it is affixed to the Tezos Mainnet or an alternate network.
41 | - Pending proofs
42 | - Resolves a remote proof and concatenates.
43 | - Custom error classes
44 | - Deno support
45 | - Significantly improved inline documentation
46 |
47 | ### Changed
48 |
49 | - Operation templating logic to be centralized in `Operation.from`. Static
50 | `from` method of subclasses inherit implementation from the super class and
51 | behave no differently.
52 |
53 | ### Removed
54 |
55 | - Support for the version 0 serialization format.
56 | - The `Proof.prototype.derive` method. Derivation should be obtained from the
57 | `Proof.prototype.derivation` property, which uses the stored hash as input.
58 |
59 | ## [0.1.0] - 2021-04-15
60 |
61 | ### Added
62 |
63 | - Serialization/deserialization to/from JSON
64 | - Basic operations
65 | - Append/Prepend arbitrary data
66 | - SHA-256
67 | - Blake2b (256-bit digest)
68 | - Derive block from input
69 | - Fetch block headers from RPC to check timestamp
70 |
71 | [0.1.0]: https://gitlab.com/tzstamp/proof/-/releases/0.1.0
72 | [0.2.0]: https://gitlab.com/tzstamp/proof/-/releases/0.2.0
73 | [0.3.0]: https://gitlab.com/tzstamp/proof/-/releases/0.3.0
74 | [0.3.2]: https://github.com/marigold-dev/tzstamp/releases/tag/0.3.2
75 | [0.3.4]: https://github.com/marigold-dev/tzstamp/releases/tag/0.3.4
--------------------------------------------------------------------------------
/packages/helpers/_build.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S deno run --unstable --allow-read=. --allow-write=.
2 |
3 | import { basename, join, resolve } from "./dev_deps.ts";
4 |
5 | const OUTDIR = resolve("dist");
6 |
7 | console.log("Cleaning distribution directory");
8 | await Deno.remove(OUTDIR, { recursive: true }).catch(() => {});
9 | await Deno.mkdir(OUTDIR);
10 |
11 | Promise.all([
12 | // Node modules
13 | Deno.emit("mod.ts", {
14 | compilerOptions: {
15 | module: "commonjs",
16 | sourceMap: false,
17 | noResolve: true,
18 | },
19 | }).then(writeNodeModules),
20 | // Node types
21 | Deno.emit("mod.ts", {
22 | compilerOptions: {
23 | declaration: true,
24 | emitDeclarationOnly: true,
25 | noResolve: true,
26 | removeComments: false,
27 | },
28 | }).then(writeNodeTypes),
29 | // Browser bundle
30 | Deno.emit("mod.ts", {
31 | bundle: "module",
32 | }).then(writeBrowserBundle),
33 | ]).catch((error) => {
34 | console.log(error);
35 | Deno.exit(1);
36 | });
37 |
38 | async function writeNodeModules({ files }: Deno.EmitResult) {
39 | console.log("Emitting node modules");
40 | for (const [filePath, source] of Object.entries(files)) {
41 | let fileName = basename(filePath).slice(0, -6) + ".js";
42 | switch (fileName) {
43 | case "mod.js":
44 | fileName = "index.js";
45 | break;
46 | }
47 | if (fileName) {
48 | await Deno.writeTextFile(
49 | join(OUTDIR, fileName),
50 | source.replace(
51 | new RegExp(`file://${Deno.cwd()}/(.*)\.ts`, "g"),
52 | "./$1",
53 | ),
54 | );
55 | }
56 | }
57 | }
58 |
59 | async function writeNodeTypes({ files }: Deno.EmitResult) {
60 | console.log("Emitting node types");
61 | for (const [filePath, source] of Object.entries(files)) {
62 | let fileName = basename(filePath).slice(0, -8) + ".d.ts";
63 | switch (fileName) {
64 | case "mod.d.ts":
65 | fileName = "index.d.ts";
66 | break;
67 | }
68 | await Deno.writeTextFile(
69 | join(OUTDIR, fileName),
70 | source
71 | .split("\n")
72 | .filter((line) => !line.startsWith("/// [command options]',
16 | remarks: [ 'Use "tzstamp help " for detailed command usage.' ],
17 | options: [
18 | [ '--help, -h', description ],
19 | [ '--version, -v', Version.description ],
20 | [ '--verbose, -V', 'Logs extra messages.' ],
21 | [ '--no-color', 'Disables color output.' ]
22 | ],
23 | get examples () {
24 | const result = []
25 | for (const [ subcommand, resolver ] of modules.entries()) {
26 | result.push(subcommand + ' '.repeat(SPACER - subcommand.length) + resolver().description)
27 | }
28 | return result
29 | },
30 | footer:
31 | 'Copyright (c) 2021 John David Pressman, Benjamin Herman\n' +
32 | 'For use under the MIT License\n' +
33 | 'Source: '
34 | }
35 |
36 | async function handler (options, printGlobalHelp = false) {
37 | const query = options._[0]
38 | let mod
39 | if (!query) {
40 | mod = globalHelp
41 | } else if (printGlobalHelp) {
42 | if (query) {
43 | console.log(`Unrecognized subcommand "${query}".`)
44 | }
45 | mod = globalHelp
46 | } else {
47 | mod = resolveModule(query)
48 | }
49 | console.log('\n' + chalk.bold(mod.title) + ' - ' + mod.description)
50 | if (mod.usage) {
51 | console.log('\n' + chalk.underline`Usage:`)
52 | console.log(INDENT + mod.usage)
53 | }
54 | if (mod.remarks) {
55 | for (const remark of mod.remarks) {
56 | console.log('\n' + remark)
57 | }
58 | }
59 | if (mod.options) {
60 | console.log('\n' + chalk.underline(mod == globalHelp ? 'Global options:' : 'Options:'))
61 | for (const [ key, description ] of mod.options) {
62 | console.log(INDENT + key + ' '.repeat(20 - key.length) + description)
63 | }
64 | }
65 | if (mod.examples) {
66 | console.log('\n' + chalk.underline(mod == globalHelp ? 'Commands:' : 'Examples:'))
67 | for (const example of mod.examples) {
68 | console.log(INDENT + example)
69 | }
70 | }
71 | console.log()
72 | if (mod.footer) {
73 | console.log(mod.footer)
74 | }
75 | process.exit(2)
76 | }
77 |
78 | module.exports = {
79 | handler,
80 | title: 'Help',
81 | description,
82 | usage: 'tzstamp help [command]'
83 | }
84 |
--------------------------------------------------------------------------------
/packages/export-commonjs/mod.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ImportDeclarationStructure,
3 | OptionalKind,
4 | Project,
5 | ts,
6 | } from "./deps.ts";
7 |
8 | export interface ExportOptions {
9 | filePaths: string | string[];
10 | outDir: string;
11 | dependencyMap?: Map;
12 | shims?: OptionalKind[];
13 | clean?: boolean;
14 | }
15 |
16 | /**
17 | * Exports a Deno project as CommonJS.
18 | *
19 | * @param filesPaths File inclusion glob matches
20 | * @param outDir Directory to output files to
21 | * @param clean Indicates the output directory should be emptied before output
22 | * @param dependencyMap Dependency substitution map
23 | * @param shims Shim injections
24 | */
25 | export async function exportCommonJS({
26 | filePaths,
27 | outDir,
28 | clean = true,
29 | dependencyMap,
30 | shims,
31 | }: ExportOptions): Promise {
32 | // Clean output directory
33 | if (clean) {
34 | await Deno.remove(outDir, { recursive: true }).catch(() => {});
35 | await Deno.mkdir(outDir);
36 | }
37 |
38 | // Create project
39 | const project = new Project({
40 | compilerOptions: {
41 | module: ts.ModuleKind.CommonJS,
42 | moduleResolution: ts.ModuleResolutionKind.NodeJs,
43 | target: ts.ScriptTarget.ES2019,
44 | outDir,
45 | declaration: true,
46 | sourceMap: false,
47 | noEmitOnError: true,
48 | strict: true,
49 | },
50 | });
51 |
52 | project.addSourceFilesAtPaths(filePaths);
53 |
54 | // Map import and export declarations
55 | for (const sourceFile of project.getSourceFiles()) {
56 | const declarations = [
57 | ...sourceFile.getImportDeclarations(),
58 | ...sourceFile.getExportDeclarations(),
59 | ];
60 | for (const declaration of declarations) {
61 | const specifier = declaration.getModuleSpecifierValue();
62 | if (!specifier) continue;
63 | if (dependencyMap?.has(specifier)) {
64 | declaration.setModuleSpecifier(
65 | dependencyMap.get(specifier) as string,
66 | );
67 | } else if (specifier.endsWith(".ts")) {
68 | declaration.setModuleSpecifier(
69 | specifier.slice(0, -3).concat(".js"),
70 | );
71 | }
72 | }
73 | if (shims) {
74 | sourceFile.addImportDeclarations(shims);
75 | }
76 | }
77 |
78 | // Emit files
79 | const emitResult = project.emitToMemory();
80 | await emitResult.saveFiles();
81 |
82 | // Diagnose
83 | const diagnostics = [
84 | ...project.getPreEmitDiagnostics(),
85 | ...emitResult.getDiagnostics(),
86 | ];
87 | if (diagnostics.length) {
88 | console.error(
89 | project.formatDiagnosticsWithColorAndContext(diagnostics),
90 | );
91 | Deno.exit(1);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/readme.md:
--------------------------------------------------------------------------------
1 | # TzStamp Tezos-style Merkle Trees
2 |
3 | Fast-appendable Tezos-style Merkle trees for
4 | [TzStamp tools](https://tzstamp.io).
5 |
6 | Tezos-style Merkle trees use the [BLAKE2b](https://www.blake2.net/) hashing
7 | algorithm and implicitly repeat the last leaf until the tree is perfect.
8 |
9 | This implementation has logarithmic time-complexity appends to allow for
10 | progressive root derivation over a long runtime. You can find the
11 | [official Tezos implementation here](https://gitlab.com/tezos/tezos/-/blob/master/src/lib_crypto/blake2B.ml).
12 |
13 | ## Usage
14 |
15 | ```js
16 | // Node + NPM
17 | const { MerkleTree } = require("@tzstamp/tezos-merkle");
18 |
19 | // Deno
20 | import { MerkleTree } from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/tezos-merkle/mod.ts";
21 | ```
22 |
23 | See the
24 | [full reference documentation here](https://doc.deno.land/https/raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/tezos-merkle/mod.ts).
25 |
26 | ### Building a Merkle tree
27 |
28 | ```js
29 | const merkleTree = new MerkleTree();
30 |
31 | // Append data blocks
32 | merkleTree.append(
33 | new Uint8Array([1, 4, 7]),
34 | new Uint8Array([55, 66, 77]),
35 | new Uint8Array([0, 0, 0, 255]),
36 | );
37 |
38 | // Get leaf count
39 | merkleTree.size; // 3
40 |
41 | // Get the current root
42 | merkleTree.root; // Uint8Array(32)
43 |
44 | // Check if a block is included in the tree
45 | merkleTree.has(new Uint8Array([1, 4, 7])); // true
46 | merkleTree.has(new Uint8Array([255])); // false
47 | ```
48 |
49 | Block deduplication (off by default) can be configured like so:
50 |
51 | ```js
52 | const merkleTree = new MerkleTree({
53 | deduplicate: true,
54 | });
55 |
56 | merkleTree.append(
57 | new Uint8Array([1, 2, 3, 4]),
58 | new Uint8Array([1, 2, 3, 4]), // deduplicated
59 | );
60 |
61 | merkleTree.size; // 1
62 | ```
63 |
64 | ### Generating paths
65 |
66 | Block-to-root paths can be computed like so:
67 |
68 | ```js
69 | const paths = merkleTree.path(4); // Path from 5th block to the root
70 |
71 | path.block; // Uint8Array
72 | path.root; // Uint8Array(32)
73 |
74 | // Sibling nodes along path to root
75 | path.siblings;
76 | // [ { hash: Uint8Array {}, relation: "left" }, ... ]
77 | ```
78 |
79 | A [timestamp proof segment](https://github.com/marigold-dev/tzstamp/tree/main/proof) can be constructed
80 | with the `.toProof` method:
81 |
82 | ```js
83 | path.toProof(); // Proof{}
84 | ```
85 |
86 | A `paths` generator function is provided to compute paths for all blocks in the
87 | tree, facilitating mass proof construction.
88 |
89 | ```js
90 | for (const path of merkleTree.paths()) {
91 | const proof = path.toProof();
92 | await storeMyProof(proof);
93 | }
94 | ```
95 |
96 | ## License
97 |
98 | [MIT](license.txt)
99 |
--------------------------------------------------------------------------------
/manage/man/man1/tzstamp-manage.md:
--------------------------------------------------------------------------------
1 | % TZSTAMP-MANAGE(1)
2 | % John David Pressman, Benjamin Herman
3 | % January 2021
4 |
5 | # NAME
6 |
7 | tzstamp-manage - Manage an instance of the tzstamp Tezos smart contract
8 |
9 | # SYNOPSIS
10 |
11 | **tzstamp-manage** **deploy** *CONTRACT_NAME*
12 |
13 | **tzstamp-manage** **upload-hash** *CONTRACT_KT1* *HASH*
14 |
15 | **tzstamp-manage** **view** {**stats** | **storage**} *CONTRACT_KT1*
16 |
17 | **tzstamp-manage** **estimate** *CONTRACT_NAME* *CONTRACT_KT1*
18 |
19 | **tzstamp-manage** **is-tzstamp** *CONTRACT_KT1*
20 |
21 | # DESCRIPTION
22 |
23 | **tzstamp-manage** is a helper utility for managing instances of the tzstamp
24 | smart contract. This is especially useful for users who need to manage instances
25 | at scale, such as those working with multiple private chains. Access to a wallet
26 | is required to make use of deployment and manual uploading of merkle roots.
27 |
28 | # OPTIONS
29 |
30 | **\-\-node**
31 | : Tezos node RPC URL to publish kt1 transactions with. For private chains this should be a custom node instance that publishes to the chain.
32 |
33 | **\-\-indexer**
34 | : Chainviewer instance to view the network with. There is currently no standard for Tezos chainviewer API's, so the assumption is made that you are connecting to an instance of the (open source) Better Call Dev indexer: https://api.better-call.dev/v1/docs/index.html
35 |
36 | **\-\-network**
37 | : The name of the network to publish transactions to. (e.g. delphinet)
38 |
39 | **\-\-secret\-path**
40 | : Filepath to an (unencrypted) tezos secret key. You can also export the environment variable TEZOS_WALLET_SECRET with the key itself.
41 |
42 | # EXAMPLES
43 |
44 | **Deploying A Contract**
45 | : tzstamp-manage deploy simple \-\-node https://testnet-tezos.giganode.io/
46 |
47 | **Manually Uploading A Merkle Root On TestNet**
48 | : tzstamp-manage upload-hash KT1AkQkRdLgE9NKSTCaPPZPgQuX7NUEtXzdj 84714a61037b3b4fa539008681cbfa97c7256930279ff4b54bad7366521afc67 \-\-node https://testnet-tezos.giganode.io/ \-\-faucet tz1MKs91KPzkpmZYz7Dvbd9dyq86murA1BrN.json
49 |
50 | **Manually Uploading A Merkle Root On MainNet**
51 |
52 | : tzstamp-manage upload-hash KT1K5npkpWK6wxkcBg97dZD77c2J7DmWvxSb a4e9de2410c9e7c3ac4c57bbc18beedc5935d5c8118e345a72baee00a9820b67 \-\-secret-path secret.txt
53 |
54 | **Viewing Contract Storage**
55 | : tzstamp-manage view storage KT1AkQkRdLgE9NKSTCaPPZPgQuX7NUEtXzdj \-\-node https://testnet-tezos.giganode.io/
56 |
57 | **Estimating Burn Rate For A Custom Contract**
58 | : tzstamp-manage estimate --node https://testnet-tezos.giganode.io --faucet tz1aiE7jFTAutxE8VBmKJcid7xxPsCXgWzJH.json noop KT1PXq6SVTH3fBgJk9hqyVakJERkRp4pUYEL
59 |
60 | **Checking If A Contract Is A TzStamp Instance**
61 | : tzstamp-manage is-tzstamp KT1AkQkRdLgE9NKSTCaPPZPgQuX7NUEtXzdj \-\-node https://testnet-tezos.giganode.io/
62 |
--------------------------------------------------------------------------------
/packages/proof/schemas.ts:
--------------------------------------------------------------------------------
1 | import { Schema, validate } from "./deps.ts";
2 |
3 | /**
4 | * Checks that an instance is valid against a [JTD] schema,
5 | * narrowing the instance type on successful validation.
6 | *
7 | * ```ts
8 | * enum Color {
9 | * Red = "red",
10 | * Blue = "blue",
11 | * Green = "green",
12 | * };
13 | *
14 | * const colors: Schema = {
15 | * enum: ["red", "blue", "green"]
16 | * };
17 | *
18 | * const green = "green";
19 | *
20 | * if (isValid(colors, green)) {
21 | * // `green` is narrowed to `Color`
22 | * }
23 | * ```
24 | *
25 | * @param schema JTD schema
26 | * @param instance Value to validate
27 | *
28 | * [JTD]: https://jsontypedef.com
29 | */
30 | export function isValid(schema: Schema, instance: unknown): instance is T {
31 | return validate(schema, instance).length == 0;
32 | }
33 |
34 | /**
35 | * Operation template [JSON Type Definition](https://datatracker.ietf.org/doc/html/rfc8927) schema
36 | */
37 | export const operationSchema: Schema = {
38 | discriminator: "type",
39 | mapping: {
40 | "join": {
41 | optionalProperties: {
42 | prepend: { type: "string" },
43 | append: { type: "string" },
44 | },
45 | },
46 | "blake2b": {
47 | optionalProperties: {
48 | length: { type: "uint32" },
49 | key: { type: "string" },
50 | },
51 | },
52 | "sha256": {
53 | properties: {},
54 | },
55 | },
56 | } as const;
57 |
58 | /**
59 | * Proof template [JSON Type Definition](https://datatracker.ietf.org/doc/html/rfc8927) schema
60 | */
61 | export const proofSchema: Schema = {
62 | properties: {
63 | version: { type: "uint32" },
64 | hash: { type: "string" },
65 | operations: {
66 | elements: operationSchema,
67 | },
68 | },
69 | optionalProperties: {
70 | network: { type: "string" },
71 | timestamp: { type: "timestamp" },
72 | remote: { type: "string" },
73 | },
74 | } as const;
75 |
76 | /**
77 | * Affixed proof template [JSON Type Definition](https://datatracker.ietf.org/doc/html/rfc8927) schema
78 | */
79 | export const affixedProofSchema: Schema = {
80 | properties: {
81 | ...proofSchema.properties,
82 | network: { type: "string" },
83 | timestamp: { type: "timestamp" },
84 | },
85 | } as const;
86 |
87 | /**
88 | * Unresolved proof template [JSON Type Definition](https://datatracker.ietf.org/doc/html/rfc8927) schema
89 | */
90 | export const unresolvedProofSchema: Schema = {
91 | properties: {
92 | ...proofSchema.properties,
93 | remote: { type: "string" },
94 | },
95 | } as const;
96 |
97 | /**
98 | * Tezos block header [JSON Type Definition](https://datatracker.ietf.org/doc/html/rfc8927) schema
99 | */
100 | export const blockHeaderSchema: Schema = {
101 | properties: {
102 | timestamp: { type: "timestamp" },
103 | },
104 | additionalProperties: true,
105 | } as const;
106 |
--------------------------------------------------------------------------------
/packages/helpers/base58.test.ts:
--------------------------------------------------------------------------------
1 | import * as Base58 from "./base58.ts";
2 | import { assertEquals, assertThrows } from "./dev_deps.ts";
3 |
4 | Deno.test({
5 | name: "Encode byte array as Base-58 string",
6 | fn() {
7 | // Encode 'hello' in UTF-8
8 | const hello = new Uint8Array([104, 101, 108, 108, 111]);
9 | assertEquals(Base58.encode(hello), "Cn8eVZg");
10 |
11 | // Encode empty byte array
12 | const empty = new Uint8Array([]);
13 | assertEquals(Base58.encode(empty), "");
14 |
15 | // Encode byte array with leading zeroes
16 | const leadingZeroes = new Uint8Array([0, 0, 0, 10, 20, 30, 40]);
17 | assertEquals(Base58.encode(leadingZeroes), "111FwdkB");
18 | },
19 | });
20 |
21 | Deno.test({
22 | name: "Decode byte array from Base-58 string",
23 | fn() {
24 | // Decode valid base-58 encoding
25 | assertEquals(
26 | Base58.decode("Cn8eVZg"),
27 | new Uint8Array([104, 101, 108, 108, 111]),
28 | );
29 |
30 | // Decode empty string
31 | assertEquals(Base58.decode(""), new Uint8Array([]));
32 |
33 | // Decode invalid base-58 string ('l' is not in the base-58 alphabet)
34 | assertThrows(() => Base58.decode("malformed"), SyntaxError);
35 |
36 | // Decode with leading zeroes
37 | assertEquals(
38 | Base58.decode("111FwdkB"),
39 | new Uint8Array([0, 0, 0, 10, 20, 30, 40]),
40 | );
41 | },
42 | });
43 |
44 | Deno.test({
45 | name: "Encode byte array as Base-58 string with checksum",
46 | fn() {
47 | // Checksum-encode 'hello' in UTF-8
48 | const hello = new Uint8Array([104, 101, 108, 108, 111]);
49 | assertEquals(Base58.encodeCheck(hello), "2L5B5yqsVG8Vt");
50 |
51 | // Checksum-encode empty byte array
52 | const empty = new Uint8Array([]);
53 | assertEquals(Base58.encodeCheck(empty), "3QJmnh");
54 |
55 | // Checksum-encode with prefix
56 | const random = crypto.getRandomValues(new Uint8Array(20));
57 | assertEquals(
58 | Base58.encodeCheck(random.slice(10, 20), random.slice(0, 10)),
59 | Base58.encodeCheck(random),
60 | );
61 | },
62 | });
63 |
64 | Deno.test({
65 | name: "Decode byte array from base-58 string with checksum",
66 | fn() {
67 | // Checksum-decode valid base-58 encoding
68 | assertEquals(
69 | Base58.decodeCheck("2L5B5yqsVG8Vt"),
70 | new Uint8Array([104, 101, 108, 108, 111]),
71 | );
72 |
73 | // Checksum-decode valid base-58 encoding with bad checksum
74 | assertThrows(() => Base58.decodeCheck("abcdefghij"), Error);
75 |
76 | // Checksum-decode empty string
77 | assertEquals(Base58.decodeCheck("3QJmnh"), new Uint8Array([]));
78 |
79 | // Checksum-decode invalid base-58 string ('l' is not in the base-58 alphabet)
80 | assertThrows(() => Base58.decodeCheck("malformed"), SyntaxError);
81 |
82 | // Checksum-decode with correct prefix
83 | assertEquals(
84 | Base58.decodeCheck("4HUtdVgL7ZXk3", new Uint8Array([1, 2, 3])),
85 | new Uint8Array([5, 6, 7]),
86 | );
87 |
88 | // Checksum-decode with incorrect prefix
89 | assertThrows(() =>
90 | Base58.decodeCheck("4HUtdVgL7ZXk3", new Uint8Array([2, 4, 6]))
91 | );
92 | },
93 | });
94 |
--------------------------------------------------------------------------------
/cli/test/test.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs/promises')
3 | const { tmpdir } = require('os')
4 | const { randomBytes } = require('crypto')
5 | const { Hex, blake2b } = require('@tzstamp/helpers')
6 | const { exec } = require('child_process')
7 | const { join, resolve } = require('path')
8 |
9 | const SERVERS = process.env.SERVERS || 'http://localhost:8080'
10 |
11 | void async function () {
12 |
13 | // Setup temp storage directory
14 | // This will be a folder with random suffix,
15 | // in the format of "tzstamp-xxxxxx" in your OS's default temp directory
16 | console.log('Creating temporary directory')
17 | let testDir
18 | if (process.env.TESTDIR) {
19 | await fs.mkdir(process.env.TESTDIR, {
20 | recursive: true
21 | })
22 | testDir = resolve(process.env.TESTDIR)
23 | } else {
24 | const tempRoot = join(tmpdir(), 'tzstamp-')
25 | testDir = await fs.mkdtemp(tempRoot)
26 | }
27 | console.log(`Created temporary directory at "${testDir}"`)
28 |
29 | // Create and store mock files
30 | // Files are named "fileN.dat" and stored in the temp directory
31 | // Each file is 1KB of random bytes
32 | console.log('Generating mock files')
33 | const files = []
34 | for (let i = 0; i < 6; ++i) {
35 | const contents = Uint8Array.from(randomBytes(1024))
36 | const fileName = `file${i}.dat`
37 | const filePath = path.join(testDir, fileName)
38 |
39 | // File entries
40 | files.push({
41 | name: fileName,
42 | path: filePath,
43 | hash: Hex.stringify(blake2b(contents)),
44 | contents
45 | })
46 |
47 | // Write to temp dir
48 | await fs.writeFile(filePath, contents)
49 | }
50 |
51 | console.log('Stamping single file')
52 | await execTzstamp(
53 | `stamp -d ${testDir} -s ${SERVERS} ${files[0].path}`
54 | )
55 |
56 | console.log('Stamping single hash')
57 | await execTzstamp(
58 | `stamp -d ${testDir} -s ${SERVERS} ${files[1].hash}`
59 | )
60 |
61 | console.log('Stamping multiple files and hash')
62 | await execTzstamp(
63 | `stamp -d ${testDir} -s ${SERVERS} ${files[2].path} ${files[3].path} ${files[4].hash} ${files[5].hash}`
64 | )
65 |
66 | console.log('Stamping and waiting for publication')
67 | await execTzstamp(
68 | `stamp --wait -d ${testDir} -s ${SERVERS} ${files[0].path}`
69 | )
70 |
71 | console.log('Verifying file against proof')
72 | await execTzstamp(
73 | `verify ${files[0].path} ${files[0].path + '.proof.json'}`
74 | )
75 |
76 | console.log('Verifying hash against proof')
77 | await execTzstamp(
78 | `verify ${files[1].hash} ${join(testDir, files[1].hash + '.proof.json')}`
79 | )
80 |
81 | console.log('All tests passed')
82 | process.exit(0)
83 |
84 | }().catch((error) => {
85 | console.error(error.message)
86 | process.exit(1)
87 | })
88 |
89 | function execTzstamp (args) {
90 | return new Promise((resolve, reject) => {
91 | exec(`node . ${args}`, (error, _, stderr) => {
92 | if (error) {
93 | reject(error)
94 | } else {
95 | resolve(stderr)
96 | }
97 | })
98 | })
99 | }
100 |
--------------------------------------------------------------------------------
/cli/src/manual_verify.js:
--------------------------------------------------------------------------------
1 | const { Hex } = require('@tzstamp/helpers')
2 | const { Blake2bOperation, Sha256Operation, JoinOperation, UnresolvedProof, AffixedProof } = require('@tzstamp/proof')
3 | const chalk = require('chalk')
4 | const Help = require('./help')
5 | const { getProof } = require('./helpers')
6 |
7 | async function handler (options) {
8 | // Early exits
9 | if (options._ == undefined || !options._.length) {
10 | return Help.handler({ _: [ 'manual-verify' ] })
11 | }
12 |
13 | const proofLocation = options._[0]
14 | let proof = await getProof(proofLocation, options.verbose)
15 | if (proof instanceof UnresolvedProof) {
16 | if (options.verbose) {
17 | console.log('Resolving partial proof')
18 | }
19 | proof = await proof.resolve()
20 | }
21 | if (!(proof instanceof AffixedProof)) {
22 | throw new Error('Proof is incomplete or damaged')
23 | }
24 |
25 | // Warn if not affixed to Mainnet
26 | if (!proof.mainnet) {
27 | console.log('\n' + chalk.red.bold`Warning`)
28 | console.log(`The proof is affixed to the alternative Tezos network "${proof.network}"`)
29 | console.log('It might not be trustworthy!\n')
30 | }
31 |
32 | // Print manual verification instructions
33 | let step = 1
34 | console.log(chalk.bold`Manual verification instructions`)
35 | console.log(`${step++}. Begin with hash ${Hex.stringify(proof.hash)}`)
36 | for (const operation of proof.operations) {
37 | if (operation instanceof Sha256Operation) {
38 | console.log(`${step++}. Take the SHA-256 hash.`)
39 | } else if (operation instanceof Blake2bOperation) {
40 | console.log(
41 | `${step++}. Take the BLAKE2b ${operation.key ? 'keyed ' : ' '}hash (${operation.length}-byte digest).`
42 | )
43 | if (operation.key) {
44 | console.log(`Key is 0x${Hex.stringify(operation.key)}`)
45 | }
46 | } else if (operation instanceof JoinOperation) {
47 | if (!operation.prepend && !operation.append) {
48 | continue
49 | }
50 | if (operation.prepend) {
51 | console.log(`${step++}. Prepend the bytes 0x${Hex.stringify(operation.prepend)}.`)
52 | }
53 | if (operation.append) {
54 | console.log(`${step++}. Append the bytes 0x${Hex.stringify(operation.append)}.`)
55 | }
56 | }
57 | }
58 | console.log(`${step++}. Prepend the bytes 0x0134.`)
59 | console.log(`${step++}. Base-58 encode with a checksum, as per Bitcoin's implementation.`)
60 | if (proof.mainnet) {
61 | console.log(`${step++}. Visit a trusted mainnet indexer.`)
62 | } else {
63 | console.log(`${step++}. Visit an indexer for network "${proof.network}".`)
64 | }
65 | console.log(`${step++}. Search for the Base-58 encoded block hash.`)
66 | console.log(`${step++}. Confirm that the timestamp recorded is ${proof.timestamp.toLocaleString()}.`)
67 | console.log()
68 | }
69 |
70 | module.exports = {
71 | handler,
72 | title: 'Manual Verify',
73 | description: 'Prints manual proof verification instructions.',
74 | usage: 'tzstamp manual-verify ',
75 | examples: [
76 | 'tzstamp manual-verify myFile.proof.json',
77 | 'tzstamp manual-verify https://example.com/myProof.json'
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/server/lib/aggregator.js:
--------------------------------------------------------------------------------
1 | const { MerkleTree } = require('@tzstamp/tezos-merkle')
2 | const { Hex } = require('@tzstamp/helpers')
3 | const { buildHighProof } = require('./proofs')
4 |
5 | /**
6 | * Aggregator context containing an active Merkle tree and publishing utilities
7 | */
8 | class Aggregator {
9 |
10 | /**
11 | * @param {import('./storage').ProofStorage} storage
12 | * @param {import('@taquito/rpc').RpcClient} rpc
13 | * @param {import('@taquito/taquito').ContractAbstraction} contract
14 | */
15 | constructor(storage, rpc, contract) {
16 | this.cycle()
17 | this.pendingProofs = new Set()
18 | this._storage = storage
19 | this._rpc = rpc
20 | this._contract = contract
21 | }
22 |
23 | /**
24 | * Resets the active Merkle tree.
25 | *
26 | * @returns the recent active Merkle tree
27 | */
28 | cycle() {
29 | const currentTree = this.merkleTree
30 | this.merkleTree = new MerkleTree({ deduplicate: true })
31 | return currentTree
32 | }
33 |
34 | /**
35 | * Publishes the aggregator root.
36 | */
37 | async publish() {
38 | if (this.merkleTree.size == 0) {
39 | console.log('Aggregator is empty. Skipping publication')
40 | return
41 | }
42 | try {
43 | const rpcUrl = this._rpc.getRpcUrl()
44 | const merkleTree = this.cycle()
45 | const payload = Hex.stringify(merkleTree.root)
46 | console.log(`Publishing aggregator root "${payload}" (${merkleTree.size} leaves)`)
47 | const [block, opGroup] = await this._invoke(payload)
48 | console.log(`Block hash is ${block.hash}`)
49 | console.log(`Saving proofs for root "${payload}" (${merkleTree.size} leaves)`)
50 | const highProof = await buildHighProof(block, opGroup.hash, merkleTree.root, rpcUrl)
51 | console.log(`Derivation: ${highProof.derivation}`)
52 | console.log(`Derivation Hex: ${Hex.stringify(highProof.derivation)}`)
53 | console.log(`Block Hash found using derivation: ${highProof.blockHash}`)
54 | await this._output(merkleTree, highProof)
55 | console.log(`Operation for root "${payload}" injected: https://tzkt.io/${opGroup.hash}`)
56 | } catch (err) {
57 | console.log(err)
58 | }
59 | }
60 |
61 | /**
62 | * Invokes the cached smart contract with the payload.
63 | *
64 | * @param {string} payload
65 | */
66 | async _invoke(payload) {
67 | const opGroup = await this._contract.methods.default(payload).send()
68 | const level = await opGroup.confirmation(3)
69 | const block = await this._rpc.getBlock({ block: level - 2 }) // 2 blocks before 3rd confirmation
70 | return [block, opGroup]
71 | }
72 |
73 | /**
74 | * Outputs proofs to storage.
75 | *
76 | * @param {MerkleTree} merkleTree
77 | * @param {import('./proof').AffixedProof} highProof
78 | */
79 | async _output(merkleTree, highProof) {
80 | for (const path of merkleTree.paths()) {
81 | const proof = path.toProof().concat(highProof)
82 | const proofId = Hex.stringify(path.leaf)
83 | await this._storage.storeProof(proof, proofId)
84 | this.pendingProofs.delete(proofId)
85 | }
86 | }
87 | }
88 |
89 | module.exports = {
90 | Aggregator
91 | }
92 |
--------------------------------------------------------------------------------
/manage/README.md:
--------------------------------------------------------------------------------
1 | # TzStamp Manage
2 |
3 | **tzstamp-manage** is a helper utility for managing and deploying instances of
4 | the tzstamp smart contract. This is especially useful for users who need to
5 | manage instances at scale, such as those working with multiple private chains.
6 | Access to a wallet is required to make use of deployment and manual uploading
7 | of merkle roots.
8 |
9 | For more information on using tzstamp-manage [see its manual page](man/man1/tzstamp-manage.md).
10 |
11 | ## Install on Debian 10.6
12 |
13 | Assuming a fresh install you'll need to `apt-get` some dependencies:
14 |
15 | sudo apt-get install git nodejs npm
16 |
17 | The global nodejs installation will have to be upgraded from Debian 10's default
18 | before you can run TzStamp:
19 |
20 | sudo npm --global --upgrade install npm
21 |
22 | sudo npm --global --upgrade install node
23 |
24 | Next clone the tzstamp contract management utility.
25 |
26 | git clone https://github.com/marigold-dev/tzstamp
27 | cd tzstamp/manage
28 |
29 | Install the dependencies for the management utility.
30 |
31 | npm install
32 |
33 | Before you can deploy a contract or upload a Merkle root, you'll need to
34 | [download a faucet key](https://faucet.tzalpha.net/) for the Tezos testnet.
35 |
36 | You're now ready to use the utility.
37 |
38 | ## Basic Use
39 |
40 | ### Deploying a smart contract
41 |
42 | TzStamp has a variety of possible backend smart contracts. However upgrades in
43 | the TzStamp proof protocol currently mean there's no upside to using any
44 | available contract besides 'noop', which accepts the bytes of a hash
45 | without storing it. TzStamp's proof protocol lets it avoid having to store the
46 | Merkle root on chain. Instead the TzStamp server constructs a long lived proof
47 | by showing the hash of this operation must be part of its parent block.
48 |
49 | Once [you have the faucet key](https://faucet.tzalpha.net/) you can deploy the 'noop' contract by running:
50 |
51 | ./index.js deploy noop --faucet tz1abAjdogmGma8EuSDE8xNbwfEtGAKMSrd4.json --node https://testnet-tezos.giganode.io
52 |
53 | [Use a chainviewer](https://better-call.dev/) to verify you've originated the
54 | contract.
55 |
56 | ### Manually upload a Merkle root
57 |
58 | In rare cases it may be desirable to manually push a Merkle root to the
59 | contract. For example if TzStamp crashes with a WIP tree that an administrator
60 | would like to upload in its current state. You can do that with the following
61 | commands:
62 |
63 | #### On TestNet
64 |
65 | tzstamp-manage upload-hash KT1AkQkRdLgE9NKSTCaPPZPgQuX7NUEtXzdj 84714a61037b3b4fa539008681cbfa97c7256930279ff4b54bad7366521afc67 --node https://testnet-tezos.giganode.io/ --faucet tz1MKs91KPzkpmZYz7Dvbd9dyq86murA1BrN.json
66 |
67 | #### On MainNet
68 |
69 | tzstamp-manage upload-hash KT1K5npkpWK6wxkcBg97dZD77c2J7DmWvxSb a4e9de2410c9e7c3ac4c57bbc18beedc5935d5c8118e345a72baee00a9820b67 --secret-path secret.txt
70 |
71 | ### Viewing Contract Storage
72 |
73 | tzstamp-manage view storage KT1AkQkRdLgE9NKSTCaPPZPgQuX7NUEtXzdj --node https://testnet-tezos.giganode.io/
74 |
75 | ### Checking If A Contract Is A TzStamp Instance
76 |
77 | **Note**: The stored hashes are currently out of date, do not rely on this to
78 | determine if a contract is a TzStamp contract or not.
79 |
80 | tzstamp-manage is-tzstamp KT1AkQkRdLgE9NKSTCaPPZPgQuX7NUEtXzdj --node https://testnet-tezos.giganode.io/
81 |
--------------------------------------------------------------------------------
/manage/test/test.js:
--------------------------------------------------------------------------------
1 | const { randomBytes } = require('crypto')
2 | const { Hex, blake2b } = require('@tzstamp/helpers')
3 | const { exec } = require('child_process')
4 |
5 | const { NODE_URI } = process.env
6 | const { FAUCET_PATH } = process.env
7 | const NODE_FLAG = `--node ${NODE_URI || 'https://testnet-tezos.giganode.io'}`
8 | const FAUCET_FLAG = `--faucet ${FAUCET_PATH}`
9 |
10 | void async function () {
11 |
12 | // Create and store mock files
13 | // Files are named "fileN.dat" and stored in the temp directory
14 | // Each file is 1KB of random bytes
15 | console.log('Generating mock file hashes')
16 | const fakeFileHashes = []
17 | for (let i = 0; i < 6; ++i) {
18 | const contents = Uint8Array.from(randomBytes(1024))
19 | fakeFileHashes.push(Hex.stringify(blake2b(contents)))
20 | }
21 |
22 | // Deploy kt1
23 | // Invoke "deploy" subcommand with variety of argument types
24 | // Move on once all commands have completed
25 | console.log('Deploying contracts')
26 | const deployments = [
27 | await deploy('noop') // $ tzstamp-manage deploy noop
28 | ]
29 |
30 | // Manual hash uploads
31 | // Invoke "upload-hash" subcommand on temporary file hashes
32 | console.log('Manually uploading hashes')
33 | for (const kt1 of deployments) {
34 | for (const fakeFileHash of fakeFileHashes) {
35 | await upload(kt1, fakeFileHash)
36 | }
37 | }
38 |
39 | console.log('All tests passed')
40 | process.exit(0)
41 |
42 | }().catch(handleError)
43 |
44 | /**
45 | * Originate a tzstamp contract
46 | */
47 | async function deploy (contractName) {
48 | const [ stdout, stderr ] = await execTzstamp(`deploy ${contractName}`)
49 | expect(!stderr, `Stderr while deploying ${contractName}: ${stderr}`)
50 | const kt1Regex = new RegExp('(KT1.+)...')
51 | const kt1 = stdout.match(kt1Regex)
52 | try {
53 | kt1[1]
54 | } catch (error) {
55 | throw new Error(`Wrong stdout while deploying ${contractName}: ${stdout}`)
56 | }
57 | expect(
58 | kt1[1].length == 36,
59 | `Wrong stdout while deploying ${contractName}: ${stdout}`
60 | )
61 | return kt1[1]
62 | }
63 |
64 | /**
65 | * Manually upload a hash to tzstamp contract
66 | */
67 | async function upload (kt1, uploadHash) {
68 | const [ stdout, stderr ] = await execTzstamp(`upload-hash ${kt1} ${uploadHash}`)
69 | expect(!stderr, `Stderr while uploading ${uploadHash}: ${stderr}`)
70 | const opRegex = new RegExp('Operation injected')
71 | const opMsg = stdout.match(opRegex)
72 | expect(
73 | opMsg[0] === 'Operation injected',
74 | `Wrong stdout while uploading ${uploadHash}: ${stdout}`
75 | )
76 | return
77 | }
78 |
79 | /**
80 | * Execute tzstamp-manage CLI command
81 | */
82 | function execTzstamp (args) {
83 | return new Promise((resolve, reject) => {
84 | exec(`node . ${NODE_FLAG} ${FAUCET_FLAG} ${args}`, (error, stdout, stderr) => {
85 | if (error) {
86 | reject(error)
87 | } else {
88 | resolve([ stdout, stderr ])
89 | }
90 | })
91 | })
92 | }
93 |
94 | /**
95 | * Expect truthy expression
96 | */
97 | function expect (expr, message) {
98 | if (!expr) {
99 | throw new Error(message)
100 | }
101 | }
102 |
103 | /**
104 | * Handle cleanup after test errors
105 | */
106 | async function handleError (error) {
107 | console.error(error.stack)
108 | process.exit(1)
109 | }
110 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tzstamp/tezos-merkle",
3 | "version": "0.3.4",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@tzstamp/tezos-merkle",
9 | "version": "0.3.4",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@tzstamp/helpers": "^0.3.4",
13 | "@tzstamp/proof": "^0.3.4"
14 | },
15 | "devDependencies": {
16 | "@types/node": "^15.0.2"
17 | }
18 | },
19 | "node_modules/@types/node": {
20 | "version": "15.0.2",
21 | "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
22 | "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
23 | "dev": true
24 | },
25 | "node_modules/@tzstamp/helpers": {
26 | "version": "0.3.4",
27 | "resolved": "https://registry.npmjs.org/@tzstamp/helpers/-/helpers-0.3.4.tgz",
28 | "integrity": "sha512-UM/Sd15xy5EBKJx5UZL/D2H1YETJxyTnbnmEO9zXiTTQ6DAlBuKdsihhspHqymySEznCRO9b6dHkaPkQsqM6Vg=="
29 | },
30 | "node_modules/@tzstamp/proof": {
31 | "version": "0.3.4",
32 | "resolved": "https://registry.npmjs.org/@tzstamp/proof/-/proof-0.3.4.tgz",
33 | "integrity": "sha512-K3/aMpDyolmtYH2KoNxTloFF3OSPn1K1MtdA76zmjOqnyQ9T00h2f4bbvpdBL4ONRPLR2wMcS74VYiFG/FwUtg==",
34 | "dependencies": {
35 | "@tzstamp/helpers": "^0.3.4",
36 | "jtd": "^0.1.1",
37 | "node-fetch": "^2.6.1"
38 | }
39 | },
40 | "node_modules/jtd": {
41 | "version": "0.1.1",
42 | "resolved": "https://registry.npmjs.org/jtd/-/jtd-0.1.1.tgz",
43 | "integrity": "sha512-D1unKfAE0A/sVLqJZs8mCSW812w651GJuV94WcXRAbx0Xv2j2yjW8qeO5pugXbTMX4k2/ojIH8kQ06LGj9QcaA=="
44 | },
45 | "node_modules/node-fetch": {
46 | "version": "2.6.1",
47 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
48 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
49 | "engines": {
50 | "node": "4.x || >=6.0.0"
51 | }
52 | }
53 | },
54 | "dependencies": {
55 | "@types/node": {
56 | "version": "15.0.2",
57 | "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
58 | "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
59 | "dev": true
60 | },
61 | "@tzstamp/helpers": {
62 | "version": "0.3.4",
63 | "resolved": "https://registry.npmjs.org/@tzstamp/helpers/-/helpers-0.3.4.tgz",
64 | "integrity": "sha512-UM/Sd15xy5EBKJx5UZL/D2H1YETJxyTnbnmEO9zXiTTQ6DAlBuKdsihhspHqymySEznCRO9b6dHkaPkQsqM6Vg=="
65 | },
66 | "@tzstamp/proof": {
67 | "version": "0.3.4",
68 | "resolved": "https://registry.npmjs.org/@tzstamp/proof/-/proof-0.3.4.tgz",
69 | "integrity": "sha512-K3/aMpDyolmtYH2KoNxTloFF3OSPn1K1MtdA76zmjOqnyQ9T00h2f4bbvpdBL4ONRPLR2wMcS74VYiFG/FwUtg==",
70 | "requires": {
71 | "@tzstamp/helpers": "^0.3.4",
72 | "jtd": "^0.1.1",
73 | "node-fetch": "^2.6.1"
74 | }
75 | },
76 | "jtd": {
77 | "version": "0.1.1",
78 | "resolved": "https://registry.npmjs.org/jtd/-/jtd-0.1.1.tgz",
79 | "integrity": "sha512-D1unKfAE0A/sVLqJZs8mCSW812w651GJuV94WcXRAbx0Xv2j2yjW8qeO5pugXbTMX4k2/ojIH8kQ06LGj9QcaA=="
80 | },
81 | "node-fetch": {
82 | "version": "2.6.1",
83 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
84 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/packages/helpers/blake2b.test.ts:
--------------------------------------------------------------------------------
1 | import { Blake2b } from "./blake2b.ts";
2 | import { assert, assertEquals, assertThrows } from "./dev_deps.ts";
3 |
4 | Deno.test({
5 | name: "Blake2b class interface",
6 | fn() {
7 | const hash = new Blake2b(undefined, 54);
8 | assert(!hash.finalized);
9 | assertEquals(hash.digestLength, 54);
10 | hash.update(new Uint8Array([]));
11 | assert(!hash.finalized);
12 | hash.digest();
13 | assert(hash.finalized);
14 | assertThrows(() => hash.update(new Uint8Array([])));
15 | assertThrows(() => hash.digest());
16 | assertThrows(() => new Blake2b(new Uint8Array(65)));
17 | assertThrows(() => new Blake2b(undefined, -1));
18 | assertThrows(() => new Blake2b(undefined, 65));
19 | },
20 | });
21 |
22 | Deno.test({
23 | name: "BLAKE2b test vectors",
24 | fn() {
25 | assertEquals(
26 | Blake2b.digest(new Uint8Array(), undefined, 64),
27 | // deno-fmt-ignore
28 | [
29 | 120, 106, 2, 247, 66, 1, 89, 3,
30 | 198, 198, 253, 133, 37, 82, 210, 114,
31 | 145, 47, 71, 64, 225, 88, 71, 97,
32 | 138, 134, 226, 23, 247, 31, 84, 25,
33 | 210, 94, 16, 49, 175, 238, 88, 83,
34 | 19, 137, 100, 68, 147, 78, 176, 75,
35 | 144, 58, 104, 91, 20, 72, 183, 85,
36 | 213, 111, 112, 26, 254, 155, 226, 206,
37 | ],
38 | );
39 | assertEquals(
40 | Blake2b.digest(new TextEncoder().encode("abc"), undefined, 64),
41 | // deno-fmt-ignore
42 | [
43 | 186, 128, 165, 63, 152, 28, 77, 13,
44 | 106, 39, 151, 182, 159, 18, 246, 233,
45 | 76, 33, 47, 20, 104, 90, 196, 183,
46 | 75, 18, 187, 111, 219, 255, 162, 209,
47 | 125, 135, 197, 57, 42, 171, 121, 45,
48 | 194, 82, 213, 222, 69, 51, 204, 149,
49 | 24, 211, 138, 168, 219, 241, 146, 90,
50 | 185, 35, 134, 237, 212, 0, 153, 35,
51 | ],
52 | );
53 | assertEquals(
54 | Blake2b.digest(
55 | Uint8Array.from({ length: 255 }, (_, i) => +i),
56 | undefined,
57 | 64,
58 | ),
59 | // deno-fmt-ignore
60 | [
61 | 91, 33, 197, 253, 136, 104, 54, 118,
62 | 18, 71, 79, 162, 231, 14, 156, 250,
63 | 34, 1, 255, 238, 232, 250, 250, 181,
64 | 121, 122, 213, 143, 239, 161, 124, 155,
65 | 91, 16, 125, 164, 163, 219, 99, 32,
66 | 186, 175, 44, 134, 23, 213, 165, 29,
67 | 249, 20, 174, 136, 218, 56, 103, 194,
68 | 212, 31, 12, 193, 79, 166, 121, 40,
69 | ],
70 | );
71 | assertEquals(
72 | Blake2b.digest(
73 | Uint8Array.from({ length: 255 }, (_, i) => +i),
74 | Uint8Array.from({ length: 64 }, (_, i) => +i),
75 | 64,
76 | ),
77 | // deno-fmt-ignore
78 | [
79 | 20, 39, 9, 214, 46, 40, 252, 204,
80 | 208, 175, 151, 250, 208, 248, 70, 91,
81 | 151, 30, 130, 32, 29, 197, 16, 112,
82 | 250, 160, 55, 42, 164, 62, 146, 72,
83 | 75, 225, 193, 231, 59, 161, 9, 6,
84 | 213, 209, 133, 61, 182, 164, 16, 110,
85 | 10, 123, 249, 128, 13, 55, 61, 109,
86 | 238, 45, 70, 214, 46, 242, 164, 97,
87 | ],
88 | );
89 | assertEquals(
90 | Blake2b.digest(new Uint8Array()),
91 | // deno-fmt-ignore
92 | [
93 | 14, 87, 81, 192, 38, 229, 67, 178,
94 | 232, 171, 46, 176, 96, 153, 218, 161,
95 | 209, 229, 223, 71, 119, 143, 119, 135,
96 | 250, 171, 69, 205, 241, 47, 227, 168,
97 | ],
98 | );
99 | },
100 | });
101 |
--------------------------------------------------------------------------------
/cli/src/verify.js:
--------------------------------------------------------------------------------
1 | const Help = require('./help')
2 | const chalk = require('chalk')
3 | const { getHash, getProof, getNode, getIndexer } = require('./helpers')
4 | const { compare } = require('@tzstamp/helpers')
5 | const { UnresolvedProof, AffixedProof, VerifyStatus } = require('@tzstamp/proof')
6 |
7 | async function handler (options) {
8 | // Early exits
9 | if (options._ == undefined || options._.length < 2) {
10 | return Help.handler({ _: [ 'verify' ] })
11 | }
12 |
13 | // Get the hash and proof
14 | const [ target, proofLocation ] = options._
15 | const hash = await getHash(target, options.verbose)
16 | let proof = await getProof(proofLocation, options.verbose)
17 | if (!compare(hash, proof.hash)) {
18 | throw new Error('Proof is unrelated to the file hash')
19 | }
20 | if (proof instanceof UnresolvedProof) {
21 | if (options.verbose) {
22 | console.log('Resolving partial proof')
23 | }
24 | proof = await proof.resolve()
25 | }
26 | if (!(proof instanceof AffixedProof)) {
27 | throw new Error('Proof is incomplete or damaged')
28 | }
29 |
30 | // Warn if not affixed to Mainnet
31 | if (!proof.mainnet) {
32 | console.log('\n' + chalk.red.bold`Warning`)
33 | console.log(`The proof is affixed to the alternative Tezos network "${proof.network}"`)
34 | console.log('It might not be trustworthy!\n')
35 | }
36 |
37 | // Verify proof
38 | const node = options.node || getNode(proof.network)
39 | const indexer = getIndexer(proof.network)
40 | try {
41 | if (options.verbose) {
42 | console.log(chalk.dim`Querying node ${node} for the header of block ${proof.blockHash}\n`)
43 | }
44 | const verifyResult = await proof.verify(node)
45 | if (!verifyResult.verified) {
46 | console.log(chalk.red.bold`Could not verify proof`)
47 | switch (verifyResult.status) {
48 | case VerifyStatus.NetError:
49 | console.log('A network error occurred')
50 | break
51 | case VerifyStatus.NotFound:
52 | console.log('The block hash asserted by the proof was not found')
53 | console.log(`Asserted block hash: ${proof.blockHash}`)
54 | console.log(`Node queried: ${node}`)
55 | break
56 | case VerifyStatus.Mismatch:
57 | console.log('The proof timestamp does not match on-chain timestamp')
58 | break
59 | }
60 | process.exit(1)
61 | }
62 | console.log(chalk.green.bold`Verified`)
63 | console.log(`Target: ${target}`)
64 | console.log(`Hash existed at ${proof.timestamp.toLocaleString()}`)
65 | console.log(`Block hash: ${proof.blockHash}`)
66 | console.log(`Node queried: ${node}`)
67 | if (indexer) {
68 | console.log(`Verify block on TzKT: <${new URL(proof.blockHash, indexer).toString()}>`)
69 | }
70 | console.log()
71 | } catch (error) {
72 | throw new Error(`Could not verify proof: ${error.message}`)
73 | }
74 | }
75 |
76 | const parseOptions = {
77 | string: [
78 | 'node'
79 | ],
80 | alias: {
81 | node: 'n'
82 | }
83 | }
84 |
85 | module.exports = {
86 | handler,
87 | parseOptions,
88 | title: 'Verify',
89 | description: 'Verifies that a file or hash existed at the time asserted by a proof.',
90 | usage: 'tzstamp verify ',
91 | remarks: [
92 | 'Will print out a warning if the proof is affixed to an alternative network.'
93 | ],
94 | options: [
95 | [ '--node, -n', 'Sets a custom Tezos node server' ]
96 | ],
97 | examples: [
98 | 'tzstamp verify myFile.txt myFile.proof.json',
99 | 'tzstamp verify --node https://rpc.example.com/ myFile.txt https://example.com/myProof.json',
100 | 'tzstamp verify rawHashProof.json'
101 | ]
102 | }
103 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # TzStamp Server
2 |
3 | tzstamp is a [cryptographic timestamping service](https://www.gwern.net/Timestamping)
4 | that uses the Tezos blockchain to prove a file existed at or before a certain time.
5 |
6 | **tzstamp-server** is the software implementing the tzstamp service. Files are
7 | hashed using the SHA-256 or blake2b algorithm and then submitted to the server.
8 | TzStamp aggregates the hashes in a merkle tree structure, or a tree of hashes.
9 | The root of that tree is published to a smart contract on the Tezos blockchain,
10 | allowing many hashes to be shown to have existed at a certain time on-chain for
11 | the cost of one. A further benefit of the merkle tree structure is that users
12 | can prove their hash was included in the merkle root without relying on tzstamp
13 | to store their submitted hash long term. Once the root is published, users download an
14 | inclusion proof consisting of the nodes necessary to derive a merkle root given
15 | the submitted hash. Because nodes are stored as a binary tree, the number of
16 | nodes necessary to derive the root grows logarithmically to the tree size,
17 | ensuring that the proof stays small even as the server begins to handle millions
18 | or billions of entries.
19 |
20 | See https://www.gwern.net/Timestamping for more background information on
21 | trusted timestamping services.
22 |
23 | For more information on using tzstamp-server [see its manual page](https://github.com/marigold-dev/tzstamp/blob/main/server/man/man1/tzstamp-server.md).
24 |
25 | ## Setup TestNet TzStamp On Debian 10.6
26 |
27 | Assuming a fresh install you'll need to `apt-get` some dependencies:
28 |
29 | sudo apt-get install git nodejs npm
30 |
31 | The global nodejs installation will have to be upgraded from Debian 10's default
32 | before you can run TzStamp:
33 |
34 | sudo npm --global --upgrade install npm
35 |
36 | sudo npm --global --upgrade install node
37 |
38 | Before you can deploy a contract, you'll need to [download a faucet
39 | key](https://faucet.tzalpha.net/) for the Tezos testnet. Make sure to remember
40 | where you store this key as we'll use it in the following steps.
41 |
42 | Next clone the tzstamp contract management utility.
43 |
44 | git clone https://github.com/marigold-dev/tzstamp
45 | cd tzstamp/manage
46 |
47 | Install the dependencies for the management utility.
48 |
49 | npm install
50 |
51 | Deploy the 'noop' contract.
52 |
53 | ./index.js deploy noop --faucet tz1abAjdogmGma8EuSDE8xNbwfEtGAKMSrd4.json --node https://testnet-tezos.giganode.io
54 |
55 | [Use a chainviewer](https://better-call.dev/) to verify you've originated the
56 | contract. Write down the KT1, you'll use it in the server configuration.
57 |
58 | Clone the tzstamp server software.
59 |
60 | git clone https://github.com/marigold-dev/tzstamp
61 | cd tzstamp/server
62 |
63 | Install the tzstamp server dependencies.
64 |
65 | npm install
66 |
67 | Create a `.env` config file in the project root. Here is an
68 | example setup:
69 |
70 | PORT=80
71 | BASE_URL=https://tzstamp.example.com/ # Base URL for the API
72 | SECRET=edpk... # Tezos wallet secret
73 | CONTRACT_ADDRESS=KT1AtaeG5PhuFyyivbfPZRUBkVMiqyxpo2cH # On mainnet
74 | SCHEDULE="0 0 * * *" # Publish at midnight every day
75 |
76 | Used variables are as follows:
77 | - `PROOFS_DIR`: Directory to store proofs in. Defaults to `"proofs"`. Can be an absolute or relative path.
78 | - `PORT`: API server port to listen on. Defaults to `"8000"`.
79 | - `BASE_URL`: Base URL for the API. Used in responses containing a dynamic endpoint. Does not need to match the server port if, for example, a reverse proxy is being used. Defaults to `"http://localhost:8000"`
80 | - `KEY_FILE`: Path to JSON key file.
81 | - `SECRET`: Bare secret key. Takes precedence over `KEY_KEY`.
82 | - `CONTRACT_ADDRESS`: "KT1..." smart contract address. Defaults to `"KT1AtaeG5PhuFyyivbfPZRUBkVMiqyxpo2cH"`.
83 | - `RPC_URL`: Tezos node RPC base URL. Defaults to `"https://mainnet.tezos.marigold.dev"`. Any public or private accessible Tezos RPC may be used.
84 | - `SCHEDULE`: 5- or 6-field [cron expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). Defaults to `"0 0 * * *"`, which is daily at midnight, local to the server.
85 |
86 | One of `SECRET` or `KEY_FILE` must be set.
87 |
88 | Run the server.
89 |
90 | npm start
91 |
92 | ## Setup TestNet TzStamp On MacOS
93 |
94 | See Debian 10.6 instructions, [but use homebrew](https://brew.sh/) to install nodejs instead.
95 |
96 | brew install node
97 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/merkletree.test.ts:
--------------------------------------------------------------------------------
1 | import { MerkleTree } from "./merkletree.ts";
2 | import { Blake2b, concat } from "./deps.ts";
3 | import { assert, assertEquals, assertThrows } from "./dev_deps.ts";
4 |
5 | const blocks: Uint8Array[] = Array.from(
6 | { length: 7 },
7 | () => crypto.getRandomValues(new Uint8Array(32)),
8 | );
9 |
10 | const manualRoot = hashcat(
11 | hashcat(
12 | hashcat(
13 | Blake2b.digest(blocks[0]),
14 | Blake2b.digest(blocks[1]),
15 | ),
16 | hashcat(
17 | Blake2b.digest(blocks[2]),
18 | Blake2b.digest(blocks[3]),
19 | ),
20 | ),
21 | hashcat(
22 | hashcat(
23 | Blake2b.digest(blocks[4]),
24 | Blake2b.digest(blocks[5]),
25 | ),
26 | hashcat(
27 | Blake2b.digest(blocks[6]),
28 | Blake2b.digest(blocks[6]),
29 | ),
30 | ),
31 | );
32 |
33 | function hashcat(a: Uint8Array, b: Uint8Array): Uint8Array {
34 | return Blake2b.digest(concat(a, b));
35 | }
36 |
37 | Deno.test({
38 | name: "Merkle tree appending",
39 | fn() {
40 | const merkleTree = new MerkleTree();
41 |
42 | // Single append
43 | merkleTree.append(blocks[0]);
44 | assertEquals(merkleTree.size, 1);
45 |
46 | // Multi append
47 | merkleTree.append(blocks[1], blocks[2]);
48 | assertEquals(merkleTree.size, 3);
49 |
50 | // Deduplication
51 | const dedupeMerkleTree = new MerkleTree({ deduplicate: true });
52 | dedupeMerkleTree.append(
53 | blocks[0],
54 | blocks[0],
55 | );
56 | assertEquals(dedupeMerkleTree.size, 1);
57 | },
58 | });
59 |
60 | Deno.test({
61 | name: "Merkle tree root derivation",
62 | fn() {
63 | const merkleTree = new MerkleTree();
64 | merkleTree.append(...blocks);
65 |
66 | // Compare progressively calculated root
67 | assertEquals(merkleTree.root, manualRoot);
68 |
69 | const sequence: Uint8Array[] = [];
70 | for (let i = 0; i < 65; ++i) {
71 | sequence.push(new Uint8Array([i]));
72 | }
73 |
74 | const bigMerkleTree = new MerkleTree();
75 | bigMerkleTree.append(...sequence);
76 |
77 | // Calculate big manual root
78 | const raise = (array: Uint8Array[], length: number): Uint8Array[] => {
79 | const result = [];
80 | for (let i = 0; i < length; i += 2) {
81 | result.push(hashcat(
82 | array[i] ?? array[array.length - 1],
83 | array[i + 1] ?? array[array.length - 1],
84 | ));
85 | }
86 | return result;
87 | };
88 |
89 | const L0 = sequence.map((item) => Blake2b.digest(item));
90 | const L1 = raise(L0, 128);
91 | const L2 = raise(L1, 64);
92 | const L3 = raise(L2, 32);
93 | const L4 = raise(L3, 16);
94 | const L5 = raise(L4, 8);
95 | const L6 = raise(L5, 4);
96 | const manualBigRoot = hashcat(L6[0], L6[1]);
97 |
98 | // Large tail
99 | assertEquals(bigMerkleTree.root, manualBigRoot);
100 | },
101 | });
102 |
103 | Deno.test({
104 | name: "Merkle tree convenience accessors",
105 | fn() {
106 | const merkleTree = new MerkleTree();
107 | merkleTree.append(...blocks);
108 |
109 | // Access leaves layer
110 | assertEquals(merkleTree.size, blocks.length);
111 |
112 | // Check for leaf inclusion
113 | assert(merkleTree.has(blocks[2]));
114 | assert(merkleTree.has(Uint8Array.from(blocks[3])));
115 | assert(!merkleTree.has(new Uint8Array([0, 55])));
116 | },
117 | });
118 |
119 | Deno.test({
120 | name: "Merkle tree path generation",
121 | fn() {
122 | const merkleTree = new MerkleTree();
123 | merkleTree.append(...blocks);
124 |
125 | // Get out-of-bound paths
126 | assertThrows(() => merkleTree.path(2.5), RangeError);
127 | assertThrows(() => merkleTree.path(-1), RangeError);
128 | assertThrows(() => merkleTree.path(1000), RangeError);
129 |
130 | // Iteratively test each path
131 | for (const path of merkleTree.paths()) {
132 | let result = path.leaf;
133 | for (const { hash, relation } of path.siblings) {
134 | switch (relation) {
135 | case "left":
136 | result = hashcat(hash, result);
137 | break;
138 | case "right":
139 | result = hashcat(result, hash);
140 | break;
141 | }
142 | }
143 | assertEquals(result, path.root);
144 | assertEquals(path.root, merkleTree.root);
145 | }
146 | },
147 | });
148 |
149 | Deno.test({
150 | name: "Path to proof",
151 | fn() {
152 | const merkleTree = new MerkleTree();
153 | merkleTree.append(...blocks);
154 | assertEquals(merkleTree.root, manualRoot);
155 | for (const path of merkleTree.paths()) {
156 | const proof = path.toProof();
157 | assertEquals(proof.hash, path.block);
158 | assertEquals(proof.derivation, manualRoot);
159 | }
160 | },
161 | });
162 |
--------------------------------------------------------------------------------
/upgrade/tzstamp-upgrade.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S deno run --allow-read
2 |
3 | import { parse } from "https://deno.land/std@0.97.0/flags/mod.ts";
4 | import { createHash } from "https://deno.land/std@0.97.0/hash/mod.ts";
5 | import { unreachable } from "https://deno.land/std@0.97.0/testing/asserts.ts";
6 | import { Schema, validate } from "https://deno.land/x/jtd@v0.1.0/mod.ts";
7 | import { Hex } from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/helpers/mod.ts";
8 | import {
9 | AffixedProof,
10 | Blake2bOperation,
11 | JoinOperation,
12 | Operation,
13 | Sha256Operation,
14 | } from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/proof/mod.ts";
15 |
16 | const options = parse(Deno.args, {
17 | string: [
18 | "version",
19 | "proof",
20 | "hash",
21 | "file",
22 | "timestamp",
23 | ],
24 | });
25 |
26 | function isValid(schema: Schema, instance: unknown): instance is T {
27 | return validate(schema, instance).length == 0;
28 | }
29 |
30 | const numverProofSchema: Schema = {
31 | properties: {
32 | version: { type: "uint32" },
33 | },
34 | additionalProperties: true,
35 | };
36 |
37 | interface NumverProofTemplate {
38 | version: number;
39 | }
40 |
41 | const version0ProofSchema: Schema = {
42 | properties: {
43 | version: { type: "uint32" },
44 | network: { type: "string" },
45 | ops: {
46 | elements: {
47 | elements: { type: "string" },
48 | },
49 | },
50 | },
51 | };
52 |
53 | interface Version0ProofTemplate extends NumverProofTemplate {
54 | version: 0;
55 | network: string;
56 | ops: string[][];
57 | }
58 |
59 | async function upgradeVersion0(): Promise {
60 | if (
61 | !options.proof ||
62 | !options.timestamp ||
63 | (!options.file && !options.hash)
64 | ) {
65 | console.log(
66 | "Version 0 usage: tzstamp-upgrade --version 0 --timestamp --proof [--file | --hash ]",
67 | );
68 | Deno.exit(2);
69 | }
70 | if (options.file && options.hash) {
71 | throw new Error("Supply either file or hash, not both");
72 | }
73 |
74 | let timestamp: Date;
75 | try {
76 | timestamp = new Date(options.timestamp);
77 | } catch (error) {
78 | throw new Error(`Unable to parse timestamp: ${error.message}`);
79 | }
80 |
81 | // Get version 0 proof
82 | let v0Proof: Version0ProofTemplate;
83 | try {
84 | const text = await Deno.readTextFile(options.proof);
85 | const template: unknown = JSON.parse(text);
86 | if (
87 | !isValid(numverProofSchema, template) ||
88 | template.version != 0 ||
89 | !isValid(version0ProofSchema, template)
90 | ) {
91 | throw new Error("Invalid version 0 proof template");
92 | }
93 | v0Proof = template;
94 | } catch (error) {
95 | throw new Error(`Unable to read proof: ${error.message}`);
96 | }
97 |
98 | // Get hash
99 | let hash: Uint8Array;
100 | try {
101 | if (options.file) {
102 | const buffer = await Deno.readFile(options.file);
103 | hash = new Uint8Array(
104 | createHash("sha256")
105 | .update(buffer)
106 | .digest(),
107 | );
108 | } else if (options.hash) {
109 | hash = Hex.parse(options.hash);
110 | } else {
111 | unreachable();
112 | }
113 | } catch (error) {
114 | throw new Error(`Unable to get hash: ${error.message}`);
115 | }
116 |
117 | // Build operations
118 | const operations: Operation[] = [];
119 | for (let i = 0; i < v0Proof.ops.length; ++i) {
120 | const op = v0Proof.ops[i][0];
121 | switch (op) {
122 | case "blake2b":
123 | operations.push(new Blake2bOperation());
124 | break;
125 | case "sha-256":
126 | operations.push(new Sha256Operation());
127 | break;
128 | case "prepend": {
129 | const prepend = Hex.parse(v0Proof.ops[i][1]);
130 | let append = undefined;
131 | if (v0Proof.ops[i + 1][0] == "append") {
132 | append = Hex.parse(v0Proof.ops[i + 1][1]);
133 | ++i;
134 | }
135 | operations.push(new JoinOperation({ prepend, append }));
136 | break;
137 | }
138 | case "append":
139 | operations.push(
140 | new JoinOperation({
141 | append: Hex.parse(v0Proof.ops[i][1]),
142 | }),
143 | );
144 | }
145 | }
146 |
147 | // Create proof
148 | return new AffixedProof({
149 | hash,
150 | operations,
151 | network: v0Proof.network,
152 | timestamp,
153 | });
154 | }
155 |
156 | async function upgrade() {
157 | if (!options.version) {
158 | console.log("Usage: tzstamp-upgrade --version [options]");
159 | console.log("Available versions are: 0");
160 | Deno.exit(2);
161 | }
162 | let proof: AffixedProof;
163 | switch (options.version) {
164 | case "0":
165 | proof = await upgradeVersion0();
166 | break;
167 | default:
168 | console.log(`Unsupported version "${options.version}"`);
169 | Deno.exit(1);
170 | }
171 | console.log(JSON.stringify(proof));
172 | }
173 |
174 | await upgrade().catch((error) => {
175 | console.error(error.message);
176 | Deno.exit(1);
177 | });
178 |
--------------------------------------------------------------------------------
/packages/helpers/sha256.ts:
--------------------------------------------------------------------------------
1 | /** Initialization vector */
2 | const IV = [
3 | 0x6a09e667,
4 | 0xbb67ae85,
5 | 0x3c6ef372,
6 | 0xa54ff53a,
7 | 0x510e527f,
8 | 0x9b05688c,
9 | 0x1f83d9ab,
10 | 0x5be0cd19,
11 | ] as const;
12 |
13 | /** Round constants */
14 | // deno-fmt-ignore
15 | const K = [
16 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
17 | 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
18 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
19 | 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
20 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
21 | 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
22 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
23 | 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
24 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
25 | 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
26 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
27 | 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
28 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
29 | 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
30 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
31 | 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
32 | ] as const;
33 |
34 | /**
35 | * [SHA-256] streaming hash function.
36 | *
37 | * ```js
38 | * const message = new TextEncoder().encode("hello");
39 | * const hash = new Sha256();
40 | *
41 | * hash.update(message);
42 | * hash.finalized; // false
43 | *
44 | * hash.digest(); // Uint8Array(32)
45 | * hash.finalized; // true
46 | * ```
47 | *
48 | * [SHA-256]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
49 | */
50 | export class Sha256 {
51 | private state = new ArrayBuffer(32);
52 | private buffer = new ArrayBuffer(64);
53 | private final = false;
54 | private counter = 0;
55 | private size = 0n;
56 |
57 | /** Returns true if the hash function is finalized. */
58 | get finalized(): boolean {
59 | return this.final;
60 | }
61 |
62 | constructor() {
63 | const state = new DataView(this.state);
64 | for (let i = 0; i < 8; ++i) {
65 | state.setUint32(i * 4, IV[i], false);
66 | }
67 | }
68 |
69 | /**
70 | * Feeds input into the hash function in 64 byte blocks.
71 | * Throws is the hash function is finalized.
72 | *
73 | * @param input Input bytes
74 | */
75 | update(input: Uint8Array): this {
76 | if (this.final) {
77 | throw new Error("Cannot update finalized hash function.");
78 | }
79 | const buffer = new Uint8Array(this.buffer);
80 | this.size += BigInt(input.length * 8);
81 | for (let i = 0; i < input.length; ++i) {
82 | buffer[this.counter++] = input[i];
83 | if (this.counter == 64) {
84 | this.compress();
85 | this.counter = 0;
86 | }
87 | }
88 | return this;
89 | }
90 |
91 | private compress(): void {
92 | const state = new DataView(this.state);
93 | const buffer = new DataView(this.buffer);
94 |
95 | // Logical functions
96 | const rotate = (x: number, y: number) => x >>> y | x << 32 - y;
97 | const choose = (x: number, y: number, z: number) => x & y ^ ~x & z;
98 | const majority = (x: number, y: number, z: number) => x & y ^ x & z ^ y & z;
99 | const Σ0 = (x: number) => rotate(x, 2) ^ rotate(x, 13) ^ rotate(x, 22);
100 | const Σ1 = (x: number) => rotate(x, 6) ^ rotate(x, 11) ^ rotate(x, 25);
101 | const σ0 = (x: number) => rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3);
102 | const σ1 = (x: number) => rotate(x, 17) ^ rotate(x, 19) ^ (x >>> 10);
103 |
104 | // Prepare the message schedule
105 | const W = new Uint32Array(64);
106 | for (let i = 0; i < 16; ++i) {
107 | W[i] = buffer.getUint32(i * 4, false);
108 | }
109 | for (let i = 16; i < 64; ++i) {
110 | W[i] = σ1(W[i - 2]) + W[i - 7] + σ0(W[i - 15]) + W[i - 16];
111 | }
112 |
113 | // Initialize working state
114 | const h = new Uint32Array(8);
115 | for (let i = 0; i < 8; ++i) {
116 | h[i] = state.getUint32(i * 4, false);
117 | }
118 |
119 | // 64 rounds
120 | for (let i = 0; i < 64; ++i) {
121 | const T1 = h[7] + Σ1(h[4]) + choose(h[4], h[5], h[6]) + K[i] + W[i];
122 | const T2 = Σ0(h[0]) + majority(h[0], h[1], h[2]);
123 | h[7] = h[6];
124 | h[6] = h[5];
125 | h[5] = h[4];
126 | h[4] = h[3] + T1;
127 | h[3] = h[2];
128 | h[2] = h[1];
129 | h[1] = h[0];
130 | h[0] = T1 + T2;
131 | }
132 |
133 | // Update state
134 | for (let i = 0; i < 8; ++i) {
135 | const word = state.getUint32(i * 4, false) + h[i];
136 | state.setUint32(i * 4, word, false);
137 | }
138 | }
139 |
140 | /** Finalizes the state and produces a digest. */
141 | digest(): Uint8Array {
142 | const state = new Uint8Array(this.state);
143 | if (this.final) {
144 | return state;
145 | }
146 | const buffer = new DataView(this.buffer);
147 | buffer.setUint8(this.counter++, 128);
148 | if (this.counter > 56) {
149 | while (this.counter < 64) {
150 | buffer.setUint8(this.counter++, 0);
151 | }
152 | this.compress();
153 | this.counter = 0;
154 | }
155 | while (this.counter < 56) {
156 | buffer.setUint8(this.counter++, 0);
157 | }
158 | buffer.setBigUint64(56, this.size, false);
159 | this.compress();
160 | this.final = true;
161 | return state;
162 | }
163 |
164 | /**
165 | * Produces an immediate SHA-256 digest.
166 | *
167 | * @param input Input bytes
168 | */
169 | static digest(input: Uint8Array): Uint8Array {
170 | return new Sha256()
171 | .update(input)
172 | .digest();
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/packages/proof/readme.md:
--------------------------------------------------------------------------------
1 | # TzStamp Proofs
2 |
3 | Cryptographic timestamp proofs for use with [TzStamp tools](https://tzstamp.io).
4 |
5 | TzStamp's proof protocol works by:
6 |
7 | 1. Constructing a local Merkle tree of user submitted hashes.
8 | 2. Publishing the root of that tree in a Tezos blockchain transaction.
9 | 3. Constructing a chain of commitments starting from the local Merkle leaves up
10 | to the hash of the Tezos block containing the transaction.
11 |
12 | The use of a local Merkle tree allows users to verify their proofs without
13 | storing many unrelated hashes or relying on a central server. Tezos uses its own
14 | Merkle Tree structure internally for operations and blocks. TzStamp 'bridges'
15 | its local Merkle Tree with the on-chain tree by deriving a chain of commitments
16 | between them. The chain of commitments approach lets TzStamp proofs avoid
17 | relying on indexers. This is useful both for internal development reasons (at
18 | time of writing Tezos indexers don't share a standard API) and for reducing the
19 | attack surface of the overall TzStamp system. Leveraging the Tezos Merkle Trees
20 | in this way also lets the smart contract used for publishing hashes avoid
21 | storing any data at all. Instead a no-op function accepting the hash as an
22 | argument can be called. The resulting operation will stay in node archives long
23 | enough for TzStamp to bridge its local tree root with the on-chain Merkle Tree.
24 | At that point the proof is no longer reliant on Tezos nodes keeping a copy of
25 | the operation at all, so it doesn't concern a TzStamp instance if the nodes
26 | garbage collect it.
27 |
28 |
29 |
30 | ## Usage
31 |
32 | ```js
33 | // Node + NPM
34 | const {/*...*/} = require("@tzstamp/proof");
35 |
36 | // Deno
37 | import {/*...*/} from "https://raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/proof/mod.ts";
38 | ```
39 |
40 | See the
41 | [full reference documentation here](https://doc.deno.land/https/raw.githubusercontent.com/marigold-dev/tzstamp/0.3.4/proof/mod.ts).
42 |
43 | ### Constructing a proof
44 |
45 | Build an array of operations for the proof:
46 |
47 | ```js
48 | const {
49 | Sha256Operation,
50 | Blake2bOperation,
51 | JoinOperation,
52 | } = require("@tzstamp/proof");
53 |
54 | // Create operations
55 | const operations = [
56 | // Hash operations
57 | new Sha256Operation(),
58 | new Blake2bOperation(32),
59 | new Blake2bOperation(64, myKey),
60 |
61 | // Join operations
62 | new JoinOperation({ prepend: uint8Array }),
63 | new JoinOperation({ append: uint8Array }),
64 | ];
65 | ```
66 |
67 | Create a proof:
68 |
69 | ```js
70 | const { Proof } = require("@tzstamp/proof");
71 |
72 | // Proof segment
73 | const proof = Proof.create({
74 | hash: myInputHash, // Uint8Array
75 | operations,
76 | }); // Proof{}
77 |
78 | // Proof affixed to the Tezos blockchain
79 | // The derivation of this proof is taken to be a raw Tezos block hash.
80 | const affixedProof = Proof.create({
81 | hash: myInputHash,
82 | operations,
83 | network: "NetXdQprcVkpaWU", // Tezos network identifier
84 | timestamp: new Date("1970-01-01T00:00:00.000Z"), // Timestamp asserted by proof
85 | }); // AffixedProof{}
86 |
87 | // Remote resolvable proof
88 | // The proof is to be concatenated with the proof segment published at the remote address
89 | const unresolvedProof = Proof.create({
90 | hash: myInputHash,
91 | operations: [...],
92 | remote: "https://tzstamp.example.com/proof/...",
93 | }); // UnresolvedProof{}
94 | ```
95 |
96 | ### Verifying affixed proofs
97 |
98 | Affixed proofs (`AffixedProof` subclass) may be verified against a Tezos RPC.
99 |
100 | ```js
101 | if (proof.isAffixed()) {
102 | proof.blockHash; // Base58 encoded block hash
103 | proof.mainnet; // Indicates that the affixed network is the Tezos Mainnet
104 |
105 | const result = await proof.verify(rpcURL);
106 | // Ex:
107 | // VerifyResult { verified: false, status: "notFound", message: "Derived block could not be found"}
108 | // VerifyResult { verified: true, status: "verified", message: "Verified proof" }
109 | }
110 | ```
111 |
112 | ### Concatenating proof segments
113 |
114 | Long proofs may be constructed in segments and concatenated. Concatenation is
115 | only possible if the derivation of the first proof (the output of each operation
116 | applied to its input hash) matched the input hash of the second proof.
117 |
118 | ```js
119 | const proofA = Proof.create({
120 | hash: inputHash,
121 | operations: [/*...*/],
122 | });
123 |
124 | const proofB = Proof.create({
125 | hash: midHash,
126 | operations: [/*...*/],
127 | });
128 |
129 | const proofAB = proofA.concat(proofB); // Throws if `proofA.derivation` is not equal to `proofB.hash`
130 | ```
131 |
132 | ### Resolving unresolved proofs
133 |
134 | Unresolved proofs (`UnresolvedProof` subclass) may be resolved by fetching the
135 | next proof segment from a remote server and concatenating.
136 |
137 | ```js
138 | if (proof.isUnresolved()) {
139 | const fullProof = await proof.resolve();
140 | }
141 | ```
142 |
143 | ### Serializing and Deserializing
144 |
145 | Note: The `tzstamp-proof` v0.3.0 release supports _version 1_ proof templates.
146 | Use the `tzstamp-proof` v0.1.0 release for _version 0_ proof templates, and the
147 | Demo releases for pre-version proof templates.
148 |
149 | ```js
150 | // Deserialize from JSON
151 | const json = fs.readFileSync("my-proof.json", "utf-8");
152 | const proof = Proof.from(json);
153 |
154 | // Serialize to JSON
155 | const json = JSON.stringify(proof);
156 | fs.writeFileSync("my-proof.json", json);
157 | ```
158 |
159 | ## License
160 |
161 | [MIT](license.txt)
162 |
--------------------------------------------------------------------------------
/packages/helpers/base58.ts:
--------------------------------------------------------------------------------
1 | import { Sha256 } from "./sha256.ts";
2 | import { compare, concat } from "./bytes.ts";
3 |
4 | const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
5 |
6 | /** Mismatched prefix error */
7 | export class PrefixError extends Error {
8 | name = "PrefixError";
9 | }
10 |
11 | /** Mismatched checksum error */
12 | export class ChecksumError extends Error {
13 | name = "ChecksumError";
14 | }
15 |
16 | /**
17 | * Base58 string validation regular expression.
18 | * Tests a string against the common Base58 alphabet
19 | * as defined in the the [Base58 Encoding Scheme].
20 | *
21 | * [Base58 Encoding Scheme]: https://tools.ietf.org/id/draft-msporny-base58-01.html#alphabet
22 | */
23 | export const validator = /^[1-9A-HJ-NP-Za-km-z]*$/;
24 |
25 | /**
26 | * Encodes a byte array payload as a Base58 string
27 | * as described in the [Base58 Encoding Scheme].
28 | *
29 | * ```js
30 | * Base58.encode(new Uint8Array([55, 66, 77]));
31 | * // "KZXr"
32 | * ```
33 | *
34 | * [Base58 Encoding Scheme]: https://tools.ietf.org/id/draft-msporny-base58-01.html#encode
35 | *
36 | * @param payload Byte array to encode
37 | */
38 | export function encode(payload: Uint8Array): string {
39 | // Empty array
40 | if (payload.length == 0) {
41 | return "";
42 | }
43 |
44 | // Convert to integer
45 | let int = 0n;
46 | for (const byte of payload) {
47 | int = BigInt(byte) + (int << 8n);
48 | }
49 |
50 | let encoding = "";
51 |
52 | // Encode as base-58
53 | for (let n = int; n > 0n; n /= 58n) {
54 | const mod = Number(n % 58n);
55 | encoding = ALPHABET[mod] + encoding;
56 | }
57 |
58 | // Prepend padding for leading zeroes in the byte array
59 | for (let i = 0; payload[i] == 0; ++i) {
60 | encoding = ALPHABET[0] + encoding;
61 | }
62 |
63 | return encoding;
64 | }
65 |
66 | /**
67 | * Decodes a Base58 string to a byte array payload
68 | * as described in the [Base58 Encoding Scheme].
69 | *
70 | * Throws `SyntaxError` if the input string contains letters
71 | * not included in the [Base58 Alphabet].
72 | *
73 | * ```js
74 | * Base58.decode("u734C");
75 | * // Uint8Array(4) [ 35, 37, 31, 49 ]
76 | * ```
77 | *
78 | * [Base58 Alphabet]: https://tools.ietf.org/id/draft-msporny-base58-01.html#alphabet
79 | * [Base58 Encoding Scheme]: https://tools.ietf.org/id/draft-msporny-base58-01.html#decode
80 | *
81 | * @param string Base58 string to decode
82 | */
83 | export function decode(string: string): Uint8Array {
84 | // Validate string
85 | if (!validator.test(string)) {
86 | throw new SyntaxError(`Invalid Base58 string`);
87 | }
88 |
89 | // Convert to integer
90 | let int = 0n;
91 | for (const char of string) {
92 | const index = ALPHABET.indexOf(char);
93 | int = int * 58n + BigInt(index);
94 | }
95 |
96 | const bytes: number[] = [];
97 |
98 | // Construct byte array
99 | for (let n = int; n > 0n; n /= 256n) {
100 | bytes.push(Number(n % 256n));
101 | }
102 |
103 | // Prepend leading zeroes
104 | for (let i = 0; string[i] == ALPHABET[0]; ++i) {
105 | bytes.push(0);
106 | }
107 |
108 | return new Uint8Array(bytes.reverse());
109 | }
110 |
111 | /**
112 | * Encodes a byte array payload as a Base58
113 | * string with a checksum.
114 | *
115 | * See the [Bitcoin source code] for the
116 | * original C++ implementation.
117 | *
118 | * ```js
119 | * Base58.encodeCheck(new Uint8Array([55, 66, 77]));
120 | * // "36TSqepyLV"
121 | * ```
122 | *
123 | * Optionally, a prefix can be specified, which
124 | * will be concatenated with the payload before
125 | * encoding.
126 | *
127 | * ```js
128 | * Base58.encodeCheck(
129 | * new Uint8Array([55, 66, 77]),
130 | * new Uint8Array([22, 33, 44]), // prefix
131 | * );
132 | * // "2F7PrbRwKSeYvf"
133 | * ```
134 | *
135 | * [Bitcoin source code]: https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp#L135
136 | *
137 | * @param payload Byte array to encode
138 | * @param prefix Optional prefix bytes
139 | */
140 | export function encodeCheck(
141 | payload: Uint8Array,
142 | prefix = new Uint8Array(),
143 | ): string {
144 | const input = concat(prefix, payload);
145 | const checksum = Sha256.digest(Sha256.digest(input)).slice(0, 4);
146 | return encode(concat(input, checksum));
147 | }
148 |
149 | /**
150 | * Decodes and validates a Base58 string with a
151 | * checksum to a byte array.
152 | *
153 | * Throws `ChecksumError` if the checksum does not match.
154 | *
155 | * Throws `SyntaxError` if the input string contains
156 | * letters not included in the [Base58 Alphabet].
157 | *
158 | * See the [Bitcoin source code] for the
159 | * original C++ implementation.
160 | *
161 | * ```js
162 | * Base58.decodeCheck("6sx8oP1Sgpe");
163 | * // Uint8Array(4) [ 35, 37, 31, 49 ]
164 | * ```
165 | *
166 | * Optionally, a prefix can be specified, which will
167 | * be separated from the payload after decoding.
168 | *
169 | * Throws `PrefixError` if the prefix does not match
170 | * the leading decoded bytes.
171 | *
172 | * ```js
173 | * Base58.decodeCheck(
174 | * "2dWKxb85CS1cWb5mm",
175 | * new Uint8Array([ 86, 88, 92, 84 ])
176 | * );
177 | * // Uint8Array(4) [ 35, 37, 31, 49 ]
178 | * ```
179 | *
180 | * [Bitcoin source code]: https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp#L144
181 | *
182 | * @param string Base58 string to decode
183 | * @param prefix Optional prefix bytes
184 | */
185 | export function decodeCheck(string: string, prefix = new Uint8Array()) {
186 | const raw = decode(string);
187 | const prefixedPayload = raw.slice(0, -4);
188 | const checksum = Sha256.digest(Sha256.digest(prefixedPayload)).slice(0, 4);
189 |
190 | // Validate checksum
191 | if (!compare(checksum, raw.slice(-4))) {
192 | throw new ChecksumError("Base58 checksum does not match");
193 | }
194 |
195 | // Check prefix
196 | if (!compare(prefixedPayload.slice(0, prefix.length), prefix)) {
197 | throw new PrefixError("Prefix bytes do not match");
198 | }
199 |
200 | return prefixedPayload.slice(prefix.length);
201 | }
202 |
--------------------------------------------------------------------------------
/server/lib/api.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa')
2 | const Router = require('@koa/router')
3 | const bodyParser = require('koa-bodyparser')
4 | const { Hex, Blake2b } = require('@tzstamp/helpers')
5 | const { timeout } = require('cron')
6 | const cors = require('@koa/cors');
7 |
8 | /**
9 | * @typedef {import('./storage').ProofStorage} ProofStorage
10 | * @typedef {import('./aggregator').Aggregator} Aggregator
11 | */
12 |
13 | /**
14 | * Configures the RESTful API
15 | *
16 | * @param {Koa.Middleware} stampHandler
17 | * @param {Koa.Middleware} proofHandler
18 | * @param {Koa.Middleware} statusHandler
19 | */
20 | exports.configureAPI = async function (stampHandler, proofHandler, statusHandler) {
21 | const router = new Router()
22 | .post('/stamp', stampHandler)
23 | .get('/proof/:id', proofHandler)
24 | .get('/status', statusHandler)
25 | const app = new Koa()
26 | .use(cors())
27 | .use(errorHandler)
28 | .use(bodyParser())
29 | .use(router.routes())
30 | .use(router.allowedMethods({ throw: true }))
31 | return app
32 | }
33 |
34 | /**
35 | * Generates a handler for POST requests to the "/stamp" endpoint.
36 | *
37 | * @param {string} baseURL
38 | * @param {Aggregator} aggregator
39 | * @param {string} schedule
40 | * @returns {Koa.Middleware}
41 | */
42 | exports.stampHandler = function (baseURL, aggregator, schedule) {
43 | return (ctx) => {
44 |
45 | // Validate input
46 | const hashHex = ctx.request.body.data
47 | ctx.assert(hashHex != undefined, 400, 'Data field is missing')
48 | ctx.assert(typeof hashHex == 'string', 400, 'Data field is wrong type')
49 | ctx.assert(hashHex.length, 400, 'Data field is empty')
50 | ctx.assert(Hex.validator.test(hashHex), 400, 'Data field is not a hexadecimal string')
51 | ctx.assert(hashHex.length <= 128, 400, 'Data field is larger than 64 bytes')
52 |
53 | const hash = Hex.parse(hashHex)
54 | const proofId = Hex.stringify(
55 | Blake2b.digest(hash)
56 | )
57 |
58 | // Aggregate hash for publication
59 | if (!aggregator.pendingProofs.has(proofId)) {
60 | aggregator.merkleTree.append(hash)
61 | aggregator.pendingProofs.add(proofId)
62 | }
63 |
64 | // Respond that the proof is pending
65 | const acceptedType = ctx.accepts('text/plain', 'application/json')
66 | const url = new URL(`/proof/${proofId}`, baseURL)
67 | ctx.status = 202
68 | const secondsToPublication = (timeout(schedule) / 1000).toFixed(0)
69 | switch (acceptedType) {
70 | default:
71 | case 'text/plain':
72 | ctx.type = 'text/plain'
73 | ctx.body = `${url}\nTime to publication (seconds): ${secondsToPublication}\n`
74 | break
75 | case 'application/json':
76 | ctx.type = 'application/json'
77 | ctx.body = {
78 | url,
79 | secondsToPublication
80 | }
81 | }
82 | }
83 | }
84 |
85 | /**
86 | * Generates a handler for GET requests to the "/proof" endpoint.
87 | *
88 | * @param {string} baseURL
89 | * @param {ProofStorage} storage
90 | * @param {Aggregator} aggregator
91 | * @returns {Koa.Middleware}
92 | */
93 | exports.proofHandler = function (baseURL, storage, aggregator) {
94 | return async (ctx) => {
95 | const proofId = ctx.params.id
96 | ctx.assert(Hex.validator.test(proofId), 400, 'Invalid proof ID')
97 |
98 | if (aggregator.pendingProofs.has(proofId)) {
99 | // Respond that proof is pending
100 | const acceptedType = ctx.accepts('text/plain', 'application/json')
101 | const url = new URL(`/proof/${proofId}`, baseURL)
102 | ctx.status = 202
103 | switch (acceptedType) {
104 | default:
105 | case 'text/plain':
106 | ctx.type = 'text/plain'
107 | ctx.body = 'Pending\n'
108 | break
109 | case 'application/json':
110 | ctx.type = 'application/json'
111 | ctx.body = {
112 | url,
113 | status: 'Pending'
114 | }
115 | }
116 | } else {
117 | // Respond with proof from storage
118 | try {
119 | const proof = await storage.getProof(proofId)
120 | ctx.body = proof.toJSON()
121 | } catch (error) {
122 | ctx.assert(error.code == 'ENOENT', 500, 'Error fetching proof')
123 | ctx.throw(404, 'Proof not found')
124 | }
125 | }
126 | }
127 | }
128 |
129 | /**
130 | * Generates a handler for GET requests to the "/status" endpoint.
131 | *
132 | * @param {string} network
133 | * @param {string} contractAddress
134 | * @param {string} schedule
135 | * @returns {Koa.Middleware}
136 | */
137 | exports.statusHandler = function (network, contractAddress, schedule) {
138 | return (ctx) => {
139 | const acceptedType = ctx.accepts('text/plain', 'application/json')
140 | const secondsToPublication = (timeout(schedule) / 1000).toFixed(0)
141 | switch (acceptedType) {
142 | default:
143 | case 'text/plain':
144 | ctx.body = 'Proof Version: 1\n'
145 | ctx.body += `Network: ${network}\n`
146 | ctx.body += `Contract: ${contractAddress}\n`
147 | ctx.body += `Time to publication (seconds): ${secondsToPublication}\n`
148 | break
149 | case 'application/json':
150 | ctx.body = {
151 | proofVersion: 1,
152 | network,
153 | contract: contractAddress,
154 | secondsToPublication
155 | }
156 | }
157 | }
158 | }
159 |
160 | /**
161 | * Handles errors throw by downstream middleware
162 | */
163 | async function errorHandler (ctx, next) {
164 | try {
165 | await next()
166 | if (ctx.status == 404) {
167 | ctx.throw(404, 'Not found')
168 | }
169 | } catch (error) {
170 | const acceptedType = ctx.accepts('text/plain', 'application/json')
171 | ctx.status = error.status || 500
172 | switch (acceptedType) {
173 | default:
174 | case 'text/plain':
175 | ctx.body = error.message + '\n'
176 | break
177 | case 'application/json':
178 | ctx.body = { error: error.message }
179 | break
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/packages/proof/operation.ts:
--------------------------------------------------------------------------------
1 | import { Blake2b, concat, Hex, Sha256 } from "./deps.ts";
2 | import { isValid, operationSchema } from "./schemas.ts";
3 |
4 | /**
5 | * Operation template
6 | */
7 | export type OperationTemplate =
8 | | JoinTemplate
9 | | Blake2bTemplate
10 | | Sha256Template;
11 |
12 | /**
13 | * Proof operation
14 | */
15 | export abstract class Operation {
16 | /**
17 | * Represents the operation as a human friendly string.
18 | */
19 | abstract toString(): string;
20 |
21 | /**
22 | * Converts the operation to a JSON-serializable template.
23 | */
24 | abstract toJSON(): OperationTemplate;
25 |
26 | /**
27 | * Commits the operation to the input.
28 | */
29 | abstract commit(input: Uint8Array): Uint8Array;
30 |
31 | /**
32 | * Creates a subclassed operation from a template object.
33 | * Throws `InvalidTemplateError` if the template is invalid.
34 | * Throws `UnsupportedOperationError` if operation is not supported.
35 | *
36 | * ```ts
37 | * Operation.from({
38 | * type: "sha256",
39 | * });
40 | * // Sha256Operation {}
41 | * ```
42 | *
43 | * @param template Template object
44 | */
45 | static from(template: unknown): Operation {
46 | if (!isValid(operationSchema, template)) {
47 | throw new SyntaxError("Invalid operation template");
48 | }
49 | switch (template.type) {
50 | case "join":
51 | return new JoinOperation({
52 | prepend: template.prepend ? Hex.parse(template.prepend) : undefined,
53 | append: template.append ? Hex.parse(template.append) : undefined,
54 | });
55 | case "blake2b":
56 | return new Blake2bOperation(
57 | template.length,
58 | template.key ? Hex.parse(template.key) : undefined,
59 | );
60 | case "sha256":
61 | return new Sha256Operation();
62 | }
63 | }
64 | }
65 |
66 | /**
67 | * Join operation template
68 | */
69 | export interface JoinTemplate {
70 | type: "join";
71 | prepend?: string;
72 | append?: string;
73 | }
74 |
75 | /**
76 | * Join operation constructor options
77 | */
78 | export interface JoinOptions {
79 | prepend?: Uint8Array;
80 | append?: Uint8Array;
81 | }
82 |
83 | /**
84 | * Join operation
85 | */
86 | export class JoinOperation extends Operation {
87 | /**
88 | * Data to prepend
89 | */
90 | readonly prepend: Uint8Array;
91 |
92 | /**
93 | * Data to append
94 | */
95 | readonly append: Uint8Array;
96 |
97 | /**
98 | * Throws `TypeError` if no data is set.
99 | *
100 | * @param prepend Data to prepend
101 | * @param append Data to append
102 | */
103 | constructor({ prepend, append }: JoinOptions) {
104 | super();
105 | this.prepend = prepend ?? new Uint8Array();
106 | this.append = append ?? new Uint8Array();
107 | }
108 |
109 | toString(): string {
110 | const prependString = this.prepend.length
111 | ? `Prepend 0x${Hex.stringify(this.prepend)}`
112 | : "";
113 | const conjunction = this.prepend.length && this.append.length ? ", " : "";
114 | const appendString = this.append.length
115 | ? `Append 0x${Hex.stringify(this.append)}`
116 | : "";
117 | return prependString + conjunction + appendString;
118 | }
119 |
120 | toJSON(): JoinTemplate {
121 | const template: JoinTemplate = { type: "join" };
122 | if (this.prepend.length) {
123 | template.prepend = Hex.stringify(this.prepend);
124 | }
125 | if (this.append.length) {
126 | template.append = Hex.stringify(this.append);
127 | }
128 | return template;
129 | }
130 |
131 | commit(input: Uint8Array): Uint8Array {
132 | return concat(this.prepend, input, this.append);
133 | }
134 | }
135 |
136 | /**
137 | * BLAKE2b hash operation template
138 | */
139 | export interface Blake2bTemplate {
140 | type: "blake2b";
141 | length?: number;
142 | key?: string;
143 | }
144 |
145 | /**
146 | * [BLAKE2b] hash operation
147 | *
148 | * [BLAKE2b]: https://www.blake2.net
149 | */
150 | export class Blake2bOperation extends Operation {
151 | readonly length?: number;
152 | readonly key?: Uint8Array;
153 |
154 | /**
155 | * Throws a `RangeError` if either of the optional
156 | * `length` or `key` fields are incompatible with the
157 | * [BLAKE2b] hashing algorithm.
158 | *
159 | * @param length Digest length. Defaults to 32
160 | * @param key Hashing key
161 | *
162 | * [BLAKE2b]: https://www.blake2.net
163 | */
164 | constructor(length = 32, key?: Uint8Array) {
165 | super();
166 | if (length < 0 || length > 64) {
167 | throw new RangeError("BLAKE2b digest length must be between 0-64 bytes.");
168 | }
169 | if (key && key.length > 64) {
170 | throw new RangeError(
171 | "BLAKE2b key length must be no longer than 64 bytes.",
172 | );
173 | }
174 | this.length = length;
175 | this.key = key;
176 | }
177 |
178 | toString(): string {
179 | return `BLAKE2b hash, ${this.length}-byte digest` +
180 | (this.key ? ` with key 0x${Hex.stringify(this.key)}` : "");
181 | }
182 |
183 | toJSON(): Blake2bTemplate {
184 | const template: Blake2bTemplate = { type: "blake2b" };
185 | if (this.length != 32) {
186 | template.length = this.length;
187 | }
188 | if (this.key) {
189 | template.key = Hex.stringify(this.key);
190 | }
191 | return template;
192 | }
193 |
194 | commit(input: Uint8Array): Uint8Array {
195 | return Blake2b.digest(input, this.key, this.length);
196 | }
197 | }
198 |
199 | /**
200 | * SHA-256 hash operation template
201 | */
202 | export interface Sha256Template {
203 | type: "sha256";
204 | }
205 |
206 | /**
207 | * SHA-256 hash operation
208 | */
209 | export class Sha256Operation extends Operation {
210 | toString(): string {
211 | return "SHA-256 hash";
212 | }
213 |
214 | toJSON(): OperationTemplate {
215 | return { type: "sha256" };
216 | }
217 |
218 | commit(input: Uint8Array): Uint8Array {
219 | const digest = Sha256.digest(input);
220 | return new Uint8Array(digest);
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/packages/tezos-merkle/merkletree.ts:
--------------------------------------------------------------------------------
1 | import { Blake2b, concat, Hex } from "./deps.ts";
2 | import { Path, Sibling } from "./path.ts";
3 |
4 | /**
5 | * Tezos-style Merkle tree constructor options
6 | */
7 | export interface MerkleTreeOptions {
8 | deduplicate?: boolean;
9 | }
10 |
11 | /**
12 | * Appendable Tezos-style Merkle tree
13 | *
14 | * Based on the Merkle tree implementation found within the
15 | * [Tezos source code](https://gitlab.com/tezos/tezos/-/blob/master/src/lib_crypto/blake2B.ml).
16 | *
17 | * The hashing algorithm is BLAKE2b with 32-byte digests. The last
18 | * leaf is implicitly duplicated until the tree is perfect. The root
19 | * of an empty tree is the BLAKE2b digest of no input.
20 | *
21 | * Appends have a logarithmic time complexity.
22 | */
23 | export class MerkleTree {
24 | private blockSet = new Set();
25 | private blocks: Uint8Array[] = [];
26 | private layers: Uint8Array[][] = [[]];
27 | private readonly deduplicate: boolean;
28 |
29 | /**
30 | * @param deduplicate Deduplicate leaves. Defaults to false
31 | */
32 | constructor({ deduplicate = false }: MerkleTreeOptions = {}) {
33 | this.deduplicate = deduplicate;
34 | }
35 |
36 | /**
37 | * Root hash
38 | */
39 | get root(): Uint8Array {
40 | return this.blocks.length
41 | ? this.layers[this.layers.length - 1][0]
42 | : Blake2b.digest(new Uint8Array());
43 | }
44 |
45 | /**
46 | * The number of leaves included within the Merkle tree
47 | */
48 | get size(): number {
49 | return this.blocks.length;
50 | }
51 |
52 | /**
53 | * Appends data blocks to the Merkle tree.
54 | *
55 | * ```js
56 | * const merkleTree = new MerkleTree();
57 | *
58 | * merkleTree.append(
59 | * new Uint8Array([ 1, 2 ]),
60 | * new Uint8Array([ 3, 4 ]),
61 | * new Uint8Array([ 5, 6 ]),
62 | * );
63 | *
64 | * merkleTree.size; // 3
65 | * ```
66 | *
67 | * Merkle trees configured to deduplicate blocks will silently
68 | * drop previously-included blocks:
69 | *
70 | * ```js
71 | * const merkleTree = new MerkleTree({
72 | * deduplicate: true,
73 | * });
74 | *
75 | * merkleTree.append(
76 | * new Uint8Array([ 1, 2 ]),
77 | * new Uint8Array([ 1, 2 ]), // deduplicated
78 | * );
79 | *
80 | * merkleTree.size; // 1
81 | * ```
82 | *
83 | * Appends have logarithmic time-complexity. Internally, the
84 | * last leaf is implicitly duplicated until the tree is perfect.
85 | *
86 | * @param blocks Data blocks
87 | */
88 | append(...blocks: Uint8Array[]): void {
89 | for (const block of blocks) {
90 | // Deduplicate leaves already in tree
91 | const blockHex = Hex.stringify(block);
92 | if (this.deduplicate && this.blockSet.has(blockHex)) {
93 | continue;
94 | }
95 | this.blocks.push(block);
96 | this.blockSet.add(blockHex);
97 |
98 | // Create leaf
99 | const leaf = Blake2b.digest(block);
100 |
101 | // Create cursor variables
102 | let index = this.layers[0].length;
103 | let height = 0;
104 |
105 | // Create a "tail" variable, representing the repeated
106 | // leaf nodes extrapolated to the current layer height
107 | let computeTail = true;
108 | let tail = leaf;
109 |
110 | // Create leaf
111 | this.layers[0].push(leaf);
112 |
113 | // While the current layer doesn't contain the root
114 | while (this.layers[height].length > 1) {
115 | // Calculate parent node
116 | const concatenation = (index % 2)
117 | ? concat(this.layers[height][index - 1], this.layers[height][index]) // Concat with left sibling
118 | : concat(this.layers[height][index], tail); // There is no right sibling; concat with tail
119 | const parent = Blake2b.digest(concatenation);
120 |
121 | // Advance cursor
122 | index = Math.floor(index / 2);
123 | ++height;
124 |
125 | // Create new layer if needed
126 | if (this.layers[height] == undefined) {
127 | this.layers[height] = [];
128 | }
129 |
130 | // Store parent node
131 | this.layers[height][index] = parent;
132 |
133 | // Stop computing the tail once the remaining layers are balanced
134 | if (Math.log2(this.layers[height].length) % 1 == 0) {
135 | computeTail = false;
136 | }
137 |
138 | // Advance tail
139 | if (computeTail) {
140 | tail = Blake2b.digest(concat(tail, tail));
141 | }
142 | }
143 | }
144 | }
145 |
146 | /**
147 | * Checks if a block is included in the tree.
148 | *
149 | * @param block Data block
150 | */
151 | has(block: Uint8Array): boolean {
152 | const blockHex = Hex.stringify(block);
153 | return this.blockSet.has(blockHex);
154 | }
155 |
156 | /**
157 | * Calculates the path from a leaf at the given index to the root hash.
158 | * Throws `RangeError` if there is no leaf with the given index.
159 | *
160 | * @param index Index of the leaf.
161 | */
162 | path(index: number): Path {
163 | if (!(index in this.blocks)) {
164 | throw new RangeError("Leaf index is out of range");
165 | }
166 |
167 | const siblings: Sibling[] = [];
168 | let height = 0;
169 | let cursor = index;
170 | let tail = this.layers[0][this.layers[0].length - 1];
171 |
172 | // Step through each layer, except the highest
173 | while (height < this.layers.length - 1) {
174 | // Add path sibling
175 | const relation = cursor % 2 ? "left" : "right";
176 | const hash = relation == "left"
177 | ? this.layers[height][cursor - 1]
178 | : this.layers[height][cursor + 1] ?? tail;
179 | siblings.push({ hash, relation });
180 |
181 | ++height;
182 | cursor = Math.floor(cursor / 2);
183 | tail = Blake2b.digest(concat(tail, tail));
184 | }
185 |
186 | return new Path({
187 | block: this.blocks[index],
188 | siblings,
189 | root: this.root,
190 | });
191 | }
192 |
193 | /**
194 | * Generates all leaf-to-root paths in the Merkle tree.
195 | */
196 | *paths(): Generator {
197 | for (const index in this.layers[0]) {
198 | yield this.path(+index);
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/cli/src/helpers.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises')
2 | const chalk = require('chalk')
3 | const { createReadStream } = require('fs')
4 | const fetch = require('node-fetch')
5 | const { createHash } = require('crypto')
6 | const { Hex } = require('@tzstamp/helpers')
7 | const { Proof } = require('@tzstamp/proof')
8 | const { cwd } = require('process')
9 | const { dirname, resolve } = require('path')
10 |
11 | /**
12 | * Valid 32-64 byte hexadecimal string
13 | */
14 | const VALID_HASH = /^[0-9a-f]{64,128}$/i
15 |
16 | /**
17 | * Valid http(s) address
18 | */
19 | const VALID_HTTP = /^https?:\/\//
20 |
21 | function sha256Async(stream) {
22 | return new Promise((resolve, reject) => {
23 | if (stream.readableEnded) {
24 | reject(new Error('Stream has ended'))
25 | }
26 | const hash = createHash('SHA256')
27 | stream
28 | .on('data', data => hash.update(data))
29 | .on('end', () => resolve(new Uint8Array(hash.digest())))
30 | .on('error', reject)
31 | })
32 | }
33 |
34 | function hashFile(path) {
35 | const stream = createReadStream(path)
36 | return sha256Async(stream)
37 | }
38 |
39 | /**
40 | * Normalizes a raw hexadecimal hash or file path into a hash byte array
41 | *
42 | * @param {Promise} target File path or file hash
43 | */
44 | async function getHash(target, verbose) {
45 | try {
46 | if (VALID_HASH.test(target)) {
47 | return Hex.parse(target)
48 | } else {
49 | if (verbose) {
50 | console.log(chalk.dim`Hashing file ${target}`)
51 | }
52 | return await hashFile(target)
53 | }
54 | } catch (error) {
55 | throw new Error(`Could not ${VALID_HASH.test(target) ? 'parse' : 'get file'} hash: ${error.message}`)
56 | }
57 | }
58 |
59 | async function fetchProofText(url) {
60 | const response = await fetch(url, {
61 | headers: {
62 | accept: 'application/json'
63 | }
64 | })
65 | switch (response.status) {
66 | case 200:
67 | return await response.text()
68 | case 202:
69 | throw new Error('Requested proof is pending publication')
70 | case 404:
71 | throw new Error('Requested proof could not be found')
72 | default:
73 | throw new Error(`Bad server response ${response.statusText}`)
74 | }
75 | }
76 |
77 | /**
78 | * Retrieves and parses a proof from a local file or remote URL.
79 | *
80 | * @param {string} location Filepath or URL
81 | * @returns {Promise}
82 | */
83 | async function getProof(location, verbose) {
84 | // Get proof text
85 | let text
86 | if (VALID_HTTP.test(location)) {
87 | if (verbose) {
88 | console.log(chalk.dim`Fetching proof from URL ${location}`)
89 | }
90 | try {
91 | text = await fetchProofText(location)
92 | } catch (error) {
93 | throw new Error(`Error fetching proof: ${error.message}`)
94 | }
95 | } else {
96 | if (verbose) {
97 | console.log(chalk.dim`Reading proof from file ${location}`)
98 | }
99 | try {
100 | text = await fs.readFile(location, 'utf-8')
101 | } catch (error) {
102 | throw new Error(`Error reading proof file: ${error.message}`)
103 | }
104 | }
105 |
106 | // Parse proof
107 | let json
108 | if (verbose) {
109 | console.log(chalk.dim`Parsing proof`)
110 | }
111 | try {
112 | json = JSON.parse(text)
113 | } catch (_) {
114 | throw new Error(
115 | `${VALID_HTTP.test(location)
116 | ? 'Fetched proof'
117 | : 'Proof file'
118 | } is not valid JSON`
119 | )
120 | }
121 | if (json instanceof Object && json.version === 0) {
122 | throw new Error(
123 | 'Unable to parse version 0 proofs. Use to upgrade the proofs manually.'
124 | )
125 | }
126 | return Proof.from(json)
127 | }
128 |
129 | /**
130 | * Gets an appropriate fallback Tezos node.
131 | *
132 | * @param {string} network
133 | * @returns {string} Tezos node URL
134 | */
135 | function getNode(network) {
136 | switch (network) {
137 | case 'NetXdQprcVkpaWU':
138 | return 'https://mainnet.tezos.marigold.dev/'
139 | case 'NetXnHfVqm9iesp':
140 | return 'https://ithacanet.tezos.marigold.dev/'
141 | case 'NetXLH1uAxK7CCh':
142 | return 'https://jakartanet.tezos.marigold.dev/'
143 | }
144 | }
145 |
146 | /**
147 | * Gets an appropriate indexer.
148 | *
149 | * @param {string} network
150 | * @returns {string|undefined} Indexer URL
151 | */
152 | function getIndexer(network) {
153 | switch (network) {
154 | case 'NetXdQprcVkpaWU':
155 | return 'http://tzkt.io/'
156 | case 'NetXnHfVqm9iesp':
157 | return 'https://ithacanet.tzkt.io/'
158 | case 'NetXLH1uAxK7CCh':
159 | return 'https://jakartanet.tzkt.io/'
160 | }
161 | }
162 |
163 | function* nameCandidates(base) {
164 | yield `${base}.proof.json`
165 | for (let i = 1; ; ++i) {
166 | yield `${base}-${i}.proof.json`
167 | }
168 | }
169 |
170 | function* safeNames(target, directory, entries) {
171 | for (const name of nameCandidates(target)) {
172 | if (entries.includes(name)) {
173 | continue
174 | }
175 | yield resolve(directory, name)
176 | }
177 | }
178 |
179 | /**
180 | * Creates a generator of safe file names to store proofs under.
181 | *
182 | * @param {string} fileOrDir
183 | * @returns {Promise>} File name
184 | */
185 | async function getSafeNames(target, directory, verbose) {
186 | // Resolve directory
187 | let dir = directory
188 | if (directory == undefined) {
189 | if (VALID_HASH.test(target)) {
190 | dir = cwd()
191 | } else {
192 | const fullTarget = resolve(target)
193 | if (verbose) {
194 | console.log(chalk.dim`Inspecting path ${fullTarget}`)
195 | }
196 | const stat = await fs.stat(fullTarget)
197 | dir = stat.isDirectory()
198 | ? target
199 | : dirname(fullTarget)
200 | }
201 | }
202 |
203 | // Get directory listing
204 | if (verbose) {
205 | console.log(chalk.dim`Reading directory ${dir}`)
206 | }
207 | const entries = await fs.readdir(dir)
208 |
209 | // Produce safe name
210 | return safeNames(target, dir, entries)
211 | }
212 |
213 | module.exports = {
214 | getHash,
215 | getProof,
216 | getNode,
217 | getIndexer,
218 | getSafeNames
219 | }
220 |
--------------------------------------------------------------------------------
/cli/src/stamp.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises')
2 | const { randomBytes } = require('crypto')
3 | const { Hex } = require('@tzstamp/helpers')
4 | const { Proof, FetchError, UnresolvedProof, Blake2bOperation, InvalidTemplateError } = require('@tzstamp/proof')
5 | const chalk = require('chalk')
6 | const fetch = require('node-fetch')
7 | const delay = require('delay')
8 | const Help = require('./help')
9 | const { getHash, getSafeNames } = require('./helpers')
10 |
11 | async function longPollProof (proof) {
12 | async function resolve () {
13 | try {
14 | return await proof.resolve()
15 | } catch (error) {
16 | if (error instanceof InvalidTemplateError) {
17 | return
18 | }
19 | if (!(error instanceof FetchError)) {
20 | throw new Error(`Error while waiting for publication: ${error.message}`)
21 | }
22 | switch (error.status) {
23 | case 202:
24 | return
25 | default:
26 | throw new Error(`Bad server response while waiting for publication: ${error.status}`)
27 | }
28 | }
29 | }
30 |
31 | // Check immediately
32 | const newProof = await resolve()
33 | if (newProof) {
34 | return newProof
35 | }
36 |
37 | // Check every 30 seconds after
38 | for (;;) {
39 | await delay(30000)
40 | const newProof = await resolve()
41 | if (newProof) {
42 | return newProof
43 | }
44 | }
45 | }
46 |
47 | async function handler (options) {
48 | // Early exits
49 | if (options._ == undefined || !options._.length) {
50 | return Help.handler({ _: [ 'stamp' ] })
51 | }
52 |
53 | const targets = options._
54 | const servers = options.servers
55 | ? options.servers.split(',')
56 | : [ 'https://api.tzstamp.io/' ]
57 | const allProofs = []
58 |
59 | // Submit each target file/hash to each aggregator server
60 | for (const target of targets) {
61 | const proofs = []
62 | const paths = await getSafeNames(target, options.directory, options.verbose)
63 |
64 | // Create base proof
65 | const baseProof = new Proof({
66 | hash: await getHash(target, options.verbose),
67 | operations: options.nonce
68 | ? [ new Blake2bOperation(32, new Uint8Array(randomBytes(32))) ]
69 | : []
70 | })
71 | if (!options.nonce && options.verbose) {
72 | console.log(chalk.dim`--no-nonce: Submitting raw hashes`)
73 | }
74 |
75 | // Submit hash to each aggregator server
76 | submit: for (const server of servers) {
77 | // POST hash to server
78 | let response
79 | const endpoint = new URL('stamp', server)
80 | if (options.verbose) {
81 | console.log(chalk.dim`Submitting ${Hex.stringify(baseProof.derivation)} to <${server}> for aggregation`)
82 | }
83 | try {
84 | response = await fetch(endpoint, {
85 | method: 'POST',
86 | headers: {
87 | 'content-type': 'application/json',
88 | accept: 'application/json'
89 | },
90 | body: JSON.stringify({
91 | data: Hex.stringify(baseProof.derivation)
92 | })
93 | })
94 | } catch (error) {
95 | console.error(`Could not reach aggregator <${server}>: ${error.message}`)
96 | continue submit
97 | }
98 | if (!response.ok) {
99 | console.error(`Could not submit hash to aggregator <${server}>: ${response.status} ${response.statusText}`)
100 | continue submit
101 | }
102 |
103 | // Extract proof URL from response
104 | if (options.verbose) {
105 | console.log(chalk.dim`Parsing response from <${server}>`)
106 | }
107 | try {
108 | const { url: proofURL } = await response.json()
109 | if (options.verbose) {
110 | console.log(chalk.dim`Successfully submitted hash to <${server}>`)
111 | }
112 | const remoteProof = new UnresolvedProof({
113 | hash: baseProof.derivation,
114 | operations: [],
115 | remote: proofURL
116 | })
117 | proofs.push(baseProof.concat(remoteProof))
118 | } catch (error) {
119 | console.error(`Could not parse response from aggregator <${server}>`)
120 | continue submit
121 | }
122 | }
123 |
124 | // Save proofs
125 | for (const proof of proofs) {
126 | allProofs.push({
127 | target,
128 | proof,
129 | proofPath: paths.next().value
130 | })
131 | }
132 | }
133 |
134 | // Store proofs
135 | for (const { target, proofPath, proof } of allProofs) {
136 | let finalProof
137 |
138 | // Wait for publication
139 | if (options.wait) {
140 | if (options.verbose) {
141 | console.log(chalk.dim`Waiting for publication`)
142 | }
143 | for (const { proof } of allProofs) {
144 | finalProof = await longPollProof(proof)
145 | if (options.verbose) {
146 | console.log(chalk.dim`Proof at <${proof.remote}> published`)
147 | }
148 | }
149 | } else {
150 | finalProof = proof
151 | }
152 |
153 | // Serialize and write to filesystem
154 | if (options.verbose) {
155 | console.log(chalk.dim`Writing proof to ${proofPath}`)
156 | }
157 | await fs.writeFile(proofPath, JSON.stringify(finalProof))
158 | console.log(`Stored proof for ${target} at ${proofPath}.`)
159 | }
160 | }
161 |
162 | const parseOptions = {
163 | boolean: [
164 | 'wait',
165 | 'nonce'
166 | ],
167 | string: [
168 | 'servers',
169 | 'directory'
170 | ],
171 | alias: {
172 | wait: 'w',
173 | directory: 'd',
174 | servers: 's'
175 | },
176 | default: {
177 | nonce: true
178 | }
179 | }
180 |
181 | module.exports = {
182 | handler,
183 | parseOptions,
184 | title: 'Stamp',
185 | description: 'Creates a timestamp proof.',
186 | usage: 'tzstamp stamp [options] [...]',
187 | remarks: [
188 | 'Raw file hashes must be between 32 and 64 bytes.',
189 | 'Default aggregator server is .'
190 | ],
191 | options: [
192 | [ '--wait, -w', 'Waits until all proofs are published before continuing execution.' ],
193 | [ '--servers, -s', 'Comma-separated list of aggregator servers.' ],
194 | [ '--directory, -d', 'Sets proof output directory.' ],
195 | [ '--nonce', 'Nonces the file hash hash(es) before submitting to the aggregator(s). True by default' ]
196 | ],
197 | examples: [
198 | 'tzstamp stamp -s https://api.example.com/ myFile.txt',
199 | 'tzstamp stamp --wait myFile.txt',
200 | 'tzstamp stamp 4a5be57589b4ddc42d87e4df775161e5bbcdf772058093d524b04dd88533a50a',
201 | 'tzstamp stamp --directory /tmp file1.txt file2.txt file3.txt'
202 | ]
203 | }
204 |
--------------------------------------------------------------------------------
/packages/helpers/blake2b.ts:
--------------------------------------------------------------------------------
1 | /** Initialization vector */
2 | const IV = [
3 | 0x6a09e667f3bcc908n,
4 | 0xbb67ae8584caa73bn,
5 | 0x3c6ef372fe94f82bn,
6 | 0xa54ff53a5f1d36f1n,
7 | 0x510e527fade682d1n,
8 | 0x9b05688c2b3e6c1fn,
9 | 0x1f83d9abfb41bd6bn,
10 | 0x5be0cd19137e2179n,
11 | ] as const;
12 |
13 | /** Message Schedule */
14 | const SIGMA = [
15 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
16 | [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
17 | [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
18 | [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
19 | [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
20 | [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
21 | [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
22 | [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
23 | [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
24 | [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
25 | ] as const;
26 |
27 | /** Mix word indices */
28 | const MIX_INDICES = [
29 | [0, 4, 8, 12],
30 | [1, 5, 9, 13],
31 | [2, 6, 10, 14],
32 | [3, 7, 11, 15],
33 | [0, 5, 10, 15],
34 | [1, 6, 11, 12],
35 | [2, 7, 8, 13],
36 | [3, 4, 9, 14],
37 | ] as const;
38 |
39 | /**
40 | * [BLAKE2b] streaming hash function.
41 | *
42 | * ```js
43 | * const message = new TextEncoder().encode("hello");
44 | * const hash = new Blake2b();
45 | *
46 | * hash.update(message);
47 | * hash.finalized; // false
48 | *
49 | * hash.digest(); // Uint8Array(32)
50 | * hash.finalized; // true
51 | * ```
52 | *
53 | * [BLAKE2b]: https://www.blake2.net
54 | */
55 | export class Blake2b {
56 | private state = new ArrayBuffer(64);
57 | private buffer = new ArrayBuffer(128);
58 | private offset = 0n;
59 | private final = false;
60 | private counter = 0;
61 |
62 | /** Length of digest in bytes. */
63 | readonly digestLength: number;
64 |
65 | /** Returns true if the hash function is finalized. */
66 | get finalized(): boolean {
67 | return this.final;
68 | }
69 |
70 | /**
71 | * Throws `RangeError` if either the key or digest length are
72 | * larger than 64 bytes, or if the digest length is negative.
73 | *
74 | * @param key Hash key. Must be between 0-64 bytes.
75 | * @param digestLength Length of digest in bytes, defaulting to 32. Must be between 0-64 bytes.
76 | */
77 | constructor(key: Uint8Array = new Uint8Array(), digestLength: number = 32) {
78 | if (key.length > 64) {
79 | throw new RangeError("Key must be less than 64 bytes");
80 | }
81 | if (digestLength < 0) {
82 | throw new RangeError("Digest length must be a positive value");
83 | }
84 | if (digestLength > 64) {
85 | throw new RangeError("Digest length must be less than 64 bytes");
86 | }
87 | this.digestLength = digestLength;
88 | this.init(key);
89 | }
90 |
91 | private init(key: Uint8Array): void {
92 | const state = new DataView(this.state);
93 | for (let i = 0; i < 8; ++i) {
94 | state.setBigUint64(i * 8, IV[i], true);
95 | }
96 | const firstWord = state.getBigUint64(0, true) ^
97 | 0x01010000n ^
98 | BigInt(key.length << 8 ^ this.digestLength);
99 | state.setBigUint64(0, firstWord, true);
100 | if (key.length) {
101 | this.update(key);
102 | this.counter = 128;
103 | }
104 | }
105 |
106 | /**
107 | * Feeds input into the hash function in 128 byte blocks.
108 | * Throws if the hash function is finalized.
109 | *
110 | * @param input Input bytes
111 | */
112 | update(input: Uint8Array): this {
113 | if (this.final) {
114 | throw new Error("Cannot update finalized hash function.");
115 | }
116 | const buffer = new Uint8Array(this.buffer);
117 | for (let i = 0; i < input.length; ++i) {
118 | if (this.counter == 128) {
119 | this.counter = 0;
120 | this.offset += 128n;
121 | this.compress();
122 | }
123 | buffer[this.counter++] = input[i];
124 | }
125 | return this;
126 | }
127 |
128 | private compress(last = false): void {
129 | const state = new DataView(this.state);
130 | const buffer = new DataView(this.buffer);
131 | const vector = new BigUint64Array(16);
132 |
133 | // Initialize work vector
134 | for (let i = 0; i < 8; ++i) {
135 | vector[i] = state.getBigUint64(i * 8, true);
136 | }
137 | vector.set(IV, 8);
138 | vector[12] ^= this.offset;
139 | vector[13] ^= this.offset >> 64n;
140 | if (last) {
141 | vector[14] = ~vector[14];
142 | }
143 |
144 | // Twelve rounds of mixing
145 | const rotate = (x: bigint, y: bigint) => x >> y | x << 64n - y;
146 | for (let i = 0; i < 12; ++i) {
147 | const s = SIGMA[i % 10];
148 | for (let j = 0; j < 8; ++j) {
149 | const x = buffer.getBigUint64(s[2 * j] * 8, true);
150 | const y = buffer.getBigUint64(s[2 * j + 1] * 8, true);
151 | const [a, b, c, d] = MIX_INDICES[j];
152 | vector[a] += vector[b] + x;
153 | vector[d] = rotate(vector[d] ^ vector[a], 32n);
154 | vector[c] += vector[d];
155 | vector[b] = rotate(vector[b] ^ vector[c], 24n);
156 | vector[a] += vector[b] + y;
157 | vector[d] = rotate(vector[d] ^ vector[a], 16n);
158 | vector[c] += vector[d];
159 | vector[b] = rotate(vector[b] ^ vector[c], 63n);
160 | }
161 | }
162 |
163 | // Update state
164 | for (let i = 0; i < 8; ++i) {
165 | const word = state.getBigUint64(i * 8, true) ^ vector[i] ^ vector[i + 8];
166 | state.setBigUint64(i * 8, word, true);
167 | }
168 | }
169 |
170 | /**
171 | * Finalizes the state and produces a digest.
172 | * Throws if the hash function is already finalized.
173 | */
174 | digest(): Uint8Array {
175 | if (this.final) {
176 | throw new Error("Cannot re-finalize hash function.");
177 | }
178 | this.offset += BigInt(this.counter);
179 | const buffer = new Uint8Array(this.buffer);
180 | while (this.counter < 128) {
181 | buffer[this.counter++] = 0;
182 | }
183 | this.compress(true);
184 | this.final = true;
185 | return new Uint8Array(this.state).slice(0, this.digestLength);
186 | }
187 |
188 | /**
189 | * Produces an immediate BLAKE2b digest.
190 | *
191 | * @param input Input bytes
192 | * @param key Hash key. Must be between 0-64 bytes.
193 | * @param digestLength Length of digest in bytes, defaulting to 32. Must be between 0-64 bytes.
194 | */
195 | static digest(
196 | input: Uint8Array,
197 | key?: Uint8Array,
198 | digestLength?: number,
199 | ): Uint8Array {
200 | return new Blake2b(key, digestLength)
201 | .update(input)
202 | .digest();
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/packages/proof/operation.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Blake2bOperation,
3 | Blake2bTemplate,
4 | JoinOperation,
5 | JoinTemplate,
6 | Operation,
7 | Sha256Operation,
8 | Sha256Template,
9 | } from "./operation.ts";
10 | import { Blake2b, concat, Hex, Sha256 } from "./deps.ts";
11 | import { assertEquals, assertThrows } from "./dev_deps.ts";
12 |
13 | Deno.test({
14 | name: "Invalid operation templating",
15 | fn() {
16 | assertThrows(
17 | () => Operation.from(null),
18 | SyntaxError,
19 | );
20 | assertThrows(
21 | () => Operation.from({}),
22 | SyntaxError,
23 | );
24 | assertThrows(
25 | () => Operation.from({ type: "bogus" }),
26 | SyntaxError,
27 | );
28 | },
29 | });
30 |
31 | Deno.test({
32 | name: "Join operation",
33 | fn() {
34 | const prepend = crypto.getRandomValues(new Uint8Array(16));
35 | const append = crypto.getRandomValues(new Uint8Array(16));
36 | const wrapOp = new JoinOperation({ prepend, append });
37 | const wrapTemplate: JoinTemplate = {
38 | type: "join",
39 | prepend: Hex.stringify(prepend),
40 | append: Hex.stringify(append),
41 | };
42 | const prependOp = new JoinOperation({ prepend });
43 | const prependTemplate: JoinTemplate = {
44 | type: "join",
45 | prepend: Hex.stringify(prepend),
46 | };
47 | const appendOp = new JoinOperation({ append });
48 | const appendTemplate: JoinTemplate = {
49 | type: "join",
50 | append: Hex.stringify(append),
51 | };
52 | const nullOp = new JoinOperation({});
53 | const nullTemplate: JoinTemplate = {
54 | type: "join",
55 | };
56 | assertEquals(wrapOp.prepend, prepend);
57 | assertEquals(wrapOp.append, append);
58 | assertEquals(nullOp.prepend, new Uint8Array());
59 | assertEquals(nullOp.append, new Uint8Array());
60 | assertEquals(
61 | wrapOp.toString(),
62 | `Prepend 0x${Hex.stringify(prepend)}, Append 0x${Hex.stringify(append)}`,
63 | );
64 | assertEquals(prependOp.toString(), `Prepend 0x${Hex.stringify(prepend)}`);
65 | assertEquals(appendOp.toString(), `Append 0x${Hex.stringify(append)}`);
66 | assertEquals(nullOp.toString(), "");
67 | assertEquals(
68 | wrapOp.commit(new Uint8Array([67])),
69 | concat(prepend, 67, append),
70 | );
71 | assertEquals(
72 | prependOp.commit(new Uint8Array([39])),
73 | concat(prepend, 39),
74 | );
75 | assertEquals(
76 | appendOp.commit(new Uint8Array([244])),
77 | concat(244, append),
78 | );
79 | assertEquals(
80 | nullOp.commit(new Uint8Array([132])),
81 | new Uint8Array([132]),
82 | );
83 | assertEquals(wrapOp.toJSON(), wrapTemplate);
84 | assertEquals(prependOp.toJSON(), prependTemplate);
85 | assertEquals(appendOp.toJSON(), appendTemplate);
86 | assertEquals(nullOp.toJSON(), nullTemplate);
87 | assertEquals(wrapOp, Operation.from(wrapTemplate));
88 | assertEquals(prependOp, Operation.from(prependTemplate));
89 | assertEquals(appendOp, Operation.from(appendTemplate));
90 | assertEquals(nullOp, Operation.from(nullTemplate));
91 | assertThrows(
92 | () => Operation.from({ type: "join", extraneous: true }),
93 | SyntaxError,
94 | );
95 | },
96 | });
97 |
98 | Deno.test({
99 | name: "BLAKE2b hash operation",
100 | fn() {
101 | const key = crypto.getRandomValues(new Uint8Array(32));
102 | const input = crypto.getRandomValues(new Uint8Array(32));
103 | const lenKeyOp = new Blake2bOperation(64, key);
104 | const lenKeyTemplate: Blake2bTemplate = {
105 | type: "blake2b",
106 | length: 64,
107 | key: Hex.stringify(key),
108 | };
109 | const lenOp = new Blake2bOperation(44);
110 | const lenTemplate: Blake2bTemplate = {
111 | type: "blake2b",
112 | length: 44,
113 | };
114 | const keyOp = new Blake2bOperation(undefined, key);
115 | const keyTemplate: Blake2bTemplate = {
116 | type: "blake2b",
117 | key: Hex.stringify(key),
118 | };
119 | const defaultOp = new Blake2bOperation();
120 | const defaultTemplate: Blake2bTemplate = { type: "blake2b" };
121 | assertEquals(lenKeyOp.length, 64);
122 | assertEquals(lenOp.length, 44);
123 | assertEquals(keyOp.length, 32);
124 | assertEquals(keyOp.key, key);
125 | assertEquals(
126 | lenKeyOp.toString(),
127 | `BLAKE2b hash, 64-byte digest with key 0x${Hex.stringify(key)}`,
128 | );
129 | assertEquals(
130 | lenOp.toString(),
131 | "BLAKE2b hash, 44-byte digest",
132 | );
133 | assertEquals(
134 | keyOp.toString(),
135 | `BLAKE2b hash, 32-byte digest with key 0x${Hex.stringify(key)}`,
136 | );
137 | assertEquals(
138 | defaultOp.toString(),
139 | "BLAKE2b hash, 32-byte digest",
140 | );
141 | assertEquals(
142 | lenKeyOp.commit(input),
143 | new Blake2b(key, 64).update(input).digest(),
144 | );
145 | assertEquals(
146 | lenOp.commit(input),
147 | new Blake2b(undefined, 44).update(input).digest(),
148 | );
149 | assertEquals(
150 | keyOp.commit(input),
151 | new Blake2b(key, 32).update(input).digest(),
152 | );
153 | assertEquals(
154 | defaultOp.commit(input),
155 | new Blake2b().update(input).digest(),
156 | );
157 | assertEquals(lenKeyOp.toJSON(), lenKeyTemplate);
158 | assertEquals(lenOp.toJSON(), lenTemplate);
159 | assertEquals(keyOp.toJSON(), keyTemplate);
160 | assertEquals(defaultOp.toJSON(), defaultTemplate);
161 | assertEquals(lenKeyOp, Operation.from(lenKeyTemplate));
162 | assertEquals(lenOp, Operation.from(lenTemplate));
163 | assertEquals(keyOp, Operation.from(keyTemplate));
164 | assertEquals(defaultOp, Operation.from(defaultTemplate));
165 | assertThrows(
166 | () => new Blake2bOperation(-1),
167 | RangeError,
168 | );
169 | assertThrows(
170 | () => new Blake2bOperation(65),
171 | RangeError,
172 | );
173 | assertThrows(
174 | () => new Blake2bOperation(undefined, new Uint8Array(65)),
175 | RangeError,
176 | );
177 | assertThrows(
178 | () => Operation.from({ type: "blake2b", extraneous: true }),
179 | SyntaxError,
180 | );
181 | },
182 | });
183 |
184 | Deno.test({
185 | name: "SHA-256 hash operation",
186 | fn() {
187 | const op = new Sha256Operation();
188 | const input = crypto.getRandomValues(new Uint8Array(32));
189 | const template: Sha256Template = { type: "sha256" };
190 | assertEquals(op.toString(), "SHA-256 hash");
191 | assertEquals(
192 | op.commit(input),
193 | Sha256.digest(input),
194 | );
195 | assertEquals(op.toJSON(), template);
196 | assertEquals(op, Operation.from(template));
197 | assertThrows(
198 | () => Operation.from({ type: "sha256", extraneous: true }),
199 | SyntaxError,
200 | );
201 | },
202 | });
203 |
--------------------------------------------------------------------------------
/website/public/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* colors */
3 | --text-color: #000;
4 | --subtle-text-color: #0009;
5 | --code-color: #6d5700;
6 | --code-block-color: rgb(221, 221, 221);
7 | --link-color: #346092;
8 | --button-color: #f3f3f3;
9 | --button-text-color: #000;
10 | --disabled-button-color: #cacaca;
11 | --disabled-button-text-color: #646464;
12 | --border-color: #a8a8a8;
13 | --accent-color: #4a6b96;
14 | --background-color: #fff;
15 | --input-text-color: #000;
16 | --input-background-color: #fff;
17 | --navbar-color: #4a6b96;
18 | --navbar-text-color: #fff;
19 | --alt-section-color: #d8d6cf;
20 | --footer-color: #d2deee;
21 | /* effects */
22 | --box-shadow: 3px 3px 6px #0003;
23 | /* spacing */
24 | --thin-spacing: 0.8rem;
25 | --normal-spacing: 1.4rem;
26 | --wide-spacing: 2.4rem;
27 | --border-radius: 6px;
28 | --content-width: 60rem;
29 | /* typography */
30 | --font-stack-sans-serif: Futura, "Trebuchet MS", Arial, sans-serif;
31 | --font-stack-monospace: monaco, Consolas, "Lucida Console", monospace;
32 | --line-height: 1.5;
33 | }
34 |
35 | * {
36 | margin: 0;
37 | padding: 0;
38 | box-sizing: border-box;
39 | }
40 |
41 | body {
42 | color: var(--text-color);
43 | background-color: var(--background-color);
44 | font-family: var(--font-stack-sans-serif);
45 | line-height: var(--line-height);
46 | }
47 |
48 | div.container {
49 | margin: 0 auto;
50 | max-width: var(--content-width);
51 | }
52 |
53 | body>nav {
54 | box-shadow: var(--box-shadow);
55 | font-weight: bold;
56 | user-select: none;
57 | }
58 |
59 | nav.top {
60 | --link-color: var(--navbar-text-color);
61 | padding: var(--normal-spacing);
62 | background-color: var(--navbar-color);
63 | }
64 |
65 | nav.top ul {
66 | display: flex;
67 | gap: var(--normal-spacing);
68 | list-style-type: none;
69 | align-items: center;
70 | }
71 |
72 | nav.top ul>li:first-child {
73 | margin-right: auto;
74 | }
75 |
76 | nav.top ul>li:first-child>a>svg {
77 | height: 4rem;
78 | width: 12rem;
79 | filter: drop-shadow(2px 2px 4px #0003);
80 | }
81 |
82 | nav.top ul>li:first-child>a>svg>use {
83 | fill: var(--navbar-text-color);
84 | }
85 |
86 | body>section {
87 | margin: var(--wide-spacing) 0;
88 | padding: var(--wide-spacing);
89 | }
90 |
91 | body>section ul {
92 | padding-left: var(--normal-spacing);
93 | }
94 |
95 | body>section.alt {
96 | background-color: var(--alt-section-color);
97 | box-shadow: var(--box-shadow);
98 | }
99 |
100 | body>section.split>.container {
101 | display: flex;
102 | gap: var(--wide-spacing);
103 | }
104 |
105 | body>header {
106 | margin: var(--wide-spacing) 0;
107 | padding: var(--normal-spacing);
108 | text-align: center;
109 | }
110 |
111 | body>header ul {
112 | display: flex;
113 | margin-top: var(--wide-spacing);
114 | justify-content: center;
115 | gap: var(--normal-spacing);
116 | list-style: none;
117 | }
118 |
119 | body>header ul>li svg {
120 | display: block;
121 | width: 4rem;
122 | }
123 |
124 | body>header ul>li svg>use {
125 | stroke: var(--link-color);
126 | }
127 |
128 | body>footer {
129 | color: var(--subtle-text-color);
130 | padding: var(--normal-spacing);
131 | margin-top: var(--wide-spacing);
132 | background-color: var(--footer-color);
133 | }
134 |
135 | h1 {
136 | font-size: 200%;
137 | }
138 |
139 | h2 {
140 | display: inline-block;
141 | border-bottom: 4px solid var(--accent-color);
142 | margin-bottom: var(--thin-spacing);
143 | }
144 |
145 | h3 {
146 | margin-top: var(--thin-spacing);
147 | margin-bottom: var(--normal-spacing);
148 | }
149 |
150 | h4 {
151 | margin-top: var(--thin-spacing);
152 | margin-bottom: var(--thin-spacing);
153 | }
154 |
155 | a {
156 | color: var(--link-color);
157 | text-decoration: none;
158 | }
159 |
160 | a:hover {
161 | text-decoration: underline;
162 | }
163 |
164 | code {
165 | font-family: var(--font-stack-monospace);
166 | color: var(--code-color);
167 | }
168 |
169 | code.block {
170 | display: inline-block;
171 | padding: var(--thin-spacing);
172 | background-color: var(--code-block-color);
173 | border-radius: var(--border-radius);
174 | font-weight: bold;
175 | }
176 |
177 | form {
178 | margin-top: var(--normal-spacing);
179 | margin-bottom: var(--normal-spacing);
180 | }
181 |
182 | label {
183 | display: block;
184 | font-weight: bold;
185 | margin-bottom: 0.4rem;
186 | }
187 |
188 | label+input {
189 | display: block;
190 | margin-bottom: 0.8rem;
191 | }
192 |
193 | input {
194 | max-width: 100%;
195 | padding: 0.6rem;
196 | font-family: var(--font-stack-sans-serif);
197 | font-size: inherit;
198 | color: var(--input-text-color);
199 | background-color: var(--input-background-color);
200 | border: 1px solid var(--border-color);
201 | border-radius: var(--border-radius);
202 | }
203 |
204 | input[type=file]::file-selector-button {
205 | padding: 0.4rem;
206 | margin-right: 0.8rem;
207 | border: 1px solid var(--color-border);
208 | font-family: inherit;
209 | color: var(--button-text-color);
210 | background-color: var(--button-color);
211 | }
212 |
213 | output {
214 | display: block;
215 | margin: var(--normal-spacing) 0 0 0;
216 | white-space: pre-line;
217 | }
218 |
219 | button {
220 | margin-right: 0.4rem;
221 | padding: 0.4rem;
222 | font-family: inherit;
223 | margin-bottom: 0.4rem;
224 | border: 1px solid var(--border-color);
225 | color: var(--button-text-color);
226 | background-color: var(--button-color);
227 | }
228 |
229 | button:disabled {
230 | color: var(--disabled-button-text-color);
231 | background-color: var(--disabled-button-color);
232 | }
233 |
234 | p {
235 | margin-top: var(--thin-spacing);
236 | margin-bottom: var(--thin-spacing);
237 | }
238 |
239 | @media (max-width: 768px) {
240 | :root {
241 | --normal-spacing: 1rem;
242 | }
243 | nav.top ul {
244 | display: block;
245 | flex-direction: column;
246 | text-align: right;
247 | line-height: 2;
248 | }
249 | nav.top ul>li:first-child {
250 | float: left;
251 | }
252 | }
253 |
254 | @media (max-width: 900px) {
255 | body>section.split>.container {
256 | flex-direction: column;
257 | }
258 | }
259 |
260 | @media (prefers-color-scheme: dark) {
261 | :root {
262 | --text-color: #e7e7e7;
263 | --link-color: #9494d4;
264 | --code-color: #ceb552;
265 | --code-block-color: #353536;
266 | --accent-color: #849fc2;
267 | --navbar-color: #2f2f35;
268 | --navbar-text-color: #e7e7e7;
269 | --alt-section-color: #363638;
270 | --background-color: #242425;
271 | --footer-color: #363638;
272 | --subtle-text-color: #e7e7e780;
273 | --button-color: #777777;
274 | --button-text-color: #e7e7e7;
275 | --disabled-button-color: #414141;
276 | --disabled-button-text-color: #757575;
277 | --input-background-color: #494949;
278 | --input-text-color: #e7e7e7;
279 | --border-color: #141414;
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/website/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
99 |
--------------------------------------------------------------------------------