Skip to content

Conversation

@tmcgroul
Copy link
Contributor

this change supposed to retry getBlock request if amount of transactions is suspiciously small

let validator = getResultValidator(nullable(GetBlock))
let block = validator(result)
if (this.txThreshold && block != null && block.transactions != null) {
let transactions = block.transactions.filter(tx => !isVoteTransaction(tx))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

my bad i didn't understand my measurements at first. it doesn't take ~30ms to filter it's just ~0.030ms in average

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds retry logic for Solana getBlock RPC requests when the number of non-vote transactions in a block is suspiciously low (below a configurable threshold). The feature helps handle cases where RPC nodes may return incomplete block data.

Changes:

  • Adds transaction threshold validation to retry getBlock requests with fewer non-vote transactions than expected
  • Introduces ThresholdRequests class to track retry attempts per slot (max 3 retries)
  • Adds optional txThreshold parameter throughout the stack (CLI, RPC client, worker)

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.

File Description
solana/solana-rpc/src/rpc.ts Core implementation: adds validateGetBlockResult method, ThresholdRequests class, and constructor changes for threshold support
solana/solana-rpc/src/rpc-worker.ts Passes txThreshold from worker options to Rpc constructor
solana/solana-rpc/src/rpc-remote.ts Adds txThreshold to RemoteRpcOptions type definition
solana/solana-dump/src/dumper.ts Adds CLI option --tx-threshold and passes it to RemoteRpcPool

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +190 to +197
inc(slot: number) {
if (this.inner.size > 100) {
let keys = this.inner.keys()
for (let i = 0; i < 20; i++) {
let res = keys.next()
this.inner.delete(assertNotNull(res.value))
}
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The ThresholdRequests.inc method uses a non-deterministic cleanup strategy that deletes the first 20 entries from the Map when size exceeds 100. Since Map iteration order is insertion order, this means older slots get removed. However, if batch requests are processed out-of-order or slots are retried non-sequentially, this could incorrectly delete entries for slots that are still being retried. Consider using a more robust cleanup strategy, such as removing entries for slots that are outside the current processing window, or using a timestamp-based eviction policy.

Copilot uses AI. Check for mistakes.
program.option('--max-confirmation-attempts <N>', 'Maximum number of confirmation attempts', positiveInt, 10)
program.option('--assert-log-messages-not-null', 'Check if tx.meta.logMessages is not null', false)
program.option('--validate-chain-continuity', 'Check if block parent hash matches previous block hash', false)
program.option('--tx-threshold <N>', 'Retry getBlock call if transactions count is less than threshold')
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The CLI option for tx-threshold does not specify a parser function (like positiveInt) to validate the input. This means the value will be parsed as a string rather than a number, which will cause issues when passed to the Rpc constructor. Add positiveInt as the parser function to ensure proper validation and type conversion.

Suggested change
program.option('--tx-threshold <N>', 'Retry getBlock call if transactions count is less than threshold')
program.option('--tx-threshold <N>', 'Retry getBlock call if transactions count is less than threshold', positiveInt)

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +156
if (retries < 3) {
this.requests.inc(slot)
throw new RetryError(`transactions count is less than threshold: ${transactions.length} < ${this.txThreshold}`)
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The retry counting logic increments the counter before throwing the RetryError. This creates an off-by-one issue where the actual number of retries will be one less than expected. When retries reaches 2, it increments to 3, but then the next attempt (which should be the 4th total attempt) won't retry because retries < 3 will be false. The condition should check retries < 2 if you want to allow 3 total attempts (1 initial + 2 retries), or increment after throwing if you want the current behavior to match the intended semantics.

Copilot uses AI. Check for mistakes.


class ThresholdRequests {
inner: Map<number, number>
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The 'inner' field of ThresholdRequests is not marked as private, making it accessible from outside the class. This breaks encapsulation and allows external code to directly modify the internal state. Consider making this field private to maintain proper encapsulation.

Suggested change
inner: Map<number, number>
private inner: Map<number, number>

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +160
validateGetBlockResult(result: unknown, req: RpcRequest) {
let validator = getResultValidator(nullable(GetBlock))
let block = validator(result)
if (this.txThreshold && block != null && block.transactions != null) {
let transactions = block.transactions.filter(tx => !isVoteTransaction(tx))
if (transactions.length < this.txThreshold) {
let slot = req.params![0] as any as number
let retries = this.requests.get(slot)
if (retries < 3) {
this.requests.inc(slot)
throw new RetryError(`transactions count is less than threshold: ${transactions.length} < ${this.txThreshold}`)
}
}
}
return block
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The validateGetBlockResult method lacks documentation explaining the retry logic and the threshold behavior. Consider adding a JSDoc comment that explains: (1) what txThreshold represents, (2) why vote transactions are filtered out before checking the threshold, (3) the maximum number of retries (currently 3), and (4) what happens when retries are exhausted (the block is returned as-is).

Copilot uses AI. Check for mistakes.
@@ -18,6 +18,7 @@ export type RemoteRpcOptions = Pick<
* Remove vote transactions from all relevant responses
*/
noVotes?: boolean
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The txThreshold field in RemoteRpcOptions lacks documentation. Consider adding a JSDoc comment explaining what this parameter controls, similar to how the noVotes field is documented. This helps users of the API understand the purpose and usage of this option.

Suggested change
noVotes?: boolean
noVotes?: boolean
/**
* Limit the number of transactions returned for a block or response.
* If set, blocks with more transactions than this threshold may be truncated.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +62
if (this.txThreshold != null) {
assert(this.txThreshold > 0)
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The assertion checks that txThreshold > 0, but there's no check for what happens when txThreshold is 0. While the condition 'if (this.txThreshold && ...)' on line 148 would treat 0 as falsy and skip the validation, it would be clearer to either: (1) explicitly document that 0 disables the feature, or (2) include 0 in the assertion if it's not a valid value. This improves code clarity and prevents confusion about the intended behavior.

Copilot uses AI. Check for mistakes.
if (this.txThreshold && block != null && block.transactions != null) {
let transactions = block.transactions.filter(tx => !isVoteTransaction(tx))
if (transactions.length < this.txThreshold) {
let slot = req.params![0] as any as number
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The type cast 'as any as number' is used to extract the slot from req.params. While this works, it's fragile and bypasses TypeScript's type safety. Consider using a more type-safe approach, such as checking that req.params is defined and that the first element is a number before casting, or defining a proper type for the getBlock request parameters.

Suggested change
let slot = req.params![0] as any as number
if (!Array.isArray(req.params) || typeof req.params[0] !== 'number') {
throw new DataValidationError('invalid getBlock request parameters: expected numeric slot as first parameter')
}
let slot = req.params[0]

Copilot uses AI. Check for mistakes.
let retries = this.requests.get(slot)
if (retries < 3) {
this.requests.inc(slot)
throw new RetryError(`transactions count is less than threshold: ${transactions.length} < ${this.txThreshold}`)
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The error message uses 'transactions count' which is grammatically awkward. Consider changing it to 'transaction count' (singular) for better readability, as 'count' is already a measure of quantity.

Copilot uses AI. Check for mistakes.
program.option('--max-confirmation-attempts <N>', 'Maximum number of confirmation attempts', positiveInt, 10)
program.option('--assert-log-messages-not-null', 'Check if tx.meta.logMessages is not null', false)
program.option('--validate-chain-continuity', 'Check if block parent hash matches previous block hash', false)
program.option('--tx-threshold <N>', 'Retry getBlock call if transactions count is less than threshold')
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The description for the tx-threshold option mentions 'transactions count' which is grammatically awkward (similar to the error message in rpc.ts). Consider changing it to 'transaction count' for consistency and better readability.

Copilot uses AI. Check for mistakes.
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.

3 participants