catch up - #1
Open
Corey-Code wants to merge 1235 commits into
Open
Conversation
Document, with mermaid diagrams, how EIP-1559 fees are produced across Blockbook and Trezor Suite and which component owns which decision: blockbook provides ground-truth inputs (chain base fee, priority-fee tiers, congestion/trend, block gas), while the wallet owns the fee policy (base-fee source, 2x head-room buffer, maxFeePerGas composition, per-coin clamps). Covers the pull path (EthereumTypeGetEip1559Fees), the push path (subscribeNewBlock evmData), Suite's EthereumFeeLevels, the end-to-end flow, and the maxFeePerGas formula. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the two existing fee panels (estimated fee rate, alternative fee provider requests) out of General into a new top-level "Fees" section, so fee observability has one home as EVM EIP-1559 fee metrics are added. Pure relocation: panels, queries and the metrics they read are unchanged; only their x-panel-keys move general.* -> fees.* and a row.fees section is introduced. grafana.json is the git-ignored artifact rendered from these two sources by contrib/scripts/render_grafana.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EVM EIP-1559 fees were invisible in metrics (blockbook_estimated_fee is
UTXO-only). Add two gauges populated on the successful estimateFee /
EthereumTypeGetEip1559Fees paths (provider cache hit and on-chain estimate):
- blockbook_eth_eip1559_fee{tier,kind}: per-tier maxFeePerGas / priority fee
- blockbook_eth_eip1559_base_fee: the next-block base fee underlying them
Values are raw wei (base units, like estimated_fee); Grafana divides by 1e9
to show Gwei. Emitted only on the two successful returns so error/disabled
paths never write zeros, and nil tiers are skipped so no empty series appear.
Dashboard (Fees section): maxFeePerGas by tier, priority tip by tier (own
axis - tips are ~1000x smaller), and the maxFee/baseFee buffer ratio with a
dashed 2x reference, which separates a congested chain from an estimator
regression - the "fees too high" diagnostic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add blockbook_eth_eip1559_fee_source_total{source} incremented on each served
estimate at the serve boundary: provider (alternative provider cache hit),
onchain_fallback (provider configured but cache stale/unready, so eth_feeHistory
was used) or onchain (no provider configured).
This is the one signal neither alternative_fee_provider_requests (background
fetch outcome) nor the provider cache-age gauge captures: how often the wallet-
facing estimate actually bypassed the provider. Emitted only on the two
successful returns, alongside the fee gauges.
Dashboard: a non-stacked "EIP-1559 fee source" panel so a provider->fallback
transition shows as a visible crossover.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add blockbook_eth..._provider_last_sync_timestamp_seconds{provider}, the unix
time of the last successful refresh of the cached EVM provider (infura/1inch)
fees, set via a shared observeSync() called from both providers' processData
(replacing the bare p.lastSync = time.Now()) so lastSync and the metric always
share one instant.
Cache age is plotted as time() - metric. A timestamp gauge, not a computed
*_age_seconds one (the repo's usual form): it is written only on a successful
refresh, so the plotted age keeps climbing when a provider wedges and survives
restarts. This is the leading indicator that explains the onchain_fallback
counted by eth_eip1559_fee_source_total - once age crosses the stale window
(30x the poll period) the pull path falls back to on-chain.
Dashboard: a cache-age panel with a dashed default-cutoff (30m) reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add blockbook_eth_block_gas_used_ratio, gasUsed/gasLimit of the most recently connected EVM block (0..1) - the congestion signal that drives the next base fee (>0.5 it rises up to +12.5%/block, <0.5 it falls). This is the push path's own view of why fees move, independent of any provider. Set synchronously in OnNewBlock before the async broadcast: OnNewBlock is invoked in monotonic height order by the single writeBlockWorker, whereas the per-block broadcast goroutines can reorder and let an older block clobber a newer gauge value. Last-value semantics, nil-guarded for non-EVM/pre-London. Dashboard: a "Latest block gas-used ratio" panel (percentunit) with a dashed 50% balance-point reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…verlay Add blockbook_eth_block_base_fee, the realized base fee of the most recently connected block (push path), in raw wei. Set in the same OnNewBlock helper as the gas-used ratio, nil-guarded. Paired with eth_eip1559_base_fee (the pull path's next-block projection) on one overlay panel - solid mined vs dashed projected, both in Gwei. The two should track within a block's +/-12.5% step; a persistent gap means the feeHistory projection (or a provider's estimatedBaseFee) is drifting from the chain - the base-fee half of a "fees too high" investigation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documentation-only follow-up from an adversarial review of the new Fees section (no functional change, no Go change): - order the section pull-estimate -> provider-health -> push -> legacy, and label the relocated UTXO "Estimated fee rate" so an EVM $coin selection reads as expected-empty rather than an outage - buffer-ratio: note Infura's high tier sits ~2.5x by design, so the dashed 2x line is the on-chain estimator's target only - base-fee overlay & cache-age: scope the "tracks within +/-12.5%" and "30x poll period" expectations to the path/provider they actually hold for (provider coins serve a cached projection up to the stale window old) - eth_eip1559_base_fee help: drop the unconditional "next-block projection" claim (true only on Erigon-like backends) and Infura's estimatedBaseFee field name (1inch feeds the same gauge from baseFee) - priority-tip: "separate panel", not "own axis" Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bounds-check the per-tier column index so a non-compliant backend returning a reward row shorter than the requested percentile count is skipped instead of panicking EthereumTypeGetEip1559Fees. Divide the tip average by the rows that actually contributed, so a skipped short row does not deflate the tier. Document the accepted zero-tip case on idle chains, and add a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 1inch alternative fee provider never set staleSyncDuration, so it defaulted to the zero value (0s), making the cache immediately stale after every write. GetEip1559Fees always fell back to on-chain estimation via eth_feeHistory, defeating the purpose of having a provider cache. Mirror the same pattern used by the Infura provider: introduce oneInchFeeStalePeriods=30 and oneInchFeeStaleDuration, and set p.staleSyncDuration in the constructor so cached fees are reused within the 30×pollPeriod window (e.g. 30min for a 60s poll).
Replace the Infura gas API with 1inch Gas Price API for Ethereum mainnet archive. The 1inch API returns raw wei integers and uses Bearer token auth (ONE_INCH_API_KEY). The staleSyncDuration bug that made 1inch cache always immediately stale was fixed in the preceding commit.
Grafana: - alt_fee_provider_requests tooltip mode: single -> multi (inconsistent with other fee panels) - eip1559_max_fee description: generalize Infura-only language to mention both providers and clarify instant tier behavior differs per provider - eip1559_buffer_ratio description: generalize Infura-only padding reference - alt_fee_provider_cache_age description: generalize "Infura (the EVM default)" to mention both providers use the same 30x stale window docs/fees.md: - Replace Infura-only references with provider-agnostic language throughout - Keep accurate historical context (Infura as original source of fees-too-high reports) - Update mermaid flowcharts from "Infura fees" to "provider fees" - Document both Infura and 1inch behavior in the provider section
1inch returns 4 tiers (low/medium/high/instant) with tighter per-tier values than Infura (3 tiers). The previous 1:1 mapping (suite low/medium/high ← 1inch low/medium/high) produced fees that were too conservative compared to Infura, causing ~2x fee differences between providers. Remap so the suite's low/medium/high tiers are fed from comparable aggressiveness: 1inch medium → suite low 1inch high → suite medium 1inch instant → suite high 1inch's low tier is discarded (too conservative to be useful). The fee formula (maxFeePerGas = 2×baseFee + tip) is already correct on the suite side (fix/evm-fee branch) — this change ensures the input tier values are comparable regardless of which provider serves the response.
op-geth and arbitrum-nitro do not implement the newPendingTransactions
subscription filter. Without disableMempoolSync: true, Blockbook logs:
initializeMempool EthSubscribe newPendingTransactions:
invalid subscription type for subscribe
and wastes resources on repeated resubscribe attempts.
This was already fixed for bsc_archive, polygon_archive, and
base_archive. Apply the same fix to the remaining L2 coin configs:
- optimism, optimism_archive
- base
- arbitrum, arbitrum_archive
- arbitrum_nova, arbitrum_nova_archive
…ncurrency Trezor Suite's Blockbook websocket client caps concurrent in-flight requests at 42 (blockchain-link/src/workers/blockbook/index.ts:481). The old default of 0 disabled the server-side limit entirely, making per-connection getAccountInfo abuse bounded only by the 2500 msgs/10 min rate limiter. Change the default to 42 so the server-side descriptor-count limit activates automatically. The env var override is preserved for operators who want a different value.
… connections, reduce timeouts - Use Promise.all for cross-coin execution: coins run concurrently (they connect to independent backends). Each coin buffers its own output and flushes contiguously after completion. - Add preloadSamples() to TestContext: eagerly resolves block, tx, address, and fiat samples once at coin startup so downstream tests hit the cache instead of triggering redundant probe chains. - Replace per-call WebSocket connections with a wsPool: maintains 3 persistent connections, eliminating the 5s handshake overhead for every WS test. Pool acquires/releases per request with automatic dead-connection replacement. - Reduce timeouts: HTTP 30s->15s, WS dial 5s->3s, WS message 15s->10s.
…utput per-coin - Add a mandatory timeout to wsConnection.connect() using a 'settled' guard that rejects after wsDialTimeoutMs even when DNS or TCP stalls (the ws library's handshakeTimeout only applies after TCP connect). - Make wsPool.init() parallel (Promise.allSettled) so all connections are attempted concurrently; a partial pool is still usable. - Remove getSampleAddressTx from preloadSamples() — it probes up to 40 transactions with paginated lookups and is only needed by a few address-listing tests; keep it lazy. - Flush each coin's output immediately upon completion instead of waiting for Promise.all, so results from finished coins are visible even if another coin hangs. - Use Promise.allSettled instead of Promise.all so a rejection in one coin does not discard results from other coins.
… init race E2e-only remainder of the original review-findings commit; its tests/integration.go part (configurable init retry) was dropped along with the Go-side test parallelization.
…ions Promise.allSettled discarded runCoin rejections: a coin whose context creation or preload threw vanished from the summaries with its buffered output unflushed, and the run printed 'passed for N coin(s)' with exit code 0. Catch the rejection per coin and push a failed CoinRun summary so the aggregate failure path fires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
preloadSamples() ran bare inside runCoin, outside the per-test try/catch, so one transient HTTP or schema-validation error during the probe chain rejected the whole coin. Handle it like the status preflight: emit a single SamplePreload failure with the buffered output preserved and stop the coin there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wsConnection already multiplexes concurrent requests by ID and tests within a coin run sequentially, so the 3-connection lease pool bought nothing over one persistent socket while adding an unbounded acquire retry loop and a permanently-cached failed init. One shared connection with redial-on-close keeps the handshake-per-coin win; every wait is bounded by the dial and message timeouts, and a failed dial is not cached so the next call retries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and panel debug_traceBlockByHash is typically sub-100ms, reaches seconds only when the node is overloaded, and ~10-15s observations are trace_timeout / rpc_timeout expiries — not routine latency. Say so in the histogram help text, and exclude failed calls from the Grafana p50/p95 queries so timeout expiries do not skew the quantiles (failures are already charted in the sync RPC errors panel). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y deletion The generated Debian package installs a daily cron wrapper that runs bin/logrotate.sh as root, while postinst chowns the per-coin log directory to the unprivileged Blockbook service user. The cleanup script expanded the discovered log path unquoted in both `fuser` and `rm -f $log`, so a file whose name contains shell word-splitting tokens (e.g. "blockbook-bitcoin.log -rf /opt/coins/data/bitcoin/backend") would be split into multiple operands. On GNU rm, `-rf` is honored even after an earlier operand, letting the service user cause the root cron job to recursively delete an attacker-chosen path outside the log tree (backend/RocksDB data, config, other coins' data). Quote "$LOGS" and "$log" and pass `--` before operands so an attacker-controlled path is always treated as a single non-option argument. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deploy is a fail-fast:false matrix, but wait-for-sync and e2e-tests were gated on needs.deploy.result == 'success'. A single failed coin therefore skipped sync verification and e2e coverage for every coin that DID deploy, leaving freshly deployed instances unverified. Run both jobs on partial failure too; only mode=build (skipped) and cancellation keep them off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven *_test.py files (~1000 lines covering runner.py, build/deploy plans, backend policy and wait_for_sync) existed but were executed by no workflow or Makefile target, so regressions in the pipeline scripts shipped with green CI. Run them via unittest discover in the lint job; they are stdlib-only, need no secrets, and are safe for fork PRs on the GitHub-hosted runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lower SYNC_TIMEOUT_SECONDS to 300 and the job timeout-minutes to 6, keeping the one-minute buffer over the script deadline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only wait-for-sync had a timeout; every other job fell back to GitHub's 360-minute default, so a hung apt lock, unresponsive backend RPC or stalled install could occupy a self-hosted runner for six hours and block everything queued behind it. Ceilings are sized well above normal runtimes so they only trip on genuine hangs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pass matrix.coin to deploy-bb-and-backend.sh via an env var instead of
expanding ${{ }} directly into the run: script, matching how every other
dynamic value in this workflow is handled. Set persist-credentials: false
on all checkouts so the GITHUB_TOKEN is not left in .git/config on the
persistent self-hosted runners when post-step cleanup is skipped
(cancelled job, crashed runner agent) — testing.yml already does this on
the same runner pool. Nothing in the build/deploy path performs
authenticated git operations after checkout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without a concurrency group, every push to a PR queued a full unit->connectivity->integration chain on the limited self-hosted pool even when a newer commit had already superseded it. Cancel-in-progress applies only to pull_request events; push runs on master/develop always run to completion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…de-off Document two previously-implicit contract points on the privatePending hint: - estimateFee URL selection is best-effort (nonceURL, else urls[0]); unlike the nonce floor, gas has no client value to compensate a wrong relay node, so a declared-but-unknown sender may miss a predecessor held by another relay. - the deliberate trade-off vs pre-#1629 behavior: hint-less senders whose private tx was accepted by another replica are estimated on the primary RPC; widening routing for them would reopen the #1629 drain, so declaring the hint is the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exported contract still promised what the pre-rebase implementation did: "any entry raises the reported pending nonce to at least value+1". The floor no longer works that way - a declared nonce is an occupied slot that the walk crosses only if the slots below it are occupied too, so a nonce declared above an unfilled slot strands instead of lifting the answer over the hole. A client integrator reads that guarantee from blockbook-api.ts, so leaving it stale is the one place the semantic change could reach a caller. Says so in the ts_doc (and its generated TypeScript), in the hint section of docs/evm-send.md, and in the help of the two pending-floor counters, which a caller-declared nonce can now drive just as a cached one does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…et politeness Two hint-section claims aged out under the rebase onto the Blinklabs alignment and its missing eviction: - Nothing said a declaration is never expired server-side. Now that a dropped transaction's cached copy is retired within minutes (alternativeMissingTxTimeout), the asymmetry is worth stating: only the caller can prune its declaration, and a wallet deriving it from Blockbook's own pending answers self-heals on the next re-fetch. - The no-quota-drain argument leaned on wallets declaring the field rarely. The shipped trezor-suite producer (trezor-suite@754da40740) declares on every estimate keystroke while any own pending tx exists, so the argument now rests on what actually bounds the traffic: only senders tracking an in-flight tx are routed, and only while it is pending. Plus the missing full stop the stranded-counter help lost to its own appended sentence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hint series hand-edited blockbook-api.ts but never taught openapi.yaml the new field, which the parity typecheck reports as drift (WsAccountInfoReq: blockbook-api.ts has properties not declared in openapi.yaml). The PR checks stayed green because only the deploy workflow runs that typecheck - it would have surfaced at deploy time. Declare WsPrivatePending as a schema, reference it from WsAccountInfoReq and WsEstimateFeeReq.specific, and pin it with its own parity assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rs read it Adversarial review of the rebase found the series updated the floor_raised/floor_stranded texts for the declared hint but left every place describing WHO gets routed on the pre-hint story: - the nonce/estimate request counters and their panels still claimed only 15-min recent private senders reach the relay, and told the operator a sustained climb means the gating regressed - under the hint a declaring wallet is routed on every account sync and estimate keystroke while anything is pending, so the volume legitimately tracks pendinghood and the old reading would misdiagnose it. - EthereumTypeGetNonces' godoc, observeAlternativeNonceRequest and the estimate-routing comment still described useForNonces as the only gate, and the nonce bound as pending+cached. - api/types.go, ws_types.go (mirrored in blockbook-api.ts) and one nonce_hint_test comment still stated the retired raises-above-value contract or the pre-alignment relay model; the walk is the contract everywhere else since "state the hint's contract as the walk". - ws_types.go's getAccountInfo field doc said "When present" where the actual gate is a non-empty nonces array - a txids-only declaration changes nothing. Also unbreaks configs/metrics.yaml: the rebase resolution added a help sentence with an unquoted colon, which YAML rejects and every server test setup fatals on - caught only under -tags unittest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… case Two review findings on the hint's test coverage: - The "declared for another address is invisible" table case declared nothing - a byte-for-byte duplicate of the another-sender's-cache case above it - and the property its name claimed is not one raiseToPendingFloor has: declared nonces are folded in for whichever address is queried, unfiltered; scoping them to the request's own descriptor is the caller's job. The case now declares and pins that actual contract instead of asserting a guarantee nobody implements. - Nothing exercised the seam between the request and the chain: the declared nonces cross websocket.go's AddressFilter and worker.go's variadic forward, and dropping either compiles while every test stays green, because the fake chain discarded the parameter. The fake now walks the pending nonce across the declared slots the way production does, and a server-level getAccountInfo test asserts the walked answer - deleting either forwarding site now fails it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review of #1639 asked whether routing on the declaration can lower the answer below the primary's: the declared floor covers the declaring wallet's own txs, but nothing compensates for pending txs it does not know about (another device on the same account), and the relay's answer replaces the primary's entirely. It cannot, but only because of a relay property the docs never stated: Blink's pending eth_getTransactionCount answers the greater of the node's pending count and the relay's latest accepted nonce + 1 - a public-mempool superset - so a routed answer never falls below what the primary would have said. Say so where the routing is designed (docs/evm-send.md) and decided (the gate's comment), along with what a relay without the property would require: a floor-only nonce path, which the fallback branch already computes for the declaring wallet at zero relay cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… vector Review of #1639 pressed on what bounds the relay reads a privatePending declaration can force. The honest answer is that nothing needs to: sendTransaction is equally unauthenticated and spends the same relay quota on every call - fanned out to EVERY relay URL, at no cost for a rejected transaction - so a declaration buys an attacker strictly less quota spend per request than the endpoint that has always been there. The trust-boundary section now says exactly that, replacing the per-connection-limit reasoning, and notes that relay-quota protection, if ever wanted, belongs in the server's per-IP rate limiting uniformly across methods rather than inside this feature. The same review round called the surrounding prose very verbose; the routing-gate comments and the relay-superset passage are cut to their contract statements, with docs/evm-send.md as the single narrative home. Plus two touch-ups: the env prefix is computed once, and the template's literal µ is restored from the escape a tooling round-trip left behind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
base_archive's full block-detail response scales with tx count (250-300+ txs/block) and measured 7-9s live on dev, tripping the hardcoded 15s AbortSignal.timeout during sample preload while avalanche/optimism (fewer txs/block) stayed well under it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bumps [github.com/gorilla/websocket](https://github.com/gorilla/websocket) from 1.5.0 to 1.5.3. - [Release notes](https://github.com/gorilla/websocket/releases) - [Commits](gorilla/websocket@v1.5.0...v1.5.3) --- updated-dependencies: - dependency-name: github.com/gorilla/websocket dependency-version: 1.5.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
TestWebsocketShutdownWaitsForInFlightWork closed its completion channel after workDone(), but workDone() is precisely what releases Shutdown: requestWg.Done() unblocks the waiter goroutine that closes done and lets Shutdown return nil. When the worker goroutine was descheduled in that window, the non-blocking select observed finished as still open and the test failed with "Shutdown returned before tracked goroutine finished" -- as it did on the production builder for blockbook-arbitrum-archive while the same suite passed for blockbook-ethereum-archive two minutes earlier in the same job. Signal completion before releasing Shutdown, so the ordering the test asserts is guaranteed rather than hoped for: close(finished) now happens-before requestWg.Done(), hence Shutdown cannot return while finished is open. The elapsed >= 50ms assertion still catches a Shutdown that fails to wait for in-flight work at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Document the protocol enrichment layer behind the ercProtocols column family: the reusable storage, reorg-safety, probing and caching pieces, how ERC-4626 uses them, and the steps to add a new protocol. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
chaincfg.SigNetParams is built by CustomSignetParams, which never sets AddressMagicLen. The zero value made btcutil skip the version byte when base58-decoding, so every P2PKH and P2SH address on Bitcoin and Groestlcoin signet failed with "decoded address is of unknown size" while bech32 addresses worked. Fixes #1305 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.5.2 is exposed to two receipt/log correctness bugs that both land wrong data in Blockbook's index, since ERC20 transfers are built from eth_getLogs: - #23064: receipt domains were never rolled back on an in-RAM reorg unwind, because GetDiffset filled only the four domains that existed before ReceiptDomain was added. Small reorgs near the tip leave eth_getLogs serving phantom logs with wrong logIndex while eth_getBlockReceipts stays correct; the damage survives restarts and is frozen into snapshot files (erigontech/erigon#23062, observed on 3.5.2). - #22951: wrong logIndex on archive nodes from receipt-domain reads bypassing the overlay DomainReader. Affects 3.5.1-3.5.4. Also picked up on the way: the JSON-RPC handleBatch deadlock (#22459), which Blockbook can hit through BatchCallContext, and a ~9-15 GiB/day native memory leak in the RPC gzip path on archive nodes (#22700). Drop-in at every hop, no re-sync. Checksums taken from the release's own erigon_v3.5.5_checksums.txt. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tron has no account nonces (replay protection is ref-block + expiration) and TronClient.NonceAt is a local stub, so EthereumTypeGetNonces was returning a fabricated confirmedNonce of 0 whenever withConfirmed was requested. Return constants and never claim a confirmed nonce. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(eth): batch contract metadata reads via Multicall3, negative-cache conclusive misses Resolve name/symbol/decimals for all index-unknown contracts of an address in one aggregate3 eth_call instead of up to three serialized calls per contract, with a gas-starvation canary so a starved sub-call is re-read instead of recorded as "not a token". Conclusive "no token at this address" verdicts are kept in an in-memory LRU (15 min TTL, reorg-generation scoped) so repeated requests stop re-probing them; chain read errors are never cached. The ERC-4626 negative cache is generalized into the shared negativeProbeCache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(eth): clean up contract-info batch after review - render the ContractInfo address form in EthereumRPC.GetContractInfo via the parser, one owner for single and batched paths; drop the TronRPC override and the batch path's field overwrite - drop the dead filterDesc parameter of prefetchContractInfos: a single-contract filter can never fill a batch, so gate at the call site instead of paying a db read per contract - derive the batch metric fields and aggregate3 sub-calls from one contractInfoFields table instead of two parallel lists - collapse the two identical 15-minute negative-probe TTL constants into defaultNegativeProbeTTL owned by probe_cache.go - delete the unreachable aggregate3 short-response guard (the decoder already rejects count mismatches) and an inconsistent nil guard - merge the split nil-verdict handling in getProbedContractDescriptorInfo - hoist erc4626ContractKey to one computation per token - tests: t.TempDir, self-contained newContractProbeChain, deduped preambles Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(eth): tighten contract-info batch comments Cut the comments added by the Multicall3 contract-info batch down to 1-2-line why-notes and drop what the code already says. - fix the negativeProbeCache doc, which sat on negativeProbeCacheEntry - fix a test comment claiming the chunk size bounds contracts; Multicall3MaxCalls bounds sub-calls, contracts are (max-1)/3 - drop the (nil, nil) verdict paragraph repeated on three declarations and the wall-clock TTL rationale repeated on two - drop the worker.go call-site block restating prefetchContractInfos Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* perf(evm): serve GetChainInfo backend identity from a 60s cache GetChainInfo runs net_version and web3_clientVersion on every call, and it is on the request path of /, /api/ and /api/v2/ as well as of every sync iteration. On EVM chains those two are its only backend round trips - the tip already comes from the subscription-fed bestHeader cache - so an Arbitrum node was answering ~140k of them a day from the sync loop alone, plus two per API request. Both values change only when the backend is restarted onto a new build, so cache them behind a 60s TTL: one caller refreshes while the rest keep serving the previous snapshot, and a failed refresh is not retried for a TTL. This drops the cost to a fixed two calls a minute, independent of API traffic. Since these calls were the only thing in GetChainInfo that could observe an unreachable EVM backend, the cache stops vouching for one after 5 minutes and returns the error again, so backendError and inSync=false still work. Below that a transient failure no longer flips a whole instance out of sync over a version string. A changed chain id is logged as an error, keeping the sanity check the per-request net_version gave us for free. Avalanche gets the same treatment for its extra info.getNodeVersion call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(avalanche): do not hold nodeVersionMu across the info.getNodeVersion RPC The avm version cache keyed its TTL suppression on already having a value, so a node whose info endpoint is down - or that answers without an avm entry - never reached the TTL branch and probed on every GetChainInfo. Worse, the probe ran with nodeVersionMu held, so concurrent /, /api/ and /api/v2/ callers serialized behind an RPC that can sit until b.Timeout, which is worse than the uncached code this replaced. Key suppression on the last attempt instead of on the value, and mirror the backendIdentityCache lock discipline: one caller probes with the mutex released, the rest return the current value at once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(evm): rework the identity cache per review - one generic TTLValue[T] replaces the eth and avalanche copies of the TTL + single-flight + serve-stale state machine - refreshes of a warm cache run in the background, so no request ever blocks on an RPC that can sit until the timeout (and Avalanche no longer stacks two such windows) - liveness keys on how long refreshes have been failing, not on snapshot age: a burst after an idle gap no longer reports a healthy backend down - failed fetches retry after 5s instead of a full TTL, so recovery from a >5min outage is noticed in seconds, not up to a minute late - the in-flight flag is reset in a defer, so a panicking fetch cannot wedge the cache; a panic in a background refresh is contained - a chain id flip is latched and refused as an error instead of being adopted and served as healthy - single-flight and retry pacing now also apply to a cold cache Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(evm): simplify the TTL cache and its call sites - TTLValue moves to common/ - it is domain-free infrastructure and the next likely consumer (btc GetChainInfo) cannot import a coin package - ttl and fetch are set once at construction: Get(now) is the whole call site, the per-request method-value allocation on the hot path is gone, and the conditionally-legal nil fetch argument no longer exists - lastErr and failingSince fold into one ttlFailure struct so the invariant between them cannot be half-updated - stateLocked no longer releases a mutex it did not acquire - the preset panic error becomes a package sentinel (juju errors.New does a runtime.Caller per call) - avalanche: the cachedNodeVersion/nodeVersionCached ladder collapses into probeNodeVersion returning (string, error) with the real RPC error instead of a ""-as-error round trip; the four decoded-but-unread response fields go away, dropping the repo's only avalanchego import (go.mod tidy left for an environment that can write the module cache) - validatedChainID becomes atomic.Uint64 with CompareAndSwap so its safety no longer rests on a cross-file comment - the avalanche test file re-tested TTLValue mechanics through the deleted wrapper; the mechanics tests live in common with the type Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…etention (#1749) * fix(eth): accept an alternative cache retention equal to the mempool's mempoolRetentionInverted rejected alternativeMempoolTxTimeout == mempoolTxTimeout, so a deployment could not hold a private transaction as pending for exactly as long as its address index survives - it had to give up a margin on one side or the other. Equality is not the inverted case the guard exists for. SendRawTransaction stores the cache entry before AddTransactionToMempool, so the cache timestamp is never the younger one, and the cache's own exits clear the wrapped mempool too. Only a cache configured to strictly outlive the mempool can leave a private transaction indexed nowhere while still served as pending (#1573), so that alone stays rejected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(configs): hold private transactions for the whole mempool retention A privately relayed transaction stuck behind a nonce gap has been mined ~40 hours after broadcast. At the previous alternativeMempoolTxTimeout of 3h the cache dropped it long before that, so the sender saw nothing pending and had nothing to cancel while the transaction was still mineable. Raise it to each coin's mempoolTxTimeout - 48h on ethereum, 12h elsewhere - which is the longest the retention ordering permits, so the transaction stays visible and its nonce slot stays reserved for as long as its address index lives. robinhood_archive gains the explicit mempoolTxTimeout the others already carried. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(eth): log when the alternative provider stops or resumes surfacing a tx Whether a relay keeps answering eth_getTransactionByHash beyond its advertised pending window was only inferable from the residence histogram, and not at all while the cache retention was shorter than that window - every entry left first. It decides whether a raised alternativeMempoolTxTimeout can keep a nonce-gapped transaction pending at all, or whether the missing eviction takes it first. Log the surfacing transitions instead: the first null of a run, a run the relay ends by answering again, and each eviction, distinguishing a cache timeout reached with the relay still answering from one reached on nulls. Logged per transition rather than per probe, with the sender and nonce so the gap holding the transaction back is visible. markMissing now reports whether it opened the run and clearMissing returns the run it ended, which is what makes once-per-transition logging possible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(metrics): extend cache residence buckets to the raised retention The buckets stopped at 12600s, chosen for a 3h cache retention. With the retention now at 12h and 48h every long-lived exit falls into +Inf, hiding exactly the split the raise was made to observe - a transaction the relay surfaced until the cache timeout versus one evicted on nulls hours earlier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(eth): drop the assumption that the cache retention equals the relay window Three places reasoned from the cache retention being the relay's 3h pending window, so reaching the timeout meant the window had just closed and a final null was expected. With the retention at 12h/48h that is inverted: an entry only gets that far if the relay surfaced it nearly the whole time, so a null on the final probe is a coincidence - which is exactly what the two timeout log lines now tell apart. Also correct the alternativeMissingTxTimeout guidance: timeout-only eviction needs it at or above the cache retention, which the pending window no longer implies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(metrics): remove the colon that broke metrics.yaml parsing The bucket-extension rationale was appended to a plain YAML scalar with a colon in it, which ends the scalar and reads as a mapping value. metrics.yaml is embedded and parsed by every package that builds a metrics registry, so the whole unit-test suite failed on it. The help texts here use dashes for this reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(eth): restamp the wrapped mempool entry when a re-send overwrites its cache entry A re-send of the same raw tx overwrites the cache entry with a fresh timestamp, while AddTransactionToMempool keeps an existing wrapped entry's original one. With the equal retentions the configs now ship, the mempool sweep then drops the address index a whole rebroadcast gap before the cache stops serving the tx as pending - the #1573 inversion the removed 45h margin used to absorb. insertMempoolTx now reports the same-txid overwrite and cacheMempoolTransaction mirrors it into the wrapped mempool by removing and re-adding the entry, so both stores age from the re-send. The fix lives at this call site because the resync path calls AddTransactionToMempool for every backend tx - refreshing there would defeat the sweep. The false "written first, never younger" justification in the guard comment and evm-send-mempools.md now cites the restamp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(eth): make the surfacing boundary logs match the exits reconcile takes The missing-run transition promised eviction after missingTimeout, but the check only reruns at the next due probe - 15 min for entries older than an hour - so the eviction log three lines later routinely reported 7.5x the promised horizon. The transition now names the real one, max(missingTimeout, probeInterval). The probe-error branch also evicts at the cache timeout but logged only the generic failure line, which fires identically for retained entries - during a relay outage every boundary crossing went unrecorded. It now logs its own boundary exit like the other two. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(eth): back reconcile probes off to hourly past the relay's pending window The ladder capped at 15 min, a cadence calibrated for the old 3h retention; at 48h that is ~190 probes for one stuck tx, each a fresh dial plus an eth_getTransactionCount per URL, against quota treated as scarce (#1629). The hourly rung cuts the multi-hour tail ~4x for up to an hour of drop-detection latency on entries already stuck past 3h; timeout eviction is checked before the backoff gate and is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(eth): point the retention error at the effective knob and log drift The startup error suggested lowering alternativePendingTxWindow, which AlternativeMempoolTxTimeoutDuration ignores once an explicit alternativeMempoolTxTimeout is set - and every shipped config now sets one, so the suggested remedy could never resolve the failure. Name the knob that feeds the comparison. The guard also only rejects one drift direction: a later bump of mempoolTxTimeout alone silently detaches the aligned retentions. That direction is safe but shortens private-tx visibility, so log it once at startup instead of guessing in a config review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(config): scope the raised-retention promise to relay surfacing behavior With alternativeMissingTxTimeout left at its 2 min default, a relay that stops answering at its advertised ~3h window has every stuck tx evicted as provider_missing at roughly that window - the full cache retention is only reachable when the relay keeps surfacing past it, which the new transition logs are shipped to establish. Say so instead of stating the outcome as fact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(eth): let the wrapped mempool own the re-send restamp The caller-side remove + re-add reimplemented "refresh this entry's time" through the coarsest API available: it briefly dropped the tx from the address index, re-ran createTxEntry with a duplicate OnNewTx push per rebroadcast, and keyed the decision on the provider cache rather than the store being restamped - missing a tx indexed earlier from the public feed and then re-sent privately. AddOrRefreshTransactionInMempool on MempoolEthereumType now restamps an existing entry in place under one lock; the cache path calls it unconditionally and insertMempoolTx returns a single bool again. The resync path keeps calling AddTransactionToMempool, whose doc now states that an existing entry keeps its original time. Also from the cleanup pass: the retention-drift startup log is gated on an explicit mempoolTxTimeout via a mempoolRetentionDrifted predicate (the derived default is cache + 30m by construction and logged as drift on every boot), CreateMempool's two cacheEnabled branches merged, the timed-out probe-error exit logs one line carrying the error instead of stacking on the generic warning, a hand-rolled maximum became the builtin max, and an unreachable nil guard in SetupMempool is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(eth): trim branch comments to brief why-focused notes Condense the comments this branch added to 1-2-line clean-code notes, dropping arithmetic, cross-references and rationale already stated at the log sites or in the production comments the tests mirror. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(eth): align stale pending-window guidance with the raised cache retention The retention raise decoupled the cache timeout from the 3h pending window, but the alternativeMissingTxTimeout remedy in the ethrpc.go doc comments and evm-send.md still pointed at the window, and the evm-send-mempools.md diagrams still hardcoded the 3h/+30min pair, the AddTransactionToMempool coupling and the pre-backoff probe tiers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(btc): serve mempool.space precise recommended fees (#1754) Add a mempoolspaceprecise alternative fee provider consuming mempool.space/api/v1/fees/precise (sub-sat/vB recommended fees) with block targets mapped to Suite levels: 1->fastestFee, 3->halfHourFee, 6->hourFee, 500->economyFee, 1008->minimumFee. Conversion is exact (no significant-digit rounding) so served values match the mempool.space UI. Existing providers are untouched for backwards compatibility; bitcoin, regtest and testnet4 configs switch to the new provider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(btc): validate precise fees after conversion, not before A raw positivity check let non-finite or extreme values through: 1e100 or NaN overflow int(math.Round(fee*1000)) with an implementation-dependent result (negative on amd64) and values below 0.0005 round to zero, in both cases caching an invalid fee instead of keeping the previous table. Range-check the rounded sat/kB value before the float->int conversion and validate the converted entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A Tron send is the only moment Blockbook sees a transaction before it is indexed, and with no pending-tx feed a later "my tx vanished" report has nothing to reconcile against. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…routine (#1764) Records in addrContractsCache are handed out of getUnpackedAddrDescContracts after addrContractsCacheMux is released, and the block-connect path then mutates them (Contracts append, Ids/MultiTokenValues insert, Txs and TotalTxs counters) with no lock held. The 5-minute periodicStoreAddrContractsCache goroutine packed those same records concurrently, holding only addrContractsCacheMux, which gives no mutual exclusion against that mutation. BulkConnect's Ethereum path takes no lock at all. Because packUnpackedAddrContracts reads a slice length and then ranges the slice as two separate reads, and cfAddressContracts is a positional varint format whose element counts the unpacker trusts, an overlapping flush could emit a record whose remaining bytes are shifted. Replace the timer goroutine with storeAddrContractsCacheIfDue, called from ConnectBlock and BulkConnect.connectBlockEthereumType once the period elapses. The flush now runs on the only goroutine that mutates these records, so no additional locking or copying is needed. This mirrors flushAddrContractsCacheIfOverCap, which already flushes synchronously from the same path. Dropping the goroutine also removes a shutdown hazard: it had no stop channel and was never joined, so a tick landing inside closeDB() would have used column family handles that were already destroyed. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contract metadata strings are read from name()/symbol()/tokenURI() via eth_call and decoded without any length limit, so an unusual contract can return an oversized string that is then decoded and, for name/symbol, stored in the cfContracts column family unbounded. Add two independent bounds: - cap the decoded string in parseSimpleStringProperty at 64 KiB, covering every reader including the live, unstored tokenURI path - clamp stored name/symbol to 256 bytes (rune-safe) at packContractInfo, the single funnel for every write path (EVM single, Multicall3 batch, Tron) Add regression tests for both bounds. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GetXpubUtxo labelled every utxo with its branch position in data.addresses instead of the change index that branch is configured with, so any descriptor whose change indexes are not 0,1 in that order got a path pointing at a different address of the same account. GetXpubAddress already translates the position (fixed in ca3a023); this brings the utxo path in line with it. The two forms that hit it in practice are a single-branch change descriptor (.../1/*, as exported by bitcoin-cli listdescriptors) and a reordered or non-default multipath descriptor (.../<1;0>/*). Bare xpubs and .../<0;1>/* are unaffected because position and index coincide, which is why the existing coverage never caught it. Affects /api/v2/utxo/<descriptor> and the websocket getAccountUtxo. Only the path field was wrong; address, txid, vout and value were not. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd (#1756) * fix(sync): un-wedge steady-state EVM sync that falls behind the backend The sequential connect loop could never re-evaluate the parallel gate while it ran: on chains producing blocks faster than the serial per-block cost (block + logs + trace), the round never terminated and the index diverged from the tip indefinitely (~130k blocks/day observed on Robinhood Chain). - getBlockChain now probes the backend tip every 100 connected blocks and yields errResync when it falls more than max(4*workers, 64) blocks behind, letting resyncIndex re-enter the parallel path; yields are counted in blockbook_index_sync_yields{reason="fell-behind"} - the steady-state parallel pool size now comes from the -workers flag (capped by the block range) instead of a hardcoded 4; the trigger threshold stays at 4 so small rounds keep their current behavior Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(sync): rename fell-behind yield reason to fell_behind and document it Match the snake_case convention of the metric's other label values and add the third reason to metrics.yaml, docs/sync.md and the Grafana panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed hash (#1742) * fix(sync): don't treat a fetchable block as a reorg without an expected hash getBlockChain passes its loop hash to shouldRestartSyncOnMissingBlock, but ethereum-type chains leave BlockHeader.Next unset, so that hash is empty for every block after the first of a sync round. The probe then compares a freshly fetched hash against "" and always finds them unequal, yielding errResync and turning normal backend tip lag into a full resync round per block. Observed on avax.trezor.io, where QuickNode's load-balanced pool answers eth_getBlockByNumber for the tip with coreth's "cannot query unfinalized data" until the serving node has accepted the block. Forks remain covered: getBlockChain's prevHash check catches a replaced block, resyncIndex compares its own hash at localBestHeight, and the parallel path is untouched because it always queues a real hash from GetBlockHash. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(avalanche): widen tip recheck window for backend acceptance lag tipRecheckThreshold=3 combined with the sequential path's internal 250 ms cap allows only ~500 ms before a missing tip block is treated as suspicious, which sits inside the normal acceptance skew of a load-balanced Avalanche pool. Raise both thresholds to 14 (~3.25 s, roughly three AVAX blocks); recheckThreshold has to move as well or ApplyMissingBlockRetryOverride clamps the tip value back down to 10. retryDelayMs stays at 1000 on purpose: the 250 ms cap applies only to the sequential tip path, while getBlockWorker uses the raw value, so lowering it would quietly speed up bulk-sync retries. avalanche.json carried no override at all and was inheriting the same too-tight defaults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(eth): keep block-not-found sentinels unwrappable from getBlockRaw errors.Annotatef from the juju/errors pin has no Unwrap, so annotating a bchain.ErrBlockNotFound made stdErrors.Is fail for every caller downstream. In getBlockChain that silently disabled two things: the end-of-chain exit at the tip, gated on gotNotFound, and the IndexBlockNotFoundRetries counter. It only shows up where a tip read fails with an error rather than a null result, which on Avalanche is every tip read ("cannot query unfinalized data"), so the sequential path never returned. Measured on blockbook-dev: index_block_not_found_retries=0 next to index_resync_errors{error="get_block"} =1594 over ~330 blocks — ~4.8 retries per block, one 1.17 s block period divided by the 250 ms retry delay. With no ResyncIndex iteration ever completing, updateBackendInfo stopped running, leaving blockbook_synchronized at 0 and backend_best_height frozen while best_height kept advancing. Return the sentinel unannotated so it stays comparable, matching what the null-result branch directly below already does. Genuine tip skew still retries, since height > bestHeight is false in that case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(eth): keep block-not-found sentinel unwrappable from eth_getLogs path Review follow-ups for PR #1742: processEventsForBlock gets the same sentinel carve-out as getBlockRaw so getBlockChain's end-of-chain exit and retry accounting also work when a lagging pool member rejects eth_getLogs, and onRetryableMiss no longer logs a chain-state recheck on the by-height walk where shouldRestartSyncOnMissingBlock is a no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(eth): stop indexing CALLCODE frames as internal transfers CALLCODE runs foreign code in the caller's own context, exactly like DELEGATECALL, so the value the callTracer reports on such a frame never leaves the caller. Indexing from->to credited the code address with ETH it never received, letting a contract fabricate arbitrary internal transfers to arbitrary addresses (issue #1225). Fixes #1225 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(eth): index internal transfers only for CALL frames, warn on unknown types Invert the trace-type deny-list into an explicit CALL allowlist so a future value-carrying frame type cannot silently reopen the fake-transfer vector (#1225); unknown types with value are logged instead of indexed. STATICCALL joins the ignore branch so it is safe by design, not by the tracer omitting its value field. Also replace the test-local hexToBig helper with hexutil.MustDecodeBig. Addresses review comments on #1738. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both Ethereum-type confirmation counts are unsigned subtractions with no check that the reference height is actually ahead. computeConfirmations subtracts the asynchronously cached tip from a height read straight from the backend, and the tx cache subtracts the indexed best height from the chain height an Ethereum-type tx was cached at. Either reference can be behind by a block or more, and the count then wraps to ~4.29e9 instead of a small number. Clamp both to 1, the mined minimum, matching the guard Tron's computeBlockConfirmations already has. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(api): serialize xpub cache updates to prevent data race The process-wide xpub cache handed out xpubData values whose addresses backing arrays were shared, then mutated them outside cachedXpubsMux. Two concurrent getAccountInfo requests (details >= txids) for the same descriptor could race and, across a block connect, drop the newest confirmed txid while leaving a matching (too-low) count and a correct balance - a self-consistent but incomplete history served for roughly one block interval. Serialize updates per descriptor with a sharded lock and copy-on-write the address slices, so every published cache entry is an immutable, coherent snapshot. Distinct descriptors stay parallel and the caller's per-tx response building stays concurrent. Add a -race regression test that reproduces the write/write and read/write races on the pre-fix code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(api): keep warm xpub cache hits parallel and skip needless clones Address review comments: - return the published snapshot without the per-descriptor lock when the request cannot mutate it (same tip, same gap, no txid load needed) - clone the address matrix only when a rescan or txid load will mutate it - extract needsRescan so the CoW guard and rescan trigger cannot drift - test: preserve the initXpubCache nil sentinel, use t.TempDir/t.Cleanup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…1736) * fix(api): match the outpoint when resolving a spending transaction Without the extended index there is no direct output -> spender index, so setSpendingTxToVout finds the spender by walking the address index and matching on address, value and spending txid. It never checked which output the candidate input references, so two outputs of one transaction paying the same value to the same address both resolved to whichever spender was found first - reporting a transaction that does not spend that output, and an equally arbitrary spentIndex. Require the input to reference this vout as well. Also return early for an unspent output: it has no spender, and the scan has no early exit for that case, so it would walk the address index all the way to the chain tip. GetTransactionFromBchainTx already guards the same call this way. Instances built with -extendedindex read the exact per-output SpentTxid and were never affected. The per-output spent flag was always correct, so this never affected balances or UTXO selection. Fixes #1029 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(api): address review comments on the spending txid test - add the //go:build unittest tag the package's other tests carry - pin the outpoint match directly: call setSpendingTxToVout on the unspent sibling, which the guard in GetSpendingTxid would skip - use newRefreshTestMetrics instead of uniquified process-wide metrics - use t.TempDir/t.Cleanup, drop the manual cleanup func - build fixture blocks via the exported dbtestdata accessors - pass a nil mempool; the tested path never touches it and the real one leaks sync goroutines - drop cases covering pre-existing behavior (n=0, out-of-range vout) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.