Skip to content

Commit

Permalink
Create coded exceptions for invariant violations (#2242)
Browse files Browse the repository at this point in the history
# Summary

Invariant violations are a kind of error that should never get hit ever, and if they do your program is broken.

In one sense we don't want error codes for them because they're not intended to be caught in downstream programs. On the other hand I've tried to come up with a reason _not_ to give them codes and I can't.

Making them a `@solana/error` gives us compression and error decoding for free.

Addresses #2118.
  • Loading branch information
steveluscher committed Mar 4, 2024
1 parent 27479b3 commit 9084fdd
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 17 deletions.
10 changes: 10 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR = 55 as const;
export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_ACCOUNTS = 70 as const;
export const SOLANA_ERROR__EXPECTED_INSTRUCTION_TO_HAVE_DATA = 71 as const;
export const SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH = 72 as const;
// Reserve error codes starting with [3507000-3507999] for invariant violations
export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING = 3507000 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE =
3507001 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING = 3507002 as const;
export const SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE = 3507003 as const;
// Reserve error codes starting with [4615000-4615999] for the Rust enum `InstructionError`
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN = 4615000 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR = 4615001 as const;
Expand Down Expand Up @@ -322,6 +328,10 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_ACCOUNTS_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_INSTRUCTION_TRACE_LENGTH_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_BUILTIN_PROGRAMS_MUST_CONSUME_COMPUTE_UNITS
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING
| typeof SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_CANNOT_CREATE_SUBSCRIPTION_REQUEST
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_EXPECTED_SERVER_SUBSCRIPTION_ID
| typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED
Expand Down
8 changes: 8 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ import {
SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR,
SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH,
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
SOLANA_ERROR__MAX_NUMBER_OF_PDA_SEEDS_EXCEEDED,
Expand Down Expand Up @@ -275,6 +277,12 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: {
byteLength: number;
};
[SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING]: {
cacheKey: string;
};
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]: {
unexpectedValue: unknown;
};
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: {
value: string;
};
Expand Down
19 changes: 19 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ import {
SOLANA_ERROR__INSTRUCTION_PROGRAM_ID_MISMATCH,
SOLANA_ERROR__INVALID_KEYPAIR_BYTES,
SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE,
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE,
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE,
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
Expand Down Expand Up @@ -302,6 +306,21 @@ export const SolanaErrorMessages: Readonly<{
'Expected instruction to have progress address $expectedProgramAddress, got $actualProgramAddress.',
[SOLANA_ERROR__INVALID_KEYPAIR_BYTES]: 'Key pair bytes must be of length 64, got $byteLength.',
[SOLANA_ERROR__INVALID_SEEDS_POINT_ON_CURVE]: 'Invalid seeds; point must fall off the Ed25519 curve.',
[SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING]:
'Invariant violation: Found no abortable iterable cache entry for key `$cacheKey`. It ' +
'should be impossible to hit this error; please file an issue at ' +
'https://sola.na/web3invariant',
[SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE]:
'Invariant violation: Switch statement non-exhaustive. Received unexpected value ' +
'`$unexpectedValue`. It should be impossible to hit this error; please file an issue at ' +
'https://sola.na/web3invariant',
[SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE]:
'Invariant violation: WebSocket message iterator state is corrupt; iterated without first ' +
'resolving existing message promise. It should be impossible to hit this error; please ' +
'file an issue at https://sola.na/web3invariant',
[SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING]:
'Invariant violation: WebSocket message iterator is missing state storage. It should be ' +
'impossible to hit this error; please file an issue at https://sola.na/web3invariant',
[SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]',
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`',
[SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CLOSED_BEFORE_MESSAGE_BUFFERED,
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_CONNECTION_CLOSED,
SOLANA_ERROR__RPC_SUBSCRIPTIONS_TRANSPORT_FAILED_TO_CONNECT,
Expand Down Expand Up @@ -132,13 +134,14 @@ export async function createWebSocketConnection({
const state = iteratorState.get(iteratorKey);
if (!state) {
// There should always be state by now.
throw new Error('Invariant: WebSocket message iterator is missing state storage');
throw new SolanaError(
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_STATE_MISSING,
);
}
if (state.__hasPolled) {
// You should never be able to poll twice in a row.
throw new Error(
'Invariant: WebSocket message iterator state is corrupt; ' +
'iterated without first resolving existing message promise',
throw new SolanaError(
SOLANA_ERROR__INVARIANT_VIOLATION_WEBSOCKET_MESSAGE_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE,
);
}
const queuedMessages = state.queuedMessages;
Expand Down
11 changes: 8 additions & 3 deletions packages/rpc-subscriptions/src/cached-abortable-iterable.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
SolanaError,
} from '@solana/errors';

type CacheEntry<TIterable extends AsyncIterable<unknown>> = {
abortController: AbortController;
iterable: Promise<TIterable> | TIterable;
Expand All @@ -7,7 +12,6 @@ type CacheEntry<TIterable extends AsyncIterable<unknown>> = {
type CacheKey = string | symbol;
type Config<TInput extends unknown[], TIterable extends AsyncIterable<unknown>> = Readonly<{
getAbortSignalFromInputArgs: (...args: TInput) => AbortSignal;
getCacheEntryMissingErrorMessage?: (cacheKey: CacheKey) => string;
getCacheKeyFromInputArgs: (...args: TInput) =>
| CacheKey
// `undefined` implies 'do not cache'
Expand All @@ -32,7 +36,6 @@ function registerIterableCleanup(iterable: AsyncIterable<unknown>, cleanupFn: Ca

export function getCachedAbortableIterableFactory<TInput extends unknown[], TIterable extends AsyncIterable<unknown>>({
getAbortSignalFromInputArgs,
getCacheEntryMissingErrorMessage,
getCacheKeyFromInputArgs,
onCacheHit,
onCreateIterable,
Expand All @@ -41,7 +44,9 @@ export function getCachedAbortableIterableFactory<TInput extends unknown[], TIte
function getCacheEntryOrThrow(cacheKey: CacheKey) {
const currentCacheEntry = cache.get(cacheKey);
if (!currentCacheEntry) {
throw new Error(getCacheEntryMissingErrorMessage ? getCacheEntryMissingErrorMessage(cacheKey) : undefined);
throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION_CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING, {
cacheKey: cacheKey.toString(),
});
}
return currentCacheEntry;
}
Expand Down
4 changes: 0 additions & 4 deletions packages/rpc-subscriptions/src/rpc-subscriptions-coalescer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ export function getRpcSubscriptionsWithSubscriptionCoalescing<TRpcSubscriptionsM
AsyncIterable<unknown>
>({
getAbortSignalFromInputArgs: ({ abortSignal }) => abortSignal,
getCacheEntryMissingErrorMessage: __DEV__
? deduplicationKey =>
`Invariant: Found no cache entry for subscription with deduplication key \`${deduplicationKey?.toString()}\``
: undefined,
getCacheKeyFromInputArgs: () => deduplicationKey,
async onCacheHit(_iterable, _config) {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ export function getWebSocketTransportWithConnectionSharding<TTransport extends R
}: Config<TTransport>): TTransport {
return getCachedAbortableIterableFactory({
getAbortSignalFromInputArgs: ({ signal }) => signal,
getCacheEntryMissingErrorMessage: __DEV__
? shardKey => `Invariant: Found no cache entry for connection with shard key \`${shardKey?.toString()}\``
: undefined,
getCacheKeyFromInputArgs: ({ payload }) => (getShard ? getShard(payload) : NULL_SHARD_CACHE_KEY),
onCacheHit: (connection, { payload }) => connection.send_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(payload),
onCreateIterable: (abortSignal, config) =>
Expand Down
8 changes: 5 additions & 3 deletions packages/rpc-types/src/commitment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE, SolanaError } from '@solana/errors';

export type Commitment = 'confirmed' | 'finalized' | 'processed';

function getCommitmentScore(commitment: Commitment): number {
Expand All @@ -9,9 +11,9 @@ function getCommitmentScore(commitment: Commitment): number {
case 'processed':
return 0;
default:
return ((_: never) => {
throw new Error(`Unrecognized commitment \`${commitment}\`.`);
})(commitment);
throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION_SWITCH_MUST_BE_EXHAUSTIVE, {
unexpectedValue: commitment satisfies never,
});
}
}

Expand Down

0 comments on commit 9084fdd

Please sign in to comment.