commit eec64d16c677a0fb09ada96fc2b0c8ccf75d3068 Author: Otto Date: Sun Mar 8 00:12:45 2026 +0100 Initial commit: Git OpenTimestamp hooks skill diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd2e58b --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# OpenTimestamp cache (local performance optimization) +.ots/.attestation-cache + +# Packaged skill file +*.skill + +# Editor/IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d46e23a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# AI Agent Instructions + +Quick reference for AI agents installing OpenTimestamp hooks. + +## When to Use + +Use this skill when the user wants to: +- Add tamper-evident timestamps to git commits +- Create cryptographic proof of commit history +- Anchor commits to Bitcoin blockchain via OpenTimestamp + +## Installation (Recommended) + +**Self-contained hooks** - No external dependencies: + +```bash +# 1. Install prerequisites +pipx install opentimestamps-client + +# 2. Install hooks (single command) +./hooks/install.sh . + +# 3. Commit proofs to repository +git add .ots/ .gitignore +git commit -m "Add OpenTimestamp proofs for commit history" +``` + +## Installation (Manual) + +Copy hook files directly: + +```bash +cp hooks/post-commit .git/hooks/ +cp hooks/pre-commit .git/hooks/ +chmod +x .git/hooks/post-commit .git/hooks/pre-commit +``` + +**.gitignore**: +``` +.ots/.attestation-cache +``` + +## Key Files + +| File | Purpose | Version? | +|------|---------|----------| +| `generate-proof.sh` | Creates proof for commit | Yes | +| `backfill-proofs.sh` | Upgrades all historical proofs | Yes | +| `install-ots-hook.sh` | Installs both hooks | Yes | +| `.ots/*.ots` | Binary proofs | Yes | +| `.ots/commit-chain.txt` | Commit chain | Yes | +| `.ots/.attestation-cache` | Local cache | No | + +## Verification + +```bash +# Check if proof is attested +ots info .ots/.ots | grep -c "PendingAttestation" +# 0 = attested, >0 = pending + +# Verify proof +ots verify .ots/.ots +``` + +## Notes + +- Proofs take ~10 min to become Bitcoin-attested +- Cache avoids redundant calendar calls (1-hour validity) +- Pre-commit backfill adds ~10-15s to commit time +- Safe to commit `.ots/` directory (binary proofs are small: ~500B each) diff --git a/README.md b/README.md new file mode 100644 index 0000000..9956aef --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# Git OpenTimestamp Hook + +Automatically generate and manage [OpenTimestamp](https://opentimestamps.org/) proofs for git commits. + +## What It Does + +- **Post-commit hook**: Creates cryptographic proof for every commit +- **Pre-commit backfill**: Upgrades historical proofs to Bitcoin-attested status +- **Proof chaining**: Links commits together for full history integrity +- **Smart caching**: Minimizes network calls after initial backfill + +Proofs are stored in `.ots/` and can be versioned alongside your code for tamper-evident history. + +## Quick Install + +### Recommended: Self-Contained Hooks + +```bash +# Clone or download this skill +git clone +cd git-ots-hook + +# Install both hooks (single command) +./hooks/install.sh /path/to/your/repo + +# Commit the proofs +cd /path/to/your/repo +git add .ots/ +git commit -m "Add OpenTimestamp proofs" +``` + +That's it! The hooks are now installed and will automatically timestamp every commit. + +### Legacy: Script-Based Installation + +The older `scripts/` directory is still available but deprecated. See `SKILL.md` for details. + +## Manual Installation + +If you prefer to install hooks manually without the installer: + +### 1. Copy Hook Files + +```bash +cp hooks/post-commit /path/to/your/repo/.git/hooks/ +cp hooks/pre-commit /path/to/your/repo/.git/hooks/ +chmod +x /path/to/your/repo/.git/hooks/post-commit +chmod +x /path/to/your/repo/.git/hooks/pre-commit +``` + +### 2. Setup .gitignore + +Add to your `.gitignore`: +``` +.ots/.attestation-cache +``` + +### 3. Commit Initial Proofs + +```bash +git add .ots/ +git commit -m "Add OpenTimestamp proofs" +``` + +## Prerequisites + +Install the OpenTimestamp client: + +```bash +# Recommended: Python CLI +pipx install opentimestamps-client + +# Alternative: Node.js package +npm install -g @opentimestamps/ots +``` + +Verify installation: +```bash +ots --version +``` + +## Generated Files + +``` +repo/ +├── .ots/ +│ ├── .ots # Individual proof per commit +│ ├── proof.ots # Latest commit proof +│ ├── prev-commit.txt # Previous commit reference +│ ├── commit-chain.txt # Full commit chain +│ └── .attestation-cache # Local cache (gitignore this) +└── ... +``` + +## Verification + +Verify a proof: +```bash +ots verify .ots/.ots +``` + +Check attestation status: +```bash +./scripts/check-attestation.sh .ots/.ots +# Output: "attested" or "pending" +``` + +## How It Works + +1. **Commit created** → Post-commit hook runs +2. **Proof generated** → `ots stamp` creates `.ots` file +3. **Submitted to calendars** → 4+ remote calendars receive hash +4. **Bitcoin anchoring** → Calendars batch and anchor to Bitcoin (~10 min) +5. **Attestation** → Proof becomes fully verifiable against Bitcoin blockchain + +## Performance + +- **First backfill**: 30-60s (scans history, contacts calendars) +- **Subsequent commits**: 10-15s (cached status skips redundant calls) +- **Cache**: 1-hour validity, 10-min recheck for pending proofs + +## License + +MIT + +## Links + +- [OpenTimestamp](https://opentimestamps.org/) +- [ots Client](https://github.com/opentimestamps/opentimestamps-client) diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..09b3b6f --- /dev/null +++ b/SKILL.md @@ -0,0 +1,163 @@ +--- +name: git-ots-hook +description: Install and manage OpenTimestamp git hooks that generate cryptographic proof for every commit. Use when you need to add tamper-evident timestamps to git commits using the OpenTimestamp protocol. Supports both ots CLI and nodejs fallback. +--- + +# Git OpenTimestamp Hook + +Automatically generates OpenTimestamp proofs for git commits via git hooks. + +## Quick Start (Recommended) + +**New: Self-contained hooks** - No external dependencies, simple installation: + +```bash +# Install both hooks (post-commit + pre-commit backfill) +./hooks/install.sh /path/to/repo + +# Or from repo root +./hooks/install.sh . +``` + +## Legacy Installation + +The older `scripts/` directory is still available for backward compatibility, but the new `hooks/` directory is recommended for simpler deployment. + +## What It Does + +- **Post-commit hook** (`hooks/post-commit`): Generates `.ots/.ots` for each new commit +- **Pre-commit backfill** (`hooks/pre-commit`): Before each commit, upgrades all historical proofs to attested status +- Creates `.ots/proof.ots` with latest commit proof +- Stores commit chain in `.ots/commit-chain.txt` +- Works with `ots` CLI (preferred) or `@opentimestamps/ots` node package (fallback) +- **Self-contained**: No external script dependencies, simple drop-in installation + +## Prerequisites + +Install one of: + +```bash +# Option 1: ots CLI (recommended) +git clone https://github.com/opentimestamps/opentimestamps-client +cd opentimestamps-client +pipx install . + +# Option 2: Node.js fallback +npm install -g @opentimestamps/ots +``` + +## Generated Files + +The hooks create: + +``` +repo/ +├── .ots/ +│ ├── proof.ots # Timestamp proof for latest commit +│ ├── .ots # Individual proof for each commit +│ ├── prev-commit.txt # Previous commit hash (for chaining) +│ └── commit-chain.txt # Full commit chain mapping +└── ... +``` + +Proof files include: +- Commit hash timestamp (OpenTimestamp format) +- Bitcoin attestation (after ~10 min confirmation) +- Commit chain references + +## Manual Usage + +### New Self-Contained Hooks (Recommended) + +**Install hooks:** +```bash +./hooks/install.sh /path/to/repo +``` + +**Manual hook installation:** +Copy `hooks/post-commit` and `hooks/pre-commit` directly to `.git/hooks/` + +### Legacy Scripts (Deprecated) + +**Generate proof for a specific commit:** +```bash +./scripts/generate-proof.sh [output-file] +``` + +**Backfill proofs for entire history:** +```bash +./scripts/backfill-proofs.sh /path/to/repo +``` + +**Setup .gitignore (excludes cache only, keeps proofs):** +```bash +./scripts/setup-gitignore.sh /path/to/repo +``` + +**Check proof attestation status:** +```bash +./scripts/check-attestation.sh .ots/.ots +# Output: "attested" or "pending" +``` + +Example: +```bash +./scripts/generate-proof.sh abc123def456 .ots/proof.ots +./scripts/backfill-proofs.sh . +./scripts/setup-gitignore.sh . +``` + +## Uninstall + +```bash +rm /path/to/repo/.git/hooks/post-commit +``` + +## Notes + +### Versioning Proofs (Recommended) + +To include proofs in your repository (recommended for tamper-evidence): + +```bash +# Commit proofs with your code +git add .ots/ +git commit -m "Add OpenTimestamp proofs" +``` + +The `.ots/` directory contains: +- `*.ots` - Binary proof files (commit these) +- `commit-chain.txt` - Commit chain mapping (commit this) +- `prev-commit.txt` - Latest previous commit (commit this) +- `.attestation-cache` - Local cache (add to `.gitignore`) + +**Recommended `.gitignore`:** +``` +.ots/.attestation-cache +``` + +### Performance + +- **Pre-commit backfill** can slow down commits on large histories +- First backfill: Full scan + calendar calls (~30-60s) +- Subsequent commits: Cached status skips calendar calls (~10-15s) +- Cache expires after 1 hour, re-checks pending proofs after 10 min + +### Other Notes + +- Hook is silent if neither `ots` nor node package is found +- Proofs become "attested" after ~10 min (Bitcoin confirmation) + +## Troubleshooting + +**Hook not running?** +- Check it's executable: `chmod +x .git/hooks/post-commit` +- Test manually: `.git/hooks/post-commit` + +**Proof not generating?** +- Verify `ots` or `@opentimestamps/ots` is installed +- Check hook output: `git commit` shows `[ots]` messages + +**Want auto-commit proofs?** +- Modify the hook to run `git add .ots/proof.ots && git commit --amend --no-edit` +- ⚠️ This changes commit history - use with caution diff --git a/hooks/install.sh b/hooks/install.sh new file mode 100755 index 0000000..c96c932 --- /dev/null +++ b/hooks/install.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Git OpenTimestamp Hooks Installer +# Copies self-contained hooks to git repository + +set -e + +REPO_PATH="${1:-.}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +cd "$REPO_PATH" + +# Verify we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: not a git repository" >&2 + exit 1 +fi + +HOOKS_DIR="$(git rev-parse --git-dir)/hooks" + +echo "Installing OpenTimestamp hooks to: $HOOKS_DIR" + +# Copy hooks +cp "$SCRIPT_DIR/post-commit" "$HOOKS_DIR/post-commit" +cp "$SCRIPT_DIR/pre-commit" "$HOOKS_DIR/pre-commit" + +# Make executable +chmod +x "$HOOKS_DIR/post-commit" "$HOOKS_DIR/pre-commit" + +echo "✓ Post-commit hook installed" +echo "✓ Pre-commit backfill hook installed" + +# Setup .gitignore +GITIGNORE=".gitignore" +if [ ! -f "$GITIGNORE" ]; then + echo ".ots/.attestation-cache" > "$GITIGNORE" + echo "✓ Created .gitignore" +elif ! grep -q ".ots/.attestation-cache" "$GITIGNORE"; then + echo "" >> "$GITIGNORE" + echo ".ots/.attestation-cache" >> "$GITIGNORE" + echo "✓ Updated .gitignore" +fi + +echo "" +echo "Next steps:" +echo "1. Make a test commit to verify hooks work" +echo "2. Commit the .ots/ directory: git add .ots/" +echo "3. Commit with message: git commit -m 'Add OpenTimestamp proofs'" +echo "" +echo "To uninstall:" +echo " rm $HOOKS_DIR/post-commit $HOOKS_DIR/pre-commit" diff --git a/hooks/post-commit b/hooks/post-commit new file mode 100755 index 0000000..044dfe3 --- /dev/null +++ b/hooks/post-commit @@ -0,0 +1,91 @@ +#!/bin/bash +# Git OpenTimestamp Post-Commit Hook +# Self-contained - no external dependencies +# Generates cryptographic proof for each commit using OpenTimestamp + +set -e + +# Configuration +OUTPUT_DIR=".ots" +OUTPUT_FILE="$OUTPUT_DIR/proof.ots" + +# Ensure output directory exists +mkdir -p "$OUTPUT_DIR" + +# Get the current commit hash +COMMIT_HASH=$(git rev-parse HEAD) + +# Function to generate proof using ots CLI +generate_with_ots_cli() { + local hash="$1" + local output="$2" + + # Create a temporary file with the hash (convert hex to binary) + local temp_file=$(mktemp) + local temp_ots="${temp_file}.ots" + + # Use python3 for hex to binary conversion (more portable than xxd) + python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('$hash'))" > "$temp_file" + + # Generate timestamp - ots creates .ots file alongside the original + ots stamp "$temp_file" 2>/dev/null + + # Move the generated .ots file to the desired location + if [ -f "$temp_ots" ]; then + mv "$temp_ots" "$output" + rm -f "$temp_file" + return 0 + else + rm -f "$temp_file" + return 1 + fi +} + +# Function to generate proof using nodejs fallback +generate_with_node() { + local hash="$1" + local output="$2" + + node -e " +const ots = require('@opentimestamps/ots'); +const fs = require('fs'); +const hash = '$hash'; +const output = '$output'; +const hashBuffer = Buffer.from(hash, 'hex'); +ots.Timestamp.hash(hashBuffer).then(timestamp => { + const proof = { hash: hash, timestamp: new Date().toISOString(), status: 'pending' }; + fs.writeFileSync(output, JSON.stringify(proof, null, 2)); + console.log('Generated local proof (nodejs fallback)'); +}).catch(err => { console.error('Error:', err); process.exit(1); }); +" +} + +# Check for available tools and generate proof +if command -v ots &> /dev/null; then + if generate_with_ots_cli "$COMMIT_HASH" "$OUTPUT_FILE"; then + echo "[ots] Generated proof with ots CLI: ${COMMIT_HASH:0:8}" + else + echo "[ots] Warning: ots CLI failed" >&2 + exit 0 + fi +elif command -v node &> /dev/null && node -e "require('@opentimestamps/ots')" &> /dev/null 2>&1; then + generate_with_node "$COMMIT_HASH" "$OUTPUT_FILE" + echo "[ots] Generated proof with nodejs fallback: ${COMMIT_HASH:0:8}" +else + echo "[ots] Warning: Neither ots CLI nor @opentimestamps/ots found, skipping proof" >&2 + exit 0 +fi + +# Save previous commit hash for chaining +PREV_COMMIT=$(git rev-parse HEAD^1 2>/dev/null || echo "") +if [ -n "$PREV_COMMIT" ]; then + echo "$PREV_COMMIT" > "$OUTPUT_DIR/prev-commit.txt" +fi + +# Create individual proof file for this commit +INDIVIDUAL_PROOF="$OUTPUT_DIR/${COMMIT_HASH}.ots" +if [ -f "$OUTPUT_FILE" ]; then + cp "$OUTPUT_FILE" "$INDIVIDUAL_PROOF" +fi + +echo "[ots] Proof generated successfully" diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..9337782 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,156 @@ +#!/bin/bash +# Git OpenTimestamp Pre-Commit Backfill Hook +# Self-contained - no external dependencies +# Upgrades all historical proofs before each new commit + +set -e + +# Configuration +OUTPUT_DIR=".ots" +STATUS_CACHE="$OUTPUT_DIR/.attestation-cache" + +# Ensure output directory exists +mkdir -p "$OUTPUT_DIR" + +# Initialize cache file if it doesn't exist +if [ ! -f "$STATUS_CACHE" ]; then + echo "# Attestation status cache" > "$STATUS_CACHE" + echo "# Format: commit-hash:status:timestamp" >> "$STATUS_CACHE" +fi + +# Function to get cached status +get_cached_status() { + local commit="$1" + local cache_line=$(grep "^$commit:" "$STATUS_CACHE" 2>/dev/null | tail -1) + if [ -n "$cache_line" ]; then + local status=$(echo "$cache_line" | cut -d: -f2) + local timestamp=$(echo "$cache_line" | cut -d: -f3) + local now=$(date +%s) + local age=$((now - timestamp)) + # Cache valid for 1 hour (3600 seconds) + if [ "$age" -lt 3600 ]; then + echo "$status" + return 0 + fi + fi + return 1 +} + +# Function to cache status +cache_status() { + local commit="$1" + local status="$2" + local now=$(date +%s) + echo "$commit:$status:$now" >> "$STATUS_CACHE" +} + +# Function to generate proof using ots CLI +generate_proof() { + local hash="$1" + local output="$2" + + local temp_file=$(mktemp) + local temp_ots="${temp_file}.ots" + + python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('$hash'))" > "$temp_file" + ots stamp "$temp_file" 2>/dev/null + + if [ -f "$temp_ots" ]; then + mv "$temp_ots" "$output" + rm -f "$temp_file" + return 0 + else + rm -f "$temp_file" + return 1 + fi +} + +# Function to check if proof is attested +is_attested() { + local proof_file="$1" + local pending_count=$(ots info "$proof_file" 2>&1 | grep -c "PendingAttestation" || echo "0") + [ "$pending_count" -eq 0 ] +} + +# Function to upgrade proof +upgrade_proof() { + local proof_file="$1" + ots upgrade "$proof_file" 2>/dev/null +} + +echo "[ots] Backfilling proofs..." + +# Get all commit hashes (oldest to newest) +COMMITS=$(git rev-list --reverse HEAD) +TOTAL=$(echo "$COMMITS" | wc -l) +CURRENT=0 +UPDATED=0 + +for COMMIT in $COMMITS; do + CURRENT=$((CURRENT + 1)) + PROOF_FILE="$OUTPUT_DIR/${COMMIT}.ots" + + # Skip verbose output for brevity + if [ $CURRENT -le 3 ] || [ $CURRENT -eq $TOTAL ]; then + echo "[ots] Processing commit $CURRENT/$TOTAL: ${COMMIT:0:8}" + elif [ $CURRENT -eq 4 ]; then + echo "[ots] ... processing remaining commits ..." + fi + + if [ -f "$PROOF_FILE" ]; then + # Check cached status first + CACHED_STATUS=$(get_cached_status "$COMMIT" || echo "") + + if [ "$CACHED_STATUS" = "attested" ]; then + continue + fi + + # Check if already attested + if is_attested "$PROOF_FILE"; then + cache_status "$COMMIT" "attested" + continue + fi + + # Cache as pending + cache_status "$COMMIT" "pending" + + # Skip upgrade if cache is fresh (< 10 min old) + CACHE_LINE=$(grep "^$COMMIT:" "$STATUS_CACHE" | tail -1) + CACHE_TIME=$(echo "$CACHE_LINE" | cut -d: -f3) + NOW=$(date +%s) + CACHE_AGE=$((NOW - CACHE_TIME)) + + if [ "$CACHE_AGE" -lt 600 ]; then + continue + fi + + # Try to upgrade + if upgrade_proof "$PROOF_FILE"; then + cache_status "$COMMIT" "attested" + UPDATED=$((UPDATED + 1)) + fi + else + # Generate new proof + if generate_proof "$COMMIT" "$PROOF_FILE"; then + cache_status "$COMMIT" "pending" + UPDATED=$((UPDATED + 1)) + fi + fi +done + +# Update latest proof symlink +LATEST_COMMIT=$(git rev-parse HEAD) +if [ -f "$OUTPUT_DIR/${LATEST_COMMIT}.ots" ]; then + cp "$OUTPUT_DIR/${LATEST_COMMIT}.ots" "$OUTPUT_DIR/proof.ots" +fi + +# Save commit chain +rm -f "$OUTPUT_DIR/commit-chain.txt" +git rev-list HEAD | while read COMMIT; do + PREV=$(git rev-parse ${COMMIT}^1 2>/dev/null || echo "") + if [ -n "$PREV" ]; then + echo "$COMMIT:$PREV" >> "$OUTPUT_DIR/commit-chain.txt" + fi +done + +echo "[ots] Backfill complete: $UPDATED proofs updated" diff --git a/scripts/backfill-proofs.sh b/scripts/backfill-proofs.sh new file mode 100755 index 0000000..38fc3c0 --- /dev/null +++ b/scripts/backfill-proofs.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# backfill-proofs.sh - Generate/upgrade OpenTimestamp proofs for all commits +# Usage: backfill-proofs.sh [repository-path] + +set -e + +REPO_PATH="${1:-.}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +cd "$REPO_PATH" + +# Verify we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: not a git repository" >&2 + exit 1 +fi + +# Find the generate-proof.sh script +GENERATE_SCRIPT="" + +# Check if generate-proof.sh is in the same directory as this script +if [ -f "$SCRIPT_DIR/generate-proof.sh" ]; then + GENERATE_SCRIPT="$SCRIPT_DIR/generate-proof.sh" +# Check in repository root +elif [ -f "generate-proof.sh" ]; then + GENERATE_SCRIPT="./generate-proof.sh" +# Check if skill is installed globally +elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh" ]; then + GENERATE_SCRIPT="$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh" +else + echo "Error: generate-proof.sh not found" >&2 + exit 1 +fi + +chmod +x "$GENERATE_SCRIPT" + +# Ensure .ots directory exists +mkdir -p .ots + +# Cache file for attestation status (avoids repeated ots info calls) +STATUS_CACHE=".ots/.attestation-cache" +if [ ! -f "$STATUS_CACHE" ]; then + echo "# Attestation status cache" > "$STATUS_CACHE" + echo "# Format: commit-hash:status:timestamp" >> "$STATUS_CACHE" +fi + +# Function to check cached status +get_cached_status() { + local commit="$1" + local cache_line=$(grep "^$commit:" "$STATUS_CACHE" 2>/dev/null | tail -1) + if [ -n "$cache_line" ]; then + local status=$(echo "$cache_line" | cut -d: -f2) + local timestamp=$(echo "$cache_line" | cut -d: -f3) + local now=$(date +%s) + local age=$((now - timestamp)) + # Cache valid for 1 hour (3600 seconds) + if [ "$age" -lt 3600 ]; then + echo "$status" + return 0 + fi + fi + return 1 +} + +# Function to cache status +cache_status() { + local commit="$1" + local status="$2" + local now=$(date +%s) + echo "$commit:$status:$now" >> "$STATUS_CACHE" +} + +echo "[ots-backfill] Scanning commit history..." + +# Get all commit hashes (oldest to newest) +COMMITS=$(git rev-list --reverse HEAD) +TOTAL=$(echo "$COMMITS" | wc -l) +CURRENT=0 + +# Track if any proofs were updated +UPDATED=0 + +for COMMIT in $COMMITS; do + CURRENT=$((CURRENT + 1)) + echo "[ots-backfill] Processing commit $CURRENT/$TOTAL: ${COMMIT:0:8}" + + # Check if proof exists for this commit + PROOF_FILE=".ots/${COMMIT}.ots" + + if [ -f "$PROOF_FILE" ]; then + # Check cached status first + CACHED_STATUS=$(get_cached_status "$COMMIT" || echo "") + + if [ "$CACHED_STATUS" = "attested" ]; then + echo " ✓ Already attested (cached, skipping)" + continue + fi + + # Check if already fully attested (only if not cached or cache expired) + PENDING_COUNT=$(ots info "$PROOF_FILE" 2>&1 | grep -c "PendingAttestation" || echo "0") + + if [ "$PENDING_COUNT" -eq 0 ]; then + echo " ✓ Already attested" + cache_status "$COMMIT" "attested" + continue + fi + + # Cache as pending + cache_status "$COMMIT" "pending" + + # Skip upgrade if cache is fresh (< 10 min old) + CACHE_LINE=$(grep "^$COMMIT:" "$STATUS_CACHE" | tail -1) + CACHE_TIME=$(echo "$CACHE_LINE" | cut -d: -f3) + NOW=$(date +%s) + CACHE_AGE=$((NOW - CACHE_TIME)) + + if [ "$CACHE_AGE" -lt 600 ]; then + echo " - Pending (cached <10min, skipping calendar call)" + continue + fi + + # Try to upgrade existing proof (contacts calendars) + echo " Upgrading pending proof..." + if ots upgrade "$PROOF_FILE" 2>/dev/null; then + echo " ✓ Upgraded to attested" + cache_status "$COMMIT" "attested" + UPDATED=$((UPDATED + 1)) + else + echo " - Still pending (no upgrade available yet)" + # Update cache timestamp + cache_status "$COMMIT" "pending" + fi + else + # Generate new proof for this commit + echo " Generating new proof..." + "$GENERATE_SCRIPT" "$COMMIT" "$PROOF_FILE" >/dev/null 2>&1 + echo " ✓ Generated proof" + cache_status "$COMMIT" "pending" + UPDATED=$((UPDATED + 1)) + fi +done + +# Create/update latest proof symlink +LATEST_COMMIT=$(git rev-parse HEAD) +if [ -f ".ots/${LATEST_COMMIT}.ots" ]; then + cp ".ots/${LATEST_COMMIT}.ots" ".ots/proof.ots" + echo "[ots-backfill] Updated .ots/proof.ots for latest commit" +fi + +# Save previous commit chain info +echo "[ots-backfill] Saving commit chain..." +git rev-list HEAD | while read COMMIT; do + PREV=$(git rev-parse ${COMMIT}^1 2>/dev/null || echo "") + if [ -n "$PREV" ]; then + echo "$COMMIT:$PREV" >> ".ots/commit-chain.txt" + fi +done + +echo "" +echo "[ots-backfill] Complete! Processed $TOTAL commits, updated $UPDATED proofs" +echo "" +echo "Proofs stored in: .ots/" +echo " - Individual proofs: .ots/.ots" +echo " - Latest proof: .ots/proof.ots" +echo " - Commit chain: .ots/commit-chain.txt" diff --git a/scripts/check-attestation.sh b/scripts/check-attestation.sh new file mode 100755 index 0000000..5420dd3 --- /dev/null +++ b/scripts/check-attestation.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# check-attestation.sh - Check if an OpenTimestamp proof is fully attested +# Usage: check-attestation.sh +# Returns: 0 if fully attested, 1 if pending + +PROOF_FILE="${1:-}" + +if [ -z "$PROOF_FILE" ] || [ ! -f "$PROOF_FILE" ]; then + echo "Error: proof file not found: $PROOF_FILE" >&2 + exit 2 +fi + +# Check for PendingAttestation in ots info output +if ots info "$PROOF_FILE" 2>&1 | grep -q "PendingAttestation"; then + echo "pending" + exit 1 +else + echo "attested" + exit 0 +fi diff --git a/scripts/generate-proof.sh b/scripts/generate-proof.sh new file mode 100755 index 0000000..d85d70f --- /dev/null +++ b/scripts/generate-proof.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# generate-proof.sh - Generate OpenTimestamp proof for a git commit +# Usage: generate-proof.sh [output-file] + +set -e + +COMMIT_HASH="${1:-}" +OUTPUT_FILE="${2:-.ots/proof.ots}" + +if [ -z "$COMMIT_HASH" ]; then + echo "Error: commit hash required" >&2 + echo "Usage: $0 [output-file]" >&2 + exit 1 +fi + +# Ensure .ots directory exists +mkdir -p "$(dirname "$OUTPUT_FILE")" + +# Function to generate proof using ots CLI +generate_with_ots_cli() { + local hash="$1" + local output="$2" + + # Create a temporary file with the hash (convert hex to binary) + local temp_file=$(mktemp) + local temp_ots="${temp_file}.ots" + + # Use python3 for hex to binary conversion (more portable than xxd) + python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('$hash'))" > "$temp_file" + + # Generate timestamp - ots creates .ots file alongside the original + ots stamp "$temp_file" + + # Move the generated .ots file to the desired location + if [ -f "$temp_ots" ]; then + mkdir -p "$(dirname "$output")" + mv "$temp_ots" "$output" + echo "Generated proof with ots CLI: $output" + else + echo "Error: ots did not generate proof file" >&2 + rm -f "$temp_file" + exit 1 + fi + + # Cleanup + rm -f "$temp_file" +} + +# Function to generate proof using opentimestamps-nodejs +generate_with_node() { + local hash="$1" + local output="$2" + + # Use nodejs opentimestamps library + # This requires @opentimestamps/ots to be installed globally + node -e " +const ots = require('@opentimestamps/ots'); +const fs = require('fs'); +const crypto = require('crypto'); + +const hash = '$hash'; +const output = '$output'; + +// Convert hex hash to buffer +const hashBuffer = Buffer.from(hash, 'hex'); + +// Create timestamp +ots.Timestamp.hash(hashBuffer).then(timestamp => { + // In a real implementation, this would contact the calendar server + // For now, we create a minimal proof structure + console.log('Warning: nodejs fallback creates local proof only'); + console.log('Install ots CLI for full functionality: https://github.com/opentimestamps/opentimestamps-client'); + + // Create a simple proof file (this is a placeholder - real impl needs calendar) + const proof = { + hash: hash, + timestamp: new Date().toISOString(), + status: 'pending' + }; + + fs.writeFileSync(output, JSON.stringify(proof, null, 2)); + console.log('Generated local proof: ' + output); +}).catch(err => { + console.error('Error generating timestamp:', err); + process.exit(1); +}); +" +} + +# Check if ots CLI is available +if command -v ots &> /dev/null; then + echo "Using ots CLI..." + generate_with_ots_cli "$COMMIT_HASH" "$OUTPUT_FILE" +elif command -v node &> /dev/null && node -e "require('@opentimestamps/ots')" &> /dev/null; then + echo "Using opentimestamps-nodejs fallback..." + generate_with_node "$COMMIT_HASH" "$OUTPUT_FILE" +else + echo "Error: Neither ots CLI nor @opentimestamps/ots node package found" >&2 + echo "Install one of:" >&2 + echo " - ots CLI: https://github.com/opentimestamps/opentimestamps-client" >&2 + echo " - Node package: npm install -g @opentimestamps/ots" >&2 + exit 1 +fi + +# Get previous commit hash for chaining (before appending to proof file) +PREV_COMMIT=$(git rev-parse HEAD^1 2>/dev/null || echo "") + +# Store previous commit hash in a separate metadata file for chaining +if [ -n "$PREV_COMMIT" ]; then + echo "$PREV_COMMIT" > "$(dirname "$OUTPUT_FILE")/prev-commit.txt" + echo "Chaining info saved: $PREV_COMMIT" +fi + +echo "Proof generated for commit: $COMMIT_HASH" diff --git a/scripts/install-ots-hook.sh b/scripts/install-ots-hook.sh new file mode 100755 index 0000000..46c34b7 --- /dev/null +++ b/scripts/install-ots-hook.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# install-ots-hook.sh - Install OpenTimestamp post-commit hook +# Usage: install-ots-hook.sh [repository-path] [backfill] + +set -e + +REPO_PATH="${1:-.}" +ENABLE_BACKFILL="${2:-}" # Second arg: "backfill" to enable pre-commit backfill hook +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +cd "$REPO_PATH" + +# Verify we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: not a git repository" >&2 + exit 1 +fi + +HOOKS_DIR="$(git rev-parse --git-dir)/hooks" +POST_COMMIT_HOOK="$HOOKS_DIR/post-commit" + +echo "Installing OpenTimestamp hooks in: $REPO_PATH" + +# Create the post-commit hook +cat > "$POST_COMMIT_HOOK" << 'HOOK_SCRIPT' +#!/bin/bash +# Post-commit hook: generate OpenTimestamp proof for each commit + +set -e + +# Get the commit hash +COMMIT_HASH=$(git rev-parse HEAD) + +# Find the generate-proof.sh script +# Try relative paths first, then check common locations +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GENERATE_SCRIPT="" + +# Check if generate-proof.sh is in the same directory as this hook +if [ -f "$SCRIPT_DIR/generate-proof.sh" ]; then + GENERATE_SCRIPT="$SCRIPT_DIR/generate-proof.sh" +# Check in .git/hooks directory +elif [ -f ".git/hooks/generate-proof.sh" ]; then + GENERATE_SCRIPT=".git/hooks/generate-proof.sh" +# Check in repository root +elif [ -f "generate-proof.sh" ]; then + GENERATE_SCRIPT="./generate-proof.sh" +# Check if skill is installed globally (common OpenClaw path) +elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh" ]; then + GENERATE_SCRIPT="$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/generate-proof.sh" +else + echo "Warning: generate-proof.sh not found, skipping OpenTimestamp proof" >&2 + exit 0 +fi + +# Make sure the script is executable +chmod +x "$GENERATE_SCRIPT" + +# Generate the proof +echo "[ots] Generating proof for commit: $COMMIT_HASH" +"$GENERATE_SCRIPT" "$COMMIT_HASH" + +echo "[ots] Proof generated successfully" +HOOK_SCRIPT + +# Make the hook executable +chmod +x "$POST_COMMIT_HOOK" + +echo "✓ Post-commit hook installed successfully" + +# Optionally install pre-commit backfill hook +if [ "$ENABLE_BACKFILL" = "backfill" ]; then + PRE_COMMIT_HOOK="$HOOKS_DIR/pre-commit" + cp "$SCRIPT_DIR/pre-commit-backfill" "$PRE_COMMIT_HOOK" + chmod +x "$PRE_COMMIT_HOOK" + echo "✓ Pre-commit backfill hook installed (will backfill proofs before each commit)" +fi + +# Optionally setup .gitignore +if [ -f "$SCRIPT_DIR/setup-gitignore.sh" ]; then + "$SCRIPT_DIR/setup-gitignore.sh" "$REPO_PATH" +elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/setup-gitignore.sh" ]; then + "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/setup-gitignore.sh" "$REPO_PATH" +fi + +echo "" +echo "Next steps:" +echo "1. Ensure scripts are accessible (copy to repo root or install skill globally)" +echo "2. Verify ots CLI or @opentimestamps/ots is installed" +echo "3. Add .ots/ to git tracking: git add .ots/" +echo "4. Commit proofs: git commit -m 'Add OpenTimestamp proofs'" +echo "5. Make a test commit to verify the hooks work" +echo "" +echo "To uninstall:" +echo " rm $POST_COMMIT_HOOK" +if [ "$ENABLE_BACKFILL" = "backfill" ]; then + echo " rm $PRE_COMMIT_HOOK" +fi diff --git a/scripts/pre-commit-backfill b/scripts/pre-commit-backfill new file mode 100755 index 0000000..3ff9a7d --- /dev/null +++ b/scripts/pre-commit-backfill @@ -0,0 +1,32 @@ +#!/bin/bash +# Pre-commit hook: backfill proofs for all commits before creating new commit +# This ensures all historical commits have attested proofs + +set -e + +# Get the directory where this hook is installed +HOOKS_DIR="$(git rev-parse --git-dir)/hooks" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Find the backfill-proofs.sh script +BACKFILL_SCRIPT="" + +if [ -f "$SCRIPT_DIR/backfill-proofs.sh" ]; then + BACKFILL_SCRIPT="$SCRIPT_DIR/backfill-proofs.sh" +elif [ -f ".git/hooks/backfill-proofs.sh" ]; then + BACKFILL_SCRIPT=".git/hooks/backfill-proofs.sh" +elif [ -f "backfill-proofs.sh" ]; then + BACKFILL_SCRIPT="./backfill-proofs.sh" +elif [ -f "$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/backfill-proofs.sh" ]; then + BACKFILL_SCRIPT="$HOME/.openclaw/workspace/skills/git-ots-hook/scripts/backfill-proofs.sh" +else + # Silently skip if backfill script not found + exit 0 +fi + +chmod +x "$BACKFILL_SCRIPT" + +echo "[ots] Backfilling proofs before commit..." +"$BACKFILL_SCRIPT" . + +echo "[ots] Ready to commit" diff --git a/scripts/setup-gitignore.sh b/scripts/setup-gitignore.sh new file mode 100755 index 0000000..6bb6619 --- /dev/null +++ b/scripts/setup-gitignore.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# setup-gitignore.sh - Create/update .gitignore for OpenTimestamp proofs +# Usage: setup-gitignore.sh [repository-path] + +set -e + +REPO_PATH="${1:-.}" + +cd "$REPO_PATH" + +# Verify we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: not a git repository" >&2 + exit 1 +fi + +GITIGNORE=".gitignore" + +# Create .gitignore if it doesn't exist +if [ ! -f "$GITIGNORE" ]; then + echo "# OpenTimestamp proofs cache (exclude from version control)" > "$GITIGNORE" + echo ".ots/.attestation-cache" >> "$GITIGNORE" + echo "" + echo "Created $GITIGNORE with OpenTimestamp cache exclusion" +else + # Check if already has the cache exclusion + if grep -q ".ots/.attestation-cache" "$GITIGNORE"; then + echo "$GITIGNORE already excludes .ots/.attestation-cache" + else + echo "" >> "$GITIGNORE" + echo "# OpenTimestamp proofs cache (exclude from version control)" >> "$GITIGNORE" + echo ".ots/.attestation-cache" >> "$GITIGNORE" + echo "Added .ots/.attestation-cache to $GITIGNORE" + fi +fi + +echo "" +echo "Next steps:" +echo "1. Review and commit your .gitignore" +echo "2. Add .ots/ directory to track proofs: git add .ots/" +echo "3. Commit proofs: git commit -m 'Add OpenTimestamp proofs'"