Skip to content

tabmac/TabNotary

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TabNotary

Blockchain-anchored document notarization for trustless contract verification.

TabNotary is an open-source implementation for notarizing documents on the blockchain. It creates tamper-proof, independently verifiable records by anchoring SHA-256 document hashes on-chain. Originally built for Tabmac venue contracts, the architecture is generic and can be adapted to any document notarization use case.

Try it out: https://venue.tabmac.com/verify

Other Languages: 中文 | Español | Deutsch


Table of Contents


How It Works

TabNotary implements a three-phase notarization pipeline:

Phase 1: SNAPSHOT                Phase 2: ANCHOR              Phase 3: VERIFY
┌─────────────────────┐         ┌──────────────────────┐     ┌──────────────────────┐
│ Collect document    │         │ Submit SHA-256 hash  │     │ Upload PDF or paste  │
│ data at moment of   │────────>│ to smart contract on │────>│ hash to independently│
│ agreement           │         │ Base blockchain      │     │ verify on-chain      │
│                     │         │                      │     │                      │
│ - Canonicalize JSON │         │ - anchorContract()   │     │ - PDF integrity      │
│ - Sort keys (det.)  │         │ - Wait 10 confirms   │     │   (client-side)      │
│ - SHA-256 hash      │         │ - Record tx details  │     │ - Blockchain lookup  │
│ - Store snapshot    │         │ - Retry on failure   │     │   (server-side)      │
└─────────────────────┘         └──────────────────────┘     └──────────────────────┘

Key properties:

  • Deterministic — Identical input always produces identical hash via canonical JSON serialization (sorted keys)
  • Tamper-evident — Any modification to the document changes the hash, which won't match on-chain
  • Independently verifiable — Anyone can verify a hash against the public blockchain without trusting the issuer
  • Privacy-preserving — Only the hash is stored on-chain; no personal data is exposed
  • Immutable — Once anchored, the record cannot be altered or deleted by any party

Architecture Overview

┌────────────────────────────────────────────────────────────────────────┐
│                           BACKEND SERVER                              │
│                                                                       │
│  ┌─────────────────┐    ┌──────────────────┐    ┌──────────────────┐  │
│  │ Contract        │    │ Blockchain       │    │ Notarization     │  │
│  │ Snapshot        │───>│ Notary Service   │<───│ Worker           │  │
│  │ (canonicalize   │    │ (ethers.js +     │    │ (30s interval,   │  │
│  │  + SHA-256)     │    │  Base RPC)       │    │  job queue)      │  │
│  └─────────────────┘    └──────────────────┘    └──────────────────┘  │
│           │                      │                       │            │
│           v                      v                       v            │
│  ┌─────────────────────────────────────────────────────────────────┐  │
│  │                        MongoDB                                  │  │
│  │  snapshots collection  │  notarization_jobs  │  bookings        │  │
│  └─────────────────────────────────────────────────────────────────┘  │
│                                                                       │
│  ┌──────────────────────────────────────────┐                         │
│  │ Verification API (public, no auth)       │                         │
│  │ POST /api/verify-contract                │                         │
│  │ GET  /api/verify-contract/:hash          │                         │
│  └──────────────────────────────────────────┘                         │
└────────────────────────────────────────────────────────────────────────┘
         │                                              ▲
         │              Base Blockchain                 │
         │         ┌─────────────────────┐              │
         └────────>│  TabmacNotary.sol   │──────────────┘
                   │  (Solidity ^0.8.20) │   isAnchored() view call
                   │                     │
                   │  anchorContract()   │
                   │  isAnchored()       │
                   └─────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│                         FRONTEND (Browser)                            │
│                                                                       │
│  ┌──────────────────────────┐    ┌─────────────────────────────────┐  │
│  │ Step 1: PDF Integrity    │    │ Step 2: Blockchain Verification │  │
│  │ (runs entirely in        │───>│ (calls /api/verify-contract)    │  │
│  │  browser via Web Crypto) │    │                                 │  │
│  │                          │    │ Displays: network, block #,     │  │
│  │ - Extract text (pdfjs)   │    │ tx hash, confirmations,        │  │
│  │ - Find canonical payload │    │ Basescan explorer link          │  │
│  │ - SHA-256 hash & compare │    │                                 │  │
│  └──────────────────────────┘    └─────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────┘

Components

1. Smart Contract (Solidity/Base)

File: contracts/TabmacNotary.sol

A minimal, auditable Solidity contract deployed on the Base blockchain (L2).

contract TabmacNotary is Ownable {
    mapping(bytes32 => bool) public anchored;

    function anchorContract(
        bytes32 bookingHash,      // keccak256(documentId) — for indexed lookup
        bytes32 contractHash,     // SHA-256 hash of canonical document
        uint8 canonicalizationVersion,
        uint16 snapshotVersion    // monotonic version per document
    ) external onlyOwner { ... }

    function isAnchored(
        bytes32 contractHash,
        uint16 snapshotVersion
    ) external view returns (bool) { ... }
}

Key design decisions:

  • onlyOwner for writes — Only the notary wallet can anchor hashes, preventing spam
  • isAnchored() is public — Anyone can verify without permission
  • Composite keykeccak256(contractHash, snapshotVersion) allows versioned snapshots
  • Event emissionContractAnchored event enables off-chain indexing
  • Base L2 — Low gas costs (~$0.01-0.05 per anchor) while inheriting Ethereum security

2. Canonicalization & Hashing (Backend)

File: backend/contractSnapshot.js

Produces a deterministic, reproducible hash from any document data.

// 1. Build structured payload from document data
const snapshot = buildCanonicalSnapshot(document, metadata, events);

// 2. Recursively sort all object keys for determinism
const sorted = deepSortKeys(snapshot);

// 3. Serialize to JSON bytes
const bytes = Buffer.from(JSON.stringify(sorted), 'utf-8');

// 4. SHA-256 hash
const hash = crypto.createHash('sha256').update(bytes).digest('hex');
// → "a1b2c3d4e5f6..." (64 hex characters)

Why canonicalization matters: JSON serialization is non-deterministic — {"b":1,"a":2} and {"a":2,"b":1} are semantically identical but produce different hashes. By recursively sorting all keys before serialization, the same input always produces the same hash, regardless of the order data was assembled.

3. Blockchain Notary Service (Backend)

File: backend/blockchainNotary.js

Handles all blockchain interactions using ethers.js v6.

Function Purpose
anchorHash(docId, hash, canonVersion, snapVersion) Submit anchor transaction
waitForConfirmation(txHash, depth) Wait for N block confirmations
checkConfirmation(txHash) Poll current confirmation depth
verifyAnchor(hash, snapVersion) Read-only check if hash is on-chain
toBytes32(hexStr) Convert SHA-256 hex to Solidity bytes32
hashBookingId(id) keccak256 of document ID for indexing

4. Notarization Worker (Backend)

File: backend/notarizationWorker.js

A background job processor that runs on a 30-second interval.

Job Lifecycle:
  queued → processing → sent → confirming → confirmed
                │                               │
                └──── failed (after 5 retries) ──┘
                       with exponential backoff
                       (30s, 2min, 8min, 32min)

Features:

  • Processes up to 5 jobs per cycle to avoid overloading the RPC
  • Idempotency check before submitting (calls isAnchored() first)
  • Requires 10 block confirmations before marking as confirmed
  • Exponential backoff on failure: 4^attempts * 30s
  • Updates the parent document record with full blockchain proof details

5. Verification API (Backend)

File: backend/contract-verification.js

Public, unauthenticated endpoints for hash verification.

POST /api/verify-contract
Body: { "contractHash": "a1b2c3d4..." }

GET /api/verify-contract/:hash

Response:

{
  "verified": true,
  "onChainVerified": true,
  "message": "This contract hash matches a record anchored on the Base blockchain.",
  "contractHash": "a1b2c3d4...",
  "snapshotMetadata": {
    "snapshotVersion": 1,
    "schemaVersion": "1.0.0",
    "hashAlgorithm": "SHA-256",
    "createdAt": "2026-01-15T10:30:00.000Z"
  },
  "blockchainProof": {
    "chainId": 8453,
    "networkName": "Base",
    "contractAddress": "0x59Db...",
    "transactionHash": "0xabc...",
    "blockNumber": 12345678,
    "blockTimestamp": "2026-01-15T10:35:00.000Z",
    "confirmationDepth": 20,
    "anchoredAt": "2026-01-15T10:35:00.000Z",
    "explorerUrl": "https://basescan.org/tx/0xabc..."
  }
}

Security:

  • Rate limited: 10 requests per IP per minute
  • No PII in response — only hash metadata and blockchain proof
  • No authentication required (public verification by design)

6. Verification UI (Frontend)

File: frontend/Verify.tsx

A React component implementing a two-step verification flow with visual feedback.

Input methods:

  1. PDF Upload — Runs both Step 1 (integrity) and Step 2 (blockchain)
  2. Manual Hash — Paste a SHA-256 hash directly, skips Step 1
  3. URL Parameter/verify/:hash auto-triggers Step 2

7. Client-Side PDF Integrity (Frontend)

File: frontend/contractIntegrity.ts

Verifies document integrity entirely in the browser — no data leaves the client.

PDF Document
    │
    ├── Extract text (pdfjs-dist)
    ├── Find "Contract Hash (SHA-256): [64-char hex]"
    ├── Find canonical payload between markers:
    │   ---BEGIN CANONICAL PAYLOAD---
    │   [base64-encoded data]
    │   ---END CANONICAL PAYLOAD---
    │
    ├── Decode base64 → bytes
    ├── SHA-256(bytes) via Web Crypto API
    └── Compare computed hash vs extracted hash
        ├── MATCH → Document is authentic
        └── MISMATCH → Document has been tampered with

Environment Variables

Required for Blockchain

These are the minimum environment variables needed to run blockchain notarization:

Variable Description Example
BASE_RPC_URL_1 Base mainnet JSON-RPC endpoint (Alchemy, Infura, etc.) https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
NOTARY_WALLET_PRIVATE_KEY Private key of the wallet that owns the smart contract. This wallet pays gas fees and is the only address authorized to call anchorContract(). Keep this secret. your_64_char_hex_key (no 0x prefix)
NOTARY_CONTRACT_ADDRESS Deployed TabmacNotary contract address on Base 0xYourContractAddress
NOTARIZATION_ENABLED Master switch to enable/disable notarization true

Optional Configuration

Variable Description Default
BASE_EXPLORER Block explorer base URL (for generating tx links) https://basescan.org/
BASESCAN_API_KEY Basescan API key for contract verification during deployment (none)
NOTARY_CONTRACT_VERSION Version string stored with notarization records 1.0.0

Frontend Variables

Variable Description Example
VITE_API_URL Backend API base URL https://api.example.com

Obtaining Environment Values

1. Base RPC URL (BASE_RPC_URL_1)

You need a JSON-RPC endpoint for the Base blockchain. Options:

Provider Free Tier URL Format
Alchemy 300M compute units/month https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY
Infura 100K requests/day https://base-mainnet.infura.io/v3/YOUR_PROJECT_ID
QuickNode 50 req/sec https://YOUR_ENDPOINT.base-mainnet.quiknode.pro/YOUR_TOKEN
Public RPC Rate limited https://mainnet.base.org

For testnet (Base Sepolia): Use https://sepolia.base.org or your provider's Sepolia endpoint.

2. Notary Wallet Private Key (NOTARY_WALLET_PRIVATE_KEY)

This is the private key of the Ethereum wallet that will deploy and interact with the smart contract.

# Generate a new wallet using ethers.js
node -e "const { ethers } = require('ethers'); const w = ethers.Wallet.createRandom(); console.log('Address:', w.address); console.log('Private Key:', w.privateKey.slice(2));"

Important:

  • The wallet needs ETH on Base to pay gas fees (~$0.01-0.05 per anchor transaction)
  • Fund via a bridge from Ethereum mainnet → Base, or buy directly on Base
  • For testnet, use the Base Sepolia Faucet
  • Never commit this key to version control — use .env files or secrets management

3. Contract Address (NOTARY_CONTRACT_ADDRESS)

Obtained after deploying the smart contract (see Smart Contract Deployment).

4. Basescan API Key (BASESCAN_API_KEY)

Required only for verifying the contract source code on Basescan after deployment.

  1. Create an account at basescan.org
  2. Go to API Keys in your account settings
  3. Create a new API key

Smart Contract Deployment

Prerequisites

cd contracts
npm install

Configure .env

BASE_RPC_URL_1=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
NOTARY_WALLET_PRIVATE_KEY=your_64_char_hex_private_key
BASESCAN_API_KEY=your_basescan_api_key

Deploy to Base Sepolia (Testnet)

npx hardhat run scripts/deploy.js --network baseSepolia

Deploy to Base Mainnet

npx hardhat run scripts/deploy.js --network base

The deploy script will:

  1. Deploy the TabmacNotary contract
  2. Wait for 5 block confirmations
  3. Verify the source code on Basescan
  4. Print the contract address — add this to your .env as NOTARY_CONTRACT_ADDRESS

Verify an Existing Contract

npx hardhat verify --network base YOUR_CONTRACT_ADDRESS

Verification Flow

Two-Step Verification

┌─────────────────────────────────────────────────────────────────────┐
│                     USER UPLOADS PDF                                │
│                           │                                         │
│                           v                                         │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ STEP 1: Document Integrity (client-side, in-browser)       │    │
│  │                                                             │    │
│  │  1. Extract text from PDF (pdfjs-dist)                      │    │
│  │  2. Find "Contract Hash (SHA-256): <64-char hex>"           │    │
│  │  3. Find canonical payload between BEGIN/END markers        │    │
│  │  4. Decode base64 payload                                   │    │
│  │  5. Compute SHA-256 of decoded bytes (Web Crypto API)       │    │
│  │  6. Compare computed hash vs extracted hash                 │    │
│  │                                                             │    │
│  │  Result: PASSED (green) / TAMPERED (red) / UNAVAILABLE      │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                           │                                         │
│                    (auto-proceeds if passed)                        │
│                           │                                         │
│                           v                                         │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ STEP 2: Blockchain Verification (server-side)              │    │
│  │                                                             │    │
│  │  1. POST /api/verify-contract { contractHash }              │    │
│  │  2. Server looks up hash in database                        │    │
│  │  3. Server calls isAnchored() on smart contract             │    │
│  │  4. Returns verification status + blockchain proof          │    │
│  │                                                             │    │
│  │  Result: VERIFIED (green) / RECORD FOUND (yellow) /         │    │
│  │          NOT FOUND (red)                                    │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

Verification Entry Points

Entry Point Step 1 Step 2 Use Case
PDF Upload Yes Auto User has the PDF document
Manual Hash Input Skipped Yes User has only the hash
URL /verify/:hash Skipped Auto QR code / email link

Verification States

State Color Meaning
Verified on Blockchain Green Hash found on-chain with 10+ confirmations
Record Found Yellow Hash exists in database, pending on-chain anchoring
Not Found Red Hash not in database or on-chain
Document Integrity Passed Green PDF canonical payload matches printed hash
Document Tampered Red Computed hash doesn't match — PDF was modified
Integrity Unavailable Yellow Legacy PDF without embedded canonical payload

Data Structures

Canonical Snapshot Schema

The canonical snapshot captures all material contract terms at the moment of agreement:

{
  schemaVersion: "1.0.0",
  bookingId: "uuid",
  proposalId: "uuid",

  parties: {
    customer: { tabmacUserId, email, name, company },
    host: { tabmacUserId, email, name, venueId, venueName }
  },

  venue: { id, name, address, timezone },

  event: {
    eventType, guestCount, spaceType, serviceStyle,
    timeSlots: [{ slotIndex, date, startTime, endTime, guestQuote }]
  },

  pricing: {
    hostQuote, commissionAmount, taxAmount, gratuityAmount,
    transactionFeeAmount, guestQuote, depositAmount,
    itemizedLineItems: [{ label, amount, category }],
    currency: "usd"
  },

  payment: {
    stripePaymentIntentId, stripeChargeId, amount,
    capturedAt, paymentMethodBrand, paymentMethodLast4
  },

  formationEvents: [{
    eventType, actorRole, actorEmail, agreedAt,
    signingMethod, serverTimestamp
  }],

  cancellationPolicy: { type, terms },

  metadata: { createdAt, canonicalizationVersion: "1.0.0" }
}

Note: This schema is specific to Tabmac's venue booking use case. When adapting TabNotary, replace this with your own document schema — the canonicalization and hashing logic remains the same regardless of payload structure.

Blockchain Proof Response

{
  chainId: 8453,                    // Base mainnet
  networkName: "Base",
  contractAddress: "0x59Db...",     // TabmacNotary address
  transactionHash: "0xabc...",      // Anchor transaction
  blockNumber: 12345678,            // Block containing the tx
  blockTimestamp: "2026-01-15T...", // Block timestamp (UTC)
  confirmationDepth: 20,            // Current confirmations
  anchoredAt: "2026-01-15T...",     // When notarization completed
  explorerUrl: "https://basescan.org/tx/0xabc..."
}

Notarization Job Lifecycle

                  ┌─────────┐
                  │ queued   │ ← Created when snapshot is stored
                  └────┬────┘
                       │ Worker picks up job
                       v
                 ┌───────────┐
                 │ processing│ ← Checking idempotency
                 └─────┬─────┘
                       │ anchorContract() tx submitted
                       v
                  ┌─────────┐
                  │  sent    │ ← Waiting for block inclusion
                  └────┬────┘
                       │ Transaction included in block
                       v
                ┌────────────┐
                │ confirming │ ← Waiting for 10 confirmations
                └──────┬─────┘
                       │ 10 confirmations reached
                       v
                ┌────────────┐
                │ confirmed  │ ← Final state, proof stored
                └────────────┘

  On error at any step:
                ┌─────────┐
                │ failed   │ ← After 5 retries with exponential backoff
                └─────────┘

Security Considerations

On-Chain Security

  • Owner-only writes — Only the deployer wallet can anchor hashes, preventing unauthorized entries
  • Immutable records — Once anchored, records cannot be modified or deleted
  • Public readsisAnchored() is a view function callable by anyone

Backend Security

  • Private key management — The NOTARY_WALLET_PRIVATE_KEY should be stored in a secrets manager (AWS Secrets Manager, Vault, etc.), never in version control
  • Rate limiting — Verification endpoint is rate-limited to prevent abuse
  • No PII exposure — Verification responses contain only hash metadata and blockchain proof
  • Idempotency — Worker checks isAnchored() before submitting to avoid duplicate transactions

Client-Side Security

  • Local verification — Step 1 (PDF integrity) runs entirely in the browser via Web Crypto API
  • No data exfiltration — PDF content is never sent to the server during integrity verification
  • Cryptographic guarantee — SHA-256 via crypto.subtle.digest() (native browser implementation)

Wallet Security Best Practices

  • Use a dedicated hot wallet with minimal ETH balance (enough for ~100 transactions)
  • Monitor wallet balance and set up alerts for unusual activity
  • Consider a multisig for contract ownership in production
  • Rotate the wallet periodically by transferring contract ownership

Adapting for Your Use Case

TabNotary's architecture is generic. To adapt it for your documents:

1. Define Your Snapshot Schema

Replace buildCanonicalSnapshot() in contractSnapshot.js with your document structure:

function buildCanonicalSnapshot(yourDocument) {
  return {
    schemaVersion: '1.0.0',
    documentId: yourDocument.id,
    // ... your fields ...
    metadata: {
      createdAt: new Date().toISOString(),
      canonicalizationVersion: '1.0.0',
    },
  };
}

2. Keep the Canonicalization Layer

The deepSortKeys()JSON.stringify()SHA-256 pipeline works for any JSON-serializable data. Don't change this unless you have a specific reason.

3. Embed in Your PDFs

To enable client-side verification, embed the hash and canonical payload in your generated PDFs:

Contract Hash (SHA-256)
a1b2c3d4e5f6...

---BEGIN CANONICAL PAYLOAD---
eyJhIjoiYiIsImMiOiJkIn0=    (base64 of canonical JSON)
---END CANONICAL PAYLOAD---

4. Deploy Your Own Contract

Deploy TabmacNotary.sol on your preferred EVM chain. The contract works on any EVM-compatible network (Ethereum, Base, Arbitrum, Polygon, etc.). Adjust BASE_RPC_URL_1 and chain ID accordingly.

5. Swap the Database

The implementation uses MongoDB, but the pattern works with any database. You need two collections/tables:

  • Snapshots — Store canonical payload + hash + version
  • Notarization Jobs — Job queue for the worker

License

GPL-3.0 — See LICENSE for details.


Contributing

Contributions are welcome. Please open an issue first to discuss proposed changes.

Acknowledgments

Built by the Tabmac team. Deployed on Base blockchain.

About

Independently verify that a PDF contract is authentic and has not been tampered with.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors