Understanding the TEAL Opcode Budget
Unlike other blockchains such as Ethereum, Algorand does not charge fees based on the computational cost of a smart contract call. Instead, smart contracts are given an overall opcode budget of 700 (application calls fail if this limit is surpassed). Opcode costs for each of TEAL’s opcodes are given here.
There are many advantages to this approach. Unlike in Ethereum, developers do not have to sacrifice readability for negligible performance improvements, as fees are flat instead of dependent on computational usage. This limit also ensures that computationally expensive contracts do not add unnecessary bloat to single blocks.
In this guide, we’ll go over how the Algorand Virtual Machine (AVM) handles these opcode budgets. We’ll also take advantage of a clever feature of atomic transactions to (finitely) extend it.
Basic familiarity with PyTEAL is assumed, but not more than what’s covered in Algorand’s PyTeal overview.
For this tutorial, you’ll need the following requirements:
- A pre-funded Algorand account. You can fund an account using the testnet bank.
- A running sandbox instance.
- The latest version of PyTeal installed (0.9.1 at the time of writing).
pip install pyteal==0.9.1
- The Algorand Python SDK used for interacting with the Algorand blockchain.
pip install py-algorand-sdk
1. Python SDK Setup
For this project, you’ll want to have two files:
testing.py. Below is some Algorand Python SDK boilerplate for executing transactions, which should go in
import base64 from algosdk.future import transaction from algosdk import account, mnemonic, logic from algosdk.v2client import algod from algosdk.logic import get_application_address from contracts import * def compile_program(client, source_code): compile_response = client.compile(source_code) return base64.b64decode(compile_response['result']) def create_app(client, private_key, approval_program, clear_program, global_schema, local_schema): sender = account.address_from_private_key(private_key) on_complete = transaction.OnComplete.NoOpOC.real params = client.suggested_params() txn = transaction.ApplicationCreateTxn(sender, params, on_complete, approval_program, clear_program, global_schema, local_schema) return execute_transaction(client, txn, private_key) def execute_transaction(client, txn, private_key): signed_txn = txn.sign(private_key) tx_id = signed_txn.transaction.get_txid() client.send_transactions([signed_txn]) transaction.wait_for_confirmation(client, tx_id, 10) return client.pending_transaction_info(tx_id) def execute_group_transaction(client, txns, private_key): stxns =  for txn in txns: stxns.append(txn.sign(private_key)) tx_id = client.send_transactions(stxns) transaction.wait_for_confirmation(client, tx_id, 10) ALGOD_TOKEN = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ALGOD_ADDRESS = "http://localhost:4001" algod_client = algod.AlgodClient(ALGOD_TOKEN, ALGOD_ADDRESS) MNEMONIC = "YOUR_MNEMONIC_HERE" # replace with your own mnemonic pkey = mnemonic.to_private_key(MNEMONIC) compiled_approval = compile_program(algod_client, approval_program()) compiled_clearstate = compile_program(algod_client, clear_state_program()) global_schema = transaction.StateSchema(0, 0) # no ints or bytes stored in global or local state local_schema = transaction.StateSchema(0, 0)
create_app functions are all taken from Algorand’s PyTEAL overview.
I’ve also added two new functions:
execute_group_transaction, which call an arbitrary transaction and grouped transactions (given an array of transactions), respectively.
create_app function has also been modified to use
execute_transaction to avoid redundancy.
As you might have noticed, we are importing from
contracts.py at the top, as well as calling undefined functions
clear_state_program(). Let’s go make them.
2. Creating and Executing an Empty Contract
contracts.py, create the following two functions:
from pyteal import * def approval_program(): return compileTeal(Approve(), Mode.Application, version=5) def clear_state_program(): return compileTeal(Approve(), Mode.Application, version=5)
These two basic functions handle every potential app call with an
Approve() statement. You may see some examples using
Return(Int(1)) as a default approval return value; this has now been replaced by the equivalent
Approve() statement (with
Reject() corresponding to
Now try running
testing.py. You should get nothing in return. Fantastic!
Next, let’s properly set up our
approval_program() to handle different app calls:
def approval_program(): handle_noop=Seq([ Approve(), ]) program = Cond( [Txn.application_id() == Int(0), Approve()], [Txn.on_completion() == OnComplete.OptIn, Reject()], [Txn.on_completion() == OnComplete.CloseOut, Reject()], [Txn.on_completion() == OnComplete.UpdateApplication, Reject()], [Txn.on_completion() == OnComplete.DeleteApplication, Reject()], [Txn.on_completion() == OnComplete.NoOp, handle_noop] ) return compileTeal(program, Mode.Application, version=5)
Here, we’ve (obviously) allowed for app creation (when an app call transaction is sent with an application ID of 0). Update and delete transactions are not allowed, and opt-in/close-out calls are also rejected since our smart contract will not be dealing with the local state. The most important transaction type to handle here is a NoOp transaction, which we’ve fed through to an empty
Seq() right now.
Now, let’s deploy and execute this smart contract with the Python SDK. At the bottom of
testing.py, add the following:
createAppTxn = create_app(algod_client, pkey, compiled_approval, compiled_clearstate, global_schema, local_schema) app_id = createAppTxn['application-index'] sender = account.address_from_private_key(pkey) noopTxn = transaction.ApplicationNoOpTxn(sender, algod_client.suggested_params(), app_id) execute_transaction(algod_client, noopTxn, pkey)
The contract will now execute the
Approve() statement in handle_noop, but nothing should print to the console.
3. Opcode Overview
Let’s now explore the mechanics behind the Algorand Virtual Machine (AVM) opcode budget system. We’ll be working before the
Approve() statement in the handle_noop
Seq, as this is what will be executed during a standard app call. From the opcode budget list, we see that the Keccak256 hash has a cost of 130. To test this out, add the following lines to your
handle_noop=Seq([ Pop(Keccak256(Bytes("a"))), Pop(Keccak256(Bytes("b"))), Pop(Keccak256(Bytes("c"))), Pop(Keccak256(Bytes("d"))), Pop(Keccak256(Bytes("e"))), Approve(), ])
There is no significance to the letters we are feeding in; the cost will be the same regardless of the input bytes. To ensure that the execution stack is clear before the
Approve() is reached, we are popping each generated hash as we go. If we try running
testing.py again, we see nothing happens, as expected: (5 hashes) * (130 per hash) = 650 < 700. However, if we add another hash to our
handle_noop=Seq([ Pop(Keccak256(Bytes("a"))), Pop(Keccak256(Bytes("b"))), Pop(Keccak256(Bytes("c"))), Pop(Keccak256(Bytes("d"))), Pop(Keccak256(Bytes("e"))), Pop(Keccak256(Bytes("f"))), Approve(), ])
The execution fails, with “logic eval error: dynamic cost budget exceeded”.
4. Control Flow
How are opcode budgets calculated across different potential execution paths? In previous versions of the AVM, opcode budgets were calculated line-by-line, regardless of which statements would be executed. For example, an If-Else pair with Keccak256 hashes in each code block would contribute 2*130 to the overall budget, despite only one hash being computed during execution. However, the AVM now tallies opcodes as a program executes, ensuring there is sufficient budget remaining before executing each statement and failing only if the 700 budget will be exceeded.
To demonstrate, wrap the final two Keccak256 hashes in an If-Else pair:
handle_noop=Seq([ Pop(Keccak256(Bytes("a"))), Pop(Keccak256(Bytes("b"))), Pop(Keccak256(Bytes("c"))), Pop(Keccak256(Bytes("d"))), If(Int(1)).Then( Pop(Keccak256(Bytes("e"))), ).Else( Pop(Keccak256(Bytes("f"))), ), Approve(), ])
As expected, the transaction stays within its opcode budget and does not fail. While this if statement is obviously not useful, it nicely demonstrates the underlying mechanism for computing opcode usage; as either path in the if statement results in 5 total hash computations.
5. Expanding the budget
What if you need a budget larger than 700? As of TEAL 4, opcode budgets are shared across group transactions, so your total shared budget for a grouped transaction can be up to 16 * 700 = 11200 (from the 16 maximum number of transactions in an atomic transaction). To show how this works, let’s first construct the following group transaction in
noopTxn = transaction.ApplicationNoOpTxn(sender, algod_client.suggested_params(), APP_ID, ) noopTxn2 = transaction.ApplicationNoOpTxn(sender, algod_client.suggested_params(), APP_ID, ) groupTxnId = transaction.calculate_group_id([noopTxn, noopTxn2]) noopTxn.group = groupTxnId noopTxn2.group = groupTxnId execute_group_transaction(algod_client, [noopTxn, noopTxn2], pkey)
Here, we are creating two nearly-identical NoOp application calls, differing only by a single parameter (0 for
noopTxn and 1 for
noopTxn2). We then package them into a single group transaction and execute them using the helper function
execute_group_transaction provided at the beginning. Next, let’s update
contracts.py to handle these different parameters. Inside the handle_noop
Seq(), let’s modify our If-Else:
handle_noop=Seq([ Pop(Keccak256(Bytes("a"))), Pop(Keccak256(Bytes("b"))), Pop(Keccak256(Bytes("c"))), Pop(Keccak256(Bytes("d"))), If(Btoi(Txn.application_args)).Then(Seq([ Pop(Keccak256(Bytes("e"))), Pop(Keccak256(Bytes("f"))), ])) .Else(Seq([ Pop(Keccak256(Bytes("g"))), ])), Approve(), ])
The Btoi(Txn.application_args) will evaluate to false (
Int(0)) if our argument is 0, and true otherwise. From our group transaction, we see that the first NoOp transaction will evaluate to false, while the second will be true. Running
testing.py again, we find that the execution fails: Although we have a total budget of (700-10) * 2 = 1380, the first transaction computes hashes for the letters a-d and g for an opcode cost of 5 * 130 = 650, while the second transaction computes hashes for a-f for an opcode cost of 6 * 130 = 780. The total opcode cost for the group transaction, therefore, is 650 + 780 = 1430, exceeding the 1400 limit.
However, if we remove the “g” hash in the
Else block (just commenting it out), the execution succeeds. While the second transaction alone (the one in which the first
If block is executed) will compute hashes for the letters a-f for an opcode cost of 6 * 130 = 780 > 700, the first transaction only computes the four hashes before the if-else block, for an opcode cost of 4 * 130 = 520. The total opcode cost for the group transaction is then 520 + 780 = 1300, which is less than the 1400 limit.
Congratulations! You now understand the TEAL opcode budget and are able to use atomic transactions to increase this budget amount. TEAL is evolving rapidly, so make sure to stay informed for future changes, such as budget increases beyond the current 700 or other ways to increase the budget yourself, such as by creating “inner applications.”