gilesb 7ead2026ef Add Bitcoin anchoring via OpenTimestamps
Implements merkle tree anchoring for ActivityPub activities with
provable Bitcoin timestamps. Activities are batched into merkle trees,
submitted to OpenTimestamps (free), and proofs stored on IPFS.

Key features:
- Merkle tree construction with membership proofs
- OpenTimestamps integration for Bitcoin anchoring
- JSONL backup file on persistent volume (survives DB wipes)
- API endpoints for creating/verifying anchors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 01:22:39 +00:00
2026-01-07 17:16:05 +00:00
2026-01-07 12:04:58 +00:00

Art DAG L2 Server - ActivityPub

Ownership registry and ActivityPub federation for Art DAG.

What it does

  • Registry: Maintains owned assets with content hashes
  • Activities: Creates signed ownership claims (Create activities)
  • Federation: ActivityPub endpoints for follow/share
  • L1 Integration: Records completed L1 runs as owned assets
  • Authentication: User registration, login, JWT tokens

Setup

pip install -r requirements.txt

# Configure (optional - defaults shown)
export ARTDAG_DOMAIN=artdag.rose-ash.com
export ARTDAG_USER=giles
export ARTDAG_DATA=~/.artdag/l2
export DATABASE_URL=postgresql://artdag:artdag@localhost:5432/artdag

# Generate signing keys (required for federation)
python setup_keys.py

# Start server
python server.py

JWT Secret Configuration

The JWT secret is used to sign authentication tokens. Without a persistent secret, tokens are invalidated on server restart.

Generate a secret

# Generate a 64-character hex secret
openssl rand -hex 32
# Or with Python
python -c "import secrets; print(secrets.token_hex(32))"

Local development

export JWT_SECRET="your-generated-secret-here"
python server.py

Create a Docker secret:

# From a generated value
openssl rand -hex 32 | docker secret create jwt_secret -

# Or from a file
echo "your-secret-here" > jwt_secret.txt
docker secret create jwt_secret jwt_secret.txt
rm jwt_secret.txt

Reference in docker-compose.yml:

services:
  l2-server:
    secrets:
      - jwt_secret

secrets:
  jwt_secret:
    external: true

The server reads secrets from /run/secrets/jwt_secret automatically.

Key Setup

ActivityPub requires RSA keys for signing activities. Generate them:

# Local
python setup_keys.py

# Or with custom paths
python setup_keys.py --data-dir /data/l2 --user giles

# In Docker, exec into container or mount volume
docker exec -it <container> python setup_keys.py

Keys are stored in $ARTDAG_DATA/keys/:

  • {username}.pem - Private key (chmod 600, NEVER share)
  • {username}.pub - Public key (included in actor profile)

Important: Private keys are gitignored. Back them up securely. Losing them invalidates all your signatures.

Client Commands

Upload Media

Register a media asset (image, video, audio) with a content hash:

curl -X POST http://localhost:8200/assets \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "name": "my-video",
    "content_hash": "abc123...",
    "asset_type": "video",
    "tags": ["art", "generated"]
  }'

Upload Recipe

Record an L1 run as an owned asset. This fetches the run details from the L1 server and registers the output:

curl -X POST http://localhost:8200/assets/record-run \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "run_id": "uuid-from-l1",
    "l1_server": "https://celery-artdag.rose-ash.com",
    "output_name": "my-rendered-video"
  }'

API Endpoints

Server Info

Method Path Description
GET / Home page with stats

Assets

Method Path Description
GET /assets List all assets
GET /assets/{name} Get asset by name
POST /assets Upload media - register new asset
POST /assets/record-run Upload recipe - record L1 run

ActivityPub

Method Path Description
GET /.well-known/webfinger?resource=acct:user@domain Actor discovery
GET /users/{username} Actor profile
GET /users/{username}/outbox Published activities
POST /users/{username}/inbox Receive activities
GET /users/{username}/followers Followers list
GET /objects/{content_hash} Get object by hash
GET /activities/{index} Get activity by index

Authentication

Method Path Description
POST /auth/register Register new user
POST /auth/login Login, get JWT token
GET /auth/me Get current user

Data Storage

Data stored in PostgreSQL:

  • users - Registered users
  • assets - Asset registry
  • activities - Signed activities
  • followers - Followers list

RSA keys stored in $ARTDAG_DATA/keys/ (files, not database).

Architecture

L2 Server (port 8200)
    │
    ├── POST /assets (upload media) → Register asset → Create activity → Sign
    │
    ├── POST /assets/record-run (upload recipe) → Fetch L1 run → Register output
    │       │
    │       └── GET L1_SERVER/runs/{id}
    │
    ├── GET /users/{user}/outbox → Return signed activities
    │
    └── POST /users/{user}/inbox → Receive Follow requests
Description
No description provided
Readme 2.2 MiB
Languages
Python 91.3%
HTML 8.4%
Dockerfile 0.2%
Shell 0.1%