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

A utility to estimate the compute unit consumption of a transaction message #2703

Conversation

steveluscher
Copy link
Collaborator

Summary

Correctly budgeting a compute unit limit for your transaction message can increase the probabilty that your transaction will be accepted for processing. If you don't declare a compute unit limit on your transaction, validators will assume an upper limit of 200K compute units (CU) per instruction.

Since validators have an incentive to pack as many transactions into each block as possible, they may choose to include transactions that they know will fit into the remaining compute budget for the current block over transactions that might not. For this reason, you should set a compute unit limit on each of your transaction messages, whenever possible.

This is a utility that helps you to estimate the actual compute unit cost of a given transaction message using the simulator.

Example

import { getSetComputeLimitInstruction } from "@solana-program/compute-budget";
import {
  createSolanaRpc,
  getComputeUnitEstimateForTransactionMessageFactory,
  pipe,
} from "@solana/web3.js";

// Create an estimator function.
const rpc = createSolanaRpc("http://127.0.0.1:8899");
const getComputeUnitEstimateForTransactionMessage =
  getComputeUnitEstimateForTransactionMessageFactory({
    rpc,
  });

// Create your transaction message.
const transactionMessage = pipe(
  createTransactionMessage({ version: "legacy" })
  /* ... */
);

// Request an estimate of the actual compute units this message will consume.
const computeUnitsEstimate = await getComputeUnitEstimateForTransactionMessage(
  transactionMessage
);

// Set the transaction message's compute unit budget.
const transactionMessageWithComputeUnitLimit =
  prependTransactionMessageInstruction(
    getSetComputeLimitInstruction({ units: computeUnitsEstimate }),
    transactionMessage
  );

Notes

  • The compute unit estimate is just that – an estimate. The compute unit consumption of the actual transaction might be higher or lower than what was observed in simulation. Unless you are confident that your particular transaction message will consume the same or fewer compute units as was estimated, you might like to augment the estimate by either a fixed number of CUs or a multiplier.
  • If you are preparing an unsigned transaction, destined to be signed and submitted to the network by a wallet, you might like to leave it up to the wallet to determine the compute unit limit. Consider that the wallet might have a more global view of how many compute units certain types of transactions consume, and might be able to make better estimates of an appropriate compute unit budget.

Test Plan

cd packages/library
pnpm turbo test:unit:node test:unit:browser

Copy link

changeset-bot bot commented May 10, 2024

🦋 Changeset detected

Latest commit: 2afc747

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 36 packages
Name Type
@solana/web3.js-experimental Patch
@solana/accounts Patch
@solana/addresses Patch
@solana/assertions Patch
@solana/codecs-core Patch
@solana/codecs-data-structures Patch
@solana/codecs-numbers Patch
@solana/codecs-strings Patch
@solana/codecs Patch
@solana/compat Patch
@solana/errors Patch
@solana/fast-stable-stringify Patch
@solana/functional Patch
@solana/instructions Patch
@solana/keys Patch
@solana/options Patch
@solana/programs Patch
@solana/rpc-api Patch
@solana/rpc-graphql Patch
@solana/rpc-parsed-types Patch
@solana/rpc-spec-types Patch
@solana/rpc-spec Patch
@solana/rpc-subscriptions-api Patch
@solana/rpc-subscriptions-spec Patch
@solana/rpc-subscriptions-transport-websocket Patch
@solana/rpc-subscriptions Patch
@solana/rpc-transformers Patch
@solana/rpc-transport-http Patch
@solana/rpc-types Patch
@solana/rpc Patch
@solana/signers Patch
@solana/sysvars Patch
@solana/transaction-confirmation Patch
@solana/transaction-messages Patch
@solana/transactions Patch
@solana/webcrypto-ed25519-polyfill Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Collaborator Author

steveluscher commented May 10, 2024

@steveluscher steveluscher force-pushed the 05-09-a_utility_to_estimate_the_compute_unit_consumption_of_a_transaction_message branch from 2768f69 to 8ce397a Compare May 10, 2024 05:35
Copy link

socket-security bot commented May 10, 2024

👍 Dependency issues cleared. Learn more about Socket for GitHub ↗︎

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report↗︎

@@ -878,6 +878,46 @@ const signedTransaction = await signTransaction([signer], transactionMessageWith
// => "Property 'lifetimeConstraint' is missing in type"
```

### Calibrating A Transaction Message's Compute Unit Budget

Correctly budgeting a compute unit limit for your transaction message can increase the probabilty that your transaction will be accepted for processing. If you don't declare a compute unit limit on your transaction, validators will assume an upper limit of 200K compute units (CU) per instruction.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

…validators will assume an upper limit of 200K compute units (CU) per instruction.

I didn't look this up. Is this true?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We looked into this a bit when we were looking at priority fees a while back. It's accurate, 200k per instruction (excluding those to compute budget program) with a 1.4M cap (which is the max CU allowed per transaction): https://github.com/solana-labs/solana/blob/4293f11cf13fc1e83f1baa2ca3bb2f8ea8f9a000/program-runtime/src/compute_budget.rs#L13


Correctly budgeting a compute unit limit for your transaction message can increase the probabilty that your transaction will be accepted for processing. If you don't declare a compute unit limit on your transaction, validators will assume an upper limit of 200K compute units (CU) per instruction.

Since validators have an incentive to pack as many transactions into each block as possible, they may choose to include transactions that they know will fit into the remaining compute budget for the current block over transactions that might not. For this reason, you should set a compute unit limit on each of your transaction messages, whenever possible.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

cc/ @joncinque to check over.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep this is correct! It could even be more forceful, since validators will skip transactions that might go over the block limit in the default implementation.

For example, if there are 599k CUs left in the block, and the transaction has 3 top-level instructions, the validator gives it the default limit of 600k CUs, and will immediately skip it.

> The compute unit estimate is just that – an estimate. The compute unit consumption of the actual transaction might be higher or lower than what was observed in simulation. Unless you are confident that your particular transaction message will consume the same or fewer compute units as was estimated, you might like to augment the estimate by either a fixed number of CUs or a multiplier.

> [!NOTE]
> If you are preparing an _unsigned_ transaction, destined to be signed and submitted to the network by a wallet, you might like to leave it up to the wallet to determine the compute unit limit. Consider that the wallet might have a more global view of how many compute units certain types of transactions consume, and might be able to make better estimates of an appropriate compute unit budget.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thoughts on this, @jordaaash?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Feel free to tag wallet folks.

Comment on lines +164 to +177
if (unitsConsumed == null) {
// This should never be hit, because all RPCs should support `unitsConsumed` by now.
throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@steveluscher steveluscher force-pushed the 05-09-a_utility_to_estimate_the_compute_unit_consumption_of_a_transaction_message branch from 8ce397a to efb8b95 Compare May 10, 2024 05:40
@steveluscher steveluscher force-pushed the 05-09-update_web3.js_readme_for_new_transactionmessage_api branch from 20e490a to 4852d1d Compare May 10, 2024 05:50
@steveluscher steveluscher force-pushed the 05-09-a_utility_to_estimate_the_compute_unit_consumption_of_a_transaction_message branch from efb8b95 to 6a74b53 Compare May 10, 2024 05:50
*/
const existingSetComputeUnitLimitInstructionIndex =
transactionMessage.instructions.findIndex(isSetComputeLimitInstruction);
const maxComputeUnitLimitInstruction = createComputeUnitLimitInstruction(4294967295 /* U32::MAX */);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Speculative: this might increase the estimate because it's higher than the actual max allowed, which is 1.4M - so there might be CUs consumed normalising it to 1.4M that won't be consumed when it's set to a realistic value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh nice catch! I tried this.

<1.4M u32::MAX
image.png image.png

Same number of CUs. Looks like the min() gets called unconditionally, so takes the same amount?

https://github.com/anza-xyz/agave/blob/75cc70d3b6671362bf2605326b05e623b4a0e679/program-runtime/src/compute_budget_processor.rs#L128C9-L133

  1. Bad: being wrong about this in the future
  2. Also bad: hardcoding MAX_COMPUTE_UNIT_LIMIT into JS clients then not being able to take it back.

I think I'll leave it the way it is here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

value: { unitsConsumed },
} = await rpc
.simulateTransaction(wireTransactionBytes, {
...simulateConfig,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is library and we can be opinionated, should we default the commitment to Confirmed instead of simulate's default of Finalized? It's most likely how they'll eventually send the transaction, and can still be explicitly set if needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

// FIXME: The simulation response returns compute units as a u64, but the `SetComputeLimit`
// instruction only accepts a u32. Until this changes, downcast it.
const downcastUnitsConsumed = unitsConsumed > 4294967295n ? 4294967295 : Number(unitsConsumed);
return downcastUnitsConsumed;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure how much knowledge we want to bake in here, but in practice if this is >1.4M it's an error

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I just want to let the server tell me, and if it tells me a wrong thing then oh well.

@steveluscher steveluscher force-pushed the 05-09-update_web3.js_readme_for_new_transactionmessage_api branch from 4852d1d to 6552499 Compare May 10, 2024 15:15
Base automatically changed from 05-09-update_web3.js_readme_for_new_transactionmessage_api to master May 10, 2024 15:16
@steveluscher steveluscher force-pushed the 05-09-a_utility_to_estimate_the_compute_unit_consumption_of_a_transaction_message branch from 6a74b53 to ab0cf40 Compare May 10, 2024 17:27
@steveluscher steveluscher force-pushed the 05-09-a_utility_to_estimate_the_compute_unit_consumption_of_a_transaction_message branch from ab0cf40 to 2afc747 Compare May 10, 2024 18:03
@steveluscher steveluscher merged commit 0908628 into master May 14, 2024
11 checks passed
@steveluscher steveluscher deleted the 05-09-a_utility_to_estimate_the_compute_unit_consumption_of_a_transaction_message branch May 14, 2024 17:11
Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

This looks really great, thanks!


Correctly budgeting a compute unit limit for your transaction message can increase the probabilty that your transaction will be accepted for processing. If you don't declare a compute unit limit on your transaction, validators will assume an upper limit of 200K compute units (CU) per instruction.

Since validators have an incentive to pack as many transactions into each block as possible, they may choose to include transactions that they know will fit into the remaining compute budget for the current block over transactions that might not. For this reason, you should set a compute unit limit on each of your transaction messages, whenever possible.
Copy link
Contributor

Choose a reason for hiding this comment

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

Yep this is correct! It could even be more forceful, since validators will skip transactions that might go over the block limit in the default implementation.

For example, if there are 599k CUs left in the block, and the transaction has 3 top-level instructions, the validator gives it the default limit of 600k CUs, and will immediately skip it.

Comment on lines +41 to +49
function createComputeUnitLimitInstruction(units: number): IInstruction<typeof COMPUTE_BUDGET_PROGRAM_ADDRESS> {
const data = new Uint8Array(5);
data[0] = SET_COMPUTE_UNIT_LIMIT_INSTRUCTION_INDEX;
getU32Encoder().write(units, data, 1 /* offset */);
return Object.freeze({
data,
programAddress: COMPUTE_BUDGET_PROGRAM_ADDRESS,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless it's exposed somewhere else, it might be nice to expose this too, and add an example of adding the compute unit limit instruction after getting the units consumed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's actually here:

https://github.com/solana-program/compute-budget/blob/f425ee62857b3154c85dc0f551898d5cfef67fa8/clients/js/src/generated/instructions/setComputeUnitLimit.ts#L78-L95

I would have used that actual function here, but unfortunately it would cause a circular dependency.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha, that works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants