* feat(node): owner-only push enforcement behind GITLAWB_ENFORCE_OWNER_PUSH (#31) A valid did:key signature on git-receive-pack is authentication, not authorization: any party can generate a key, derive its DID, sign, and push to an unprotected branch. Owner checks only ran on protected branches, so a non-owner could write to unprotected branches of an arbitrary repo. Add an opt-in flag GITLAWB_ENFORCE_OWNER_PUSH (default false, mirroring GITLAWB_REQUIRE_SIGNED_PEER_WRITES) that requires the authenticated pusher to be the repo owner on every branch. Default-off keeps live nodes unaffected. Design follows the maintainer's review on #31: - Authorize on the canonical AuthenticatedDid injected by require_signature, via Extension<AuthenticatedDid> on the handler — not a re-parse of the headers. The legacy branch-protection check is left as-is. - Fail closed: an absent identity or a non-owner caller is rejected. The policy lives in a pure fn (owner_push_rejection) so it is unit-tested without a DB. - The gate runs before branch protection, so one rejection yields one error body. - Factor the duplicated owner-match into auth::is_repo_owner, reused by receive-pack, protect.rs, and visibility.rs (each keeps its own message). events.rs / peers.rs are left out — they aren't owner checks. - Name the push gate by intent (caller_authorized_to_push) so the planned Phase 2 UCAN git/push capability is a pure addition, not a rewrite. Tests: owner (full + short DID) allowed, non-owner rejected, missing-DID rejected, flag-off legacy. Docs: .env.example + docs/RUN-A-NODE.md. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(node): address review on #68 — 403 status, finish owner-match consolidation Per @beardthelion's review: - Return 403 Forbidden (not 400) for authorization denials: the owner-push gate, the branch-protection denial, and the owner checks in protect.rs / visibility.rs. 400 reads as a malformed request and some git/CI clients retry it. - Finish the is_repo_owner consolidation: route the branch-protection check and the repos list filter through the helper. The branch-protection check now uses the verified AuthenticatedDid (auth.0) instead of re-parsing the headers. - Drop the second identity source on the push path entirely: the post-push event recording also uses auth.0, so extract_did_from_auth (and the handler's HeaderMap param + import) are removed. - Tests assert the Forbidden variant (so a future status regression fails loudly) and add direct coverage for is_repo_owner / caller_authorized_to_push. - Docs: warn that enabling the flag blocks delegated and CI agents until the Phase 2 UCAN git/push capability is honored. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: FrozenRaspberry <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
6.1 KiB
Running a gitlawb node
Step-by-step guide to staking $GITLAWB, registering your node on-chain, and earning protocol fees as a PoS operator.
Prerequisites
- A wallet with at least 10,000 $GITLAWB (minimum stake) plus a small amount of ETH on Base for gas
- Docker or Rust 1.91+ (for running the node process)
- A public HTTP URL (your-host.com) — can be a VPS, Fly.io app, or anything reachable. A Fly.io config is provided at
infra/fly/fly.toml(deploy from the repo root withfly deploy -c infra/fly/fly.toml)
1. Install the CLI
curl -fsSL https://gitlawb.com/install.sh | sh
# or build from source:
cargo install --path crates/gl
2. Create a gitlawb identity
Every node is identified by an Ed25519 DID keypair:
gl identity new
gl identity show
# → did:key:z6Mk...
This is your node DID — it's distinct from your Ethereum wallet.
3. Register your node on-chain
This stakes $GITLAWB and links your node DID to your operator wallet:
export GITLAWB_OPERATOR_PRIVATE_KEY=0xYOUR_KEY
export GITLAWB_TOKEN=0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3
export GITLAWB_CONTRACT_NODE_STAKING=0xNODE_STAKING_ADDR
export GITLAWB_CHAIN_RPC_URL=https://mainnet.base.org
gl node register \
--stake 10000 \
--http-url https://my-node.example.com
What this does:
- Checks your $GITLAWB balance
- Calls
token.approve(NodeStaking, 10000e18)if needed - Calls
NodeStaking.registerNode(didHash, httpUrl, 10000e18) - Transfers 10,000 $GITLAWB into escrow
4. Run the node
Option A — Docker
docker run -d \
--name gitlawb-node \
-p 7545:7545 \
-p 7546:7546/udp \
-v gitlawb-data:/data \
-e DATABASE_URL=postgresql://user:pass@host/gitlawb \
-e GITLAWB_PUBLIC_URL=https://my-node.example.com \
-e GITLAWB_OPERATOR_PRIVATE_KEY=$GITLAWB_OPERATOR_PRIVATE_KEY \
-e GITLAWB_CONTRACT_NODE_STAKING=$GITLAWB_CONTRACT_NODE_STAKING \
-e GITLAWB_CHAIN_RPC_URL=$GITLAWB_CHAIN_RPC_URL \
-e GITLAWB_OPERATOR_STRICT_MODE=true \
ghcr.io/gitlawb/node:latest
Option B — docker-compose
See docker-compose.yml — bundles node + Postgres.
Option C — From source
cargo run -p gitlawb-node --release
Required env for on-chain PoS mode:
GITLAWB_CONTRACT_NODE_STAKING— contract addressGITLAWB_OPERATOR_PRIVATE_KEY— key that posts heartbeatsGITLAWB_CHAIN_RPC_URL— Base RPC URL (default Sepolia)
Optional:
GITLAWB_OPERATOR_STRICT_MODE=true— refuse to start if not registered or not currently activeGITLAWB_HEARTBEAT_INTERVAL_HOURS=20— how often to post heartbeats (must be < 24)
5. Verify
gl node onchain-status
Expected output:
On-chain status for did:key:z6Mk...
Operator wallet: 0x...
Staked: 10000 $GITLAWB
HTTP URL: https://my-node.example.com
Last heartbeat: 1760000000 (unix)
Active flag: true
Currently active: true
Pending rewards: 0 $GITLAWB
Check your node logs — you should see operator heartbeat loop starting and a heartbeat tx within a few seconds of startup.
6. Earn rewards
The node's heartbeat loop runs every 20 hours. As long as you stay active:
- Every Sunday, the
FeeDistributordistributes accumulated fees. - 75% of the weekly pot is split across all active nodes (pro-rata by stake).
- Your share accrues as
pendingRewardson-chain — claim anytime without unstaking.
gl node claim
7. Unstake (if you want out)
7-day cooldown, two steps:
# Step 1: request unstake (starts 7-day timer)
gl node unstake-request
# ... wait 7 days ...
# Step 2: complete — returns stake + any pending rewards
gl node unstake
During the cooldown your node still earns rewards if it keeps heartbeating.
Hardening: owner-only push
By default the node authenticates every git-receive-pack push (a valid RFC 9421
did:key signature) but does not check that the pusher owns the repo, except
on branches that are explicitly protected. Because did:key is self-certifying,
any party can generate a key, derive its DID, sign, and push to an unprotected
branch — authentication is not authorization.
To require the authenticated pusher to be the repo owner on every branch, set:
GITLAWB_ENFORCE_OWNER_PUSH=true
- Default
false— preserves current behavior so live nodes are unaffected by an upgrade. Turn it on once you're ready for owner-only writes. - When
true— a push whose authenticated DID is not the repo owner is rejected (HTTP 403) before any ref update is applied. The owner is matched in both the fulldid:key:z6Mk…form and its barez6Mk…suffix. - Caution: this blocks every non-owner pusher, including your own delegated and
CI agents. Push authorization is owner-only today — a UCAN
git/pushcapability is verified but not yet honored for authorization, so delegated keys cannot push while this is on. Don't enable it until every identity that pushes to your repos is the owner, or you'll lock out your own automation. Scoped collaborator / UCAN-delegated push rights are a planned follow-up.
Operational checklist
| Concern | Recommendation |
|---|---|
| Heartbeat gas | ~$0.03/month on Base L2 — negligible |
| Missed heartbeats | After 3 days without one, you're excluded from rewards until you beat again |
| Operator key | Dedicated wallet, small ETH balance, not your main treasury |
| Monitoring | Watch lastHeartbeat on-chain; alert if > 22h since last beat |
| Public URL | Must resolve and serve /health — peers will ping it |
Troubleshooting
"insufficient $GITLAWB balance" — fund the operator wallet with at least 10,000 $GITLAWB.
Node refuses to start with "strict-mode operator check failed" — either gl node register first, or unset GITLAWB_OPERATOR_STRICT_MODE.
Rewards are 0 after a week — run gl node onchain-status. If currentlyActive: false, check your heartbeat loop (node logs for operator heartbeat sent).
Want to rotate operator wallet — requires unstake → re-register with new wallet. No in-place rotation in v1.
See ECONOMICS.md for the full reward math.