StatVault · Witness Federation
Become a Witness
A StatVault receipt is anchored to a Knox evidence Merkle root. Every external party who countersigns that root raises the trust model from monocentric (“trust StatVault”) to federated (“trust the math with N independent signers”). You can become a witness in about five minutes with a GitHub repo.
What the workflow does
- Fetches /api/v1/trust/merkle for the current root.
- Signs the root with your ed25519 private key (stored as a workflow secret).
- POSTs the signature to
/api/v1/trust/witnesstwice daily.
Setup (one-time)
- Generate an ed25519 keypair locally:
openssl genpkey -algorithm ed25519 -out witness.pem
- Save the workflow to
.github/workflows/statvault-witness.ymlin any repo you control. - Add repo secrets:
WITNESS_PRIVKEY_PEM(contents ofwitness.pem), optionallyWITNESS_LABEL(a short name shown next to your pubkey). - Trigger the workflow once manually. Your signature appears on every receipt anchored to a co-signed root.
Workflow file
Also downloadable at /witness-workflow.yml.
# StatVault Receipt Witness — drop this in .github/workflows/ to become a witness.
#
# Setup once:
# 1. Generate an ed25519 keypair (openssl genpkey -algorithm ed25519 -out witness.pem)
# 2. Add as repo secret: WITNESS_PRIVKEY_PEM = <contents of witness.pem>
# 3. (Optional) Add WITNESS_LABEL = "your-name-here" so signatures are attributable.
#
# Each scheduled run fetches the current Knox Merkle root, signs it with your
# ed25519 key, and POSTs the signature to https://statvault.org/api/v1/trust/witness.
#
# Federates the trust model — every external witness raises the credibility of
# every StatVault receipt anchored to a co-signed root.
name: StatVault Witness
on:
schedule:
# Twice daily is plenty — the witness route accepts 12/hour per pubkey.
- cron: "0 6,18 * * *"
workflow_dispatch:
jobs:
sign-and-post:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Sign current Merkle root
env:
WITNESS_PRIVKEY_PEM: ${{ secrets.WITNESS_PRIVKEY_PEM }}
WITNESS_LABEL: ${{ secrets.WITNESS_LABEL }}
run: |
set -euo pipefail
if [ -z "${WITNESS_PRIVKEY_PEM:-}" ]; then
echo "::error::WITNESS_PRIVKEY_PEM secret is not set"
exit 1
fi
PRIVKEY_FILE="$(mktemp)"
trap 'rm -f "$PRIVKEY_FILE"' EXIT
printf '%s' "$WITNESS_PRIVKEY_PEM" > "$PRIVKEY_FILE"
# 1. Fetch current Merkle root
PROOF_JSON="$(curl -sSfL https://statvault.org/api/v1/trust/merkle)"
ROOT_HASH="$(echo "$PROOF_JSON" | jq -r '.root')"
CHAIN_LENGTH="$(echo "$PROOF_JSON" | jq -r '.chainLength')"
if ! echo "$ROOT_HASH" | grep -Eq '^[0-9a-f]{64}$'; then
echo "::error::Invalid root hash from API: $ROOT_HASH"
exit 1
fi
# 2. Sign the UTF-8 bytes of the hex root (matches verifier expectation)
SIG_HEX="$(printf '%s' "$ROOT_HASH" \
| openssl pkeyutl -sign -inkey "$PRIVKEY_FILE" -rawin \
| xxd -p -c 256)"
# 3. Derive raw ed25519 public key hex from the private key
PUBKEY_HEX="$(openssl pkey -in "$PRIVKEY_FILE" -pubout -outform DER \
| tail -c 32 | xxd -p -c 256)"
# 4. POST the signature
PAYLOAD="$(jq -n \
--arg rootHash "$ROOT_HASH" \
--arg signerPubkey "$PUBKEY_HEX" \
--arg signature "$SIG_HEX" \
--arg signerLabel "${WITNESS_LABEL:-gh-actions-witness}" \
'{rootHash:$rootHash, signerPubkey:$signerPubkey, signature:$signature, signerLabel:$signerLabel}')"
RESPONSE="$(curl -sS -X POST \
-H 'content-type: application/json' \
-d "$PAYLOAD" \
https://statvault.org/api/v1/trust/witness)"
echo "Witness response: $RESPONSE"
# Treat already-witnessed (root_not_current race) as success.
if echo "$RESPONSE" | jq -e '.ok' >/dev/null; then
echo "Signed Merkle root $ROOT_HASH (chain length $CHAIN_LENGTH)"
elif echo "$RESPONSE" | jq -e '.error == "root_not_current"' >/dev/null; then
echo "::notice::Root advanced mid-flight; will catch the next one."
else
echo "::error::Witness POST failed: $RESPONSE"
exit 1
fi
Reputation
Each pubkey accrues a public reputation derived from sample size, recency, and signature validity. New signers start as novice; five fresh valid signatures move you to trusted; twenty-five+ with healthy recency reaches veteran. The formula is deterministic and visible in src/lib/witness-reputation.ts.
Per-pubkey rate limit: 12 POSTs/hour. The default twice-daily schedule is well under the limit.