Skip to content

Commit a43f621

Browse files
committed
retry solana blocks if amount of transactions is suspicious
1 parent 105da0e commit a43f621

File tree

4 files changed

+64
-9
lines changed

4 files changed

+64
-9
lines changed

solana/solana-dump/src/dumper.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface Options extends DumperOptions {
1717
maxConfirmationAttempts: number
1818
assertLogMessagesNotNull: boolean
1919
validateChainContinuity: boolean
20+
txThreshold?: number
2021
}
2122

2223

@@ -31,6 +32,7 @@ export class SolanaDumper extends Dumper<Block, Options> {
3132
program.option('--max-confirmation-attempts <N>', 'Maximum number of confirmation attempts', positiveInt, 10)
3233
program.option('--assert-log-messages-not-null', 'Check if tx.meta.logMessages is not null', false)
3334
program.option('--validate-chain-continuity', 'Check if block parent hash matches previous block hash', false)
35+
program.option('--tx-threshold <N>', 'Retry getBlock call if transactions count is less than threshold')
3436
}
3537

3638
protected fixUnsafeIntegers(): boolean {
@@ -69,7 +71,8 @@ export class SolanaDumper extends Dumper<Block, Options> {
6971
url: options.endpoint,
7072
capacity: Number.MAX_SAFE_INTEGER,
7173
retryAttempts: Number.MAX_SAFE_INTEGER,
72-
requestTimeout: 30_000
74+
requestTimeout: 30_000,
75+
txThreshold: options.txThreshold,
7376
})
7477

7578
return new SolanaRpcDataSource({

solana/solana-rpc/src/rpc-remote.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type RemoteRpcOptions = Pick<
1818
* Remove vote transactions from all relevant responses
1919
*/
2020
noVotes?: boolean
21+
txThreshold?: number
2122
}
2223

2324

solana/solana-rpc/src/rpc-worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {Commitment, GetBlockOptions, Rpc} from './rpc'
55
import type {RemoteRpcOptions} from './rpc-remote'
66

77

8-
const {noVotes, ...rpcOptions} = getServerArguments<RemoteRpcOptions>()
8+
const {noVotes, txThreshold, ...rpcOptions} = getServerArguments<RemoteRpcOptions>()
99

1010
const rpc = new Rpc(new RpcClient({
1111
...rpcOptions,
1212
fixUnsafeIntegers: true
13-
}))
13+
}), txThreshold)
1414

1515

1616
getServer()

solana/solana-rpc/src/rpc.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {createLogger} from '@subsquid/logger'
2-
import {CallOptions, RpcClient, RpcError, RpcProtocolError} from '@subsquid/rpc-client'
3-
import {RpcCall, RpcErrorInfo} from '@subsquid/rpc-client/lib/interfaces'
4-
import {GetBlock} from '@subsquid/solana-rpc-data'
2+
import {CallOptions, RetryError, RpcClient, RpcError, RpcProtocolError} from '@subsquid/rpc-client'
3+
import {RpcCall, RpcErrorInfo, RpcRequest} from '@subsquid/rpc-client/lib/interfaces'
4+
import {GetBlock, isVoteTransaction} from '@subsquid/solana-rpc-data'
5+
import {assertNotNull} from '@subsquid/util-internal'
56
import {
67
array,
78
B58,
@@ -49,10 +50,18 @@ export interface RpcApi {
4950

5051

5152
export class Rpc implements RpcApi {
53+
private requests: ThresholdRequests
54+
5255
constructor(
5356
public readonly client: RpcClient,
54-
public readonly log = createLogger('sqd:solana-rpc')
55-
) {}
57+
public readonly txThreshold?: number,
58+
public readonly log = createLogger('sqd:solana-rpc'),
59+
) {
60+
if (this.txThreshold != null) {
61+
assert(this.txThreshold > 0)
62+
}
63+
this.requests = new ThresholdRequests()
64+
}
5665

5766
call<T=any>(method: string, params?: any[], options?: CallOptions<T>): Promise<T> {
5867
return this.client.call(method, params, options)
@@ -107,7 +116,7 @@ export class Rpc implements RpcApi {
107116
call[i] = {method: 'getBlock', params}
108117
}
109118
return this.reduceBatchOnRetry<GetBlock | 'skipped' | null | undefined>(call, {
110-
validateResult: getResultValidator(nullable(GetBlock)),
119+
validateResult: (result, req) => this.validateGetBlockResult(result, req),
111120
validateError: captureNoBlockAtSlot
112121
})
113122
}
@@ -132,6 +141,23 @@ export class Rpc implements RpcApi {
132141

133142
return pack.flat()
134143
}
144+
145+
validateGetBlockResult(result: unknown, req: RpcRequest) {
146+
let validator = getResultValidator(nullable(GetBlock))
147+
let block = validator(result)
148+
if (this.txThreshold && block != null && block.transactions != null) {
149+
let transactions = block.transactions.filter(tx => !isVoteTransaction(tx))
150+
if (transactions.length < this.txThreshold) {
151+
let slot = req.params![0] as any as number
152+
let retries = this.requests.get(slot)
153+
if (retries < 3) {
154+
this.requests.inc(slot)
155+
throw new RetryError(`transactions count is less than threshold: ${transactions.length} < ${this.txThreshold}`)
156+
}
157+
}
158+
}
159+
return block
160+
}
135161
}
136162

137163

@@ -152,3 +178,28 @@ function getResultValidator<V extends Validator>(validator: V): (result: unknown
152178
}
153179
}
154180
}
181+
182+
183+
class ThresholdRequests {
184+
inner: Map<number, number>
185+
186+
constructor() {
187+
this.inner = new Map()
188+
}
189+
190+
inc(slot: number) {
191+
if (this.inner.size > 100) {
192+
let keys = this.inner.keys()
193+
for (let i = 0; i < 20; i++) {
194+
let res = keys.next()
195+
this.inner.delete(assertNotNull(res.value))
196+
}
197+
}
198+
let val = this.inner.get(slot) ?? 0
199+
this.inner.set(slot, val + 1)
200+
}
201+
202+
get(slot: number) {
203+
return this.inner.get(slot) ?? 0
204+
}
205+
}

0 commit comments

Comments
 (0)