-
Notifications
You must be signed in to change notification settings - Fork 911
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
A utility to estimate the compute unit consumption of a transaction message #2703
Conversation
🦋 Changeset detectedLatest commit: 2afc747 The changes in this PR will be included in the next version bump. This PR includes changesets to release 36 packages
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 |
This stack of pull requests is managed by Graphite. Learn more about stacking. Join @steveluscher and the rest of your teammates on Graphite |
2768f69
to
8ce397a
Compare
👍 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. |
@@ -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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on this, @jordaaash?
There was a problem hiding this comment.
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.
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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc/ @joncinque
8ce397a
to
efb8b95
Compare
20e490a
to
4852d1d
Compare
efb8b95
to
6a74b53
Compare
*/ | ||
const existingSetComputeUnitLimitInstructionIndex = | ||
transactionMessage.instructions.findIndex(isSetComputeLimitInstruction); | ||
const maxComputeUnitLimitInstruction = createComputeUnitLimitInstruction(4294967295 /* U32::MAX */); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
---|---|
Same number of CUs. Looks like the min()
gets called unconditionally, so takes the same amount?
- Bad: being wrong about this in the future
- 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually the absence of an effect here probably has more to do with this.
value: { unitsConsumed }, | ||
} = await rpc | ||
.simulateTransaction(wireTransactionBytes, { | ||
...simulateConfig, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
4852d1d
to
6552499
Compare
6a74b53
to
ab0cf40
Compare
New and removed dependencies detected. Learn more about Socket for GitHub ↗︎
🚮 Removed packages: npm/@types/node@18.19.33, npm/agadoo@3.0.0, npm/chalk@2.4.2, npm/dataloader@1.4.0, npm/eslint-config-turbo@1.13.3, npm/eslint-plugin-jest@27.9.0, npm/eslint-plugin-react-hooks@4.6.2, npm/eslint-plugin-simple-import-sort@10.0.0, npm/eslint-plugin-sort-keys-fix@1.1.2, npm/eslint-plugin-typescript-sort-keys@3.2.0, npm/eslint@8.57.0, npm/jest-environment-jsdom@30.0.0-alpha.3, npm/jest-runner-eslint@2.2.0, npm/jest-runner-prettier@1.0.0, npm/jest-watch-master@1.0.0, npm/jest-watch-select-projects@2.0.0, npm/jest-watch-typeahead@2.2.2, npm/jest@30.0.0-alpha.3, npm/node-fetch@2.7.0, npm/prettier@3.2.5, npm/rollup@3.29.4, npm/ts-node@10.9.2, npm/tsup@8.0.2, npm/turbo@1.13.3, npm/typescript@5.4.5, npm/undici-types@5.26.5 |
ab0cf40
to
2afc747
Compare
There was a problem hiding this 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. |
There was a problem hiding this comment.
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.
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, | ||
}); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually here:
I would have used that actual function here, but unfortunately it would cause a circular dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, that works
🎉 This PR is included in version 1.91.9 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up. |
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
Notes
Test Plan
cd packages/library pnpm turbo test:unit:node test:unit:browser