@o1-labs/mina-archive-node-graphql 0.0.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +92 -268
  3. package/build/src/blockchain/utils.js.map +1 -1
  4. package/build/src/config.js +86 -0
  5. package/build/src/config.js.map +1 -0
  6. package/build/src/db/archive-node-adapter/archive-node-adapter.js +29 -2
  7. package/build/src/db/archive-node-adapter/archive-node-adapter.js.map +1 -1
  8. package/build/src/db/archive-node-adapter/postgres-options.js +35 -0
  9. package/build/src/db/archive-node-adapter/postgres-options.js.map +1 -0
  10. package/build/src/db/sql/best-chain.js +5 -0
  11. package/build/src/db/sql/best-chain.js.map +1 -0
  12. package/build/src/db/sql/events-actions/queries.js +102 -21
  13. package/build/src/db/sql/events-actions/queries.js.map +1 -1
  14. package/build/src/db/sql/verification-key-updates/queries.js +100 -0
  15. package/build/src/db/sql/verification-key-updates/queries.js.map +1 -0
  16. package/build/src/db/sql/zkapp-commands/queries.js +363 -0
  17. package/build/src/db/sql/zkapp-commands/queries.js.map +1 -0
  18. package/build/src/db/sql/zkapp-commands/types.js +2 -0
  19. package/build/src/db/sql/zkapp-commands/types.js.map +1 -0
  20. package/build/src/errors/error.js +4 -1
  21. package/build/src/errors/error.js.map +1 -1
  22. package/build/src/index.js +44 -7
  23. package/build/src/index.js.map +1 -1
  24. package/build/src/resolvers.js +42 -23
  25. package/build/src/resolvers.js.map +1 -1
  26. package/build/src/schema-version.js +2 -0
  27. package/build/src/schema-version.js.map +1 -0
  28. package/build/src/server/cors.js +41 -0
  29. package/build/src/server/cors.js.map +1 -0
  30. package/build/src/server/graceful-shutdown.js +42 -0
  31. package/build/src/server/graceful-shutdown.js.map +1 -0
  32. package/build/src/server/graphql-armor.js +39 -0
  33. package/build/src/server/graphql-armor.js.map +1 -0
  34. package/build/src/server/logger.js +45 -0
  35. package/build/src/server/logger.js.map +1 -0
  36. package/build/src/server/metrics.js +72 -0
  37. package/build/src/server/metrics.js.map +1 -0
  38. package/build/src/server/plugins.js +31 -15
  39. package/build/src/server/plugins.js.map +1 -1
  40. package/build/src/server/rate-limit.js +143 -0
  41. package/build/src/server/rate-limit.js.map +1 -0
  42. package/build/src/server/readiness.js +45 -0
  43. package/build/src/server/readiness.js.map +1 -0
  44. package/build/src/server/request-logging.js +58 -0
  45. package/build/src/server/request-logging.js.map +1 -0
  46. package/build/src/server/server.js +38 -13
  47. package/build/src/server/server.js.map +1 -1
  48. package/build/src/services/actions-service/actions-service.js +26 -15
  49. package/build/src/services/actions-service/actions-service.js.map +1 -1
  50. package/build/src/services/blocks-service/blocks-service.js +10 -9
  51. package/build/src/services/blocks-service/blocks-service.js.map +1 -1
  52. package/build/src/services/network-service/network-service.js +17 -9
  53. package/build/src/services/network-service/network-service.js.map +1 -1
  54. package/build/src/services/verification-key-updates-service/verification-key-updates-service.interface.js +2 -0
  55. package/build/src/services/verification-key-updates-service/verification-key-updates-service.interface.js.map +1 -0
  56. package/build/src/services/verification-key-updates-service/verification-key-updates-service.js +41 -0
  57. package/build/src/services/verification-key-updates-service/verification-key-updates-service.js.map +1 -0
  58. package/build/src/services/zkapp-commands-service/zkapp-commands-service.interface.js +2 -0
  59. package/build/src/services/zkapp-commands-service/zkapp-commands-service.interface.js.map +1 -0
  60. package/build/src/services/zkapp-commands-service/zkapp-commands-service.js +115 -0
  61. package/build/src/services/zkapp-commands-service/zkapp-commands-service.js.map +1 -0
  62. package/build/src/tracing/jaeger-tracing.js +2 -1
  63. package/build/src/tracing/jaeger-tracing.js.map +1 -1
  64. package/build/tests/devnet-dump/action-state-invariants.js +165 -0
  65. package/build/tests/devnet-dump/action-state-invariants.js.map +1 -0
  66. package/build/tests/devnet-dump/devnet-dump.test.js +24 -10
  67. package/build/tests/devnet-dump/devnet-dump.test.js.map +1 -1
  68. package/build/tests/devnet-dump/setup.js +63 -17
  69. package/build/tests/devnet-dump/setup.js.map +1 -1
  70. package/build/tests/integration/action-state-ordering.test.js +139 -0
  71. package/build/tests/integration/action-state-ordering.test.js.map +1 -0
  72. package/build/tests/integration/action-state-setup.js +48 -0
  73. package/build/tests/integration/action-state-setup.js.map +1 -0
  74. package/build/tests/integration/consensus-vs-chain.test.js +81 -0
  75. package/build/tests/integration/consensus-vs-chain.test.js.map +1 -0
  76. package/build/tests/integration/integration.test.js +166 -2
  77. package/build/tests/integration/integration.test.js.map +1 -1
  78. package/build/tests/integration/postgres-timeout.test.js +92 -0
  79. package/build/tests/integration/postgres-timeout.test.js.map +1 -0
  80. package/build/tests/integration/setup.js +58 -1
  81. package/build/tests/integration/setup.js.map +1 -1
  82. package/build/tests/integration/verification-key-setup.js +49 -0
  83. package/build/tests/integration/verification-key-setup.js.map +1 -0
  84. package/build/tests/integration/verification-key-updates.test.js +159 -0
  85. package/build/tests/integration/verification-key-updates.test.js.map +1 -0
  86. package/build/tests/live-api/events.test.js +11 -7
  87. package/build/tests/live-api/events.test.js.map +1 -1
  88. package/build/tests/live-api/networkState.test.js +1 -0
  89. package/build/tests/live-api/networkState.test.js.map +1 -1
  90. package/build/tests/live-network/live-network.test.js +1 -0
  91. package/build/tests/live-network/live-network.test.js.map +1 -1
  92. package/build/tests/resolvers.test.js +65 -37
  93. package/build/tests/resolvers.test.js.map +1 -1
  94. package/build/tests/services/verification-key-updates-service/verification-key-updates-service.test.js +35 -0
  95. package/build/tests/services/verification-key-updates-service/verification-key-updates-service.test.js.map +1 -0
  96. package/build/tests/services/zkapp-commands-service/zkapp-commands-service.test.js +159 -0
  97. package/build/tests/services/zkapp-commands-service/zkapp-commands-service.test.js.map +1 -0
  98. package/build/tests/test-helpers.js +15 -0
  99. package/build/tests/test-helpers.js.map +1 -1
  100. package/build/tests/unit/best-chain-anchor.test.js +70 -0
  101. package/build/tests/unit/best-chain-anchor.test.js.map +1 -0
  102. package/build/tests/unit/config.test.js +106 -0
  103. package/build/tests/unit/config.test.js.map +1 -0
  104. package/build/tests/unit/cors.test.js +95 -0
  105. package/build/tests/unit/cors.test.js.map +1 -0
  106. package/build/tests/unit/error-masking.test.js +119 -0
  107. package/build/tests/unit/error-masking.test.js.map +1 -0
  108. package/build/tests/unit/graceful-shutdown.test.js +99 -0
  109. package/build/tests/unit/graceful-shutdown.test.js.map +1 -0
  110. package/build/tests/unit/graphql-armor.test.js +121 -0
  111. package/build/tests/unit/graphql-armor.test.js.map +1 -0
  112. package/build/tests/unit/logging.test.js +120 -0
  113. package/build/tests/unit/logging.test.js.map +1 -0
  114. package/build/tests/unit/metrics.test.js +114 -0
  115. package/build/tests/unit/metrics.test.js.map +1 -0
  116. package/build/tests/unit/network-service.test.js +66 -0
  117. package/build/tests/unit/network-service.test.js.map +1 -0
  118. package/build/tests/unit/postgres-options.test.js +56 -0
  119. package/build/tests/unit/postgres-options.test.js.map +1 -0
  120. package/build/tests/unit/rate-limit.test.js +271 -0
  121. package/build/tests/unit/rate-limit.test.js.map +1 -0
  122. package/build/tests/unit/readiness.test.js +75 -0
  123. package/build/tests/unit/readiness.test.js.map +1 -0
  124. package/build/tests/unit/schema-nullability.test.js +40 -0
  125. package/build/tests/unit/schema-nullability.test.js.map +1 -0
  126. package/build/tests/unit/schema-version.test.js +48 -0
  127. package/build/tests/unit/schema-version.test.js.map +1 -0
  128. package/build/tests/unit/yoga-http-contract.test.js +30 -0
  129. package/build/tests/unit/yoga-http-contract.test.js.map +1 -0
  130. package/build/tests/unit/zkapp-commands-gating.test.js +38 -0
  131. package/build/tests/unit/zkapp-commands-gating.test.js.map +1 -0
  132. package/build/zkapp/utils.js +2 -0
  133. package/build/zkapp/utils.js.map +1 -1
  134. package/package.json +33 -14
  135. package/schema.graphql +126 -8
  136. package/src/blockchain/types.ts +65 -1
  137. package/src/blockchain/utils.ts +26 -2
  138. package/src/config.ts +133 -0
  139. package/src/db/archive-node-adapter/archive-node-adapter.interface.ts +26 -3
  140. package/src/db/archive-node-adapter/archive-node-adapter.ts +65 -2
  141. package/src/db/archive-node-adapter/postgres-options.ts +90 -0
  142. package/src/db/sql/best-chain.ts +34 -0
  143. package/src/db/sql/events-actions/queries.ts +168 -24
  144. package/src/db/sql/verification-key-updates/queries.ts +132 -0
  145. package/src/db/sql/zkapp-commands/queries.ts +406 -0
  146. package/src/db/sql/zkapp-commands/types.ts +43 -0
  147. package/src/envionment.d.ts +20 -0
  148. package/src/errors/error.ts +17 -1
  149. package/src/index.ts +64 -7
  150. package/src/resolvers-types.ts +311 -24
  151. package/src/resolvers.ts +70 -34
  152. package/src/schema-version.ts +24 -0
  153. package/src/server/cors.ts +97 -0
  154. package/src/server/graceful-shutdown.ts +76 -0
  155. package/src/server/graphql-armor.ts +78 -0
  156. package/src/server/logger.ts +72 -0
  157. package/src/server/metrics.ts +104 -0
  158. package/src/server/plugins.ts +53 -19
  159. package/src/server/rate-limit.ts +274 -0
  160. package/src/server/readiness.ts +78 -0
  161. package/src/server/request-logging.ts +83 -0
  162. package/src/server/server.ts +70 -15
  163. package/src/services/actions-service/actions-service.ts +87 -24
  164. package/src/services/blocks-service/blocks-service.ts +16 -14
  165. package/src/services/network-service/network-service.ts +30 -12
  166. package/src/services/verification-key-updates-service/verification-key-updates-service.interface.ts +9 -0
  167. package/src/services/verification-key-updates-service/verification-key-updates-service.ts +72 -0
  168. package/src/services/zkapp-commands-service/zkapp-commands-service.interface.ts +9 -0
  169. package/src/services/zkapp-commands-service/zkapp-commands-service.ts +181 -0
  170. package/src/tracing/jaeger-tracing.ts +2 -1
  171. package/build/tests/consensus/run-compare.js +0 -40
  172. package/build/tests/consensus/run-compare.js.map +0 -1
  173. package/build/tests/consensus/select-consensus-precomputed.js +0 -80
  174. package/build/tests/consensus/select-consensus-precomputed.js.map +0 -1
  175. package/build/tests/consensus/types.js +0 -15
  176. package/build/tests/consensus/types.js.map +0 -1
@@ -4,6 +4,8 @@ import type {
4
4
  Events,
5
5
  NetworkState,
6
6
  Blocks,
7
+ VerificationKeyUpdates,
8
+ ZkappCommands,
7
9
  } from '../../blockchain/types.js';
8
10
  import type { DatabaseAdapter } from './archive-node-adapter.interface.js';
9
11
  import type {
@@ -11,16 +13,29 @@ import type {
11
13
  EventFilterOptionsInput,
12
14
  BlockQueryInput,
13
15
  BlockSortByInput,
16
+ VerificationKeyUpdateFilterInput,
17
+ ZkappCommandFilterOptionsInput,
14
18
  } from '../../resolvers-types.js';
15
19
  import { getTables, USED_TABLES } from '../../db/sql/events-actions/queries.js';
20
+ import { buildPostgresOptions } from './postgres-options.js';
16
21
  import { EventsService } from '../../services/events-service/events-service.js';
17
22
  import { IEventsService } from '../../services/events-service/events-service.interface.js';
18
23
  import { ActionsService } from '../../services/actions-service/actions-service.js';
19
24
  import { IActionsService } from '../../services/actions-service/actions-service.interface.js';
25
+ import { ZkappCommandsService } from '../../services/zkapp-commands-service/zkapp-commands-service.js';
26
+ import { IZkappCommandsService } from '../../services/zkapp-commands-service/zkapp-commands-service.interface.js';
20
27
  import { NetworkService } from '../../services/network-service/network-service.js';
21
28
  import { INetworkService } from '../../services/network-service/network-service.interface.js';
22
29
  import { BlocksService } from '../../services/blocks-service/blocks-service.js';
23
30
  import { IBlocksService } from '../../services/blocks-service/blocks-service.interface.js';
31
+ import { VerificationKeyUpdatesService } from '../../services/verification-key-updates-service/verification-key-updates-service.js';
32
+ import { IVerificationKeyUpdatesService } from '../../services/verification-key-updates-service/verification-key-updates-service.interface.js';
33
+
34
+ /**
35
+ * Connect deadline for the readiness pinger, in seconds. Kept short so a probe
36
+ * fails fast rather than hanging for the driver's 30s default.
37
+ */
38
+ const PING_CONNECT_TIMEOUT_S = 2;
24
39
 
25
40
  export class ArchiveNodeAdapter implements DatabaseAdapter {
26
41
  /**
@@ -30,21 +45,38 @@ export class ArchiveNodeAdapter implements DatabaseAdapter {
30
45
  * `postgres.Sql` instance across the adapter is safe.
31
46
  */
32
47
  private client: postgres.Sql;
48
+ /**
49
+ * Dedicated single-connection client for readiness pings. postgres.js queues
50
+ * onto busy connections, so using the main pool can make a merely-busy DB look
51
+ * unreachable while long GraphQL queries are running.
52
+ */
53
+ private pingClient: postgres.Sql;
33
54
  private eventsService: IEventsService;
34
55
  private actionsService: IActionsService;
56
+ private zkappCommandsService: IZkappCommandsService;
35
57
  private networkService: INetworkService;
36
58
  private blocksService: IBlocksService;
59
+ private verificationKeyUpdatesService: IVerificationKeyUpdatesService;
37
60
 
38
61
  constructor(connectionString: string | undefined) {
39
62
  if (!connectionString)
40
63
  throw new Error(
41
64
  'Missing Postgres Connection String. Please provide a valid connection string in the environment variables or in your configuration file to connect to the Postgres database.'
42
65
  );
43
- this.client = postgres(connectionString);
66
+ this.client = postgres(connectionString, buildPostgresOptions());
67
+ this.pingClient = postgres(connectionString, {
68
+ max: 1,
69
+ idle_timeout: 60,
70
+ connect_timeout: PING_CONNECT_TIMEOUT_S,
71
+ });
44
72
  this.eventsService = new EventsService(this.client);
45
73
  this.actionsService = new ActionsService(this.client);
74
+ this.zkappCommandsService = new ZkappCommandsService(this.client);
46
75
  this.networkService = new NetworkService(this.client);
47
76
  this.blocksService = new BlocksService(this.client);
77
+ this.verificationKeyUpdatesService = new VerificationKeyUpdatesService(
78
+ this.client
79
+ );
48
80
  }
49
81
 
50
82
  async getEvents(
@@ -61,10 +93,27 @@ export class ArchiveNodeAdapter implements DatabaseAdapter {
61
93
  return this.actionsService.getActions(input, options);
62
94
  }
63
95
 
96
+ async getZkappCommands(
97
+ input: ZkappCommandFilterOptionsInput,
98
+ options: unknown
99
+ ): Promise<ZkappCommands> {
100
+ return this.zkappCommandsService.getZkappCommands(input, options);
101
+ }
102
+
64
103
  async getNetworkState(options: unknown): Promise<NetworkState> {
65
104
  return this.networkService.getNetworkState(options);
66
105
  }
67
106
 
107
+ async getVerificationKeyUpdates(
108
+ input: VerificationKeyUpdateFilterInput,
109
+ options: unknown
110
+ ): Promise<VerificationKeyUpdates> {
111
+ return this.verificationKeyUpdatesService.getVerificationKeyUpdates(
112
+ input,
113
+ options
114
+ );
115
+ }
116
+
68
117
  async getBlocks(
69
118
  query: BlockQueryInput | null | undefined,
70
119
  limit: number | null | undefined,
@@ -95,7 +144,21 @@ export class ArchiveNodeAdapter implements DatabaseAdapter {
95
144
  }
96
145
  }
97
146
 
147
+ async ping(): Promise<boolean> {
148
+ try {
149
+ await this.pingClient`SELECT 1`;
150
+ return true;
151
+ } catch (error) {
152
+ console.warn(
153
+ `[readiness] database ping failed: ${
154
+ error instanceof Error ? error.message : String(error)
155
+ }`
156
+ );
157
+ return false;
158
+ }
159
+ }
160
+
98
161
  async close() {
99
- return this.client.end();
162
+ await Promise.all([this.client.end(), this.pingClient.end()]);
100
163
  }
101
164
  }
@@ -0,0 +1,90 @@
1
+ import type postgres from 'postgres';
2
+
3
+ export { buildPostgresOptions, resolvePoolConfig, POOL_DEFAULTS };
4
+ export type { PostgresPoolConfig };
5
+
6
+ /**
7
+ * Connection-pool and timeout configuration for the archive-node Postgres
8
+ * client. Without these limits a single expensive query can hold a connection
9
+ * open indefinitely, exhausting the pool and cascading into an outage. The
10
+ * defaults are deliberately conservative so the server is safe to expose
11
+ * publicly out of the box, and every value is tunable via the environment.
12
+ */
13
+ interface PostgresPoolConfig {
14
+ /**
15
+ * Maximum pooled connections in total, not per host: a multi-host `PG_CONN`
16
+ * fails over to the first reachable host rather than fanning out, so the pool
17
+ * only ever points at one host at a time.
18
+ */
19
+ max: number;
20
+ /** Seconds a connection may sit idle before it is closed. */
21
+ idleTimeout: number;
22
+ /** Seconds to wait for a new connection before giving up. */
23
+ connectTimeout: number;
24
+ /**
25
+ * Server-side `statement_timeout` in milliseconds. A query running longer
26
+ * than this is cancelled by Postgres. `0` disables the timeout.
27
+ */
28
+ statementTimeout: number;
29
+ }
30
+
31
+ const POOL_DEFAULTS: PostgresPoolConfig = {
32
+ max: 10,
33
+ idleTimeout: 30,
34
+ connectTimeout: 30,
35
+ statementTimeout: 15_000,
36
+ };
37
+
38
+ /**
39
+ * Parse a non-negative integer from an env value, falling back to `fallback`
40
+ * when the value is missing or malformed. We fall back rather than throw so a
41
+ * stray typo never silently disables a safety limit (e.g. `max` becoming 0).
42
+ */
43
+ function intFromEnv(value: string | undefined, fallback: number): number {
44
+ if (value === undefined || value.trim() === '') return fallback;
45
+ const parsed = Number(value);
46
+ if (!Number.isInteger(parsed) || parsed < 0) return fallback;
47
+ return parsed;
48
+ }
49
+
50
+ type EnvSource = Record<string, string | undefined>;
51
+
52
+ function resolvePoolConfig(env: EnvSource = process.env): PostgresPoolConfig {
53
+ return {
54
+ // `max` must be at least 1 — a pool of 0 connections can never serve a query.
55
+ max: Math.max(1, intFromEnv(env.PG_MAX_CONNECTIONS, POOL_DEFAULTS.max)),
56
+ idleTimeout: intFromEnv(env.PG_IDLE_TIMEOUT, POOL_DEFAULTS.idleTimeout),
57
+ connectTimeout: intFromEnv(
58
+ env.PG_CONNECT_TIMEOUT,
59
+ POOL_DEFAULTS.connectTimeout
60
+ ),
61
+ statementTimeout: intFromEnv(
62
+ env.PG_STATEMENT_TIMEOUT,
63
+ POOL_DEFAULTS.statementTimeout
64
+ ),
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Build the options object passed to `postgres()`. `statement_timeout` is sent
70
+ * as a startup connection parameter so it applies to every query on every
71
+ * connection in the pool.
72
+ */
73
+ function buildPostgresOptions(
74
+ env: EnvSource = process.env
75
+ ): postgres.Options<Record<string, never>> {
76
+ const config = resolvePoolConfig(env);
77
+ return {
78
+ max: config.max,
79
+ idle_timeout: config.idleTimeout,
80
+ connect_timeout: config.connectTimeout,
81
+ connection: {
82
+ // Sent as a startup connection parameter, so it applies to every query.
83
+ // Stringified deliberately: postgres.js drops falsy startup parameters,
84
+ // so numeric 0 would be omitted instead of explicitly disabling it.
85
+ // Its type narrows this specific key to number, but the startup-packet
86
+ // implementation serialises strings and the index signature allows them.
87
+ statement_timeout: String(config.statementTimeout) as unknown as number,
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,34 @@
1
+ import type postgres from 'postgres';
2
+
3
+ /**
4
+ * The height of the best chain's tip — the anchor every "walk back from the tip"
5
+ * recursive CTE starts from.
6
+ *
7
+ * This is deliberately NOT `(SELECT MAX(height) FROM blocks)`. The archive keeps every
8
+ * block it ever saw, so after a hard fork the abandoned chain's blocks stay in the table
9
+ * ABOVE the new chain's tip, marked `orphaned`. A global max then lands on a dead block:
10
+ * with the `chain_status = 'pending'` guard the anchor matches nothing and the walk
11
+ * silently degrades to canonical-only rows; without the guard the walk follows the dead
12
+ * chain. Either way the live tip and everything the network produced since the fork is
13
+ * invisible to `inBestChain`, events, actions and action-state resolution.
14
+ *
15
+ * Seen on mainnet at the Mesa upgrade (2026-09-03): the old chain reached height 548187
16
+ * before stopping, the fork block was 548147, and for the whole first hour
17
+ * `blocks(inBestChain: true)` returned nothing above 548147 while `networkState`
18
+ * correctly reported a pending tip of 548164 — the two disagreed because only one of
19
+ * them excluded orphans.
20
+ *
21
+ * Restricting the max to non-orphaned blocks makes the anchor agree with
22
+ * `networkState.pendingMaxBlockHeight` (which is computed per `chain_status`), and it is
23
+ * a no-op in normal operation, where the highest block is always the pending tip.
24
+ */
25
+ export const BEST_CHAIN_TIP_HEIGHT_SQL =
26
+ "(SELECT MAX(height) FROM blocks WHERE chain_status <> 'orphaned')";
27
+
28
+ /**
29
+ * {@link BEST_CHAIN_TIP_HEIGHT_SQL} as a fragment for tagged-template queries. The text is
30
+ * a constant with no user input, which is what makes `unsafe` safe here.
31
+ */
32
+ export function bestChainTipHeight(db_client: postgres.Sql) {
33
+ return db_client.unsafe(BEST_CHAIN_TIP_HEIGHT_SQL);
34
+ }
@@ -2,6 +2,7 @@ import type postgres from 'postgres';
2
2
  import { ArchiveNodeDatabaseRow } from './types.js';
3
3
  import { BlockStatusFilter } from '../../../blockchain/types.js';
4
4
  import { BLOCK_RANGE_SIZE } from '../../../server/server.js';
5
+ import { bestChainTipHeight } from '../best-chain.js';
5
6
 
6
7
  function fullChainCTE(db_client: postgres.Sql, from?: string, to?: string) {
7
8
  let toAsNum = to ? Number(to) : undefined;
@@ -20,20 +21,27 @@ function fullChainCTE(db_client: postgres.Sql, from?: string, to?: string) {
20
21
  FROM
21
22
  blocks b
22
23
  WHERE
23
- height = (SELECT max(height) FROM blocks)
24
+ height = ${bestChainTipHeight(db_client)}
24
25
  )
25
26
  UNION ALL
26
27
  SELECT
27
28
  b.id, b.state_hash, b.parent_hash, b.parent_id, b.height, b.global_slot_since_genesis, b.global_slot_since_hard_fork, b.timestamp, b.chain_status, b.ledger_hash, b.last_vrf_output
28
29
  FROM
29
30
  blocks b
31
+ -- The walk terminates because genesis has parent_id = NULL, so the
32
+ -- id = parent_id join can never match there. No block is its own parent
33
+ -- (parent_id is a FK to blocks.id; a self-reference would be a cycle), so
34
+ -- no explicit id <> parent_id guard is needed. A schema/hard-fork change
35
+ -- that ever self-references a block would loop here — see the matching
36
+ -- note in getZkappsWithPendingEventsQuery.
30
37
  INNER JOIN pending_chain ON b.id = pending_chain.parent_id
31
- AND pending_chain.id <> pending_chain.parent_id
32
38
  AND pending_chain.chain_status <> 'canonical'
33
- ),
39
+ ),
34
40
  full_chain AS (
35
41
  SELECT
36
- DISTINCT id, state_hash, parent_id, parent_hash, height, global_slot_since_genesis, global_slot_since_hard_fork, timestamp, chain_status, ledger_hash, (SELECT max(height) FROM blocks) - height AS distance_from_max_block_height, last_vrf_output
42
+ DISTINCT id, state_hash, parent_id, parent_hash, height, global_slot_since_genesis, global_slot_since_hard_fork, timestamp, chain_status, ledger_hash, ${bestChainTipHeight(
43
+ db_client
44
+ )} - height AS distance_from_max_block_height, last_vrf_output
37
45
  FROM
38
46
  (
39
47
  SELECT
@@ -63,10 +71,9 @@ function fullChainCTE(db_client: postgres.Sql, from?: string, to?: string) {
63
71
  // If no params ar provided, then we query the last BLOCK_RANGE_SIZE blocks
64
72
  fromAsNum
65
73
  ? db_client`AND b.height >= ${fromAsNum} AND b.height < ${toAsNum!}`
66
- : db_client`AND b.height >= (
67
- SELECT MAX(b2.height)
68
- FROM blocks b2
69
- ) - ${BLOCK_RANGE_SIZE}`
74
+ : db_client`AND b.height >= ${bestChainTipHeight(
75
+ db_client
76
+ )} - ${BLOCK_RANGE_SIZE}`
70
77
  }
71
78
  ) AS full_chain
72
79
  )
@@ -256,8 +263,8 @@ function emittedActionsCTE(db_client: postgres.Sql) {
256
263
 
257
264
  function emittedActionStateCTE(
258
265
  db_client: postgres.Sql,
259
- fromActionState?: string,
260
- endActionState?: string
266
+ fromActionStateHeight?: string,
267
+ endActionStateHeight?: string
261
268
  ) {
262
269
  return db_client`
263
270
  emitted_action_state AS (
@@ -308,14 +315,28 @@ function emittedActionStateCTE(
308
315
  INNER JOIN zkapp_field zkf4 ON zkf4.id = zks.element4
309
316
  WHERE
310
317
  1 = 1
318
+ -- Filter on the position of the checkpoint ON THE CHAIN, resolved by
319
+ -- resolveActionStateBoundary() before this query runs.
320
+ --
321
+ -- This used to compare zkf0.id against the id of the requested action state.
322
+ -- zkapp_field is the interning table for every field value in the archive,
323
+ -- and its id records when a value was FIRST WRITTEN, not where it sits on
324
+ -- the chain. Any archive filled out of chain order — a bulk import, a
325
+ -- hard-fork migration, a bootstrap — breaks that assumption, and the filter
326
+ -- then silently dropped real actions or returned actions from before the
327
+ -- checkpoint. See tests/integration/action-state-ordering.test.ts.
328
+ --
329
+ -- Greater-or-equal keeps the checkpoint's own block in the answer. o1js
330
+ -- removes it in createActionsList by comparing actionStateOne with the
331
+ -- requested value, so excluding it here would change what clients receive.
311
332
  ${
312
- fromActionState
313
- ? db_client`AND zkf0.id >= (SELECT id FROM zkapp_field WHERE field = ${fromActionState})`
333
+ fromActionStateHeight
334
+ ? db_client`AND emitted_actions.height >= ${fromActionStateHeight}`
314
335
  : db_client``
315
336
  }
316
337
  ${
317
- endActionState
318
- ? db_client`AND zkf0.id <= (SELECT id FROM zkapp_field WHERE field = ${endActionState})`
338
+ endActionStateHeight
339
+ ? db_client`AND emitted_actions.height <= ${endActionStateHeight}`
319
340
  : db_client``
320
341
  }
321
342
  )`;
@@ -378,17 +399,21 @@ export function getActionsQuery(
378
399
  status: BlockStatusFilter,
379
400
  to?: string,
380
401
  from?: string,
381
- fromActionState?: string,
382
- endActionState?: string
402
+ fromActionStateHeight?: string,
403
+ endActionStateHeight?: string
383
404
  ) {
384
405
  return db_client<ArchiveNodeDatabaseRow[]>`
385
- WITH
406
+ WITH
386
407
  ${fullChainCTE(db_client, from, to)},
387
408
  ${accountIdentifierCTE(db_client, address, tokenId)},
388
409
  ${blocksAccessedCTE(db_client, status)},
389
410
  ${emittedZkAppCommandsCTE(db_client)},
390
411
  ${emittedActionsCTE(db_client)},
391
- ${emittedActionStateCTE(db_client, fromActionState, endActionState)}
412
+ ${emittedActionStateCTE(
413
+ db_client,
414
+ fromActionStateHeight,
415
+ endActionStateHeight
416
+ )}
392
417
  SELECT *
393
418
  FROM emitted_action_state
394
419
  `;
@@ -412,9 +437,115 @@ JOIN max_heights mh
412
437
  `;
413
438
  }
414
439
 
415
- export function checkActionState(db_client: postgres.Sql, actionState: string) {
416
- return db_client`
417
- SELECT field FROM zkapp_field WHERE field = ${actionState}
440
+ export type ActionStateBoundaryRow = {
441
+ boundary_height: string | null;
442
+ window_start: string | null;
443
+ /**
444
+ * true — the account still held the requested action state when the window
445
+ * began, so nothing was missed before it.
446
+ * false — the state had already moved on, so actions are missing.
447
+ * null — the account was never accessed below the window, which means the
448
+ * checkpoint itself is inside it.
449
+ */
450
+ state_entering_window: boolean | null;
451
+ };
452
+
453
+ /**
454
+ * Resolves an action state to its position on the chain, for one account.
455
+ *
456
+ * This replaces the old `checkActionState`, which only asked whether the value
457
+ * existed anywhere in `zkapp_field`. That accepted any field value in the whole
458
+ * database — action data, app state, another account's action state — so the
459
+ * server answered with data that had nothing to do with the request instead of
460
+ * raising an error.
461
+ *
462
+ * The lookup is deliberately NOT limited to the queried block range. A checkpoint
463
+ * is a position, not data: resolving it does not need its block to be inside the
464
+ * window. Restricting it to the window would make an out-of-range checkpoint
465
+ * resolve to nothing, and `height >= NULL` selects no rows — the server would
466
+ * then answer with an empty list, which a reducer reads as "no actions to fold".
467
+ *
468
+ * Returns one row:
469
+ *
470
+ * boundary_height the lowest block height at which this account's action
471
+ * state was the requested value, over the canonical chain
472
+ * plus the current pending chain. NULL means the value is
473
+ * not an action state of this account at all.
474
+ * window_start the first block height this query covers, derived the
475
+ * same way as fullChainCTE.
476
+ * state_entering_window this account's action state at its last accessed block
477
+ * below window_start, or NULL if it was never accessed
478
+ * below it. If this still equals the requested value, no
479
+ * action was missed between the checkpoint and the start
480
+ * of the window, so the answer is complete even though the
481
+ * checkpoint itself lies outside it.
482
+ */
483
+ export function resolveActionStateBoundary(
484
+ db_client: postgres.Sql,
485
+ address: string,
486
+ tokenId: string,
487
+ actionState: string,
488
+ from?: string,
489
+ to?: string
490
+ ) {
491
+ const fromAsNum = from ? Number(from) : null;
492
+ const toAsNum = to ? Number(to) : null;
493
+
494
+ return db_client<ActionStateBoundaryRow[]>`
495
+ WITH RECURSIVE pending_chain AS (
496
+ (
497
+ SELECT id, parent_id, chain_status
498
+ FROM blocks
499
+ WHERE height = ${bestChainTipHeight(db_client)}
500
+ )
501
+ UNION ALL
502
+ SELECT b.id, b.parent_id, b.chain_status
503
+ FROM blocks b
504
+ -- Same invariant as fullChainCTE: genesis has parent_id = NULL, and no
505
+ -- block is its own parent, so an explicit id <> parent_id guard never
506
+ -- excludes a row.
507
+ INNER JOIN pending_chain ON b.id = pending_chain.parent_id
508
+ AND pending_chain.chain_status <> 'canonical'
509
+ ),
510
+ bounds AS (
511
+ SELECT ${
512
+ fromAsNum !== null
513
+ ? db_client`${fromAsNum}::bigint`
514
+ : toAsNum !== null
515
+ ? db_client`${toAsNum - BLOCK_RANGE_SIZE}::bigint`
516
+ : db_client`${bestChainTipHeight(
517
+ db_client
518
+ )} - ${BLOCK_RANGE_SIZE}::bigint`
519
+ } AS window_start
520
+ ),
521
+ target AS (
522
+ SELECT id FROM zkapp_field WHERE field = ${actionState}
523
+ ),
524
+ account_action_states AS (
525
+ SELECT b.height, zks.element0 AS state_field_id
526
+ FROM accounts_accessed aa
527
+ INNER JOIN blocks b ON b.id = aa.block_id
528
+ INNER JOIN zkapp_accounts zkacc ON zkacc.id = aa.zkapp_id
529
+ INNER JOIN zkapp_action_states zks ON zks.id = zkacc.action_state_id
530
+ WHERE aa.account_identifier_id = (
531
+ SELECT ai.id FROM account_identifiers ai
532
+ WHERE ai.public_key_id = (SELECT id FROM public_keys WHERE value = ${address})
533
+ AND ai.token_id = (SELECT id FROM tokens WHERE value = ${tokenId})
534
+ )
535
+ AND (b.chain_status = 'canonical' OR b.id IN (SELECT id FROM pending_chain))
536
+ )
537
+ SELECT
538
+ (
539
+ SELECT min(height) FROM account_action_states
540
+ WHERE state_field_id = (SELECT id FROM target)
541
+ ) AS boundary_height,
542
+ (SELECT window_start FROM bounds) AS window_start,
543
+ (
544
+ SELECT state_field_id FROM account_action_states
545
+ WHERE height < (SELECT window_start FROM bounds)
546
+ ORDER BY height DESC
547
+ LIMIT 1
548
+ ) = (SELECT id FROM target) AS state_entering_window
418
549
  `;
419
550
  }
420
551
 
@@ -427,16 +558,27 @@ export function getTables(db_client: postgres.Sql) {
427
558
  export const USED_TABLES = [
428
559
  'blocks',
429
560
  'account_identifiers',
561
+ 'public_keys',
562
+ 'tokens',
430
563
  'accounts_accessed',
431
564
  'blocks_zkapp_commands',
565
+ 'public_keys',
566
+ 'tokens',
432
567
  'zkapp_commands',
568
+ 'zkapp_fee_payer_body',
433
569
  'zkapp_account_update',
434
570
  'zkapp_account_update_body',
571
+ 'zkapp_updates',
435
572
  'zkapp_events',
436
573
  'zkapp_field_array',
437
574
  'zkapp_field',
575
+ 'zkapp_account_precondition',
576
+ 'zkapp_network_precondition',
577
+ 'zkapp_global_slot_bounds',
578
+ 'zkapp_states_nullable',
438
579
  'zkapp_verification_key_hashes',
439
580
  'zkapp_verification_keys',
581
+ 'zkapp_updates',
440
582
  'zkapp_accounts',
441
583
  'zkapp_action_states',
442
584
  ] as const;
@@ -447,17 +589,19 @@ export function getZkappsWithPendingEventsQuery(db_client: postgres.Sql) {
447
589
  -- start at the tip
448
590
  SELECT id, parent_id, chain_status
449
591
  FROM blocks
450
- WHERE height = (SELECT MAX(height) FROM blocks)
592
+ WHERE height = ${bestChainTipHeight(db_client)}
451
593
 
452
594
  UNION ALL
453
595
 
454
596
  -- walk back until we re-join the canonical branch
597
+ -- The id <> parent_id guard was dropped here for the same reason as in
598
+ -- fullChainCTE: no block is its own parent, so it never excludes a row, and
599
+ -- the walk terminates on genesis's NULL parent_id regardless.
455
600
  SELECT b.id, b.parent_id, b.chain_status
456
601
  FROM blocks b
457
602
  JOIN pending_chain pc
458
603
  ON b.id = pc.parent_id
459
- WHERE pc.id <> pc.parent_id
460
- AND pc.chain_status <> 'canonical'
604
+ WHERE pc.chain_status <> 'canonical'
461
605
  )
462
606
  SELECT DISTINCT
463
607
  pk.value AS public_key
@@ -0,0 +1,132 @@
1
+ import type postgres from 'postgres';
2
+ import { BlockStatusFilter } from '../../../blockchain/types.js';
3
+
4
+ export type VerificationKeyUpdateDatabaseRow = {
5
+ account_update_id: number;
6
+ address: string;
7
+ token_id: string;
8
+ verification_key_hash: string;
9
+ state_hash: string;
10
+ parent_hash: string;
11
+ height: string;
12
+ global_slot_since_genesis: string;
13
+ global_slot_since_hard_fork: string;
14
+ timestamp: string;
15
+ chain_status: string;
16
+ ledger_hash: string;
17
+ distance_from_max_block_height: string;
18
+ last_vrf_output: string;
19
+ status: string;
20
+ hash: string;
21
+ memo: string;
22
+ authorization_kind: string;
23
+ sequence_number: number;
24
+ zkapp_account_updates_ids: number[];
25
+ };
26
+
27
+ export function getVerificationKeyUpdatesQuery(
28
+ client: postgres.Sql,
29
+ verificationKeyHash: string,
30
+ from: number,
31
+ to: number,
32
+ status: BlockStatusFilter
33
+ ) {
34
+ const statusClause =
35
+ status === BlockStatusFilter.all
36
+ ? client`AND b.chain_status <> 'orphaned'`
37
+ : client`AND b.chain_status = ${status.toLowerCase()}`;
38
+
39
+ return client<VerificationKeyUpdateDatabaseRow[]>`
40
+ WITH RECURSIVE pending_chain AS (
41
+ SELECT id, parent_id
42
+ FROM blocks
43
+ WHERE chain_status = 'pending'
44
+ AND height = (
45
+ SELECT MAX(height) FROM blocks WHERE chain_status = 'pending'
46
+ )
47
+
48
+ UNION ALL
49
+
50
+ SELECT parent.id, parent.parent_id
51
+ FROM blocks parent
52
+ INNER JOIN pending_chain child ON parent.id = child.parent_id
53
+ WHERE parent.chain_status <> 'canonical'
54
+ AND child.id <> child.parent_id
55
+ ),
56
+ -- Resolve the requested hash to the set of zkapp_updates rows that SET
57
+ -- that verification key, before touching any block. The set is small — one
58
+ -- row per distinct update that ever wrote this key — and MATERIALIZED
59
+ -- keeps the planner from inlining it back into the join tree, where it
60
+ -- would hash the whole of zkapp_updates and zkapp_account_update_body once
61
+ -- per query. Measured on a 681k-block devnet archive over the maximum
62
+ -- 10 000-block range: 405 ms inlined, 199 ms materialised.
63
+ target_updates AS MATERIALIZED (
64
+ SELECT zu.id
65
+ FROM zkapp_verification_key_hashes vkh
66
+ INNER JOIN zkapp_verification_keys vk ON vk.hash_id = vkh.id
67
+ INNER JOIN zkapp_updates zu ON zu.verification_key_id = vk.id
68
+ WHERE vkh.value = ${verificationKeyHash}
69
+ ),
70
+ full_chain AS (
71
+ SELECT b.*
72
+ FROM blocks b
73
+ WHERE b.height >= ${from}
74
+ AND b.height < ${to}
75
+ AND (
76
+ b.chain_status = 'canonical'
77
+ OR b.id IN (SELECT id FROM pending_chain)
78
+ )
79
+ )
80
+ SELECT
81
+ zau.id AS account_update_id,
82
+ pk.value AS address,
83
+ t.value AS token_id,
84
+ ${verificationKeyHash}::text AS verification_key_hash,
85
+ b.state_hash,
86
+ b.parent_hash,
87
+ b.height,
88
+ b.global_slot_since_genesis,
89
+ b.global_slot_since_hard_fork,
90
+ b.timestamp,
91
+ b.chain_status,
92
+ b.ledger_hash,
93
+ (SELECT MAX(height) FROM blocks) - b.height
94
+ AS distance_from_max_block_height,
95
+ b.last_vrf_output,
96
+ bzc.status,
97
+ zc.hash,
98
+ zc.memo,
99
+ zaub.authorization_kind,
100
+ bzc.sequence_no AS sequence_number,
101
+ zc.zkapp_account_updates_ids
102
+ FROM full_chain b
103
+ INNER JOIN blocks_zkapp_commands bzc ON bzc.block_id = b.id
104
+ INNER JOIN zkapp_commands zc ON zc.id = bzc.zkapp_command_id
105
+ INNER JOIN LATERAL UNNEST(zc.zkapp_account_updates_ids)
106
+ WITH ORDINALITY AS update_ref(id, position) ON TRUE
107
+ INNER JOIN zkapp_account_update zau ON zau.id = update_ref.id
108
+ -- An account update SETS a verification key through update_id. The other
109
+ -- column that names a verification key on this table,
110
+ -- verification_key_hash_id, is the PRECONDITION: the key the update
111
+ -- requires the account to already have. Filtering on that one would answer
112
+ -- "who called this contract" instead of "who deployed it".
113
+ INNER JOIN zkapp_account_update_body zaub ON zaub.id = zau.body_id
114
+ AND zaub.update_id IN (SELECT id FROM target_updates)
115
+ INNER JOIN account_identifiers ai ON ai.id = zaub.account_identifier_id
116
+ INNER JOIN public_keys pk ON pk.id = ai.public_key_id
117
+ INNER JOIN tokens t ON t.id = ai.token_id
118
+ WHERE bzc.status = 'applied'
119
+ ${statusClause}
120
+ -- state_hash is what makes this order total. Two competing tips sit at the
121
+ -- same height, and pending_chain seeds from every block at the maximum
122
+ -- pending height, so both are in the answer. A command carried by both —
123
+ -- the normal case during a reorg; one command was measured in 8 blocks at
124
+ -- a single height on devnet — then produces rows that agree on height,
125
+ -- sequence_no, account-update position and zkapp_account_update.id alike.
126
+ ORDER BY
127
+ b.height ASC,
128
+ b.state_hash ASC,
129
+ bzc.sequence_no ASC,
130
+ update_ref.position ASC
131
+ `;
132
+ }