-
Notifications
You must be signed in to change notification settings - Fork 191
retry solana blocks if amount of transactions is suspicious #424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: open-beta
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ interface Options extends DumperOptions { | |
| maxConfirmationAttempts: number | ||
| assertLogMessagesNotNull: boolean | ||
| validateChainContinuity: boolean | ||
| txThreshold?: number | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -31,6 +32,7 @@ export class SolanaDumper extends Dumper<Block, Options> { | |
| 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') | ||
|
||
| } | ||
|
|
||
| protected fixUnsafeIntegers(): boolean { | ||
|
|
@@ -69,7 +71,8 @@ export class SolanaDumper extends Dumper<Block, Options> { | |
| url: options.endpoint, | ||
| capacity: Number.MAX_SAFE_INTEGER, | ||
| retryAttempts: Number.MAX_SAFE_INTEGER, | ||
| requestTimeout: 30_000 | ||
| requestTimeout: 30_000, | ||
| txThreshold: options.txThreshold, | ||
| }) | ||
|
|
||
| return new SolanaRpcDataSource({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,7 @@ export type RemoteRpcOptions = Pick< | |||||||||||||
| * Remove vote transactions from all relevant responses | ||||||||||||||
| */ | ||||||||||||||
| noVotes?: boolean | ||||||||||||||
|
||||||||||||||
| 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. | |
| */ |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,8 @@ | ||||||||||||
| import {createLogger} from '@subsquid/logger' | ||||||||||||
| import {CallOptions, RpcClient, RpcError, RpcProtocolError} from '@subsquid/rpc-client' | ||||||||||||
| import {RpcCall, RpcErrorInfo} from '@subsquid/rpc-client/lib/interfaces' | ||||||||||||
| import {GetBlock} from '@subsquid/solana-rpc-data' | ||||||||||||
| import {CallOptions, RetryError, RpcClient, RpcError, RpcProtocolError} from '@subsquid/rpc-client' | ||||||||||||
| import {RpcCall, RpcErrorInfo, RpcRequest} from '@subsquid/rpc-client/lib/interfaces' | ||||||||||||
| import {GetBlock, isVoteTransaction} from '@subsquid/solana-rpc-data' | ||||||||||||
| import {assertNotNull} from '@subsquid/util-internal' | ||||||||||||
| import { | ||||||||||||
| array, | ||||||||||||
| B58, | ||||||||||||
|
|
@@ -49,10 +50,18 @@ export interface RpcApi { | |||||||||||
|
|
||||||||||||
|
|
||||||||||||
| export class Rpc implements RpcApi { | ||||||||||||
| private requests: ThresholdRequests | ||||||||||||
|
|
||||||||||||
| constructor( | ||||||||||||
| public readonly client: RpcClient, | ||||||||||||
| public readonly log = createLogger('sqd:solana-rpc') | ||||||||||||
| ) {} | ||||||||||||
| public readonly txThreshold?: number, | ||||||||||||
| public readonly log = createLogger('sqd:solana-rpc'), | ||||||||||||
| ) { | ||||||||||||
|
Comment on lines
55
to
+59
|
||||||||||||
| if (this.txThreshold != null) { | ||||||||||||
| assert(this.txThreshold > 0) | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+60
to
+62
|
||||||||||||
| this.requests = new ThresholdRequests() | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| call<T=any>(method: string, params?: any[], options?: CallOptions<T>): Promise<T> { | ||||||||||||
| return this.client.call(method, params, options) | ||||||||||||
|
|
@@ -107,7 +116,7 @@ export class Rpc implements RpcApi { | |||||||||||
| call[i] = {method: 'getBlock', params} | ||||||||||||
| } | ||||||||||||
| return this.reduceBatchOnRetry<GetBlock | 'skipped' | null | undefined>(call, { | ||||||||||||
| validateResult: getResultValidator(nullable(GetBlock)), | ||||||||||||
| validateResult: (result, req) => this.validateGetBlockResult(result, req), | ||||||||||||
| validateError: captureNoBlockAtSlot | ||||||||||||
| }) | ||||||||||||
| } | ||||||||||||
|
|
@@ -132,6 +141,23 @@ export class Rpc implements RpcApi { | |||||||||||
|
|
||||||||||||
| return pack.flat() | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| 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)) | ||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||
| if (transactions.length < this.txThreshold) { | ||||||||||||
| let slot = req.params![0] as any as number | ||||||||||||
|
||||||||||||
| 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
AI
Jan 30, 2026
There was a problem hiding this comment.
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
AI
Jan 30, 2026
There was a problem hiding this comment.
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
AI
Jan 30, 2026
There was a problem hiding this comment.
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
AI
Jan 30, 2026
There was a problem hiding this comment.
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.
| inner: Map<number, number> | |
| private inner: Map<number, number> |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.