Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/olive-gifts-say.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@solana/instruction-plans': patch
'@solana/errors': patch
---

Throw early when the default transaction plan executor encounters a non-divisible transaction plan.
2 changes: 2 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ export const SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN =
export const SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE = 7618001;
export const SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = 7618002;
export const SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN = 7618003;
export const SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED = 7618004;

// Codec-related errors.
// Reserve error codes in the range [8078000-8078999].
Expand Down Expand Up @@ -463,6 +464,7 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN
| typeof SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN
| typeof SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE
| typeof SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED
| typeof SOLANA_ERROR__INVALID_BLOCKHASH_BYTE_LENGTH
| typeof SOLANA_ERROR__INVALID_NONCE
| typeof SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING
Expand Down
3 changes: 3 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import {
SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE,
SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED,
SOLANA_ERROR__INVALID_BLOCKHASH_BYTE_LENGTH,
SOLANA_ERROR__INVALID_NONCE,
SOLANA_ERROR__INVARIANT_VIOLATION__CACHED_ABORTABLE_ITERABLE_CACHE_ENTRY_MISSING,
Expand Down Expand Up @@ -426,6 +427,8 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__INSTRUCTION_ERROR__UNSUPPORTED_SYSVAR]: 'Unsupported sysvar',
[SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND]: 'Invalid instruction plan kind: $kind.',
[SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN]: 'The provided instruction plan is empty.',
[SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED]:
'This transaction plan executor does not support non-divisible sequential plans. To support them, you may create your own executor such that multi-transaction atomicity is preserved — e.g. by targetting RPCs that support transaction bundles.',
Comment on lines +430 to +431
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is an important part of the PR to review. Lmk what you think. 🙏

[SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN]:
'The provided transaction plan failed to execute. See the `transactionPlanResult` attribute and the `cause` error for more details.',
[SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import '@solana/test-matchers/toBeFrozenObject';
import {
SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ARGUMENT,
SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED,
SolanaError,
} from '@solana/errors';

Expand All @@ -16,7 +17,6 @@ import { createTransactionPlanExecutor } from '../transaction-plan-executor';
import {
canceledSingleTransactionPlanResult,
failedSingleTransactionPlanResult,
nonDivisibleSequentialTransactionPlanResult,
parallelTransactionPlanResult,
sequentialTransactionPlanResult,
successfulSingleTransactionPlanResult,
Expand Down Expand Up @@ -195,22 +195,33 @@ describe('createTransactionPlanExecutor', () => {
expect(executeTransactionMessage).toHaveBeenNthCalledWith(2, messageB, { abortSignal: undefined });
});

it('successfully executes a non-divisible sequential transaction plan', async () => {
it('throws when encountering a non-divisible sequential transaction plan', async () => {
expect.assertions(1);
const messageA = createMessage('A');
const messageB = createMessage('B');
const executeTransactionMessage = jest.fn().mockImplementation(forwardId);
const executor = createTransactionPlanExecutor({ executeTransactionMessage });

const promise = executor(nonDivisibleSequentialTransactionPlan([messageA, messageB]));
await expect(promise).resolves.toStrictEqual(
nonDivisibleSequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageA, createTransaction('A')),
successfulSingleTransactionPlanResult(messageB, createTransaction('B')),
]),
await expect(promise).rejects.toThrow(
new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED),
);
});

it('does no execute transactions before checking for non-divisible plans', async () => {
expect.assertions(1);
const messageA = createMessage('A');
const messageB = createMessage('B');
const messageC = createMessage('C');
const executeTransactionMessage = jest.fn().mockImplementation(forwardId);
const executor = createTransactionPlanExecutor({ executeTransactionMessage });

await executor(
sequentialTransactionPlan([messageA, nonDivisibleSequentialTransactionPlan([messageB, messageC])]),
).catch(() => {});
expect(executeTransactionMessage).not.toHaveBeenCalled();
});

it('passes the abort signal to the `executeTransactionMessage` function', async () => {
expect.assertions(2);
const messageA = createMessage('A');
Expand Down Expand Up @@ -544,7 +555,7 @@ describe('createTransactionPlanExecutor', () => {
parallelTransactionPlan([
sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]),
messageE,
nonDivisibleSequentialTransactionPlan([messageF, messageG]),
sequentialTransactionPlan([messageF, messageG]),
]),
);

Expand All @@ -559,7 +570,7 @@ describe('createTransactionPlanExecutor', () => {
successfulSingleTransactionPlanResult(messageD, createTransaction('D')),
]),
successfulSingleTransactionPlanResult(messageE, createTransaction('E')),
nonDivisibleSequentialTransactionPlanResult([
sequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageF, createTransaction('F')),
successfulSingleTransactionPlanResult(messageG, createTransaction('G')),
]),
Expand Down Expand Up @@ -589,7 +600,7 @@ describe('createTransactionPlanExecutor', () => {
parallelTransactionPlan([
sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]),
messageE,
nonDivisibleSequentialTransactionPlan([messageF, messageG]),
sequentialTransactionPlan([messageF, messageG]),
]),
);

Expand All @@ -607,7 +618,7 @@ describe('createTransactionPlanExecutor', () => {
canceledSingleTransactionPlanResult(messageD),
]),
successfulSingleTransactionPlanResult(messageE, createTransaction('E')),
nonDivisibleSequentialTransactionPlanResult([
sequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageF, createTransaction('F')),
successfulSingleTransactionPlanResult(messageG, createTransaction('G')),
]),
Expand Down Expand Up @@ -640,7 +651,7 @@ describe('createTransactionPlanExecutor', () => {
parallelTransactionPlan([
sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]),
messageE,
nonDivisibleSequentialTransactionPlan([messageF, messageG]),
sequentialTransactionPlan([messageF, messageG]),
]),
{ abortSignal },
);
Expand All @@ -662,7 +673,7 @@ describe('createTransactionPlanExecutor', () => {
canceledSingleTransactionPlanResult(messageD),
]),
successfulSingleTransactionPlanResult(messageE, createTransaction('E')),
nonDivisibleSequentialTransactionPlanResult([
sequentialTransactionPlanResult([
successfulSingleTransactionPlanResult(messageF, createTransaction('F')),
successfulSingleTransactionPlanResult(messageG, createTransaction('G')),
]),
Expand Down Expand Up @@ -691,7 +702,7 @@ describe('createTransactionPlanExecutor', () => {
parallelTransactionPlan([
sequentialTransactionPlan([messageA, parallelTransactionPlan([messageB, messageC]), messageD]),
messageE,
nonDivisibleSequentialTransactionPlan([messageF, messageG]),
sequentialTransactionPlan([messageF, messageG]),
]),
{ abortSignal },
);
Expand All @@ -710,7 +721,7 @@ describe('createTransactionPlanExecutor', () => {
canceledSingleTransactionPlanResult(messageD),
]),
canceledSingleTransactionPlanResult(messageE),
nonDivisibleSequentialTransactionPlanResult([
sequentialTransactionPlanResult([
canceledSingleTransactionPlanResult(messageF),
canceledSingleTransactionPlanResult(messageG),
]),
Expand Down
36 changes: 32 additions & 4 deletions packages/instruction-plans/src/transaction-plan-executor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED,
SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
SolanaError,
} from '@solana/errors';
Expand All @@ -17,7 +18,6 @@ import type {
import {
canceledSingleTransactionPlanResult,
failedSingleTransactionPlanResult,
nonDivisibleSequentialTransactionPlanResult,
parallelTransactionPlanResult,
sequentialTransactionPlanResult,
successfulSingleTransactionPlanResult,
Expand Down Expand Up @@ -86,6 +86,10 @@ export function createTransactionPlanExecutor(config: TransactionPlanExecutorCon
canceled: abortSignal?.aborted ?? false,
};

// Fail early if there are non-divisible sequential plans in the
// transaction plan as they are not supported by this executor.
assertDivisibleSequentialPlansOnly(plan);

const cancelHandler = () => {
context.canceled = true;
};
Expand Down Expand Up @@ -136,16 +140,18 @@ async function traverseSequential(
transactionPlan: SequentialTransactionPlan,
context: TraverseContext,
): Promise<TransactionPlanResult> {
if (!transactionPlan.divisible) {
throw new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED);
}

const results: TransactionPlanResult[] = [];

for (const subPlan of transactionPlan.plans) {
const result = await traverse(subPlan, context);
results.push(result);
}

return transactionPlan.divisible
? sequentialTransactionPlanResult(results)
: nonDivisibleSequentialTransactionPlanResult(results);
return sequentialTransactionPlanResult(results);
}

async function traverseParallel(
Expand Down Expand Up @@ -195,3 +201,25 @@ function findErrorFromTransactionPlanResult(result: TransactionPlanResult): Erro
}
}
}

function assertDivisibleSequentialPlansOnly(transactionPlan: TransactionPlan): void {
const kind = transactionPlan.kind;
switch (kind) {
case 'sequential':
if (!transactionPlan.divisible) {
throw new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__NON_DIVISIBLE_TRANSACTION_PLANS_NOT_SUPPORTED);
}
for (const subPlan of transactionPlan.plans) {
assertDivisibleSequentialPlansOnly(subPlan);
}
return;
case 'parallel':
for (const subPlan of transactionPlan.plans) {
assertDivisibleSequentialPlansOnly(subPlan);
}
return;
case 'single':
default:
return;
}
}