Skip to content
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

feat: add support for compute budget instructions #24086

Merged
merged 3 commits into from
Apr 26, 2022
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
189 changes: 189 additions & 0 deletions web3.js/src/compute-budget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import * as BufferLayout from '@solana/buffer-layout';

import {
encodeData,
decodeData,
InstructionType,
IInstructionInputData,
} from './instruction';
import {PublicKey} from './publickey';
import {TransactionInstruction} from './transaction';

/**
* Compute Budget Instruction class
*/
export class ComputeBudgetInstruction {
/**
* @internal
*/
constructor() {}

/**
* Decode a compute budget instruction and retrieve the instruction type.
*/
static decodeInstructionType(
instruction: TransactionInstruction,
): ComputeBudgetInstructionType {
this.checkProgramId(instruction.programId);

const instructionTypeLayout = BufferLayout.u8('instruction');
const typeIndex = instructionTypeLayout.decode(instruction.data);

let type: ComputeBudgetInstructionType | undefined;
for (const [ixType, layout] of Object.entries(
COMPUTE_BUDGET_INSTRUCTION_LAYOUTS,
)) {
if (layout.index == typeIndex) {
type = ixType as ComputeBudgetInstructionType;
break;
}
}

if (!type) {
throw new Error(
'Instruction type incorrect; not a ComputeBudgetInstruction',
);
}

return type;
}

/**
* Decode request units compute budget instruction and retrieve the instruction params.
*/
static decodeRequestUnits(
instruction: TransactionInstruction,
): RequestUnitsParams {
this.checkProgramId(instruction.programId);
const {units, additionalFee} = decodeData(
COMPUTE_BUDGET_INSTRUCTION_LAYOUTS.RequestUnits,
instruction.data,
);
return {units, additionalFee};
}

/**
* Decode request heap frame compute budget instruction and retrieve the instruction params.
*/
static decodeRequestHeapFrame(
instruction: TransactionInstruction,
): RequestHeapFrameParams {
this.checkProgramId(instruction.programId);
const {bytes} = decodeData(
COMPUTE_BUDGET_INSTRUCTION_LAYOUTS.RequestHeapFrame,
instruction.data,
);
return {bytes};
}

/**
* @internal
*/
static checkProgramId(programId: PublicKey) {
if (!programId.equals(ComputeBudgetProgram.programId)) {
throw new Error(
'invalid instruction; programId is not ComputeBudgetProgram',
);
}
}
}

/**
* An enumeration of valid ComputeBudgetInstructionType's
*/
export type ComputeBudgetInstructionType =
// FIXME
// It would be preferable for this type to be `keyof ComputeBudgetInstructionInputData`
// but Typedoc does not transpile `keyof` expressions.
// See https://github.com/TypeStrong/typedoc/issues/1894
'RequestUnits' | 'RequestHeapFrame';

type ComputeBudgetInstructionInputData = {
RequestUnits: IInstructionInputData & Readonly<RequestUnitsParams>;
RequestHeapFrame: IInstructionInputData & Readonly<RequestHeapFrameParams>;
};

/**
* Request units instruction params
*/
export interface RequestUnitsParams {
/** Units to request for transaction-wide compute */
units: number;

/** Additional fee to pay */
additionalFee: number;
}

/**
* Request heap frame instruction params
*/
export type RequestHeapFrameParams = {
/** Requested transaction-wide program heap size in bytes. Must be multiple of 1024. Applies to each program, including CPIs. */
bytes: number;
};

/**
* An enumeration of valid ComputeBudget InstructionType's
* @internal
*/
export const COMPUTE_BUDGET_INSTRUCTION_LAYOUTS = Object.freeze<{
[Instruction in ComputeBudgetInstructionType]: InstructionType<
ComputeBudgetInstructionInputData[Instruction]
>;
}>({
RequestUnits: {
index: 0,
layout: BufferLayout.struct<
ComputeBudgetInstructionInputData['RequestUnits']
>([
BufferLayout.u8('instruction'),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think because the ComputeBudgetInstructions use borsh rather than bincode for serialization, the instruction buffer is only a u8 rather than a u32 compared to other instructions?

Copy link
Member

Choose a reason for hiding this comment

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

Yup, that's correct!

BufferLayout.u32('units'),
BufferLayout.u32('additionalFee'),
]),
},
RequestHeapFrame: {
index: 1,
layout: BufferLayout.struct<
ComputeBudgetInstructionInputData['RequestHeapFrame']
>([BufferLayout.u8('instruction'), BufferLayout.u32('bytes')]),
},
});

/**
* Factory class for transaction instructions to interact with the Compute Budget program
*/
export class ComputeBudgetProgram {
/**
* @internal
*/
constructor() {}

/**
* Public key that identifies the Compute Budget program
*/
static programId: PublicKey = new PublicKey(
'ComputeBudget111111111111111111111111111111',
);

static requestUnits(params: RequestUnitsParams): TransactionInstruction {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

returning a TransactionInstruction here rather than a Transaction, the StakeProgram seemed to return primarily Transaction, but think TransactionInstruction is a bit more composable

const type = COMPUTE_BUDGET_INSTRUCTION_LAYOUTS.RequestUnits;
const data = encodeData(type, params);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this PR still uses the BufferLayout to match the other programs in the sdk, but it is also possible to switch to using the borsh style serialization/deserialization i.e. what PublicKey does

Copy link
Member

Choose a reason for hiding this comment

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

This approach is fine

return new TransactionInstruction({
keys: [],
programId: this.programId,
data,
});
}

static requestHeapFrame(
params: RequestHeapFrameParams,
): TransactionInstruction {
const type = COMPUTE_BUDGET_INSTRUCTION_LAYOUTS.RequestHeapFrame;
const data = encodeData(type, params);
return new TransactionInstruction({
keys: [],
programId: this.programId,
data,
});
}
}
1 change: 1 addition & 0 deletions web3.js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './account';
export * from './blockhash';
export * from './bpf-loader-deprecated';
export * from './bpf-loader';
export * from './compute-budget';
export * from './connection';
export * from './epoch-schedule';
export * from './ed25519-program';
Expand Down
186 changes: 186 additions & 0 deletions web3.js/test/compute-budget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';

import {
Keypair,
Connection,
LAMPORTS_PER_SOL,
Transaction,
ComputeBudgetProgram,
ComputeBudgetInstruction,
PublicKey,
SystemProgram,
sendAndConfirmTransaction,
} from '../src';
import {helpers} from './mocks/rpc-http';
import {url} from './url';

use(chaiAsPromised);

describe('ComputeBudgetProgram', () => {
it('requestUnits', () => {
const params = {
units: 150000,
additionalFee: 0,
};
const transaction = new Transaction().add(
ComputeBudgetProgram.requestUnits(params),
);
expect(transaction.instructions).to.have.length(1);
const [computeBudgetInstruction] = transaction.instructions;
expect(params).to.eql(
ComputeBudgetInstruction.decodeRequestUnits(computeBudgetInstruction),
);
});

it('requestHeapFrame', () => {
const params = {
bytes: 33 * 1024,
};
const transaction = new Transaction().add(
ComputeBudgetProgram.requestHeapFrame(params),
);
expect(transaction.instructions).to.have.length(1);
const [computeBudgetInstruction] = transaction.instructions;
expect(params).to.eql(
ComputeBudgetInstruction.decodeRequestHeapFrame(computeBudgetInstruction),
);
});

it('ComputeBudgetInstruction', () => {
const requestUnits = ComputeBudgetProgram.requestUnits({
units: 150000,
additionalFee: 0,
});
const requestHeapFrame = ComputeBudgetProgram.requestHeapFrame({
bytes: 33 * 1024,
});

const requestUnitsTransaction = new Transaction().add(requestUnits);
expect(requestUnitsTransaction.instructions).to.have.length(1);
const requestUnitsTransactionType =
ComputeBudgetInstruction.decodeInstructionType(
requestUnitsTransaction.instructions[0],
);
expect(requestUnitsTransactionType).to.eq('RequestUnits');

const requestHeapFrameTransaction = new Transaction().add(requestHeapFrame);
expect(requestHeapFrameTransaction.instructions).to.have.length(1);
const requestHeapFrameTransactionType =
ComputeBudgetInstruction.decodeInstructionType(
requestHeapFrameTransaction.instructions[0],
);
expect(requestHeapFrameTransactionType).to.eq('RequestHeapFrame');
});

if (process.env.TEST_LIVE) {
const STARTING_AMOUNT = 2 * LAMPORTS_PER_SOL;
const FEE_AMOUNT = LAMPORTS_PER_SOL;
it('live compute budget actions', async () => {
const connection = new Connection(url, 'confirmed');

const baseAccount = Keypair.generate();
const basePubkey = baseAccount.publicKey;
await helpers.airdrop({
connection,
address: basePubkey,
amount: STARTING_AMOUNT,
});

expect(await connection.getBalance(baseAccount.publicKey)).to.eq(
STARTING_AMOUNT,
);

const seed = 'hi there';
const programId = Keypair.generate().publicKey;
const createAccountWithSeedAddress = await PublicKey.createWithSeed(
basePubkey,
seed,
programId,
);
const space = 0;

let minimumAmount = await connection.getMinimumBalanceForRentExemption(
space,
);

const createAccountWithSeedParams = {
fromPubkey: basePubkey,
newAccountPubkey: createAccountWithSeedAddress,
basePubkey,
seed,
lamports: minimumAmount,
space,
programId,
};

const createAccountFeeTooHighTransaction = new Transaction().add(
ComputeBudgetProgram.requestUnits({
units: 2,
additionalFee: 2 * FEE_AMOUNT,
}),
SystemProgram.createAccountWithSeed(createAccountWithSeedParams),
);
await expect(
sendAndConfirmTransaction(
connection,
createAccountFeeTooHighTransaction,
[baseAccount],
{preflightCommitment: 'confirmed'},
),
).to.be.rejected;

expect(await connection.getBalance(baseAccount.publicKey)).to.eq(
STARTING_AMOUNT,
);

const createAccountFeeTransaction = new Transaction().add(
ComputeBudgetProgram.requestUnits({
units: 2,
additionalFee: FEE_AMOUNT,
}),
SystemProgram.createAccountWithSeed(createAccountWithSeedParams),
);
await sendAndConfirmTransaction(
connection,
createAccountFeeTransaction,
[baseAccount],
{preflightCommitment: 'confirmed'},
);
expect(await connection.getBalance(baseAccount.publicKey)).to.be.at.most(
STARTING_AMOUNT - FEE_AMOUNT - minimumAmount,
);

async function expectRequestHeapFailure(bytes: number) {
const requestHeapFrameTransaction = new Transaction().add(
ComputeBudgetProgram.requestHeapFrame({bytes}),
);
await expect(
sendAndConfirmTransaction(
connection,
requestHeapFrameTransaction,
[baseAccount],
{preflightCommitment: 'confirmed'},
),
).to.be.rejected;
}
const NOT_MULTIPLE_OF_1024 = 33 * 1024 + 1;
const BELOW_MIN = 1024;
const ABOVE_MAX = 257 * 1024;
await expectRequestHeapFailure(NOT_MULTIPLE_OF_1024);
await expectRequestHeapFailure(BELOW_MIN);
await expectRequestHeapFailure(ABOVE_MAX);

const VALID_BYTES = 33 * 1024;
const requestHeapFrameTransaction = new Transaction().add(
ComputeBudgetProgram.requestHeapFrame({bytes: VALID_BYTES}),
);
await sendAndConfirmTransaction(
connection,
requestHeapFrameTransaction,
[baseAccount],
{preflightCommitment: 'confirmed'},
);
}).timeout(10 * 1000);
}
});