Skip to content

feat(api): v1 chain/* + transactions/* reads — first product-API route group - #169

Merged
arkadianet merged 4 commits into
mainfrom
feat/v1-api-read-gap-chain-tx
Jul 8, 2026
Merged

feat(api): v1 chain/* + transactions/* reads — first product-API route group#169
arkadianet merged 4 commits into
mainfrom
feat/v1-api-read-gap-chain-tx

Conversation

@arkadianet

@arkadianet arkadianet commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Stacked on #168 (the G2 shared primitives). Review/merge #168 first.

What this is

The first route group of /api/v1/* and the PR that mounts /api/v1 for realchain/* + transactions/* reads, the highest-priority self-sufficiency gap (today a v1 client must drop to the compat surface for these). It's the first consumer of the G2 primitives, so it sets the handler/envelope/tier/test pattern every later group copies.

Endpoints (all T0, Phase-1, governor HeavyRead class)

  • chain/* (all 11): blocks list / by-id / by-id txs / at-height / by-ids; headers list / by-id / at-height; modifiers (explicit kind); ad-proofs; tx Merkle proof (left/right).
  • transactions/* reads: {tx_id} (unified confirmed + unconfirmed, output-derived fee, confirmed flag); {submit,check} canonical (Appendix A O1). {tx_id}/detail already shipped — left as-is.

Everything uses the v1 nested error envelope + canonical reasons, cursor page builder, snake_case glossary names, and the shared governor. Server now serves with into_make_service_with_connect_info so the governor/auth read the real peer IP, and warn_startup_posture is called at the startup seam.

Honest, not stubbed

Where a capability isn't wired yet the endpoint returns the true reason, never fake data or new schema: chain_reader_unavailable, indexer_disabled (+indexer_syncing/indexer_halted), submit_disabled, ad_proofs_unavailable (pruned), delivered_by: null outside the near-tip ring.

Design/hook corrections (flagged for the record)

  1. timestamp_unix_ms+timestamp_iso / size_bytes per the synthesis doc (overrides the pre-coherence fragment's timestamp/size).
  2. Miner address via ergo_ser::address::encode_p2pk_from_pubkey (ergo-api has no ergo-wallet dep — the cite was stale).
  3. Fee proposition embedded as a v1 const oracle-pinned to test-vectors/mainnet/fee_proposition.hex (avoids pulling ergo-mempool into the API crate).
  4. Full-path routers merged instead of nest("/api/v1") — the operator router already carries flat /api/v1/* routes a nest would conflict with; behaviorally identical.
  5. Frozen enum has no modifier_not_found → missing modifier answers block_not_found, missing proof tx_not_in_block.

The V1Box shape here is the transactions-group projection; the future boxes/* group owns the canonical box shape and MUST match these field names (they're already glossary-exact: box_id/value-as-string/ergo_tree/creation_height/registers/output_index/spent_by/global_index/assets).

Test plan

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace

ergo-api/tests/v1_chain_tx_routes.rs — 24 route-level tests locking envelope/page/reason/exact snake_case field names against the design schemas (the convention-lock, enforced), plus lib unit tests (ISO-timestamp oracle vectors, fee-proposition oracle-pin, submit-reason map). Compat routes untouched.

Deferred (scoped)

tx build/simulate/fee-estimate/status (Phase-2 tx-intelligence group); mempool/{submit,check} alias (ships with the mempool group); OpenAPI aggregator registration of the new routes (§1.7 follow-up pass).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

Summary by CodeRabbit

  • New Features

    • Added a new /api/v1 API surface for blocks, headers, proofs, and transactions.
    • Introduced cursor-based pagination and consistent response envelopes for list endpoints.
    • Added transaction submission and validation endpoints.
  • Bug Fixes

    • Improved API key verification and access control handling.
    • Added request throttling to help keep heavy endpoints responsive.
    • Standardized error responses and status codes across v1 endpoints.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@arkadianet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 090f05f7-42fd-4bec-952c-4e45f40f797e

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0910e and 2cfcc40.

📒 Files selected for processing (10)
  • ergo-api/src/blockchain.rs
  • ergo-api/src/blockchain/boxes.rs
  • ergo-api/src/blockchain/transactions.rs
  • ergo-api/src/server.rs
  • ergo-api/src/v1/mod.rs
  • ergo-api/src/v1/routes/chain.rs
  • ergo-api/src/v1/routes/dto.rs
  • ergo-api/src/v1/routes/mod.rs
  • ergo-api/src/v1/routes/transactions.rs
  • ergo-api/tests/v1_chain_tx_routes.rs
📝 Walkthrough

Walkthrough

This PR introduces a new native /api/v1/* product API with shared infrastructure: tier-based authorization (require_tier), a per-IP token-bucket rate governor, a versioned cursor pagination codec, and a canonical nested error envelope. It adds chain and transactions route handlers with DTOs projecting internal types, refactors ApiSecurity to expose a shared verify method, widens visibility of existing response builders, wires the new router and governor into the server, and adds integration tests.

Changes

Native v1 Product API

Layer / File(s) Summary
Shared API key verification
ergo-api/src/auth.rs, ergo-api/Cargo.toml
Adds ApiSecurity::verify for constant-time key checking; require_api_key middleware refactored to use it; adds base64 workspace dependency.
Crate-visibility widening
ergo-api/src/blockchain.rs, ergo-api/src/blockchain/boxes.rs, ergo-api/src/blockchain/transactions.rs
Widens build_indexed_box_response/build_indexed_tx_response to pub(crate) for reuse by v1 routes.
Canonical error envelope
ergo-api/src/v1/error.rs
Adds Reason enum, HTTP status mapping, V1Error/V1ErrorInner, v1_error helper, and contract tests covering 111 reasons.
Cursor pagination codec
ergo-api/src/v1/cursor.rs
Implements versioned base64url cursor encode/decode, clamp_limit, and Page::from_overfetch with unit tests.
Tier-based auth and startup posture
ergo-api/src/v1/auth.rs
Adds Tier, V1AuthConfig/V1AuthState, require_tier middleware, and assess_posture/warn_startup_posture with tests.
Per-IP rate governor
ergo-api/src/v1/governor.rs
Adds RouteClass, GovernorConfig, token-bucket Governor, GovernorState, and governor_mw middleware with tests.
v1 module surface
ergo-api/src/lib.rs, ergo-api/src/v1/mod.rs
Declares v1 module, re-exports submodule APIs, and adds client_ip helper.
DTOs and projections
ergo-api/src/v1/routes/dto.rs
Defines wire DTOs for headers, blocks, modifiers, proofs, boxes, transactions, and projection functions from internal types.
Chain route handlers
ergo-api/src/v1/routes/chain.rs
Implements chain/* handlers for blocks, headers, modifiers, AD-proofs, and Merkle proofs.
Transactions route handlers
ergo-api/src/v1/routes/transactions.rs
Implements transaction read/submit/check handlers, fee computation, box resolution, and error reason mapping.
Router assembly and state
ergo-api/src/v1/routes/mod.rs
Defines V1State, query parsing helpers, and v1_router wiring all endpoints with governor middleware.
Server wiring
ergo-api/src/server.rs
Mounts v1_router and Governor into the assembled router, switches serving to connect-info-aware make_service, and calls warn_startup_posture.
Integration tests
ergo-api/tests/v1_chain_tx_routes.rs
Adds integration tests with stub node/read/submit fixtures covering chain and transactions routes and error envelopes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router as v1_router
  participant Governor
  participant AuthMw as require_tier
  participant Handler as chain/transactions handler
  participant State as V1State

  Client->>Router: HTTP request /api/v1/*
  Router->>Governor: governor_mw charge tokens for RouteClass
  Governor-->>Router: allow or 429 rate_limited
  Router->>AuthMw: require_tier(Tier)
  AuthMw-->>Router: allow, unauthorized, or SensitiveOpDisabled
  Router->>Handler: dispatch to handler
  Handler->>State: read chain/mempool/indexer data
  State-->>Handler: data or error
  Handler-->>Client: JSON response or v1_error envelope
Loading
sequenceDiagram
  participant Server
  participant Posture as warn_startup_posture
  participant Listener
  participant Router as v1_router

  Server->>Listener: bind and get local_addr
  Server->>Posture: warn_startup_posture(security, bind_addr)
  Posture-->>Server: log WARN if insecure posture
  Server->>Router: mount v1_router(V1State, Governor)
  Server->>Server: serve via make_service_with_connect_info
Loading

Possibly related PRs

  • arkadianet/ergo#88: Adds an operator write endpoint POST /api/v1/votes that depends on the ApiSecurity/require_api_key refactor and /api/v1 routing/auth wiring introduced here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the first mounted v1 chain and transactions read route group.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v1-api-read-gap-chain-tx

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
ergo-api/tests/v1_chain_tx_routes.rs (2)

676-733: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Misleading test name / missing direct /submit rejection coverage.

submit_rejection_maps_to_v1_envelope (line 709) actually posts to /api/v1/transactions/check, not /submit. The /submit path only has success (submit_ok_returns_tx_id) and disabled-bridge (submit_without_bridge_is_submit_disabled) coverage — there's no test verifying a business-error rejection (e.g. double_spend) through /submit itself, only through /check.

Consider either renaming this test to reflect it exercises /check, or adding a parallel case that posts the same rejection through /submit to lock both paths independently.

🤖 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 `@ergo-api/tests/v1_chain_tx_routes.rs` around lines 676 - 733, The test name
is misleading because submit_rejection_maps_to_v1_envelope sends the request to
/api/v1/transactions/check instead of the /submit endpoint. Either rename this
test to match the /check route, or add a separate submit-path rejection test in
v1_chain_tx_routes.rs that uses the same StubSubmit error case through app(Deps)
and send(..., "/api/v1/transactions/submit", ...) to cover the /submit
business-error behavior independently.

350-353: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

No route-level coverage for auth-tier gating or active rate limiting.

This file's stated purpose is to be the "convention-lock" template for later route groups, and the PR objectives explicitly call out auth-tier handling and the per-IP governor as part of this group. However, every request here is sent from a loopback ConnectInfo (governor-exempt) and no test exercises require_tier rejection (e.g. missing/invalid API key → 401/403) or governor throttling behavior for a non-exempt IP against these mounted routes.

Unit tests for Governor/require_tier presumably exist at the middleware layer, but that doesn't confirm the middleware is actually wired correctly onto these specific routes. Worth adding at least one integration case per concern (non-loopback IP hitting a rate limit, and a tier-gated route rejecting an unauthorized caller) to close this gap for the "template" this file is meant to establish.

Also applies to: 658-733

🤖 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 `@ergo-api/tests/v1_chain_tx_routes.rs` around lines 350 - 353, Add integration
coverage in the route tests so this template verifies the middleware wiring, not
just the happy path. In v1_chain_tx_routes.rs, keep the existing loopback-based
cases but add at least one request against a non-loopback ConnectInfo to
exercise Governor throttling, and another request to a require_tier-protected
route with missing/invalid API key to confirm 401/403 rejection. Use the
existing route setup helpers and request-building patterns in the test module so
the new cases target the mounted endpoints directly.
🤖 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 `@ergo-api/src/v1/auth.rs`:
- Around line 132-140: The tiered auth check in auth middleware is failing open
because `check_key` returns `NoKeyConfigured` as allowed for both
`Tier::Operator` and `Tier::Admin`. Update the `Tier::Operator`/`Tier::Admin`
branches in `auth.rs` so that `NoKeyConfigured` is treated as a rejection and
returns an unauthorized response instead of calling `next.run`, and apply the
same change to the duplicate tiered-route logic referenced by the other affected
block.

In `@ergo-api/src/v1/error.rs`:
- Line 156: The RouteDisabled error variant is mapped inconsistently with the
`_disabled` status contract, so update the `ErrorReason` handling in `error.rs`
to keep `RouteDisabled` on the 409 `_disabled` path unless it is truly
transient. Adjust the match arm used by the status mapping logic (including the
related branches around the error/status conversion helpers) so `RouteDisabled`
resolves the same way as other disabled reasons, or rename it to an
`_unavailable`-style reason if you intend to keep 503 semantics.

In `@ergo-api/src/v1/governor.rs`:
- Around line 142-145: Governor::new currently accepts unvalidated
GovernorConfig values, allowing negative or non-finite knobs to break throttling
and even add tokens during cost subtraction. Add validation in Governor::new
(and any shared config initialization used by the related constructors around
the referenced locations) to reject negative route weights and zero/non-finite
refill or burst values before creating the Arc<Self>, returning or panicking
consistently with the existing API. Use the Governor and GovernorConfig symbols
to locate the constructor and enforce these checks at the boundary so invalid
configs never reach bucket/token logic.
- Around line 189-191: In the pruning path inside the governor bucket management
logic, the retain check is using stale token counts for idle entries, so one-off
IP buckets never become eligible for removal. Update the pruning flow around the
map.retain call to recompute each bucket’s virtual refill state before
evaluating whether it should be kept, using the same refill logic as the
current-IP path in the governor code. Make sure the condition in this pruning
branch uses the refreshed tokens value, so buckets idle past idle_prune_after
can actually be dropped and max_tracked_ips remains an effective bound.

In `@ergo-api/src/v1/mod.rs`:
- Around line 48-55: The client_ip() helper currently trusts only
ConnectInfo<SocketAddr>, which can make all traffic from a local reverse proxy
appear as loopback and bypass the governor exemption and Admin loopback checks.
Update the v1 request source handling in client_ip() and the related
startup/config path to support a trusted-proxy or PROXY-protocol source of the
real peer address, or explicitly reject deployments where the app is behind a
local reverse proxy. Make sure the fix preserves the security boundary around
loopback rather than inferring it from the proxy’s socket address.

In `@ergo-api/src/v1/routes/transactions.rs`:
- Around line 42-47: The parse_id32 helper currently accepts any 64-character
hex string, including uppercase or mixed-case IDs, which bypasses the
lowercase-only validation used elsewhere. Update parse_id32 to mirror
valid_modifier_id by checking that the input is ASCII lowercase before calling
hex::decode, while keeping the existing length and decode/try_into flow intact.

---

Nitpick comments:
In `@ergo-api/tests/v1_chain_tx_routes.rs`:
- Around line 676-733: The test name is misleading because
submit_rejection_maps_to_v1_envelope sends the request to
/api/v1/transactions/check instead of the /submit endpoint. Either rename this
test to match the /check route, or add a separate submit-path rejection test in
v1_chain_tx_routes.rs that uses the same StubSubmit error case through app(Deps)
and send(..., "/api/v1/transactions/submit", ...) to cover the /submit
business-error behavior independently.
- Around line 350-353: Add integration coverage in the route tests so this
template verifies the middleware wiring, not just the happy path. In
v1_chain_tx_routes.rs, keep the existing loopback-based cases but add at least
one request against a non-loopback ConnectInfo to exercise Governor throttling,
and another request to a require_tier-protected route with missing/invalid API
key to confirm 401/403 rejection. Use the existing route setup helpers and
request-building patterns in the test module so the new cases target the mounted
endpoints directly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 94078387-9529-416a-988d-19367c154857

📥 Commits

Reviewing files that changed from the base of the PR and between 39cb0d6 and 7e0910e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • ergo-api/Cargo.toml
  • ergo-api/src/auth.rs
  • ergo-api/src/blockchain.rs
  • ergo-api/src/blockchain/boxes.rs
  • ergo-api/src/blockchain/transactions.rs
  • ergo-api/src/lib.rs
  • ergo-api/src/server.rs
  • ergo-api/src/v1/auth.rs
  • ergo-api/src/v1/cursor.rs
  • ergo-api/src/v1/error.rs
  • ergo-api/src/v1/governor.rs
  • ergo-api/src/v1/mod.rs
  • ergo-api/src/v1/routes/chain.rs
  • ergo-api/src/v1/routes/dto.rs
  • ergo-api/src/v1/routes/mod.rs
  • ergo-api/src/v1/routes/transactions.rs
  • ergo-api/tests/v1_chain_tx_routes.rs

Comment thread ergo-api/src/v1/auth.rs
Comment thread ergo-api/src/v1/error.rs Outdated
Comment thread ergo-api/src/v1/governor.rs Outdated
Comment thread ergo-api/src/v1/governor.rs Outdated
Comment thread ergo-api/src/v1/mod.rs
Comment thread ergo-api/src/v1/routes/transactions.rs
arkadianet pushed a commit that referenced this pull request Jul 7, 2026
SECURITY:
- auth.rs: require_tier fails CLOSED on NoKeyConfigured for Operator/Admin
  (401 unauthorized), Admin denies before the loopback check. Public unaffected.
  Matches the compat require_api_key credential-reject posture; an always-mounted
  admin gate (shutdown/secret-export) must DENY when no key is configured. The
  boot-warn stays a signal, not the gate.
- mod.rs: is_trusted_loopback() is the single loopback-privilege authority; new
  local_reverse_proxy flag (default false) on GovernorConfig + V1AuthConfig
  withdraws socket-derived loopback trust (governor exemption AND Admin
  loopback-preferred) when an operator declares a local reverse proxy — no longer
  inferable from a proxy's socket. XFF not consulted.

CORRECTNESS:
- governor.rs: Governor::new -> Result<_, GovernorConfigError>; GovernorConfig::
  validate rejects non-finite/non-positive refill+burst and non-finite/negative
  weights before any bucket math.
- governor.rs: prune recomputes each bucket's virtual refill before retain, so
  idle-drained buckets that virtually refilled to full actually drop
  (max_tracked_ips is now an effective bound).
- error.rs: Reason::RouteDisabled -> RouteUnavailable (wire route_unavailable,
  503) — resolves the _disabled->409 vs _unavailable->503 suffix-rule
  inconsistency; 'route/bridge not wired' is transient = _unavailable.

parse_id32 lowercase-only SKIPPED (superseded by #171 case-insensitive; also
doesn't exist on this base branch). +14 unit tests (auth fail-closed matrix,
governor validation/pruning/proxy, is_trusted_loopback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
@arkadianet
arkadianet force-pushed the feat/v1-api-read-gap-chain-tx branch 2 times, most recently from c447f44 to 3e338c1 Compare July 7, 2026 18:08
@arkadianet
arkadianet changed the base branch from main to feat/v1-api-g2-primitives July 7, 2026 22:23
@arkadianet
arkadianet changed the base branch from feat/v1-api-g2-primitives to main July 7, 2026 23:27
arkadianet added a commit that referenced this pull request Jul 8, 2026
…governor, auth tiers (G2) (#168)

* feat(api): v1 error envelope + canonical reason enum (G2a)

The first of the four G2 shared primitives every /api/v1/* endpoint
inherits. A nested, machine-first error envelope
{error:{reason,message,detail}} (design v1-api-design.md §1.3) replacing
the frozen flat {reason,detail} compat shape, plus the canonical Reason
enum (§1.4 / coherence Part A) with a pure reason->HTTP-status mapping.

One enum, one spelling per concept, so a disabled subsystem answers its
*_disabled reason instead of a bare 404. 111 variants (110 canonical +
invalid_cursor, required by the §1.5 cursor codec). Status overrides off
the mechanical suffix rule are documented at each arm (unsupported_intent
->422, compiler/oracle_unavailable->501, timeout->504) and pinned by an
oracle-parity table test against the spec.

Not mounted on any route; the first route-group PR consumes it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* feat(api): v1 opaque versioned cursor codec + page builder (G2b)

The one cursor-pagination codec (design §1.5, coherence C.4): every v1
collection is cursor-paginated (?limit=&cursor=, never offset on the
wire). Wire form is base64url-nopad(version-byte || compact-JSON payload),
generic over a small CursorPayload each group picks (height key, global
index, mempool keyset, ...).

Opaqueness is load-bearing (locked decision D5): because clients treat
the cursor as opaque, Phase-2 can swap a Phase-1 offset-alias payload for
a stable-seek key without breaking clients; the version byte makes an
incompatible future encoding fail closed (invalid_cursor) rather than
mis-seek. Includes clamp_limit (per-group default/max from §2.2) and
Page::from_overfetch (overfetch-by-one has_more, no total needed).

Adds base64 (0.22, already in Cargo.lock via ergo-compiler) to ergo-api
— no new crate. Tamper/version/shape rejections tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* feat(api): v1 per-IP rate/cost governor middleware (G2c)

The load-bearing control behind the T0 'public but bounded' contract
(design §2.1–§2.2): every open surface sits behind a per-IP token bucket
whose refill/burst are config knobs, and each RouteClass (cheap read /
heavy read / compute) consumes a class-weighted number of tokens. A
depleted bucket answers 429 rate_limited (the §1.3 envelope) with a
Retry-After header; the operator's own UI (loopback) is exempt by default.

Dependency-light: a std HashMap<IpAddr,Bucket> behind a std Mutex,
refilled lazily on access (no timer task), with opportunistic pruning of
idle full buckets. Client IP comes from ConnectInfo (added shared
client_ip helper; XFF deliberately untrusted); a connect-info gap falls
back to one shared 'unknown' bucket, never an exemption. Attaches per
subtree via from_fn_with_state(gov.state(class), governor_mw).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* feat(api): v1 T0/T1/T2 auth tier split + boot-warn (G2d)

The exposure-tier gate (design §2.1): Public (no gate — governor-bounded),
Operator (valid api_key), Admin (api_key AND loopback-preferred). Reuses
the EXISTING api-key scheme — factored the Blake2b-256 + constant-time
compare into ApiSecurity::verify and pointed the frozen compat gate
(require_api_key) at it too, so there is one credential path node-wide.
The v1 gate answers the §1.3 envelope (unauthorized/401) rather than the
legacy 403 shape.

Admin is warn-and-allow off-loopback by default (logged loudly), with a
config flag to hard-deny remote admin ops (sensitive_op_disabled). Adds
the startup boot-warn: assess_posture (pure, tested predicate) +
warn_startup_posture, which screams when T1/T2 are network-reachable
under no key or a known weak/default key (the shipped 'hello' template
value). Not wired into server startup here (this PR changes no server
behavior) — exported with its call site documented for the next PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* chore(review): address CodeRabbit #169 G2-foundation findings

SECURITY:
- auth.rs: require_tier fails CLOSED on NoKeyConfigured for Operator/Admin
  (401 unauthorized), Admin denies before the loopback check. Public unaffected.
  Matches the compat require_api_key credential-reject posture; an always-mounted
  admin gate (shutdown/secret-export) must DENY when no key is configured. The
  boot-warn stays a signal, not the gate.
- mod.rs: is_trusted_loopback() is the single loopback-privilege authority; new
  local_reverse_proxy flag (default false) on GovernorConfig + V1AuthConfig
  withdraws socket-derived loopback trust (governor exemption AND Admin
  loopback-preferred) when an operator declares a local reverse proxy — no longer
  inferable from a proxy's socket. XFF not consulted.

CORRECTNESS:
- governor.rs: Governor::new -> Result<_, GovernorConfigError>; GovernorConfig::
  validate rejects non-finite/non-positive refill+burst and non-finite/negative
  weights before any bucket math.
- governor.rs: prune recomputes each bucket's virtual refill before retain, so
  idle-drained buckets that virtually refilled to full actually drop
  (max_tracked_ips is now an effective bound).
- error.rs: Reason::RouteDisabled -> RouteUnavailable (wire route_unavailable,
  503) — resolves the _disabled->409 vs _unavailable->503 suffix-rule
  inconsistency; 'route/bridge not wired' is transient = _unavailable.

parse_id32 lowercase-only SKIPPED (superseded by #171 case-insensitive; also
doesn't exist on this base branch). +14 unit tests (auth fail-closed matrix,
governor validation/pruning/proxy, is_trusted_loopback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

---------

Co-authored-by: arkadianet <rkadias@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
arkadianet and others added 4 commits July 8, 2026 16:22
The v1 `transactions/{tx_id}` handler reuses the existing extra-index
projection pipeline (`build_indexed_tx_response` / `build_indexed_box_response`)
rather than re-deriving box dereferencing and confirmation math, so the
confirmed-tx read shape can never drift from `/blockchain/transaction/byId`.
Widen the two builders from `pub(super)` to `pub(crate)` so the `v1` module
can call them; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…e group

Mount the highest-priority self-sufficiency surface (`dev-docs/v1-api-design.md`
§3.5–§3.6) on the G2 shared primitives, and wire `/api/v1` into the server for
real. This is the first consumer of the v1 error envelope, cursor page builder,
and rate/cost governor, so it sets the handler/DTO/test template later groups
copy.

Endpoints (all T0, Phase-1, `HeavyRead` governor class):
- chain/* (11): blocks list (cursor-paginated full history), block-by-id,
  block transactions, blocks at-height, blocks by-ids (POST, cap 200),
  headers list, header-by-id, headers at-height, modifiers-by-id (adds the
  explicit `kind` discriminant the untagged Scala BlockSection lacks),
  ad-proofs section, and the tx Merkle membership proof (side byte → left/right).
- transactions/{tx_id}: unified confirmed (extra-index) + unconfirmed (mempool
  overlay) read; `transactions/{submit,check}`: canonical submit paths (O1).

Conventions enforced: snake_case glossary field names verbatim, `value` as
string, `<name>_unix_ms` + `<name>_iso` timestamps (self-contained ISO helper,
oracle-pinned to known unix epochs), `{items, page}` collection envelope,
`{error:{reason,message,detail}}` with canonical reasons, and honest
`*_unavailable` / `*_disabled` gating instead of a bare 404 for a subsystem
that is off. The fee-proposition ErgoTree is oracle-pinned to the mainnet
test-vector fixture (no `ergo-mempool` dep pulled into the API crate).

Server wiring: `v1_router` merged under `/api/v1` with one shared governor per
node (later groups reuse the same per-IP budget); `warn_startup_posture` called
at the documented startup seam; the server now serves with
`into_make_service_with_connect_info` so the governor / auth tier can read the
real peer IP.

24 route-level integration tests lock the envelope, page, reason, and exact
field-name contracts. Gate green: fmt + clippy (-D warnings) + tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…available)

Governor::new now returns Result (config validation); default config is
statically valid → expect. Reason::RouteDisabled renamed RouteUnavailable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…Rabbit #169)

parse_id32 length-checked then hex::decode'd, which accepts uppercase/mixed-case
hex — decoding to identical bytes but leaving the tx-id validator misaligned with
the chain routes' `valid_modifier_id` (64-char LOWERCASE hex). Reuse that shared
validator so a non-canonical id is a `400 invalid_tx_id`, never a lookup. Adds a
unit test pinning lowercase-accept / uppercase-and-mixed-reject / bad-len / non-hex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
@arkadianet
arkadianet force-pushed the feat/v1-api-read-gap-chain-tx branch from 3e338c1 to 2cfcc40 Compare July 8, 2026 06:25
@arkadianet
arkadianet merged commit d95f725 into main Jul 8, 2026
9 checks passed
@arkadianet
arkadianet deleted the feat/v1-api-read-gap-chain-tx branch July 8, 2026 07:08
arkadianet pushed a commit that referenced this pull request Jul 8, 2026
…dr chain guard, token capped

Four verified findings on the boxes/tokens/addresses group:

- mod.rs parse_id32: enforce canonical lowercase hex via the shared
  `valid_modifier_id`, so `/boxes/*` and `/tokens/*` reject uppercase/mixed-case
  consistently with the tx routes (`hex::decode` alone accepts uppercase).
- boxes.rs render_unspent_page: the offset cursor advances the CONFIRMED window,
  but `has_more` was read from the post-`exclude_spent` merged count — a pool-spent
  view filter could drop the (limit+1)th sentinel and report no next page, silently
  truncating paging. Capture the confirmed-overfetch signal BEFORE the retain and
  page from it (new `offset_page_explicit`; `offset_page` now delegates to it).
- addresses.rs address transactions: guard the chain reader when there ARE txs to
  build, so a missing reader is an honest `chain_reader_unavailable` instead of a
  500 from `build_indexed_tx_response`. Empty results need no chain (authoritative
  from the index), so the guard is scoped to the non-empty case.
- tokens.rs scan_token_holders: base `capped` on the actual unspent scan hitting
  the cap, not `token_total_boxes` (which counts SPENT + unspent) — the latter
  wrongly flagged a COMPLETE scan as capped for tokens with many spent but few
  unspent boxes. Conservative: an exactly-cap unspent set may over-report, never
  under.

Skipped (with reason): the tx_by_id/indexer() dedup nitpick (pure refactor of
already-merged #169 code, out of scope for this PR) and the token_holders scan
cache (a perf feature needing keyed+height-invalidated cached state, not a
minimal fix; the scan is already bounded by HOLDER_SCAN_CAP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
arkadianet added a commit that referenced this pull request Jul 8, 2026
…hape) (#170)

* refactor(api): expose blockchain pool-overlay + address helpers as pub(crate)

The v1 boxes/tokens/addresses group reuses the confirmed-side readers and the
unconfirmed pool-output overlays already proven on the /blockchain/* surface,
rather than re-deriving them. Widen the (non-compat) helpers the new group needs
to pub(crate) and re-export them — mirroring the prior group's builder exposure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* feat(api): extend shared V1Box + add box/token/address v1 DTOs

The boxes group owns the canonical box shape, so it extends the existing shared
V1Box rather than defining a second: `decoded` becomes omit-unless-requested
(§0.3), and a canonical projector detects the pool sentinel (inclusion_height==0)
to render an unconfirmed box with null on-chain metadata (§0.4). Adds V1Token,
V1Balance, V1AddressTxSummary, the holders/stats DTOs, and CollectionMeta (the
{items,page,meta} envelope, Part D). Shape locked by unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* feat(api): v1 boxes/* + tokens/* + addresses/* reads — second route group

Implements §3.7 Phase-1 endpoints on the G2 primitives: box by-id/by-address/
by-ergo-tree/by-template/by-token (+unspent variants) + global-index range;
token by-id/holders/stats + honest state_unavailable for the not-yet-indexable
mint-order list (G3); address balance/transactions, with boxes/unspent dual-
mounted at addresses/* (O10, one handler). v1 mounts unconditionally and gates
inside each handler (indexer_disabled/_syncing/_halted), never a bare 404 for a
disabled subsystem. Single by-id reads are CheapRead; paginated/scan/range are
HeavyRead. 22 route-level tests lock envelope/page/reason/field-names + the
dual-mount agreement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* chore(rebase): adapt boxes group to G2 Governor::new Result

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* fix(api): address CodeRabbit #170 — parse_id32 case, boxes paging, addr chain guard, token capped

Four verified findings on the boxes/tokens/addresses group:

- mod.rs parse_id32: enforce canonical lowercase hex via the shared
  `valid_modifier_id`, so `/boxes/*` and `/tokens/*` reject uppercase/mixed-case
  consistently with the tx routes (`hex::decode` alone accepts uppercase).
- boxes.rs render_unspent_page: the offset cursor advances the CONFIRMED window,
  but `has_more` was read from the post-`exclude_spent` merged count — a pool-spent
  view filter could drop the (limit+1)th sentinel and report no next page, silently
  truncating paging. Capture the confirmed-overfetch signal BEFORE the retain and
  page from it (new `offset_page_explicit`; `offset_page` now delegates to it).
- addresses.rs address transactions: guard the chain reader when there ARE txs to
  build, so a missing reader is an honest `chain_reader_unavailable` instead of a
  500 from `build_indexed_tx_response`. Empty results need no chain (authoritative
  from the index), so the guard is scoped to the non-empty case.
- tokens.rs scan_token_holders: base `capped` on the actual unspent scan hitting
  the cap, not `token_total_boxes` (which counts SPENT + unspent) — the latter
  wrongly flagged a COMPLETE scan as capped for tokens with many spent but few
  unspent boxes. Conservative: an exactly-cap unspent set may over-report, never
  under.

Skipped (with reason): the tx_by_id/indexer() dedup nitpick (pure refactor of
already-merged #169 code, out of scope for this PR) and the token_holders scan
cache (a perf feature needing keyed+height-invalidated cached state, not a
minimal fix; the scan is already bounded by HOLDER_SCAN_CAP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

---------

Co-authored-by: arkadianet <rkadias@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant