fix(node): harden Arweave anchoring and add verification (#26) - #224
fix(node): harden Arweave anchoring and add verification (#26)#224Gravirei wants to merge 30 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis change replaces Irys uploads with bundler requests, embeds chained ref certificates and pusher signature metadata, adds anchor lifecycle persistence, and exposes gateway-based Arweave transaction verification. ChangesArweave integrity flow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant git_receive_pack
participant issue_ref_certificate
participant Bundler
participant verify_anchor_endpoint
participant verify_anchor
participant ArweaveGateway
participant Db
Client->>git_receive_pack: Authenticated push request
git_receive_pack->>issue_ref_certificate: Pass pusher signature and proof
issue_ref_certificate->>Db: Store chained certificate
git_receive_pack->>Bundler: POST /v1/tx with certificate anchor
Bundler-->>git_receive_pack: Return transaction ID
Client->>verify_anchor_endpoint: GET /api/v1/arweave/verify/{tx_id}
verify_anchor_endpoint->>verify_anchor: Verify transaction ID
verify_anchor->>ArweaveGateway: GET gateway/{tx_id}
ArweaveGateway-->>verify_anchor: Return anchored payload
verify_anchor->>Db: Load predecessor certificate
verify_anchor-->>Client: Return validity, errors, and certificate
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
0801800 to
bd09c35
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/arweave.rs`:
- Around line 351-373: Update the prev-linkage validation in verify_anchor to
fetch and hash the local certificate at sequence c.seq - 1, rather than the
newest certificate returned by get_most_recent_cert. Only perform the comparison
when that predecessor exists, while preserving the existing mismatch error
handling and payload hashing behavior.
- Around line 319-343: Update the signature decoding in the certificate
verification flow to use the URL-safe, no-padding base64 engine matching
node_keypair.sign_b64 output, while preserving the existing 64-byte validation
and error-result handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fee43d49-2e4b-4bbd-9921-8248f57fea48
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
crates/gitlawb-node/src/api/arweave.rscrates/gitlawb-node/src/api/events.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/arweave.rscrates/gitlawb-node/src/auth/mod.rscrates/gitlawb-node/src/cert.rscrates/gitlawb-node/src/config.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/server.rscrates/gitlawb-node/src/test_support.rs
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Use the configured gateway's data URL when verifying an anchor
crates/gitlawb-node/src/arweave.rs:265
arweave_gatewaydefaults tohttps://arweave.net, but this requests/v1/tx/{id}, which is the bundler API used for uploads rather than an Arweave gateway data URL. Consequently every normally uploaded anchor is reported invalid with the documented default configuration. Fetch the item through the gateway's data path (or add a separately named bundler-read configuration), and cover the default configuration rather than a mock of the bundler path. -
[P1] Bind each anchor to the certificate for its own ref update
crates/gitlawb-node/src/api/repos.rs:1306
Certificates are issued once per update above this block, but every iteration subsequently reads the repository-wide latest certificate. In a multi-ref push, all permanent anchors therefore embed the last update's certificate; another completed push can also win the asynchronous race. Sinceverify_anchornever compares the certificate's repo/ref/old/new fields with the outer anchor, it returns valid for an anchor whose advertised transition was never signed. Preserve the returned certificate per update and reject a mismatch during verification. -
[P1] Preserve certificate history and fail closed on a missing predecessor
crates/gitlawb-node/src/db/mod.rs:2013
The existing(repo_id, ref_name)upsert overwrites the predecessor whenever that ref is pushed again, while the newseq/prevdesign requires that predecessor to remain available.verify_anchorthen silently skips the check whenget_cert_by_seqreturnsNoneor errors, so an ordinary repeated push produces a truncated chain reported as valid. Make chain entries append-only and treat an unavailable declared predecessor as invalid (or explicitly unverifiable). -
[P2] Allocate chain sequence numbers atomically
crates/gitlawb-node/src/cert.rs:31
Sequence allocation is a read-then-increment with no transaction, lock, or unique(repo_id, seq)constraint. Concurrent successful pushes can receive the same sequence and predecessor;get_cert_by_seqthen selects an arbitrary row. This makes a signed chain nondeterministic under normal concurrent traffic. Allocate the sequence transactionally and enforce uniqueness, with retry on collision. -
[P1] Do not describe raw signature bytes as a verifiable pusher authorization proof
crates/gitlawb-node/src/auth/mod.rs:252
Only the 64-byte Ed25519 signature is persisted. The RFC 9421Signature-Input, covered component values, method/path, and content digest are discarded, andverify_anchornever verifiespusher_sig. A third party therefore cannot reconstruct the signing string or bind these bytes to this push, yet the endpoint can report the anchor valid. Persist a complete verifiable authorization artifact and validate it, or remove the proof/verification claim. -
[P1] Bound the untrusted response read on the public verification route
crates/gitlawb-node/src/arweave.rs:279
The new unauthenticated, unthrottled route buffers the full gateway response withresp.bytes()before attempting JSON parsing. A caller can repeatedly select large data items and force corresponding memory and bandwidth consumption on the node. Apply a strict response-size limit (and a route-appropriate rate limit) before buffering or parsing the body. -
[P3] Implement the promised anchor failure lifecycle instead of dropping failed uploads
crates/gitlawb-node/src/api/repos.rs:1324
Upload failures only log a warning; no anchor row is created, retried, confirmed, or marked failed. The new pending/confirmed/failed methods are unused, and success-only rows are always inserted aspending. This does not meet the linked issue's stated retry/visible-gap acceptance criterion, so transient bundler failures silently leave history unanchored. Persist pending work before upload and drive it through bounded retry and terminal status handling. -
[P2] Keep the documented anchoring configuration working during the rename
crates/gitlawb-node/src/config.rs:91
This removesGITLAWB_IRYS_URLwithout a fallback, while both.env.exampleandREADME.mdstill instruct operators to set it. Upgrading an existing documented deployment leavesbundler_urlempty and silently disables both anchoring paths. Support the legacy variable for a deprecation period or make the migration explicit and update all operator documentation in the same change. -
[P3] Expose the new signed fields through the certificate API
crates/gitlawb-node/src/api/certs.rs:45
The certificate signing payload now includesseq,prev, andpusher_sig, but both list and get responses omit all three fields. Consumers of the established certificate API (includinggl cert) therefore cannot reconstruct the signed payload or inspect chain continuity after this change. Serialize the new fields and update the client display/verification path accordingly.
|
@Gravirei please rebase to main and fix conflicts |
c94e8ed to
ae4f5fc
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
crates/gitlawb-node/src/test_support.rs (1)
4983-4988: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winMake
seed_certproduce chain-valid fixtures.This helper creates the 10- and 55-certificate datasets, but every certificate has
seq: 1and a zero predecessor. The tests therefore cannot catch regressions that ignore monotonic ordering orprevlinks. Pass sequence/predecessor values through the helper or add a dedicated chained fixture.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/test_support.rs` around lines 4983 - 4988, Update the seed_cert fixture helper so generated certificates form a valid chain: assign increasing sequence values and set each certificate’s prev field to the preceding certificate’s identifier or digest, with the first certificate using the chain’s root predecessor. Ensure both the 10- and 55-certificate datasets exercise monotonic ordering and linked predecessors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/arweave.rs`:
- Around line 292-293: Update the payload parsing in verify_anchor_endpoint so
serde_json::from_slice failure is handled as an invalid verification result
rather than propagated as an internal error. Return VerifyResult with valid set
to false and an appropriate error string, while preserving the existing JSON
parsing path.
- Around line 281-293: Update the response handling around resp.bytes() in the
verification flow to enforce the 1 MiB limit before unbounded buffering: reject
any Content-Length above 1_048_576, and stream or otherwise cap reads so
responses without a trustworthy length header cannot exceed the limit. Preserve
the existing invalid VerifyResult fields and JSON parsing for accepted payloads.
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 923-938: Migration version 13 must backfill distinct
per-repository sequence values before enforcing uniqueness. In the migration’s
`stmts` array, add an update that assigns deterministic, non-colliding `seq`
values to existing `ref_certificates` rows grouped by `repo_id`, then create
`idx_ref_certs_repo_seq`; preserve the existing column additions and append-only
index changes.
- Around line 5157-5162: Ensure each certificate created through make_cert
receives a unique seq value before insertion, either by incrementing it within
make_cert or overriding it at every test call site. Update the affected
certificate setup so list_ref_certificates_respects_limit and
insert_ref_certificate_append_only use distinct sequence numbers and avoid the
(repo_id, seq) uniqueness conflict.
---
Nitpick comments:
In `@crates/gitlawb-node/src/test_support.rs`:
- Around line 4983-4988: Update the seed_cert fixture helper so generated
certificates form a valid chain: assign increasing sequence values and set each
certificate’s prev field to the preceding certificate’s identifier or digest,
with the first certificate using the chain’s root predecessor. Ensure both the
10- and 55-certificate datasets exercise monotonic ordering and linked
predecessors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4e15d3f3-7966-4b86-a8fa-640f7c92e10a
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
crates/gitlawb-node/src/api/arweave.rscrates/gitlawb-node/src/api/certs.rscrates/gitlawb-node/src/api/events.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/arweave.rscrates/gitlawb-node/src/auth/mod.rscrates/gitlawb-node/src/cert.rscrates/gitlawb-node/src/config.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/main.rscrates/gitlawb-node/src/server.rscrates/gitlawb-node/src/test_support.rs
🚧 Files skipped from review as they are similar to previous changes (5)
- crates/gitlawb-node/src/api/events.rs
- crates/gitlawb-node/src/server.rs
- crates/gitlawb-node/src/api/arweave.rs
- crates/gitlawb-node/src/config.rs
- crates/gitlawb-node/src/cert.rs
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Backfill legacy certificate sequence numbers before adding the unique index
crates/gitlawb-node/src/db/mod.rs:910
Migration 12 gives every existing certificateseq = 1, but migration 13 then creates a unique(repo_id, seq)index. Version 10 only deduplicated(repo_id, ref_name), so any existing repository with certificates for two refs has duplicate(repo_id, 1)rows and cannot complete this migration or start. This is also immediately exposed by the changedseed_certfixture, which inserts tenseq = 1rows and makeslist_certs_respects_limit_paramfail. Assign deterministic per-repository sequence/chain values before creating the index and update the fixture. -
[P1] Enforce the verification response limit before buffering the gateway body
crates/gitlawb-node/src/arweave.rs:282
The new unauthenticated verification route callsresp.bytes()and only checks the 1 MiB limit after the whole Arweave object has been downloaded and allocated. A caller can select a huge or chunked gateway item and exhaust node memory/bandwidth before the rejection occurs. Stream into a capped reader (and reject a known excessive content length up front) instead of buffering first. -
[P1] Restore cryptographic verification to
gl cert show
crates/gl/src/cert.rs:151
This branch has drifted from its base and removes the base's--verify,--expect-node, Ed25519 verification routine, and tests, although the command is still documented as verifying a certificate. It now only prints a proposed payload and returns success for a modified or self-signed certificate. Please rebase without reverting that fail-closed CLI contract, then update its canonical payload forseq,prev, andpusher_sig. -
[P2] Hold the certificate-chain lock through allocation and insertion
crates/gitlawb-node/src/db/mod.rs:2160
pg_advisory_xact_lockis transaction-scoped, but this standalone pooled query commits beforeissue_ref_certificatereads the previous certificate or inserts the new one. Concurrent pushes can therefore select the same sequence; with three or more contenders the single retry can collide again, leaving an accepted push without its certificate/anchor evidence. Use one acquired connection/transaction for the lock, predecessor read, and insert (or a robust serialization/retry strategy). -
[P2] Bind the embedded certificate to the enclosing Arweave anchor
crates/gitlawb-node/src/arweave.rs:303
Verification checks the copied certificate signature but never compares the untrusted outerrepo,owner_did, ref, SHAs, or node DID to that certificate. An attacker can publish a payload with a valid public certificate while claiming a different ref update and receivevalid: true. Reject field mismatches (and match a locally recorded transaction too if this endpoint is meant to validate local anchors). -
[P2] Keep
gl statuscompatible with the remote created bygl init
crates/gl/src/status.rs:146
This branch regresses the base's multi-remote lookup: the status command now accepts only agitlawb://fetch URL onorigin, whilegl initadds the same URL under thegitlawbremote. Immediately after the supported init flow,gl statusreports that the repository is not a Gitlawb repo and skips the PR/issue queries. Rebase without reverting the base's lookup for thegitlawbremote and other Gitlawb fetch/push URLs. -
[P2] Do not hard-code
mainafter a plaingit init
crates/gl/src/init.rs:41
This branch reverts the base's branch/commit-state handling. Plaingit inithonors the user'sinit.defaultBranch, but the command unconditionally instructsgit push gitlawb main. Onmaster, feature, detached, or unborn HEADs that instruction either targets a nonexistent/wrong ref or fails before the first commit. Rebase without dropping the previous branch/commit-state handling, or initializemainwith the compatibility fallback. -
[P2] Preserve the legacy command-line spelling during the bundler rename
crates/gitlawb-node/src/config.rs:75
The environment fallback runs only after Clap parses arguments. Existing operators invokinggitlawb-node --irys-url …now receive an unknown-argument startup error even though the PR claims compatibility forGITLAWB_IRYS_URL. Add a deprecated long alias or normalize a retained legacy option as well as the environment variable. -
[P2] Do not remove unrelated CI safety gates from this anchoring change
.github/workflows/pr-checks.yml:189
This is stale-base drift rather than part of the Arweave feature: the branch deletes the shipped Windows CLI test lane and the onlygitlawb-coredependency-purity gate, together with its allowlist and checker script. That removes platform regression visibility and a supply-chain control for the shared cryptographic core; rebase without deleting these protections, or justify and replace them in a separately scoped change.
ae4f5fc to
e9a1a5e
Compare
|
@coderabbitai please make an intensive review of this changes |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
crates/gitlawb-node/src/api/events.rs (1)
429-435: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy liftUse valid per-repository certificate-chain fixtures.
crates/gitlawb-node/src/api/events.rs#L429-L435: replace the process-global sequence generator with per-chain fixture state.crates/gitlawb-node/src/api/events.rs#L447-L452: deriveprevfrom the preceding certificate instead of always using the genesis hash.crates/gitlawb-node/src/test_support.rs#L1481-L1486: avoid assigning a global sequence to an otherwise standalone certificate.crates/gitlawb-node/src/test_support.rs#L4967-L4973: make sequence generation scoped to a repository/chain.crates/gitlawb-node/src/test_support.rs#L4990-L4995: generate matching predecessor hashes for multi-certificate fixtures.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/api/events.rs` around lines 429 - 435, Replace the process-global NEXT_FCERT_SEQ/ref_cert_seq state in crates/gitlawb-node/src/api/events.rs:429-435 with sequence state scoped to each certificate chain; update the certificate construction at crates/gitlawb-node/src/api/events.rs:447-452 to derive prev from the preceding certificate. In crates/gitlawb-node/src/test_support.rs:1481-1486, leave standalone certificates without a global sequence; in lines 4967-4973, scope sequence generation to the repository or chain; and in lines 4990-4995, generate predecessor hashes that match the preceding certificates in multi-certificate fixtures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gitlawb-node/src/api/certs.rs`:
- Around line 55-57: The certificate JSON responses in
crates/gitlawb-node/src/api/certs.rs must include complete pusher-signature
metadata. Update both the listed-certificates response at lines 55-57 and the
single-certificate response at lines 98-100 to include signature_input,
content_digest, and request_path alongside the existing pusher_sig fields.
---
Nitpick comments:
In `@crates/gitlawb-node/src/api/events.rs`:
- Around line 429-435: Replace the process-global NEXT_FCERT_SEQ/ref_cert_seq
state in crates/gitlawb-node/src/api/events.rs:429-435 with sequence state
scoped to each certificate chain; update the certificate construction at
crates/gitlawb-node/src/api/events.rs:447-452 to derive prev from the preceding
certificate. In crates/gitlawb-node/src/test_support.rs:1481-1486, leave
standalone certificates without a global sequence; in lines 4967-4973, scope
sequence generation to the repository or chain; and in lines 4990-4995, generate
predecessor hashes that match the preceding certificates in multi-certificate
fixtures.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5645b021-f503-4733-92c1-424a3ae2349e
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
crates/gitlawb-node/src/api/arweave.rscrates/gitlawb-node/src/api/certs.rscrates/gitlawb-node/src/api/events.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/arweave.rscrates/gitlawb-node/src/auth/mod.rscrates/gitlawb-node/src/cert.rscrates/gitlawb-node/src/config.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/main.rscrates/gitlawb-node/src/server.rscrates/gitlawb-node/src/test_support.rs
|
@coderabbitai make an intensive review on this please |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
crates/gitlawb-node/src/arweave.rs (1)
303-304: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winNon-JSON gateway payload still returns 500 instead of
valid: false.serde_json::from_slice(&body_bytes)?propagates throughverify_anchor_endpoint(mapped toAppError::Internal), inconsistent with the other graceful branches and the "could be JSON or raw bytes" comment. Convert a parse failure intoVerifyResult { valid: false, .. }with an error string.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/arweave.rs` around lines 303 - 304, Update the anchor parsing in verify_anchor_endpoint so serde_json::from_slice failures are handled as an invalid verification result rather than propagated as AppError::Internal. Return VerifyResult with valid set to false and an error string for non-JSON payloads, while preserving the existing successful JSON path and other graceful branches.
🧹 Nitpick comments (2)
crates/gitlawb-node/src/server.rs (1)
222-228: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider throttling the unauthenticated verify route. Each
GET /api/v1/arweave/verify/{tx_id}triggers an outbound gateway fetch plus a DB lookup with no auth or per-IP brake, so it's an amplification/DoS surface (node → gateway) reachable by anonymous callers. Given the other cost-bearing routes here carry a per-IPIpRateLimiter, consider wrappingarweave_routessimilarly. (tx_idis a fixed-host path segment, so this is a load concern, not SSRF.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/server.rs` around lines 222 - 228, Wrap the arweave_routes router, including GET /api/v1/arweave/verify/{tx_id}, with the existing per-IP IpRateLimiter used by other cost-bearing routes. Preserve the current list_anchors and verify_anchor_endpoint handlers while ensuring anonymous requests are throttled before triggering gateway or database work.crates/gitlawb-node/src/db/mod.rs (1)
2834-2870: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
cert_idis never persisted on anchor rows.RecordAnchorInputV2has nocert_idfield andrecord_arweave_anchor's INSERT omits it, so thecert_idcolumn added in migration v12 stays NULL for every anchor even thoughlist_arweave_anchors/list_pending_anchorsproject it. The push path inapi/repos.rsalready has the issued certificate in scope (ref_certs_clone), so the anchor→certificate DB linkage this column was added for is currently unreachable. Consider threading the cert id through so audits can join anchors to their certs. (gateway_urlon the input is likewise accepted but ignored by this function.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gitlawb-node/src/db/mod.rs` around lines 2834 - 2870, The anchor record flow does not persist the issued certificate ID. Add a cert_id field to RecordAnchorInputV2, pass the corresponding ID from the push path using ref_certs_clone, and include it in record_arweave_anchor’s INSERT and bindings so cert_id is stored on each anchor row; also remove or persist gateway_url consistently instead of silently ignoring it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@crates/gitlawb-node/src/arweave.rs`:
- Around line 303-304: Update the anchor parsing in verify_anchor_endpoint so
serde_json::from_slice failures are handled as an invalid verification result
rather than propagated as AppError::Internal. Return VerifyResult with valid set
to false and an error string for non-JSON payloads, while preserving the
existing successful JSON path and other graceful branches.
---
Nitpick comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 2834-2870: The anchor record flow does not persist the issued
certificate ID. Add a cert_id field to RecordAnchorInputV2, pass the
corresponding ID from the push path using ref_certs_clone, and include it in
record_arweave_anchor’s INSERT and bindings so cert_id is stored on each anchor
row; also remove or persist gateway_url consistently instead of silently
ignoring it.
In `@crates/gitlawb-node/src/server.rs`:
- Around line 222-228: Wrap the arweave_routes router, including GET
/api/v1/arweave/verify/{tx_id}, with the existing per-IP IpRateLimiter used by
other cost-bearing routes. Preserve the current list_anchors and
verify_anchor_endpoint handlers while ensuring anonymous requests are throttled
before triggering gateway or database work.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d81eba6b-bb9a-4ee3-af1e-5e62923f6b5f
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
crates/gitlawb-node/src/api/arweave.rscrates/gitlawb-node/src/api/certs.rscrates/gitlawb-node/src/api/events.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/arweave.rscrates/gitlawb-node/src/auth/mod.rscrates/gitlawb-node/src/cert.rscrates/gitlawb-node/src/config.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/main.rscrates/gitlawb-node/src/server.rscrates/gitlawb-node/src/test_support.rs
beardthelion
left a comment
There was a problem hiding this comment.
Reviewed the current head against merged state. The certificate-chain design is genuinely strong and every serious finding from the earlier rounds is resolved on this head: per-ref cert binding, prev-hash linkage, fail-closed on a missing predecessor, atomic seq allocation (transaction-scoped advisory lock plus a (repo_id, seq) unique index and a 23505 retry), the gateway data-URL fix, the migration backfill-before-unique-index, and the full pusher-signature metadata on both cert endpoints. One functional bug blocks it: the verify endpoint never validates a real anchor. Findings highest first.
Findings
-
[P1] Compare the outer anchor repo against the certificate using the same identifier domain
crates/gitlawb-node/src/arweave.rs:334
The verify cross-check doesouter_repo != Some(&c.repo_id), but the outer anchor'srepois written as the slug{owner_key}/{name}(api/repos.rs:1222) while the embedded certificate'srepo_idis the repo UUID (issue_ref_certificate(&record.id, ...),api/repos.rs:1001). Slug never equals UUID, so every honestly produced anchor pushes a repo-mismatch error and returnsvalid: false. The endpoint cannot go green on real data: push to a public repo with a bundler configured, takearweave_tx_idfrom/api/v1/arweave/anchors, andGET /api/v1/arweave/verify/{tx_id}reports invalid despite a good node signature. This is the cross-check added in response to the earlier "verify does not compare the transition" finding, so the fix landed but across mismatched identifier domains; the ref/old/new/node comparisons beside it are correct. Either carry the UUID in the outer anchor, or resolve the slug to the repo id on the verify side before comparing. The verify tests set the embedded cert'srepo_idequal to the outer slug, which is why CI stays green while production never matches. -
[P2] Make the pusher authorization proof load-bearing, not silently skippable
crates/gitlawb-node/src/arweave.rs:465
The pusher-proof check is gated on all four ofpusher_sig,signature_input,content_digest,request_pathbeing present, but the node signing payload (cert.rs) covers onlypusher_sig— not the other three. A holder of a valid node signature can nullsignature_input/content_digest/request_path; the node signature still verifies (those fields are unsigned), the wholeif let (Some, Some, Some, Some)block is skipped, and verification returns valid with the independent RFC 9421 proof never checked. That defeats the stated goal of letting a third party verify the pusher authorization without trusting the node alone; it bites under node-key compromise. Bind the three context fields into the node payload and treat a presentpusher_sigwith missing context as invalid rather than passing. -
[P2] Bound the gateway body by bytes read, not Content-Length
crates/gitlawb-node/src/arweave.rs:293
The 1 MiB guard only short-circuits when the gateway sends an honestContent-Length; a chunked or header-omitting (or low-lying) response skips the pre-check, andresp.bytes().awaitthen buffers the whole body before the post-check runs. The verify route is unauthenticated (IP-rate-limited only) andtx_idis caller-chosen, so a permissionless caller can drive multi-hundred-MB allocations on the async worker, bounded only by the 10s client timeout. Stream with a running cap (resp.chunk()loop, abort past 1 MiB) rather than buffering first. -
[P2] Add an executed upgrade-path test for the v13 seq backfill
crates/gitlawb-node/src/db/mod.rs:923
The v13 backfill andidx_ref_certs_repo_seqbuild are never exercised throughrun_migrations()against pre-existing multi-cert data:v10_upgrade_dedup_via_migrationre-applies only v10 (it deletes just the v10 row fromschema_migrations), andmigration_v11_creates_owner_did_columnseeds no certificates. The backfill logic itself is sound (ROW_NUMBER() OVER (PARTITION BY repo_id ORDER BY issued_at, id)over a NOT NULL column with a total tiebreaker), but a data migration this consequential needs a test that seedsschema_migrationsat v12, inserts several same-repo/different-ref certs (allseq=1after v12), runs the migrations, and asserts distinctseqplus the unique index present. -
[P3] Cluster of smaller items
crates/gitlawb-node/src/arweave.rs:272
Config docs still point at the dead knob:.env.example, README, and thearweave.rsmodule comment referenceGITLAWB_IRYS_URLand never mentionGITLAWB_BUNDLER_URLorGITLAWB_ARWEAVE_GATEWAY(not a break —main.rsfalls back to the old env var with a deprecation warning — but the docs should match). A gateway-fetch failure or a malformed embeddednode_didreturns a 500 that echoes the internal error string (api/arweave.rs) rather than a cleanvalid:falsewith the right status.tx_idis unvalidated before being appended to the gateway URL (no host-swap SSRF given the fixed authority andredirect::none, but validate to the 43-char base64url shape as cheap defense).repo_lock_hashusesDefaultHasher, which the std docs do not guarantee stable across Rust versions, so two differently-built nodes on one Postgres could hash a repo to different lock keys (backstopped by the unique index and retry, so retry storms rather than corruption; prefer a stable hash). The outerold_sha/new_sha/node_didcross-checks are skipped when the field is absent (is_some()guards), unlike repo/ref; a forger who omits them still gets valid. Minor: the deadlock_repo_cert_issuancehelper locks on the pool connection (immediate release, a no-op) and should be removed or made a session lock, and the endpoint doc comment says "most recent local cert" while the code chains againstseq-1.
Core design is sound and the prior integrity findings are genuinely resolved; the P1 is the blocker and it is a last-mile identifier mismatch, not a redesign.
jatmn
left a comment
There was a problem hiding this comment.
Blocking findings
1. verify_anchor compares anchor repo slug against certificate repo_id
Severity: Blocking
Files: crates/gitlawb-node/src/arweave.rs:335, crates/gitlawb-node/src/api/repos.rs:1328, crates/gitlawb-node/src/cert.rs:97
The outer anchor payload stores the human-readable repo slug (owner_short/repo_name), while the embedded RefCertificate stores the database UUID (record.id). The verify cross-check does:
} else if outer_repo != Some(&c.repo_id) {For every real anchor these values differ, so the endpoint returns valid: false even when the node signature, prev hash, and pusher proof are correct. This makes the verify endpoint unusable on production data.
Fix: Either store repo_id in the anchor payload (or add a dedicated repo_id field), or resolve the slug to the repo UUID on the verify side before comparing. Prefer storing the UUID in the anchor and keeping the slug for display only.
2. gl cert show --verify uses the old 7-field signed payload
Severity: Blocking
Files: crates/gl/src/cert.rs:259-267, crates/gitlawb-node/src/cert.rs:25-36
The CLI reconstructs the node-signed payload with only the original seven fields:
{ "repo_id", "ref", "old", "new", "pusher", "node", "ts" }The node now signs ten fields:
{ "repo_id", "ref", "old", "new", "pusher", "node", "ts", "seq", "prev", "pusher_sig" }Because serde_json serializes maps alphabetically, the added keys change the signed bytes. gl cert show --verify will report INVALID for every certificate issued after this PR, even though the node and the Arweave verifier accept them.
The CLI tests payload_serialization_matches_frozen_canonical_form and verify_signature_round_trip_and_tamper still pin the old format and therefore pass while real-world verification fails.
Fix: Update verify_signature in gl/src/cert.rs to include seq, prev, and pusher_sig, and update the frozen canonical tests accordingly.
High-priority findings
3. New anchor lifecycle methods are dead code
Severity: High
Files: crates/gitlawb-node/src/db/mod.rs:2920-2975, crates/gitlawb-node/src/api/repos.rs:1342-1354
confirm_arweave_anchor, fail_arweave_anchor, and list_pending_anchors are marked #[allow(dead_code)] and never called. Anchors are inserted with status = 'pending', deadline_height = NULL, and receipt_sig = NULL, and no background worker ever updates them. The new status fields are therefore unreliable for monitoring.
Fix: Either add a background confirmation worker in this PR, or remove the unused columns/methods and defer the lifecycle feature to a follow-up.
4. record_arweave_anchor failures are silently dropped
Severity: High
File: crates/gitlawb-node/src/api/repos.rs:1342-1354
let _ = db_clone
.record_arweave_anchor(&crate::db::RecordAnchorInputV2 { ... })
.await;If the local DB insert fails, the anchor transaction exists on Arweave but is not recorded locally, with no log or metric.
Fix: Log a warning/error and optionally increment a metric when the insert fails.
5. contracts_info leaks URLs that may contain credentials
Severity: High
File: crates/gitlawb-node/src/server.rs:583-605
The unauthenticated /api/v1/contracts endpoint returns rpc_url, bundler_url, and arweave_gateway verbatim. If an operator configures a private RPC or paid bundler URL containing an API key, the key is exposed to anonymous callers.
Fix: Mask or omit URLs that may contain credentials, or require authentication for this endpoint.
6. Arweave verify route uses the per-DID creation limiter
Severity: High
Files: crates/gitlawb-node/src/server.rs:225-236, crates/gitlawb-node/src/main.rs:297-298
The verify route is throttled with state.rate_limiter, which is configured as a per-DID repo-creation limiter (10 requests per hour). Even though the middleware keys by IP, the threshold is far too restrictive for a public verification endpoint.
Fix: Add a dedicated Arweave IP rate limiter (e.g. GITLAWB_ARWEAVE_RATE_LIMIT) with a sensible default.
7. Historical certificates get broken prev values after migration
Severity: High
File: crates/gitlawb-node/src/db/mod.rs:923-950
Migration v13 renumbers seq but does not backfill prev. Existing rows except the first per repo keep the zero sentinel, so verify_anchor will reject all pre-upgrade anchors whose embedded certificate has seq > 1.
Fix: Backfill prev in v13 using the same canonical JSON + SHA-256 that cert::prev_hash uses, or explicitly document that historical anchors are intentionally unverifiable.
Medium-priority findings
8. Pusher proof is silently skipped when incomplete
Severity: Medium
File: crates/gitlawb-node/src/arweave.rs:465-551
verify_anchor only runs the RFC 9421 pusher verification when all four of pusher_sig, signature_input, content_digest, and request_path are present. If any is missing (e.g. pre-v13 certificates), the endpoint can still return valid: true without checking who authorized the push.
Fix: At minimum, emit an informational error/warning when a certificate is expected to carry a pusher proof but one is missing.
9. tx_id path parameter is not validated before gateway fetch
Severity: Medium
File: crates/gitlawb-node/src/arweave.rs:267
The user-supplied tx_id is appended directly to the gateway URL. Without length/format validation (Arweave IDs are 43-character base64url strings), the endpoint can be abused as a limited open proxy or SSRF vector against the configured gateway.
Fix: Validate tx_id against ^[A-Za-z0-9_-]{43}$ and return 400 Bad Request early.
10. Outer anchor checks are optional for old_sha, new_sha, node_did
Severity: Medium
File: crates/gitlawb-node/src/arweave.rs:351-371
The cross-check only errors if these fields are present and mismatch. If a malicious anchor omits them, verification still passes. They should be mandatory when a certificate is embedded.
Fix: Fail closed when old_sha, new_sha, or node_did are missing from the outer payload.
11. Advisory lock key uses unstable DefaultHasher
Severity: Medium
File: crates/gitlawb-node/src/db/mod.rs:2241-2246
repo_lock_hash derives the per-repo advisory lock key with std::collections::hash_map::DefaultHasher, which is not guaranteed stable across Rust versions or platforms. Different node builds could compute different lock keys and lose cross-instance serialization.
Fix: Use a stable hash such as the first 8 bytes of SHA-256(repo_id).
12. v1 schema was edited in-place
Severity: Medium
File: crates/gitlawb-node/src/db/mod.rs:462-473, :525-538, :648-663
The migration catalogue comment explicitly states that future changes must be added as new migrations and never appended to v1. However, v1 now already contains the columns that migrations v12/v13 add. While IF NOT EXISTS keeps upgrades safe, this breaks the migration narrative and can confuse future maintainers.
Fix: Either revert the v1 additions and rely solely on v12/v13, or document that v1 in this branch intentionally includes later columns.
Lower-priority findings
13. Test fixtures do not form valid certificate chains
Severity: Low-Medium
Files: crates/gitlawb-node/src/api/events.rs:429-454, crates/gitlawb-node/src/test_support.rs:4964-4997, crates/gitlawb-node/src/db/mod.rs:5215-5252
Test helpers use process-global sequence counters and hard-coded zero prev hashes. They do not exercise per-repo contiguous sequences or cryptographically correct prev linkage, so chain-related regressions could slip through.
Fix: Use per-repo sequence allocation and compute prev from the predecessor certificate, matching production issuance.
14. local_cert events omit the new certificate fields
Severity: Low-Medium
File: crates/gitlawb-node/src/api/events.rs:243-256
The event feed does not include seq, prev, pusher_sig, signature_input, content_digest, or request_path, so event consumers cannot validate chain continuity or reconstruct the pusher proof.
Fix: Include the new fields in the event payload.
beardthelion
left a comment
There was a problem hiding this comment.
Traced the signing/verification payload construction on both the node and gl sides line-by-line, and cross-checked the migration runner and the pre-existing rate-limiter/auth-middleware code this PR reuses. Most of the design (append-only per-repo hash chain, advisory-lock-serialized seq allocation, streamed-and-capped gateway fetch, fail-closed cross-checks) is solid. One finding should block merge.
Findings
-
[P1] gl cert show always reports a legitimate push-issued certificate as invalid
crates/gl/src/cert.rs:265
The node signs the certificate over 13 fields (crates/gitlawb-node/src/cert.rscert_payload), includingsignature_input,content_digest, andrequest_path.gl's client-sideverify_signaturerebuilds the payload to check the signature against but stops atpusher_sig, never reading or including those three fields.git_receive_packalways passesSome(..)for all three (the route sits behindrequire_signature, which unconditionally sets them), so every certificate issued for a real push carries them, and the server API (api/certs.rs) already returns them in the JSON —cmd_showjust never parses or forwards them. The byte mismatch means the Ed25519 checkgl cert showruns fails for every push-issued certificate, reporting a validly node-signed cert as tampered. The PR's owngl/src/cert.rstests don't catch this because they only exercise the old 10-field shape (pusher_sig: null, no context fields).
Fix: addsignature_input,content_digest,request_pathtogl'sverify_signaturepayload and its call sites, matching the server'scert_payloadexactly, then add a test that signs and verifies a cert with all three fields populated. -
[P3] .env.example and README still document the renamed config knob
.env.example,README.md
Both still referenceGITLAWB_IRYS_URLonly; neither mentions the newGITLAWB_BUNDLER_URLorGITLAWB_ARWEAVE_GATEWAY(config.rs).main.rsdoes fall back to the legacy env var with a deprecation warning, so this isn't a functional break, just stale operator-facing docs for a knob this PR renamed. -
[P3] verify_anchor 500s on a malformed node_did instead of returning valid:false
crates/gitlawb-node/src/arweave.rs
Every other malformed-input case inverify_anchor(gateway non-2xx, oversized/undecodable body, non-JSON payload) returnsOk(VerifyResult{valid:false, ..}). The node-DID parse (gitlawb_core::did::Did::from_str(&c.node_did).map_err(..)?) still uses?, so a certificate whose embeddednode_didfails to parse propagates asErr, which the handler turns into a 500 instead of the same controlled{valid:false}response every other bad-input path returns.
… docs, and fix node_did 500 on verify_anchor
… binding; P2/P3 items
…ment, env.example defaults, anchor compat fields
…nverifiable anchors, separate rate limiter
…ss-check, v20 index drop, gateway pairing, positive accept test, P3 fixes
…tems Bundlers (Irys, Turbo) require the upload to be a signed Arweave data item; the previous unsigned JSON POST with an x-bundler-tags header was neither a supported upload protocol nor authenticated. Build and sign ANS-104 items with the node keypair (the signature IS the upload credential) and embed the indexing metadata as item tags inside the signed item. - ans104: deepHash matching @irys/arbundles (length-tagged SHA-384 chain), Avro-style tag serialization, build_signed_data_item/verify_data_item, pinned to an independent reference vector plus tamper/forge rejection tests - anchor_ref_update/anchor_encrypted_manifest now POST signed data items; x-bundler-tags header helpers removed - repos.rs passes the node keypair at both anchor call sites - bundler POST tests now run against a real in-process server that parses the item, verifies the signature, and checks tags/payload (denies on any failure); adds an end-to-end wrong-key rejection test
…immutable Review follow-ups on the signed ANS-104 anchoring work: - Funded upload model: the node's ANS-104 signature is authorship, not payment. Add GITLAWB_BUNDLER_ACCOUNT, send it as x-bundler-address on every upload, refuse to start when a bundler URL is set without a funded account, and correct the docs that claimed signature-as-authentication. - Redact gateway/bundler URLs everywhere they surface publicly: drop userinfo, query, and fragment in mask_credential_url, the anchors listing, the gateway-inference log, and the verify error body (which reqwest seeded with the raw URL). - Fail closed in verify_anchor: when outer repo/owner identity is present but the repo row cannot be corroborated, the result is invalid instead of silently skipping the check. - Keep the released v1 migration byte-identical to origin/main; the cert chain and anchor column work lives in v18+, with an upgrade test that replays the deployed v1 schema and proves certs and anchors round-trip.
…tion, detached post-receive continuation
- ans104: tags preimage is the spec nested [[name,value]] list, not Avro tag
bytes; deep_hash is recursive; regenerate 0-tags vector and add an interop
fixture signed by arbundles' deepHash + Node crypto.
- tamper: b64url-decode the sig, flip a byte, re-encode, assert the specific
signature error for both the 13-field and 7-field verify paths.
- payer: require GITLAWB_BUNDLER_TOKEN alongside the account; send
x-irys-paid-by and upload to /tx/{token}; make .env.example startable and
gate it with a config test reading the shipped file.
- urls: structural join_path helper preserving query and rejecting fragments on
both bundler and gateway URLs; redact creds and truncate error bodies; mask
the raw DB error at the fail-closed repo lookup.
- continuation: move record_push, trust score, and per-ref certificate
issuance into an owned post_receive_continuation spawned at the durability
boundary, so a disconnect after the pack lands can no longer drop certs or
the replication tail; add post_receive_continuation_survives_handler_abort
and update the U5 ordering gate.
- migrations: note v20's one-way drop and fix the stale v1 comment.
…tags, funded-account bundler docs Post-receive work becomes durable: git_receive_pack persists a post_receive_jobs row (id, pusher DID, repo, ref updates, RFC 9421 attestation) BEFORE acking the push, so a crash between the pack landing and the bookkeeping (record_push, trust score, certs, replication tail) is recoverable instead of dropping a durable push with no record. Startup drains rows a previous process left processing/failed and replays them; every effect is idempotent so a replay is safe: - push_events is keyed on the job id (ON CONFLICT (id) DO NOTHING) so a replay never double-counts the push - certificate ids are deterministic per (job, ref) and insert_ref_certificate_tx is idempotent, so a replay cannot mint a second certificate - the Arweave anchor upload is gated on an existence check for the exact transition, so a replay cannot write a second permanent artifact Also lands the reviewed ANS-104 flat-tags preimage (arbundles getSignatureData semantics, empty-tag reference vector), legacy GITLAWB_IRYS_URL adoption gated on the funded account/token pair, the centralized redaction boundary for bundler credentials in errors, the immutable-v1 migration note, and the README GITLAWB_BUNDLER_ACCOUNT/GITLAWB_BUNDLER_TOKEN rows. Refs Gitlawb#224
…explicit gateway Round-4 reviewer findings on Gitlawb#224: - Move Arweave anchoring out of the spawned replication tail and into the awaited post-receive job body: the tail now reports (announce, cid_map) over a oneshot channel, and anchor_ref_updates runs after the replication tail returns. A failed upload, an unpersistable row, or an unanswerable existence check now fails the job instead of leaking into the tail, so the startup drain retries the whole unit. The per-ref existence check keeps retries and job replays from paying for a second on-chain artifact. - Drop the implicit gateway default (arweave.net) and refuse to start with a bundler configured but no explicit GITLAWB_ARWEAVE_GATEWAY: the old behavior silently paired the gateway to the bundler URL, which broke /verify for production deployments (devnet transactions are not resolvable via arweave.net). Enforced in Config::validate() next to the existing ACCOUNT/TOKEN checks. - .env.example: split the commented devnet/production bundler blocks and document that anchoring needs URL + funded ACCOUNT + TOKEN + a gateway on the same network. - README: GITLAWB_BUNDLER_ACCOUNT is the funded payer (x-irys-paid-by), GITLAWB_BUNDLER_TOKEN is the payment-token slug billed at /tx/{token} (not an API key), GITLAWB_ARWEAVE_GATEWAY has no default. - anchors list: limit 0 now falls back to the default page size instead of returning nothing. - Tests: P4 unit (upload OK but row insert blocked -> job body errors, retry re-uploads exactly once, replay never re-calls the bundler), P5 unit (unanswerable existence check -> fail closed, no upload), and an end-to-end job test (bundler 500 -> job stays failed, drain retries, replay after the row exists never re-uploads).
…shold, accurate security docs Round-4 review consolidation on Gitlawb#224. The anchor row is now a durable per-transition outbox/state machine instead of a retry wrapper around the bundler HTTP call: - claim: an atomic INSERT ... ON CONFLICT DO NOTHING against a new unique (repo, ref_name, old_sha, new_sha) index creates the durable claim BEFORE any paid upload. Competing workers converge on a single payer. - prepare: the signed item's deterministic ANS-104 id is persisted on the row before the request is sent. - upload: outcomes are classified (Accepted / Rejected / Uncertain). A connection drop or malformed success leaves the row uploading, never recorded. - record: the accepted tx id is persisted as the terminal state. - recovery of a non-terminal claim probes the gateway for the persisted item id before re-uploading: present -> record as-is (no double pay), absent -> re-upload, no verdict -> fail closed without uploading. Also on the review: - satisfies_threshold counts distinct signer DIDs, not signature entries (regression test: a duplicated signature of one key fails a 2-of-3). - the anchor's issuer is state.node_did, not the pusher; the e2e test asserts the stored row's node_did matches the node and not the pusher. - certificate issuance failure fails the job (retryable) instead of a warning followed by done. - post-receive job processing claims the job atomically; two concurrent drainers converge on one executor (new test). - the upload client validates the bundler's success id at the boundary: empty/missing/malformed ids are Uncertain, only a 43-char base64url id is Accepted (new test). - list_anchors omits arweave_url when no gateway is configured (new test) and lists only recorded (terminal) rows. - SECURITY.md corrected to the runtime: UCANs are signed JSON envelopes, not JWTs; the middleware verifies the delegation chain; read enforcement is wired; owner-push enforcement defaults off. - migration v20 commentary attributes the dropped index to v10; migration v22 carries the outbox schema (state/item_id/claim_token/claimed_at, arweave_tx_id nullable, dedup, unique transition index). Full suite: 1417 passed / 0 failed across the workspace (877 in gitlawb-node). clippy --workspace --all-targets -D warnings clean.
Derive ANS-104 data-item ID from the 64-byte Ed25519 signature region (base64url(sha256(sig))) instead of hashing the entire serialized item, so gateway reconciliation probes the correct identity. Acquire an exclusive lease (compare-and-swap on claim_token) before recovering a nonterminal anchor row. A quiescence window protects in-flight uploads from double-payment. Every prepare, upload-state, failure, and terminal-record update is conditional on the lease token. Filter anchor listings by the repository's current visibility in SQL (CTE join to a public, non-quarantined repo_group) so a public-to- private transition immediately removes the node's indexed metadata from the global listing. Bind the bundler's success-response ID to the locally derived item ID; a mismatch is classified as Uncertain so recovery reconciles without fabricating a terminal anchor. Gateway probes now verify the response body's derived ID matches the probed item_id. Version the signed certificate payload (version 2 = 14 fields). Verification tries 14-field, falls back to 13-field (post-PR) then 7-field (pre-PR). Add interoperability tests for both formats. Migration v23 adds lease_since column to arweave_anchors. Closes round-2 review findings P1-1..P2-4.
…limiter, renumber migrations anchor_item_present now handles both JSON anchor payloads (real gateway) and raw ANS-104 bytes: a 200 with parseable JSON confirms the item exists; raw bytes still derive the ANS-104 id for verification. The f2a test gateway now serves JSON in present mode matching real gateway behavior. Add arweave_rate_limiter to sweep_rate_limiters and the all-limiters test so expired IP keys are cleaned up on the periodic timer. Renumber this branch's migrations v20-v23 to v27-v30, above Gitlawb#173's v19-v26 range.
…d filter rejection, manifest ID binding, migration renumbering, schema-aware gateway probe
…m id, isolate durable anchor from Pinata, pin anchor-claim layer with concurrent test
beardthelion
left a comment
There was a problem hiding this comment.
Re-reviewed at 0b7a4f08. Merge-base matches origin/main, CI is 12/12 green on this head, and the rebase ask from the last round is satisfied. I read all four comment surfaces: seven inline CodeRabbit threads, all resolved and outdated; jatmn's open round is still CHANGES_REQUESTED on 0f8c7f26, with no new human review on this push yet. The certificate-chain core from earlier rounds still holds (per-ref certs, issuer pinning, fail-closed corroboration, streaming 1 MiB cap on verify). I ran anchor_record_failure_is_reconciled_without_double_pay green on this head.
The rebased integration did not close the gateway-binding gaps or the Pinata coupling on the durable path. Five ranked open PRs overlap repos.rs, auth/mod.rs, and db/mod.rs on this branch; expect more conflict churn if those land first, but the findings below are about correctness on this head, not merge order.
Findings
-
[P2] Bind
verify_anchorgateway bodies to the requestedtx_id
crates/gitlawb-node/src/arweave.rs:711verify_anchorfetchesGET {gateway}/{tx_id}but only JSON-parses the body and runs cert/identity checks. It never comparestx_idtoans104::data_item_id(body_bytes)or any payload id field before returningvalid: true. A gateway or proxy that returns the same valid anchor JSON for every id can certify the wrong transaction.anchor_item_present's raw-bytes path already binds ids; apply the same binding here (schema gate plus id match), and add a RED test where the mock returns identical JSON forwrong-txandright-tx. -
[P2] Bind the JSON recovery probe to the probed
item_id
crates/gitlawb-node/src/arweave.rs:524On a JSON 200,
anchor_item_presentreturnsOk(true)whenschema == gitlawb/ref-update/v1without checking the response matches the requesteditem_id. The raw-bytes path derivesdata_item_idand compares (:533-545). A generic gitlawb-shaped JSON 200 can make crash recovery treat an uncertain upload as landed and skip a needed re-upload. Require id binding on the JSON path too, or fall through to the bytes check and fail closed on mismatch. -
[P2] Keep best-effort Pinata work out of the durable anchor completion path
crates/gitlawb-node/src/api/repos.rs:2674run_post_receive_jobawaitsanchor_cid_rxbeforeanchor_ref_updates, but the Ok sender lives inside the detached Pinata task and runs only afterpin_sem_pinata.acquire_owned().awaitandpinata::pin_new_objectscomplete (:3371-3427). Comments mark Pinata as best-effort, yet the durable job blocks on it. When the global pin semaphore is saturated, the job staysprocessingand anchor rows never reachrecordeduntil restart retries the same dependency. CID is alreadyOptioninanchor_ref_updates; report the announce decision (and any locally available CID) before dispatching Pinata, or treat CID enrichment as a separate best-effort update. Add an isolation test that holds the pin semaphore and proves the job reaches terminal anchor state without waiting for Pinata. -
[P2] Add a load-bearing test for the per-transition anchor claim
crates/gitlawb-node/src/db/mod.rs:4473two_concurrent_workers_claim_the_job_onceracesclaim_post_receive_jobonly. TheON CONFLICT (repo, ref_name, old_sha, new_sha) DO NOTHINGguard at:4473is real code but not pinned by a test that races two workers at the anchor-claim layer. Add a test where two tasks reachclaim_anchor_claimconcurrently and assert exactly one upload path wins.
Not an ask, recorded only: verify_anchor still accepts JSON gateway bodies only; raw ANS-104 bytes at GET /{tx_id} fail with "not valid JSON" even when authentic. Worth aligning with anchor_item_present's dual representation handling, but secondary to the id-binding gaps above.
One process note, not a finding: this branch overlaps #134 on server.rs / arweave.rs and several ranked security PRs on auth/repo paths. Rebase conflict resolution on those surfaces will need another pass when those PRs land.
Remove stale upstream code blocks concatenated by rebase conflict resolution (inline record_push/issue_ref_certificate, duplicate guard.release/result handling, dead standalone sweep_rate_limiters). Fix EncryptTaskCtx missing fields, repo_store local_path visibility, git_receive_pack test arg order, and f2a_tail missing ref_certs arg.
0b7a4f0 to
aa9fc8b
Compare
beardthelion
left a comment
There was a problem hiding this comment.
Re-reviewed at aa9fc8b2. CI on that head is 10/12 green: fmt + clippy, MSRV, audit, Docker, and release jobs pass; test (stable) and test (beta) fail before any test logic runs because migration v26 aborts (gh run 33011515250). jatmn's last human review is on 6bf13d1e (2026-08-15); the commits since then address most of that round (explicit gateway validation, synchronous anchor outbox in the job body, README bundler-token contract, verify_anchor id binding). I am not re-litigating those as open on this head.
Findings
-
[P1] Strip the cert-chain DDL out of migration v26
crates/gitlawb-node/src/db/mod.rs:1168Migration v26 (
pin_repair_sweep_discovery_cursor) currently runs anUPDATE ref_certificates SET seq = ..., createsidx_ref_certs_repo_seq, and addssignature_input/content_digest/request_path. Those statements belong in v32 (append_only_certs_and_pusher_proof), after v29 addsseq. On a fresh database v26 runs beforeseqexists, so every#[sqlx::test]panics attest_support.rs:43withcolumn "seq" of relation "ref_certificates" does not exist. I confirmed againstorigin/main, where v26 only adds the twopin_repair_sweepdiscovery columns. The duplicate block atdb/mod.rs:1200-1231is rebase debris fromaa9fc8b2; remove it from v26 and keep the cert-chain work solely in v32. -
[P2] Add a migration-order regression test that would have caught v26
crates/gitlawb-node/src/db/mod.rs:8099v26_discovery_continuation_applies_on_upgradeonly checks the pin-repair discovery columns. It does not exercise a full fresh-chain apply through v32, which is the path CI runs. After fixing v26, add a test that applies migrations v1 through v32 on an empty database (or extend the existing replay upgrade test atdb/mod.rs:10004) so cert-chain DDL cannot land before theseqcolumn again.
jatmn's Aug 15 gateway-inference, detached-tail durability, README bundler-token, and upload-without-DB-record findings look addressed on this head: gateway inference is gone from main.rs, Config::validate() requires an explicit gateway when a bundler is set, run_post_receive_job awaits anchor_ref_updates with the outbox pattern, and README matches config.rs on the token slug. I am not restating those as open asks.
Not an ask, recorded only: gl cert show still prints VALID on internally consistent certs without --verify (issuer not anchored to the queried node). verify_anchor skips prev-chain checks for legacy zero-prev rows and warns when outer repo/owner_did are omitted and the repo is absent from the local DB. Both match documented tradeoffs; operators should use gl cert show --verify for third-party checks.
One process note, not a finding: expect a rebase conflict with #134 on arweave.rs if both land close together. This PR also overlaps repos.rs, db/mod.rs, and auth/mod.rs with several in-flight security PRs (#285, #306, #325, #314, #382); merge order may require a follow-up pass on merged state.
- Restore v19 (pinned_cids_repo_provenance) dropped by rebase concat - Remove stale cert-chain stmts from v26 (belongs in v32 only) - Add ESCAPE '!' to list_ref_certificates_by_prefix LIKE query - Add arweave_rate_limiter to AppState sweep - Move post-receive job spawn above guard.release for disconnect safety - Add PusherSignature/PusherProof to signed_request_as test helper
jatmn
left a comment
There was a problem hiding this comment.
I found issues that need to be addressed before this is ready.
Findings
-
[P1] Make the post-receive outbox durable before the ref update can become externally successful
crates/gitlawb-node/src/api/repos.rs:2258
crates/gitlawb-node/src/api/repos.rs:2321
smart_http::receive_pack completes before this handler constructs and persists post_receive_jobs. At that point Git has already advanced the ref on disk, while the only recovery mechanism added by this PR still exists solely in the request future. A process crash, shutdown, or database failure between those two operations leaves an accepted transition with no job for drain_post_receive_jobs to enumerate. That loses the promised push accounting, trust update, per-ref certificate, and durable anchor for the original transition; sending the client an error cannot undo the landed ref, and retrying can be a no-op or represent a different transition.Please address the root atomicity/recovery boundary rather than adding another retry around enqueue_post_receive_job: ensure every ref transition that Git can expose has a durable, deterministic recovery record, or add startup reconciliation that derives and queues exactly the missing work for already-landed transitions. The solution must preserve the existing exactly-once properties across restart and concurrency: one accounting event, deterministic per-job/per-ref certificates, and no duplicate paid anchor. Add fault-injection coverage for a successful receive-pack followed by failed job persistence, then restart/drain and assert the original transition is recovered exactly once.
-
[P1] Authenticate the ANS-104 envelope before treating the embedded certificate as an anchor
crates/gitlawb-node/src/arweave.rs:694
crates/gitlawb-node/src/ans104.rs:392
verify_anchor binds the response bytes to the requested item ID and calls data_item_data, but that helper is intentionally structural-only: it does not validate the ANS-104 Ed25519 signature, its deep-hash, or the envelope owner. The subsequent certificate verification proves that a copied certificate was signed by this node; it does not prove the permanent data item containing that certificate was authored by this node. An attacker can create a syntactically valid item under their own key, embed a copied valid certificate and matching outer fields, and have the verify endpoint report it as valid.Please make envelope authenticity an explicit prerequisite of payload parsing/adjudication. Recompute the ANS-104 signature preimage from the served item, verify it with the configured node key rather than a key supplied by the artifact, and reject owner, signature, or deep-hash mismatches before accepting any JSON or certificate. Keep the certificate-version fallbacks for historical certificates separate from envelope authentication: old certificate payloads may remain supported, but every newly claimed signed anchor still needs a valid node-authored ANS-104 wrapper. Add a positive trusted-envelope test plus rejection tests for a tampered signature, a foreign owner, and a forged envelope containing an otherwise valid copied certificate.
Review guidance
This PR has accumulated follow-up findings because it joins several security-sensitive state machines: Git ref visibility, durable database recovery, certificate-chain provenance, paid immutable uploads, and public verification. Each layer is individually careful, but correctness depends on the boundaries between them. In both findings, the code validates or persists a later layer after an earlier irreversible effect has already occurred: Git makes a ref durable before the recovery record exists, and the verifier trusts an inner certificate before authenticating the enclosing immutable artifact.
For the remaining revision, please drive the implementation from the end-to-end invariants rather than from individual error branches:
- For every accepted ref transition, define the durable identifier and recovery record before that transition can be acknowledged or otherwise become visible; test crash/restart at each boundary through accounting, certificate issuance, and anchoring.
- For every verifier verdict, establish the trust anchor outside the fetched artifact first, authenticate each enclosing format from outside-in, and only then interpret nested claims.
- Exercise fault-injection and adversarial tests that remove or invert each boundary. A happy-path upload, a valid inner certificate, or a passing retry test is not sufficient proof that a crash or substituted remote artifact cannot bypass the contract.
Keeping those invariants explicit should reduce further review churn while preserving the intended scope: durable per-transition bookkeeping and trustworthy signed Arweave anchors, without broadening replication behavior or redesigning the certificate protocol.
beardthelion
left a comment
There was a problem hiding this comment.
I re-read head 04271ad against merged state. CI is green on this push. The R4/R5 work closes the signed-tuple legacy corroboration hole, binds verify to the served ANS-104 item id, requires Content-Digest before certificate proof, masks gateway credentials on the anchors listing, and adds real accept-path verify tests. Two verify/recovery gaps are still open.
Findings
-
[P2] Verify the ANS-104 data-item signature before reporting valid
crates/gitlawb-node/src/arweave.rs:705
Upload signs ref anchors with the node keypair (build_ref_anchor_item, line 139) and the mock bundler enforcesans104::verify_data_item.verify_anchoronly checks thatdata_item_id(body) == tx_id, unwraps the payload, and validates the embedded certificate. It never checks that the item's Ed25519 signature or owner matchesnode_did. The accept tests wrap payloads withserve_signed_anchor(..., &node_kp, ...), so they pass without exercising a foreign signer. Anyone can republish a legitimately node-signed certificate inside their own ANS-104 item and getvalid: trueon their tx id without this node having uploaded it. -
[P2] Cap the gateway presence probe body the same way verify caps its fetch
crates/gitlawb-node/src/arweave.rs:507
verify_anchorstreams the gateway response with a 1 MiB running cap (lines 661-690).anchor_item_presentcallsresp.bytes().awaitwith no size limit before it parses the item id. Post-receive recovery uses that probe to decide whether a crashed upload landed before re-paying. A hostile or misconfigured gateway can force an unbounded allocation on the worker path while verify itself is bounded. -
[P3] Return 400 for a malformed
?repo=filter on the anchors listing
crates/gitlawb-node/src/db/mod.rs:4748
R3 added rejection for malformed repo filters, but the error is a bareanyhow!that propagates through the handler as 500 Internal. Map that shape toBadRequestat the handler boundary so clients get a 400, not an internal error.
Not an ask, recorded only: legacy migration rows with the all-zero default prev still skip chain linkage with a warn only; signed-tuple corroboration blocks seq tamper when a matching stored row exists. Encrypted-manifest anchoring on the replication tail still lacks the UploadOutcome / probe reconciliation the durable ref path has; the PR scope treats that tail as best-effort.
One process note, not a finding: this branch overlaps open PRs on auth/mod.rs (#306) and repos.rs (#285, #325). Base is current main on this head, but landing those first may still need a conflict pass on the shared hunks.
Summary
Anchor ref updates and encrypted-blob manifests to Arweave as signed ANS-104 data items, and add
/api/v1/arweave/verify/{tx_id}which fetches the anchor from the gateway and validates the embedded certificate chain (node Ed25519 signature, SHA-256prevlinkage, pusher RFC 9421 proof). Also harden the Arweave surface: spec-correct ANS-104 preimages, tamper rejection, fail-closed corroboration, credential redaction on every public output, structural URL handling that rejects fragments, an explicit verified bundler-funding configuration, and durable post-receive jobs persisted before the push is acknowledged.Durability scope is deliberate and narrow: push accounting, per-ref certificates, and the Arweave anchor are the durable unit — a crash between the pack landing and that bookkeeping is recovered by the startup drain. The replication tail (Pinata/IPFS pinning, gossip, GraphQL push, peer notify) remains best-effort and is not part of the durable unit.
Closes #26.
Kind of change
What changed
gitlawb-node
item.rawTagsin the publishedarbundlesgetSignatureData), not a nested[[name, value], ...]list — the nested form is what Arweave layer-one transactions use, and a data item's signature would not reproduce with it. Zero tags is an empty blob, notdeepHash([]). The empty-tags reference vector and a 3-tag interop fixture are produced by the independentarbundlespackage (createData+sign) and pinned as hex in tests; the node's own signer produces items this verifier accepts.{bundler}/tx/{token}as raw signed data items with metadata embedded as tags, paying via thex-irys-paid-byheader (IrysUploadHeaders.PAID_BY). The upload client classifies every outcome at the boundary (Acceptedonly for a well-formed 43-char base64url id; empty/missing/malformed ids and connection drops areUncertain— never success, never a fabricated anchor), and exposes the content-derived ANS-104 data-item id plus a gateway presence probe (anchor_item_present) that the durable job uses to reconcile a possibly-landed upload without paying twice.verify_anchor()validates the full chain against the local DB; refusals are fail-closed — an uncorroborated outer repo/owner identity makes the result invalid, and the raw DB error never reaches the caller. Every send-error and non-success-body path on the upload side is masked throughremote_send_error/remote_response_errorso bundler account/token never leak; the same masking covers verify_anchor's connection and mid-stream errors. Tamper tests flip a signature byte on both the 13-field and 7-field verify paths and assert the specific signature error.record_push, trust score, per-refissue_ref_certificate) and the Arweave anchor now run inside a durable post-receive job.git_receive_packpersists the job row (id, pusher DID, repo, ref updates, RFC 9421 attestation) BEFORE acknowledging the push, then spawnsprocess_post_receive_job; a crash between the pack landing and the bookkeeping is recovered by the startup drain (drain_post_receive_jobsin main), which resets stale rows topendingand replays them. Jobs are claimed atomically (UPDATE ... WHERE status IN ('pending','failed')): two concurrent drainers converge on a single executor per job. Every effect is idempotent:push_eventsis keyed on the job id (ON CONFLICT (id) DO NOTHING), certificate ids are deterministic per (job, ref) withinsert_ref_certificate_txidempotent, and a certificate issuance failure now fails the job (retryable) instead of warning and completing. The replication tail (Pinata/IPFS pinning, gossip, GraphQL push, peer notify) is spawned from the same job but is not awaited: the tail reports(announce, cid_map)back over a oneshot channel, andanchor_ref_updatesruns in the job body only after that report arrives.INSERT ... ON CONFLICT DO NOTHINGagainst a new unique(repo, ref_name, old_sha, new_sha)index creates the durable claim inpendingBEFORE any paid upload; exactly one worker wins, so exactly one worker can ever pay. (2) prepare — the signed item's deterministic ANS-104 id is persisted on the row before the request is sent. (3) upload — outcomes are classified: a definitive provider rejection marks the rowfailed; a connection drop or malformed success leaves ituploading(the item may have been accepted). (4) record — the accepted tx id is persisted as the terminalrecordedstate. A retry that finds a non-terminal claim probes the gateway for the persisted item id first: present → record as-is (no second paid artifact), absent → re-upload, no verdict → fail the job without uploading. The anchor's issuer isstate.node_did, never the pusher.satisfies_thresholdcounts distinct signer DIDs, not signature entries; a repeated signature from one maintainer counts once, so copy-pasted signatures cannot fabricate a threshold.GITLAWB_BUNDLER_TOKENadded;validate()refuses to start with a bundler URL without both a funded account and a payment token (Irys bills at/tx/{token}). An anchoring node must now also set an explicitGITLAWB_ARWEAVE_GATEWAY: the implicitarweave.netdefault is gone, because it silently paired the gateway to the bundler URL and broke/verifyfor production deployments (devnet transactions are not resolvable viaarweave.net). The legacyGITLAWB_IRYS_URLis adopted vialegacy_bundler_url_fallbackonly when the funded account/token pair is also set; a bare legacy URL no longer silently enables anchoring (the node warns and starts with anchoring disabled).devnet.irys.xyz+matic+ devnet gateway) and Production (node2.irys.xyz+ethereum+https://arweave.net) shapes, and the comment documents that anchoring needs the URL, a funded ACCOUNT, the TOKEN, and a gateway on the same network.GITLAWB_BUNDLER_ACCOUNTis the funded account that pays (sent asx-irys-paid-by),GITLAWB_BUNDLER_TOKENis the payment-token slug billed at/tx/{token}(it is NOT an API key and is not sent in the paid-by header), andGITLAWB_ARWEAVE_GATEWAYhas no default.RefCertificategainsseq/prev/pusher_sig/signature_input/content_digest/request_path;arweave_anchorsgainscert_id, renamesirys_tx_id→arweave_tx_id. The released v1 migration stays byte-identical (it has NO cert-chain columns); column work lives in append-only v18/v19, v20 drops the superseded(repo_id, ref_name)unique index (documented one-way, attributed to the v10 migration that created it), v21 adds thepost_receive_jobstable, and v22 carries the anchor outbox:state/item_id/claim_token/claimed_atcolumns,arweave_tx_idmade nullable (a claimed row has no tx yet), dedup of pre-v22 duplicate rows, and the unique per-transition index. The anchors listing reads onlyrecorded(terminal) rows. An upgrade test replays the deployed v1 schema.{payload, s}, base64url Ed25519 over the payload), not JWTs; the middleware verifies the full delegation chain whenX-Ucanis present; read enforcement and per-path visibility rules are wired;GITLAWB_ENFORCE_OWNER_PUSHdefaults off.mask_credential_urldrops userinfo, query, and fragment; used by contracts info, the anchors listing, the gateway-inference log, and the verify error body. Bundler/gateway URLs are built with a structuraljoin_url_paththat preserves the query and rejects fragments; reqwest errors have the URL redacted and bodies truncated.Reviewer checklist coverage
verify_data_item_matches_independent_interop_fixture; flat-tags referencedeep_hash_matches_independent_reference_vector.test_verify_anchor_rejects_tampered_13_field_signature,test_verify_anchor_rejects_tampered_7_field_signature.test_verify_anchor_fails_closed_when_outer_identity_cannot_be_corroborated; DB error masked at the fail-closed lookup.list_anchors_drops_query_and_fragment_credentials,test_verify_anchor_error_does_not_leak_gateway_query_credentials,test_anchor_ref_update_redacts_credentials_in_error_body,test_manifest_anchor_redacts_credentials_in_error_body,test_verify_anchor_interrupted_stream_error_is_masked,redaction_helpers_scrub_urls_and_secrets,server::tests::drops_query_and_fragment_credentials.test_anchor_preserves_bundler_path_prefix,test_anchor_preserves_bundler_query,test_anchor_rejects_fragment_in_bundler_url,test_verify_anchor_preserves_gateway_query,test_verify_anchor_rejects_fragment_in_gateway_url.bundler_url_requires_a_funded_account(config),bundler_url_requires_an_explicit_gateway(config),arweave_gateway_has_no_default_network(config),legacy_irys_url_is_adopted_only_with_funded_account_pair(config),env_example_bundler_block_is_startable,test_anchor_ref_update_rejects_missing_bundler_account(request).post_receive_job_survives_handler_abort(crash-between-enqueue-and-spawn recovery plus idempotent replay),inv22_replication_tail_spawns_at_the_durability_boundary(ordering gate binds enqueue-before-spawn-before-release).anchor_record_failure_is_reconciled_without_double_pay(accepted upload whoserecordedUPDATE fails → row leftuploadingwith its item id; the retry probes the gateway, finds the item, records it WITHOUT a second paid request),anchor_claim_db_failure_never_uploads(claim unanswerable → fail closed, no upload),anchor_probe_failure_never_uploads(gateway probe returns no verdict → fail closed, no upload, row stays non-terminal),post_receive_job_anchor_failure_retries_and_replay_never_reuploads(bundler 500 → rowfailed, the drain probes, re-uploads, records,done; replay never pays twice; the stored anchor'snode_didis the node's, not the pusher's),two_concurrent_workers_claim_the_job_once(atomic job claim → exactly one upload).test_upload_rejects_empty_missing_and_malformed_success_ids(empty/missing/malformed bundler ids areUncertain; a 43-char base64url id isAccepted).satisfies_threshold_rejects_duplicated_signature(a duplicated signature of one key fails a 2-of-3).list_anchors_limit_zero_uses_default_limit(limit=0falls back to the default page size instead of returning nothing),list_anchors_without_gateway_omits_arweave_url(no gateway → no relative/tx_idURL, the durable tx id still lists).upgrade_path_tests::upgrading_released_v1_schema_lands_cert_and_anchor_columns.How a reviewer can verify
DATABASE_URL=postgresql://gitlawb:changeme@localhost:5432/gitlawb cargo test --workspaceAll 1417 tests across the workspace pass (877 in the
gitlawb-nodesuite).cargo fmt --all -- --checkandcargo clippy --workspace --all-targets -- -D warningsare clean.