Skip to content

Sync Trezor upstream while preserving Syscoin support - #32

Merged
sidhujag merged 304 commits into
masterfrom
codex/sync-blockbook-upstream
Oct 20, 2026
Merged

sidhujag merged 304 commits into
masterfrom
codex/sync-blockbook-upstream

Conversation

@sidhujag

@sidhujag sidhujag commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Merge Trezor Blockbook master at 3e6a90a287fb4e6d597978baf0aceeec985b5e41 into Syscoin, bringing in 301 upstream commits including the spending-outpoint fix, bounded filtered-history scans, transaction lookup improvements, and maintenance updates.

Resolve the merge while preserving Syscoin SPT parsing/indexing, Bridge V2 metadata and SPV proofs, fee/burn send options, and Pali transaction summaries. Adapt the new single-output DB reader to Syscoin records, count rejected asset matches against the scan budget, and preserve combined mempool/confirmed pagination. Descriptor scan exhaustion returns an explicit error rather than a successful partial history, including when longer indexed scripts share a complete address descriptor as a prefix. Regenerate the API types and align OpenAPI with existing Syscoin fields, base64 memos, and SPT token standards.

Validation:

  • Full Go unit suite passed in Docker with Go 1.25.4 and RocksDB 9.10.0 (54 test packages; 3,067 passing tests/subtests).
  • After the API schema corrections, API, server, and TypeScript-generator tests passed again, including the new memo/SPT wire-type regressions.
  • New regression coverage includes complete-descriptor prefix collisions at the scan limit, Syscoin basic/extended output decoding, normal/bulk indexing and reorg restoration, transaction-summary pagination, asset-filter scan limits, and broadcast parameter/metric handling.
  • Syscoin mainnet and testnet Blockbook HTTP connectivity checks passed. Live backend RPC/sync tests were not run because node endpoints are not configured locally.
  • OpenAPI lint, generated schema, and TypeScript typecheck passed (11 existing composition warnings).
  • 69 CI pipeline script tests and 22 Grafana renderer tests passed; dashboard consistency, coin JSON, backend artifact checks, and both-environment config tests passed.
  • Port registry and minified CSS regenerated; formatting and diff checks passed.

PR #31 remains separate: this branch does not enable -extendedindex or rebuild any deployed database. Hosted unit tests are queued on the self-hosted runner; local results above are complete.

pragmaxim and others added 30 commits July 9, 2026 10:31
…ndex

EthereumRPC.GetTransaction relied solely on eth_getTransactionByHash, which
returns null for mined transactions older than the backend's transaction-by-hash
retention window on some archive nodes (observed on QuikNode Base, now the Base
backend). Such transactions are still fully retained and reachable via
eth_getBlockByHash and eth_getTransactionReceipt, but blockbook reported them as
ErrTxNotFound, failing both production lookups and the base=main/rpc/GetTransaction
integration test.

Fall back to reconstructing the transaction from its receipt (for the block hash)
plus the full block body when eth_getTransactionByHash returns null. Genuinely
unknown transactions still return ErrTxNotFound, and the primary path is unchanged
for backends that retain the full index.

Fixes trezor#1609

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recoverMinedTransaction reconstructed a pruned-index transaction by fetching the
entire block body (eth_getBlockByHash, fullTxs=true) and scanning it for the hash.
On endpoints that call GetTransaction per tx (address/xpub/block/balancehistory),
a cold-cache page of transactions from the pruned window fetched and re-parsed the
full block body once per tx - the same ~592 KB block up to 1000x on a block page.

Fetch the transaction directly by its receipt's (blockHash, transactionIndex) via
eth_getTransactionByBlockHashAndIndex - an O(1), ~900x smaller lookup - and reuse
the already-fetched receipt for EthTxToTx instead of fetching it a second time.
Verified against the QuikNode Base archive backend: the by-index result is
byte-identical to the block-body scan across transactions 64-926 days old.

Fall back to the block-body scan when the positional lookup is unavailable, so the
change is strictly non-regressive for any backend, and give each recovery RPC its
own timeout budget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add <network>_ALLOWED_EVM_CALL_METHODS, a comma-separated list of 4-byte
method selectors that permits websocket rpcCall requests to any address
when the calldata starts with an allowed selector (e.g. 0xdd62ed3e for
ERC-20 allowance). Complements ALLOWED_RPC_CALL_TO: a call passes when
either its target address or its selector is allowed; with neither set
rpcCall stays unrestricted. Malformed calldata never matches (full hex
decode, fail closed) and malformed or empty selector config fails
startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turn ALLOWED_RPC_CALL_TO and ALLOWED_EVM_CALL_METHODS into runtime
settings: the environment variables remain the startup defaults, while
an override written through the authenticated internal API

  GET/POST/DELETE /admin/runtime-settings/<KEY>

is persisted in RocksDB (own cfDefault rows, independent of the
periodically stored internalState blob and invisible to older
versions), survives restarts, takes precedence over the environment
and takes effect on websocket rpcCall immediately, without a restart.

The live allowlists are an immutable snapshot behind an atomic pointer
on InternalState, so the rpcCall hot path stays lock-free and both
servers (public websocket, internal admin) share one view regardless
of which of them a deployment runs. Writes are strictly ordered:
validate (400 on invalid input) -> store to DB (500 and untouched live
state on failure) -> publish snapshot -> log the change with old/new
value, source and client address. POST of an exactly empty value
explicitly unconfigures a dimension; whitespace- or separator-only
values are rejected so a botched input cannot silently un-restrict
rpcCall; DELETE reverts to the environment default and validates it
first so a malformed environment cannot brick the next restart.

The TO-list parser is unified with the selector parser: entries are
trimmed, empty entries skipped and a set-but-empty value fails startup
(previously "," allowlisted the empty string, which matched rpcCall
requests with an empty to field).

A minimal /admin/runtime-settings page lists the current values and
their sources with curl hints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avoids allocating a decoded copy of the full calldata (up to 4 MiB) on
every rpcCall allowlist check. The full hex encoding is still validated
byte-by-byte to preserve the existing fail-closed behavior for malformed
calldata.
Addresses the API unification review on PR trezor#1597:

- POST /admin/contract-info/ returns a typed {"updated":N} object instead
  of a pre-serialized string that jsonHandler double-encoded on the wire;
  a malformed body is now a 400 (public APIError) rather than a 500
- apiContractInfo dispatches on method like apiRuntimeSetting: POST/PUT
  write the collection path only (an address segment is rejected instead
  of silently ignored), unknown methods are a 400 instead of falling into
  the GET path
- new DELETE /admin/contract-info/<address> purges cached contract
  metadata (DB row + LRU entry) so it is re-fetched from the backend on
  the next read; idempotent, missing row reports deleted:false
- shared urlPathSegment helper unifies path-segment extraction; contract
  addresses get no case normalization (the chain parser is the authority),
  runtime-setting keys keep their uppercasing at the call site
- runtime-settings routes and initRpcCallAllowlists move out of the
  ChainEthereumType guard (the mechanism is chain-generic and the init
  already runs unconditionally in NewWebsocketServer); contract-info and
  internal-data-errors stay EVM-gated, nav links follow the same split

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Smaller items from the PR trezor#1597 review:

- lowercase the "invalid EVM call method selector" parse error to match
  the sibling parser's message convention
- trim environment values wherever they are resolved (startup and the
  DELETE fallback), so stray whitespace from an env file never surfaces
  in values; a set but whitespace-only env value is a configuration
  error rather than unset — treating it as unset would silently
  un-restrict rpcCall
- document the differing To/Methods map key formats on RpcCallAllowlists
  so a future allowlist dimension picks a convention deliberately
- runtimeSettingStore gains GetRuntimeSetting; resolveRuntimeSetting
  reads through the interface instead of requiring *db.RocksDB

Admin runtime-settings page gets an inline editing UI (edit/save/delete
per setting, replacing the curl-only instructions) and the page/nav/
routes are consistently available on all chain types.

Hardening found by review of the new DELETE /admin/contract-info/:

- bump protocolGen before the cache purge in DeleteContractInfoForAddress
  so a concurrent GetContractInfo cannot re-insert the deleted row into
  the LRU (same idiom as SetErcProtocol); without it the purge could be
  silently undone until eviction or restart
- DELETE returns (and logs) the purged record: the backend re-fetch
  restores only name/symbol/decimals, not the sync-owned
  createdInBlock/destructedInBlock, so the response gives the operator
  a POST-back restore path and the docs now state exactly what is
  discarded
- a failed save in the admin UI keeps the row in edit state instead of
  stranding an orphaned input, and error responses are unwrapped to
  their message

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Supports the bbctl counterpart (trezor/bbctl#10) of the runtime-settings
admin API in a two-replica deployment where each replica owns its DB:

- GET /admin/runtime-settings/ (bare collection path) returns every
  setting with its effective value and source as a JSON array, so a
  management tool reads the whole state per replica in one request;
  other methods on the bare path stay a 400
- initRpcCallAllowlists warns at startup when a stored override shadows
  a different (or malformed) environment value, making env/DB drift —
  a replica that missed an admin update, or an env change rolled out
  while an override exists — visible; the shadowed value is only
  compared, never parsed, so drift cannot fail a restart
- docs/env.md explains the deployment roles of the two sources: the env
  var is the deploy-managed baseline that survives a replica resync
  (the DB is wiped, and unset allowlists would silently un-restrict
  rpcCall), the stored override is the runtime layer that survives
  restarts until the next deploy ships an updated environment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /admin/contract-info/ (bare collection path) lists the stored
records, mirroring the runtime-settings list convention. Unlike runtime
settings the collection is unbounded — sync stores a record per contract
creation, millions on a busy chain — so the list is paginated:
?limit=<1..10000> (default 1000) with a from cursor, and the response
{"contracts":[...],"next":"<address>"} carries the from of the next
page. ListContractInfos iterates cfContracts in address-descriptor
order (the CF holds only plain addrDesc keys; protocol rows live in
their own CF).

The former 400 for GET of the bare path moves to the list; DELETE and
POST semantics on the bare path are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-deploy step in the e2e-tests job exercising the admin API
(/admin/runtime-settings/, /admin/contract-info/) of the deployed
base_archive dev instance, leaving its database and live rpcCall policy
exactly as found — at the end of a run and at every instant during it:

- runtime settings use a zero-policy-impact roundtrip: the POSTed value
  is always the current effective value (or the explicit-empty override
  when unset), so the parsed allowlist never changes; the final DELETE
  restores the original source; db-sourced settings are only rewritten
  in place, since their env fallback is unknown from outside
- the contract roundtrip uses a reserved never-real address, pre-cleans
  it (healing a crashed run) and deletes it at the end; the cleanup and
  restore flags are raised before the POSTs, so a response lost after a
  server-side commit still gets cleaned up
- negative tests: 401 without credentials (503 = admin disabled fails
  loudly), unknown key, invalid values proven to change nothing, POST to
  an address path, unsupported method, list pagination limits
- target URL derives from BB_DEV_API_URL_HTTP_<coin> plus the coin
  config's ports.blockbook_internal (BB_ADMIN_E2E_URL overrides for
  local runs); the coin gate compares against coins_csv deploy aliases
  (test_coins_csv carries test names and would never match); skips are
  annotated (::warning:: for a missing BB_RUNTIME_ENV, ::notice:: for
  coin-not-deployed) so a dead signal cannot look like a pass
- credentials are parsed from BB_ENV inside the script (surrounding
  quotes stripped like systemd EnvironmentFile) and never printed;
  transient network errors are retried once, then fail with a clean
  pipeline error; helpers reuse runner.py and wait_for_sync.py

Verified live against blockbook-dev3: two consecutive runs green via
the derived URL, instance state identical before and after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BB_DEV_API_URL_HTTP_<coin> repo variables follow the tests.json
test-name convention (BB_DEV_API_URL_HTTP_base for base_archive), so
resolving them by the deploy alias failed with "missing
BB_DEV_API_URL_HTTP_base_archive". Map the deploy alias through
runner.load_test_coin_name (the same helper deploy_plan.py uses) before
the lookup; the coin gate keeps comparing deploy aliases. Unit test
pins the mapping against the real base_archive.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The paginated contract listing discarded the error from
GetAddressesFromAddrDesc and fell through to return next="" for a
boundary row whose key did not decode to an address. That silent
truncation is indistinguishable from a completed listing and drops the
remaining rows; in-page rows with no decoded address were likewise
returned with an empty Contract.

A corrupt key now returns an error, matching the unpackContractInfo
error handling in the same loop. Unreachable for well-formed EVM/Tron
contract descriptors, but a visible failure beats a confusing partial
page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(tron-testnet): upgrade backend to 4.8.2

* fix(tron-testnet): path to the .jar backend file

* fix(tron-testnet): update backend version
…esilliency (trezor#1582)

* feat(fiat-api): add cap limits for fiat rate endpoints

* feat(evm-contract): verify contract logs to avoid possible OOM

* feat(websocket): add lower concurrency cap on getMempoolFilters

* fix(fiat-api): apply currency count cap to raw selector list length

The MaxFiatRatesCurrencies check fired only when a unique normalized code
was appended, so an all-duplicate list bypassed the cap while still costing
a full trim/lowercase/dedup pass per element. Reject over-long lists up
front so the normalization work itself is bounded by the cap; this also
makes the in-loop unique-count check unreachable, so drop it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(fiat-api): normalize currency selectors once at entry points

setFiatRateToBalanceHistories re-normalized a slice that both of its
callers (GetBalanceHistory, GetXpubBalanceHistory) had already passed
through normalizeFiatCurrencies, and getFiatRatesResult re-lowercased
selectors already lowercased by its callers. Drop the redundant passes and
document the pre-normalized precondition instead. Normalization was the
only error path of setFiatRateToBalanceHistories, so it no longer returns
an error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(evm-contract): drop unreachable ERC-1155 count overflow guard

The endValues <= len(data) check already bounds countValues to
len(data)/evmWordHex, far below maxInt, so the truncation guard on the
int conversion could never fire (the load-bearing overflow check lives in
erc1155BatchArrayEnd).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(websocket): clarify scope of getMempoolFilters response cap

The slot is deliberately held from before the handler computes the
response until it is written to the websocket (or drained on close): the
cap exists to bound peak memory in computed-but-unwritten filter
responses, so it covers compute, queueing, and write together rather than
compute concurrency alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(eth-parsing): skip unparseable log with a warning and continue with next logs

* test(websocket): cover mempool filter slot release after successful write

Adds TestWebsocketOutputLoopReleasesMempoolFilterSlotAfterWrite, which drives
a release-bearing WsRes through outputLoop over a live websocket connection and
asserts the mempool-filters slot is freed after WriteJSON. This pins the
primary release path (outputLoop's c.finalize(m)) that the existing semaphore
and drain-on-close tests do not exercise; without it, removing that call would
leak a slot on every successful response and go undetected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
getEnsRecord read an attacker-controlled 32-byte word from an ENS
NameRegistered log and computed a slice end as 194+64+(int(c)<<1). For
c in [0x7FFFFFFFFFFFFF7F, 0x7FFFFFFFFFFFFFFF] the signed left shift wraps
and de lands in [0, 256], slipping the `de < 0` guard, so
l.Data[194+64:de] became a low>high slice expression and panicked. The
panic runs in the GetBlock log-processing goroutine, which has no
recover(), so it crashes the whole process before the block is
committed -> deterministic crash-loop on restart.

- getEnsRecord: guard the slice start (de < 194+64) instead of de < 0.
- processParam: same fix (de < d) for the sibling data[d:de] slice
  (already under ParseInputData's recover(); correctness only).
- GetBlock: recover() the log goroutine as defense-in-depth, returning
  an error instead of letting the process die (adds runtime/debug).
- tests: regression cases for both overflow paths (de=0, de=256, and
  the processParam variant), verified to panic without the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tart in getEnsRecord

Address two review comments:
- Add d+64 < d overflow guard at line 159 in processParam, same class as
  the existing de < d fix.
- Replace magic 194+64 with a local const nameStart in getEnsRecord.
TestGetTickersForTimestamps_ConcurrentReadersAndWriters and
TestLogTickersInfo_ConcurrentWithCacheWriters wait `waitTimeout` for
their reader/writer/logger goroutines to drain after `stop` is closed.
The deadline was 3s. That is only a deadlock safety-net, but it is too
tight on a CPU-starved CI runner (the Unit Tests job compiles RocksDB in
parallel), so `wg.Wait()` occasionally misses it and the test fails with
"concurrent ... did not finish in time".

Raise it to 30s: still fails fast on a genuine deadlock, but tolerant of
scheduling delays under load. Test-only change; the concurrency exercise
(driven by testDuration) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetTransactionFromBchainTx resolved each input's previous output by calling
GetTxAddresses, which unpacks an entire TxAddresses record (all inputs and
outputs) just to read a single output. For transactions with many inputs that
reference large previous transactions this allocates on the order of
inputs x record size, driving heap usage up sharply on high-fan-in addresses.

Add db.GetTxAddressesOutput, which returns a single Outputs[vout] by skipping
over the height, inputs and preceding outputs by length without allocating
them, and use it in the vin-resolution loop. Allocation now scales with the
number of inputs rather than inputs x record size.

Adds TestGetTxAddressesOutput (both index modes) and BenchmarkTxInputResolution
covering the input fan-in scaling.
…on self-hosted runners

The actions/setup-node built-in npm cache saves ~/.npm with paths relative to
the workspace. On runners where the home directory lives outside the workspace
tree, tar cannot restore these paths. The explicit actions/cache for
tests/openapi/node_modules already handles caching and skips npm ci on hit,
making the global npm cache redundant.
actions/checkout v4.3.1 -> v7.0.0
actions/cache v4.3.0 -> v6.1.0
actions/setup-node v4.4.0 -> v6.4.0
actions/upload-artifact v4.6.2 -> v7.0.1

All four actions were targeting Node.js 20, which is now deprecated on
GitHub Actions runners running Node.js 24. The new major versions bundle
a compatible runtime.
Instead of setup-node's built-in cache:npm (which saves ~/.npm with paths
relative to the workspace, causing tar failures when home is outside the
workspace tree), cache a local .npm-cache directory inside the workspace
via explicit actions/cache, and pass --cache .npm-cache to npm ci.

On node_modules cache miss, the local cache avoids re-downloading packages
from the registry. The restore-keys fallback preserves partial cache
benefit even across lockfile changes.
On node_modules cache hit, npm ci is skipped and .npm-cache was never
created, causing the post-job cache save to warn:
  Path Validation Error: Path(s) specified for caching do not exist

Pre-create the directory with mkdir -p so the path always exists for the
save phase. On cache hit the directory is empty and the save is a no-op.
…ng chains

The getSampleIndexedHeight probe searches from bestHeight-2 backward for an
indexed block hash. At 12 blocks (txSearchWindow), chains with slower
indexers (ethereum-classic on core-geth/blockbook-dev2) sometimes have zero
indexed blocks in the window, causing flaky failures.

20 blocks covers ~4.5 minutes of ETC chain history, ample for any realistic
indexing lag. The loop stops on the first hit, so healthy runs are unaffected.
* fix(ws): cap subscribeFiatRates tokens array to prevent DoS

The subscribeFiatRates handler stored the request's tokens slice whole
for the connection lifetime and re-iterated it on every fiat-rate
broadcast (per subscribed currency) while holding the global
fiatRatesSubscriptionsLock. It was the only WebSocket list parameter
without a length cap: a single message up to the 4 MiB read limit could
carry ~10^6 token strings, enabling amplified heap retention and
periodic global-lock contention that stalls fiat-rate subscription
handling.

Reject requests whose tokens array exceeds
maxWebsocketSubscribeFiatRatesTokens (1000) with a public API error,
matching the existing caps on addresses, currencies, timestamps, and
estimateFee blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(websocket): cleanup comment

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(eth): prevent duplicate ERC-721 holdings

* chore(contrib-script): move the erc-721 holdings check to a gist

* refactor(eth): address PR review comments on NFT self-transfer handling

- derive the NFT self-transfer invariant from a shared isNFTSelfTransfer
  helper on both the connect and the disconnect side so they cannot drift
- document that addToContract callers pass mutateHoldings=false only for
  ERC721 self-transfers
- add a ConnectBlock -> DisconnectBlock round-trip test verifying that an
  ERC721 self-transfer keeps holdings untouched and tx counts balanced

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…urable

The cap on concurrently executing requests per connection is a hard-coded
const (48). Internal batch clients that pipeline many requests on a single
connection (e.g. bulk getAccountInfo address scans by an exchange backend)
trip it and get disconnected mid-batch, with no way to tune the limit --
unlike the adjacent websocket message-rate and getAccountInfo limits,
which are already env-configurable.

Read the limit from <NETWORK>_WS_PENDING_REQUESTS_LIMIT following the
existing WS limiter env pattern; the default stays 48 and 0 disables the
cap (the server-wide work limit remains as a backstop). Document the
variable in docs/env.md and cover the override with a test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enders

EthereumTypeGetNonces routed eth_getTransactionCount to the alternative
send-tx provider on a bare nil-check, i.e. for every address detail
request. On instances with *_ALTERNATIVE_SENDTX_URLS configured this
exhausted the provider's rate quota, flooding the error log with 429s
and - worse - forcing the fallback to the primary RPC exactly for the
private-tx senders the feature exists for, handing them a stale pending
nonce.

Only senders with a privately submitted transaction in flight need the
provider's nonce view. Record the sender of every transaction accepted
by an alternative provider (decoded from the raw tx hex, so it works
regardless of the mempool-fetch mode and survives a failed post-send
fetch-back) in a TTL map, and route nonce lookups through the provider
only for those senders. Everybody else is served by the primary RPC.

The fallback log is demoted to a warning: with the gate in place it
only fires when a provider call for an actual private sender fails.

Fixes trezor#1627

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A broadcast to alternative providers succeeds if ANY configured URL
accepts the transaction, but nonce lookups always queried urls[0]. When
the first provider rejects a broadcast that a later one accepts, the
sender's nonce reads would hit a provider that never saw the
transaction and report a stale pending nonce.

Record the accepting URL alongside the sender and route that sender's
eth_getTransactionCount lookups to it, falling back to the first
configured URL when unknown. Single-URL deployments are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nonce-routing entry recorded at send time survived for the full
alternativeMempoolTxTimeout even after the sender's transactions were
mined, superseded or replaced. Address polling therefore kept consuming
the alternative provider's rate-limit quota for minutes after nothing
private remained pending.

Drop the sender's entry when their last cached transaction leaves the
alternative mempool cache. RemoveTransaction is the choke point every
eviction funnels through (reconcile, RBF replacement, mined-tx removal
via the mempool callback); the read-path TTL eviction in GetTransaction
now goes through it too. The entry is kept when it is newer than the
evicted transaction, so a follow-up send whose post-send fetch-back
failed does not lose its routing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the alternative-provider nonce lookup fails for a gated sender,
the fallback answer comes from the primary RPC, which cannot see the
sender's privately submitted pending transactions. Returning it as-is
would let a wallet build its next transaction on a stale nonce and
accidentally replace the in-flight private transaction.

Raise the fallback answer to the floor implied by the alternative
mempool cache (highest cached account nonce of the sender + 1), so the
reported pending nonce never contradicts the pending view Blockbook
itself exposes for those transactions. Without cached transactions
there is nothing to raise by and the primary answer stands, which is
correct for the common case of a sender whose transaction was just
mined.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cranycrane and others added 24 commits October 1, 2026 13:09
Actions variables are rendered verbatim in job log step headers, so the
BB_DEV_RPC_URL_* / BB_PROD_RPC_URL_* values move into one BB_RPC_URLS
secret holding KEY=VALUE lines. The exporter masks each URL before
writing it to GITHUB_ENV and fails if those names are still present
among repository variables.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add fixture-driven xpub tests to the OpenAPI e2e suite: account totals,
tokens filters, txid paging, UTXOs, balance history, an invalid-xpub
negative case, and HTTP/WebSocket parity for getAccountInfo and
getAccountUtxo. Fixtures are the public Trezor test seed account xpubs
for bitcoin, testnet, litecoin, dogecoin, bcash and zcash, with observed
tx and used-address counts as floors so a shrinking index fails the run.

The negative case exposed that a malformed xpub was answered with HTTP
500; GetXpubAddress now returns a public 400 "Invalid xpub" error.

Harness: optional OPENAPI_USER_AGENT for HTTP and WebSocket, 5xx retries
with backoff, and a missing fixture file skips instead of failing.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ild1 (Nile) (trezor#1831)

* chore(tron): update java-tron 4.8.2.1->4.8.2.3

Both 4.8.2.2 and 4.8.2.3 are mandatory upgrades that change transaction
execution rules (block versions 37 and 38). A node on 4.8.2.1 computes
different results for the affected transactions and stops following the
chain once one is included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(tron-nile): update java-tron 4.8.2-pq1-build1->4.8.2.2-pq1-build1

Nile 4.8.2.2-PQ1-build1 is a mandatory upgrade; there is no Nile build of 4.8.2.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lamsterdam (trezor#1830)

Sepolia activates Glamsterdam (Amsterdam EL + Gloas CL) at epoch 353024,
2026-10-06 13:53:36 UTC. Prysm 7.2.0 is the first release scheduling Gloas
on Sepolia and Erigon 3.7.0 the first with amsterdamTime in its Sepolia
chainspec (3.5.x and 3.6.x, incl. release branches, lack it). Without both
the backend halts at the fork.

Erigon 3.6+ writes snapshot formats 3.5 cannot read, so this upgrade is
one-way. No removed startup flags are used by these configs, and the 3.6/3.7
JSON-RPC breaking changes do not touch calls Blockbook makes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
GetMempoolEntry has never had a caller - outside the per-coin implementations
the only reference was the metrics wrapper, and MempoolEntry.FeeSat /
ModifiedFeeSat were read nowhere - so no endpoint, mempool tracking or fee
estimation path used it.

It had also drifted out of sync with the backend: Bitcoin Core 24.0 removed the
top-level fee, modifiedfee, ancestorfees and descendantfees fields together with
-deprecatedrpc=fees, so a call against a modern node decoded them as zero rather
than failing. Dropping the dead path is preferable to teaching it the nested
fees object nobody would read.

Follow-up to trezor#1634 (items 2 and 3).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… batches (trezor#1828)

* fix(fiat): keep current tickers updating when CoinGecko rejects token batches

Since 2026-09-29 the api.coingecko.com CloudFront WAF answers 403 "Request
blocked" to simple/price URLs longer than ~3.1k chars. Token prices were
requested in ~6000 char batches, so every token-platform coin failed each
cycle and, because one failed batch aborted CurrentTickers, native rates
froze too (eth/bsc/base tickers stuck at 2026-09-29 06:1x UTC).

- cap the ids parameter at 2000 URL-encoded chars
- skip a failed token batch (stop on a Cloudflare ban) and report it as
  errCoingeckoIncompleteTokenRates alongside the fetched rates; a failed
  coins/list is reported the same way
- on an incomplete ticker keep the previous rates of the missing tokens,
  publish it and record status="partial" in the update-duration metric

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(fiat): size token batches from CloudFront's 2048-char query limit

api.coingecko.com is served by both Cloudflare and CloudFront; only the
CloudFront edge blocks long requests, and its limit is exactly 2048 chars
of query string (2048 passes, 2049 gets 403, reproducibly). The earlier
~3.1k estimate came from bisecting with responses mixed between the two
CDNs. Derive the ids budget from that limit minus a 100-char margin and
the vs_currencies overhead, and assert it in the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(fiat): drop failed token rates instead of carrying them over

Carrying a failed batch's previous token rates under the new ticker
timestamp mislabelled their age, and on chains priced in the native coin
(bnb, eth on L2s) mixed a stale token/native ratio with a fresh native
price. Suite treats a missing token as "No tickers found!" and falls back
to CoinGecko itself, keeping its own last good rate when that fails too.

- publish an incomplete ticker with only the token rates fetched this cycle
- retry failed batches once after the others (not after a Cloudflare ban)
- stamp the ticker when the native price is fetched, so ts bounds the age
  of every rate in it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(grafana): plot CoinGecko request counters as per-interval increases

The CoinGecko requests and historical range panels plotted the raw
cumulative counters, so restarts showed as drops, the daily history
download as a permanent step, and the all-status total line flattened
the throttle and error series. Plot increase over $__interval as bars
with sum/max legend calcs instead, with a 5m minimum interval so the
window always spans at least two scrapes.

market_chart downloads run in bursts of hundreds of calls and dwarfed
the other endpoints, so they move to a separate "CoinGecko historical
requests" panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(metrics): extend fiat update duration buckets to 1 hour

Current-ticker runs on token-heavy EVM chains take 7-46 minutes in prod, so
every observation fell into +Inf and the p95 panel was capped at 300s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rezor#1837)

A client can send an ad:<hex> descriptor of any length (up to ~2 MiB over
WebSocket). GetAddress passed it to GetAddressChainExtraData, which
base58-encoded it in quadratic time (64 KiB took 6.6 s, ~1-2 h per max-size
message) and forwarded the result to three node endpoints. Only a 20-byte
account has chain data, so return early for any other length.

Also stop logging the rejected input verbatim at INFO, which wrote up to
4 MiB of log per request.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…or#1838)

A side or contract filter that rarely matches walked the whole address
index. Stop after 200k non-matching entries and return a public error
asking to narrow from/to; matching entries stay bounded by paging.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…tails=basic (trezor#1801)

* perf(eth): read only the header of the address-contracts record at details=basic

Every Ethereum-type account request decoded the whole token list of the
address even at details=basic, which only consumes the three tx counters
in the fixed header. For 0xdEaD (~210k contracts) that was ~80 ms of CPU
and hundreds of thousands of allocations per request.

Add GetAddrDescContractsHeader, a counters-only reader of the
cfAddressContracts record, and select it from getEthereumTypeAddressBalances
when the contract array cannot be reached: details=basic, no per-contract
vout filter and no contract filter. Every other request keeps the full
decode. No format change, no migration, no API change.

Fixes trezor#1790

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(eth): fold the header-only read into GetAddrDescContracts and pin the record

Review follow-up for trezor#1801:
- GetAddrDescContracts takes a headerOnly argument instead of a second
  exported reader, and reads the record with GetPinnedCF so the value is
  not copied out of the block cache (255 µs → 9 µs per cached 5.6 MB
  record, 1.44 → 0.82 ms on a cache miss)
- details=basic always takes the header path; totalResults is consumed
  only above basic, so a per-contract filter cannot observe the
  undecoded contract array
- drop the 200k-contract benchmark fixtures, which were built at test
  binary init on every run

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…er explorer page (trezor#1826)

* perf(db): decode only the requested token rate in fiat ticker lookups

A daily ticker stores thousands of token rates. Token lookups decoded all of
them into a map per visited row just to read one, which on eth-b9 was 80% of
all allocated bytes and ~24% of CPU (explorer tokenAmountSpan).

FiatRatesFindTicker(s) with a token now decode the fiat rates plus the one
token rate (exact key first, then lowercase, as findTokenRate does).
FindTicker with 5,000-token rows: 144us -> 16us, 452KB -> 4KB per lookup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(server): memoize historical token rates per explorer page

tokenAmountSpan looked up the historical rate of every token-transfer row.
Stored token rates are daily, so rows of the same contract on the same UTC
day share one lookup per page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(db,server): simplify the token rate lookup and its per-page memo

Fold the token-only ticker decoder into unpackCurrencyRatesTicker behind a
token filter and share the rates-section loop, so the wire format is decoded
in one place and getTickerFromIterator loses its branch. Read keys through
a new unpackStringBytes next to unpackString instead of inlining it.

Drop the lowerFound/lowerRate state: a lowercase hit is written straight into
the result and an exact hit overwrites it. The kept rate is stored under the
caller's token so GetTokenRate hits without a second ToLower.

Key the explorer memo on fiat.DailyTickerTimestamp, which wraps the existing
ceilUnix, instead of a second copy of the day arithmetic in server. Remove the
GetTokenRate pre-check that GetContractBaseRate already performs and the
one-use func type. Document that token lookups return a single-rate ticker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(db,server): simplify the token rate memo key and decoder match

Key the explorer memo on TxDate, which amountSpan already sets alongside
TxTicker for the same day bucket, instead of a fiat helper exported only
for this purpose. Return on the first exact-or-lowercase key hit when
decoding a single token rate: explorer rows pass EIP-55 addresses while
stored keys are lowercase, so waiting for a later exact hit scanned every
row to the end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
trezor#1809)

* chore(btc): bump Bitcoin Core 29.2 → 31.1 for mainnet and all testnets

29.x goes EOL when Core 32.0 ships (~10 Oct 2026); 31.1 is current stable
and its release notes contain no breaking change on Blockbook's RPC surface.

Also drop bitcoind options that are dead on 31.1: mempoolfullrbf (removed in
Core 29.0), deprecatedrpc=warnings (coreWarnings decodes both shapes) and
deprecatedrpc=estimatefee (RPC removed in Core 0.17).

Closes trezor#1805

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: link the Bitcoin Core 31.1 changelog entry to its PR

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ezor#1847)

Private addnode peers can't live in the public coin configs. The build
now appends BB_ADDNODE_<alias> (comma/whitespace separated host[:port])
to backend.additional_params.addnode, validating each entry so the value
can't inject other bitcoind options. CI reads it from the new
BB_BACKEND_PEERS secret in the build job only, masks every peer, and
fails if the name is set as an Actions variable.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…) (trezor#1800)

Three sampler limits made the post-deploy e2e job fail on every attempt:

- REST /api/v2/block/{id} ignored pageSize, so the "3-tx" sample fetch pulled
  every tx of a BSC block (~38 ms each, cold) and tripped the 25 s client
  timeout. The endpoint now honours pageSize (default and max 1000), matching
  the WebSocket getBlock; documented in openapi.yaml.
- ethereum-classic has ~73% empty blocks; a 3-block probe found no tx-bearing
  block 42% of the time. The probe now spans txSearchWindow blocks and the
  failure names the real cause instead of "missing indexed block hash".
- The sample address was the first sender of a random tx, on BSC often a bot
  with 10^8 txs whose even time-bounded balance history is unservable. The
  sampler now prefers a participant with at most 10k txs, probing neighbouring
  txs within a small budget before falling back.

Verified live against all 21 CI coins on the dev hosts: 566 ok, 0 fail.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…bytes and hit/miss metrics (trezor#1813)

* perf(api): raise the default xpub cache cap from 1024 to 5000 entries

On the production Bitcoin instance the xpub cache sits pinned at its
1024-entry cap with ~1850 connected wallets, so hot accounts are evicted
and re-derived minutes later. A miss re-derives every address of the
account (~50-100 us each) and re-reads every balance, which is why
getAccountInfo averages 60-120 ms instead of well under a millisecond.

A cache entry costs ~50 KB for a realistic account mix (4 KB empty,
~80 KB for a 100-address account), so a full 5000-entry cache is
~250 MB. The cache only grows with distinct xpubs actually requested,
so idle instances pay nothing. Per-coin xpubConfig overrides still apply.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(api): expose xpub cache bytes and hit/miss metrics

The only xpub cache metric was the entry count, so the cap could not be
sized against memory and thrash could only be inferred from the gauge
sitting at the ceiling. Each entry now carries a heap estimate (per
address, balance, utxo and txid; within ~7% of runtime.MemStats on
synthetic accounts) that is debited on insert and credited on eviction,
published as blockbook_xpub_cache_bytes. Lookups are counted as hits,
absent misses (full derivation) or stale misses (every address rescanned
after a new block). Two Grafana panels show bytes and activity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(grafana): move xpub cache panels to ids 365-366 to avoid clashing with trezor#1810

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(api): simplify xpub cache accounting and trim at the raised cap

- single putCachedXpubLocked insert path mirrors deleteCachedXpubLocked, so
  the byte total has exactly two entry points and the test exercises the
  production insert instead of its own copy
- one missReason string decided where absent/stale are known replaces the
  two booleans and the trailing switch
- gauges are set under the lock; both call sites drop their copy-out locals
- trim evicts via a linear least-recently-accessed scan instead of
  allocating and sorting the whole map under cachedXpubsMux; at 5000
  entries that was ~2 ms and 123 KB of garbage per miss, blocking hits
- one resetXpubCacheLocked test helper resets map and byte total in all
  four tests that touch the package-global cache
- xpub cache activity panel gets the 5m min interval for rate() queries

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(api): cap the xpub cache by bytes and split gap/history cache misses

- xpubConfig.maxCacheBytes (default 1 GiB) evicts least recently used
  entries once the estimated cache size exceeds it; the entry cap alone
  does not bound memory since an entry holds the xpub's whole txid history.
  Trims needing more than one eviction sort once instead of rescanning the
  map per eviction (full 5000-entry eviction 124 ms -> 0.8 ms under the lock).
- A cached entry re-derived for a different gap is counted as reason=gap
  instead of absent, so two clients alternating gaps on one xpub are not
  mistaken for an undersized cache.
- Loading txids from the db for an entry cached at the current tip without
  history is counted as reason=history instead of a hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… (trezor#1797)

* fix(grafana): name every legend by replica, never by coin (trezor#1717)

The dashboard's single-select $coin dropdown already fixes the coin for
every panel, yet 93 legends repeated it ("Polygon Archive - EstimateSmartFee")
while 80 others named no replica at all, so neighbouring panels read
differently and a misbehaving replica could not be told apart.

Every legend now starts with {{instance}} for a per-replica series, or with
"all replicas - " where the query sums replicas away on purpose (fleet load,
external providers). Fault-oriented panels that used to aggregate by coin
(RPC rate/latency, sync errors, reorgs, send-path routing, per-replica caches,
fiat fetches) now group by instance so the offending replica is visible; the
sync RPC latency p50/p95 series also get distinct suffixes instead of sharing
one name.

render_grafana.py --check enforces the convention (no {{coin}} under a $coin
filter, a replica scope on every legend, and no legend claiming a scope its
query lacks), and the README documents it.

Closes trezor#1717

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(grafana): add HyperEVM Archive and Robinhood Archive to the coin dropdown

Both chains have been deployed for a while but the custom `$coin` variable
still ended at Avalanche, so their panels were only reachable by typing the
name. Values match the `coin` metric label, i.e. the `coin.name` of each
coin config.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…lloc (trezor#1846)

* build: link the Blockbook binary against jemalloc instead of glibc malloc

RocksDB pushes multi-megabyte record values, memtable blocks and write batches through the C heap on every
block. glibc malloc returns memory to the kernel only from the top of an arena heap, so those buffers stay
resident after they are freed once small long-lived allocations pin the heaps, and with eight arenas per
core the retained memory grows for hours on busy EVM chains (backend3 Optimism reached 136 GiB and was
OOM-killed twice in October 2026). jemalloc purges freed extents wherever they sit and returns allocations
above 8 MiB immediately, which is also the allocator RocksDB upstream builds and tests with.

The build image installs libjemalloc-dev and the Go link pulls in libjemalloc_pic.a by path, so the package
stays self-contained and still installs with a bare dpkg -i. RocksDB's own build is told not to detect
jemalloc, because that would link ldb and sst_dump against libjemalloc.so. JEMALLOC=0 restores glibc malloc
for A/B comparison.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* build(deb): set the jemalloc arena and purge defaults in the packaged unit

The trial on Optimism showed the native floor drifting from 3 GB to 10 GB
with jemalloc's defaults (four arenas per CPU, no background purging) and
staying at 3 GB with narenas:8 and background_thread:true. An override
applied as a unit drop-in was lost on the next deploy, so the packaged
unit now carries it; blockbook.env can still replace it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(eth): use the node-reported block hash for headers

go-ethereum recomputes the header hash from the fields it knows, so headers
with fields it does not know yet (Glamsterdam's blockAccessListHash and
slotNumber) get a wrong hash. Sepolia sync has been stuck at the fork block
since then. Keep the hash from the node's response for HeaderByNumber and
newHeads, falling back to the computed hash when it is missing. Tron now
uses the same header type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(eth): require the node-reported hash in headers

A computed fallback silently returns a wrong hash for headers with fields
go-ethereum does not know; fail the decode instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(tron): fold TronRPCClient.HeaderByNumber into TronClient

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Dependabot runs receive only Dependabot secrets, so BB_RPC_URLS is empty
there and the connectivity/integration jobs would hit default localhost
backends. Skip those jobs when the actor is Dependabot and add a
workflow_dispatch trigger so a maintainer can run them on the branch.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ls (trezor#1810)

* feat(metrics): export Go GC CPU share and live heap, add Grafana panels

Production runs GOGC=100 with no GOMEMLIMIT anywhere, and the default Go
collector only exports the memstats gauges, so the one number that says
whether GC tuning would help - the share of process CPU the collector
burns - was not observable without a pprof session, which packaged
builds do not expose.

GetMetrics now swaps the stock Go collector for one that also exports
the runtime/metrics CPU classes and GC families (live/goal heap, cycles,
pauses, scan sizes). The memstats gauges existing panels use stay, the
allocs/frees-by-size histograms are excluded (~30 series per instance
for no extra insight), and a repeat registration in one process is a
no-op so multi-coin test binaries keep working. Net: ~41 series per
instance.

Two System-row panels read them: "Go GC share of process CPU" (GC over
non-idle CPU, so a 32-core host does not dilute it) and "Go live heap vs
GC goal" (the gap GOGC=100 costs in RAM and what a GOMEMLIMIT would
cap).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(grafana): move the Go GC CPU panel to id 369, off the id trezor#1828 took

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…rezor#1799)

getEthereumTokensTransfers pre-sized the result and skipped an element on a
GetContractInfo error, leaving a zero-valued TokenTransfer with empty contract,
from and to in the API response. A pending contract creation whose init code
looks like transfer(address,uint256) hits this path, because the calldata
parser sets the contract to the missing "to".

Build the slice with append and, when the metadata read fails, keep the
transfer with its addresses and value and the coin's default decimals.

Closes trezor#1715

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…010, EIP-7708) (trezor#1802)

* fix(polygon): stop indexing the native token contract 0x…1010 as an ERC-20

The MRC20 system contract emits an ERC-20 Transfer for every native POL move
routed through it, which Blockbook already records as an internal transfer.
Indexing it as a token gave every holder a "Matic Token" pseudo-balance equal
to their native balance, and clients that sum tokens with the native balance
counted POL twice.

EthereumParser now keeps a set of ignored contracts whose transfer logs and
transfer() payloads are not token transfers; the address is checked only for
transfer-event logs, before they are decoded. Polygon registers 0x…1010.
Token transfers are re-derived from receipt logs at both index and read time,
so historical transaction details are cleaned without a reindex, and
BlockChainParser.EthereumTypeIsIgnoredContract lets the address token list
skip holdings indexed before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(eth): skip EIP-7708 ETH-transfer logs from the system address

Since Glamsterdam (live on Sepolia since 2026-10-06) every nonzero ETH transfer
emits an ERC-20 shaped Transfer log from 0xffff…fffe, so each ETH move was also
indexed as an ERC20 transfer of a pseudo-token, next to the native value and
the internal transfer.

The system address has no code and cannot emit logs on a chain without
EIP-7708, so every EthereumParser ignores it; no per-chain configuration or
fork height is needed, and chains adopting EIP-7708 later are covered. On a
post-fork Sepolia block this halves the log-parsing time and allocations.

Closes trezor#1845

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ator (trezor#1793)

* fix(typescriptify): make blockbook-api.ts reproducible from the generator

Regenerating blockbook-api.ts no longer reproduces the committed file: the
library inlines ts_type unions, so the hand-added TxChainExtraData and
AccountChainExtraData aliases that tests/openapi/src/parity.ts imports
disappear, and it emits every pointer field as optional, so the nullable
WsNewBlock.evmData turns into `evmData?:` and fails the parity check. The
2-space indent on top rewrote every line and hid real drift.

The generator now owns the output: 4-space indent, a tsAliases table that Go
fields reference via ts_type:"<Name>", and an opt-in ts_nullable:"true" tag
that emits `name: T | null` for pointer fields without omitempty. Unreferenced
aliases, misused tags and missing field lines fail the run instead of
silently degrading. Nullability stays opt-in because a generic pointer rule
would flip ~25 other fields and break both parity and the Suite contract.

Regenerated blockbook-api.ts picks up the drift the Go structs already had:
TronChainExtraData.claimedVoteReward and feeLimit and the Erc4626Token.asset
doc string.

Closes trezor#1566
Closes trezor#1734

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: fail the unit-tests job when blockbook-api.ts is stale

Adds `make typescriptify` to regenerate blockbook-api.ts inside the build
image and `make check-typescriptify` to diff the committed file against a
fresh run. The check runs in the unit-tests job rather than Lint because the
generator links RocksDB through cgo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(typescriptify): build the chainExtraData unions from a Go payload registry

The generator carried the TxChainExtraData/AccountChainExtraData unions as
TypeScript source in a Go table, and the catch-all `{ payloadType: string;
payload?: any }` member meant `payloadType === 'tron'` never narrowed the
payload (TronChainExtraData | any collapses to any).

bchain.ChainExtraPayloads now maps each payloadType to its payload structs;
the generator renders a closed union per wrapper from it and types the
wrapper fields through ManageType, so the ts_type tags and the alias table
go away. A Tron parser test pins GetChainExtraPayloadType to the registry so
a new payload cannot ship untyped, and the generator refuses an empty or
non-struct registry.

Regenerated blockbook-api.ts also picks up TronChainExtraData.withdrawnUnfreeze
from the rebase onto master (trezor#1682).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(typescriptify): discover chainExtraData wrappers through an interface

The generator still named the two wrapper structs and their registry slot in
a table. Each wrapper now implements bchain.ChainExtraPayloadWrapper, which
picks its payload struct from a registry entry, and the generator finds
wrappers while walking the struct graph it already scans for ts_nullable.
The generated file is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(typescriptify): check staleness in the unit tests

The committed blockbook-api.ts is now compared against the generator's
output by TestCommittedFileIsCurrent, which the existing `make test` already
compiles and runs; the check-typescriptify targets and the extra CI step,
which rebuilt the whole cgo graph in a second container, are gone.

ManageType takes the wrapper's reflect.Type directly, renderUnions no longer
touches the converter, and the explicit 4-space indent, already the library
default, is dropped. The generated file is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(agents): document how API structs reach blockbook-api.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@sidhujag
sidhujag marked this pull request as ready for review October 20, 2026 17:39
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T17:42:35.034967Z efd92ca Draft marked ready
🔒 Security Review ✅ Completed 2026-10-20T17:45:53.206638Z efd92ca Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sidhujag
sidhujag merged commit 9a5ecfa into master Oct 20, 2026
1 of 2 checks passed
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.

9 participants