Skip to content

feat(API): refactor merge http servlets - #20

Open
SeriousCoding789 wants to merge 28 commits into
SeriousCoding789:developfrom
Little-Peony:refactor_merge_http_servlets
Open

feat(API): refactor merge http servlets#20
SeriousCoding789 wants to merge 28 commits into
SeriousCoding789:developfrom
Little-Peony:refactor_merge_http_servlets

Conversation

@SeriousCoding789

@SeriousCoding789 SeriousCoding789 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Implements tronprotocol#6922.

develop serves the four HTTP surfaces (FULL, SOLIDITY, PBFT, and the standalone SolidityNode) with parallel sets of servlets and four hand-written registration lists. This PR collapses them onto one servlet per endpoint:

  • Deletes the 100 per-surface servlets (*OnSolidityServlet, *OnPBFTServlet, http/solidity/*SolidityServlet). 98 of them carry no logic at all — the whole class body is walletOnSolidity.futureGet(() -> super.doGet(req, resp)), i.e. a class per endpoint whose only job is to switch the read cursor.
  • Replaces them with a cursor filter (WalletCursorFilter + SolidityCursorFilter / PbftCursorFilter) mounted on /* of the solidity and pbft ports. Switching the cursor is a per-request, thread-level concern; it does not need a subclass per endpoint.
  • Replaces the four registration lists with @HttpApi / @HttpApiExcluded declared on the servlet itself, and derives a read-only HttpApiRegistry from them by classpath scan. Adding an endpoint becomes a one-place change instead of a four-place change.
  • The registry is validated at startup, before Jetty binds. Duplicate or malformed suffixes, a non-READ endpoint on a cursor surface, a servlet declaring neither annotation or both, an endpoint declared on a nested or abstract class, and a missing @Component all fail the node with TronError(API_SERVER_INIT) instead of silently dropping an endpoint.

356 files changed, +2136 / −4751.

Why are these changes required?

Duplicating an endpoint across surfaces is not free — it drifts silently, and two live examples on develop came out of this work:

  • The standalone SolidityNode's own copy of gettransactioninfobyid never picked up the visible=trueconvertLogAddressToTronAddress step the base servlet has, so it returns log[].address in hex where FullNode returns base58.
  • The PBFT registration list has been out of step with the other three surfaces for years: 5 sapling endpoints that were taken off FULL/SOLIDITY in 2020 stayed active on PBFT, and 2 read endpoints the other three surfaces expose were never mounted there.

Both are "change one place, forget the other" bugs. With one servlet per endpoint and a derived registry, a surface can no longer fall behind on its own.

Behaviour differences vs develop

Every per-surface servlet was classified by whether its body contains futureGet: 98 pure cursor delegations (cannot drift) and 2 hand-copied implementations (can). Per-surface result:

Surface Per-endpoint logic Endpoint set
FULL unchanged 120 → 120, no change
SOLIDITY equivalent (delegation → filter) 44 → 44, no change
SOLIDITY_NODE 2 endpoints differ, see below 44 → 44, no change
PBFT equivalent (delegation → filter) 47 → 44, −5 / +2

Client-visible changes, all deliberate and worth a release note:

  1. Standalone SolidityNode /walletsolidity/gettransactioninfobyid — with visible=true on a transaction that has logs, log[].address changes from hex to Tron base58, matching FullNode. This is the drift fix above; visible=false and log-free transactions are unaffected.
  2. PBFT port drops 5 sapling endpointsgetmerkletreevoucherinfo, isspend, scanandmarknotebyivk, scannotebyivk, scannotebyovk now return 404 on /walletpbft/*. They were disabled on every other surface in 2020; PBFT is catching up, not regressing.
  3. PBFT port gains 2 read endpointsgetpaginatednowwitnesslist and gettransactioninfobyblocknum, which FULL / SOLIDITY / SolidityNode already expose. Pure addition.

Error responses on the two former SolidityNode copies now go through Util.processError (the standard {"Error": ...} body) instead of writing the raw exception message into the response.

This PR has been tested by:

  • Unit Tests — 24 new tests, all passing:

    • HttpApiRegistryTest (17). Twelve drive one validation branch each through a fixture package under http/regtest/* and assert the boot failure it produces: a servlet declaring neither annotation or both, an endpoint on a nested or abstract class, a duplicate (surface, suffix), a blank suffix, a / in a suffix, a * or whitespace suffix, a missing @Component, an empty surface list, and a non-READ endpoint on a cursor surface. One builds a valid fixture package. The remaining four are the mount-parity tests: each service's mounted path set equals the registry's derived set for its surface, so an endpoint cannot be declared and left unmounted, or mounted without being declared.
    • CursorFilterInstallationTest (4) runs each service's real addFilter and asserts solidity and pbft install exactly one cursor filter on /*, while the FULL port and the standalone SolidityNode install none — a cursor filter on either of those would take the port off HEAD.
    • WalletCursorFilterTest (3) asserts the cursor is set before chain.doFilter and reset in finally including when the servlet throws, and that the PBFT subclass switches to the PBFT cursor rather than SOLIDITY.

    There is no frozen route snapshot: the registry is validated in full when the class is first touched — before Jetty binds, and for every surface whether or not the node enables it — so the invariants are enforced at boot and the tests drive the failure paths rather than re-asserting them over the live table.

  • Manual Testing — brought up a private chain and checked the mounted endpoint set on every port, including that endpoints marked @HttpApiExcluded are unreachable.

Follow up

  • Grouping the servlet package by function, as raised in the issue discussion. Kept out of this PR so the diff stays a mechanical de-duplication and the endpoint set remains directly diffable; worth doing once this lands.
  • Publishing the per-endpoint inventory and the PBFT surface changelog alongside the release note.

Extra details

@HttpApi / @HttpApiExcluded are deliberately not @Inherited, and the registry reads them with getDeclaredAnnotation only. Inheritable exposure is exactly what produced the 100 wrapper classes this PR removes — a subclass must never silently inherit its parent's surface set.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: ffc9b235-aa4e-468a-9acb-faf21de26da9


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.

@Little-Peony
Little-Peony force-pushed the refactor_merge_http_servlets branch 2 times, most recently from 124abb6 to 04a92f7 Compare August 24, 2026 09:58
@SeriousCoding789 SeriousCoding789 changed the title Refactor merge http servlets feat(API): refactor merge http servlets Aug 26, 2026
lvs0075 and others added 23 commits August 31, 2026 16:43
…-publish-jdk8

fix(build): skip errorprone publishing on JDK 8
Replace the hand-written HttpApiDef enum with HttpApiRegistry, built at
class-load from a single @httpapi(value, access, surfaces) declaration on
each servlet. The four http services mount from the derived registry, so an
endpoint is declared once and every surface stays in sync by construction.

- @httpapi is never @inherited and is read only via getDeclaredAnnotation,
  so a servlet subclass can never inherit its parent's exposure.
- Every concrete servlet in the package must declare @httpapi or
  @HttpApiExcluded; the registry fails the boot otherwise, before any Jetty
  bind, and also refuses to boot on an empty scan.
- The API x surface x access audit matrix is generated and checked against a
  committed snapshot; an independent fixture of the pre-refactor hand-written
  routes is the parity baseline, so the derived table cannot validate itself.

That baseline caught five shielded read endpoints (getmerkletreevoucherinfo,
scanandmarknotebyivk, scannotebyivk, scannotebyovk, isspend) that the enum had
dropped from the PBFT surface though they are live on develop; restored.

Tests: HttpApiRegistryTest with negative fixtures for each invariant, plus
WalletCursorFilterTest and CursorFilterInstallationTest pinning the read-cursor
filter behaviour and installation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
createshieldedcontractparameters and its withoutask variant compose an
unsigned transaction via wallet.createShieldedContractParameters*, so
they are BUILD endpoints; they were mislabeled Access.READ.
Replace the hand-written HttpApiDef enum with HttpApiRegistry, built at
class-load from a single @httpapi(value, access, surfaces) declaration on
each servlet. The four http services mount from the derived registry, so an
endpoint is declared once and every surface stays in sync by construction.

- @httpapi is never @inherited and is read only via getDeclaredAnnotation,
  so a servlet subclass can never inherit its parent's exposure.
- Every concrete servlet in the package must declare @httpapi or
  @HttpApiExcluded; the registry fails the boot otherwise, before any Jetty
  bind, and also refuses to boot on an empty scan.
- The API x surface x access audit matrix is generated and checked against a
  committed snapshot; an independent fixture of the pre-refactor hand-written
  routes is the parity baseline, so the derived table cannot validate itself.

Five sapling shielded note-scan endpoints (getmerkletreevoucherinfo,
scanandmarknotebyivk, scannotebyivk, scannotebyovk, isspend) were live on
develop's PBFT port only while disabled on every other surface; they stay
disabled here too (@HttpApiExcluded), aligning all four surfaces. The parity
test records this as an intentional PBFT removal vs the develop baseline.

Tests: HttpApiRegistryTest with negative fixtures for each invariant, plus
WalletCursorFilterTest and CursorFilterInstallationTest pinning the read-cursor
filter behaviour and installation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Migration correctness is verified by diffing against upstream, so the repo
no longer carries the frozen fixtures: remove pre-refactor-routes.txt and
api-audit-matrix.txt, their two tests in HttpApiRegistryTest, and the now
unused HttpApiRegistry.auditMatrix(). The rule-based invariant tests
(non-READ is FULL-only, @inherited guard, completeness net, suffix and
surface rejections) stay.

Also adds nested / whitespace / wildcard probe fixtures under regtest.
An adversarial review of HttpApiRegistry found four ways a mistaken annotation
could reach a running node instead of failing the boot.

- Suffix syntax. The suffix is concatenated into a jetty path spec, but only
  '/' and blank were rejected. "*" passed validation and mounted the servlet as
  the prefix wildcard /wallet/*, swallowing every sibling endpoint and, because
  a wildcard match rewrites getServletPath(), slipping past the filters that key
  off it; a suffix carrying whitespace was checked with trim() but stored raw,
  mounting an endpoint at a path no client can request. Both are now rejected by
  a single path-token rule [A-Za-z0-9_.-]+, which all 120 live endpoints already
  satisfy (four of them are camelCase, so the rule is not lowercase-only).

- Unmountable endpoint declarations. A servlet that is abstract, or nested
  inside another class, was dropped before its annotations were read, so an
  @httpapi on one produced no endpoint and no error — the silent omission this
  registry exists to prevent, and invisible to the completeness test because it
  iterated the same filtered list. Such classes are now scanned and must declare
  neither annotation.

- Spring stereotypes. The @component check only accepted the literal
  annotation, so a servlet marked with a meta-annotated stereotype would have
  aborted the boot despite being a valid bean. It now uses get semantics
  (direct + meta, never inherited from a superclass).

- Boot failure path. HttpService.start() calls addServlet() synchronously, so a
  registry failure escaped ServiceContainer's TronError wrapper and never
  reached the logged System.exit. Registry failures are now raised as
  TronError(API_SERVER_INIT); validation itself still throws IllegalStateException
  so it stays unit-testable.

Adds negative tests for the wildcard, whitespace and nested cases, and asserts
every live suffix satisfies the path-token rule.
These two assemble ShieldedTRC20Parameters for a caller to put into a
transaction, so they are BUILD rather than READ. An earlier commit had already
corrected this; the annotation refactor reverted it while aligning access
values against the old HttpApiDef enum, which was the source of the mistake.
@Little-Peony
Little-Peony force-pushed the refactor_merge_http_servlets branch from 4c26109 to 9e23145 Compare September 1, 2026 03:08
HttpApiRegistry validates the whole table when it is first touched, so
six of the tests could not fail on their own: any violation aborts the
static initializer and takes every other test with it.

Removed: the non-empty smoke test, the @inherited guard (buildFromPackage
asserts it in production code), and the four sweeps over the live
registry for FULL-only access, @component, exactly-one-annotation and
suffix syntax — all four re-check what validate() throws on while the
table is being built.

The rejection tests keep the real coverage: each drives one validation
branch through a fixture package and asserts the boot failure message.
The four mount-parity tests, which pin that each service mounts exactly
the registry's set for its surface, are untouched.

all(), Entry#getAccess() and scanConcreteServlets() had no caller left
outside those tests, so the registry drops them and Entry stops carrying
an access field nothing reads.

Also drops WalletCursorFilterTest#testCursorIsResetOnEverySequential-
Request: calling a stateless filter twice adds nothing over the single
set-then-reset ordering test next to it.
…ched

Jetty's ConnectionLimit suspends accept() on every connector when the cap is
hit and never re-enables it while slow clients hold their sockets, so 50 idle
connections per port block all HTTP clients. Its own throttle branch only
runs when an idle timeout is set; set 5s so existing connections drain and
the acceptor resumes. Normal traffic is unaffected: the value applies only
while over the limit and Jetty restores the connector default afterwards.
… set

Gate the pbft variant of gettransactioninfobyblocknum, which was mounted on
the pbft surface without a matching filterPaths entry while its full and
solidity variants were already gated. Drop the five sapling shielded note-scan
suffixes on all three surfaces: their servlets are @HttpApiExcluded and the
paths are no longer mounted anywhere, so the entries were dead configuration.
Every remaining entry now corresponds to a mounted endpoint, and every gated
suffix is gated on each surface that mounts it.

@Component
@Slf4j(topic = "API")
@HttpApi(value = "getaccount", access = Access.READ,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MUST] The rate limiter is keyed by the servlet class name, so all surfaces now share one limiter instance.

Before this PR each surface had its own wrapper servlet class (e.g. GetAccountOnSolidityServlet, GetAccountOnPBFTServlet), so the http_<class> keys were distinct and every port had its own permit pool. After the merge, a single GetAccountServlet bean is mounted on /wallet, /walletsolidity and /walletpbft — the same simple name resolves to the same RateLimiterContainer entry, giving one shared QPS budget across three ports. Total capacity drops from 3×qps to 1×qps.

Also, existing rate.limiter.httpMap entries referencing the old *OnSolidity / *OnPBFT class names no longer match anything and are silently ignored (addRateContainer falls back to the default adapter).

Suggestion: key the limiter by surface + suffix (or request context path), and/or document the httpMap key migration in the PR.

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.

5 participants