feat(api): v1 mempool/* group + O4 depth ring — closes the read-gap (+ UI migration) - #171
Conversation
|
Warning Review limit reached
Next review available in: 32 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (22)
📝 WalkthroughWalkthroughThis PR adds a new ChangesV1 Product API
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Server as server.rs
participant V1Router as v1_router
participant Governor as governor_mw
participant Route as v1 handler
Client->>Server: request /api/v1/...
Server->>V1Router: merge mounted router
V1Router->>Governor: apply RouteClass middleware
Governor->>Route: allow request
Route-->>Client: JSON response or v1_error
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (6)
ergo-api/src/v1/routes/chain.rs (1)
144-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSimplify the discard pattern.
if let Some(bad) = ... { let _ = bad; return invalid_hex(); }can be replaced with.any(...)since the matched id is unused.♻️ Proposed simplification
- if let Some(bad) = ids.iter().find(|id| !valid_modifier_id(id)) { - let _ = bad; - return invalid_hex(); - } + if ids.iter().any(|id| !valid_modifier_id(id)) { + return invalid_hex(); + }🤖 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/src/v1/routes/chain.rs` around lines 144 - 147, The `ids.iter().find(...)` result is only used to check for the presence of an invalid modifier id, and the matched value is discarded in `chain.rs`’s validation block. Replace the `if let Some(bad)` pattern in that section with a direct `.any(...)` check against `valid_modifier_id` so the code expresses the intent without binding an unused variable, while keeping the same `invalid_hex()` early return behavior.ergo-api/src/v1/routes/transactions.rs (1)
285-292: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueUnnecessary intermediate allocation in fee computation.
Collecting into a
Vec<(String, u64)>then re-mapping to(&str, u64)just to satisfyfee_from_hex_values's lifetime bound is more roundabout than needed;hex::encodeoutput could be sliced directly with.as_str()inline in a single pass, orfee_from_hex_valuescould take an owned-string iterator.♻️ Proposed simplification
- let fee = fee_from_hex_values( - tx.output_candidates - .iter() - .map(|c| (hex::encode(c.ergo_tree_bytes()), c.value)) - .collect::<Vec<_>>() - .iter() - .map(|(t, v)| (t.as_str(), *v)), - ); + let fee: u64 = tx + .output_candidates + .iter() + .filter(|c| hex::encode(c.ergo_tree_bytes()) == FEE_PROPOSITION_ERGO_TREE_HEX) + .map(|c| c.value) + .sum();🤖 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/src/v1/routes/transactions.rs` around lines 285 - 292, The fee calculation in transactions::fee_from_hex_values is doing an unnecessary Vec allocation and second pass just to convert encoded trees into borrowed strings. Simplify the tx.output_candidates iterator pipeline so it feeds fee_from_hex_values in a single pass without collecting first, either by passing the hex::encode result directly as a borrowed string where possible or by changing fee_from_hex_values to accept owned String values. Keep the change localized around the fee computation in the transactions route and preserve the existing fee behavior.ergo-api/src/v1/routes/mod.rs (1)
1-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRouter doc omits the mempool/* group it mounts.
The module and
v1_routerdoc comments describechain/*,transactions/*,boxes/*,tokens/*,addresses/*, but never mentionmempool/*— even though lines 299-367 mount an extensive mempool route set (summary, transactions, by-*, fee-histogram, submit/check aliases). Given this PR's stated purpose is adding the mempool group, the docs should reflect it.Also applies to: 284-293
🤖 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/src/v1/routes/mod.rs` around lines 1 - 13, The route-group documentation in the `mod.rs` module comment and the `v1_router` docs is missing the newly mounted `mempool/*` group. Update the comments near `v1_router` and the module header to explicitly include `mempool/*` alongside the other mounted groups, reflecting the full set of routes now returned by `v1_router` (including the mempool summary, transactions, by-*, fee-histogram, and submit/check aliases).ergo-api/tests/v1_mempool_routes.rs (1)
436-445: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
io_boxpopulation is never tested with real data.
detail_returns_row_plus_io_arraysonly exercisesNoopMempoolView, soinputs/outputsare always empty. Thetransactions/{tx_id}io_boxresolution — called out as a headline new feature in the PR objectives — has no test asserting real input/output rows are rendered.Consider adding a minimal
MempoolViewstub returning a populated input/output for at least one tx id to cover the resolution path.🤖 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_mempool_routes.rs` around lines 436 - 445, The existing `detail_returns_row_plus_io_arrays` test only covers `NoopMempoolView`, so it never verifies real `io_box` resolution for `transactions/{tx_id}`. Add a focused test alongside this one that uses a minimal `MempoolView` stub returning populated input/output data for a known tx id, and assert that `inputs` and `outputs` in the response contain rendered rows rather than just empty arrays. Use the `app()` and `get(...)` route setup from the current test as the reference point, and target the `io_box`/transaction detail path exercised by `detail_returns_row_plus_io_arrays`.ergo-api/tests/v1_boxes_tokens_addresses_routes.rs (1)
377-469: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared
StubRead(and harness helpers) into a common test-support module.This
StubReadimplementation ofNodeReadStateis duplicated almost verbatim acrossv1_boxes_tokens_addresses_routes.rs,v1_chain_tx_routes.rs, andv1_mempool_routes.rs(same fields, same boilerplate values). Thesend/get/reasonharness functions are likewise near-identical across the three files. Any future change toNodeReadStaterequires updating every copy in lockstep.Consider hoisting these into a shared
tests/common/mod.rs(ortests/support.rsincluded via#[path]) and importing it from each integration test binary.🤖 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_boxes_tokens_addresses_routes.rs` around lines 377 - 469, The `StubRead` implementation and the `send`/`get`/`reason` harness helpers are duplicated across multiple route test files, so factor them into a shared test-support module and reuse them from each integration test. Move the common `NodeReadState` stub and helper functions into a shared location such as `tests/common/mod.rs` or a `tests/support.rs` module, then update `v1_boxes_tokens_addresses_routes`, `v1_chain_tx_routes`, and `v1_mempool_routes` to import the shared symbols instead of maintaining separate copies.ergo-api/src/v1/routes/tokens.rs (1)
24-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffCache the bounded holder scan for repeated
/holdersand/statscalls
scan_token_holdersruns independently in both handlers, so pagination and stats requests replay the same bounded O(n) scan. Caching the(token_id, as_of_height)result would avoid repeating the CPU/I/O work on hot tokens.🤖 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/src/v1/routes/tokens.rs` around lines 24 - 30, `scan_token_holders` is being executed separately by the `/holders` and `/stats` handlers, so repeated requests re-run the same bounded scan work. Add a shared cache keyed by `(token_id, as_of_height)` around the bounded holder scan path and have both handlers read from it before invoking `scan_token_holders`; keep the cache lookup/store near the existing `HOLDER_SCAN_CAP`/`SCAN_BATCH` flow so hot-token pagination and stats reuse the same result.
🤖 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/server.rs`:
- Around line 1194-1223: The mempool-depth sampler is being started from the
router assembly path and its JoinHandle is dropped immediately, so repeated
calls to `server.rs` router setup can spawn duplicate detached tasks. Update the
`spawn_depth_sampler` usage in the `v1_mempool_depth` setup to keep the returned
handle somewhere owned by the process or to guard startup so it runs only once
per process, using the existing `MempoolDepthRing`/`spawn_depth_sampler` symbols
to locate the code.
In `@ergo-api/src/v1/routes/boxes.rs`:
- Around line 11-17: The v1 box routes still let extractor failures bypass the
standard error shape, so malformed JSON or invalid query parameters can return
Axum’s default rejection body instead of v1_error(...). Update the extraction
path used by boxes_by_ergo_tree and boxes_unspent_by_ergo_tree to wrap
body/query parsing in the v1 error envelope, either by introducing a v1
extractor wrapper or by explicitly handling JsonRejection and QueryRejection in
the route handlers so all failures return the same response format.
- Around line 211-261: The `render_unspent_page` flow can underfill pagination
when `exclude_mempool_spent` filters the overfetched confirmed results before
`offset_page`, causing `has_more` to be computed incorrectly. Fix this by
applying the spent-box filter inside the confirmed paging/fetch path (or by
continuing to overfetch until you have `limit + 1` surviving `IndexedErgoBox`
items) so `offset_page` receives a correctly sized window. Keep the change
localized around `render_unspent_page`, `fetch_confirmed`, and the
`exclude_spent` handling.
In `@ergo-api/src/v1/routes/mempool.rs`:
- Around line 90-97: MempoolCursor currently only stores the keyset values and
can be replayed across different order= requests, which can cause skipped or
duplicated rows. Update the cursor payload in mempool.rs’s MempoolCursor to
include the selected order, then validate that the decoded cursor order matches
the requested order before using it in the mempool route logic so mismatched
cursors fail closed.
In `@ergo-api/src/v1/routes/mod.rs`:
- Around line 143-150: The 64-char id validation in valid_modifier_id is
inconsistent with parse_id32, since one path enforces lowercase-only while the
other accepts mixed case. Update the route-level validation and related
docs/contracts used by the box/token/tx parsing flow so they all follow the same
rule: either reject uppercase everywhere or explicitly allow case-insensitive
ids everywhere, using valid_modifier_id and parse_id32 as the key entry points.
In `@ergo-api/src/v1/routes/tokens.rs`:
- Around line 107-165: The scan cap flag in scan_token_holders is being derived
from token_total_boxes, which includes spent boxes and can misreport capped
scans for high-turnover tokens. Update HolderScan/scan_token_holders so the
capped flag is determined from the unspent pagination behavior in
token_unspent_paged and the scan loop result, while leaving box_count based on
token_total_boxes; use scan_token_holders and HolderScan as the main places to
adjust.
---
Nitpick comments:
In `@ergo-api/src/v1/routes/chain.rs`:
- Around line 144-147: The `ids.iter().find(...)` result is only used to check
for the presence of an invalid modifier id, and the matched value is discarded
in `chain.rs`’s validation block. Replace the `if let Some(bad)` pattern in that
section with a direct `.any(...)` check against `valid_modifier_id` so the code
expresses the intent without binding an unused variable, while keeping the same
`invalid_hex()` early return behavior.
In `@ergo-api/src/v1/routes/mod.rs`:
- Around line 1-13: The route-group documentation in the `mod.rs` module comment
and the `v1_router` docs is missing the newly mounted `mempool/*` group. Update
the comments near `v1_router` and the module header to explicitly include
`mempool/*` alongside the other mounted groups, reflecting the full set of
routes now returned by `v1_router` (including the mempool summary, transactions,
by-*, fee-histogram, and submit/check aliases).
In `@ergo-api/src/v1/routes/tokens.rs`:
- Around line 24-30: `scan_token_holders` is being executed separately by the
`/holders` and `/stats` handlers, so repeated requests re-run the same bounded
scan work. Add a shared cache keyed by `(token_id, as_of_height)` around the
bounded holder scan path and have both handlers read from it before invoking
`scan_token_holders`; keep the cache lookup/store near the existing
`HOLDER_SCAN_CAP`/`SCAN_BATCH` flow so hot-token pagination and stats reuse the
same result.
In `@ergo-api/src/v1/routes/transactions.rs`:
- Around line 285-292: The fee calculation in transactions::fee_from_hex_values
is doing an unnecessary Vec allocation and second pass just to convert encoded
trees into borrowed strings. Simplify the tx.output_candidates iterator pipeline
so it feeds fee_from_hex_values in a single pass without collecting first,
either by passing the hex::encode result directly as a borrowed string where
possible or by changing fee_from_hex_values to accept owned String values. Keep
the change localized around the fee computation in the transactions route and
preserve the existing fee behavior.
In `@ergo-api/tests/v1_boxes_tokens_addresses_routes.rs`:
- Around line 377-469: The `StubRead` implementation and the
`send`/`get`/`reason` harness helpers are duplicated across multiple route test
files, so factor them into a shared test-support module and reuse them from each
integration test. Move the common `NodeReadState` stub and helper functions into
a shared location such as `tests/common/mod.rs` or a `tests/support.rs` module,
then update `v1_boxes_tokens_addresses_routes`, `v1_chain_tx_routes`, and
`v1_mempool_routes` to import the shared symbols instead of maintaining separate
copies.
In `@ergo-api/tests/v1_mempool_routes.rs`:
- Around line 436-445: The existing `detail_returns_row_plus_io_arrays` test
only covers `NoopMempoolView`, so it never verifies real `io_box` resolution for
`transactions/{tx_id}`. Add a focused test alongside this one that uses a
minimal `MempoolView` stub returning populated input/output data for a known tx
id, and assert that `inputs` and `outputs` in the response contain rendered rows
rather than just empty arrays. Use the `app()` and `get(...)` route setup from
the current test as the reference point, and target the `io_box`/transaction
detail path exercised by `detail_returns_row_plus_io_arrays`.
🪄 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: fb714446-3047-48af-a349-1b277d444d62
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (35)
ergo-api/Cargo.tomlergo-api/src/auth.rsergo-api/src/blockchain.rsergo-api/src/blockchain/balance.rsergo-api/src/blockchain/boxes.rsergo-api/src/blockchain/bytemplate.rsergo-api/src/blockchain/tokens.rsergo-api/src/blockchain/transactions.rsergo-api/src/blockchain/unspent_byaddress.rsergo-api/src/lib.rsergo-api/src/server.rsergo-api/src/traits.rsergo-api/src/v1/auth.rsergo-api/src/v1/cursor.rsergo-api/src/v1/error.rsergo-api/src/v1/governor.rsergo-api/src/v1/mempool_depth.rsergo-api/src/v1/mod.rsergo-api/src/v1/routes/addresses.rsergo-api/src/v1/routes/boxes.rsergo-api/src/v1/routes/chain.rsergo-api/src/v1/routes/dto.rsergo-api/src/v1/routes/mempool.rsergo-api/src/v1/routes/mod.rsergo-api/src/v1/routes/tokens.rsergo-api/src/v1/routes/transactions.rsergo-api/tests/fixtures/openapi_native.yamlergo-api/tests/mempool_source_schema.rsergo-api/tests/openapi_native_runtime_mount.rsergo-api/tests/submit_routes.rsergo-api/tests/v1_boxes_tokens_addresses_routes.rsergo-api/tests/v1_chain_tx_routes.rsergo-api/tests/v1_mempool_routes.rsergo-api/web/js/mempool.jsergo-node/src/api_bridge.rs
- extractor envelope (swept): shared V1Query/V1Json wrappers (v1/routes/extract.rs) replace 19 Query + 3 Json sites so malformed body/query returns the v1 envelope, not axum's default rejection. - pagination underfill: render_unspent_page overfetches until limit+1 rows SURVIVE the exclude-mempool-spent filter; cursor advances by rows consumed (no underfill/dupe). - cursor/order replay: MempoolCursor stamps the order tag, fails closed (invalid_cursor) on mismatch. - depth-sampler duplicate-spawn: AtomicBool once-guard (test suite builds routers many times under a live runtime). - scan_capped derived from the unspent scan hitting the cap, not spent-inclusive token_total_boxes. - id-case: valid_modifier_id relaxed to case-insensitive to match parse_id32 + the compat/wallet surfaces (ecosystem sends mixed-case hex). - chain.rs .any(); mod.rs docs (+mempool group); single-pass fee_from_hex_values; populated-MempoolView io_box detail test. - SKIPPED: holder-scan cache (design-deferred, #170); test-support dedup (premise false — stubs/ helpers diverge, a shared module trips -D warnings dead_code). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
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
- extractor envelope (swept): shared V1Query/V1Json wrappers (v1/routes/extract.rs) replace 19 Query + 3 Json sites so malformed body/query returns the v1 envelope, not axum's default rejection. - pagination underfill: render_unspent_page overfetches until limit+1 rows SURVIVE the exclude-mempool-spent filter; cursor advances by rows consumed (no underfill/dupe). - cursor/order replay: MempoolCursor stamps the order tag, fails closed (invalid_cursor) on mismatch. - depth-sampler duplicate-spawn: AtomicBool once-guard (test suite builds routers many times under a live runtime). - scan_capped derived from the unspent scan hitting the cap, not spent-inclusive token_total_boxes. - id-case: valid_modifier_id relaxed to case-insensitive to match parse_id32 + the compat/wallet surfaces (ecosystem sends mixed-case hex). - chain.rs .any(); mod.rs docs (+mempool group); single-pass fee_from_hex_values; populated-MempoolView io_box detail test. - SKIPPED: holder-scan cache (design-deferred, #170); test-support dedup (premise false — stubs/ helpers diverge, a shared module trips -D warnings dead_code). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
8235a68 to
32437b3
Compare
The #171 CodeRabbit wave added a populated-MempoolView detail test after the realtime group forked; its V1State construction needs the realtime: None field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…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>
Shared infra for the mempool/* group and the future stats/mempool-depth endpoint (Appendix A O4 — one ring, two surfaces). - MempoolDepthRing: bounded (512) FIFO ring with monotonic per-sample seq, mirroring the operator event ring; dependency-light (std VecDeque + Mutex), a passive store fed fully-formed observations so it is trivially unit-tested. - spawn_depth_sampler: background sampler (30s cadence) fed from mempool_summary + min-fee; the server wiring guards the spawn on a live runtime so non-async router builds never touch it. sample_into is exported for the stats consumer. - NodeReadState::mempool_weight_function(): defaulted (reads mempool_transactions so no implementer breaks), overridden O(1) in the node's SnapshotReadState so mempool/summary + fee-histogram don't clone the pool list for one enum field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
The last read-gap group: summary (+utilization, weight_function, O4 depth
history), cursor-paginated transactions, single-tx detail with io_box, the four
by-* filtered views, and the fee histogram — all on the v1 envelope + keyset
(priority_weight, tx_id) cursor + canonical reasons.
This REPLACES the pre-v1 native /api/v1/mempool/* handlers (they collided with
the v1 paths): summary/transactions[/{tx_id}] and the conditional submit/check
routes are removed from server.rs (handlers + utoipa paths + OpenAPI derive,
golden fixture regenerated) and ownership moves to v1_router. submit/check are
Overlap-O1 aliases: the SAME transactions::{submit,check} handlers, second
mount, now always-mounted answering 409 submit_disabled instead of a bare 404.
Corrections vs the fragment: source emits the REAL ApiTxSource taxonomy
(peer|api|wallet|demoted_from_block) as a flat string, not the speculative
propagated|local|... set; first_seen uses the §1.2 flat *_unix_ms/*_iso rule;
the fee-histogram fee_per_byte band is honestly null (the frozen wait-time hook
carries no per-bin fee bounds); by-* answer mempool_view_disabled when the
chain-reader bridge is absent.
Migrated the coupled tests to the v1 shapes (mempool_source_schema, submit_routes,
openapi_native_runtime_mount) + new tests/v1_mempool_routes.rs. Compat frozen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
/api/v1/mempool/transactions now returns {items,page} (v1 collection shape)
instead of the flat {transactions}; the summary fields (size/capacity_count/
total_bytes/capacity_bytes) are unchanged, so only the tx-list unwrap moves
.transactions -> .items. Keeps the mempool panel working across the API change
(design §6 UI migration, done incrementally per endpoint).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
- extractor envelope (swept): shared V1Query/V1Json wrappers (v1/routes/extract.rs) replace 19 Query + 3 Json sites so malformed body/query returns the v1 envelope, not axum's default rejection. - pagination underfill: render_unspent_page overfetches until limit+1 rows SURVIVE the exclude-mempool-spent filter; cursor advances by rows consumed (no underfill/dupe). - cursor/order replay: MempoolCursor stamps the order tag, fails closed (invalid_cursor) on mismatch. - depth-sampler duplicate-spawn: AtomicBool once-guard (test suite builds routers many times under a live runtime). - scan_capped derived from the unspent scan hitting the cap, not spent-inclusive token_total_boxes. - id-case: valid_modifier_id relaxed to case-insensitive to match parse_id32 + the compat/wallet surfaces (ecosystem sends mixed-case hex). - chain.rs .any(); mod.rs docs (+mempool group); single-pass fee_from_hex_values; populated-MempoolView io_box detail test. - SKIPPED: holder-scan cache (design-deferred, #170); test-support dedup (premise false — stubs/ helpers diverge, a shared module trips -D warnings dead_code). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
32437b3 to
8398c1c
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
The #171 CodeRabbit wave added a populated-MempoolView detail test after the realtime group forked; its V1State construction needs the realtime: None field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…webhooks follow-up) (#172) * feat(api): v1 real-time subscriptions — RealtimeBus + WS /api/v1/ws (§4.1, G5) The single biggest net-new of the v1 API: the push surface every Ergo node lacks today (everything polls). This ships the WS core + the load-bearing RealtimeBus fan-out hub; webhooks (the durable sibling) are a separate follow-up and are deliberately NOT built here. RealtimeBus (ergo-api/src/v1/realtime/) is a concrete shared type, mirroring the O4 depth-ring precedent rather than the fragment's "consumed trait" framing — the fan-out hub's drop policy, per-key filtering, and backfill window are directly unit-testable, and node-side taps simply call `publish`. Per subscriber it holds a bounded tokio mpsc queue (256 frames) + a shared filter; `publish` assigns the one global monotonic `seq`, retains the event in an 8192-entry resume window, and `try_send`s to every matching subscriber under a never-block drop policy: a full queue drops the event for that consumer and raises its `lagged` flag (the socket then closes `slow_consumer`), so one slow client can never stall the fan-out or the node. WS /api/v1/ws is a thin axum WebSocketUpgrade adapter over a transport-free Session state machine: the full frame protocol (welcome/ack/event/pong/ heartbeat/close/error/subscribe_rejected/unsubscribed/resync), subscribe/ unsubscribe/resume/ping/auth ops, per-channel partial-success rejects, selector validation (invalid_selector before the channel_unavailable liveness gate), terminal box:/tx: channels that fire once then auto-unsubscribe, control-op rate limiting, heartbeat/idle timeout, and binary rejection. T0 but bounded: a per-IP/global ConnLimiter checked pre-upgrade (429 connection_limit) plus the send-queue + rate caps. Upstream tap (honest Phase-1): a server-seam bridge task (runtime-guarded like the depth sampler) polls the existing coarse operator event ring and republishes block_applied/reorg into the bus `blocks` channel — one push path, no second event source. The fine-grained address/box/token/tx taps live in node internals and are a follow-up; until they land those classes are gated channel_unavailable. block_applied frames carry confirmed:true; the retraction event model (box_reverted/box_unspent/tx_dropped:reorged, previous_seq) is built and tested but only emitted once the fine-grained taps exist — reorg guarantee today is best-effort coarse-grained (blocks-channel reorg frame, no dropped- branch enumeration). Reuses the G2 primitives verbatim: the canonical Reason enum (realtime_disabled, channel_unavailable, invalid_selector, channel_limit, connection_limit, slow_consumer, idle_timeout, binary_unsupported, unknown_op, rate_limited), the error envelope, and the v1 DTO field names + unix_ms_to_iso timestamp rule. Test plan: cargo fmt --all --check (clean); cargo clippy --workspace --all-targets --all-features -D warnings (clean, no #[allow]); cargo test --workspace (green). 34 realtime tests: bus fan-out / per-key filtering / slow-consumer drop / backfill-gap / conn-limiter, the pure Session protocol (subscribe/reject/terminal/rate-limit/on_event), the coarse→bus projection + boot-seed, and a real end-to-end WS transport test (upgrade → welcome → subscribe → publish → event → ping/pong) over tokio-tungstenite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * chore(rebase): reconcile V1State realtime field with the #171-wave test The #171 CodeRabbit wave added a populated-MempoolView detail test after the realtime group forked; its V1State construction needs the realtime: None field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * fix(api): saturate the resume gap check for client-supplied since=u64::MAX `since` comes straight from the WS Resume frame, so `since + 1` was a remotely-triggerable overflow panic in debug builds (wrap in release). Saturating add keeps the gap semantics identical for all real cursors. Skipped the publish-lock nitpick: fan-out under the mutex is what keeps per-subscriber seq ordering, and every step is non-blocking. 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>
…scoping, v1 envelope extractors, id/helper dedup, mempool.js v1 fields - spawn_event_bridge_once: repeated router assembly in one runtime stacked bridge pollers; same AtomicBool idempotence as the depth sampler/webhook worker. - validate_url: allow_loopback waived require_https for ALL hosts; plaintext http is now excused only for loopback targets (+ regression test). - webhooks list/deliveries/patch_active: stock Query/Json answered malformed requests outside the v1 envelope; switched to V1Query/V1Json per §1.4. - is_id64 delegates to valid_modifier_id — it accepted uppercase hex the rest of v1 (and its own error text) rejects. - transactions.rs/tx_intel.rs: drop the duplicated parse_id32 and inline BlockchainState constructions for the shared helpers. - mempool.js: finish the #171 v1 migration — fee/fee_per_byte (strings) and summary.weight_function; the panel's fee columns and histogram read fields the v1 endpoint never serves. Skipped: tx-intel detail whitespace (already fixed in 8a0a6f1); chain.rs list_blocks overfetch-vs-pruned-bodies has_more (real, needs a bounded-scan design — follow-up); token-holders scan memoization (cache+invalidation complexity for an already-capped scan). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
) * feat(api): tx-intelligence node seams — non-mutating simulate + keyless builder (§4.2) Add the two boundaries the v1 transactions/* intelligence group needs, both honest-unavailable by default so a node that hasn't wired them answers a clean 503 rather than fabricating a result: - NodeSubmit::simulate — a NON-mutating dry-run (G8). Unlike SubmitMode::CheckOnly, which still mutates the mempool anti-DoS bookkeeping (invariant #7), this MUST NOT touch node state; a cleanly-invalid tx is a successful Ok(valid:false). Default impl returns route_disabled so existing NodeSubmit impls keep compiling. - NodeTxBuilder — the ONE keyless builder seam (O7). The production impl will delegate to the same selection/change/fee core the keyed wallet build uses; a second builder is never forked. Carries the keyless request/response/error types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * feat(api): v1 transactions/* intelligence — build/simulate/fee-estimate/status (§3.6) The "help me transact" endpoints on the v1 transactions group: - POST /transactions/build — keyless tx_intent -> unsigned tx over the O7 NodeTxBuilder seam; intent-shape gate rejects mint / payment-registers / raw boxes as 422 unsupported_intent, enforces caps + address/id validation, and answers route_unavailable when the builder is unwired (never fake selection). - POST /transactions/simulate — accept/reject + cost/fee/conflicts, no broadcast and no mempool mutation (G8); Ok(valid:false) is a 200. - GET /transactions/fee-estimate — REAL: mempool-derived 1/3/10-block fee tiers (nanoERG/byte strings) from the chain reader's pool_recommended_fee + floor. - GET /transactions/{tx_id}/status — REAL: confirmed (extra-index) precedes pooled (rank / ahead-bytes / competitiveness / eta); unknown id is a 200. build/simulate mount at the governor Compute class, fee-estimate/status at HeavyRead. 16 route tests (envelope/reason/field names, build happy-path with a stub builder, simulate accept+reject, fee-estimate shape, status pending/confirmed/unknown). V1State gains an Option tx_builder (prod None). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * fix(api): collapse literal whitespace runs in tx-intel error detail strings (CodeRabbit #178) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * fix(api): address CodeRabbit #178 — bridge once-guard, http-loopback scoping, v1 envelope extractors, id/helper dedup, mempool.js v1 fields - spawn_event_bridge_once: repeated router assembly in one runtime stacked bridge pollers; same AtomicBool idempotence as the depth sampler/webhook worker. - validate_url: allow_loopback waived require_https for ALL hosts; plaintext http is now excused only for loopback targets (+ regression test). - webhooks list/deliveries/patch_active: stock Query/Json answered malformed requests outside the v1 envelope; switched to V1Query/V1Json per §1.4. - is_id64 delegates to valid_modifier_id — it accepted uppercase hex the rest of v1 (and its own error text) rejects. - transactions.rs/tx_intel.rs: drop the duplicated parse_id32 and inline BlockchainState constructions for the shared helpers. - mempool.js: finish the #171 v1 migration — fee/fee_per_byte (strings) and summary.weight_function; the panel's fee columns and histogram read fields the v1 endpoint never serves. Skipped: tx-intel detail whitespace (already fixed in 8a0a6f1); chain.rs list_blocks overfetch-vs-pruned-bodies has_more (real, needs a bounded-scan design — follow-up); token-holders scan memoization (cache+invalidation complexity for an already-capped scan). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * fix(api): chain list pagination pages by height, not collected-row count A pruned/missing block body inside the overfetch window shrank the item count, so Page::from_overfetch reported has_more=false and falsely ended the listing (CodeRabbit #178). CodeRabbit's suggested scan-until-full is unbounded on a heavily pruned node, so instead has_more/next_cursor now derive from the height domain: the cursor advances to the last SCANNED height and the overfetched (limit+1)-th height is probed for a header — headers are dense on a synced chain, so presence there is exactly "another candidate row exists". Pruned gaps yield short (even empty) pages that keep advancing, bounded to limit+1 height probes per request. Applied to both list_blocks and list_headers via height_window_page. 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 Fable 5 <noreply@anthropic.com>
What this is
The
mempool/*v1 route group — the last read-gap piece. After this, a v1 client never needs the compat surface for reads.Unlike the prior additive groups, some flat
/api/v1/mempool/*handlers already existed inserver.rs(bareJson, no envelope/cursor — a pre-v1 stopgap). They collided with the v1 paths, so they're removed and ownership moves tov1_router; the OpenAPI golden was regenerated and 3 coupled tests migrated to the v1 shapes. Compat surface untouched. The only externally-visible shape change ismempool/transactions: flat{transactions}→ v1{items, page}; the web UI mempool panel is migrated in the same PR (one-line.transactions→.items; summary field names are unchanged, so the rest of the panel is untouched). Nothing breaks on merge.Endpoints (§3.8, all T0)
summary(+ utilization,weight_function, and the O4history),transactions(keyset-cursor list,?order=),transactions/{tx_id}(row +io_box),by-address,by-ergo-tree,by-box-id,by-token-id,fee-histogram, andsubmit/checkas O1 aliases (the same canonical handler from the chain/tx group, second mount — no duplicated logic).O4 — mempool-depth ring (shared infra)
New dependency-light
mempool_depth.rs: a 512-sample FIFO ring (Mutex<VecDeque>, monotonic seq) fed by a background sampler wired inserver.rs(guarded on a live Tokio runtime so test router builds don't spawn it). Lives onV1State, re-exported fromergo_api::v1, projected viaV1MempoolDepthPoint— the exact shape the futurestats/mempool-depthendpoint will consume from the same Arc (documented).Design/hook corrections (relay)
sourceemits the realApiTxSourcetaxonomy (peer|api|wallet|demoted_from_block) — the fragment'spropagated|local|…never matched the impl.first_seenuses the §1.2 flat*_unix_ms/*_isorule (not the fragment's nested object).fee_per_byte_min/maxis honestlynull(the frozen hook is wait-time-keyed, no per-bin fee bounds).409 submit_disabled(was conditional-404);by-*→mempool_view_disabledwhen the chain bridge is absent.Test plan
ergo-api/tests/v1_mempool_routes.rs(17 tests) + the migratedmempool_source_schema/submit_routes/openapi_native_runtime_mount. Compat untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
Summary by CodeRabbit
/api/v1product API with standardized error envelopes, cursor pagination, per-IP rate limiting, and tiered authentication.