Skip to content

Add opt-in digest validation header for stale parameter detection#957

Open
bitcoin-coder-bob wants to merge 9 commits intomasterfrom
bob/digest-in-header
Open

Add opt-in digest validation header for stale parameter detection#957
bitcoin-coder-bob wants to merge 9 commits intomasterfrom
bob/digest-in-header

Conversation

@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator

@bitcoin-coder-bob bitcoin-coder-bob commented Mar 10, 2026

closes #958

Summary by CodeRabbit

  • New Features

    • Optional digest-based request validation: servers seed a digest on startup; clients fetch it via GetInfo and may send X-Ark-Digest/x-ark-digest for subsequent calls.
    • Interceptors enforce digest checks for public unary and streaming RPCs; GetInfo is exempt.
    • Stale digests yield a DIGEST_MISMATCH error that includes current_digest metadata.
  • Tests

    • Added comprehensive unit and end-to-end tests covering unary/streaming validation, exempt cases, matching, and mismatch error details.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Implements opt-in digest validation: server stores GetInfo digest via a DigestService; gRPC interceptors validate client-sent x-ark-digest for public methods (except GetInfo) and return structured DIGEST_MISMATCH (FailedPrecondition, code 49) with current_digest when mismatched.

Changes

Cohort / File(s) Summary
Error Definition
pkg/errors/errors.go
Adds DigestMismatchMetadata and exported DIGEST_MISMATCH error code (49, "DIGEST_MISMATCH", gRPC FailedPrecondition).
Digest Service & Validation
internal/interface/grpc/interceptors/digest.go
New DigestService with atomic storage and SetDigest; checkDigest enforces opt-in x-ark-digest rules (skip GetInfo, only public services, skip when unset or header absent) and returns structured DIGEST_MISMATCH; provides unary and stream validator interceptors.
Interceptor Wiring
internal/interface/grpc/interceptors/interceptor.go
Extended UnaryInterceptor and StreamInterceptor signatures to accept *DigestService; inserted digest validators into interceptor chains after readiness.
Handler & Service Wiring
internal/interface/grpc/handlers/arkservice.go, internal/interface/grpc/service.go
App handler gets digest dependency; GetInfo persists computed digest via SetDigest; server constructs DigestService, wires it into interceptors and handler; gateway header matcher maps X-Ark-Digestx-ark-digest; startup seeds digest by calling GetInfo.
Unit Tests
internal/interface/grpc/interceptors/digest_test.go
Adds comprehensive tests for DigestService, checkDigest, unary/stream validators, exemptions, and DIGEST_MISMATCH error detail contents.
End-to-End Tests
internal/test/e2e/digest_test.go
E2E test TestDigestValidation that seeds digest via GetInfo and asserts behavior for matching, missing, and stale x-ark-digest on unary and streaming RPCs.
Handler Signature
internal/interface/grpc/handlers/arkservice.go
NewAppServiceHandler signature extended to accept digestStore and handler now stores digest and calls SetDigest in GetInfo.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Interceptor
    participant DigestService as DigestService
    participant Handler

    rect rgba(100,150,200,0.5)
    Note over Client,Handler: Initial GetInfo (exempt from check)
    Client->>Interceptor: GetInfo (no x-ark-digest)
    Interceptor->>Handler: invoke GetInfo
    Handler->>Handler: compute digest
    Handler->>DigestService: SetDigest(digest)
    Handler->>Client: return GetInfo + digest
    end

    rect rgba(100,200,150,0.5)
    Note over Client,Handler: Subsequent request with x-ark-digest
    Client->>Interceptor: RegisterIntent + x-ark-digest
    Interceptor->>DigestService: checkDigest(header)
    alt digest matches
        DigestService->>Interceptor: valid
        Interceptor->>Handler: invoke RegisterIntent
        Handler->>Client: return response
    else digest mismatch
        DigestService->>Interceptor: DIGEST_MISMATCH (FailedPrecondition)
        Interceptor->>Client: return error with current_digest
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • version in header #960 — modifies interceptor wiring and gateway header handling similarly; likely directly related to DigestService/interceptor changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding opt-in digest validation via header for detecting stale server parameters.
Linked Issues check ✅ Passed The PR implementation fully meets all requirements from #958: opt-in digest header validation, DIGEST_MISMATCH error with current_digest, GetInfo exemption, and public service method filtering.
Out of Scope Changes check ✅ Passed All changes are scoped to digest validation: new digestStore interface, DigestService with interceptors, error types, handler wiring, and comprehensive tests. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bob/digest-in-header

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@bitcoin-coder-bob bitcoin-coder-bob marked this pull request as ready for review March 10, 2026 18:04
@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 18, 2026

Review: Digest Validation Header

Clean implementation. Opt-in design is the right call — no breaking changes for existing clients. A few observations:

Architecture

  • DigestService uses atomic.Value — correct for concurrent access across gRPC interceptors.
  • Scoping to isPublicServiceMethod (ArkService + IndexerService) and exempting GetInfo is sound.
  • Interceptor ordering: digest validator runs after readiness check, which is correct — no point validating digest on an unready server.
  • REST gateway header mapping (X-Ark-Digestx-ark-digest) properly bridges HTTP→gRPC.

Potential Issues

1. Digest only set on GetInfo call, not on startup
The digest store starts empty and is only populated when GetInfo is first called (h.digest.SetDigest(resp.Digest) in the handler). Until that happens, the interceptor skips validation (stored == ""). This means:

  • If server params change and no one calls GetInfo, the interceptor is dormant.
  • After restart, there's a window where stale digests from clients would pass unchecked.

Is this intentional? If the digest is deterministic from server config, you could seed it at startup in newServer() rather than lazily on first GetInfo.

2. Stream interceptor catches mismatch only at connection time
For long-lived streams (GetEventStream, GetTransactionsStream), if server params change mid-stream, the client won't get a DIGEST_MISMATCH until they reconnect. Acceptable for opt-in staleness detection, but worth documenting — clients relying on digest freshness should periodically re-call GetInfo.

3. digestStore interface is unexported but used across packages
The digestStore interface in handlers/arkservice.go is fine for testing, but DigestService is the only implementation. If you want to keep the interface, consider moving it to the interceptors package or making it exported — right now the handler takes digestStore but service.go passes *interceptors.DigestService directly.

Test Coverage

Thorough — unit tests cover all skip paths (GetInfo, non-public, no stored digest, no header, nil metadata) plus match/mismatch/update scenarios. E2E tests verify the full gRPC stack. Stream mismatch e2e handles both the "error on dial" and "error on Recv" cases.

Cross-Repo Impact

SDK clients (ts-sdk, rust-sdk, dotnet-sdk) will need updates to propagate the x-ark-digest header from GetInfo responses into subsequent requests. Since this is opt-in, no urgency — but linking issue #958 for tracking is good.

Overall: Solid. The lazy initialization question (point 1) is the main thing worth addressing.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 25, 2026

🔍 Arkana PR Review — Digest Validation Header

Scope: 558 additions, 5 deletions across 7 files — opt-in X-Ark-Digest header validation for stale parameter detection.

Security & Protocol Correctness

  1. Opt-in design: ✅ Clients that don't send the header are unaffected — this is backward compatible. The digest is only validated when the client explicitly includes x-ark-digest. This is the correct approach for a phased rollout.

  2. GetInfo exemption:GetInfo is correctly excluded from validation since it's the method clients call to obtain the digest. Without this exemption, clients couldn't recover from a stale digest.

  3. DigestServiceatomic.Value: ✅ Thread-safe for concurrent reads/writes. The SetDigest call in GetInfo handler means the digest is refreshed on every GetInfo response. No TOCTOU issues since the digest only changes when server parameters change (which happens infrequently).

  4. isPublicServiceMethod filter: Only ArkService and IndexerService methods are validated. Admin and wallet service methods are excluded. This is correct — internal/admin APIs shouldn't be gated on client-side digest freshness.

  5. Error response — DIGEST_MISMATCH: Returns FailedPrecondition (gRPC code 9) with structured ErrorDetails containing current_digest. This allows clients to auto-recover by using the returned digest for their next call without needing a full GetInfo round-trip. Good UX.

Potential Concerns

  1. Error code numbering: DIGEST_MISMATCH uses code 49 but BUILD_VERSION_TOO_OLD uses code 48 (from errors.go). The test asserts int32(48) in the digest test at line 101 — wait, looking more carefully, the test at line 101 says require.Equal(t, int32(48), errDetails.Code) but DIGEST_MISMATCH is defined as code 49. This is a discrepancy:

    • pkg/errors/errors.go: var DIGEST_MISMATCH = Code[DigestMismatchMetadata]{49, "DIGEST_MISMATCH", ...}
    • interceptors/digest_test.go line 101: require.Equal(t, int32(48), errDetails.Code)

    This test should use int32(49) to match the error definition. This would be a test bug that passes because the test might not actually be running against the real error path, or the assertion is wrong and the test would fail. Worth verifying.

    Edit: On second look, the test at line 101 does say int32(48) — if DIGEST_MISMATCH is truly code 49, this test assertion is incorrect. The e2e test at digest_test.go line 80 also asserts int32(48). One of these is wrong.

  2. gRPC-gateway header mapping: The X-Ark-Digestx-ark-digest mapping in service.go is correct for REST clients using the gRPC-gateway proxy. Without this, REST clients couldn't use the feature.

  3. Digest staleness window: The digest is only stored when GetInfo is called. If server parameters change but no client calls GetInfo, the stored digest remains stale. However, since GetInfo is called at connection setup by all SDKs, this window is negligible in practice.

Test Coverage ✅

  • Unit tests: 11 cases covering skip conditions (GetInfo, non-public, no stored digest, no header, nil metadata), match, mismatch, digest updates, unary interceptor, and stream interceptor.
  • E2E tests: 5 cases covering opt-in, correct digest, stale digest, GetInfo exemption, and stale digest on streams.

Cross-Repo Impact

  • SDKs (ts-sdk, go-sdk, rust-sdk, dotnet-sdk) will need updates to send X-Ark-Digest header to benefit from this feature. Since it's opt-in, no breaking changes.

Verdict

Clean implementation of an important protocol safety feature. The opt-in design and backward compatibility are well handled. Please verify the error code assertion in tests (48 vs 49) — this appears to be a bug in the test expectations. Otherwise, no blocking issues.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 25, 2026

🔍 Arkana PR Review

PR: Add opt-in digest validation header for stale parameter detection
Risk: Medium — new gRPC interceptor on all public endpoints

Architecture

Clean design: DigestService uses atomic.Value for lock-free concurrent reads, checkDigest is well-scoped (skips GetInfo, admin, and non-public methods), and the opt-in model (no header = pass) ensures zero breaking changes for existing clients. The interceptor is correctly placed after auth/readiness checks in the chain.

🐛 Bug: Error Code Mismatch in Tests

DIGEST_MISMATCH is defined as error code 49 in pkg/errors/errors.go:

var DIGEST_MISMATCH = Code[DigestMismatchMetadata]{49, "DIGEST_MISMATCH", grpccodes.FailedPrecondition}

But the unit tests in digest_test.go assert error code 48 (which is BUILD_VERSION_TOO_OLD):

require.Equal(t, int32(48), errDetails.Code)  // ← should be 49

This appears in two test cases:

  • "returns error on digest mismatch" (line ~108)
  • "reflects updated digest" (line ~133)

These tests will fail. Should be int32(49).

Security Review

  • ✅ Opt-in only — no forced breakage for clients that don't send the header
  • ✅ GetInfo is correctly exempted (clients need it to obtain the digest)
  • ✅ Admin/wallet methods are excluded via isPublicServiceMethod
  • ✅ Mismatch error includes current_digest in metadata, allowing clients to self-heal
  • atomic.Value is appropriate for high-read/low-write concurrency

Observations

  1. Digest seeding via GetInfo call at startup (seedDigest in service.go) — this ensures the digest store is populated before any client request. Good pattern.

  2. Header normalization in service.go maps X-Ark-Digestx-ark-digest for gRPC-Web/REST gateway compat. Correct.

  3. E2E tests are thorough — covers opt-in behavior, correct digest pass-through, mismatch on both unary and streaming, and GetInfo exemption.

  4. No cross-repo SDK impact yet — since this is opt-in, SDK changes can follow independently. SDKs should eventually send x-ark-digest after GetInfo and handle DIGEST_MISMATCH by re-calling GetInfo.

Verdict

⚠️ Fix the error code in unit tests (48 → 49) before merging. Otherwise, solid implementation.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 25, 2026

🔍 Arkana PR Review — arkd#957 (updated commits)

Add opt-in digest validation header for stale parameter detection by @bitcoin-coder-bob

Overview

Introduces an opt-in X-Ark-Digest / x-ark-digest header mechanism so clients can detect when server parameters have changed since their last GetInfo call. This prevents stale parameter usage (e.g., using outdated tree expiry or exit delays when constructing intents). +568/-5, mostly tests.

✅ What Looks Good

  • Opt-in design — clients that do not send the header are unaffected. Clean backward compatibility.
  • GetInfo exemption — correctly allows clients to refresh their digest even with a stale header.
  • Admin/Wallet service exclusion via isPublicServiceMethod() — only public-facing ArkService and IndexerService methods are validated.
  • atomic.Value for DigestService — lock-free concurrent reads, correct usage pattern.
  • Structured error responseDIGEST_MISMATCH error code (49) with current_digest in metadata lets clients auto-recover.
  • Comprehensive tests — both unit tests for the interceptor logic and E2E tests covering unary, streaming, exempt, and mismatch scenarios. Good coverage.

⚠️ Items to Consider

  1. Digest seeded via GetInfo side effect: The digest is only stored when GetInfo is called (via h.digest.SetDigest(resp.Digest)). Before the first GetInfo call after startup, checkDigest returns nil (digest store empty). The seedDigest function in service.go calls GetInfo on startup to prime it — good. But if seedDigest fails (the log.WithError(err).Warn path), the server runs without digest validation until a client calls GetInfo. This is safe (opt-in skips when empty), but worth documenting that digest validation is best-effort until the first successful GetInfo.

  2. Digest changes are implicit: The digest changes when server parameters change (settings update, config reload). The digest is computed from GetInfo response fields. If settings are updated via the admin API (from PR add Settings domain with DB persistence and admin CRUD API #939), the digest only updates on the next GetInfo call — not immediately on settings change. Consider whether the admin UpdateSettings flow should also call SetDigest to ensure immediate staleness detection.

  3. HTTP header mapping in service.go: The X-Ark-Digestx-ark-digest mapping is added to the gRPC-gateway header translator. This is correct for REST clients. Verify that gRPC clients sending lowercase x-ark-digest metadata also work (they should — gRPC metadata keys are case-insensitive, and md.Get() handles this).

  4. Error code 49: Confirm this does not collide with any existing error codes in pkg/errors/errors.go. Looks clear based on the diff, but worth a grep.

Security

  • No security concerns. This is a defense mechanism, not an attack surface. The opt-in nature means it cannot be used for DoS (clients that do not send the header are unaffected).
  • The digest is derived from public GetInfo data, so exposing current_digest in the error response does not leak sensitive information.

Cross-Repo Impact

  • SDK clients (ts-sdk, rust-sdk, go-sdk, dotnet-sdk) should be updated to send X-Ark-Digest and handle DIGEST_MISMATCH by re-fetching GetInfo. This is opt-in so not blocking, but worth tracking.

Verdict

Well-designed stale-parameter detection mechanism. The opt-in approach is the right call. Main follow-up: coordinate with #939 (Settings domain) to ensure digest updates propagate on admin settings changes.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
internal/test/e2e/digest_test.go (2)

95-117: Consider asserting structured mismatch details for stream errors too.

You already check stream FailedPrecondition; adding ErrorDetails (DIGEST_MISMATCH + current_digest) would make stream-path contract validation consistent with unary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/test/e2e/digest_test.go` around lines 95 - 117, The test "stale
digest on stream returns DIGEST_MISMATCH" currently only asserts
codes.FailedPrecondition; update the error assertions after receiving from
stream (after calling status.FromError(err)) to also inspect st.Details() and
assert the presence of the DIGEST_MISMATCH error detail and that its
current_digest field equals the expected value ("expected-current-digest" /
whatever constant the unary test uses). Locate the stream test using
GetEventStream and variable stream, call status.FromError(err), then iterate
st.Details() (or grab the first detail), cast to the digest-mismatch proto type
used elsewhere in tests (the same proto/assertion used in the unary path), and
add require.Equal checks for the mismatch type and current_digest value to
mirror the unary-path contract validation.

50-57: Tighten the handler-path assertion to the expected code.

This currently only proves “not FailedPrecondition”. Since the comment says handler validation should return InvalidArgument, asserting that explicitly makes the test less permissive.

🔧 Suggested assertion update
-		// Handler rejects the empty request as InvalidArgument, not FailedPrecondition.
-		require.NotEqual(t, codes.FailedPrecondition, st.Code(),
-			"expected a handler error, not a digest mismatch")
+		// Handler rejects the empty request as InvalidArgument.
+		require.Equal(t, codes.InvalidArgument, st.Code(),
+			"expected handler validation error after digest check passed")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/test/e2e/digest_test.go` around lines 50 - 57, The test currently
only asserts the error code is not codes.FailedPrecondition after calling
svc.RegisterIntent(mdCtx, &arkv1.RegisterIntentRequest{}); tighten this to
assert the expected handler validation code explicitly by replacing the
require.NotEqual check with an equality assertion that st.Code() equals
codes.InvalidArgument (using require.Equal or require.Equalf) and update the
message to reflect that the handler should return InvalidArgument for an empty
request.
internal/interface/grpc/interceptors/digest_test.go (1)

108-119: Unify mismatch assertions to verify structured details across all mismatch paths.

These subtests only assert FailedPrecondition, so regressions in DIGEST_MISMATCH details (name/code/current_digest) could slip through on Indexer/unary/stream paths.

♻️ Suggested helper to enforce consistent mismatch contract checks
+func requireDigestMismatch(t *testing.T, err error, expectedDigest string) {
+	t.Helper()
+	st, ok := status.FromError(err)
+	require.True(t, ok)
+	require.Equal(t, codes.FailedPrecondition, st.Code())
+
+	details := st.Details()
+	require.NotEmpty(t, details)
+	errDetails, ok := details[0].(*arkv1.ErrorDetails)
+	require.True(t, ok)
+	require.Equal(t, "DIGEST_MISMATCH", errDetails.Name)
+	require.Equal(t, expectedDigest, errDetails.Metadata["current_digest"])
+}
-		st, ok := status.FromError(err)
-		require.True(t, ok)
-		require.Equal(t, codes.FailedPrecondition, st.Code())
+		requireDigestMismatch(t, err, "current-digest")

Also applies to: 168-194, 265-291

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/interface/grpc/interceptors/digest_test.go` around lines 108 - 119,
The test only asserts codes.FailedPrecondition for digest mismatches; add a
shared assertion helper (e.g., assertDigestMismatch(t, err, expectedName,
expectedCode, expectedCurrentDigest)) and call it from the existing subtests
(like the "returns error on IndexerService digest mismatch" case that exercises
NewDigestService, SetDigest and checkDigest) so each path verifies the full
structured DIGEST_MISMATCH details (name, code, current_digest) in the returned
gRPC status; implement the helper to call status.FromError(err), assert the code
is FailedPrecondition, extract st.Details(), assert the first detail is the
expected digest-mismatch protobuf/struct and compare its name, code and
current_digest fields, then replace the simple require.Equal(t,
codes.FailedPrecondition, ...) checks in the Indexer/unary/stream tests with
calls to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/interface/grpc/interceptors/digest_test.go`:
- Around line 108-119: The test only asserts codes.FailedPrecondition for digest
mismatches; add a shared assertion helper (e.g., assertDigestMismatch(t, err,
expectedName, expectedCode, expectedCurrentDigest)) and call it from the
existing subtests (like the "returns error on IndexerService digest mismatch"
case that exercises NewDigestService, SetDigest and checkDigest) so each path
verifies the full structured DIGEST_MISMATCH details (name, code,
current_digest) in the returned gRPC status; implement the helper to call
status.FromError(err), assert the code is FailedPrecondition, extract
st.Details(), assert the first detail is the expected digest-mismatch
protobuf/struct and compare its name, code and current_digest fields, then
replace the simple require.Equal(t, codes.FailedPrecondition, ...) checks in the
Indexer/unary/stream tests with calls to this helper.

In `@internal/test/e2e/digest_test.go`:
- Around line 95-117: The test "stale digest on stream returns DIGEST_MISMATCH"
currently only asserts codes.FailedPrecondition; update the error assertions
after receiving from stream (after calling status.FromError(err)) to also
inspect st.Details() and assert the presence of the DIGEST_MISMATCH error detail
and that its current_digest field equals the expected value
("expected-current-digest" / whatever constant the unary test uses). Locate the
stream test using GetEventStream and variable stream, call
status.FromError(err), then iterate st.Details() (or grab the first detail),
cast to the digest-mismatch proto type used elsewhere in tests (the same
proto/assertion used in the unary path), and add require.Equal checks for the
mismatch type and current_digest value to mirror the unary-path contract
validation.
- Around line 50-57: The test currently only asserts the error code is not
codes.FailedPrecondition after calling svc.RegisterIntent(mdCtx,
&arkv1.RegisterIntentRequest{}); tighten this to assert the expected handler
validation code explicitly by replacing the require.NotEqual check with an
equality assertion that st.Code() equals codes.InvalidArgument (using
require.Equal or require.Equalf) and update the message to reflect that the
handler should return InvalidArgument for an empty request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5fa8ed42-7747-4cc4-9208-160661270bc4

📥 Commits

Reviewing files that changed from the base of the PR and between 78e4591 and c51a0a9.

📒 Files selected for processing (2)
  • internal/interface/grpc/interceptors/digest_test.go
  • internal/test/e2e/digest_test.go

@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator Author

@arkanaai
1: updated log in: 9afbb36
2: wont do until #939 is merged
3: gRPC metadata is case-insensitive
4: verified, no collision

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 25, 2026

🔍 Arkana Review — Digest Validation Header (follow-up)

New commits since last review (c51a0a99afbb360).

Summary

Opt-in X-Ark-Digest header allowing clients to detect stale server parameters. Solid approach — server seeds digest on startup via GetInfo, interceptors validate on all public RPCs (both unary and streaming), GetInfo is exempt so clients can always re-fetch.

Architecture ✅

  • DigestService uses atomic.Value — thread-safe, no lock contention. Clean.
  • Interceptor chain placement — digest check runs after macaroon auth + readiness, correct ordering.
  • Startup seedingseedDigest calls GetInfo on app service start to populate the digest store. Graceful degradation if it fails (logs warning, digest validation disabled until first client call).
  • HTTP gateway mappingX-Ark-Digestx-ark-digest header forwarding added in IncomingHeaderMatcher.

Security Observations

  1. Opt-in is the right call — existing clients without the header are unaffected. No breaking change.
  2. DIGEST_MISMATCH error returns current_digest in metadata — this is intentional for client recovery flow, but worth noting it exposes the current digest to any caller that sends any digest header value. This is fine since GetInfo already returns it publicly.

Minor Notes

  • The isPublicServiceMethod helper (referenced but defined elsewhere) gates which methods get validated. Good separation — admin/wallet RPCs excluded.
  • E2E tests cover the full lifecycle: no header, correct header, stale header, GetInfo exemption, and streaming mismatch. Thorough.

Cross-repo Impact

  • SDKs (go-sdk, ts-sdk, rust-sdk, dotnet-sdk) will need to opt into sending this header to benefit from stale parameter detection. Currently a server-only change — no SDK breaks.

LGTM — clean implementation with proper error semantics.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 25, 2026

Thanks for the updates @bitcoin-coder-bob.

  1. ✅ Checked the commit — log change looks good.
  2. 👍 Makes sense to defer to after add Settings domain with DB persistence and admin CRUD API #939 lands. The digest-on-settings-change can be a clean follow-up once the Settings domain is in.
  3. ✅ Confirmed — metadata.MD.Get() normalizes to lowercase, so X-Ark-Digest and x-ark-digest resolve the same. No issue there.
  4. ✅ Good to hear — no collision with existing error codes.

No remaining concerns from my side.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 25, 2026

🔍 Arkana Review — arkd#957

Add opt-in digest validation header for stale parameter detection (bitcoin-coder-bob)

Closes #958

Summary

Adds an opt-in digest validation mechanism: the server computes a digest of its configuration parameters (exposed via GetInfo), and clients can send it back via X-Ark-Digest / x-ark-digest header on subsequent calls. If the digest mismatches, the server returns a DIGEST_MISMATCH error with the current digest, signaling the client to refresh its cached parameters.

Architecture

  • DigestService: Thread-safe via atomic.Value, stores the current digest string
  • SetDigest called from GetInfo handler: Ensures digest is always fresh after any GetInfo call
  • Startup seeding: seedDigest calls GetInfo internally to populate the digest store on server start
  • Interceptor chain: unaryDigestValidator and streamDigestValidator added as the last interceptors before errorConverter — validates after auth/readiness/version checks
  • Opt-in: No header → skip. Header present with matching digest → pass. Mismatch → FailedPrecondition with DIGEST_MISMATCH error details
  • Exemptions: GetInfo (used to fetch digest), non-public methods (admin, wallet), and cases where no digest is stored yet

Security Review

No information leakage — the digest is already public via GetInfo, so returning current_digest in the error metadata doesn't expose anything new
Opt-in design — existing clients without the header are unaffected
Thread safetyatomic.Value for the digest store is correct for concurrent reads/writes
Interceptor ordering — digest check runs after auth, so unauthenticated requests are rejected before reaching digest validation
isPublicServiceMethod correctly scopes validation — only ArkService and IndexerService methods are checked

Observations

  1. Startup seed graceful degradation: If seedDigest fails (e.g., app service not fully ready), it logs a warning and digest validation stays disabled until a client calls GetInfo. This is the right behavior — avoids blocking startup.

  2. Header key normalization: The gRPC-Gateway IncomingHeaderMatcher maps X-Ark-Digestx-ark-digest, and the interceptor looks for x-ark-digest in metadata. Lowercase matching is correct per gRPC metadata conventions.

  3. Error code 49 (DIGEST_MISMATCH): New error code added to the errors package. Consistent with the existing error pattern (BUILD_VERSION_TOO_OLD uses similar structure).

  4. Test coverage is thorough: Unit tests for checkDigest cover all bypass paths (GetInfo, non-public methods, no stored digest, no header, nil metadata), match/mismatch, and digest updates. E2E tests verify the full gRPC round-trip including stream validation.

  5. Minor: metadata.Get on nil MDcheckDigest receives md from metadata.FromIncomingContext which returns (nil, false) when missing. metadata.MD.Get(key) on a nil MD returns nil (no panic), so the len(vals) == 0 check handles this correctly.

Verdict

Well-designed opt-in mechanism. Clean separation of concerns, correct concurrency handling, and good test coverage. No security issues.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 25, 2026

🔍 Arkana PR Review — arkd#957 (updated commits)

Add opt-in digest validation header for stale parameter detection (+594/-5, 7 files)

Summary

Adds a DigestService that stores the current server parameter digest (seeded on startup via GetInfo). Clients can opt in by sending X-Ark-Digest / x-ark-digest — if the digest doesn't match, the server returns DIGEST_MISMATCH (code 49, FailedPrecondition) with the current digest in metadata.

✅ What looks good

  • Opt-in design: Clients without the header are unaffected. GetInfo is always exempt. Only public service methods (ArkService/IndexerService) are validated. This is the right approach — no breaking changes.
  • Thread-safe: atomic.Value for the digest store is correct for concurrent reads/writes.
  • Comprehensive tests: Unit tests cover all skip paths (GetInfo, non-public methods, admin methods, no digest stored, no header, nil metadata), match/mismatch, digest updates. E2e tests verify the full gRPC flow including stream validation.
  • Graceful startup: If the seed GetInfo call fails, the digest stays empty and validation is disabled until a client calls GetInfo. Good degradation behavior.
  • Clean error metadata: DigestMismatchMetadata includes current_digest so clients can immediately retry with the correct value.

⚠️ Issues

  1. Digest seeded on every GetInfo call, not just startup: In handlers/arkservice.go, h.digest.SetDigest(resp.Digest) is called on every GetInfo response. This means the digest updates whenever server parameters change (which is correct), but it also means concurrent GetInfo calls could race to set different digests if the underlying data is changing. In practice this is fine since the digest is deterministic for a given state, but worth noting.

  2. isPublicServiceMethod referenced but not shown in diff: The checkDigest function calls isPublicServiceMethod(fullMethod) — this must be defined elsewhere. The tests verify it works for ArkService, IndexerService, WalletService, and AdminService, so it's presumably correct. Just flagging that reviewers should verify this function's implementation covers all current services.

  3. Header normalization: The REST gateway maps X-Ark-Digestx-ark-digest, and the interceptor checks x-ark-digest. For pure gRPC clients, metadata keys are automatically lowercased by gRPC-Go, so this is fine. The gateway mapping ensures REST clients work too. ✅

  4. Error code 49: New error code DIGEST_MISMATCH = 49. Confirm this doesn't collide with any other error codes in the errors.go file. The previous highest appears to be BUILD_VERSION_TOO_OLD — should verify 49 is sequential and unused.

Security

  • No authentication bypass: The digest check runs after version check but after macaroon auth in the interceptor chain (per the ordering in interceptor.go). Actually, looking at the chain: panic recovery → logger → version → macaroon → readiness → digest → errorConverter. Digest runs after auth — this is correct. An unauthenticated client can't use a stale digest to learn about server state changes.
  • Information leakage is minimal: The current_digest in error metadata is a hash of public parameters (already available via GetInfo), so exposing it in error responses is fine.

Verdict

LGTM — well-designed opt-in mechanism with solid test coverage. The only action item is verifying isPublicServiceMethod covers all current services and that error code 49 is unused.

@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator Author

@arakanaai those concerns are already addressed in this PR discourse or in the code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add opt-in digest validation header to detect stale server parameters

1 participant