summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Jannotti <john.jannotti@algorand.com>2022-01-25 21:11:52 -0500
committerGitHub <noreply@github.com>2022-01-25 21:11:52 -0500
commit9bb0e4619f51bd5bbcd8123ea146a0302326d9f4 (patch)
treed1f94f28b144a0a3ac3c4b4eb1d603b41f60f6ad
parentebfcee1a637afddfcb1fb3f185b310221f3652ed (diff)
Feature/contract to contract (#3505)
* Three new globals for to help contract-to-contract usability * detritis * Check error * doc comments * Impose limits on the entire "tree" of inner calls. This also increases the realism of testing of multiple app calls in a group by creating the EvalParams with the real constructor, thus getting the pooling stuff tested here without playing games manipulating the ep after construction. * Move appID tracking into EvalContext, out of LedgerForLogic This change increases the seperation between AVM execution and the ledger being used to lookup resources. Previously, the ledger kept track of the appID being executed, to offer a narrower interface to those resources. But now, with app-to-app calls, the appID being executed must change, and the AVM needs to maintain the current appID. * Stupid linter * Fix unit tests error messages * Allow access to resources created in the same transaction group The method will be reworked, but the tests are correct and want to get them visible to team. * Access to apps created in group Also adds some tests that are currently skipped for testing - access to addresses of newly created apps - use of gaid in inner transactions Both require some work to implement the thing being tested. * Remove tracked created mechanism in favor of examining applydata. * Allow v6 AVM code to use in-group created asas, apps (& their accts) One exception - apps can not mutate (put or del) keys from the app accounts, because EvalDelta cannot encode such changes. * lint docs * typo * The review dog needs obedience training. * Use one EvalParams for logic evals, another for apps in dry run We used to use one ep per transaction, shared between sig and and app. But the new model of ep usage is to keep using one while evaluating an entire group. The app ep is now built logic.NewAppEvalParams which, hopefully, will prevent some bugs when we change something in the EvalParams and don't reflect it in what was a "raw" EvalParams construction in debugger and dry run. * Use logic.NewAppEvalParams to decrease copying and bugs in debugger * Simplify use of NewEvalParams. No more nil return when no apps. This way, NewEvalParams can be used for all creations of EvalParams, whether they are intended for logicsig or app use, greatly simplifying the way we make them for use by dry run or debugger (where they serve double duty). * Remove explicit PastSideEffects handling in tealdbg * Always create EvalParams to evaluate a transaction group. We used to have an optimization to avoid creating EvalParams unless there was an app call in the transaction group. But the interface to allow transaction processing to communicate changes into the EvalParams is complicated by that (we must only do it if there is one!) This also allows us to use the same construction function for eps created for app and logic evaluation, simplifying dry-run and debugger. The optimization is less needed now anyway: 1) The ep is now shared for the whole group, so it's only one. 2) The ep is smaller now, as we only store nil pointers instead of larger scratch space objects for non-app calls. * Correct mistaken commit * Spec improvments * More spec improvments, including resource "availability" * Recursively return inner transaction tree * Lint * No need for ConfirmedRound, so don't deref a nil pointer! * license check * Shut up, dawg. * base64 merge cleanup * Remove the extraneous field type arrays. * bsqrt * acct_holding_get, a unified opcode for account field access * Thanks, dawg * CR and more spec simplification * e2e test for inner transaction appls * Give max group size * 16 inner txns, regardless of apps present * Adjust test for allowing 256 inners * NCC audit (except double credit) plus app depth limit * Fix for double credit from inner groups, found by NCC * Spec updates * Count inners properly, increase inner txn allocbound
-rw-r--r--cmd/opdoc/opdoc.go7
-rw-r--r--config/consensus.go10
-rw-r--r--data/transactions/logic/README.md104
-rw-r--r--data/transactions/logic/README_in.md94
-rw-r--r--data/transactions/logic/TEAL_opcodes.md23
-rw-r--r--data/transactions/logic/doc.go12
-rw-r--r--data/transactions/logic/eval.go105
-rw-r--r--data/transactions/logic/evalAppTxn_test.go116
-rw-r--r--data/transactions/logic/evalStateful_test.go43
-rw-r--r--data/transactions/logic/eval_test.go10
-rw-r--r--data/transactions/logic/ledger_test.go6
-rw-r--r--data/transactions/logic/opcodes.go2
-rw-r--r--data/transactions/msgp_gen.go8
-rw-r--r--data/transactions/teal.go2
14 files changed, 403 insertions, 139 deletions
diff --git a/cmd/opdoc/opdoc.go b/cmd/opdoc/opdoc.go
index c98a1a0f7..b226b31a3 100644
--- a/cmd/opdoc/opdoc.go
+++ b/cmd/opdoc/opdoc.go
@@ -147,6 +147,11 @@ func appParamsFieldsMarkdown(out io.Writer) {
fieldSpecsMarkdown(out, logic.AppParamsFieldNames, logic.AppParamsFieldSpecByName)
}
+func acctParamsFieldsMarkdown(out io.Writer) {
+ fmt.Fprintf(out, "\n`acct_params_get` Fields:\n\n")
+ fieldSpecsMarkdown(out, logic.AcctParamsFieldNames, logic.AcctParamsFieldSpecByName)
+}
+
func ecDsaCurvesMarkdown(out io.Writer) {
fmt.Fprintf(out, "\n`ECDSA` Curves:\n\n")
fieldSpecsMarkdown(out, logic.EcdsaCurveNames, logic.EcdsaCurveSpecByName)
@@ -239,6 +244,8 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) {
assetParamsFieldsMarkdown(out)
} else if op.Name == "app_params_get" {
appParamsFieldsMarkdown(out)
+ } else if op.Name == "acct_params_get" {
+ acctParamsFieldsMarkdown(out)
} else if strings.HasPrefix(op.Name, "ecdsa") {
ecDsaCurvesMarkdown(out)
}
diff --git a/config/consensus.go b/config/consensus.go
index 45a48030e..22edb5f69 100644
--- a/config/consensus.go
+++ b/config/consensus.go
@@ -282,7 +282,9 @@ type ConsensusParams struct {
// maximum sum of the lengths of the key and value of one app state entry
MaxAppSumKeyValueLens int
- // maximum number of inner transactions that can be created by an app call
+ // maximum number of inner transactions that can be created by an app call.
+ // with EnableInnerTransactionPooling, limit is multiplied by MaxTxGroupSize
+ // and enforced over the whole group.
MaxInnerTransactions int
// should inner transaction limit be pooled across app calls?
@@ -443,8 +445,8 @@ var MaxStateDeltaKeys int
// any version, used only for decoding purposes. Never decrease this value.
var MaxLogCalls int
-// MaxInnerTransactions is the maximum number of inner transactions that may be created in an app call.
-var MaxInnerTransactions int
+// MaxInnerTransactionsPerDelta is the maximum number of inner transactions in one EvalDelta
+var MaxInnerTransactionsPerDelta int
// MaxLogicSigMaxSize is the largest logical signature appear in any of the supported
// protocols, used for decoding purposes.
@@ -513,7 +515,7 @@ func checkSetAllocBounds(p ConsensusParams) {
// There is no consensus parameter for MaxLogCalls and MaxAppProgramLen as an approximation
// Its value is much larger than any possible reasonable MaxLogCalls value in future
checkSetMax(p.MaxAppProgramLen, &MaxLogCalls)
- checkSetMax(p.MaxInnerTransactions, &MaxInnerTransactions)
+ checkSetMax(p.MaxInnerTransactions*p.MaxTxGroupSize, &MaxInnerTransactionsPerDelta)
checkSetMax(p.MaxProposedExpiredOnlineAccounts, &MaxProposedExpiredOnlineAccounts)
}
diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md
index f81173d39..68c4126e5 100644
--- a/data/transactions/logic/README.md
+++ b/data/transactions/logic/README.md
@@ -1,7 +1,7 @@
# The Algorand Virtual Machine (AVM) and TEAL.
The AVM is a bytecode based stack interpreter that executes programs
-asscoiated with Algorand transactions. TEAL is an assembly language
+associated with Algorand transactions. TEAL is an assembly language
syntax for specifying a program that is ultimately converted to AVM
bytecode. These programs can be used to check the parameters of the
transaction and approve the transaction as if by a signature. This use
@@ -16,7 +16,9 @@ few global values. In addition, _Smart Contracts_ have access to
limited state that is global to the application and per-account local
state for each account that has opted-in to the application. For both
types of program, approval is signaled by finishing with the stack
-containing a single non-zero uint64 value.
+containing a single non-zero uint64 value, though `return` can be used
+to signal an early approval which approves based only upon the top
+stack value being a non-zero uint64 value.
## The Stack
@@ -34,8 +36,8 @@ exceeded or if a byte-array element exceed 4096 bytes, the program fails.
In addition to the stack there are 256 positions of scratch
space. Like stack values, scratch locations may be uint64s or
-byte-arrays. Scratch locations are intialized as uint64 zero. Scratch
-space is acccsed by the `load(s)` and `store(s)` opcodes which move
+byte-arrays. Scratch locations are initialized as uint64 zero. Scratch
+space is accessed by the `load(s)` and `store(s)` opcodes which move
data from or to scratch space, respectively.
## Versions
@@ -82,19 +84,41 @@ even before the transaction has been included in a block. These Args
are _not_ part of the transaction ID nor of the TxGroup hash. They
also cannot be read from other programs in the group of transactions.
-A program can either authorize some delegated action on a normal private key signed or multisig account or be wholly in charge of a contract account.
-
-* If the account has signed the program (an ed25519 signature on "Program" concatenated with the program bytecode) then if the program returns true the transaction is authorized as if the account had signed it. This allows an account to hand out a signed program so that other users can carry out delegated actions which are approved by the program. Note that Smart Signature Args are _not_ signed.
-
-* If the SHA512_256 hash of the program (prefixed by "Program") is equal to the transaction Sender address then this is a contract account wholly controlled by the program. No other signature is necessary or possible. The only way to execute a transaction against the contract account is for the program to approve it.
-
-The bytecode plus the length of all Args must add up to no more than 1000 bytes (consensus parameter LogicSigMaxSize). Each opcode has an associated cost and the program cost must total no more than 20,000 (consensus parameter LogicSigMaxCost). Most opcodes have a cost of 1, but a few slow cryptographic operations are much higher. Prior to v4, the program's cost was estimated as the static sum of all the opcode costs in the program (whether they were actually executed or not). Beginning with v4, the program's cost is tracked dynamically, while being evaluated. If the program exceeds its budget, it fails.
+A program can either authorize some delegated action on a normal
+signature-based or multisignature-based account or be wholly in charge
+of a contract account.
+
+* If the account has signed the program (by providing a valid ed25519
+ signature or valid multisignature for the authorizer address on the
+ string "Program" concatenated with the program bytecode) then: if the
+ program returns true the transaction is authorized as if the account
+ had signed it. This allows an account to hand out a signed program
+ so that other users can carry out delegated actions which are
+ approved by the program. Note that Smart Signature Args are _not_
+ signed.
+
+* If the SHA512_256 hash of the program (prefixed by "Program") is
+ equal to authorizer address of the transaction sender then this is a
+ contract account wholly controlled by the program. No other
+ signature is necessary or possible. The only way to execute a
+ transaction against the contract account is for the program to
+ approve it.
+
+The bytecode plus the length of all Args must add up to no more than
+1000 bytes (consensus parameter LogicSigMaxSize). Each opcode has an
+associated cost and the program cost must total no more than 20,000
+(consensus parameter LogicSigMaxCost). Most opcodes have a cost of 1,
+but a few slow cryptographic operations have a much higher cost. Prior
+to v4, the program's cost was estimated as the static sum of all the
+opcode costs in the program (whether they were actually executed or
+not). Beginning with v4, the program's cost is tracked dynamically,
+while being evaluated. If the program exceeds its budget, it fails.
## Execution Environment for Smart Contracts (Applications)
Smart Contracts are executed in ApplicationCall transactions. Like
Smart Signatures, contracts indicate success by leaving a single
-non-zero integer on the stack. A failed smart contract call is not a
+non-zero integer on the stack. A failed Smart Contract call is not a
valid transaction, thus not written to the blockchain. Nodes maintain
a list of transactions that would succeed, given the current state of
the blockchain, called the transaction pool. Nodes draw from the pool
@@ -113,34 +137,54 @@ blockchain.
### Resource availability
Smart contracts have limits on their execution budget (700, consensus
-paramter MaxAppProgramCost), and the amount of blockchain state they
+parameter MaxAppProgramCost), and the amount of blockchain state they
may examine. Opcodes may only access blockchain resources such as
Accounts, Assets, and contract state if the given resource is
-_available_.
+_available_.
* A resource in the "foreign array" fields of the ApplicationCall
transaction (`txn.Accounts`, `txn.ForeignAssets`, and
`txn.ForeignApplications`) is _available_.
- * The `global CurrentApplicationID` and `txn.Sender` are _available_.
+ * The `txn.Sender`, `global CurrentApplicationID`, and `global
+ CurrentApplicationAddress` are _available_.
* Prior to v4, all assets were considered _available_ to the
- `asset_holding_get` opcode.
+ `asset_holding_get` opcode, and all applications were _available_
+ to the `app_local_get_ex` opcode.
* Since v6, any asset or contract that was created earlier in the
- same transaction group is _available_. In addition, any account
- that is the contract account of a contract that was created earlier
- in the group is _available_.
+ same transaction group (whether by a top-level or inner
+ transaction) is _available_. In addition, any account that is the
+ associated account of a contract that was created earlier in the
+ group is _available_.
## Constants
-Constants are loaded into storage separate from the stack and scratch space. They can then be pushed onto the stack by referring to the type and index. This makes for efficient re-use of byte constants used for account addresses, etc. Constants that are not reused can be pushed with `pushint` or `pushbytes`.
+Constants can be pushed onto the stack in two different ways:
+
+1. Constants can be pushed directly with `pushint` or
+ `pushbytes`. This method is more efficient for constants that are
+ only used once.
-The assembler will hide most of this, allowing simple use of `int 1234` and `byte 0xcafed00d`. These constants will automatically get assembled into int and byte pages of constants, de-duplicated, and operations to load them from constant storage space inserted.
+2. Constants can be loaded into storage separate from the stack and
+ scratch space, using two opcodes `intcblock` and
+ `bytecblock`. Then, constants from this storage can be pushed
+ pushed onto the stack by referring to the type and index using
+ `intc`, `intc_[0123]`, `bytec`, and `bytec_[0123]`. This method is
+ more efficient for constants that are used multiple times.
-Constants are prepared by two opcodes, `intcblock` and `bytecblock`. Both of these use [proto-buf style variable length unsigned int](https://developers.google.com/protocol-buffers/docs/encoding#varint), reproduced [here](#varuint). The `intcblock` opcode is followed by a varuint specifying the length of the array and then that number of varuint. The `bytecblock` opcode is followed by a varuint array length then that number of pairs of (varuint, bytes) length prefixed byte strings.
+The assembler will hide most of this, allowing simple use of `int 1234`
+and `byte 0xcafed00d`. Constants introduced via `int` and `byte` will
+be assembled into appropriate uses of `pushint|pushbytes` and
+`{int|byte}c, {int|byte}c_[0123]` to minimize program size.
-Constants are pushed onto the stack by `intc`, `intc_[0123]`, `pushint`, `bytec`, `bytec_[0123]`, and `pushbytes`. The assembler will handle converting `int N` or `byte N` into the appropriate form of the instruction needed.
+The opcodes intcblock and bytecblock use [proto-buf style variable length unsigned int](https://developers.google.com/protocol-buffers/docs/encoding#varint),
+reproduced [here](#varuint). The `intcblock` opcode is followed by a
+varuint specifying the number of integer constants and then that
+number of varuints. The `bytecblock` opcode is followed by a varuint
+specifying the number of byte constants, and then that number of pairs
+of (varuint, bytes) length prefixed byte strings.
### Named Integer Constants
@@ -405,10 +449,10 @@ Some of these have immediate data in the byte or bytes after the opcode.
| 55 | LocalNumByteSlice | uint64 | v3 | Number of local state byteslices in ApplicationCall |
| 56 | ExtraProgramPages | uint64 | v4 | Number of additional pages for each of the application's approval and clear state programs. An ExtraProgramPages of 1 means 2048 more total bytes, or 1024 for each program. |
| 57 | Nonparticipation | uint64 | v5 | Marks an account nonparticipating for rewards |
-| 58 | Logs | []byte | v5 | Log messages emitted by an application call (`itxn` only until v6). Application mode only |
-| 59 | NumLogs | uint64 | v5 | Number of Logs (`itxn` only until v6). Application mode only |
-| 60 | CreatedAssetID | uint64 | v5 | Asset ID allocated by the creation of an ASA (`itxn` only until v6). Application mode only |
-| 61 | CreatedApplicationID | uint64 | v5 | ApplicationID allocated by the creation of an application (`itxn` only until v6). Application mode only |
+| 58 | Logs | []byte | v5 | Log messages emitted by an application call (only with `itxn` in v5). Application mode only |
+| 59 | NumLogs | uint64 | v5 | Number of Logs (only with `itxn` in v5). Application mode only |
+| 60 | CreatedAssetID | uint64 | v5 | Asset ID allocated by the creation of an ASA (only with `itxn` in v5). Application mode only |
+| 61 | CreatedApplicationID | uint64 | v5 | ApplicationID allocated by the creation of an application (only with `itxn` in v5). Application mode only |
Additional details in the [opcodes document](TEAL_opcodes.md#txn) on the `txn` op.
@@ -507,7 +551,7 @@ Account fields used in the `acct_params_get` opcode.
| `uncover n` | remove the value at depth N in the stack and shift above items down so the Nth deep value is on top of the stack. Fails if stack depth <= N. |
| `swap` | swaps A and B on stack |
| `select` | selects one of two values based on top-of-stack: B if C != 0, else A |
-| `assert` | immediately fail unless X is a non-zero number |
+| `assert` | immediately fail unless A is a non-zero number |
| `callsub target` | branch unconditionally to TARGET, saving the next instruction on the call stack |
| `retsub` | pop the top instruction from the call stack and branch to it |
@@ -569,7 +613,7 @@ the the array, rather than setting the entire array at once.
`itxn_field` fails immediately for unsupported fields, unsupported
transaction types, or improperly typed values for a particular
-field. `itxn_field` makes aceptance decisions entirely from the field
+field. `itxn_field` makes acceptance decisions entirely from the field
and value provided, never considering previously set fields. Illegal
interactions between fields, such as setting fields that belong to two
different transaction types, are rejected by `itxn_submit`.
@@ -661,7 +705,7 @@ This requirement is enforced as follows:
* Compute the largest version number across all the transactions in a group (of size 1 or more), call it `maxVerNo`. If any transaction in this group has a program with a version smaller than `maxVerNo`, then that TEAL program will fail.
In addition, applications must be version 6 or greater to be eligible
-for calling in an inner transaction.
+for being called in an inner transaction.
## Varuint
diff --git a/data/transactions/logic/README_in.md b/data/transactions/logic/README_in.md
index 4b3ef750e..8e81b7afb 100644
--- a/data/transactions/logic/README_in.md
+++ b/data/transactions/logic/README_in.md
@@ -1,7 +1,7 @@
# The Algorand Virtual Machine (AVM) and TEAL.
The AVM is a bytecode based stack interpreter that executes programs
-asscoiated with Algorand transactions. TEAL is an assembly language
+associated with Algorand transactions. TEAL is an assembly language
syntax for specifying a program that is ultimately converted to AVM
bytecode. These programs can be used to check the parameters of the
transaction and approve the transaction as if by a signature. This use
@@ -16,7 +16,9 @@ few global values. In addition, _Smart Contracts_ have access to
limited state that is global to the application and per-account local
state for each account that has opted-in to the application. For both
types of program, approval is signaled by finishing with the stack
-containing a single non-zero uint64 value.
+containing a single non-zero uint64 value, though `return` can be used
+to signal an early approval which approves based only upon the top
+stack value being a non-zero uint64 value.
## The Stack
@@ -34,8 +36,8 @@ exceeded or if a byte-array element exceed 4096 bytes, the program fails.
In addition to the stack there are 256 positions of scratch
space. Like stack values, scratch locations may be uint64s or
-byte-arrays. Scratch locations are intialized as uint64 zero. Scratch
-space is acccsed by the `load(s)` and `store(s)` opcodes which move
+byte-arrays. Scratch locations are initialized as uint64 zero. Scratch
+space is accessed by the `load(s)` and `store(s)` opcodes which move
data from or to scratch space, respectively.
## Versions
@@ -82,19 +84,41 @@ even before the transaction has been included in a block. These Args
are _not_ part of the transaction ID nor of the TxGroup hash. They
also cannot be read from other programs in the group of transactions.
-A program can either authorize some delegated action on a normal private key signed or multisig account or be wholly in charge of a contract account.
-
-* If the account has signed the program (an ed25519 signature on "Program" concatenated with the program bytecode) then if the program returns true the transaction is authorized as if the account had signed it. This allows an account to hand out a signed program so that other users can carry out delegated actions which are approved by the program. Note that Smart Signature Args are _not_ signed.
-
-* If the SHA512_256 hash of the program (prefixed by "Program") is equal to the transaction Sender address then this is a contract account wholly controlled by the program. No other signature is necessary or possible. The only way to execute a transaction against the contract account is for the program to approve it.
-
-The bytecode plus the length of all Args must add up to no more than 1000 bytes (consensus parameter LogicSigMaxSize). Each opcode has an associated cost and the program cost must total no more than 20,000 (consensus parameter LogicSigMaxCost). Most opcodes have a cost of 1, but a few slow cryptographic operations are much higher. Prior to v4, the program's cost was estimated as the static sum of all the opcode costs in the program (whether they were actually executed or not). Beginning with v4, the program's cost is tracked dynamically, while being evaluated. If the program exceeds its budget, it fails.
+A program can either authorize some delegated action on a normal
+signature-based or multisignature-based account or be wholly in charge
+of a contract account.
+
+* If the account has signed the program (by providing a valid ed25519
+ signature or valid multisignature for the authorizer address on the
+ string "Program" concatenated with the program bytecode) then: if the
+ program returns true the transaction is authorized as if the account
+ had signed it. This allows an account to hand out a signed program
+ so that other users can carry out delegated actions which are
+ approved by the program. Note that Smart Signature Args are _not_
+ signed.
+
+* If the SHA512_256 hash of the program (prefixed by "Program") is
+ equal to authorizer address of the transaction sender then this is a
+ contract account wholly controlled by the program. No other
+ signature is necessary or possible. The only way to execute a
+ transaction against the contract account is for the program to
+ approve it.
+
+The bytecode plus the length of all Args must add up to no more than
+1000 bytes (consensus parameter LogicSigMaxSize). Each opcode has an
+associated cost and the program cost must total no more than 20,000
+(consensus parameter LogicSigMaxCost). Most opcodes have a cost of 1,
+but a few slow cryptographic operations have a much higher cost. Prior
+to v4, the program's cost was estimated as the static sum of all the
+opcode costs in the program (whether they were actually executed or
+not). Beginning with v4, the program's cost is tracked dynamically,
+while being evaluated. If the program exceeds its budget, it fails.
## Execution Environment for Smart Contracts (Applications)
Smart Contracts are executed in ApplicationCall transactions. Like
Smart Signatures, contracts indicate success by leaving a single
-non-zero integer on the stack. A failed smart contract call is not a
+non-zero integer on the stack. A failed Smart Contract call is not a
valid transaction, thus not written to the blockchain. Nodes maintain
a list of transactions that would succeed, given the current state of
the blockchain, called the transaction pool. Nodes draw from the pool
@@ -113,34 +137,54 @@ blockchain.
### Resource availability
Smart contracts have limits on their execution budget (700, consensus
-paramter MaxAppProgramCost), and the amount of blockchain state they
+parameter MaxAppProgramCost), and the amount of blockchain state they
may examine. Opcodes may only access blockchain resources such as
Accounts, Assets, and contract state if the given resource is
-_available_.
+_available_.
* A resource in the "foreign array" fields of the ApplicationCall
transaction (`txn.Accounts`, `txn.ForeignAssets`, and
`txn.ForeignApplications`) is _available_.
- * The `global CurrentApplicationID` and `txn.Sender` are _available_.
+ * The `txn.Sender`, `global CurrentApplicationID`, and `global
+ CurrentApplicationAddress` are _available_.
* Prior to v4, all assets were considered _available_ to the
- `asset_holding_get` opcode.
+ `asset_holding_get` opcode, and all applications were _available_
+ to the `app_local_get_ex` opcode.
* Since v6, any asset or contract that was created earlier in the
- same transaction group is _available_. In addition, any account
- that is the contract account of a contract that was created earlier
- in the group is _available_.
+ same transaction group (whether by a top-level or inner
+ transaction) is _available_. In addition, any account that is the
+ associated account of a contract that was created earlier in the
+ group is _available_.
## Constants
-Constants are loaded into storage separate from the stack and scratch space. They can then be pushed onto the stack by referring to the type and index. This makes for efficient re-use of byte constants used for account addresses, etc. Constants that are not reused can be pushed with `pushint` or `pushbytes`.
+Constants can be pushed onto the stack in two different ways:
+
+1. Constants can be pushed directly with `pushint` or
+ `pushbytes`. This method is more efficient for constants that are
+ only used once.
-The assembler will hide most of this, allowing simple use of `int 1234` and `byte 0xcafed00d`. These constants will automatically get assembled into int and byte pages of constants, de-duplicated, and operations to load them from constant storage space inserted.
+2. Constants can be loaded into storage separate from the stack and
+ scratch space, using two opcodes `intcblock` and
+ `bytecblock`. Then, constants from this storage can be pushed
+ pushed onto the stack by referring to the type and index using
+ `intc`, `intc_[0123]`, `bytec`, and `bytec_[0123]`. This method is
+ more efficient for constants that are used multiple times.
-Constants are prepared by two opcodes, `intcblock` and `bytecblock`. Both of these use [proto-buf style variable length unsigned int](https://developers.google.com/protocol-buffers/docs/encoding#varint), reproduced [here](#varuint). The `intcblock` opcode is followed by a varuint specifying the length of the array and then that number of varuint. The `bytecblock` opcode is followed by a varuint array length then that number of pairs of (varuint, bytes) length prefixed byte strings.
+The assembler will hide most of this, allowing simple use of `int 1234`
+and `byte 0xcafed00d`. Constants introduced via `int` and `byte` will
+be assembled into appropriate uses of `pushint|pushbytes` and
+`{int|byte}c, {int|byte}c_[0123]` to minimize program size.
-Constants are pushed onto the stack by `intc`, `intc_[0123]`, `pushint`, `bytec`, `bytec_[0123]`, and `pushbytes`. The assembler will handle converting `int N` or `byte N` into the appropriate form of the instruction needed.
+The opcodes intcblock and bytecblock use [proto-buf style variable length unsigned int](https://developers.google.com/protocol-buffers/docs/encoding#varint),
+reproduced [here](#varuint). The `intcblock` opcode is followed by a
+varuint specifying the number of integer constants and then that
+number of varuints. The `bytecblock` opcode is followed by a varuint
+specifying the number of byte constants, and then that number of pairs
+of (varuint, bytes) length prefixed byte strings.
### Named Integer Constants
@@ -285,7 +329,7 @@ the the array, rather than setting the entire array at once.
`itxn_field` fails immediately for unsupported fields, unsupported
transaction types, or improperly typed values for a particular
-field. `itxn_field` makes aceptance decisions entirely from the field
+field. `itxn_field` makes acceptance decisions entirely from the field
and value provided, never considering previously set fields. Illegal
interactions between fields, such as setting fields that belong to two
different transaction types, are rejected by `itxn_submit`.
@@ -368,7 +412,7 @@ This requirement is enforced as follows:
* Compute the largest version number across all the transactions in a group (of size 1 or more), call it `maxVerNo`. If any transaction in this group has a program with a version smaller than `maxVerNo`, then that TEAL program will fail.
In addition, applications must be version 6 or greater to be eligible
-for calling in an inner transaction.
+for being called in an inner transaction.
## Varuint
diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md
index 3a85afc72..e3291f405 100644
--- a/data/transactions/logic/TEAL_opcodes.md
+++ b/data/transactions/logic/TEAL_opcodes.md
@@ -432,10 +432,10 @@ The notation J,K indicates that two uint64 values J and K are interpreted as a u
| 55 | LocalNumByteSlice | uint64 | v3 | Number of local state byteslices in ApplicationCall |
| 56 | ExtraProgramPages | uint64 | v4 | Number of additional pages for each of the application's approval and clear state programs. An ExtraProgramPages of 1 means 2048 more total bytes, or 1024 for each program. |
| 57 | Nonparticipation | uint64 | v5 | Marks an account nonparticipating for rewards |
-| 58 | Logs | []byte | v5 | Log messages emitted by an application call (`itxn` only until v6). Application mode only |
-| 59 | NumLogs | uint64 | v5 | Number of Logs (`itxn` only until v6). Application mode only |
-| 60 | CreatedAssetID | uint64 | v5 | Asset ID allocated by the creation of an ASA (`itxn` only until v6). Application mode only |
-| 61 | CreatedApplicationID | uint64 | v5 | ApplicationID allocated by the creation of an application (`itxn` only until v6). Application mode only |
+| 58 | Logs | []byte | v5 | Log messages emitted by an application call (only with `itxn` in v5). Application mode only |
+| 59 | NumLogs | uint64 | v5 | Number of Logs (only with `itxn` in v5). Application mode only |
+| 60 | CreatedAssetID | uint64 | v5 | Asset ID allocated by the creation of an ASA (only with `itxn` in v5). Application mode only |
+| 61 | CreatedApplicationID | uint64 | v5 | ApplicationID allocated by the creation of an application (only with `itxn` in v5). Application mode only |
TypeEnum mapping:
@@ -623,7 +623,7 @@ See `bnz` for details on how branches work. `b` always jumps to the offset.
- Opcode: 0x44
- Stack: ..., A: uint64 &rarr; ...
-- immediately fail unless X is a non-zero number
+- immediately fail unless A is a non-zero number
- Availability: v3
## pop
@@ -956,11 +956,20 @@ params: Txn.ForeignApps offset or an _available_ app id. Return: did_exist flag
## acct_params_get f
- Opcode: 0x73 {uint8 account params field index}
-- Stack: ..., A: uint64 &rarr; ..., X: any, Y: uint64
+- Stack: ..., A &rarr; ..., X: any, Y: uint64
- X is field F from account A. Y is 1 if A owns positive algos, else 0
- Availability: v6
- Mode: Application
+`acct_params_get` Fields:
+
+| Index | Name | Type | Notes |
+| - | ------ | -- | --------- |
+| 0 | AcctBalance | uint64 | Account balance in microalgos |
+| 1 | AcctMinBalance | uint64 | Minimum required blance for account, in microalgos |
+| 2 | AcctAuthAddr | []byte | Address the account is rekeyed to. |
+
+
## min_balance
- Opcode: 0x78
@@ -1210,7 +1219,7 @@ bitlen interprets arrays as big-endian integers, unlike setbit/getbit
- Availability: v5
- Mode: Application
-`itxn_field` fails if A is of the wrong type for F, including a byte array of the wrong size for use as an address when F is an address field. `itxn_field` also fails if A is an account, asset, or app that is not _available_. (Addresses set into asset params of acfg transactions need not be _available_.)
+`itxn_field` fails if A is of the wrong type for F, including a byte array of the wrong size for use as an address when F is an address field. `itxn_field` also fails if A is an account, asset, or app that is not _available_, or an attempt is made extend an array field beyond the limit imposed by consensus parameters. (Addresses set into asset params of acfg transactions need not be _available_.)
## itxn_submit
diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go
index 4b6972198..e33460adb 100644
--- a/data/transactions/logic/doc.go
+++ b/data/transactions/logic/doc.go
@@ -151,7 +151,7 @@ var opDocByName = map[string]string{
"asset_params_get": "X is field F from asset A. Y is 1 if A exists, else 0",
"app_params_get": "X is field F from app A. Y is 1 if A exists, else 0",
"acct_params_get": "X is field F from account A. Y is 1 if A owns positive algos, else 0",
- "assert": "immediately fail unless X is a non-zero number",
+ "assert": "immediately fail unless A is a non-zero number",
"callsub": "branch unconditionally to TARGET, saving the next instruction on the call stack",
"retsub": "pop the top instruction from the call stack and branch to it",
@@ -308,7 +308,7 @@ var opDocExtras = map[string]string{
"log": "`log` fails if called more than MaxLogCalls times in a program, or if the sum of logged bytes exceeds 1024 bytes.",
"itxn_begin": "`itxn_begin` initializes Sender to the application address; Fee to the minimum allowable, taking into account MinTxnFee and credit from overpaying in earlier transactions; FirstValid/LastValid to the values in the invoking transaction, and all other fields to zero or empty values.",
"itxn_next": "`itxn_next` initializes the transaction exactly as `itxn_begin` does",
- "itxn_field": "`itxn_field` fails if A is of the wrong type for F, including a byte array of the wrong size for use as an address when F is an address field. `itxn_field` also fails if A is an account, asset, or app that is not _available_. (Addresses set into asset params of acfg transactions need not be _available_.)",
+ "itxn_field": "`itxn_field` fails if A is of the wrong type for F, including a byte array of the wrong size for use as an address when F is an address field. `itxn_field` also fails if A is an account, asset, or app that is not _available_, or an attempt is made extend an array field beyond the limit imposed by consensus parameters. (Addresses set into asset params of acfg transactions need not be _available_.)",
"itxn_submit": "`itxn_submit` resets the current transaction so that it can not be resubmitted. A new `itxn_begin` is required to prepare another inner transaction.",
"base64_decode": "Decodes A using the base64 encoding E. Specify the encoding with an immediate arg either as URL and Filename Safe (`URLEncoding`) or Standard (`StdEncoding`). See <a href=\"https://rfc-editor.org/rfc/rfc4648.html#section-4\">RFC 4648</a> (sections 4 and 5). It is assumed that the encoding ends with the exact number of `=` padding characters as required by the RFC. When padding occurs, any unused pad bits in the encoding must be set to zero or the decoding will fail. The special cases of `\\n` and `\\r` are allowed but completely ignored. An error will result when attempting to decode a string with a character that is not in the encoding alphabet or not one of `=`, `\\r`, or `\\n`.",
}
@@ -463,10 +463,10 @@ var txnFieldDocs = map[string]string{
"FreezeAssetAccount": "32 byte address of the account whose asset slot is being frozen or un-frozen",
"FreezeAssetFrozen": "The new frozen value, 0 or 1",
- "Logs": "Log messages emitted by an application call (`itxn` only until v6)",
- "NumLogs": "Number of Logs (`itxn` only until v6)",
- "CreatedAssetID": "Asset ID allocated by the creation of an ASA (`itxn` only until v6)",
- "CreatedApplicationID": "ApplicationID allocated by the creation of an application (`itxn` only until v6)",
+ "Logs": "Log messages emitted by an application call (only with `itxn` in v5)",
+ "NumLogs": "Number of Logs (only with `itxn` in v5)",
+ "CreatedAssetID": "Asset ID allocated by the creation of an ASA (only with `itxn` in v5)",
+ "CreatedApplicationID": "ApplicationID allocated by the creation of an application (only with `itxn` in v5)",
}
var globalFieldDocs = map[string]string{
diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go
index e3d39535d..26d234855 100644
--- a/data/transactions/logic/eval.go
+++ b/data/transactions/logic/eval.go
@@ -60,6 +60,12 @@ const MaxLogSize = 1024
// MaxLogCalls is the limit of total log calls during a program execution
const MaxLogCalls = 32
+// maxAppCallDepth is the limit on inner appl call depth
+// To be clear, 0 would prevent inner appls, 1 would mean inner app calls cannot
+// make inner appls. So the total app depth can be 1 higher than this number, if
+// you count the top-level app call.
+const maxAppCallDepth = 8
+
// stackValue is the type for the operand stack.
// Each stackValue is either a valid []byte value or a uint64 value.
// If (.Bytes != nil) the stackValue is a []byte value, otherwise uint64 value.
@@ -337,13 +343,12 @@ func NewInnerEvalParams(txg []transactions.SignedTxn, caller *EvalContext) *Eval
txgroup := transactions.WrapSignedTxnsWithAD(txg)
minTealVersion := ComputeMinTealVersion(txgroup, true)
- // Can't happen now, since innerAppsEnabledVersion > than any minimum
- // imposed otherwise. But is correct to check.
+ // Can't happen currently, since innerAppsEnabledVersion > than any minimum
+ // imposed otherwise. But is correct to check, in case of future restriction.
if minTealVersion < *caller.MinTealVersion {
minTealVersion = *caller.MinTealVersion
}
- credit, _ := transactions.FeeCredit(txgroup, caller.Proto.MinTxnFee)
- *caller.FeeCredit = basics.AddSaturate(*caller.FeeCredit, credit)
+ // Unlike NewEvalParams, do not add credit here. opTxSubmit has already done so.
if caller.Proto.EnableAppCostPooling {
for _, tx := range txgroup {
@@ -557,10 +562,6 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam
if cx.PooledApplicationBudget != nil {
*cx.PooledApplicationBudget = basics.SubSaturate(*cx.PooledApplicationBudget, uint64(cx.cost))
}
- // update allowed inner transactions (shouldn't overflow, but it's an int, so safe anyway)
- if cx.pooledAllowedInners != nil {
- *cx.pooledAllowedInners -= len(cx.Txn.EvalDelta.InnerTxns)
- }
// update side effects
cx.pastScratch[cx.GroupIndex] = &scratchSpace{}
*cx.pastScratch[cx.GroupIndex] = cx.scratch
@@ -805,11 +806,15 @@ func (cx *EvalContext) budget() int {
return cx.Proto.MaxAppProgramCost
}
-func (cx *EvalContext) allowedInners() int {
+func (cx *EvalContext) remainingInners() int {
if cx.Proto.EnableInnerTransactionPooling && cx.pooledAllowedInners != nil {
return *cx.pooledAllowedInners
}
- return cx.Proto.MaxInnerTransactions
+ // Before EnableInnerTransactionPooling, MaxInnerTransactions was the amount
+ // allowed in a single txn. No consensus version should enable inner app
+ // calls without turning on EnableInnerTransactionPoolin, else inner calls
+ // could keep branching with "width" MaxInnerTransactions
+ return cx.Proto.MaxInnerTransactions - len(cx.Txn.EvalDelta.InnerTxns)
}
func (cx *EvalContext) step() {
@@ -1863,7 +1868,7 @@ func opDup2(cx *EvalContext) {
}
func opDig(cx *EvalContext) {
- depth := int(uint(cx.program[cx.pc+1]))
+ depth := int(cx.program[cx.pc+1])
idx := len(cx.stack) - 1 - depth
// Need to check stack size explicitly here because checkArgs() doesn't understand dig
// so we can't expect our stack to be prechecked.
@@ -2326,7 +2331,7 @@ func opGtxna(cx *EvalContext) {
func opGtxnas(cx *EvalContext) {
last := len(cx.stack) - 1
- gi := int(cx.program[cx.pc+1])
+ gi := cx.program[cx.pc+1]
if int(gi) >= len(cx.TxnGroup) {
cx.err = fmt.Errorf("gtxnas lookup TxnGroup[%d] but it only has %d", gi, len(cx.TxnGroup))
return
@@ -2338,7 +2343,7 @@ func opGtxnas(cx *EvalContext) {
}
arrayFieldIdx := cx.stack[last].Uint
tx := &cx.TxnGroup[gi]
- sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, gi, false)
+ sv, err := cx.txnFieldToStack(tx, fs, arrayFieldIdx, int(gi), false)
if err != nil {
cx.err = err
return
@@ -2503,7 +2508,7 @@ func opGitxn(cx *EvalContext) {
func opGitxna(cx *EvalContext) {
lastInnerGroup := cx.getLastInnerGroup()
- gi := int(uint(cx.program[cx.pc+1]))
+ gi := int(cx.program[cx.pc+1])
if gi >= len(lastInnerGroup) {
cx.err = fmt.Errorf("gitxna %d ... but last group has %d", gi, len(lastInnerGroup))
return
@@ -2524,24 +2529,24 @@ func opGitxna(cx *EvalContext) {
cx.stack = append(cx.stack, sv)
}
-func opGaidImpl(cx *EvalContext, groupIdx int, opName string) (sv stackValue, err error) {
- if groupIdx >= len(cx.TxnGroup) {
- err = fmt.Errorf("%s lookup TxnGroup[%d] but it only has %d", opName, groupIdx, len(cx.TxnGroup))
+func opGaidImpl(cx *EvalContext, gi int, opName string) (sv stackValue, err error) {
+ if gi >= len(cx.TxnGroup) {
+ err = fmt.Errorf("%s lookup TxnGroup[%d] but it only has %d", opName, gi, len(cx.TxnGroup))
return
- } else if groupIdx > cx.GroupIndex {
- err = fmt.Errorf("%s can't get creatable ID of txn ahead of the current one (index %d) in the transaction group", opName, groupIdx)
+ } else if gi > cx.GroupIndex {
+ err = fmt.Errorf("%s can't get creatable ID of txn ahead of the current one (index %d) in the transaction group", opName, gi)
return
- } else if groupIdx == cx.GroupIndex {
+ } else if gi == cx.GroupIndex {
err = fmt.Errorf("%s is only for accessing creatable IDs of previous txns, use `global CurrentApplicationID` instead to access the current app's creatable ID", opName)
return
- } else if txn := cx.TxnGroup[groupIdx].Txn; !(txn.Type == protocol.ApplicationCallTx || txn.Type == protocol.AssetConfigTx) {
- err = fmt.Errorf("can't use %s on txn that is not an app call nor an asset config txn with index %d", opName, groupIdx)
+ } else if txn := cx.TxnGroup[gi].Txn; !(txn.Type == protocol.ApplicationCallTx || txn.Type == protocol.AssetConfigTx) {
+ err = fmt.Errorf("can't use %s on txn that is not an app call nor an asset config txn with index %d", opName, gi)
return
}
- cid, err := cx.getCreatableID(groupIdx)
+ cid, err := cx.getCreatableID(gi)
if cid == 0 {
- err = fmt.Errorf("%s can't read creatable ID from txn with group index %d because the txn did not create anything", opName, groupIdx)
+ err = fmt.Errorf("%s can't read creatable ID from txn with group index %d because the txn did not create anything", opName, gi)
return
}
@@ -2552,8 +2557,8 @@ func opGaidImpl(cx *EvalContext, groupIdx int, opName string) (sv stackValue, er
}
func opGaid(cx *EvalContext) {
- groupIdx := int(cx.program[cx.pc+1])
- sv, err := opGaidImpl(cx, groupIdx, "gaid")
+ gi := int(cx.program[cx.pc+1])
+ sv, err := opGaidImpl(cx, gi, "gaid")
if err != nil {
cx.err = err
return
@@ -2944,31 +2949,31 @@ func opStores(cx *EvalContext) {
cx.stack = cx.stack[:prev]
}
-func opGloadImpl(cx *EvalContext, groupIdx int, scratchIdx byte, opName string) (stackValue, error) {
+func opGloadImpl(cx *EvalContext, gi int, scratchIdx byte, opName string) (stackValue, error) {
var none stackValue
- if groupIdx >= len(cx.TxnGroup) {
- return none, fmt.Errorf("%s lookup TxnGroup[%d] but it only has %d", opName, groupIdx, len(cx.TxnGroup))
+ if gi >= len(cx.TxnGroup) {
+ return none, fmt.Errorf("%s lookup TxnGroup[%d] but it only has %d", opName, gi, len(cx.TxnGroup))
}
if int(scratchIdx) >= len(cx.scratch) {
return none, fmt.Errorf("invalid Scratch index %d", scratchIdx)
}
- if cx.TxnGroup[groupIdx].Txn.Type != protocol.ApplicationCallTx {
- return none, fmt.Errorf("can't use %s on non-app call txn with index %d", opName, groupIdx)
+ if cx.TxnGroup[gi].Txn.Type != protocol.ApplicationCallTx {
+ return none, fmt.Errorf("can't use %s on non-app call txn with index %d", opName, gi)
}
- if groupIdx == cx.GroupIndex {
+ if gi == cx.GroupIndex {
return none, fmt.Errorf("can't use %s on self, use load instead", opName)
}
- if groupIdx > cx.GroupIndex {
- return none, fmt.Errorf("%s can't get future scratch space from txn with index %d", opName, groupIdx)
+ if gi > cx.GroupIndex {
+ return none, fmt.Errorf("%s can't get future scratch space from txn with index %d", opName, gi)
}
- return cx.pastScratch[groupIdx][scratchIdx], nil
+ return cx.pastScratch[gi][scratchIdx], nil
}
func opGload(cx *EvalContext) {
- groupIdx := int(cx.program[cx.pc+1])
+ gi := int(cx.program[cx.pc+1])
scratchIdx := cx.program[cx.pc+2]
- scratchValue, err := opGloadImpl(cx, groupIdx, scratchIdx, "gload")
+ scratchValue, err := opGloadImpl(cx, gi, scratchIdx, "gload")
if err != nil {
cx.err = err
return
@@ -3650,7 +3655,7 @@ func opAppGlobalDel(cx *EvalContext) {
// We have a difficult naming problem here. In some opcodes, TEAL
// allows (and used to require) ASAs and Apps to to be referenced by
-// their "index" in an app call txn's foeign-apps or foreign-assets
+// their "index" in an app call txn's foreign-apps or foreign-assets
// arrays. That was a small integer, no more than 2 or so, and was
// often called an "index". But it was not a basics.AssetIndex or
// basics.ApplicationIndex.
@@ -3686,7 +3691,7 @@ func appReference(cx *EvalContext, ref uint64, foreign bool) (basics.AppIndex, e
}
} else {
// Old rules
- if ref == 0 {
+ if ref == 0 { // Even back when expected to be a real ID, ref = 0 was current app
return cx.appID, nil
}
if foreign {
@@ -3957,8 +3962,8 @@ func addInnerTxn(cx *EvalContext) error {
// unbounded.) The MaxTxGroupSize check can be, and is, precise. (That is,
// if we are at max group size, we can panic now, since we are trying to add
// too many)
- if len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns) > cx.allowedInners() || len(cx.subtxns) >= cx.Proto.MaxTxGroupSize {
- return fmt.Errorf("too many inner transactions %d", len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns))
+ if len(cx.subtxns) > cx.remainingInners() || len(cx.subtxns) >= cx.Proto.MaxTxGroupSize {
+ return fmt.Errorf("too many inner transactions %d with %d left", len(cx.subtxns), cx.remainingInners())
}
stxn := transactions.SignedTxn{}
@@ -4336,8 +4341,8 @@ func opTxSubmit(cx *EvalContext) {
// Should rarely trigger, since itxn_next checks these too. (but that check
// must be imperfect, see its comment) In contrast to that check, subtxns is
// already populated here.
- if len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns) > cx.allowedInners() || len(cx.subtxns) > cx.Proto.MaxTxGroupSize {
- cx.err = fmt.Errorf("too many inner transactions %d", len(cx.Txn.EvalDelta.InnerTxns)+len(cx.subtxns))
+ if len(cx.subtxns) > cx.remainingInners() || len(cx.subtxns) > cx.Proto.MaxTxGroupSize {
+ cx.err = fmt.Errorf("too many inner transactions %d with %d left", len(cx.subtxns), cx.remainingInners())
return
}
@@ -4393,17 +4398,23 @@ func opTxSubmit(cx *EvalContext) {
return
}
- // Disallow re-entrancy
+ // Disallow reentrancy and limit inner app call depth
if cx.subtxns[itx].Txn.Type == protocol.ApplicationCallTx {
if cx.appID == cx.subtxns[itx].Txn.ApplicationID {
cx.err = fmt.Errorf("attempt to self-call")
return
}
+ depth := 0
for parent := cx.caller; parent != nil; parent = parent.caller {
if parent.appID == cx.subtxns[itx].Txn.ApplicationID {
cx.err = fmt.Errorf("attempt to re-enter %d", parent.appID)
return
}
+ depth++
+ }
+ if depth >= maxAppCallDepth {
+ cx.err = fmt.Errorf("appl depth (%d) exceeded", depth)
+ return
}
}
@@ -4421,6 +4432,12 @@ func opTxSubmit(cx *EvalContext) {
}
}
+ // Decrement allowed inners *before* execution, else runaway recursion is
+ // not noticed.
+ if cx.pooledAllowedInners != nil {
+ *cx.pooledAllowedInners -= len(cx.subtxns)
+ }
+
ep := NewInnerEvalParams(cx.subtxns, cx)
for i := range ep.TxnGroup {
err := cx.Ledger.Perform(i, ep)
diff --git a/data/transactions/logic/evalAppTxn_test.go b/data/transactions/logic/evalAppTxn_test.go
index 786b60bf8..39058d0bc 100644
--- a/data/transactions/logic/evalAppTxn_test.go
+++ b/data/transactions/logic/evalAppTxn_test.go
@@ -622,7 +622,7 @@ txn Sender; itxn_field Receiver;
pay+
"itxn_submit; itxn Fee; int 1999; ==", ep)
- // Same first, but force the second too low
+ // Same as first, but force the second too low
TestApp(t, "itxn_begin"+
pay+
"int 3; itxn_field Fee;"+
@@ -647,6 +647,25 @@ txn Sender; itxn_field Receiver;
pay+
"int 1; itxn_field Fee;"+
"itxn_submit; itxn Fee; int 1", ep, "fee too small")
+
+ // Test that overpay in first inner group is available in second inner group
+ // also ensure only exactly the _right_ amount of credit is available.
+ TestApp(t, "itxn_begin"+
+ pay+
+ "int 2002; itxn_field Fee;"+ // double pay
+ "itxn_next"+
+ pay+
+ "int 1001; itxn_field Fee;"+ // regular pay
+ "itxn_submit;"+
+ // At beginning of second group, we should have 1 minfee of credit
+ "itxn_begin"+
+ pay+
+ "int 0; itxn_field Fee;"+ // free, due to credit
+ "itxn_next"+
+ pay+
+ "itxn_submit; itxn Fee; int 1001; ==", // second one should have to pay
+ ep)
+
}
// TestApplCreation is only determining what appl transactions can be
@@ -1603,3 +1622,98 @@ int 1
`, ep, "assert failed")
}
+
+// TestInnerCallDepth ensures that inner calls are limited in depth
+func TestInnerCallDepth(t *testing.T) {
+ t.Parallel()
+
+ ep, tx, ledger := MakeSampleEnv()
+ // Allow a lot to make the test viable
+ ep.Proto.MaxAppTxnForeignApps = 50
+ ep.Proto.MaxAppTotalTxnReferences = 50
+
+ var apps []basics.AppIndex
+ // 200 will be a simple app that always approves
+ yes := TestProg(t, `int 1`, AssemblerMaxVersion)
+ ledger.NewApp(tx.Receiver, 200, basics.AppParams{
+ ApprovalProgram: yes.Program,
+ })
+ apps = append(apps, basics.AppIndex(200))
+
+ // 201-210 will be apps that call the next lower one.
+ for i := 0; i < 10; i++ {
+ source := main(`
+ global CurrentApplicationID
+ itob
+ log
+ itxn_begin
+ int appl; itxn_field TypeEnum
+ txn NumApplications
+loop:
+ dup
+ bz done
+ dup
+ txnas Applications
+ itxn_field Applications
+ int 1
+ -
+ b loop
+
+done:
+ pop
+ ` + fmt.Sprintf("int %d", 200+i) + `; itxn_field ApplicationID
+ itxn_submit
+`)
+ idx := basics.AppIndex(200 + i + 1)
+ ledger.NewApp(tx.Receiver, idx, basics.AppParams{
+ ApprovalProgram: TestProg(t, source, AssemblerMaxVersion).Program,
+ })
+ ledger.NewAccount(appAddr(int(idx)), 10_000)
+ apps = append(apps, idx)
+ }
+ tx.ForeignApps = apps
+ ledger.NewAccount(appAddr(888), 100_000)
+
+ app, _, err := ledger.AppParams(202)
+ require.NoError(t, err)
+ TestAppBytes(t, app.ApprovalProgram, ep)
+
+ app, _, err = ledger.AppParams(208)
+ require.NoError(t, err)
+ TestAppBytes(t, app.ApprovalProgram, ep)
+
+ app, _, err = ledger.AppParams(209)
+ require.NoError(t, err)
+ TestAppBytes(t, app.ApprovalProgram, ep, "appl depth")
+}
+
+func TestInfiniteRecursion(t *testing.T) {
+ ep, tx, ledger := MakeSampleEnv()
+ source := `
+itxn_begin
+int appl; itxn_field TypeEnum
+int 0; app_params_get AppApprovalProgram
+assert
+itxn_field ApprovalProgram
+
+int 0; app_params_get AppClearStateProgram
+assert
+itxn_field ClearStateProgram
+
+itxn_submit
+`
+ // This app looks itself up in the ledger, so we need to put it in there.
+ ledger.NewApp(tx.Sender, 888, basics.AppParams{
+ ApprovalProgram: TestProg(t, source, AssemblerMaxVersion).Program,
+ ClearStateProgram: TestProg(t, "int 1", AssemblerMaxVersion).Program,
+ })
+ // We're testing if this can recur forever. It's hard to fund all these
+ // apps, but we can put a huge credit in the ep.
+ *ep.FeeCredit = 1_000_000_000
+
+ // This has been tested by hand, by setting maxAppCallDepth to 10_000_000
+ // but without that, the depth limiter stops it first.
+ // TestApp(t, source, ep, "too many inner transactions 1 with 0 left")
+
+ TestApp(t, source, ep, "appl depth (8) exceeded")
+}
diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go
index 91dff7192..447b18b67 100644
--- a/data/transactions/logic/evalStateful_test.go
+++ b/data/transactions/logic/evalStateful_test.go
@@ -496,8 +496,13 @@ func TestAppCheckOptedIn(t *testing.T) {
// Receiver is not opted in
testApp(t, "int 1; int 100; app_opted_in; int 0; ==", now)
- //testApp(t, "int 1; int 3; app_opted_in; int 0; ==", now)
- //testApp(t, "int 1; int 3; app_opted_in; int 0; ==", pre) // not an indirect reference though: app 3
+ testApp(t, "int 1; int 0; app_opted_in; int 0; ==", now)
+ // These two give the same result, for different reasons
+ testApp(t, "int 1; int 3; app_opted_in; int 0; ==", now) // refers to tx.ForeignApps[2], which is 111
+ testApp(t, "int 1; int 3; app_opted_in; int 0; ==", pre) // not an indirect reference: actually app 3
+ // 0 is a legal way to refer to the current app, even in pre (though not in spec)
+ // but current app is 888 - not opted in
+ testApp(t, "int 1; int 0; app_opted_in; int 0; ==", pre)
// Sender is not opted in
testApp(t, "int 0; int 100; app_opted_in; int 0; ==", now)
@@ -505,12 +510,19 @@ func TestAppCheckOptedIn(t *testing.T) {
// Receiver opted in
ledger.NewLocals(txn.Txn.Receiver, 100)
testApp(t, "int 1; int 100; app_opted_in; int 1; ==", now)
- testApp(t, "int 1; int 2; app_opted_in; int 1; ==", now)
+ testApp(t, "int 1; int 2; app_opted_in; int 1; ==", now) // tx.ForeignApps[1] == 100
testApp(t, "int 1; int 2; app_opted_in; int 0; ==", pre) // in pre, int 2 is an actual app id
testApp(t, "byte \"aoeuiaoeuiaoeuiaoeuiaoeuiaoeui01\"; int 2; app_opted_in; int 1; ==", now)
testProg(t, "byte \"aoeuiaoeuiaoeuiaoeuiaoeuiaoeui01\"; int 2; app_opted_in; int 1; ==", directRefEnabledVersion-1,
Expect{3, "app_opted_in arg 0 wanted type uint64..."})
+ // Receiver opts into 888, the current app in testApp
+ ledger.NewLocals(txn.Txn.Receiver, 888)
+ // int 0 is current app (888) even in pre
+ testApp(t, "int 1; int 0; app_opted_in; int 1; ==", pre)
+ // Here it is "obviously" allowed, because indexes became legal
+ testApp(t, "int 1; int 0; app_opted_in; int 1; ==", now)
+
// Sender opted in
ledger.NewLocals(txn.Txn.Sender, 100)
testApp(t, "int 0; int 100; app_opted_in; int 1; ==", now)
@@ -552,11 +564,11 @@ err
exit:
int 1`
- testApp(t, text, now, "no app for account")
+ testApp(t, text, now, "is not opted into")
// Make a different app (not 100)
ledger.NewApp(now.TxnGroup[0].Txn.Receiver, 9999, basics.AppParams{})
- testApp(t, text, now, "no app for account")
+ testApp(t, text, now, "is not opted into")
// create the app and check the value from ApplicationArgs[0] (protocol.PaymentTx) does not exist
ledger.NewApp(now.TxnGroup[0].Txn.Receiver, 100, basics.AppParams{})
@@ -580,15 +592,28 @@ byte 0x414c474f
Expect{4, "app_local_get_ex arg 0 wanted type uint64..."})
testApp(t, strings.Replace(text, "int 100 // app id", "int 2", -1), now)
// Next we're testing if the use of the current app's id works
- // as a direct reference. The error is because the sender
+ // as a direct reference. The error is because the receiver
// account is not opted into 123.
now.TxnGroup[0].Txn.ApplicationID = 123
- testApp(t, strings.Replace(text, "int 100 // app id", "int 123", -1), now, "no app for account")
- testApp(t, strings.Replace(text, "int 100 // app id", "int 2", -1), pre, "no app for account")
+ testApp(t, strings.Replace(text, "int 100 // app id", "int 123", -1), now, "is not opted into")
+ testApp(t, strings.Replace(text, "int 100 // app id", "int 2", -1), pre, "is not opted into")
testApp(t, strings.Replace(text, "int 100 // app id", "int 9", -1), now, "invalid App reference 9")
testApp(t, strings.Replace(text, "int 1 // account idx", "byte \"aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00\"", -1), now,
"no such address")
+ // opt into 123, and try again
+ ledger.NewApp(now.TxnGroup[0].Txn.Receiver, 123, basics.AppParams{})
+ ledger.NewLocals(now.TxnGroup[0].Txn.Receiver, 123)
+ ledger.NewLocal(now.TxnGroup[0].Txn.Receiver, 123, string(protocol.PaymentTx), basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"})
+ testApp(t, strings.Replace(text, "int 100 // app id", "int 123", -1), now)
+ testApp(t, strings.Replace(text, "int 100 // app id", "int 0", -1), now)
+
+ // Somewhat surprising, but in `pre` when the app argument was expected to be
+ // an actual app id (not an index in foreign apps), 0 was *still* treated
+ // like current app.
+ pre.TxnGroup[0].Txn.ApplicationID = 123
+ testApp(t, strings.Replace(text, "int 100 // app id", "int 0", -1), pre)
+
// check special case account idx == 0 => sender
ledger.NewApp(now.TxnGroup[0].Txn.Sender, 100, basics.AppParams{})
ledger.NewLocals(now.TxnGroup[0].Txn.Sender, 100)
@@ -1180,7 +1205,7 @@ intc_1
ops.Program[firstCmdOffset] = saved
_, err = EvalApp(ops.Program, 0, 100, ep)
require.Error(t, err)
- require.Contains(t, err.Error(), "no app for account")
+ require.Contains(t, err.Error(), "is not opted into")
ledger.NewApp(txn.Txn.Sender, 100, basics.AppParams{})
ledger.NewLocals(txn.Txn.Sender, 100)
diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go
index 3aea31b1b..df367c27d 100644
--- a/data/transactions/logic/eval_test.go
+++ b/data/transactions/logic/eval_test.go
@@ -116,11 +116,13 @@ func benchmarkEvalParams(txn *transactions.SignedTxn) *EvalParams {
}
func defaultEvalParamsWithVersion(txn *transactions.SignedTxn, version uint64) *EvalParams {
+ var zero uint64
ep := &EvalParams{
- Proto: makeTestProtoV(version),
- TxnGroup: make([]transactions.SignedTxnWithAD, 1),
- Specials: &transactions.SpecialAddresses{},
- Trace: &strings.Builder{},
+ Proto: makeTestProtoV(version),
+ TxnGroup: make([]transactions.SignedTxnWithAD, 1),
+ Specials: &transactions.SpecialAddresses{},
+ Trace: &strings.Builder{},
+ FeeCredit: &zero,
}
if txn != nil {
ep.TxnGroup[0].SignedTxn = *txn
diff --git a/data/transactions/logic/ledger_test.go b/data/transactions/logic/ledger_test.go
index 54929b3b8..26b959bcc 100644
--- a/data/transactions/logic/ledger_test.go
+++ b/data/transactions/logic/ledger_test.go
@@ -340,7 +340,7 @@ func (l *Ledger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key strin
}
tkvd, ok := br.locals[appIdx]
if !ok {
- return basics.TealValue{}, false, fmt.Errorf("no app for account")
+ return basics.TealValue{}, false, fmt.Errorf("account %s is not opted into %d", addr, appIdx)
}
// check deltas first
@@ -366,7 +366,7 @@ func (l *Ledger) SetLocal(addr basics.Address, appIdx basics.AppIndex, key strin
}
tkv, ok := br.locals[appIdx]
if !ok {
- return fmt.Errorf("no app for account")
+ return fmt.Errorf("account %s is not opted into %d", addr, appIdx)
}
// if writing the same value, return
@@ -394,7 +394,7 @@ func (l *Ledger) DelLocal(addr basics.Address, appIdx basics.AppIndex, key strin
}
tkv, ok := br.locals[appIdx]
if !ok {
- return fmt.Errorf("no app for account")
+ return fmt.Errorf("account %s is not opted into %d", addr, appIdx)
}
exist := false
if _, ok := tkv[key]; ok {
diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go
index 80b73b189..19855ba9d 100644
--- a/data/transactions/logic/opcodes.go
+++ b/data/transactions/logic/opcodes.go
@@ -286,7 +286,7 @@ var OpSpecs = []OpSpec{
{0x70, "asset_holding_get", opAssetHoldingGet, assembleAssetHolding, disAssetHolding, oneAny.plus(oneInt), oneAny.plus(oneInt), directRefEnabledVersion, runModeApplication, immediates("f")},
{0x71, "asset_params_get", opAssetParamsGet, assembleAssetParams, disAssetParams, oneInt, oneAny.plus(oneInt), 2, runModeApplication, immediates("f")},
{0x72, "app_params_get", opAppParamsGet, assembleAppParams, disAppParams, oneInt, oneAny.plus(oneInt), 5, runModeApplication, immediates("f")},
- {0x73, "acct_params_get", opAcctParamsGet, assembleAcctParams, disAcctParams, oneInt, oneAny.plus(oneInt), 6, runModeApplication, immediates("f")},
+ {0x73, "acct_params_get", opAcctParamsGet, assembleAcctParams, disAcctParams, oneAny, oneAny.plus(oneInt), 6, runModeApplication, immediates("f")},
{0x78, "min_balance", opMinBalance, asmDefault, disDefault, oneInt, oneInt, 3, runModeApplication, opDefault},
{0x78, "min_balance", opMinBalance, asmDefault, disDefault, oneAny, oneInt, directRefEnabledVersion, runModeApplication, opDefault},
diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go
index 009de07b5..c64d154e3 100644
--- a/data/transactions/msgp_gen.go
+++ b/data/transactions/msgp_gen.go
@@ -1845,8 +1845,8 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "struct-from-array", "InnerTxns")
return
}
- if zb0011 > config.MaxInnerTransactions {
- err = msgp.ErrOverflow(uint64(zb0011), uint64(config.MaxInnerTransactions))
+ if zb0011 > config.MaxInnerTransactionsPerDelta {
+ err = msgp.ErrOverflow(uint64(zb0011), uint64(config.MaxInnerTransactionsPerDelta))
err = msgp.WrapError(err, "struct-from-array", "InnerTxns")
return
}
@@ -1963,8 +1963,8 @@ func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "InnerTxns")
return
}
- if zb0017 > config.MaxInnerTransactions {
- err = msgp.ErrOverflow(uint64(zb0017), uint64(config.MaxInnerTransactions))
+ if zb0017 > config.MaxInnerTransactionsPerDelta {
+ err = msgp.ErrOverflow(uint64(zb0017), uint64(config.MaxInnerTransactionsPerDelta))
err = msgp.WrapError(err, "InnerTxns")
return
}
diff --git a/data/transactions/teal.go b/data/transactions/teal.go
index 3b7e78d58..3851a03e2 100644
--- a/data/transactions/teal.go
+++ b/data/transactions/teal.go
@@ -37,7 +37,7 @@ type EvalDelta struct {
Logs []string `codec:"lg,allocbound=config.MaxLogCalls"`
- InnerTxns []SignedTxnWithAD `codec:"itx,allocbound=config.MaxInnerTransactions"`
+ InnerTxns []SignedTxnWithAD `codec:"itx,allocbound=config.MaxInnerTransactionsPerDelta"`
}
// Equal compares two EvalDeltas and returns whether or not they are