├── .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 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 97 | 98 | 99 | --------------------------------------------------------------------------------