diff --git a/cmd/opdoc/opdoc.go b/cmd/opdoc/opdoc.go index 60cb1a13da..3c1be04ce5 100644 --- a/cmd/opdoc/opdoc.go +++ b/cmd/opdoc/opdoc.go @@ -32,9 +32,13 @@ func opGroupMarkdownTable(og *logic.OpGroup, out io.Writer) { fmt.Fprint(out, `| Op | Description | | --- | --- | `) + opSpecs := logic.OpsByName[logic.LogicVersion] // TODO: sort by logic.OpSpecs[].Opcode for _, opname := range og.Ops { - fmt.Fprintf(out, "| `%s` | %s |\n", markdownTableEscape(opname), markdownTableEscape(logic.OpDoc(opname))) + spec := opSpecs[opname] + fmt.Fprintf(out, "| `%s%s` | %s |\n", + markdownTableEscape(spec.Name), immediateMarkdown(&spec), + markdownTableEscape(logic.OpDoc(opname))) } } @@ -82,7 +86,7 @@ func fieldTableMarkdown(out io.Writer, names []string, types []logic.StackType, } func transactionFieldsMarkdown(out io.Writer) { - fmt.Fprintf(out, "\n`txn` Fields:\n\n") + fmt.Fprintf(out, "\n`txn` Fields (see [transaction reference](https://developer.algorand.org/docs/reference/transactions/)):\n\n") fieldTableMarkdown(out, logic.TxnFieldNames, logic.TxnFieldTypes, logic.TxnFieldDocs()) } @@ -101,6 +105,14 @@ func assetParamsFieldsMarkdown(out io.Writer) { fieldTableMarkdown(out, logic.AssetParamsFieldNames, logic.AssetParamsFieldTypes, logic.AssetParamsFieldDocs) } +func immediateMarkdown(op *logic.OpSpec) string { + markdown := "" + for _, imm := range op.Details.Immediates { + markdown = markdown + " " + imm.Name + } + return markdown +} + func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) { ws := "" opextra := logic.OpImmediateNote(op.Name) @@ -108,7 +120,7 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) { ws = " " } costs := logic.OpAllCosts(op.Name) - fmt.Fprintf(out, "\n## %s\n\n- Opcode: 0x%02x%s%s\n", op.Name, op.Opcode, ws, opextra) + fmt.Fprintf(out, "\n## %s%s\n\n- Opcode: 0x%02x%s%s\n", op.Name, immediateMarkdown(op), op.Opcode, ws, opextra) if op.Args == nil { fmt.Fprintf(out, "- Pops: _None_\n") } else if len(op.Args) == 1 { @@ -277,8 +289,8 @@ func buildLanguageSpec(opGroups map[string][]string) *LanguageSpec { records[i].Name = spec.Name records[i].Args = typeString(spec.Args) records[i].Returns = typeString(spec.Returns) - records[i].Cost = logic.OpCost(spec.Name) - records[i].Size = logic.OpSize(spec.Name) + records[i].Cost = spec.Details.Cost + records[i].Size = spec.Details.Size records[i].ArgEnum = argEnum(spec.Name) records[i].ArgEnumTypes = argEnumTypes(spec.Name) records[i].Doc = logic.OpDoc(spec.Name) diff --git a/cmd/tealdbg/local.go b/cmd/tealdbg/local.go index 9c1a05f9fa..338a25cf5f 100644 --- a/cmd/tealdbg/local.go +++ b/cmd/tealdbg/local.go @@ -337,6 +337,13 @@ func (r *LocalRunner) Setup(dp *DebugParams) (err error) { source := string(data) ops, err := logic.AssembleStringWithVersion(source, r.proto.LogicSigVersion) if err != nil { + errorLines := "" + for _, lineError := range ops.Errors { + errorLines = fmt.Sprintf("%s\n%s", errorLines, lineError.Error()) + } + if errorLines != "" { + return fmt.Errorf("%w:%s", err, errorLines) + } return err } r.runs[i].program = ops.Program diff --git a/cmd/tealdbg/local_test.go b/cmd/tealdbg/local_test.go index 5cb4feddfd..4a93825c54 100644 --- a/cmd/tealdbg/local_test.go +++ b/cmd/tealdbg/local_test.go @@ -460,7 +460,7 @@ int 100 ProgramBlobs: [][]byte{[]byte(source)}, BalanceBlob: balanceBlob, TxnBlob: txnBlob, - Proto: "future", + Proto: string(protocol.ConsensusCurrentVersion), Round: 222, LatestTimestamp: 333, GroupIndex: 0, diff --git a/config/consensus.go b/config/consensus.go index 428a175a71..8c5200e0c5 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -853,12 +853,28 @@ func initConsensusProtocols() { v25.EnableAssetCloseAmount = true Consensus[protocol.ConsensusV25] = v25 - // v24 can be upgraded to v25, with an update delay of 7 days ( see calculation above ) - v24.ApprovedUpgrades[protocol.ConsensusV25] = 140000 + // v26 adds support for teal3 + v26 := v25 + v26.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + + // Enable the InitialRewardsRateCalculation fix + v26.InitialRewardsRateCalculation = true + + // Enable transaction Merkle tree. + v26.PaysetCommit = PaysetCommitMerkle + + // Enable teal3 + v26.LogicSigVersion = 3 + + Consensus[protocol.ConsensusV26] = v26 + + // v25 or v24 can be upgraded to v26, with an update delay of 7 days ( see calculation above ) + v25.ApprovedUpgrades[protocol.ConsensusV26] = 140000 + v24.ApprovedUpgrades[protocol.ConsensusV26] = 140000 // ConsensusFuture is used to test features that are implemented // but not yet released in a production protocol version. - vFuture := v25 + vFuture := v26 vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} // FilterTimeout for period 0 should take a new optimized, configured value, need to revisit this later diff --git a/data/basics/teal.go b/data/basics/teal.go index 2b3afbe04a..38cd3780e1 100644 --- a/data/basics/teal.go +++ b/data/basics/teal.go @@ -285,7 +285,7 @@ func (tk TealKeyValue) Clone() TealKeyValue { } // ToStateSchema calculates the number of each value type in a TealKeyValue and -// reprsents the result as a StateSchema +// represents the result as a StateSchema func (tk TealKeyValue) ToStateSchema() (schema StateSchema, err error) { for _, value := range tk { switch value.Type { diff --git a/data/basics/userBalance.go b/data/basics/userBalance.go index 2882901045..9e76876b00 100644 --- a/data/basics/userBalance.go +++ b/data/basics/userBalance.go @@ -37,7 +37,7 @@ const ( // NotParticipating indicates that the associated account neither participates in the consensus, nor recieves rewards. // Accounts that are marked as NotParticipating cannot change their status, but can receive and send Algos to other accounts. // Two special accounts that are defined as NotParticipating are the incentive pool (also know as rewards pool) and the fee sink. - // These two accounts also have additional Algo transfer restrictions + // These two accounts also have additional Algo transfer restrictions. NotParticipating // MaxEncodedAccountDataSize is a rough estimate for the worst-case scenario we're going to have of the account data and address serialized. diff --git a/data/transactions/logic/Makefile b/data/transactions/logic/Makefile index 62c164dc62..d083f801a8 100644 --- a/data/transactions/logic/Makefile +++ b/data/transactions/logic/Makefile @@ -1,10 +1,33 @@ -all: TEAL_opcodes.md wat.md fields_string.go +all: TEAL_opcodes.md README.md fields_string.go + +# Location of algorandfoundation/specs repo. (Optional) +SPECS := ../../../../specs +# Location of algorand/docs repo. (Optional) +DOCS := ../../../../docs TEAL_opcodes.md: fields_string.go ../../../cmd/opdoc/opdoc.go eval.go assembler.go doc.go opcodes.go go run ../../../cmd/opdoc/opdoc.go ../../../cmd/opdoc/tmLanguage.go + @if [ -e $(SPECS)/dev/TEAL_opcodes.md ]; then \ + sed '/^$$/q' $(SPECS)/dev/TEAL_opcodes.md | cat - TEAL_opcodes.md > opcodes.spec; \ + mv opcodes.spec $(SPECS)/dev/TEAL_opcodes.md; \ + echo "TEAL_opcodes.md updated in specs repo"; \ + fi + @if [ -e $(DOCS)/docs/reference/teal/opcodes.md ]; then \ + sed 's/^# /title: /g' TEAL_opcodes.md > $(DOCS)/docs/reference/teal/opcodes.md; \ + echo "opcodes.md updated in docs repo"; \ + fi fields_string.go: fields.go go generate -wat.md: TEAL_opcodes.md README_in.md +README.md: TEAL_opcodes.md README_in.md python merge.py > README.md + @if [ -e $(SPECS)/dev/TEAL.md ]; then \ + sed '/^$$/q' $(SPECS)/dev/TEAL.md | cat - README.md > teal.spec; \ + mv teal.spec $(SPECS)/dev/TEAL.md; \ + echo "TEAL.md updated in specs repo"; \ + fi + @if [ -e $(DOCS)/docs/reference/teal/specification.md ]; then \ + sed 's/^# /title: /g' README.md > $(DOCS)/docs/reference/teal/specification.md; \ + echo "specification.md updated in docs repo"; \ + fi diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index f515904693..263e58ffbd 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -100,8 +100,6 @@ For one-argument ops, `X` is the last element on the stack, which is typically r For two-argument ops, `A` is the previous element on the stack and `B` is the last element on the stack. These typically result in popping A and B from the stack and pushing the result. -`ed25519verify` is currently the only 3 argument opcode and is described in detail in the opcode refrence. - | Op | Description | | --- | --- | | `sha256` | SHA256 hash of value X, yields [32]byte | @@ -131,9 +129,13 @@ For two-argument ops, `A` is the previous element on the stack and `B` is the la | `~` | bitwise invert value X | | `mulw` | A times B out to 128-bit long result as low (top) and high uint64 values on the stack | | `addw` | A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack | -| `concat` | pop two byte strings A and B and join them, push the result | -| `substring` | pop a byte string X. For immediate values in 0..255 M and N: extract a range of bytes from it starting at M up to but not including N, push the substring result. If N < M, or either is larger than the string length, the program fails | -| `substring3` | pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the string length, the program fails | +| `getbit` | pop a target A (integer or byte-array), and index B. Push the Bth bit of A. | +| `setbit` | pop a target A, index B, and bit C. Set the Bth bit of A to C, and push the result | +| `getbyte` | pop a byte-array A and integer B. Extract the Bth byte of A and push it as an integer | +| `setbyte` | pop a byte-array A, integer B, and small integer C (between 0..255). Set the Bth byte of A to C, and push the result | +| `concat` | pop two byte-arrays A and B and join them, push the result | +| `substring s e` | pop a byte-array A. For immediate values in 0..255 S and E: extract a range of bytes from A starting at S up to but not including E, push the substring result. If E < S, or either is larger than the array length, the program fails | +| `substring3` | pop a byte-array A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the array length, the program fails | ### Loading Values @@ -143,30 +145,34 @@ Some of these have immediate data in the byte or bytes after the opcode. | Op | Description | | --- | --- | -| `intcblock` | load block of uint64 constants | -| `intc` | push value from uint64 constants to stack by index into constants | +| `intcblock uint ...` | prepare block of uint64 constants for use by intc | +| `intc i` | push Ith constant from intcblock to stack | | `intc_0` | push constant 0 from intcblock to stack | | `intc_1` | push constant 1 from intcblock to stack | | `intc_2` | push constant 2 from intcblock to stack | | `intc_3` | push constant 3 from intcblock to stack | -| `bytecblock` | load block of byte-array constants | -| `bytec` | push bytes constant to stack by index into constants | +| `pushint uint` | push immediate UINT to the stack as an integer | +| `bytecblock bytes ...` | prepare block of byte-array constants for use by bytec | +| `bytec i` | push Ith constant from bytecblock to stack | | `bytec_0` | push constant 0 from bytecblock to stack | | `bytec_1` | push constant 1 from bytecblock to stack | | `bytec_2` | push constant 2 from bytecblock to stack | | `bytec_3` | push constant 3 from bytecblock to stack | -| `arg` | push Args[N] value to stack by index | -| `arg_0` | push Args[0] to stack | -| `arg_1` | push Args[1] to stack | -| `arg_2` | push Args[2] to stack | -| `arg_3` | push Args[3] to stack | -| `txn` | push field from current transaction to stack | -| `gtxn` | push field to the stack from a transaction in the current transaction group | -| `txna` | push value of an array field from current transaction to stack | -| `gtxna` | push value of a field to the stack from a transaction in the current transaction group | -| `global` | push value from globals to stack | -| `load` | copy a value from scratch space to the stack | -| `store` | pop a value from the stack and store to scratch space | +| `pushbytes bytes` | push the following program bytes to the stack | +| `arg n` | push Nth LogicSig argument to stack | +| `arg_0` | push LogicSig argument 0 to stack | +| `arg_1` | push LogicSig argument 1 to stack | +| `arg_2` | push LogicSig argument 2 to stack | +| `arg_3` | push LogicSig argument 3 to stack | +| `txn f` | push field F of current transaction to stack | +| `gtxn t f` | push field F of the Tth transaction in the current group | +| `txna f i` | push Ith value of the array field F of the current transaction | +| `gtxna t f i` | push Ith value of the array field F from the Tth transaction in the current group | +| `gtxns f` | push field F of the Ath transaction in the current group | +| `gtxnsa f i` | push Ith value of the array field F from the Ath transaction in the current group | +| `global f` | push value from globals to stack | +| `load i` | copy a value from scratch space to the stack | +| `store i` | pop a value from the stack and store to scratch space | **Transaction Fields** @@ -220,6 +226,14 @@ Some of these have immediate data in the byte or bytes after the opcode. | 45 | FreezeAsset | uint64 | Asset ID being frozen or un-frozen. LogicSigVersion >= 2. | | 46 | FreezeAssetAccount | []byte | 32 byte address of the account whose asset slot is being frozen or un-frozen. LogicSigVersion >= 2. | | 47 | FreezeAssetFrozen | uint64 | The new frozen value, 0 or 1. LogicSigVersion >= 2. | +| 48 | Assets | uint64 | Foreign Assets listed in the ApplicationCall transaction. LogicSigVersion >= 3. | +| 49 | NumAssets | uint64 | Number of Assets. LogicSigVersion >= 3. | +| 50 | Applications | uint64 | Foreign Apps listed in the ApplicationCall transaction. LogicSigVersion >= 3. | +| 51 | NumApplications | uint64 | Number of Applications. LogicSigVersion >= 3. | +| 52 | GlobalNumUint | uint64 | Number of global state integers in ApplicationCall. LogicSigVersion >= 3. | +| 53 | GlobalNumByteSlice | uint64 | Number of global state byteslices in ApplicationCall. LogicSigVersion >= 3. | +| 54 | LocalNumUint | uint64 | Number of local state integers in ApplicationCall. LogicSigVersion >= 3. | +| 55 | LocalNumByteSlice | uint64 | Number of local state byteslices in ApplicationCall. LogicSigVersion >= 3. | Additional details in the [opcodes document](TEAL_opcodes.md#txn) on the `txn` op. @@ -239,6 +253,7 @@ Global fields are fields that are common to all the transactions in the group. I | 6 | Round | uint64 | Current round number. LogicSigVersion >= 2. | | 7 | LatestTimestamp | uint64 | Last confirmed block UNIX timestamp. Fails if negative. LogicSigVersion >= 2. | | 8 | CurrentApplicationID | uint64 | ID of current application executing. Fails if no such application is executing. LogicSigVersion >= 2. | +| 9 | CreatorAddress | []byte | Address of the creator of the current application. Fails if no such application is executing. LogicSigVersion >= 3. | **Asset Fields** @@ -271,30 +286,35 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as | Op | Description | | --- | --- | | `err` | Error. Panic immediately. This is primarily a fencepost against accidental zero bytes getting compiled into programs. | -| `bnz` | branch if value X is not zero | -| `bz` | branch if value X is zero | -| `b` | branch unconditionally to offset | +| `bnz target` | branch to TARGET if value X is not zero | +| `bz target` | branch to TARGET if value X is zero | +| `b target` | branch unconditionally to TARGET | | `return` | use last value on stack as success value; end | | `pop` | discard value X from stack | | `dup` | duplicate last value on stack | | `dup2` | duplicate two last values on stack: A, B -> A, B, A, B | +| `dig n` | push the Nth value from the top of the stack. dig 0 is equivalent to dup | +| `swap` | swaps two last values on stack: A, B -> B, A | +| `select` | selects one of two values based on top-of-stack: A, B, C -> (if C != 0 then B else A) | +| `assert` | immediately fail unless value X is a non-zero number | ### State Access | Op | Description | | --- | --- | -| `balance` | get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender | +| `balance` | get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. The balance is observed after the effects of previous transactions in the group, and after the fee for the current transaction is deducted. | +| `min_balance` | get minimum required balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. Required balance is affected by [ASA](https://developer.algorand.org/docs/features/asa/#assets-overview) and [App](https://developer.algorand.org/docs/features/asc1/stateful/#minimum-balance-requirement-for-a-smart-contract) usage. When creating or opting into an app, the minimum balance grows before the app code runs, therefore the increase is visible there. When deleting or closing out, the minimum balance decreases after the app executes. | | `app_opted_in` | check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1} | | `app_local_get` | read from account specified by Txn.Accounts[A] from local state of the current application key B => value | -| `app_local_get_ex` | read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value} | +| `app_local_get_ex` | read from account specified by Txn.Accounts[A] from local state of the application B key C => [*... stack*, value, 0 or 1] | | `app_global_get` | read key A from global state of a current application => value | -| `app_global_get_ex` | read from application Txn.ForeignApps[A] global state key B => {0 or 1 (top), value}. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app | +| `app_global_get_ex` | read from application Txn.ForeignApps[A] global state key B => [*... stack*, value, 0 or 1]. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app | | `app_local_put` | write to account specified by Txn.Accounts[A] to local state of a current application key B with value C | | `app_global_put` | write key A and value B to global state of the current application | | `app_local_del` | delete from account specified by Txn.Accounts[A] local state key B of the current application | | `app_global_del` | delete key A from a global state of the current application | -| `asset_holding_get` | read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value} | -| `asset_params_get` | read from asset Txn.ForeignAssets[A] params field X (imm arg) => {0 or 1 (top), value} | +| `asset_holding_get i` | read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value} | +| `asset_params_get i` | read from asset Txn.ForeignAssets[A] params field X (imm arg) => {0 or 1 (top), value} | # Assembler Syntax diff --git a/data/transactions/logic/README_in.md b/data/transactions/logic/README_in.md index 64bdd299cb..ef43d52308 100644 --- a/data/transactions/logic/README_in.md +++ b/data/transactions/logic/README_in.md @@ -77,8 +77,6 @@ For one-argument ops, `X` is the last element on the stack, which is typically r For two-argument ops, `A` is the previous element on the stack and `B` is the last element on the stack. These typically result in popping A and B from the stack and pushing the result. -`ed25519verify` is currently the only 3 argument opcode and is described in detail in the opcode refrence. - @@ Arithmetic.md @@ ### Loading Values diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 1657c107b9..7a3ab2e53c 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -19,6 +19,7 @@ Ops have a 'cost' of 1 unless otherwise specified. - **Cost**: - 7 (LogicSigVersion = 1) - 35 (LogicSigVersion = 2) + - 35 (LogicSigVersion = 3) ## keccak256 @@ -29,6 +30,7 @@ Ops have a 'cost' of 1 unless otherwise specified. - **Cost**: - 26 (LogicSigVersion = 1) - 130 (LogicSigVersion = 2) + - 130 (LogicSigVersion = 3) ## sha512_256 @@ -39,6 +41,7 @@ Ops have a 'cost' of 1 unless otherwise specified. - **Cost**: - 9 (LogicSigVersion = 1) - 45 (LogicSigVersion = 2) + - 45 (LogicSigVersion = 3) ## ed25519verify @@ -219,21 +222,21 @@ Overflow is an error condition which halts execution and fails the transaction. - A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack - LogicSigVersion >= 2 -## intcblock +## intcblock uint ... - Opcode: 0x20 {varuint length} [{varuint value}, ...] - Pops: _None_ - Pushes: _None_ -- load block of uint64 constants +- prepare block of uint64 constants for use by intc `intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script. -## intc +## intc i - Opcode: 0x21 {uint8 int constant index} - Pops: _None_ - Pushes: uint64 -- push value from uint64 constants to stack by index into constants +- push Ith constant from intcblock to stack ## intc_0 @@ -263,21 +266,21 @@ Overflow is an error condition which halts execution and fails the transaction. - Pushes: uint64 - push constant 3 from intcblock to stack -## bytecblock +## bytecblock bytes ... - Opcode: 0x26 {varuint length} [({varuint value length} bytes), ...] - Pops: _None_ - Pushes: _None_ -- load block of byte-array constants +- prepare block of byte-array constants for use by bytec -`bytecblock` loads the following program bytes into an array of byte string constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script. +`bytecblock` loads the following program bytes into an array of byte-array constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script. -## bytec +## bytec i - Opcode: 0x27 {uint8 byte constant index} - Pops: _None_ - Pushes: []byte -- push bytes constant to stack by index into constants +- push Ith constant from bytecblock to stack ## bytec_0 @@ -307,12 +310,12 @@ Overflow is an error condition which halts execution and fails the transaction. - Pushes: []byte - push constant 3 from bytecblock to stack -## arg +## arg n - Opcode: 0x2c {uint8 arg index N} - Pops: _None_ - Pushes: []byte -- push Args[N] value to stack by index +- push Nth LogicSig argument to stack - Mode: Signature ## arg_0 @@ -320,7 +323,7 @@ Overflow is an error condition which halts execution and fails the transaction. - Opcode: 0x2d - Pops: _None_ - Pushes: []byte -- push Args[0] to stack +- push LogicSig argument 0 to stack - Mode: Signature ## arg_1 @@ -328,7 +331,7 @@ Overflow is an error condition which halts execution and fails the transaction. - Opcode: 0x2e - Pops: _None_ - Pushes: []byte -- push Args[1] to stack +- push LogicSig argument 1 to stack - Mode: Signature ## arg_2 @@ -336,7 +339,7 @@ Overflow is an error condition which halts execution and fails the transaction. - Opcode: 0x2f - Pops: _None_ - Pushes: []byte -- push Args[2] to stack +- push LogicSig argument 2 to stack - Mode: Signature ## arg_3 @@ -344,17 +347,17 @@ Overflow is an error condition which halts execution and fails the transaction. - Opcode: 0x30 - Pops: _None_ - Pushes: []byte -- push Args[3] to stack +- push LogicSig argument 3 to stack - Mode: Signature -## txn +## txn f - Opcode: 0x31 {uint8 transaction field index} - Pops: _None_ - Pushes: any -- push field from current transaction to stack +- push field F of current transaction to stack -`txn` Fields: +`txn` Fields (see [transaction reference](https://developer.algorand.org/docs/reference/transactions/)): | Index | Name | Type | Notes | | --- | --- | --- | --- | @@ -406,6 +409,14 @@ Overflow is an error condition which halts execution and fails the transaction. | 45 | FreezeAsset | uint64 | Asset ID being frozen or un-frozen. LogicSigVersion >= 2. | | 46 | FreezeAssetAccount | []byte | 32 byte address of the account whose asset slot is being frozen or un-frozen. LogicSigVersion >= 2. | | 47 | FreezeAssetFrozen | uint64 | The new frozen value, 0 or 1. LogicSigVersion >= 2. | +| 48 | Assets | uint64 | Foreign Assets listed in the ApplicationCall transaction. LogicSigVersion >= 3. | +| 49 | NumAssets | uint64 | Number of Assets. LogicSigVersion >= 3. | +| 50 | Applications | uint64 | Foreign Apps listed in the ApplicationCall transaction. LogicSigVersion >= 3. | +| 51 | NumApplications | uint64 | Number of Applications. LogicSigVersion >= 3. | +| 52 | GlobalNumUint | uint64 | Number of global state integers in ApplicationCall. LogicSigVersion >= 3. | +| 53 | GlobalNumByteSlice | uint64 | Number of global state byteslices in ApplicationCall. LogicSigVersion >= 3. | +| 54 | LocalNumUint | uint64 | Number of local state integers in ApplicationCall. LogicSigVersion >= 3. | +| 55 | LocalNumByteSlice | uint64 | Number of local state byteslices in ApplicationCall. LogicSigVersion >= 3. | TypeEnum mapping: @@ -423,7 +434,7 @@ TypeEnum mapping: FirstValidTime causes the program to fail. The field is reserved for future use. -## global +## global f - Opcode: 0x32 {uint8 global field index} - Pops: _None_ @@ -443,74 +454,93 @@ FirstValidTime causes the program to fail. The field is reserved for future use. | 6 | Round | uint64 | Current round number. LogicSigVersion >= 2. | | 7 | LatestTimestamp | uint64 | Last confirmed block UNIX timestamp. Fails if negative. LogicSigVersion >= 2. | | 8 | CurrentApplicationID | uint64 | ID of current application executing. Fails if no such application is executing. LogicSigVersion >= 2. | +| 9 | CreatorAddress | []byte | Address of the creator of the current application. Fails if no such application is executing. LogicSigVersion >= 3. | -## gtxn +## gtxn t f -- Opcode: 0x33 {uint8 transaction group index}{uint8 transaction field index} +- Opcode: 0x33 {uint8 transaction group index} {uint8 transaction field index} - Pops: _None_ - Pushes: any -- push field to the stack from a transaction in the current transaction group +- push field F of the Tth transaction in the current group for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`. -## load +## load i - Opcode: 0x34 {uint8 position in scratch space to load from} - Pops: _None_ - Pushes: any - copy a value from scratch space to the stack -## store +## store i - Opcode: 0x35 {uint8 position in scratch space to store to} - Pops: *... stack*, any - Pushes: _None_ - pop a value from the stack and store to scratch space -## txna +## txna f i -- Opcode: 0x36 {uint8 transaction field index}{uint8 transaction field array index} +- Opcode: 0x36 {uint8 transaction field index} {uint8 transaction field array index} - Pops: _None_ - Pushes: any -- push value of an array field from current transaction to stack +- push Ith value of the array field F of the current transaction - LogicSigVersion >= 2 -## gtxna +## gtxna t f i -- Opcode: 0x37 {uint8 transaction group index}{uint8 transaction field index}{uint8 transaction field array index} +- Opcode: 0x37 {uint8 transaction group index} {uint8 transaction field index} {uint8 transaction field array index} - Pops: _None_ - Pushes: any -- push value of a field to the stack from a transaction in the current transaction group +- push Ith value of the array field F from the Tth transaction in the current group - LogicSigVersion >= 2 -## bnz +## gtxns f + +- Opcode: 0x38 {uint8 transaction field index} +- Pops: *... stack*, uint64 +- Pushes: any +- push field F of the Ath transaction in the current group +- LogicSigVersion >= 3 + +for notes on transaction fields available, see `txn`. If top of stack is _i_, `gtxns field` is equivalent to `gtxn _i_ field`. gtxns exists so that _i_ can be calculated, often based on the index of the current transaction. + +## gtxnsa f i + +- Opcode: 0x39 {uint8 transaction field index} {uint8 transaction field array index} +- Pops: *... stack*, uint64 +- Pushes: any +- push Ith value of the array field F from the Ath transaction in the current group +- LogicSigVersion >= 3 + +## bnz target - Opcode: 0x40 {0..0x7fff forward branch offset, big endian} - Pops: *... stack*, uint64 - Pushes: _None_ -- branch if value X is not zero +- branch to TARGET if value X is not zero The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping. At LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction: bnz to byte N (with 0-indexing) was illegal for a TEAL program with N bytes before LogicSigVersion 2, and is legal after it. This change eliminates the need for a last instruction of no-op as a branch target at the end. (Branching beyond the end--in other words, to a byte larger than N--is still illegal and will cause the program to fail.) -## bz +## bz target - Opcode: 0x41 {0..0x7fff forward branch offset, big endian} - Pops: *... stack*, uint64 - Pushes: _None_ -- branch if value X is zero +- branch to TARGET if value X is zero - LogicSigVersion >= 2 See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`. -## b +## b target - Opcode: 0x42 {0..0x7fff forward branch offset, big endian} - Pops: _None_ - Pushes: _None_ -- branch unconditionally to offset +- branch unconditionally to TARGET - LogicSigVersion >= 2 See `bnz` for details on how branches work. `b` always jumps to the offset. @@ -523,6 +553,14 @@ See `bnz` for details on how branches work. `b` always jumps to the offset. - use last value on stack as success value; end - LogicSigVersion >= 2 +## assert + +- Opcode: 0x44 +- Pops: *... stack*, uint64 +- Pushes: _None_ +- immediately fail unless value X is a non-zero number +- LogicSigVersion >= 3 + ## pop - Opcode: 0x48 @@ -545,22 +583,46 @@ See `bnz` for details on how branches work. `b` always jumps to the offset. - duplicate two last values on stack: A, B -> A, B, A, B - LogicSigVersion >= 2 +## dig n + +- Opcode: 0x4b {uint8 depth} +- Pops: *... stack*, any +- Pushes: *... stack*, any, any +- push the Nth value from the top of the stack. dig 0 is equivalent to dup +- LogicSigVersion >= 3 + +## swap + +- Opcode: 0x4c +- Pops: *... stack*, {any A}, {any B} +- Pushes: *... stack*, any, any +- swaps two last values on stack: A, B -> B, A +- LogicSigVersion >= 3 + +## select + +- Opcode: 0x4d +- Pops: *... stack*, {any A}, {any B}, {uint64 C} +- Pushes: any +- selects one of two values based on top-of-stack: A, B, C -> (if C != 0 then B else A) +- LogicSigVersion >= 3 + ## concat - Opcode: 0x50 - Pops: *... stack*, {[]byte A}, {[]byte B} - Pushes: []byte -- pop two byte strings A and B and join them, push the result +- pop two byte-arrays A and B and join them, push the result - LogicSigVersion >= 2 `concat` panics if the result would be greater than 4096 bytes. -## substring +## substring s e -- Opcode: 0x51 {uint8 start position}{uint8 end position} +- Opcode: 0x51 {uint8 start position} {uint8 end position} - Pops: *... stack*, []byte - Pushes: []byte -- pop a byte string X. For immediate values in 0..255 M and N: extract a range of bytes from it starting at M up to but not including N, push the substring result. If N < M, or either is larger than the string length, the program fails +- pop a byte-array A. For immediate values in 0..255 S and E: extract a range of bytes from A starting at S up to but not including E, push the substring result. If E < S, or either is larger than the array length, the program fails - LogicSigVersion >= 2 ## substring3 @@ -568,15 +630,51 @@ See `bnz` for details on how branches work. `b` always jumps to the offset. - Opcode: 0x52 - Pops: *... stack*, {[]byte A}, {uint64 B}, {uint64 C} - Pushes: []byte -- pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the string length, the program fails +- pop a byte-array A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the array length, the program fails - LogicSigVersion >= 2 +## getbit + +- Opcode: 0x53 +- Pops: *... stack*, {any A}, {uint64 B} +- Pushes: uint64 +- pop a target A (integer or byte-array), and index B. Push the Bth bit of A. +- LogicSigVersion >= 3 + +see explanation of bit ordering in setbit + +## setbit + +- Opcode: 0x54 +- Pops: *... stack*, {any A}, {uint64 B}, {uint64 C} +- Pushes: uint64 +- pop a target A, index B, and bit C. Set the Bth bit of A to C, and push the result +- LogicSigVersion >= 3 + +bit indexing begins with low-order bits in integers. Setting bit 4 to 1 on the integer 0 yields 16 (`int 0x0010`, or 2^4). Indexing begins in the first bytes of a byte-string (as seen in getbyte and substring). Setting bits 0 through 11 to 1 in a 4 byte-array of 0s yields `byte 0xfff00000` + +## getbyte + +- Opcode: 0x55 +- Pops: *... stack*, {[]byte A}, {uint64 B} +- Pushes: uint64 +- pop a byte-array A and integer B. Extract the Bth byte of A and push it as an integer +- LogicSigVersion >= 3 + +## setbyte + +- Opcode: 0x56 +- Pops: *... stack*, {[]byte A}, {uint64 B}, {uint64 C} +- Pushes: []byte +- pop a byte-array A, integer B, and small integer C (between 0..255). Set the Bth byte of A to C, and push the result +- LogicSigVersion >= 3 + ## balance - Opcode: 0x60 - Pops: *... stack*, uint64 - Pushes: uint64 -- get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender +- get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. The balance is observed after the effects of previous transactions in the group, and after the fee for the current transaction is deducted. - LogicSigVersion >= 2 - Mode: Application @@ -600,18 +698,18 @@ params: account index, application id (top of the stack on opcode entry). Return - LogicSigVersion >= 2 - Mode: Application -params: account index, state key. Return: value. The value is zero if the key does not exist. +params: account index, state key. Return: value. The value is zero (of type uint64) if the key does not exist. ## app_local_get_ex - Opcode: 0x63 - Pops: *... stack*, {uint64 A}, {uint64 B}, {[]byte C} - Pushes: *... stack*, any, uint64 -- read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value} +- read from account specified by Txn.Accounts[A] from local state of the application B key C => [*... stack*, value, 0 or 1] - LogicSigVersion >= 2 - Mode: Application -params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. +params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist. ## app_global_get @@ -622,18 +720,18 @@ params: account index, application id, state key. Return: did_exist flag (top of - LogicSigVersion >= 2 - Mode: Application -params: state key. Return: value. The value is zero if the key does not exist. +params: state key. Return: value. The value is zero (of type uint64) if the key does not exist. ## app_global_get_ex - Opcode: 0x65 - Pops: *... stack*, {uint64 A}, {[]byte B} - Pushes: *... stack*, any, uint64 -- read from application Txn.ForeignApps[A] global state key B => {0 or 1 (top), value}. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app +- read from application Txn.ForeignApps[A] global state key B => [*... stack*, value, 0 or 1]. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app - LogicSigVersion >= 2 - Mode: Application -params: application index, state key. Return: value. Application index is +params: application index, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist. ## app_local_put @@ -681,7 +779,7 @@ params: state key. Deleting a key which is already absent has no effect on the application global state. (In particular, it does _not_ cause the program to fail.) -## asset_holding_get +## asset_holding_get i - Opcode: 0x70 {uint8 asset holding field index} - Pops: *... stack*, {uint64 A}, {uint64 B} @@ -700,7 +798,7 @@ Deleting a key which is already absent has no effect on the application global s params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value. -## asset_params_get +## asset_params_get i - Opcode: 0x71 {uint8 asset params field index} - Pops: *... stack*, uint64 @@ -727,3 +825,28 @@ params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherw params: txn.ForeignAssets offset. Return: did_exist flag (1 if exist and 0 otherwise), value. + +## min_balance + +- Opcode: 0x78 +- Pops: *... stack*, uint64 +- Pushes: uint64 +- get minimum required balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. Required balance is affected by [ASA](https://developer.algorand.org/docs/features/asa/#assets-overview) and [App](https://developer.algorand.org/docs/features/asc1/stateful/#minimum-balance-requirement-for-a-smart-contract) usage. When creating or opting into an app, the minimum balance grows before the app code runs, therefore the increase is visible there. When deleting or closing out, the minimum balance decreases after the app executes. +- LogicSigVersion >= 3 +- Mode: Application + +## pushbytes bytes + +- Opcode: 0x80 {varuint length} {bytes} +- Pops: _None_ +- Pushes: []byte +- push the following program bytes to the stack +- LogicSigVersion >= 3 + +## pushint uint + +- Opcode: 0x81 {varuint int} +- Pops: _None_ +- Pushes: uint64 +- push immediate UINT to the stack as an integer +- LogicSigVersion >= 3 diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 91a017b312..99e2d28e3e 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -157,7 +157,6 @@ func (ops *OpStream) Intc(constIndex uint) { } else { ops.trace("intc %d %d", constIndex, ops.intc[constIndex]) } - ops.tpush(StackUint64) } // Uint writes opcodes for loading a uint literal @@ -201,7 +200,6 @@ func (ops *OpStream) Bytec(constIndex uint) { } else { ops.trace("bytec %d %s", constIndex, hex.EncodeToString(ops.bytec[constIndex])) } - ops.tpush(StackBytes) } // ByteLiteral writes opcodes and data for loading a []byte literal @@ -241,7 +239,6 @@ func (ops *OpStream) Arg(val uint64) error { ops.pending.WriteByte(0x2c) ops.pending.WriteByte(uint8(val)) } - ops.tpush(StackBytes) return nil } @@ -304,6 +301,32 @@ func (ops *OpStream) Gtxna(gid, fieldNum uint64, arrayFieldIdx uint64) { ops.tpush(TxnFieldTypes[fieldNum]) } +// Gtxns writes opcodes for loading a field from the current transaction +func (ops *OpStream) Gtxns(fieldNum uint64) { + if fieldNum >= uint64(len(TxnFieldNames)) { + ops.errorf("invalid gtxns field: %d", fieldNum) + fieldNum = 0 // avoid further error in tpush as we forge ahead + } + ops.pending.WriteByte(0x38) + ops.pending.WriteByte(uint8(fieldNum)) + ops.tpush(TxnFieldTypes[fieldNum]) +} + +// Gtxnsa writes opcodes for loading an array field from the current transaction +func (ops *OpStream) Gtxnsa(fieldNum uint64, arrayFieldIdx uint64) { + if fieldNum >= uint64(len(TxnFieldNames)) { + ops.errorf("invalid gtxnsa field: %d", fieldNum) + fieldNum = 0 // avoid further error in tpush as we forge ahead + } + if arrayFieldIdx > 255 { + ops.errorf("gtxnsa array index beyond 255: %d", arrayFieldIdx) + } + ops.pending.WriteByte(0x39) + ops.pending.WriteByte(uint8(fieldNum)) + ops.pending.WriteByte(uint8(arrayFieldIdx)) + ops.tpush(TxnFieldTypes[fieldNum]) +} + // Global writes opcodes for loading an evaluator-global field func (ops *OpStream) Global(val GlobalField) { ops.pending.WriteByte(0x32) @@ -318,7 +341,7 @@ func (ops *OpStream) AssetHolding(val uint64) { ops.errorf("invalid asset holding field: %d", val) val = 0 // avoid further error in tpush as we forge ahead } - ops.pending.WriteByte(opsByName[ops.Version]["asset_holding_get"].Opcode) + ops.pending.WriteByte(OpsByName[ops.Version]["asset_holding_get"].Opcode) ops.pending.WriteByte(uint8(val)) ops.tpush(AssetHoldingFieldTypes[val]) ops.tpush(StackUint64) @@ -330,16 +353,16 @@ func (ops *OpStream) AssetParams(val uint64) { ops.errorf("invalid asset params field: %d", val) val = 0 // avoid further error in tpush as we forge ahead } - ops.pending.WriteByte(opsByName[ops.Version]["asset_params_get"].Opcode) + ops.pending.WriteByte(OpsByName[ops.Version]["asset_params_get"].Opcode) ops.pending.WriteByte(uint8(val)) ops.tpush(AssetParamsFieldTypes[val]) ops.tpush(StackUint64) } func assembleInt(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) if len(args) != 1 { - ops.error("int needs one argument") - args = []string{"0"} // By continuing, Uint will maintain type stack. + return ops.error("int needs one argument") } // check friendly TypeEnum constants te, isTypeEnum := txnTypeConstToUint64[args[0]] @@ -361,8 +384,7 @@ func assembleInt(ops *OpStream, spec *OpSpec, args []string) error { } val, err := strconv.ParseUint(args[0], 0, 64) if err != nil { - ops.error(err) - val = 0 // By continuing, Uint will maintain type stack. + return ops.error(err) } ops.Uint(val) return nil @@ -370,32 +392,62 @@ func assembleInt(ops *OpStream, spec *OpSpec, args []string) error { // Explicit invocation of const lookup and push func assembleIntC(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) if len(args) != 1 { - ops.error("intc operation needs one argument") - args = []string{"0"} // By continuing, Intc will maintain type stack. + return ops.error("intc operation needs one argument") } constIndex, err := strconv.ParseUint(args[0], 0, 64) if err != nil { - ops.error(err) - constIndex = 0 // By continuing, Intc will maintain type stack. + return ops.error(err) } ops.Intc(uint(constIndex)) return nil } func assembleByteC(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) if len(args) != 1 { ops.error("bytec operation needs one argument") - args = []string{"0"} // By continuing, Bytec will maintain type stack. } constIndex, err := strconv.ParseUint(args[0], 0, 64) if err != nil { ops.error(err) - constIndex = 0 // By continuing, Bytec will maintain type stack. } ops.Bytec(uint(constIndex)) return nil } +func asmPushInt(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) + if len(args) != 1 { + ops.errorf("%s needs one argument", spec.Name) + } + val, err := strconv.ParseUint(args[0], 0, 64) + if err != nil { + ops.error(err) + } + ops.pending.WriteByte(spec.Opcode) + var scratch [binary.MaxVarintLen64]byte + vlen := binary.PutUvarint(scratch[:], val) + ops.pending.Write(scratch[:vlen]) + return nil +} +func asmPushBytes(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) + if len(args) != 1 { + ops.errorf("%s needs one argument", spec.Name) + } + val, _, err := parseBinaryArgs(args) + if err != nil { + return ops.error(err) + } + ops.pending.WriteByte(spec.Opcode) + var scratch [binary.MaxVarintLen64]byte + vlen := binary.PutUvarint(scratch[:], uint64(len(val))) + ops.pending.Write(scratch[:vlen]) + ops.pending.Write(val) + return nil +} + func base32DecdodeAnyPadding(x string) (val []byte, err error) { val, err = base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(x) if err != nil { @@ -544,16 +596,13 @@ func parseStringLiteral(input string) (result []byte, err error) { // byte 0x.... // byte "this is a string\n" func assembleByte(ops *OpStream, spec *OpSpec, args []string) error { - var val []byte - var err error + ops.checkArgs(*spec) if len(args) == 0 { - ops.error("byte operation needs byte literal argument") - args = []string{"0x00"} // By continuing, ByteLiteral will maintain type stack. + return ops.error("byte operation needs byte literal argument") } - val, _, err = parseBinaryArgs(args) + val, _, err := parseBinaryArgs(args) if err != nil { - ops.error(err) - val = []byte{} // By continuing, ByteLiteral will maintain type stack. + return ops.error(err) } ops.ByteLiteral(val) return nil @@ -611,41 +660,38 @@ func assembleByteCBlock(ops *OpStream, spec *OpSpec, args []string) error { // addr A1EU... // parses base32-with-checksum account address strings into a byte literal func assembleAddr(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) if len(args) != 1 { - ops.error("addr operation needs one argument") - // By continuing, ByteLiteral will maintain type stack. - args = []string{"7777777777777777777777777777777777777777777777777774MSJUVU"} + return ops.error("addr operation needs one argument") } addr, err := basics.UnmarshalChecksumAddress(args[0]) if err != nil { - ops.error(err) - addr = basics.Address{} // By continuing, ByteLiteral will maintain type stack. + return ops.error(err) } ops.ByteLiteral(addr[:]) return nil } func assembleArg(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) if len(args) != 1 { - ops.error("arg operation needs one argument") - args = []string{"0"} + return ops.error("arg operation needs one argument") } val, err := strconv.ParseUint(args[0], 0, 64) if err != nil { - ops.error(err) - val = 0 // Let ops.Arg maintain type stack + return ops.error(err) } ops.Arg(val) return nil } func assembleBranch(ops *OpStream, spec *OpSpec, args []string) error { + ops.checkArgs(*spec) if len(args) != 1 { - ops.error("branch operation needs label argument") // proceeding so checkArgs runs - } else { - ops.ReferToLabel(ops.pending.Len(), args[0]) + return ops.error("branch operation needs label argument") } - ops.checkArgs(*spec) + + ops.ReferToLabel(ops.pending.Len(), args[0]) ops.pending.WriteByte(spec.Opcode) // zero bytes will get replaced with actual offset in resolveLabels() ops.pending.WriteByte(0) @@ -653,112 +699,31 @@ func assembleBranch(ops *OpStream, spec *OpSpec, args []string) error { return nil } -func assembleLoad(ops *OpStream, spec *OpSpec, args []string) error { - if len(args) != 1 { - ops.error("load operation needs one argument") - args = []string{"0"} // By continuing, tpush will maintain type stack. - } - val, err := strconv.ParseUint(args[0], 0, 64) - if err != nil { - ops.error(err) - val = 0 - } - if val > EvalMaxScratchSize { - ops.errorf("load outside 0..255: %d", val) - val = 0 - } - ops.pending.WriteByte(0x34) - ops.pending.WriteByte(byte(val)) - ops.tpush(StackAny) - return nil -} - -func assembleStore(ops *OpStream, spec *OpSpec, args []string) error { - if len(args) != 1 { - ops.error("store operation needs one argument") - args = []string{"0"} // By continuing, checkArgs, tpush will maintain type stack. - } - val, err := strconv.ParseUint(args[0], 0, 64) - if err != nil { - ops.error(err) - val = 0 - } - if val > EvalMaxScratchSize { - ops.errorf("store outside 0..255: %d", val) - val = 0 - } - ops.checkArgs(*spec) - ops.pending.WriteByte(spec.Opcode) - ops.pending.WriteByte(byte(val)) - return nil -} - func assembleSubstring(ops *OpStream, spec *OpSpec, args []string) error { - if len(args) != 2 { - ops.error("substring expects 2 args") - args = []string{"0", "0"} // By continuing, checkArgs, tpush will maintain type stack. - } - start, err := strconv.ParseUint(args[0], 0, 64) - if err != nil { - ops.error(err) - start = 0 - } - if start > EvalMaxScratchSize { - ops.error("substring limited to 0..255") - start = 0 - } - - end, err := strconv.ParseUint(args[1], 0, 64) - if err != nil { - ops.error(err) - end = start - } - if end > EvalMaxScratchSize { - ops.error("substring limited to 0..255") - end = start - } - + asmDefault(ops, spec, args) + // Having run asmDefault, only need to check extra constraints. + start, _ := strconv.ParseUint(args[0], 0, 64) + end, _ := strconv.ParseUint(args[1], 0, 64) if end < start { - ops.error("substring end is before start") - end = start + return ops.error("substring end is before start") } - opcode := byte(0x51) - ops.checkArgs(*spec) - ops.pending.WriteByte(opcode) - ops.pending.WriteByte(byte(start)) - ops.pending.WriteByte(byte(end)) - ops.trace(" pushes([]byte)") - ops.tpush(StackBytes) return nil } -func disSubstring(dis *disassembleState, spec *OpSpec) { - lastIdx := dis.pc + 2 - if len(dis.program) <= lastIdx { - missing := lastIdx - len(dis.program) + 1 - dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) - return - } - start := uint(dis.program[dis.pc+1]) - end := uint(dis.program[dis.pc+2]) - dis.nextpc = dis.pc + 3 - _, dis.err = fmt.Fprintf(dis.out, "substring %d %d\n", start, end) -} - func assembleTxn(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { return ops.error("txn expects one argument") } fs, ok := txnFieldSpecByName[args[0]] if !ok { - return ops.errorf("txn unknown arg: %v", args[0]) + return ops.errorf("txn unknown field: %v", args[0]) } _, ok = txnaFieldSpecByField[fs.field] if ok { - return ops.errorf("found txna field %v in txn op", args[0]) + return ops.errorf("found array field %v in txn op", args[0]) } if fs.version > ops.Version { - return ops.errorf("txn %s available in version %d. Missed #pragma version?", args[0], fs.version) + return ops.errorf("field %s available in version %d. Missed #pragma version?", args[0], fs.version) } val := fs.field ops.Txn(uint64(val)) @@ -782,11 +747,11 @@ func assembleTxna(ops *OpStream, spec *OpSpec, args []string) error { } fs, ok := txnFieldSpecByName[args[0]] if !ok { - return ops.errorf("txna unknown arg: %v", args[0]) + return ops.errorf("txna unknown field: %v", args[0]) } _, ok = txnaFieldSpecByField[fs.field] if !ok { - return ops.errorf("txna unknown arg: %v", args[0]) + return ops.errorf("txna unknown field: %v", args[0]) } if fs.version > ops.Version { return ops.errorf("txna %s available in version %d. Missed #pragma version?", args[0], fs.version) @@ -810,14 +775,14 @@ func assembleGtxn(ops *OpStream, spec *OpSpec, args []string) error { } fs, ok := txnFieldSpecByName[args[1]] if !ok { - return ops.errorf("gtxn unknown arg: %v", args[1]) + return ops.errorf("gtxn unknown field: %v", args[1]) } _, ok = txnaFieldSpecByField[fs.field] if ok { - return ops.errorf("found gtxna field %v in gtxn op", args[1]) + return ops.errorf("found array field %v in gtxn op", args[1]) } if fs.version > ops.Version { - return ops.errorf("gtxn %s available in version %d. Missed #pragma version?", args[1], fs.version) + return ops.errorf("field %s available in version %d. Missed #pragma version?", args[1], fs.version) } val := fs.field ops.Gtxn(gtid, uint64(val)) @@ -844,11 +809,11 @@ func assembleGtxna(ops *OpStream, spec *OpSpec, args []string) error { } fs, ok := txnFieldSpecByName[args[1]] if !ok { - return ops.errorf("gtxna unknown arg: %v", args[1]) + return ops.errorf("gtxna unknown field: %v", args[1]) } _, ok = txnaFieldSpecByField[fs.field] if !ok { - return ops.errorf("gtxna unknown arg: %v", args[1]) + return ops.errorf("gtxna unknown field: %v", args[1]) } if fs.version > ops.Version { return ops.errorf("gtxna %s available in version %d. Missed #pragma version?", args[1], fs.version) @@ -862,6 +827,53 @@ func assembleGtxna(ops *OpStream, spec *OpSpec, args []string) error { return nil } +func assembleGtxns(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) == 2 { + return assembleGtxnsa(ops, spec, args) + } + if len(args) != 1 { + return ops.error("gtxns expects one or two immediate arguments") + } + fs, ok := txnFieldSpecByName[args[0]] + if !ok { + return ops.errorf("gtxns unknown field: %v", args[0]) + } + _, ok = txnaFieldSpecByField[fs.field] + if ok { + return ops.errorf("found array field %v in gtxns op", args[0]) + } + if fs.version > ops.Version { + return ops.errorf("field %s available in version %d. Missed #pragma version?", args[0], fs.version) + } + val := fs.field + ops.Gtxns(uint64(val)) + return nil +} + +func assembleGtxnsa(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 2 { + return ops.error("gtxnsa expects two immediate arguments") + } + fs, ok := txnFieldSpecByName[args[0]] + if !ok { + return ops.errorf("gtxnsa unknown field: %v", args[0]) + } + _, ok = txnaFieldSpecByField[fs.field] + if !ok { + return ops.errorf("gtxnsa unknown field: %v", args[0]) + } + if fs.version > ops.Version { + return ops.errorf("gtxnsa %s available in version %d. Missed #pragma version?", args[0], fs.version) + } + arrayFieldIdx, err := strconv.ParseUint(args[1], 0, 64) + if err != nil { + return ops.error(err) + } + fieldNum := fs.field + ops.Gtxnsa(uint64(fieldNum), uint64(arrayFieldIdx)) + return nil +} + func assembleGlobal(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { ops.error("global expects one argument") @@ -869,7 +881,7 @@ func assembleGlobal(ops *OpStream, spec *OpSpec, args []string) error { } fs, ok := globalFieldSpecByName[args[0]] if !ok { - ops.errorf("global unknown arg: %v", args[0]) + ops.errorf("global unknown field: %v", args[0]) fs, _ = globalFieldSpecByName[GlobalFieldNames[0]] } if fs.version > ops.Version { @@ -909,32 +921,33 @@ func assembleAssetParams(ops *OpStream, spec *OpSpec, args []string) error { type assembleFunc func(*OpStream, *OpSpec, []string) error +// Basic assembly. Any extra bytes of opcode are encoded as byte immediates. func asmDefault(ops *OpStream, spec *OpSpec, args []string) error { ops.checkArgs(*spec) - if len(spec.Returns) > 0 { - ops.tpusha(spec.Returns) - ops.trace(" pushes(%s", spec.Returns[0].String()) - if len(spec.Returns) > 1 { - for _, rt := range spec.Returns[1:] { - ops.trace(", %s", rt.String()) - } - } - ops.trace(")") + if len(args) != spec.Details.Size-1 { + ops.errorf("%s expects %d immediate arguments", spec.Name, spec.Details.Size) } ops.pending.WriteByte(spec.Opcode) + for i := 0; i < spec.Details.Size-1; i++ { + val, err := strconv.ParseUint(args[i], 0, 64) + if err != nil { + return ops.error(err) + } + if val > 255 { + return ops.errorf("%s outside 0..255: %d", spec.Name, val) + } + ops.pending.WriteByte(byte(val)) + } return nil } // keywords handle parsing and assembling special asm language constructs like 'addr' -var keywords map[string]assembleFunc - -func init() { - // WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() - keywords = make(map[string]assembleFunc) - keywords["int"] = assembleInt - keywords["byte"] = assembleByte - keywords["addr"] = assembleAddr // parse basics.Address, actually just another []byte constant - // WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() +// We use OpSpec here, but somewhat degenerate, since they don't have opcodes or eval functions +var keywords = map[string]OpSpec{ + "int": {0, "int", nil, assembleInt, nil, nil, oneInt, 1, modeAny, opDetails{1, 2, nil, nil}}, + "byte": {0, "byte", nil, assembleByte, nil, nil, oneBytes, 1, modeAny, opDetails{1, 2, nil, nil}}, + // parse basics.Address, actually just another []byte constant + "addr": {0, "addr", nil, assembleAddr, nil, nil, oneBytes, 1, modeAny, opDetails{1, 2, nil, nil}}, } type lineError struct { @@ -953,6 +966,9 @@ func (le *lineError) Unwrap() error { func typecheck(expected, got StackType) bool { // Some ops push 'any' and we wait for run time to see what it is. // Some of those 'any' are based on fields that we _could_ know now but haven't written a more detailed system of typecheck for (yet). + if expected == StackAny && got == StackNone { // Any is lenient, but stack can't be empty + return false + } if (expected == StackAny) || (got == StackAny) { return true } @@ -1064,6 +1080,17 @@ func (ops *OpStream) checkArgs(spec OpSpec) { if !firstPop { ops.trace(")") } + + if len(spec.Returns) > 0 { + ops.tpusha(spec.Returns) + ops.trace(" pushes(%s", spec.Returns[0].String()) + if len(spec.Returns) > 1 { + for _, rt := range spec.Returns[1:] { + ops.trace(", %s", rt.String()) + } + } + ops.trace(")") + } } // assemble reads text from an input and accumulates the program @@ -1099,20 +1126,14 @@ func (ops *OpStream) assemble(fin io.Reader) error { ops.Version = AssemblerDefaultVersion } opstring := fields[0] - spec, ok := opsByName[ops.Version][opstring] - var asmFunc assembleFunc - if ok { - asmFunc = spec.asm - } else { - kwFunc, ok := keywords[opstring] - if ok { - asmFunc = kwFunc - } + spec, ok := OpsByName[ops.Version][opstring] + if !ok { + spec, ok = keywords[opstring] } - if asmFunc != nil { + if ok { ops.trace("%3d: %s\t", ops.sourceLine, opstring) ops.RecordSourceLine() - asmFunc(ops, &spec, fields[1:]) + spec.asm(ops, &spec, fields[1:]) ops.trace("\n") continue } @@ -1120,7 +1141,13 @@ func (ops *OpStream) assemble(fin io.Reader) error { ops.createLabel(opstring[:len(opstring)-1]) continue } - ops.errorf("unknown opcode: %v", opstring) + // unknown opcode, let's report a good error if version problem + spec, ok = OpsByName[AssemblerMaxVersion][opstring] + if ok { + ops.errorf("%s opcode was introduced in TEAL v%d", opstring, spec.Version) + } else { + ops.errorf("unknown opcode: %s", opstring) + } } // backward compatibility: do not allow jumps behind last instruction in TEAL v1 @@ -1179,9 +1206,9 @@ func (ops *OpStream) pragma(line string) error { if ops.Version == assemblerNoVersion { ops.Version = ver } else if ops.Version != ver { - return ops.errorf("version mismatch: assembling v%d with v%d assembler", ops.Version, ver) + return ops.errorf("version mismatch: assembling v%d with v%d assembler", ver, ops.Version) } else { - // ops.Version is already correct + // ops.Version is already correct, or needed to be upped. } return nil default: @@ -1344,6 +1371,8 @@ func AssembleString(text string) (*OpStream, error) { // version is assemblerNoVersion it uses #pragma version or fallsback // to AssemblerDefaultVersion. OpStream is returned to allow access // to warnings, (multiple) errors, or the PC to source line mapping. +// Note that AssemblerDefaultVersion is not the latest supported version, +// and therefore we might need to pass in explicitly a higher version. func AssembleStringWithVersion(text string, version uint64) (*OpStream, error) { sr := strings.NewReader(text) ops := OpStream{Version: version} @@ -1378,9 +1407,27 @@ func (dis *disassembleState) outputLabelIfNeeded() (err error) { type disassembleFunc func(dis *disassembleState, spec *OpSpec) +// Basic disasemble, and extra bytes of opcode are decoded as bytes integers. func disDefault(dis *disassembleState, spec *OpSpec) { - dis.nextpc = dis.pc + 1 - _, dis.err = fmt.Fprintf(dis.out, "%s\n", spec.Name) + lastIdx := dis.pc + spec.Details.Size - 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } + dis.nextpc = dis.pc + spec.Details.Size + _, dis.err = fmt.Fprintf(dis.out, "%s", spec.Name) + if dis.err != nil { + return + } + for s := 1; s < spec.Details.Size; s++ { + b := uint(dis.program[dis.pc+s]) + _, dis.err = fmt.Fprintf(dis.out, " %d", b) + if dis.err != nil { + return + } + } + _, dis.err = fmt.Fprintf(dis.out, "\n") } var errShortIntcblock = errors.New("intcblock ran past end of program") @@ -1546,17 +1593,6 @@ func disIntcblock(dis *disassembleState, spec *OpSpec) { _, dis.err = dis.out.Write([]byte("\n")) } -func disIntc(dis *disassembleState, spec *OpSpec) { - lastIdx := dis.pc + 1 - if len(dis.program) <= lastIdx { - missing := lastIdx - len(dis.program) + 1 - dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) - return - } - dis.nextpc = dis.pc + 2 - _, dis.err = fmt.Fprintf(dis.out, "intc %d\n", dis.program[dis.pc+1]) -} - func disBytecblock(dis *disassembleState, spec *OpSpec) { var bytec [][]byte bytec, dis.nextpc, dis.err = parseBytecBlock(dis.program, dis.pc) @@ -1576,28 +1612,45 @@ func disBytecblock(dis *disassembleState, spec *OpSpec) { _, dis.err = dis.out.Write([]byte("\n")) } -func disBytec(dis *disassembleState, spec *OpSpec) { - lastIdx := dis.pc + 1 - if len(dis.program) <= lastIdx { - missing := lastIdx - len(dis.program) + 1 - dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) +func disPushInt(dis *disassembleState, spec *OpSpec) { + pos := dis.pc + 1 + val, bytesUsed := binary.Uvarint(dis.program[pos:]) + if bytesUsed <= 0 { + dis.err = fmt.Errorf("could not decode int at pc=%d", pos) return } - dis.nextpc = dis.pc + 2 - _, dis.err = fmt.Fprintf(dis.out, "bytec %d\n", dis.program[dis.pc+1]) + pos += bytesUsed + _, dis.err = fmt.Fprintf(dis.out, "%s %d\n", spec.Name, val) + dis.nextpc = pos +} +func checkPushInt(cx *evalContext) int { + opPushInt(cx) + return 1 } -func disArg(dis *disassembleState, spec *OpSpec) { - lastIdx := dis.pc + 1 - if len(dis.program) <= lastIdx { - missing := lastIdx - len(dis.program) + 1 - dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) +func disPushBytes(dis *disassembleState, spec *OpSpec) { + pos := dis.pc + 1 + length, bytesUsed := binary.Uvarint(dis.program[pos:]) + if bytesUsed <= 0 { + dis.err = fmt.Errorf("could not decode bytes length at pc=%d", pos) return } - dis.nextpc = dis.pc + 2 - _, dis.err = fmt.Fprintf(dis.out, "arg %d\n", dis.program[dis.pc+1]) + pos += bytesUsed + end := uint64(pos) + length + if end > uint64(len(dis.program)) || end < uint64(pos) { + dis.err = fmt.Errorf("pushbytes too long %d %d", end, pos) + return + } + bytes := dis.program[pos:end] + _, dis.err = fmt.Fprintf(dis.out, "%s 0x%s", spec.Name, hex.EncodeToString(bytes)) + dis.nextpc = int(end) +} +func checkPushBytes(cx *evalContext) int { + opPushBytes(cx) + return 1 } +// This is also used to disassemble gtxns func disTxn(dis *disassembleState, spec *OpSpec) { lastIdx := dis.pc + 1 if len(dis.program) <= lastIdx { @@ -1611,9 +1664,10 @@ func disTxn(dis *disassembleState, spec *OpSpec) { dis.err = fmt.Errorf("invalid txn arg index %d at pc=%d", txarg, dis.pc) return } - _, dis.err = fmt.Fprintf(dis.out, "txn %s\n", TxnFieldNames[txarg]) + _, dis.err = fmt.Fprintf(dis.out, "%s %s\n", spec.Name, TxnFieldNames[txarg]) } +// This is also used to disassemble gtxnsa func disTxna(dis *disassembleState, spec *OpSpec) { lastIdx := dis.pc + 2 if len(dis.program) <= lastIdx { @@ -1628,7 +1682,7 @@ func disTxna(dis *disassembleState, spec *OpSpec) { return } arrayFieldIdx := dis.program[dis.pc+2] - _, dis.err = fmt.Fprintf(dis.out, "txna %s %d\n", TxnFieldNames[txarg], arrayFieldIdx) + _, dis.err = fmt.Fprintf(dis.out, "%s %s %d\n", spec.Name, TxnFieldNames[txarg], arrayFieldIdx) } func disGtxn(dis *disassembleState, spec *OpSpec) { @@ -1702,30 +1756,6 @@ func disBranch(dis *disassembleState, spec *OpSpec) { _, dis.err = fmt.Fprintf(dis.out, "%s %s\n", spec.Name, label) } -func disLoad(dis *disassembleState, spec *OpSpec) { - lastIdx := dis.pc + 1 - if len(dis.program) <= lastIdx { - missing := lastIdx - len(dis.program) + 1 - dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) - return - } - n := uint(dis.program[dis.pc+1]) - dis.nextpc = dis.pc + 2 - _, dis.err = fmt.Fprintf(dis.out, "load %d\n", n) -} - -func disStore(dis *disassembleState, spec *OpSpec) { - lastIdx := dis.pc + 1 - if len(dis.program) <= lastIdx { - missing := lastIdx - len(dis.program) + 1 - dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) - return - } - n := uint(dis.program[dis.pc+1]) - dis.nextpc = dis.pc + 2 - _, dis.err = fmt.Fprintf(dis.out, "store %d\n", n) -} - func disAssetHolding(dis *disassembleState, spec *OpSpec) { lastIdx := dis.pc + 1 if len(dis.program) <= lastIdx { diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 24e42eee0d..762335193a 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -27,7 +27,7 @@ import ( ) // used by TestAssemble and others, see UPDATE PROCEDURE in TestAssemble() -const bigTestAssembleNonsenseProgram = `err +const v1Nonsense = `err global MinTxnFee global MinBalance global MaxTxnLife @@ -130,6 +130,9 @@ store 2 intc 0 intc 1 mulw +` + +const v2Nonsense = ` dup2 pop pop @@ -214,6 +217,42 @@ txn FreezeAssetAccount txn FreezeAssetFrozen ` +const v3Nonsense = ` +assert +min_balance +int 0x031337 // get bit 1, negate it, put it back +int 1 +getbit +! +int 1 +setbit +byte "test" // get byte 2, increment it, put it back +int 2 +getbyte +int 1 ++ +int 2 +setbyte +swap +select +dig 2 +int 1 +gtxns ConfigAsset +int 2 +gtxnsa Accounts 0 +pushint 1000 +pushbytes "john" +` + +func pseudoOp(opcode string) bool { + // We don't test every combination of + // intcblock,bytecblock,intc*,bytec*,arg* here. Not all of + // these are truly pseudops, but it seems a good name. + return strings.HasPrefix(opcode, "int") || + strings.HasPrefix(opcode, "byte") || + strings.HasPrefix(opcode, "arg") +} + // Check that assembly output is stable across time. func TestAssemble(t *testing.T) { // UPDATE PROCEDURE: @@ -226,17 +265,14 @@ func TestAssemble(t *testing.T) { // This doesn't have to be a sensible program to run, it just has to compile. for _, spec := range OpSpecs { // Ensure that we have some basic check of all the ops, except - // we don't test every combination of - // intcblock,bytecblock,intc*,bytec*,arg* here. - if !strings.Contains(bigTestAssembleNonsenseProgram, spec.Name) && - !strings.HasPrefix(spec.Name, "int") && - !strings.HasPrefix(spec.Name, "byte") && - !strings.HasPrefix(spec.Name, "arg") { - t.Errorf("test should contain op %v", spec.Name) + if !strings.Contains(v1Nonsense+v2Nonsense, spec.Name) && + !pseudoOp(spec.Name) && spec.Version <= 2 { + t.Errorf("v2 nonsense test should contain op %v", spec.Name) } } - ops, err := AssembleStringWithVersion(bigTestAssembleNonsenseProgram, AssemblerMaxVersion) - require.NoError(t, err) + // First, we test v2, not AssemblerMaxVersion. A higher version is + // allowed to differ (and must, in the first byte). + ops := testProg(t, v1Nonsense+v2Nonsense, 2) // check that compilation is stable over time and we assemble to the same bytes this month that we did last month. expectedBytes, _ := hex.DecodeString("022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f") if bytes.Compare(expectedBytes, ops.Program) != 0 { @@ -244,6 +280,27 @@ func TestAssemble(t *testing.T) { t.Log(hex.EncodeToString(ops.Program)) } require.Equal(t, expectedBytes, ops.Program) + + // We test v3 here, and compare to AssemblerMaxVersion, with + // the intention that the test breaks the next time + // AssemblerMaxVersion is increased. At that point, we would + // add a new test for v4, and leave behind this test for v3. + + for _, spec := range OpSpecs { + // Ensure that we have some basic check of all the ops, except + if !strings.Contains(v1Nonsense+v2Nonsense+v3Nonsense, spec.Name) && + !pseudoOp(spec.Name) && spec.Version <= 3 { + t.Errorf("v3 nonsense test should contain op %v", spec.Name) + } + } + ops = testProg(t, v1Nonsense+v2Nonsense+v3Nonsense, AssemblerMaxVersion) + // check that compilation is stable over time and we assemble to the same bytes this month that we did last month. + expectedBytes, _ = hex.DecodeString("032008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f4478222105531421055427042106552105082106564c4d4b02210538212106391c0081e80780046a6f686e") + if bytes.Compare(expectedBytes, ops.Program) != 0 { + // this print is for convenience if the program has been changed. the hex string can be copy pasted back in as a new expected result. + t.Log(hex.EncodeToString(ops.Program)) + } + require.Equal(t, expectedBytes, ops.Program) } func TestAssembleAlias(t *testing.T) { @@ -285,26 +342,39 @@ func testMatch(t *testing.T, actual, expected string) { } func testProg(t *testing.T, source string, ver uint64, expected ...expect) *OpStream { - ops, err := AssembleStringWithVersion(source, ver) + program := strings.ReplaceAll(source, ";", "\n") + ops, err := AssembleStringWithVersion(program, ver) if len(expected) == 0 { + if len(ops.Errors) > 0 || err != nil || ops == nil || ops.Program == nil { + t.Log(program) + } + require.Empty(t, ops.Errors) require.NoError(t, err) require.NotNil(t, ops) - require.Empty(t, ops.Errors) require.NotNil(t, ops.Program) } else { require.Error(t, err) errors := ops.Errors - require.Len(t, errors, len(expected)) for _, exp := range expected { - var found *lineError - for _, err := range errors { - if err.Line == exp.l { - found = err + if exp.l == 0 { + // line 0 means: "must match all" + require.Len(t, expected, 1) + for _, err := range errors { + msg := err.Unwrap().Error() + testMatch(t, msg, exp.s) } + } else { + var found *lineError + for _, err := range errors { + if err.Line == exp.l { + found = err + break + } + } + require.NotNil(t, found) + msg := found.Unwrap().Error() + testMatch(t, msg, exp.s) } - require.NotNil(t, found) - msg := found.Unwrap().Error() - testMatch(t, msg, exp.s) } require.Nil(t, ops.Program) } @@ -325,11 +395,11 @@ func testLine(t *testing.T, line string, ver uint64, expected string) { func TestAssembleTxna(t *testing.T) { testLine(t, "txna Accounts 256", AssemblerMaxVersion, "txna array index beyond 255: 256") testLine(t, "txna ApplicationArgs 256", AssemblerMaxVersion, "txna array index beyond 255: 256") - testLine(t, "txna Sender 256", AssemblerMaxVersion, "txna unknown arg: Sender") + testLine(t, "txna Sender 256", AssemblerMaxVersion, "txna unknown field: Sender") testLine(t, "gtxna 0 Accounts 256", AssemblerMaxVersion, "gtxna array index beyond 255: 256") testLine(t, "gtxna 0 ApplicationArgs 256", AssemblerMaxVersion, "gtxna array index beyond 255: 256") testLine(t, "gtxna 256 Accounts 0", AssemblerMaxVersion, "gtxna group index beyond 255: 256") - testLine(t, "gtxna 0 Sender 256", AssemblerMaxVersion, "gtxna unknown arg: Sender") + testLine(t, "gtxna 0 Sender 256", AssemblerMaxVersion, "gtxna unknown field: Sender") testLine(t, "txn Accounts 0", 1, "txn expects one argument") testLine(t, "txn Accounts 0 1", 2, "txn expects one or two arguments") testLine(t, "txna Accounts 0 1", AssemblerMaxVersion, "txna expects two arguments") @@ -339,20 +409,20 @@ func TestAssembleTxna(t *testing.T) { testLine(t, "gtxna 0 Accounts 1 2", AssemblerMaxVersion, "gtxna expects three arguments") testLine(t, "gtxna a Accounts 0", AssemblerMaxVersion, "strconv.ParseUint...") testLine(t, "gtxna 0 Accounts a", AssemblerMaxVersion, "strconv.ParseUint...") - testLine(t, "txn ABC", 2, "txn unknown arg: ABC") - testLine(t, "gtxn 0 ABC", 2, "gtxn unknown arg: ABC") + testLine(t, "txn ABC", 2, "txn unknown field: ABC") + testLine(t, "gtxn 0 ABC", 2, "gtxn unknown field: ABC") testLine(t, "gtxn a ABC", 2, "strconv.ParseUint...") - testLine(t, "txn Accounts", AssemblerMaxVersion, "found txna field Accounts in txn op") - testLine(t, "txn Accounts", 1, "found txna field Accounts in txn op") + testLine(t, "txn Accounts", AssemblerMaxVersion, "found array field Accounts in txn op") + testLine(t, "txn Accounts", 1, "found array field Accounts in txn op") testLine(t, "txn Accounts 0", AssemblerMaxVersion, "") - testLine(t, "gtxn 0 Accounts", AssemblerMaxVersion, "found gtxna field Accounts in gtxn op") - testLine(t, "gtxn 0 Accounts", 1, "found gtxna field Accounts in gtxn op") + testLine(t, "gtxn 0 Accounts", AssemblerMaxVersion, "found array field Accounts in gtxn op") + testLine(t, "gtxn 0 Accounts", 1, "found array field Accounts in gtxn op") testLine(t, "gtxn 0 Accounts 1", AssemblerMaxVersion, "") } func TestAssembleGlobal(t *testing.T) { testLine(t, "global", AssemblerMaxVersion, "global expects one argument") - testLine(t, "global a", AssemblerMaxVersion, "global unknown arg: a") + testLine(t, "global a", AssemblerMaxVersion, "global unknown field: a") } func TestAssembleDefault(t *testing.T) { @@ -704,6 +774,7 @@ byte base64 avGWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz//= == int 1 //sometext && //somemoretext +int 1 == byte b64 //GWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz+8= byte b64 avGWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz//= @@ -711,10 +782,9 @@ byte b64 avGWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz//= ||` for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(text, v) - require.NoError(t, err) + ops := testProg(t, text, v) s := hex.EncodeToString(ops.Program) - require.Equal(t, mutateProgVersion(v, "01200101260320fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfed206af19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfff20fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfef2829122210122a291211"), s) + require.Equal(t, mutateProgVersion(v, "01200101260320fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfed206af19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfff20fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfef282912221022122a291211"), s) }) } } @@ -757,7 +827,7 @@ int 2` t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { testProg(t, source, v, expect{2, "reference to undefined label nowhere"}, - expect{4, "txn unknown arg: XYZ"}) + expect{4, "txn unknown field: XYZ"}) }) } } @@ -766,7 +836,7 @@ func TestAssembleDisassemble(t *testing.T) { // Specifically constructed program text that should be recreated by Disassemble() // TODO: disassemble to int/byte psuedo-ops instead of raw intcblock/bytecblock/intc/bytec t.Parallel() - text := `// version 2 + text := fmt.Sprintf(`// version %d intcblock 0 1 2 3 4 5 bytecblock 0xcafed00d 0x1337 0x2001 0xdeadbeef 0x70077007 intc_1 @@ -795,6 +865,7 @@ global LogicSigVersion global Round global LatestTimestamp global CurrentApplicationID +global CreatorAddress txn Sender txn Fee bnz label1 @@ -845,8 +916,16 @@ txn ConfigAssetClawback txn FreezeAsset txn FreezeAssetAccount txn FreezeAssetFrozen +txna Assets 0 +txn NumAssets +txna Applications 0 +txn NumApplications +txn GlobalNumUint +txn GlobalNumByteSlice +txn LocalNumUint +txn LocalNumByteSlice gtxn 12 Fee -` +`, AssemblerMaxVersion) for _, globalField := range GlobalFieldNames { if !strings.Contains(text, globalField) { t.Errorf("TestAssembleDisassemble missing field global %v", globalField) @@ -857,8 +936,7 @@ gtxn 12 Fee t.Errorf("TestAssembleDisassemble missing field txn %v", txnField) } } - ops, err := AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) + ops := testProg(t, text, AssemblerMaxVersion) t2, err := Disassemble(ops.Program) require.Equal(t, text, t2) require.NoError(t, err) @@ -870,21 +948,26 @@ func TestAssembleDisassembleCycle(t *testing.T) { t.Parallel() tests := map[uint64]string{ - 2: bigTestAssembleNonsenseProgram, - 1: bigTestAssembleNonsenseProgram[:strings.Index(bigTestAssembleNonsenseProgram, "dup2")], + 1: v1Nonsense, + 2: v1Nonsense + v2Nonsense, + 3: v1Nonsense + v2Nonsense + v3Nonsense, } + // This confirms that each program compiles to the same bytes + // (except the leading version indicator), when compiled under + // original and max versions. That doesn't *have* to be true, + // as we can introduce optimizations in later versions that + // change the bytecode emitted. But currently it is, so we + // test it for now to catch any suprises. for v, source := range tests { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(source, v) - require.NoError(t, err) + ops := testProg(t, source, v) t2, err := Disassemble(ops.Program) require.NoError(t, err) - ops2, err := AssembleStringWithVersion(t2, 2) + ops2 := testProg(t, t2, AssemblerMaxVersion) if err != nil { t.Log(t2) } - require.NoError(t, err) require.Equal(t, ops.Program[1:], ops2.Program[1:]) }) } @@ -1005,7 +1088,7 @@ func TestAssembleVersions(t *testing.T) { t.Parallel() testLine(t, "txna Accounts 0", AssemblerMaxVersion, "") testLine(t, "txna Accounts 0", 2, "") - testLine(t, "txna Accounts 0", 1, "unknown opcode: txna") + testLine(t, "txna Accounts 0", 1, "txna opcode was introduced in TEAL v2") } func TestAssembleBalance(t *testing.T) { @@ -1030,7 +1113,7 @@ func TestAssembleAsset(t *testing.T) { func TestDisassembleSingleOp(t *testing.T) { t.Parallel() // test ensures no double arg_0 entries in disassembly listing - sample := "// version 2\narg_0\n" + sample := fmt.Sprintf("// version %d\narg_0\n", AssemblerMaxVersion) ops, err := AssembleStringWithVersion(sample, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, 2, len(ops.Program)) @@ -1042,21 +1125,21 @@ func TestDisassembleSingleOp(t *testing.T) { func TestDisassembleTxna(t *testing.T) { t.Parallel() // check txn and txna are properly disassembled - txnSample := "// version 2\ntxn Sender\n" + txnSample := fmt.Sprintf("// version %d\ntxn Sender\n", AssemblerMaxVersion) ops, err := AssembleStringWithVersion(txnSample, AssemblerMaxVersion) require.NoError(t, err) disassembled, err := Disassemble(ops.Program) require.NoError(t, err) require.Equal(t, txnSample, disassembled) - txnaSample := "// version 2\ntxna Accounts 0\n" + txnaSample := fmt.Sprintf("// version %d\ntxna Accounts 0\n", AssemblerMaxVersion) ops, err = AssembleStringWithVersion(txnaSample, AssemblerMaxVersion) require.NoError(t, err) disassembled, err = Disassemble(ops.Program) require.NoError(t, err) require.Equal(t, txnaSample, disassembled) - txnSample2 := "// version 2\ntxn Accounts 0\n" + txnSample2 := fmt.Sprintf("// version %d\ntxn Accounts 0\n", AssemblerMaxVersion) ops, err = AssembleStringWithVersion(txnSample2, AssemblerMaxVersion) require.NoError(t, err) disassembled, err = Disassemble(ops.Program) @@ -1068,21 +1151,21 @@ func TestDisassembleTxna(t *testing.T) { func TestDisassembleGtxna(t *testing.T) { t.Parallel() // check gtxn and gtxna are properly disassembled - gtxnSample := "// version 2\ngtxn 0 Sender\n" + gtxnSample := fmt.Sprintf("// version %d\ngtxn 0 Sender\n", AssemblerMaxVersion) ops, err := AssembleStringWithVersion(gtxnSample, AssemblerMaxVersion) require.NoError(t, err) disassembled, err := Disassemble(ops.Program) require.NoError(t, err) require.Equal(t, gtxnSample, disassembled) - gtxnaSample := "// version 2\ngtxna 0 Accounts 0\n" + gtxnaSample := fmt.Sprintf("// version %d\ngtxna 0 Accounts 0\n", AssemblerMaxVersion) ops, err = AssembleStringWithVersion(gtxnaSample, AssemblerMaxVersion) require.NoError(t, err) disassembled, err = Disassemble(ops.Program) require.NoError(t, err) require.Equal(t, gtxnaSample, disassembled) - gtxnSample2 := "// version 2\ngtxn 0 Accounts 0\n" + gtxnSample2 := fmt.Sprintf("// version %d\ngtxn 0 Accounts 0\n", AssemblerMaxVersion) ops, err = AssembleStringWithVersion(gtxnSample2, AssemblerMaxVersion) require.NoError(t, err) disassembled, err = Disassemble(ops.Program) @@ -1115,8 +1198,7 @@ label1: func TestAssembleOffsets(t *testing.T) { t.Parallel() source := "err" - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + ops := testProg(t, source, AssemblerMaxVersion) require.Equal(t, 2, len(ops.Program)) require.Equal(t, 1, len(ops.OffsetToLine)) // vlen @@ -1132,8 +1214,7 @@ func TestAssembleOffsets(t *testing.T) { // comment err ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + ops = testProg(t, source, AssemblerMaxVersion) require.Equal(t, 3, len(ops.Program)) require.Equal(t, 2, len(ops.OffsetToLine)) // vlen @@ -1150,13 +1231,12 @@ err require.Equal(t, 2, line) source = `err -bnz label1 +b label1 err label1: err ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + ops = testProg(t, source, AssemblerMaxVersion) require.Equal(t, 7, len(ops.Program)) require.Equal(t, 4, len(ops.OffsetToLine)) // vlen @@ -1167,15 +1247,15 @@ err line, ok = ops.OffsetToLine[1] require.True(t, ok) require.Equal(t, 0, line) - // bnz + // b line, ok = ops.OffsetToLine[2] require.True(t, ok) require.Equal(t, 1, line) - // bnz byte 1 + // b byte 1 line, ok = ops.OffsetToLine[3] require.False(t, ok) require.Equal(t, 0, line) - // bnz byte 2 + // b byte 2 line, ok = ops.OffsetToLine[4] require.False(t, ok) require.Equal(t, 0, line) @@ -1192,8 +1272,7 @@ err // comment ! ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + ops = testProg(t, source, AssemblerMaxVersion) require.Equal(t, 6, len(ops.Program)) require.Equal(t, 2, len(ops.OffsetToLine)) // vlen @@ -1349,6 +1428,7 @@ func TestPragmas(t *testing.T) { // will default to 1 ops = testProg(t, "int 3", assemblerNoVersion) require.Equal(t, uint64(1), ops.Version) + require.Equal(t, uint8(1), ops.Program[0]) ops = testProg(t, "\n#pragma version 2", assemblerNoVersion) require.Equal(t, uint64(2), ops.Version) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index a8cc7147f8..2b3c1cb568 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -222,7 +222,7 @@ gtxn 0 TxID == && pop -// check global +// check global (these are set equal in defaultEvalProto()) global MinTxnFee global MinBalance == @@ -334,24 +334,24 @@ func TestBackwardCompatTEALv1(t *testing.T) { // ensure v2 fields error in v1 program func TestBackwardCompatGlobalFields(t *testing.T) { t.Parallel() - var fields []string + var fields []globalFieldSpec for _, fs := range globalFieldSpecs { if fs.version > 1 { - fields = append(fields, fs.gfield.String()) + fields = append(fields, fs) } } require.Greater(t, len(fields), 1) ledger := makeTestLedger(nil) for _, field := range fields { - text := fmt.Sprintf("global %s", field) - // check V1 assembler fails - testLine(t, text, assemblerNoVersion, "...available in version 2. Missed #pragma version?") - testLine(t, text, 0, "...available in version 2. Missed #pragma version?") - testLine(t, text, 1, "...available in version 2. Missed #pragma version?") + text := fmt.Sprintf("global %s", field.gfield.String()) + // check assembler fails if version before introduction + testLine(t, text, assemblerNoVersion, "...available in version...") + for v := uint64(0); v < field.version; v++ { + testLine(t, text, v, "...available in version...") + } - ops, err := AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) + ops := testProg(t, text, AssemblerMaxVersion) proto := config.Consensus[protocol.ConsensusV23] require.False(t, proto.Application) @@ -360,7 +360,7 @@ func TestBackwardCompatGlobalFields(t *testing.T) { ep.Ledger = ledger // check failure with version check - _, err = Eval(ops.Program, ep) + _, err := Eval(ops.Program, ep) require.Error(t, err) require.Contains(t, err.Error(), "greater than protocol supported version") _, err = Eval(ops.Program, ep) @@ -414,16 +414,17 @@ func TestBackwardCompatTxnFields(t *testing.T) { field := fs.field.String() for _, command := range tests { text := fmt.Sprintf(command, field) - asmError := "...available in version 2..." + asmError := "...available in version ..." if _, ok := txnaFieldSpecByField[fs.field]; ok { parts := strings.Split(text, " ") op := parts[0] - asmError = fmt.Sprintf("found %sa field %s in %s op", op, field, op) + asmError = fmt.Sprintf("found array field %s in %s op", field, op) } - // check V1 assembler fails + // check assembler fails if version before introduction testLine(t, text, assemblerNoVersion, asmError) - testLine(t, text, 0, asmError) - testLine(t, text, 1, asmError) + for v := uint64(0); v < fs.version; v++ { + testLine(t, text, v, asmError) + } ops, err := AssembleStringWithVersion(text, AssemblerMaxVersion) if _, ok := txnaFieldSpecByField[fs.field]; ok { diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 5f6dfbed20..4da698f345 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -65,52 +65,65 @@ var opDocList = []stringString{ {"~", "bitwise invert value X"}, {"mulw", "A times B out to 128-bit long result as low (top) and high uint64 values on the stack"}, {"addw", "A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack"}, - {"intcblock", "load block of uint64 constants"}, - {"intc", "push value from uint64 constants to stack by index into constants"}, + {"intcblock", "prepare block of uint64 constants for use by intc"}, + {"intc", "push Ith constant from intcblock to stack"}, {"intc_0", "push constant 0 from intcblock to stack"}, {"intc_1", "push constant 1 from intcblock to stack"}, {"intc_2", "push constant 2 from intcblock to stack"}, {"intc_3", "push constant 3 from intcblock to stack"}, - {"bytecblock", "load block of byte-array constants"}, - {"bytec", "push bytes constant to stack by index into constants"}, + {"pushint", "push immediate UINT to the stack as an integer"}, + {"bytecblock", "prepare block of byte-array constants for use by bytec"}, + {"bytec", "push Ith constant from bytecblock to stack"}, {"bytec_0", "push constant 0 from bytecblock to stack"}, {"bytec_1", "push constant 1 from bytecblock to stack"}, {"bytec_2", "push constant 2 from bytecblock to stack"}, {"bytec_3", "push constant 3 from bytecblock to stack"}, - {"arg", "push Args[N] value to stack by index"}, - {"arg_0", "push Args[0] to stack"}, - {"arg_1", "push Args[1] to stack"}, - {"arg_2", "push Args[2] to stack"}, - {"arg_3", "push Args[3] to stack"}, - {"txn", "push field from current transaction to stack"}, - {"gtxn", "push field to the stack from a transaction in the current transaction group"}, - {"txna", "push value of an array field from current transaction to stack"}, - {"gtxna", "push value of a field to the stack from a transaction in the current transaction group"}, + {"pushbytes", "push the following program bytes to the stack"}, + {"arg", "push Nth LogicSig argument to stack"}, + {"arg_0", "push LogicSig argument 0 to stack"}, + {"arg_1", "push LogicSig argument 1 to stack"}, + {"arg_2", "push LogicSig argument 2 to stack"}, + {"arg_3", "push LogicSig argument 3 to stack"}, + {"txn", "push field F of current transaction to stack"}, + {"gtxn", "push field F of the Tth transaction in the current group"}, + {"gtxns", "push field F of the Ath transaction in the current group"}, + {"txna", "push Ith value of the array field F of the current transaction"}, + {"gtxna", "push Ith value of the array field F from the Tth transaction in the current group"}, + {"gtxnsa", "push Ith value of the array field F from the Ath transaction in the current group"}, {"global", "push value from globals to stack"}, {"load", "copy a value from scratch space to the stack"}, {"store", "pop a value from the stack and store to scratch space"}, - {"bnz", "branch if value X is not zero"}, - {"bz", "branch if value X is zero"}, - {"b", "branch unconditionally to offset"}, + {"bnz", "branch to TARGET if value X is not zero"}, + {"bz", "branch to TARGET if value X is zero"}, + {"b", "branch unconditionally to TARGET"}, {"return", "use last value on stack as success value; end"}, {"pop", "discard value X from stack"}, {"dup", "duplicate last value on stack"}, {"dup2", "duplicate two last values on stack: A, B -> A, B, A, B"}, - {"concat", "pop two byte strings A and B and join them, push the result"}, - {"substring", "pop a byte string X. For immediate values in 0..255 M and N: extract a range of bytes from it starting at M up to but not including N, push the substring result. If N < M, or either is larger than the string length, the program fails"}, - {"substring3", "pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the string length, the program fails"}, - {"balance", "get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender"}, + {"dig", "push the Nth value from the top of the stack. dig 0 is equivalent to dup"}, + {"swap", "swaps two last values on stack: A, B -> B, A"}, + {"select", "selects one of two values based on top-of-stack: A, B, C -> (if C != 0 then B else A)"}, + {"concat", "pop two byte-arrays A and B and join them, push the result"}, + {"substring", "pop a byte-array A. For immediate values in 0..255 S and E: extract a range of bytes from A starting at S up to but not including E, push the substring result. If E < S, or either is larger than the array length, the program fails"}, + {"substring3", "pop a byte-array A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the array length, the program fails"}, + {"getbit", "pop a target A (integer or byte-array), and index B. Push the Bth bit of A."}, + {"setbit", "pop a target A, index B, and bit C. Set the Bth bit of A to C, and push the result"}, + {"getbyte", "pop a byte-array A and integer B. Extract the Bth byte of A and push it as an integer"}, + {"setbyte", "pop a byte-array A, integer B, and small integer C (between 0..255). Set the Bth byte of A to C, and push the result"}, + {"balance", "get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. The balance is observed after the effects of previous transactions in the group, and after the fee for the current transaction is deducted."}, + {"min_balance", "get minimum required balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. Required balance is affected by [ASA](https://developer.algorand.org/docs/features/asa/#assets-overview) and [App](https://developer.algorand.org/docs/features/asc1/stateful/#minimum-balance-requirement-for-a-smart-contract) usage. When creating or opting into an app, the minimum balance grows before the app code runs, therefore the increase is visible there. When deleting or closing out, the minimum balance decreases after the app executes."}, {"app_opted_in", "check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1}"}, {"app_local_get", "read from account specified by Txn.Accounts[A] from local state of the current application key B => value"}, - {"app_local_get_ex", "read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value}"}, + {"app_local_get_ex", "read from account specified by Txn.Accounts[A] from local state of the application B key C => [*... stack*, value, 0 or 1]"}, {"app_global_get", "read key A from global state of a current application => value"}, - {"app_global_get_ex", "read from application Txn.ForeignApps[A] global state key B => {0 or 1 (top), value}. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app"}, + {"app_global_get_ex", "read from application Txn.ForeignApps[A] global state key B => [*... stack*, value, 0 or 1]. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app"}, {"app_local_put", "write to account specified by Txn.Accounts[A] to local state of a current application key B with value C"}, {"app_global_put", "write key A and value B to global state of the current application"}, {"app_local_del", "delete from account specified by Txn.Accounts[A] local state key B of the current application"}, {"app_global_del", "delete key A from a global state of the current application"}, {"asset_holding_get", "read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value}"}, {"asset_params_get", "read from asset Txn.ForeignAssets[A] params field X (imm arg) => {0 or 1 (top), value}"}, + {"assert", "immediately fail unless value X is a non-zero number"}, } var opDocByName map[string]string @@ -127,20 +140,25 @@ func OpDoc(opName string) string { var opcodeImmediateNoteList = []stringString{ {"intcblock", "{varuint length} [{varuint value}, ...]"}, {"intc", "{uint8 int constant index}"}, + {"pushint", "{varuint int}"}, {"bytecblock", "{varuint length} [({varuint value length} bytes), ...]"}, {"bytec", "{uint8 byte constant index}"}, + {"pushbytes", "{varuint length} {bytes}"}, {"arg", "{uint8 arg index N}"}, {"txn", "{uint8 transaction field index}"}, - {"gtxn", "{uint8 transaction group index}{uint8 transaction field index}"}, - {"txna", "{uint8 transaction field index}{uint8 transaction field array index}"}, - {"gtxna", "{uint8 transaction group index}{uint8 transaction field index}{uint8 transaction field array index}"}, + {"gtxn", "{uint8 transaction group index} {uint8 transaction field index}"}, + {"gtxns", "{uint8 transaction field index}"}, + {"txna", "{uint8 transaction field index} {uint8 transaction field array index}"}, + {"gtxna", "{uint8 transaction group index} {uint8 transaction field index} {uint8 transaction field array index}"}, + {"gtxnsa", "{uint8 transaction field index} {uint8 transaction field array index}"}, {"global", "{uint8 global field index}"}, {"bnz", "{0..0x7fff forward branch offset, big endian}"}, {"bz", "{0..0x7fff forward branch offset, big endian}"}, {"b", "{0..0x7fff forward branch offset, big endian}"}, {"load", "{uint8 position in scratch space to load from}"}, {"store", "{uint8 position in scratch space to store to}"}, - {"substring", "{uint8 start position}{uint8 end position}"}, + {"substring", "{uint8 start position} {uint8 end position}"}, + {"dig", "{uint8 depth}"}, {"asset_holding_get", "{uint8 asset holding field index}"}, {"asset_params_get", "{uint8 asset params field index}"}, } @@ -161,18 +179,21 @@ var opDocExtraList = []stringString{ {"bz", "See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`."}, {"b", "See `bnz` for details on how branches work. `b` always jumps to the offset."}, {"intcblock", "`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script."}, - {"bytecblock", "`bytecblock` loads the following program bytes into an array of byte string constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script."}, + {"bytecblock", "`bytecblock` loads the following program bytes into an array of byte-array constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script."}, {"*", "Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `mulw`."}, {"+", "Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `addw`."}, {"txn", "FirstValidTime causes the program to fail. The field is reserved for future use."}, {"gtxn", "for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`."}, + {"gtxns", "for notes on transaction fields available, see `txn`. If top of stack is _i_, `gtxns field` is equivalent to `gtxn _i_ field`. gtxns exists so that _i_ can be calculated, often based on the index of the current transaction."}, {"btoi", "`btoi` panics if the input is longer than 8 bytes."}, {"concat", "`concat` panics if the result would be greater than 4096 bytes."}, + {"getbit", "see explanation of bit ordering in setbit"}, + {"setbit", "bit indexing begins with low-order bits in integers. Setting bit 4 to 1 on the integer 0 yields 16 (`int 0x0010`, or 2^4). Indexing begins in the first bytes of a byte-string (as seen in getbyte and substring). Setting bits 0 through 11 to 1 in a 4 byte-array of 0s yields `byte 0xfff00000`"}, {"app_opted_in", "params: account index, application id (top of the stack on opcode entry). Return: 1 if opted in and 0 otherwise."}, - {"app_local_get", "params: account index, state key. Return: value. The value is zero if the key does not exist."}, - {"app_local_get_ex", "params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value."}, - {"app_global_get_ex", "params: application index, state key. Return: value. Application index is"}, - {"app_global_get", "params: state key. Return: value. The value is zero if the key does not exist."}, + {"app_local_get", "params: account index, state key. Return: value. The value is zero (of type uint64) if the key does not exist."}, + {"app_local_get_ex", "params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist."}, + {"app_global_get_ex", "params: application index, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist."}, + {"app_global_get", "params: state key. Return: value. The value is zero (of type uint64) if the key does not exist."}, {"app_local_put", "params: account index, state key, value."}, {"app_local_del", "params: account index, state key.\n\nDeleting a key which is already absent has no effect on the application local state. (In particular, it does _not_ cause the program to fail.)"}, {"app_global_del", "params: state key.\n\nDeleting a key which is already absent has no effect on the application global state. (In particular, it does _not_ cause the program to fail.)"}, @@ -199,26 +220,21 @@ type OpGroup struct { // OpGroupList is groupings of ops for documentation purposes. var OpGroupList = []OpGroup{ - {"Arithmetic", []string{"sha256", "keccak256", "sha512_256", "ed25519verify", "+", "-", "/", "*", "<", ">", "<=", ">=", "&&", "||", "==", "!=", "!", "len", "itob", "btoi", "%", "|", "&", "^", "~", "mulw", "addw", "concat", "substring", "substring3"}}, - {"Loading Values", []string{"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "txn", "gtxn", "txna", "gtxna", "global", "load", "store"}}, - {"Flow Control", []string{"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2"}}, - {"State Access", []string{"balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get"}}, -} - -// OpCost returns the relative cost score for an op -func OpCost(opName string) int { - return opsByName[LogicVersion][opName].opSize.cost + {"Arithmetic", []string{"sha256", "keccak256", "sha512_256", "ed25519verify", "+", "-", "/", "*", "<", ">", "<=", ">=", "&&", "||", "==", "!=", "!", "len", "itob", "btoi", "%", "|", "&", "^", "~", "mulw", "addw", "getbit", "setbit", "getbyte", "setbyte", "concat", "substring", "substring3"}}, + {"Loading Values", []string{"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "pushint", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "pushbytes", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "txn", "gtxn", "txna", "gtxna", "gtxns", "gtxnsa", "global", "load", "store"}}, + {"Flow Control", []string{"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2", "dig", "swap", "select", "assert"}}, + {"State Access", []string{"balance", "min_balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get"}}, } // OpAllCosts returns an array of the relative cost score for an op by version. // If all the costs are the same the array is single entry // otherwise it has costs by op version func OpAllCosts(opName string) []int { - cost := opsByName[LogicVersion][opName].opSize.cost + cost := OpsByName[LogicVersion][opName].Details.Cost costs := make([]int, LogicVersion+1) isDifferent := false for v := 1; v <= LogicVersion; v++ { - costs[v] = opsByName[v][opName].opSize.cost + costs[v] = OpsByName[v][opName].Details.Cost if costs[v] > 0 && costs[v] != cost { isDifferent = true } @@ -230,11 +246,6 @@ func OpAllCosts(opName string) []int { return costs } -// OpSize returns the number of bytes for an op. 0 for variable. -func OpSize(opName string) int { - return opsByName[LogicVersion][opName].opSize.size -} - // see assembler.go TxnTypeNames // also used to parse symbolic constants for `int` var typeEnumDescriptions = []stringString{ @@ -307,6 +318,14 @@ var txnFieldDocList = []stringString{ {"NumAppArgs", "Number of ApplicationArgs"}, {"Accounts", "Accounts listed in the ApplicationCall transaction"}, {"NumAccounts", "Number of Accounts"}, + {"Assets", "Foreign Assets listed in the ApplicationCall transaction"}, + {"NumAssets", "Number of Assets"}, + {"Applications", "Foreign Apps listed in the ApplicationCall transaction"}, + {"NumApplications", "Number of Applications"}, + {"GlobalNumUint", "Number of global state integers in ApplicationCall"}, + {"GlobalNumByteSlice", "Number of global state byteslices in ApplicationCall"}, + {"LocalNumUint", "Number of local state integers in ApplicationCall"}, + {"LocalNumByteSlice", "Number of local state byteslices in ApplicationCall"}, {"ApprovalProgram", "Approval program"}, {"ClearStateProgram", "Clear state program"}, {"RekeyTo", "32 byte Sender's new AuthAddr"}, @@ -345,6 +364,7 @@ var globalFieldDocList = []stringString{ {"Round", "Current round number"}, {"LatestTimestamp", "Last confirmed block UNIX timestamp. Fails if negative"}, {"CurrentApplicationID", "ID of current application executing. Fails if no such application is executing"}, + {"CreatorAddress", "Address of the creator of the current application. Fails if no such application is executing"}, } // globalFieldDocs are notes on fields available in `global` diff --git a/data/transactions/logic/doc_test.go b/data/transactions/logic/doc_test.go index 70ed129660..75568d2e08 100644 --- a/data/transactions/logic/doc_test.go +++ b/data/transactions/logic/doc_test.go @@ -84,12 +84,7 @@ func TestOpDocExtra(t *testing.T) { require.Empty(t, xd) } -func TestOpCost(t *testing.T) { - c := OpCost("+") - require.Equal(t, 1, c) - c = OpCost("sha256") - require.True(t, c > 1) - +func TestOpAllCosts(t *testing.T) { a := OpAllCosts("+") require.Equal(t, 1, len(a)) require.Equal(t, 1, a[0]) @@ -101,13 +96,6 @@ func TestOpCost(t *testing.T) { } } -func TestOpSize(t *testing.T) { - c := OpSize("+") - require.Equal(t, 1, c) - c = OpSize("intc") - require.Equal(t, 2, c) -} - func TestTypeNameDescription(t *testing.T) { require.Equal(t, len(TxnTypeNames), len(typeEnumDescriptions)) for i, a := range TxnTypeNames { diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 9dddc585ee..7809dd4ce8 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -124,12 +124,14 @@ func (sv *stackValue) toTealValue() (tv basics.TealValue) { // LedgerForLogic represents ledger API for Stateful TEAL program type LedgerForLogic interface { Balance(addr basics.Address) (basics.MicroAlgos, error) + MinBalance(addr basics.Address, proto *config.ConsensusParams) (basics.MicroAlgos, error) Round() basics.Round LatestTimestamp() int64 AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) AssetParams(aidx basics.AssetIndex) (basics.AssetParams, error) ApplicationID() basics.AppIndex + CreatorAddress() basics.Address OptedIn(addr basics.Address, appIdx basics.AppIndex) (bool, error) GetLocal(addr basics.Address, appIdx basics.AppIndex, key string) (value basics.TealValue, exists bool, err error) @@ -287,7 +289,7 @@ func (pe PanicError) Error() string { } var errLoopDetected = errors.New("loop detected") -var errLogicSignNotSupported = errors.New("LogicSig not supported") +var errLogicSigNotSupported = errors.New("LogicSig not supported") var errTooManyArgs = errors.New("LogicSig has too many arguments") // EvalStateful executes stateful TEAL program @@ -326,7 +328,7 @@ func eval(program []byte, cx *evalContext) (pass bool, err error) { } } err = PanicError{x, errstr} - cx.EvalParams.log().Errorf("recovered panic in Eval: %s", err) + cx.EvalParams.log().Errorf("recovered panic in Eval: %w", err) } }() @@ -341,7 +343,7 @@ func eval(program []byte, cx *evalContext) (pass bool, err error) { }() if (cx.EvalParams.Proto == nil) || (cx.EvalParams.Proto.LogicSigVersion == 0) { - err = errLogicSignNotSupported + err = errLogicSigNotSupported return } if cx.EvalParams.Txn.Lsig.Args != nil && len(cx.EvalParams.Txn.Lsig.Args) > transactions.EvalMaxArgs { @@ -456,7 +458,7 @@ func check(program []byte, params EvalParams) (cost int, err error) { } }() if (params.Proto == nil) || (params.Proto.LogicSigVersion == 0) { - err = errLogicSignNotSupported + err = errLogicSigNotSupported return } var cx evalContext @@ -560,17 +562,17 @@ func (cx *evalContext) step() { } } - oz := spec.opSize - if oz.size != 0 && (cx.pc+oz.size > len(cx.program)) { + deets := spec.Details + if deets.Size != 0 && (cx.pc+deets.Size > len(cx.program)) { cx.err = fmt.Errorf("%3d %s program ends short of immediate values", cx.pc, spec.Name) return } - cx.cost += oz.cost + cx.cost += deets.Cost spec.op(cx) if cx.Trace != nil { immArgsString := " " if spec.Name != "bnz" { - for i := 1; i < spec.opSize.size; i++ { + for i := 1; i < spec.Details.Size; i++ { immArgsString += fmt.Sprintf("0x%02x ", cx.program[cx.pc+i]) } } @@ -608,7 +610,7 @@ func (cx *evalContext) step() { cx.pc = cx.nextpc cx.nextpc = 0 } else { - cx.pc++ + cx.pc += deets.Size } } @@ -623,23 +625,23 @@ func (cx *evalContext) checkStep() (cost int) { cx.err = fmt.Errorf("%s not allowed in current mode", spec.Name) return } - oz := spec.opSize - if oz.size != 0 && (cx.pc+oz.size > len(cx.program)) { + deets := spec.Details + if deets.Size != 0 && (cx.pc+deets.Size > len(cx.program)) { cx.err = fmt.Errorf("%3d %s program ends short of immediate values", cx.pc, spec.Name) return 1 } prevpc := cx.pc - if oz.checkFunc != nil { - cost = oz.checkFunc(cx) + if deets.checkFunc != nil { + cost = deets.checkFunc(cx) if cx.nextpc != 0 { cx.pc = cx.nextpc cx.nextpc = 0 } else { - cx.pc += oz.size + cx.pc += deets.Size } } else { - cost = oz.cost - cx.pc += oz.size + cost = deets.Cost + cx.pc += deets.Size } if cx.Trace != nil { fmt.Fprintf(cx.Trace, "%3d %s\n", prevpc, spec.Name) @@ -674,6 +676,32 @@ func opReturn(cx *evalContext) { cx.nextpc = len(cx.program) } +func opAssert(cx *evalContext) { + last := len(cx.stack) - 1 + if cx.stack[last].Uint != 0 { + cx.stack = cx.stack[:last] + return + } + cx.err = errors.New("assert failed") +} + +func opSwap(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + cx.stack[last], cx.stack[prev] = cx.stack[prev], cx.stack[last] +} + +func opSelect(cx *evalContext) { + last := len(cx.stack) - 1 // condition on top + prev := last - 1 // true is one down + pprev := prev - 1 // false below that + + if cx.stack[last].Uint != 0 { + cx.stack[pprev] = cx.stack[prev] + } + cx.stack = cx.stack[:prev] +} + func opSHA256(cx *evalContext) { last := len(cx.stack) - 1 hash := sha256.Sum256(cx.stack[last].Bytes) @@ -1009,7 +1037,6 @@ func opIntConstN(cx *evalContext, n uint) { func opIntConstLoad(cx *evalContext) { n := uint(cx.program[cx.pc+1]) opIntConstN(cx, n) - cx.nextpc = cx.pc + 2 } func opIntConst0(cx *evalContext) { opIntConstN(cx, 0) @@ -1024,6 +1051,17 @@ func opIntConst3(cx *evalContext) { opIntConstN(cx, 3) } +func opPushInt(cx *evalContext) { + val, bytesUsed := binary.Uvarint(cx.program[cx.pc+1:]) + if bytesUsed <= 0 { + cx.err = fmt.Errorf("could not decode int at pc=%d", cx.pc+1) + return + } + sv := stackValue{Uint: val} + cx.stack = append(cx.stack, sv) + cx.nextpc = cx.pc + 1 + bytesUsed +} + func opByteConstBlock(cx *evalContext) { cx.bytec, cx.nextpc, cx.err = parseBytecBlock(cx.program, cx.pc) } @@ -1038,7 +1076,6 @@ func opByteConstN(cx *evalContext, n uint) { func opByteConstLoad(cx *evalContext) { n := uint(cx.program[cx.pc+1]) opByteConstN(cx, n) - cx.nextpc = cx.pc + 2 } func opByteConst0(cx *evalContext) { opByteConstN(cx, 0) @@ -1053,6 +1090,24 @@ func opByteConst3(cx *evalContext) { opByteConstN(cx, 3) } +func opPushBytes(cx *evalContext) { + pos := cx.pc + 1 + length, bytesUsed := binary.Uvarint(cx.program[pos:]) + if bytesUsed <= 0 { + cx.err = fmt.Errorf("could not decode length at pc=%d", pos) + return + } + pos += bytesUsed + end := uint64(pos) + length + if end > uint64(len(cx.program)) || end < uint64(pos) { + cx.err = fmt.Errorf("pushbytes too long at pc=%d", pos) + return + } + sv := stackValue{Bytes: cx.program[pos:end]} + cx.stack = append(cx.stack, sv) + cx.nextpc = int(end) +} + func opArgN(cx *evalContext, n uint64) { if n >= uint64(len(cx.Txn.Lsig.Args)) { cx.err = fmt.Errorf("cannot load arg[%d] of %d", n, len(cx.Txn.Lsig.Args)) @@ -1065,7 +1120,6 @@ func opArgN(cx *evalContext, n uint64) { func opArg(cx *evalContext) { n := uint64(cx.program[cx.pc+1]) opArgN(cx, n) - cx.nextpc = cx.pc + 2 } func opArg0(cx *evalContext) { opArgN(cx, 0) @@ -1160,6 +1214,19 @@ func opDup2(cx *evalContext) { cx.stack = append(cx.stack, cx.stack[prev:]...) } +func opDig(cx *evalContext) { + depth := int(uint(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 out stack to be prechecked. + if idx < 0 { + cx.err = fmt.Errorf("dig %d with stack size = %d", depth, len(cx.stack)) + return + } + sv := cx.stack[idx] + cx.stack = append(cx.stack, sv) +} + func (cx *evalContext) assetHoldingEnumToValue(holding *basics.AssetHolding, field uint64) (sv stackValue, err error) { switch AssetHoldingField(field) { case AssetBalance: @@ -1293,6 +1360,7 @@ func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field TxnF sv.Uint = uint64(txn.ApplicationID) case OnCompletion: sv.Uint = uint64(txn.OnCompletion) + case ApplicationArgs: if arrayFieldIdx >= uint64(len(txn.ApplicationArgs)) { err = fmt.Errorf("invalid ApplicationArgs index %d", arrayFieldIdx) @@ -1301,6 +1369,7 @@ func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field TxnF sv.Bytes = nilToEmpty(txn.ApplicationArgs[arrayFieldIdx]) case NumAppArgs: sv.Uint = uint64(len(txn.ApplicationArgs)) + case Accounts: if arrayFieldIdx == 0 { // special case: sender @@ -1314,6 +1383,40 @@ func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field TxnF } case NumAccounts: sv.Uint = uint64(len(txn.Accounts)) + + case Assets: + if arrayFieldIdx >= uint64(len(txn.ForeignAssets)) { + err = fmt.Errorf("invalid Assets index %d", arrayFieldIdx) + return + } + sv.Uint = uint64(txn.ForeignAssets[arrayFieldIdx]) + case NumAssets: + sv.Uint = uint64(len(txn.ForeignAssets)) + + case Applications: + if arrayFieldIdx == 0 { + // special case: current app id + sv.Uint = uint64(txn.ApplicationID) + } else { + if arrayFieldIdx > uint64(len(txn.ForeignApps)) { + err = fmt.Errorf("invalid Applications index %d", arrayFieldIdx) + return + } + sv.Uint = uint64(txn.ForeignApps[arrayFieldIdx-1]) + } + case NumApplications: + sv.Uint = uint64(len(txn.ForeignApps)) + + case GlobalNumUint: + sv.Uint = uint64(txn.GlobalStateSchema.NumUint) + case GlobalNumByteSlice: + sv.Uint = uint64(txn.GlobalStateSchema.NumByteSlice) + + case LocalNumUint: + sv.Uint = uint64(txn.LocalStateSchema.NumUint) + case LocalNumByteSlice: + sv.Uint = uint64(txn.LocalStateSchema.NumByteSlice) + case ApprovalProgram: sv.Bytes = nilToEmpty(txn.ApprovalProgram) case ClearStateProgram: @@ -1383,7 +1486,6 @@ func opTxn(cx *evalContext) { return } cx.stack = append(cx.stack, sv) - cx.nextpc = cx.pc + 2 } func opTxna(cx *evalContext) { @@ -1407,7 +1509,6 @@ func opTxna(cx *evalContext) { return } cx.stack = append(cx.stack, sv) - cx.nextpc = cx.pc + 3 } func opGtxn(cx *evalContext) { @@ -1441,7 +1542,6 @@ func opGtxn(cx *evalContext) { } } cx.stack = append(cx.stack, sv) - cx.nextpc = cx.pc + 3 } func opGtxna(cx *evalContext) { @@ -1471,7 +1571,70 @@ func opGtxna(cx *evalContext) { return } cx.stack = append(cx.stack, sv) - cx.nextpc = cx.pc + 4 +} + +func opGtxns(cx *evalContext) { + last := len(cx.stack) - 1 + gtxid := int(cx.stack[last].Uint) + if gtxid >= len(cx.TxnGroup) { + cx.err = fmt.Errorf("gtxns lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + return + } + tx := &cx.TxnGroup[gtxid].Txn + field := TxnField(uint64(cx.program[cx.pc+1])) + fs, ok := txnFieldSpecByField[field] + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } + _, ok = txnaFieldSpecByField[field] + if ok { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } + var sv stackValue + var err error + if TxnField(field) == GroupIndex { + // GroupIndex; asking this when we just specified it is _dumb_, but oh well + sv.Uint = uint64(gtxid) + } else { + sv, err = cx.txnFieldToStack(tx, field, 0, gtxid) + if err != nil { + cx.err = err + return + } + } + cx.stack[last] = sv +} + +func opGtxnsa(cx *evalContext) { + last := len(cx.stack) - 1 + gtxid := int(cx.stack[last].Uint) + if gtxid >= len(cx.TxnGroup) { + cx.err = fmt.Errorf("gtxnsa lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + return + } + tx := &cx.TxnGroup[gtxid].Txn + field := TxnField(uint64(cx.program[cx.pc+1])) + fs, ok := txnFieldSpecByField[field] + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } + _, ok = txnaFieldSpecByField[field] + if !ok { + cx.err = fmt.Errorf("gtxnsa unsupported field %d", field) + return + } + var sv stackValue + var err error + arrayFieldIdx := uint64(cx.program[cx.pc+2]) + sv, err = cx.txnFieldToStack(tx, field, arrayFieldIdx, gtxid) + if err != nil { + cx.err = err + return + } + cx.stack[last] = sv } func (cx *evalContext) getRound() (rnd uint64, err error) { @@ -1503,6 +1666,14 @@ func (cx *evalContext) getApplicationID() (rnd uint64, err error) { return uint64(cx.Ledger.ApplicationID()), nil } +func (cx *evalContext) getCreatorAddress() ([]byte, error) { + if cx.Ledger == nil { + return nil, fmt.Errorf("ledger not available") + } + addr := cx.Ledger.CreatorAddress() + return addr[:], nil +} + var zeroAddress basics.Address func (cx *evalContext) globalFieldToStack(field GlobalField) (sv stackValue, err error) { @@ -1525,6 +1696,8 @@ func (cx *evalContext) globalFieldToStack(field GlobalField) (sv stackValue, err sv.Uint, err = cx.getLatestTimestamp() case CurrentApplicationID: sv.Uint, err = cx.getApplicationID() + case CreatorAddress: + sv.Bytes, err = cx.getCreatorAddress() default: err = fmt.Errorf("invalid global[%d]", field) } @@ -1557,7 +1730,6 @@ func opGlobal(cx *evalContext) { } cx.stack = append(cx.stack, sv) - cx.nextpc = cx.pc + 2 } // Msg is data meant to be signed and then verified with the @@ -1613,7 +1785,6 @@ func opEd25519verify(cx *evalContext) { func opLoad(cx *evalContext) { gindex := int(uint(cx.program[cx.pc+1])) cx.stack = append(cx.stack, cx.scratch[gindex]) - cx.nextpc = cx.pc + 2 } func opStore(cx *evalContext) { @@ -1621,7 +1792,6 @@ func opStore(cx *evalContext) { last := len(cx.stack) - 1 cx.scratch[gindex] = cx.stack[last] cx.stack = cx.stack[:last] - cx.nextpc = cx.pc + 2 } func opConcat(cx *evalContext) { @@ -1661,7 +1831,6 @@ func opSubstring(cx *evalContext) { start := cx.program[cx.pc+1] end := cx.program[cx.pc+2] cx.stack[last].Bytes, cx.err = substring(cx.stack[last].Bytes, int(start), int(end)) - cx.nextpc = cx.pc + 3 } func opSubstring3(cx *evalContext) { @@ -1678,6 +1847,124 @@ func opSubstring3(cx *evalContext) { cx.stack = cx.stack[:prev] } +func opGetBit(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + idx := cx.stack[last].Uint + target := cx.stack[prev] + + var bit uint64 + if target.argType() == StackUint64 { + if idx > 63 { + cx.err = errors.New("getbit index > 63 with with Uint") + return + } + mask := uint64(1) << idx + bit = (target.Uint & mask) >> idx + } else { + // indexing into a byteslice + byteIdx := idx / 8 + if byteIdx >= uint64(len(target.Bytes)) { + cx.err = errors.New("getbit index beyond byteslice") + return + } + byteVal := target.Bytes[byteIdx] + + bitIdx := idx % 8 + // We saying that bit 9 (the 10th bit), for example, + // is the 2nd bit in the second byte, and that "2nd + // bit" here means almost-highest-order bit, because + // we're thinking of the bits in the byte itself as + // being big endian. So this looks "reversed" + mask := byte(0x80) >> bitIdx + bit = uint64((byteVal & mask) >> (7 - bitIdx)) + } + cx.stack[prev].Uint = bit + cx.stack[prev].Bytes = nil + cx.stack = cx.stack[:last] +} + +func opSetBit(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + pprev := prev - 1 + + bit := cx.stack[last].Uint + idx := cx.stack[prev].Uint + target := cx.stack[pprev] + + if bit > 1 { + cx.err = errors.New("setbit value > 1") + return + } + + if target.argType() == StackUint64 { + if idx > 63 { + cx.err = errors.New("setbit index > 63 with Uint") + return + } + mask := uint64(1) << idx + if bit == uint64(1) { + cx.stack[pprev].Uint |= mask // manipulate stack in place + } else { + cx.stack[pprev].Uint &^= mask // manipulate stack in place + } + } else { + // indexing into a byteslice + byteIdx := idx / 8 + if byteIdx >= uint64(len(target.Bytes)) { + cx.err = errors.New("setbit index beyond byteslice") + return + } + + bitIdx := idx % 8 + // We saying that bit 9 (the 10th bit), for example, + // is the 2nd bit in the second byte, and that "2nd + // bit" here means almost-highest-order bit, because + // we're thinking of the bits in the byte itself as + // being big endian. So this looks "reversed" + mask := byte(0x80) >> bitIdx + if bit == uint64(1) { + target.Bytes[byteIdx] |= mask + } else { + target.Bytes[byteIdx] &^= mask + } + } + cx.stack = cx.stack[:prev] +} + +func opGetByte(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + + idx := cx.stack[last].Uint + target := cx.stack[prev] + + if idx >= uint64(len(target.Bytes)) { + cx.err = errors.New("getbyte index beyond byteslice") + return + } + cx.stack[prev].Uint = uint64(target.Bytes[idx]) + cx.stack[prev].Bytes = nil + cx.stack = cx.stack[:last] +} + +func opSetByte(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + pprev := prev - 1 + if cx.stack[last].Uint > 255 { + cx.err = errors.New("setbyte value > 255") + return + } + if cx.stack[prev].Uint > uint64(len(cx.stack[pprev].Bytes)) { + cx.err = errors.New("setbyte index > byte length") + return + } + cx.stack[pprev].Bytes[cx.stack[prev].Uint] = byte(cx.stack[last].Uint) + cx.stack = cx.stack[:prev] +} + func opBalance(cx *evalContext) { last := len(cx.stack) - 1 // account offset @@ -1696,7 +1983,32 @@ func opBalance(cx *evalContext) { microAlgos, err := cx.Ledger.Balance(addr) if err != nil { - cx.err = fmt.Errorf("failed to fetch balance of %v: %s", addr, err.Error()) + cx.err = fmt.Errorf("failed to fetch balance of %v: %w", addr, err) + return + } + + cx.stack[last].Uint = microAlgos.Raw +} + +func opMinBalance(cx *evalContext) { + last := len(cx.stack) - 1 // account offset + + accountIdx := cx.stack[last].Uint + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) + if err != nil { + cx.err = err + return + } + + microAlgos, err := cx.Ledger.MinBalance(addr, cx.Proto) + if err != nil { + cx.err = fmt.Errorf("failed to fetch minimum balance of %v: %w", addr, err) return } @@ -2018,8 +2330,6 @@ func opAssetHoldingGet(cx *evalContext) { cx.stack[prev] = value cx.stack[last].Uint = exist - - cx.nextpc = cx.pc + 2 } func opAssetParamsGet(cx *evalContext) { @@ -2053,6 +2363,4 @@ func opAssetParamsGet(cx *evalContext) { cx.stack[last] = value cx.stack = append(cx.stack, stackValue{Uint: exist}) - - cx.nextpc = cx.pc + 2 } diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 07c22473d3..af19c586ef 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" @@ -33,24 +34,39 @@ import ( type balanceRecord struct { addr basics.Address balance uint64 - apps map[basics.AppIndex]map[string]basics.TealValue + locals map[basics.AppIndex]basics.TealKeyValue holdings map[uint64]basics.AssetHolding mods map[basics.AppIndex]map[string]basics.ValueDelta } +// In our test ledger, we don't store the AppParams with its creator, +// so we need to carry the creator arround with the params, +type appParams struct { + basics.AppParams + Creator basics.Address +} + type testLedger struct { balances map[basics.Address]balanceRecord - applications map[basics.AppIndex]map[string]basics.TealValue + applications map[basics.AppIndex]appParams assets map[basics.AssetIndex]basics.AssetParams - appID uint64 + appID basics.AppIndex + creatorAddr basics.Address mods map[basics.AppIndex]map[string]basics.ValueDelta } +func makeSchemas(li uint64, lb uint64, gi uint64, gb uint64) basics.StateSchemas { + return basics.StateSchemas{ + LocalStateSchema: basics.StateSchema{NumUint: li, NumByteSlice: lb}, + GlobalStateSchema: basics.StateSchema{NumUint: gi, NumByteSlice: gb}, + } +} + func makeBalanceRecord(addr basics.Address, balance uint64) balanceRecord { br := balanceRecord{ addr: addr, balance: balance, - apps: make(map[basics.AppIndex]map[string]basics.TealValue), + locals: make(map[basics.AppIndex]basics.TealKeyValue), holdings: make(map[uint64]basics.AssetHolding), mods: make(map[basics.AppIndex]map[string]basics.ValueDelta), } @@ -63,7 +79,7 @@ func makeTestLedger(balances map[basics.Address]uint64) *testLedger { for addr, balance := range balances { l.balances[addr] = makeBalanceRecord(addr, balance) } - l.applications = make(map[basics.AppIndex]map[string]basics.TealValue) + l.applications = make(map[basics.AppIndex]appParams) l.assets = make(map[basics.AssetIndex]basics.AssetParams) l.mods = make(map[basics.AppIndex]map[string]basics.ValueDelta) return l @@ -77,28 +93,36 @@ func (l *testLedger) reset() { } } -func (l *testLedger) newApp(addr basics.Address, appID uint64) { +func (l *testLedger) newApp(addr basics.Address, appID basics.AppIndex, schemas basics.StateSchemas) { l.appID = appID - appIdx := basics.AppIndex(appID) - l.applications[appIdx] = make(map[string]basics.TealValue) + appIdx := appID + l.applications[appIdx] = appParams{ + Creator: addr, + AppParams: basics.AppParams{ + StateSchemas: schemas, + GlobalState: make(basics.TealKeyValue), + }, + } br, ok := l.balances[addr] if !ok { br = makeBalanceRecord(addr, 0) } - br.apps[appIdx] = make(map[string]basics.TealValue) + br.locals[appIdx] = make(map[string]basics.TealValue) l.balances[addr] = br } -func (l *testLedger) newAsset(assetID uint64, params basics.AssetParams) { +func (l *testLedger) newAsset(creator basics.Address, assetID uint64, params basics.AssetParams) { l.assets[basics.AssetIndex(assetID)] = params + // We're not simulating details of ReserveAddress yet. + l.setHolding(creator, assetID, params.Total, params.DefaultFrozen) } -func (l *testLedger) setHolding(addr basics.Address, assetID uint64, holding basics.AssetHolding) { +func (l *testLedger) setHolding(addr basics.Address, assetID uint64, amount uint64, frozen bool) { br, ok := l.balances[addr] if !ok { br = makeBalanceRecord(addr, 0) } - br.holdings[assetID] = holding + br.holdings[assetID] = basics.AssetHolding{Amount: amount, Frozen: frozen} l.balances[addr] = br } @@ -123,16 +147,53 @@ func (l *testLedger) Balance(addr basics.Address) (amount basics.MicroAlgos, err return basics.MicroAlgos{Raw: br.balance}, nil } +func (l *testLedger) MinBalance(addr basics.Address, proto *config.ConsensusParams) (amount basics.MicroAlgos, err error) { + if l.balances == nil { + err = fmt.Errorf("empty ledger") + return + } + br, ok := l.balances[addr] + if !ok { + err = fmt.Errorf("no such address") + return + } + + var min uint64 + + // First, base MinBalance + min = proto.MinBalance + + // MinBalance for each Asset + assetCost := basics.MulSaturate(proto.MinBalance, uint64(len(br.holdings))) + min = basics.AddSaturate(min, assetCost) + + // Base MinBalance + GlobalStateSchema.MinBalance for each created application + for _, params := range l.applications { + if params.Creator == addr { + min = basics.AddSaturate(min, proto.AppFlatParamsMinBalance) + min = basics.AddSaturate(min, params.GlobalStateSchema.MinBalance(proto).Raw) + } + } + + // Base MinBalance + LocalStateSchema.MinBalance for each opted in application + for idx := range br.locals { + min = basics.AddSaturate(min, proto.AppFlatParamsMinBalance) + min = basics.AddSaturate(min, l.applications[idx].LocalStateSchema.MinBalance(proto).Raw) + } + + return basics.MicroAlgos{Raw: min}, nil +} + func (l *testLedger) GetGlobal(appIdx basics.AppIndex, key string) (basics.TealValue, bool, error) { if appIdx == basics.AppIndex(0) { - appIdx = basics.AppIndex(l.appID) + appIdx = l.appID } - tkvd, ok := l.applications[appIdx] + params, ok := l.applications[appIdx] if !ok { return basics.TealValue{}, false, fmt.Errorf("no such app") } - // return most recent value if avialiable + // return most recent value if available tkvm, ok := l.mods[appIdx] if ok { val, ok := tkvm[key] @@ -143,20 +204,20 @@ func (l *testLedger) GetGlobal(appIdx basics.AppIndex, key string) (basics.TealV } // otherwise return original one - val, ok := tkvd[key] + val, ok := params.GlobalState[key] return val, ok, nil } func (l *testLedger) SetGlobal(key string, value basics.TealValue) error { - appIdx := basics.AppIndex(l.appID) - tkv, ok := l.applications[appIdx] + appIdx := l.appID + params, ok := l.applications[appIdx] if !ok { return fmt.Errorf("no such app") } // if writing the same value, return // this simulates real ledger behavior for tests - val, ok := tkv[key] + val, ok := params.GlobalState[key] if ok && val == value { return nil } @@ -171,14 +232,14 @@ func (l *testLedger) SetGlobal(key string, value basics.TealValue) error { } func (l *testLedger) DelGlobal(key string) error { - appIdx := basics.AppIndex(l.appID) - tkv, ok := l.applications[appIdx] + appIdx := l.appID + params, ok := l.applications[appIdx] if !ok { return fmt.Errorf("no such app") } exist := false - if _, ok := tkv[key]; ok { + if _, ok := params.GlobalState[key]; ok { exist = true } @@ -199,13 +260,13 @@ func (l *testLedger) DelGlobal(key string) error { func (l *testLedger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key string) (basics.TealValue, bool, error) { if appIdx == 0 { - appIdx = basics.AppIndex(l.appID) + appIdx = l.appID } br, ok := l.balances[addr] if !ok { return basics.TealValue{}, false, fmt.Errorf("no such address") } - tkvd, ok := br.apps[appIdx] + tkvd, ok := br.locals[appIdx] if !ok { return basics.TealValue{}, false, fmt.Errorf("no app for account") } @@ -225,13 +286,13 @@ func (l *testLedger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key s } func (l *testLedger) SetLocal(addr basics.Address, key string, value basics.TealValue) error { - appIdx := basics.AppIndex(l.appID) + appIdx := l.appID br, ok := l.balances[addr] if !ok { return fmt.Errorf("no such address") } - tkv, ok := br.apps[appIdx] + tkv, ok := br.locals[appIdx] if !ok { return fmt.Errorf("no app for account") } @@ -253,13 +314,13 @@ func (l *testLedger) SetLocal(addr basics.Address, key string, value basics.Teal } func (l *testLedger) DelLocal(addr basics.Address, key string) error { - appIdx := basics.AppIndex(l.appID) + appIdx := l.appID br, ok := l.balances[addr] if !ok { return fmt.Errorf("no such address") } - tkv, ok := br.apps[appIdx] + tkv, ok := br.locals[appIdx] if !ok { return fmt.Errorf("no app for account") } @@ -285,13 +346,13 @@ func (l *testLedger) DelLocal(addr basics.Address, key string) error { func (l *testLedger) OptedIn(addr basics.Address, appIdx basics.AppIndex) (bool, error) { if appIdx == 0 { - appIdx = basics.AppIndex(l.appID) + appIdx = l.appID } br, ok := l.balances[addr] if !ok { return false, fmt.Errorf("no such address") } - _, ok = br.apps[appIdx] + _, ok = br.locals[appIdx] return ok, nil } @@ -313,7 +374,11 @@ func (l *testLedger) AssetParams(assetID basics.AssetIndex) (basics.AssetParams, } func (l *testLedger) ApplicationID() basics.AppIndex { - return basics.AppIndex(l.appID) + return l.appID +} + +func (l *testLedger) CreatorAddress() basics.Address { + return l.creatorAddr } func (l *testLedger) LocalSchema() basics.StateSchema { @@ -331,7 +396,7 @@ func (l *testLedger) GlobalSchema() basics.StateSchema { } func (l *testLedger) GetDelta(txn *transactions.Transaction) (evalDelta basics.EvalDelta, err error) { - if tkv, ok := l.mods[basics.AppIndex(l.appID)]; ok { + if tkv, ok := l.mods[l.appID]; ok { evalDelta.GlobalDelta = tkv } if len(txn.Accounts) > 0 { @@ -343,7 +408,7 @@ func (l *testLedger) GetDelta(txn *transactions.Transaction) (evalDelta basics.E evalDelta.LocalDeltas = make(map[uint64]basics.StateDelta) for addr, br := range l.balances { if idx, ok := accounts[addr]; ok { - if delta, ok := br.mods[basics.AppIndex(l.appID)]; ok { + if delta, ok := br.mods[l.appID]; ok { evalDelta.LocalDeltas[uint64(idx)] = delta } } @@ -433,6 +498,9 @@ arg 4 opcodesRunModeApplication := `int 0 balance && +int 0 +min_balance +&& intc_0 intc 6 // 100 app_opted_in @@ -520,21 +588,19 @@ pop txn.Txn.Sender: 1, }, ) - ledger.newApp(txn.Txn.Sender, 100) - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - ledger.newAsset(5, params) - ledger.setHolding(txn.Txn.Sender, 5, basics.AssetHolding{Amount: 123, Frozen: true}) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue + ledger.newAsset(txn.Txn.Sender, 5, params) for mode, test := range tests { t.Run(fmt.Sprintf("opcodes_mode=%d", mode), func(t *testing.T) { - ops, err := AssembleStringWithVersion(test.source, AssemblerMaxVersion) - require.NoError(t, err) + ops := testProg(t, test.source, AssemblerMaxVersion) sb := strings.Builder{} ep := defaultEvalParams(&sb, &txn) ep.TxnGroup = txgroup ep.Ledger = ledger ep.Txn.Txn.ApplicationID = 100 - _, err = test.check(ops.Program, ep) + _, err := test.check(ops.Program, ep) require.NoError(t, err) _, err = test.eval(ops.Program, ep) if err != nil { @@ -581,9 +647,10 @@ pop require.Contains(t, err.Error(), "not allowed in current mode") } - // check new opcodes are not allowed in stateless mode - newOpcodeCalls := []string{ + // check stateful opcodes are not allowed in stateless mode + statefulOpcodeCalls := []string{ "int 0\nbalance", + "int 0\nmin_balance", "int 0\nint 0\napp_opted_in", "int 0\nint 0\nbyte 0x01\napp_local_get_ex", "byte 0x01\napp_global_get", @@ -596,11 +663,10 @@ pop "int 0\nint 0\nasset_params_get AssetManager", } - for _, source := range newOpcodeCalls { - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + for _, source := range statefulOpcodeCalls { + ops := testProg(t, source, AssemblerMaxVersion) ep := defaultEvalParams(nil, nil) - _, err = Check(ops.Program, ep) + _, err := Check(ops.Program, ep) require.Error(t, err) _, err = Eval(ops.Program, ep) require.Error(t, err) @@ -618,7 +684,7 @@ func TestBalance(t *testing.T) { text := `int 2 balance -int 1 +int 177 ==` ops, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) @@ -634,7 +700,7 @@ int 1 ep.Ledger = makeTestLedger( map[basics.Address]uint64{ - txn.Txn.Receiver: 1, + txn.Txn.Receiver: 177, }, ) _, err = EvalStateful(ops.Program, ep) @@ -643,7 +709,7 @@ int 1 text = `int 1 balance -int 1 +int 177 ==` ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) @@ -656,7 +722,7 @@ int 1 text = `int 0 balance -int 1 +int 13 ==` ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) @@ -664,7 +730,7 @@ int 1 copy(addr[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui02")) ep.Ledger = makeTestLedger( map[basics.Address]uint64{ - addr: 1, + addr: 13, }, ) pass, err = EvalStateful(ops.Program, ep) @@ -674,7 +740,7 @@ int 1 ep.Ledger = makeTestLedger( map[basics.Address]uint64{ - txn.Txn.Sender: 1, + txn.Txn.Sender: 13, }, ) cost, err = CheckStateful(ops.Program, ep) @@ -685,99 +751,116 @@ int 1 require.True(t, pass) } -func TestAppCheckOptedIn(t *testing.T) { - t.Parallel() - - text := `int 2 // account idx -int 100 // app idx -app_opted_in -int 1 -==` - ops, err := AssembleStringWithVersion(text, AssemblerMaxVersion) +func testApp(t *testing.T, program string, ep EvalParams, problems ...string) basics.EvalDelta { + ops := testProg(t, program, AssemblerMaxVersion) + sb := &strings.Builder{} + ep.Trace = sb + cost, err := CheckStateful(ops.Program, ep) require.NoError(t, err) + require.True(t, cost < 1000) + + // we only use this to test stateful apps. While, I suppose + // it's *legal* to have an app with no stateful ops, this + // convenience routine can assume it, and check it. + pass, err := Eval(ops.Program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "not allowed in current mode") + require.False(t, pass) + + pass, err = EvalStateful(ops.Program, ep) + if len(problems) == 0 { + require.NoError(t, err, sb.String()) + require.True(t, pass, sb.String()) + delta, err := ep.Ledger.GetDelta(&ep.Txn.Txn) + require.NoError(t, err) + return delta + } + + require.Error(t, err, sb.String()) + for _, problem := range problems { + require.Contains(t, err.Error(), problem) + } + if ep.Ledger != nil { + delta, err := ep.Ledger.GetDelta(&ep.Txn.Txn) + require.NoError(t, err) + require.Empty(t, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) + return delta + } + return basics.EvalDelta{} +} + +func TestMinBalance(t *testing.T) { + t.Parallel() txn := makeSampleTxn() txgroup := makeSampleTxnGroup(txn) ep := defaultEvalParams(nil, nil) ep.Txn = &txn ep.TxnGroup = txgroup - _, err = EvalStateful(ops.Program, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "ledger not available") - ep.Ledger = makeTestLedger( + testApp(t, "int 0; min_balance; int 1001; ==", ep, "ledger not available") + + ledger := makeTestLedger( map[basics.Address]uint64{ - txn.Txn.Receiver: 1, + txn.Txn.Sender: 234, // min_balance 0 is Sender + txn.Txn.Receiver: 123, // Accounts[0] has been packed with the Receiver }, ) - _, err = EvalStateful(ops.Program, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "cannot load account") + ep.Ledger = ledger - // Receiver is not opted in - text = `int 1 // account idx -int 100 // app idx -app_opted_in -int 0 -==` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - cost, err := CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err := EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + testApp(t, "int 0; min_balance; int 1001; ==", ep) + // Sender makes an asset, min blance goes up + ledger.newAsset(txn.Txn.Sender, 7, basics.AssetParams{Total: 1000}) + testApp(t, "int 0; min_balance; int 2002; ==", ep) + schemas := makeSchemas(1, 2, 3, 4) + ledger.newApp(txn.Txn.Sender, 77, schemas) + // create + optin + 10 schema base + 4 ints + 6 bytes (local + // and global count b/c newApp opts the creator in) + minb := 2*1002 + 10*1003 + 4*1004 + 6*1005 + testApp(t, fmt.Sprintf("int 0; min_balance; int %d; ==", 2002+minb), ep) + + testApp(t, "int 1; min_balance; int 1001; ==", ep) // 1 == Accounts[0] + // Receiver opts in + ledger.setHolding(txn.Txn.Receiver, 7, 1, true) + testApp(t, "int 1; min_balance; int 2002; ==", ep) // 1 == Accounts[0] + + testApp(t, "int 2; min_balance; int 1001; ==", ep, "cannot load account") + +} + +func TestAppCheckOptedIn(t *testing.T) { + t.Parallel() + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + ep.TxnGroup = txgroup + testApp(t, "int 2; int 100; app_opted_in; int 1; ==", ep, "ledger not available") - // Sender is not opted in - text = `int 0 // account idx -int 100 // app idx -app_opted_in -int 0 -==` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) ledger := makeTestLedger( map[basics.Address]uint64{ - txn.Txn.Sender: 1, + txn.Txn.Receiver: 1, + txn.Txn.Sender: 1, }, ) ep.Ledger = ledger - cost, err = CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) - - // Receiver opted in - text = `int 1 // account idx -int 100 // app idx -app_opted_in -int 1 -==` - ledger.newApp(txn.Txn.Receiver, 100) + testApp(t, "int 2; int 100; app_opted_in; int 1; ==", ep, "cannot load account") - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + // Receiver is not opted in + testApp(t, "int 1; int 100; app_opted_in; int 0; ==", ep) - // Sender opted in - text = `int 0 // account idx -int 100 // app idx -app_opted_in -int 1 -==` - ledger.newApp(txn.Txn.Sender, 100) + // Sender is not opted in + testApp(t, "int 0; int 100; app_opted_in; int 0; ==", ep) - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + // Receiver opted in + ledger.newApp(txn.Txn.Receiver, 100, makeSchemas(0, 0, 0, 0)) + testApp(t, "int 1; int 100; app_opted_in; int 1; ==", ep) + // Sender opted in + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) + testApp(t, "int 0; int 100; app_opted_in; int 1; ==", ep) } func TestAppReadLocalState(t *testing.T) { @@ -796,8 +879,6 @@ err exit: int 1 ==` - ops, err := AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) txn := makeSampleTxn() txgroup := makeSampleTxnGroup(txn) @@ -805,12 +886,8 @@ int 1 ep.Txn = &txn ep.Txn.Txn.ApplicationID = 100 ep.TxnGroup = txgroup - cost, err := CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - _, err = EvalStateful(ops.Program, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "ledger not available") + + testApp(t, text, ep, "ledger not available") ledger := makeTestLedger( map[basics.Address]uint64{ @@ -818,9 +895,7 @@ int 1 }, ) ep.Ledger = ledger - _, err = EvalStateful(ops.Program, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "cannot load account") + testApp(t, text, ep, "cannot load account") text = `int 1 // account idx int 100 // app id @@ -834,11 +909,8 @@ exist: err exit: int 1` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - _, err = EvalStateful(ops.Program, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "no app for account") + + testApp(t, text, ep, "no app for account") ledger = makeTestLedger( map[basics.Address]uint64{ @@ -846,18 +918,13 @@ int 1` }, ) ep.Ledger = ledger - ledger.newApp(txn.Txn.Receiver, 9999) + ledger.newApp(txn.Txn.Receiver, 9999, makeSchemas(0, 0, 0, 0)) - _, err = EvalStateful(ops.Program, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "no app for account") + testApp(t, text, ep, "no app for account") // create the app and check the value from ApplicationArgs[0] (protocol.PaymentTx) does not exist - ledger.newApp(txn.Txn.Receiver, 100) - - pass, err := EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + ledger.newApp(txn.Txn.Receiver, 100, makeSchemas(0, 0, 0, 0)) + testApp(t, text, ep) text = `int 1 // account idx int 100 // app id @@ -868,21 +935,12 @@ err exist: byte 0x414c474f ==` - ledger.balances[txn.Txn.Receiver].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} - - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) + ledger.balances[txn.Txn.Receiver].locals[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} - cost, err = CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + testApp(t, text, ep) // check special case account idx == 0 => sender - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) text = `int 0 // account idx int 100 // app id txn ApplicationArgs 0 @@ -893,17 +951,12 @@ exist: byte 0x414c474f ==` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - - ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + ledger.balances[txn.Txn.Sender].locals[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + testApp(t, text, ep) // check reading state of other app - ledger.newApp(txn.Txn.Sender, 101) - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 101, makeSchemas(0, 0, 0, 0)) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) text = `int 0 // account idx int 101 // app id txn ApplicationArgs 0 @@ -914,13 +967,8 @@ exist: byte 0x414c474f ==` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - - ledger.balances[txn.Txn.Sender].apps[101][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + ledger.balances[txn.Txn.Sender].locals[101][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + testApp(t, text, ep) // check app_local_get text = `int 0 // account idx @@ -929,13 +977,8 @@ app_local_get byte 0x414c474f ==` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - - ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + ledger.balances[txn.Txn.Sender].locals[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + testApp(t, text, ep) // check app_local_get default value text = `int 0 // account idx @@ -944,13 +987,8 @@ app_local_get int 0 ==` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) - - ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) + ledger.balances[txn.Txn.Sender].locals[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + testApp(t, text, ep) } func TestAppReadGlobalState(t *testing.T) { @@ -1008,13 +1046,13 @@ byte 0x414c474f require.Contains(t, err.Error(), "no such app") // create the app and check the value from ApplicationArgs[0] (protocol.PaymentTx) does not exist - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 1)) _, err = EvalStateful(ops.Program, ep) require.Error(t, err) require.Contains(t, err.Error(), "err opcode") - ledger.applications[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + ledger.applications[100].GlobalState[string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} cost, err = CheckStateful(ops.Program, ep) require.NoError(t, err) @@ -1044,7 +1082,7 @@ int 0 ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) - ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + ledger.balances[txn.Txn.Sender].locals[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} pass, err = EvalStateful(ops.Program, ep) require.NoError(t, err) require.True(t, pass) @@ -1248,8 +1286,8 @@ func TestAssets(t *testing.T) { Freeze: txn.Txn.Receiver, Clawback: txn.Txn.Receiver, } - ledger.newAsset(55, params) - ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: true}) + ledger.newAsset(txn.Txn.Sender, 55, params) + ledger.setHolding(txn.Txn.Sender, 55, 123, true) ep := defaultEvalParams(&sb, &txn) ep.Ledger = ledger @@ -1280,7 +1318,7 @@ int 1 ` ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) - ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: false}) + ledger.setHolding(txn.Txn.Sender, 55, 123, false) cost, err = CheckStateful(ops.Program, ep) require.NoError(t, err) require.True(t, cost < 1000) @@ -1289,7 +1327,7 @@ int 1 require.True(t, pass) // check holdings invalid offsets - require.Equal(t, opsByName[ep.Proto.LogicSigVersion]["asset_holding_get"].Opcode, ops.Program[8]) + require.Equal(t, OpsByName[ep.Proto.LogicSigVersion]["asset_holding_get"].Opcode, ops.Program[8]) ops.Program[9] = 0x02 _, err = EvalStateful(ops.Program, ep) require.Error(t, err) @@ -1311,12 +1349,12 @@ int 1 ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.DefaultFrozen = true - ledger.newAsset(55, params) + ledger.newAsset(txn.Txn.Sender, 55, params) pass, err = EvalStateful(ops.Program, ep) require.NoError(t, err) require.True(t, pass) // check holdings invalid offsets - require.Equal(t, opsByName[ep.Proto.LogicSigVersion]["asset_params_get"].Opcode, ops.Program[6]) + require.Equal(t, OpsByName[ep.Proto.LogicSigVersion]["asset_params_get"].Opcode, ops.Program[6]) ops.Program[7] = 0x20 _, err = EvalStateful(ops.Program, ep) require.Error(t, err) @@ -1339,7 +1377,7 @@ int 1 ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.URL = "" - ledger.newAsset(55, params) + ledger.newAsset(txn.Txn.Sender, 55, params) pass, err = EvalStateful(ops.Program, ep) require.NoError(t, err) require.True(t, pass) @@ -1360,7 +1398,7 @@ int 1 ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.URL = "foobarbaz" - ledger.newAsset(77, params) + ledger.newAsset(txn.Txn.Sender, 77, params) pass, err = EvalStateful(ops.Program, ep) require.NoError(t, err) require.True(t, pass) @@ -1380,7 +1418,7 @@ int 1 ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.URL = "" - ledger.newAsset(55, params) + ledger.newAsset(txn.Txn.Sender, 55, params) cost, err = CheckStateful(ops.Program, ep) require.NoError(t, err) require.True(t, cost < 1000) @@ -1464,8 +1502,8 @@ int 100 ep.Ledger = ledger saved := ops.Program[firstCmdOffset] - require.Equal(t, opsByName[0]["intc_0"].Opcode, saved) - ops.Program[firstCmdOffset] = opsByName[0]["intc_1"].Opcode + require.Equal(t, OpsByName[0]["intc_0"].Opcode, saved) + ops.Program[firstCmdOffset] = OpsByName[0]["intc_1"].Opcode _, err = EvalStateful(ops.Program, ep) require.Error(t, err) require.Contains(t, err.Error(), "cannot load account[100]") @@ -1475,7 +1513,7 @@ int 100 require.Error(t, err) require.Contains(t, err.Error(), "no app for account") - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) if name == "read" { _, err = EvalStateful(ops.Program, ep) @@ -1483,8 +1521,8 @@ int 100 require.Contains(t, err.Error(), "err opcode") // no such key } - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = basics.TealValue{Type: basics.TealUintType, Uint: 0x77} - ledger.balances[txn.Txn.Sender].apps[100]["ALGOA"] = basics.TealValue{Type: basics.TealUintType, Uint: 1} + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.balances[txn.Txn.Sender].locals[100]["ALGOA"] = basics.TealValue{Type: basics.TealUintType, Uint: 1} ledger.reset() pass, err := EvalStateful(ops.Program, ep) @@ -1492,12 +1530,12 @@ int 100 require.True(t, pass) delta, err := ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) + require.Empty(t, delta.GlobalDelta) expLocal := 1 if name == "read" { expLocal = 0 } - require.Equal(t, expLocal, len(delta.LocalDeltas)) + require.Len(t, delta.LocalDeltas, expLocal) }) } } @@ -1515,7 +1553,7 @@ func TestAppLocalStateReadWrite(t *testing.T) { }, ) ep.Ledger = ledger - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) // write int and bytes values source := `int 0 // account @@ -1556,10 +1594,10 @@ int 0x77 require.True(t, pass) delta, err := ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 1, len(delta.LocalDeltas)) + require.Empty(t, 0, delta.GlobalDelta) + require.Len(t, delta.LocalDeltas, 1) - require.Equal(t, 2, len(delta.LocalDeltas[0])) + require.Len(t, delta.LocalDeltas[0], 2) vd := delta.LocalDeltas[0]["ALGO"] require.Equal(t, basics.SetUintAction, vd.Action) require.Equal(t, uint64(0x77), vd.Uint) @@ -1584,11 +1622,11 @@ int 0x77 == ` ledger.reset() - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGO") algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1600,8 +1638,8 @@ int 0x77 require.True(t, pass) delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) // write same value after reading, expect no local delta source = `int 0 // account @@ -1625,8 +1663,8 @@ exist2: == ` ledger.reset() - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1635,8 +1673,8 @@ exist2: require.True(t, pass) delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) // write a value and expect local delta change source = `int 0 // account @@ -1646,8 +1684,8 @@ app_local_put int 1 ` ledger.reset() - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1656,9 +1694,9 @@ int 1 require.True(t, pass) delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 1, len(delta.LocalDeltas)) - require.Equal(t, 1, len(delta.LocalDeltas[0])) + require.Empty(t, delta.GlobalDelta) + require.Len(t, delta.LocalDeltas, 1) + require.Len(t, delta.LocalDeltas[0], 1) vd = delta.LocalDeltas[0]["ALGOA"] require.Equal(t, basics.SetUintAction, vd.Action) require.Equal(t, uint64(0x78), vd.Uint) @@ -1679,8 +1717,8 @@ int 0x78 == ` ledger.reset() - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1689,9 +1727,9 @@ int 0x78 require.True(t, pass) delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 1, len(delta.LocalDeltas)) - require.Equal(t, 1, len(delta.LocalDeltas[0])) + require.Empty(t, delta.GlobalDelta) + require.Len(t, delta.LocalDeltas, 1) + require.Len(t, delta.LocalDeltas[0], 1) vd = delta.LocalDeltas[0]["ALGO"] require.Equal(t, basics.SetUintAction, vd.Action) require.Equal(t, uint64(0x78), vd.Uint) @@ -1710,8 +1748,8 @@ int 0x78 // value app_local_put ` ledger.reset() - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1720,9 +1758,9 @@ app_local_put require.True(t, pass) delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 1, len(delta.LocalDeltas)) - require.Equal(t, 1, len(delta.LocalDeltas[0])) + require.Empty(t, delta.GlobalDelta) + require.Len(t, delta.LocalDeltas, 1) + require.Len(t, delta.LocalDeltas[0], 1) vd = delta.LocalDeltas[0]["ALGO"] require.Equal(t, basics.SetUintAction, vd.Action) require.Equal(t, uint64(0x78), vd.Uint) @@ -1747,11 +1785,11 @@ app_local_put int 1 ` ledger.reset() - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 500) - ledger.balances[txn.Txn.Receiver].apps[100] = make(map[string]basics.TealValue) + ledger.balances[txn.Txn.Receiver].locals[100] = make(basics.TealKeyValue) ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1763,10 +1801,10 @@ int 1 require.True(t, pass) delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 2, len(delta.LocalDeltas)) - require.Equal(t, 2, len(delta.LocalDeltas[0])) - require.Equal(t, 1, len(delta.LocalDeltas[1])) + require.Empty(t, delta.GlobalDelta) + require.Len(t, delta.LocalDeltas, 2) + require.Len(t, delta.LocalDeltas[0], 2) + require.Len(t, delta.LocalDeltas[1], 1) vd = delta.LocalDeltas[0]["ALGO"] require.Equal(t, basics.SetUintAction, vd.Action) require.Equal(t, uint64(0x78), vd.Uint) @@ -1837,7 +1875,7 @@ int 1 require.Error(t, err) require.Contains(t, err.Error(), "no such app") - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 1, 0)) // a special test for read if name == "read" { @@ -1845,7 +1883,7 @@ int 1 require.Error(t, err) require.Contains(t, err.Error(), "err opcode") // no such key } - ledger.applications[100]["ALGO"] = basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.applications[100].GlobalState["ALGO"] = basics.TealValue{Type: basics.TealUintType, Uint: 0x77} ledger.reset() pass, err := EvalStateful(ops.Program, ep) @@ -1854,7 +1892,7 @@ int 1 delta, err := ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.LocalDeltas) }) } } @@ -1932,7 +1970,7 @@ int 0x77 }, ) ep.Ledger = ledger - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1945,8 +1983,8 @@ int 0x77 delta, err := ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 2, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Len(t, delta.GlobalDelta, 2) + require.Empty(t, delta.LocalDeltas) vd := delta.GlobalDelta["ALGO"] require.Equal(t, basics.SetUintAction, vd.Action) @@ -1966,11 +2004,11 @@ int 0x77 == ` ledger.reset() - delete(ledger.applications[100], "ALGOA") - delete(ledger.applications[100], "ALGO") + delete(ledger.applications[100].GlobalState, "ALGOA") + delete(ledger.applications[100].GlobalState, "ALGO") algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} - ledger.applications[100]["ALGO"] = algoValue + ledger.applications[100].GlobalState["ALGO"] = algoValue ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1980,8 +2018,8 @@ int 0x77 delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) // write existing value after read source = `int 0 @@ -2000,8 +2038,8 @@ int 0x77 == ` ledger.reset() - delete(ledger.applications[100], "ALGOA") - ledger.applications[100]["ALGO"] = algoValue + delete(ledger.applications[100].GlobalState, "ALGOA") + ledger.applications[100].GlobalState["ALGO"] = algoValue ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -2010,8 +2048,8 @@ int 0x77 require.True(t, pass) delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) // write new values after and before read source = `int 0 @@ -2046,8 +2084,8 @@ byte 0x414c474f && ` ledger.reset() - delete(ledger.applications[100], "ALGOA") - ledger.applications[100]["ALGO"] = algoValue + delete(ledger.applications[100].GlobalState, "ALGOA") + ledger.applications[100].GlobalState["ALGO"] = algoValue ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -2066,8 +2104,8 @@ byte 0x414c474f delta, err = ledger.GetDelta(&ep.Txn.Txn) require.NoError(t, err) - require.Equal(t, 2, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Len(t, delta.GlobalDelta, 2) + require.Empty(t, delta.LocalDeltas) vd = delta.GlobalDelta["ALGO"] require.Equal(t, basics.SetUintAction, vd.Action) @@ -2107,35 +2145,22 @@ byte "myval" }, ) ep.Ledger = ledger - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - cost, err := CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err := EvalStateful(ops.Program, ep) - require.Error(t, err) - require.Contains(t, err.Error(), "no such app") - require.False(t, pass) - delta, err := ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + delta := testApp(t, source, ep, "no such app") + require.Empty(t, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) - ledger.newApp(txn.Txn.Receiver, 101) - ledger.newApp(txn.Txn.Receiver, 100) // this keeps current app id = 100 + ledger.newApp(txn.Txn.Receiver, 101, makeSchemas(0, 0, 0, 0)) + ledger.newApp(txn.Txn.Receiver, 100, makeSchemas(0, 0, 0, 0)) // this keeps current app id = 100 algoValue := basics.TealValue{Type: basics.TealBytesType, Bytes: "myval"} - ledger.applications[101]["mykey"] = algoValue + ledger.applications[101].GlobalState["mykey"] = algoValue - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) - delta, err = ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - require.Equal(t, 0, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + delta = testApp(t, source, ep) + require.Empty(t, delta.GlobalDelta) + require.Empty(t, delta.LocalDeltas) } + func TestAppGlobalDelete(t *testing.T) { t.Parallel() @@ -2175,33 +2200,18 @@ int 1 }, ) ep.Ledger = ledger - ledger.newApp(txn.Txn.Sender, 100) - sb := strings.Builder{} - ep.Trace = &sb + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - cost, err := CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err := EvalStateful(ops.Program, ep) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - delta, err := ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - require.Equal(t, 2, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + delta := testApp(t, source, ep) + require.Len(t, delta.GlobalDelta, 2) + require.Empty(t, delta.LocalDeltas) ledger.reset() - delete(ledger.applications[100], "ALGOA") - delete(ledger.applications[100], "ALGO") + delete(ledger.applications[100].GlobalState, "ALGOA") + delete(ledger.applications[100].GlobalState, "ALGO") algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} - ledger.applications[100]["ALGO"] = algoValue + ledger.applications[100].GlobalState["ALGO"] = algoValue // check delete existing source = `byte 0x414c474f // key "ALGO" @@ -2212,15 +2222,8 @@ app_global_get_ex == // two zeros ` ep.Txn.Txn.ForeignApps = []basics.AppIndex{txn.Txn.ApplicationID} - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) - delta, err = ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - - require.Equal(t, 1, len(delta.GlobalDelta)) + delta = testApp(t, source, ep) + require.Len(t, delta.GlobalDelta, 1) vd := delta.GlobalDelta["ALGO"] require.Equal(t, basics.DeleteAction, vd.Action) require.Equal(t, uint64(0), vd.Uint) @@ -2228,10 +2231,10 @@ app_global_get_ex require.Equal(t, 0, len(delta.LocalDeltas)) ledger.reset() - delete(ledger.applications[100], "ALGOA") - delete(ledger.applications[100], "ALGO") + delete(ledger.applications[100].GlobalState, "ALGOA") + delete(ledger.applications[100].GlobalState, "ALGO") - ledger.applications[100]["ALGO"] = algoValue + ledger.applications[100].GlobalState["ALGO"] = algoValue // check delete and write non-existing source = `byte 0x414c474f41 // key "ALGOA" @@ -2244,26 +2247,19 @@ byte 0x414c474f41 int 0x78 app_global_put ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) - delta, err = ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - - require.Equal(t, 1, len(delta.GlobalDelta)) + delta = testApp(t, source, ep) + require.Len(t, delta.GlobalDelta, 1) vd = delta.GlobalDelta["ALGOA"] require.Equal(t, basics.SetUintAction, vd.Action) require.Equal(t, uint64(0x78), vd.Uint) require.Equal(t, "", vd.Bytes) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.LocalDeltas) ledger.reset() - delete(ledger.applications[100], "ALGOA") - delete(ledger.applications[100], "ALGO") + delete(ledger.applications[100].GlobalState, "ALGOA") + delete(ledger.applications[100].GlobalState, "ALGO") - ledger.applications[100]["ALGO"] = algoValue + ledger.applications[100].GlobalState["ALGO"] = algoValue // check delete and write existing source = `byte 0x414c474f // key "ALGO" @@ -2273,24 +2269,17 @@ int 0x78 app_global_put int 1 ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) - delta, err = ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - - require.Equal(t, 1, len(delta.GlobalDelta)) + delta = testApp(t, source, ep) + require.Len(t, delta.GlobalDelta, 1) vd = delta.GlobalDelta["ALGO"] require.Equal(t, basics.SetUintAction, vd.Action) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.LocalDeltas) ledger.reset() - delete(ledger.applications[100], "ALGOA") - delete(ledger.applications[100], "ALGO") + delete(ledger.applications[100].GlobalState, "ALGOA") + delete(ledger.applications[100].GlobalState, "ALGO") - ledger.applications[100]["ALGO"] = algoValue + ledger.applications[100].GlobalState["ALGO"] = algoValue // check delete,write,delete existing source = `byte 0x414c474f // key "ALGO" @@ -2302,27 +2291,17 @@ byte 0x414c474f app_global_del int 1 ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - cost, err = CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) - delta, err = ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - - require.Equal(t, 1, len(delta.GlobalDelta)) + delta = testApp(t, source, ep) + require.Len(t, delta.GlobalDelta, 1) vd = delta.GlobalDelta["ALGO"] require.Equal(t, basics.DeleteAction, vd.Action) - require.Equal(t, 0, len(delta.LocalDeltas)) + require.Empty(t, delta.LocalDeltas) ledger.reset() - delete(ledger.applications[100], "ALGOA") - delete(ledger.applications[100], "ALGO") + delete(ledger.applications[100].GlobalState, "ALGOA") + delete(ledger.applications[100].GlobalState, "ALGO") - ledger.applications[100]["ALGO"] = algoValue + ledger.applications[100].GlobalState["ALGO"] = algoValue // check delete, write, delete non-existing source = `byte 0x414c474f41 // key "ALGOA" @@ -2334,18 +2313,9 @@ byte 0x414c474f41 app_global_del int 1 ` - ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) - cost, err = CheckStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err = EvalStateful(ops.Program, ep) - require.NoError(t, err) - require.True(t, pass) - delta, err = ledger.GetDelta(&ep.Txn.Txn) - require.NoError(t, err) - require.Equal(t, 1, len(delta.GlobalDelta)) - require.Equal(t, 0, len(delta.LocalDeltas)) + delta = testApp(t, source, ep) + require.Len(t, delta.GlobalDelta, 1) + require.Len(t, delta.LocalDeltas, 0) } func TestAppLocalDelete(t *testing.T) { @@ -2393,9 +2363,9 @@ int 1 }, ) ep.Ledger = ledger - ledger.newApp(txn.Txn.Sender, 100) + ledger.newApp(txn.Txn.Sender, 100, makeSchemas(0, 0, 0, 0)) ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 1) - ledger.balances[txn.Txn.Receiver].apps[100] = make(basics.TealKeyValue) + ledger.balances[txn.Txn.Receiver].locals[100] = make(basics.TealKeyValue) sb := strings.Builder{} ep.Trace = &sb @@ -2418,13 +2388,13 @@ int 1 require.Equal(t, 2, len(delta.LocalDeltas)) ledger.reset() - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") - delete(ledger.balances[txn.Txn.Receiver].apps[100], "ALGOA") - delete(ledger.balances[txn.Txn.Receiver].apps[100], "ALGO") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGO") + delete(ledger.balances[txn.Txn.Receiver].locals[100], "ALGOA") + delete(ledger.balances[txn.Txn.Receiver].locals[100], "ALGO") algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue // check delete existing source = `int 0 // account @@ -2455,10 +2425,10 @@ app_local_get_ex require.Equal(t, "", vd.Bytes) ledger.reset() - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGO") - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue // check delete and write non-existing source = `int 0 // account @@ -2492,10 +2462,10 @@ app_local_put require.Equal(t, "", vd.Bytes) ledger.reset() - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGO") - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue // check delete and write existing source = `int 0 // account @@ -2525,10 +2495,10 @@ int 1 require.Equal(t, "", vd.Bytes) ledger.reset() - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGO") - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue // check delete,write,delete existing source = `int 0 // account @@ -2561,10 +2531,10 @@ int 1 require.Equal(t, "", vd.Bytes) ledger.reset() - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].locals[100], "ALGO") - ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + ledger.balances[txn.Txn.Sender].locals[100]["ALGO"] = algoValue // check delete, write, delete non-existing source = `int 0 // account @@ -2647,8 +2617,7 @@ func TestEnumFieldErrors(t *testing.T) { Freeze: txn.Txn.Receiver, Clawback: txn.Txn.Receiver, } - ledger.newAsset(55, params) - ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: true}) + ledger.newAsset(txn.Txn.Sender, 55, params) ep.Txn = &txn ep.Ledger = ledger @@ -2726,15 +2695,14 @@ func TestReturnTypes(t *testing.T) { Freeze: txn.Txn.Receiver, Clawback: txn.Txn.Receiver, } - ledger.newAsset(1, params) - ledger.setHolding(txn.Txn.Sender, 1, basics.AssetHolding{Amount: 123, Frozen: true}) - ledger.newApp(txn.Txn.Sender, 1) + ledger.newAsset(txn.Txn.Sender, 1, params) + ledger.newApp(txn.Txn.Sender, 1, makeSchemas(0, 0, 0, 0)) ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 1) - ledger.balances[txn.Txn.Receiver].apps[1] = make(basics.TealKeyValue) + ledger.balances[txn.Txn.Receiver].locals[1] = make(basics.TealKeyValue) key, err := hex.DecodeString("33343536") require.NoError(t, err) algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} - ledger.balances[txn.Txn.Receiver].apps[1][string(key)] = algoValue + ledger.balances[txn.Txn.Receiver].locals[1][string(key)] = algoValue ep.Ledger = ledger @@ -2747,23 +2715,28 @@ func TestReturnTypes(t *testing.T) { "arg": "arg 0", "load": "load 0", "store": "store 0", - "intc": "intcblock 0\nintc 0", - "intc_0": "intcblock 0\nintc_0", - "intc_1": "intcblock 0 0\nintc_1", - "intc_2": "intcblock 0 0 0\nintc_2", - "intc_3": "intcblock 0 0 0 0\nintc_3", - "bytec": "bytecblock 0x32\nbytec 0", - "bytec_0": "bytecblock 0x32\nbytec_0", - "bytec_1": "bytecblock 0x32 0x33\nbytec_1", - "bytec_2": "bytecblock 0x32 0x33 0x34\nbytec_2", - "bytec_3": "bytecblock 0x32 0x33 0x34 0x35\nbytec_3", + "dig": "dig 0", + "intc": "intcblock 0; intc 0", + "intc_0": "intcblock 0; intc_0", + "intc_1": "intcblock 0 0; intc_1", + "intc_2": "intcblock 0 0 0; intc_2", + "intc_3": "intcblock 0 0 0 0; intc_3", + "bytec": "bytecblock 0x32; bytec 0", + "bytec_0": "bytecblock 0x32; bytec_0", + "bytec_1": "bytecblock 0x32 0x33; bytec_1", + "bytec_2": "bytecblock 0x32 0x33 0x34; bytec_2", + "bytec_3": "bytecblock 0x32 0x33 0x34 0x35; bytec_3", "substring": "substring 0 2", - "ed25519verify": "pop\npop\npop\nint 1", // ignore + "ed25519verify": "pop; pop; pop; int 1", // ignore "asset_params_get": "asset_params_get AssetTotal", "asset_holding_get": "asset_holding_get AssetBalance", + "gtxns": "gtxns Sender", + "gtxnsa": "gtxnsa ApplicationArgs 0", + "pushint": "pushint 7272", + "pushbytes": `pushbytes "jojogoodgorilla"`, } - byName := opsByName[LogicVersion] + byName := OpsByName[LogicVersion] for _, m := range []runMode{runModeSignature, runModeApplication} { t.Run(fmt.Sprintf("m=%s", m.String()), func(t *testing.T) { for name, spec := range byName { @@ -2781,8 +2754,7 @@ func TestReturnTypes(t *testing.T) { sb.WriteString(name + "\n") } source := sb.String() - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err) + ops := testProg(t, source, AssemblerMaxVersion) var cx evalContext cx.EvalParams = ep @@ -2875,7 +2847,7 @@ int 42 ledger := makeTestLedger( map[basics.Address]uint64{}, ) - ledger.appID = 42 + ledger.appID = basics.AppIndex(42) ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 3c81783863..1fd7e64950 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -53,6 +53,14 @@ func defaultEvalProtoWithVersion(version uint64) config.ConsensusParams { LogicSigMaxCost: 20000, MaxAppKeyLen: 64, MaxAppBytesValueLen: 64, + // These must be identical to keep an old backward compat test working + MinTxnFee: 1001, + MinBalance: 1001, + // Strange choices below so that we test against conflating them + AppFlatParamsMinBalance: 1002, + SchemaMinBalancePerEntry: 1003, + SchemaUintMinBalance: 1004, + SchemaBytesMinBalance: 1005, } } @@ -200,26 +208,14 @@ func TestWrongProtoVersion(t *testing.T) { } } -func TestTrivialMath(t *testing.T) { +func TestSimpleMath(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 2 -int 3 -+ -int 5 -==`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - var txn transactions.SignedTxn - txn.Lsig.Logic = ops.Program - pass, err := Eval(ops.Program, defaultEvalParams(nil, &txn)) - require.True(t, pass) - require.NoError(t, err) - }) - } + testAccepts(t, "int 2; int 3; + ;int 5;==", 1) + testAccepts(t, "int 22; int 3; - ;int 19;==", 1) + testAccepts(t, "int 8; int 7; * ;int 56;==", 1) + testAccepts(t, "int 21; int 7; / ;int 3;==", 1) + + testPanics(t, "int 1; int 2; - ;int 0; ==", 1) } func TestSha256EqArg(t *testing.T) { @@ -369,122 +365,38 @@ func TestTLHC(t *testing.T) { func TestU64Math(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0x1234567812345678 -int 0x100000000 -/ -int 0x12345678 -==`, v) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.True(t, pass) - require.NoError(t, err) - }) - } + testAccepts(t, "int 0x1234567812345678; int 0x100000000; /; int 0x12345678; ==", 1) } func TestItob(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`byte 0x1234567812345678 -int 0x1234567812345678 -itob -==`, v) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - }) - } + testAccepts(t, "byte 0x1234567812345678; int 0x1234567812345678; itob; ==", 1) } func TestBtoi(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0x1234567812345678 -byte 0x1234567812345678 -btoi -==`, v) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - }) - } + testAccepts(t, "int 0x1234567812345678; byte 0x1234567812345678; btoi; ==", 1) } func TestBtoiTooLong(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0x1234567812345678 -byte 0x1234567812345678aaaa -btoi -==`, v) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) - }) - } + testPanics(t, "int 0x1234567812345678; byte 0x1234567812345678aa; btoi; ==", 1) } func TestBnz(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 1 + testAccepts(t, ` +int 1 dup bnz safe err safe: int 1 -+`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - }) - } -} ++ +`, 1) -func TestBnz2(t *testing.T) { - t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 1 + testAccepts(t, ` +int 1 int 2 int 1 int 2 @@ -498,196 +410,48 @@ planb: after: dup pop -`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - }) - } +`, 1) } -func TestBz(t *testing.T) { +func TestV2Branches(t *testing.T) { t.Parallel() - for v := uint64(2); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0 + testAccepts(t, ` +int 0 dup bz safe err safe: int 1 -+`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - }) - } -} ++ +`, 2) -func TestB(t *testing.T) { - t.Parallel() - for v := uint64(2); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`b safe + testAccepts(t, ` +b safe err safe: -int 1`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - }) - } +int 1 +`, 2) } func TestReturn(t *testing.T) { t.Parallel() - for v := uint64(2); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 1 -return -err`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) - }) - } -} - -func TestReturnFalse(t *testing.T) { - t.Parallel() - for v := uint64(2); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0 -return -int 1`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.False(t, pass) - require.NoError(t, err) - }) - } + testAccepts(t, "int 1; return; err", 2) + testRejects(t, "int 0; return; int 1", 2) } func TestSubUnderflow(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 1 -int 0x100000000 -- -pop -int 1`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) - }) - } + testPanics(t, "int 1; int 10; -; pop; int 1", 1) } func TestAddOverflow(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0xf000000000000000 -int 0x1111111111111111 -+ -pop -int 1`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) - }) - } + testPanics(t, "int 0xf000000000000000; int 0x1111111111111111; +; pop; int 1", 1) } func TestMulOverflow(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0x111111111 -int 0x222222222 -* -pop -int 1`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) - }) - } + testPanics(t, "int 0x111111111; int 0x222222222; *; pop; int 1", 1) } func TestMulwImpl(t *testing.T) { @@ -715,10 +479,8 @@ func TestMulwImpl(t *testing.T) { func TestMulw(t *testing.T) { t.Parallel() - // multiply two numbers, ensure high is 2 and low is 0x468acf130eca8642 - for v := uint64(1); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0x111111111 + testAccepts(t, ` +int 0x111111111 int 0x222222222 mulw int 0x468acf130eca8642 // compare low (top of the stack) @@ -732,21 +494,7 @@ bnz done err done: int 1 // ret 1 -`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.True(t, pass) - require.NoError(t, err) - }) - } +`, 1) } func TestAddwImpl(t *testing.T) { @@ -770,10 +518,8 @@ func TestAddwImpl(t *testing.T) { func TestAddw(t *testing.T) { t.Parallel() - // add two numbers, ensure sum is 0x42 and carry is 0x1 - for v := uint64(2); v <= AssemblerMaxVersion; v++ { - t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 0xFFFFFFFFFFFFFFFF + testAccepts(t, ` +int 0xFFFFFFFFFFFFFFFF int 0x43 addw int 0x42 // compare sum (top of the stack) @@ -787,21 +533,7 @@ bnz done err done: int 1 // ret 1 -`, v) - require.NoError(t, err) - cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.True(t, pass) - require.NoError(t, err) - }) - } +`, 2) } func TestDivZero(t *testing.T) { @@ -1105,8 +837,8 @@ func TestTxnBadField(t *testing.T) { isNotPanic(t, err) // test txn does not accept ApplicationArgs and Accounts - txnOpcode := opsByName[LogicVersion]["txn"].Opcode - txnaOpcode := opsByName[LogicVersion]["txna"].Opcode + txnOpcode := OpsByName[LogicVersion]["txn"].Opcode + txnaOpcode := OpsByName[LogicVersion]["txna"].Opcode fields := []TxnField{ApplicationArgs, Accounts} for _, field := range fields { @@ -1170,8 +902,8 @@ func TestGtxnBadField(t *testing.T) { isNotPanic(t, err) // test gtxn does not accept ApplicationArgs and Accounts - txnOpcode := opsByName[LogicVersion]["txn"].Opcode - txnaOpcode := opsByName[LogicVersion]["txna"].Opcode + txnOpcode := OpsByName[LogicVersion]["txn"].Opcode + txnaOpcode := OpsByName[LogicVersion]["txna"].Opcode fields := []TxnField{ApplicationArgs, Accounts} for _, field := range fields { @@ -1248,7 +980,8 @@ int 9 } } -const globalV1TestProgram = `global MinTxnFee +const globalV1TestProgram = ` +global MinTxnFee int 123 == global MinBalance @@ -1269,9 +1002,12 @@ int 1 && ` -const globalV2TestProgram = `global LogicSigVersion -int 2 -== +const testAddr = "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" + +const globalV2TestProgram = globalV1TestProgram + ` +global LogicSigVersion +int 1 +> && global Round int 0 @@ -1286,6 +1022,12 @@ int 42 == && ` +const globalV3TestProgram = globalV2TestProgram + ` +global CreatorAddress +addr ` + testAddr + ` +== +&& +` func TestGlobal(t *testing.T) { t.Parallel() @@ -1299,16 +1041,19 @@ func TestGlobal(t *testing.T) { 0: {GroupSize, globalV1TestProgram, Eval, Check}, 1: {GroupSize, globalV1TestProgram, Eval, Check}, 2: { - CurrentApplicationID, globalV1TestProgram + globalV2TestProgram, - func(p []byte, ep EvalParams) (bool, error) { - pass, err := EvalStateful(p, ep) - return pass, err - }, - func(program []byte, ep EvalParams) (int, error) { return CheckStateful(program, ep) }, + CurrentApplicationID, globalV2TestProgram, + EvalStateful, CheckStateful, + }, + 3: { + CreatorAddress, globalV3TestProgram, + EvalStateful, CheckStateful, }, } ledger := makeTestLedger(nil) ledger.appID = 42 + addr, err := basics.UnmarshalChecksumAddress(testAddr) + require.NoError(t, err) + ledger.creatorAddr = addr for v := uint64(0); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { last := tests[v].lastField @@ -1320,8 +1065,7 @@ func TestGlobal(t *testing.T) { t.Errorf("TestGlobal missing field %v", globalField) } } - ops, err := AssembleStringWithVersion(testProgram, v) - require.NoError(t, err) + ops := testProg(t, testProgram, v) cost, err := check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) @@ -1551,7 +1295,7 @@ arg 8 && ` -var testTxnProgramText = testTxnProgramTextV1 + `txn ApplicationID +const testTxnProgramTextV2 = testTxnProgramTextV1 + `txn ApplicationID int 123 == && @@ -1655,6 +1399,46 @@ int 1 && ` +const testTxnProgramTextV3 = testTxnProgramTextV2 + ` +assert +txn NumAssets +int 2 +== +assert +txna Assets 0 +int 55 +== +assert +txn NumApplications +int 3 +== +assert +txn Applications 3 // Assembler will use 'txna' +int 111 +== +assert + +txn GlobalNumUint +int 3 +== +assert +txn GlobalNumByteSlice +int 0 +== +assert +txn LocalNumUint +int 1 +== +assert +txn LocalNumByteSlice +int 2 +== +assert + + +int 1 +` + func makeSampleTxn() transactions.SignedTxn { var txn transactions.SignedTxn copy(txn.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) @@ -1716,6 +1500,9 @@ func makeSampleTxn() transactions.SignedTxn { copy(txn.Txn.FreezeAccount[:], freezeAccAddr) txn.Txn.AssetFrozen = true txn.Txn.ForeignAssets = []basics.AssetIndex{55, 77} + txn.Txn.ForeignApps = []basics.AppIndex{56, 78, 111} + txn.Txn.GlobalStateSchema = basics.StateSchema{NumUint: 3, NumByteSlice: 0} + txn.Txn.LocalStateSchema = basics.StateSchema{NumUint: 1, NumByteSlice: 2} return txn } @@ -1734,7 +1521,7 @@ func makeSampleTxnGroup(txn transactions.SignedTxn) []transactions.SignedTxn { func TestTxn(t *testing.T) { t.Parallel() for _, txnField := range TxnFieldNames { - if !strings.Contains(testTxnProgramText, txnField) { + if !strings.Contains(testTxnProgramTextV3, txnField) { if txnField != FirstValidTime.String() { t.Errorf("TestTxn missing field %v", txnField) } @@ -1743,7 +1530,8 @@ func TestTxn(t *testing.T) { tests := map[uint64]string{ 1: testTxnProgramTextV1, - 2: testTxnProgramText, + 2: testTxnProgramTextV2, + 3: testTxnProgramTextV3, } clearOps, err := AssembleStringWithVersion("int 1", 1) @@ -1751,8 +1539,7 @@ func TestTxn(t *testing.T) { for v, source := range tests { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(source, v) - require.NoError(t, err) + ops := testProg(t, source, v) cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) @@ -1946,22 +1733,11 @@ int 1 for v, source := range tests { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(source, v) - require.NoError(t, err) - sb := strings.Builder{} - cost, err := Check(ops.Program, defaultEvalParams(&sb, nil)) - if err != nil { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, cost < 1000) txn := makeSampleTxn() // RekeyTo not allowed in TEAL v1 if v < rekeyingEnabledVersion { txn.Txn.RekeyTo = basics.Address{} } - txn.Lsig.Logic = ops.Program txn.Lsig.Args = [][]byte{ txn.Txn.Sender[:], txn.Txn.Receiver[:], @@ -1970,21 +1746,47 @@ int 1 txn.Txn.SelectionPK[:], txn.Txn.Note, } - txgroup := makeSampleTxnGroup(txn) - sb = strings.Builder{} - ep := defaultEvalParams(&sb, &txn) - ep.TxnGroup = txgroup - pass, err := Eval(ops.Program, ep) - if !pass || err != nil { - t.Log(hex.EncodeToString(ops.Program)) - t.Log(sb.String()) + ep := defaultEvalParams(nil, &txn) + ep.TxnGroup = makeSampleTxnGroup(txn) + testLogic(t, source, v, ep) + if v >= 3 { + gtxnsProg := strings.ReplaceAll(source, "gtxn 0", "int 0; gtxns") + gtxnsProg = strings.ReplaceAll(gtxnsProg, "gtxn 1", "int 1; gtxns") + gtxnsProg = strings.ReplaceAll(gtxnsProg, "gtxna 0", "int 0; gtxnsa") + gtxnsProg = strings.ReplaceAll(gtxnsProg, "gtxna 1", "int 1; gtxnsa") + require.False(t, strings.Contains(gtxnsProg, "gtxn ")) // Got 'em all + require.False(t, strings.Contains(gtxnsProg, "gtxna ")) // Got 'em all + testLogic(t, gtxnsProg, v, ep) } - require.NoError(t, err) - require.True(t, pass) }) } } +func testLogic(t *testing.T, program string, v uint64, ep EvalParams, problems ...string) { + ops := testProg(t, program, v) + sb := &strings.Builder{} + ep.Trace = sb + ep.Txn.Lsig.Logic = ops.Program + cost, err := Check(ops.Program, ep) + if err != nil { + t.Log(hex.EncodeToString(ops.Program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, cost < 1000) + + pass, err := Eval(ops.Program, ep) + if len(problems) == 0 { + require.NoError(t, err, sb.String()) + require.True(t, pass, sb.String()) + } else { + require.Error(t, err, sb.String()) + for _, problem := range problems { + require.Contains(t, err.Error(), problem) + } + } +} + func TestTxna(t *testing.T) { t.Parallel() source := `txna Accounts 1 @@ -2195,7 +1997,7 @@ int 0x310 func TestStringOps(t *testing.T) { t.Parallel() - program, err := assembleStringWithTrace(t, `byte 0x123456789abc + ops := testProg(t, `byte 0x123456789abc substring 1 3 byte 0x3456 == @@ -2222,14 +2024,13 @@ len int 0 == &&`, 2) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) + cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) + pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) if !pass { - t.Log(hex.EncodeToString(program)) + t.Log(hex.EncodeToString(ops.Program)) t.Log(sb.String()) } require.NoError(t, err) @@ -2238,7 +2039,7 @@ int 0 func TestConsOverflow(t *testing.T) { t.Parallel() - program, err := assembleStringWithTrace(t, `byte 0xf000000000000000 + ops := testProg(t, `byte 0xf000000000000000 dup concat dup @@ -2276,14 +2077,13 @@ concat dup concat len`, 2) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) + cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) + pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) if pass { - t.Log(hex.EncodeToString(program)) + t.Log(hex.EncodeToString(ops.Program)) t.Log(sb.String()) } require.False(t, pass) @@ -2294,26 +2094,23 @@ len`, 2) func TestSubstringFlop(t *testing.T) { t.Parallel() // fails in compiler - program, err := assembleStringWithTrace(t, `byte 0xf000000000000000 + ops := testProg(t, `byte 0xf000000000000000 substring 4 2 -len`, 2) - require.Error(t, err) - require.Nil(t, program) +len`, 2, expect{2, "substring end is before start"}) // fails at runtime - program, err = assembleStringWithTrace(t, `byte 0xf000000000000000 + ops = testProg(t, `byte 0xf000000000000000 int 4 int 2 substring3 len`, 2) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) + cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) + pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) if pass { - t.Log(hex.EncodeToString(program)) + t.Log(hex.EncodeToString(ops.Program)) t.Log(sb.String()) } require.False(t, pass) @@ -2321,16 +2118,15 @@ len`, 2) isNotPanic(t, err) // fails at runtime - program, err = assembleStringWithTrace(t, `byte 0xf000000000000000 + ops = testProg(t, `byte 0xf000000000000000 int 4 int 0xFFFFFFFFFFFFFFFE substring3 len`, 2) - require.NoError(t, err) - cost, err = Check(program, defaultEvalParams(nil, nil)) + cost, err = Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) - pass, err = Eval(program, defaultEvalParams(nil, nil)) + pass, err = Eval(ops.Program, defaultEvalParams(nil, nil)) require.False(t, pass) require.Error(t, err) require.Contains(t, err.Error(), "substring range beyond length of string") @@ -2338,17 +2134,16 @@ len`, 2) func TestSubstringRange(t *testing.T) { t.Parallel() - program, err := assembleStringWithTrace(t, `byte 0xf000000000000000 + ops := testProg(t, `byte 0xf000000000000000 substring 2 99 len`, 2) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) + cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) + pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) if pass { - t.Log(hex.EncodeToString(program)) + t.Log(hex.EncodeToString(ops.Program)) t.Log(sb.String()) } require.False(t, pass) @@ -2360,7 +2155,7 @@ func TestLoadStore(t *testing.T) { t.Parallel() for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - ops, err := AssembleStringWithVersion(`int 37 + ops := testProg(t, `int 37 int 37 store 1 byte 0xabbacafe @@ -2375,7 +2170,6 @@ load 0 load 1 + &&`, v) - require.NoError(t, err) cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) @@ -2391,18 +2185,6 @@ load 1 } } -func assembleStringWithTrace(t testing.TB, text string, version uint64) ([]byte, error) { - sr := strings.NewReader(text) - sb := strings.Builder{} - ops := OpStream{Trace: &sb, Version: version} - err := ops.assemble(sr) - if err != nil { - t.Log(sb.String()) - return nil, err - } - return ops.Program, nil -} - func TestLoadStore2(t *testing.T) { t.Parallel() progText := `int 2 @@ -2418,15 +2200,14 @@ int 5 ==` for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - program, err := assembleStringWithTrace(t, progText, v) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) + ops := testProg(t, progText, v) + cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) + pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) if !pass { - t.Log(hex.EncodeToString(program)) + t.Log(hex.EncodeToString(ops.Program)) t.Log(sb.String()) } require.NoError(t, err) @@ -2760,17 +2541,16 @@ int 1 func TestShortProgramTrue(t *testing.T) { t.Parallel() - program, err := assembleStringWithTrace(t, `intcblock 1 + ops := testProg(t, `intcblock 1 intc 0 intc 0 bnz done done:`, 2) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) + cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) require.NoError(t, err) require.True(t, cost < 1000) sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) + pass, err := Eval(ops.Program, defaultEvalParams(&sb, nil)) require.NoError(t, err) require.True(t, pass) } @@ -2853,7 +2633,7 @@ func TestPanic(t *testing.T) { oldSpec = spec opsByOpcode[v][opcode].op = opPanic opsByOpcode[v][opcode].Modes = modeAny - opsByOpcode[v][opcode].opSize.checkFunc = checkPanic + opsByOpcode[v][opcode].Details.checkFunc = checkPanic ops.Program = append(ops.Program, byte(opcode)) break } @@ -3535,7 +3315,7 @@ ed25519verify`, pkStr), AssemblerMaxVersion) func BenchmarkCheckx5(b *testing.B) { sourcePrograms := []string{ tlhcProgramText, - testTxnProgramText, + testTxnProgramTextV3, testCompareProgramText, addBenchmarkSource, addBenchmark2Source, @@ -3616,7 +3396,7 @@ intc_0 ep := defaultEvalParams(nil, nil) - origSpec := opsByName[LogicVersion]["+"] + origSpec := OpsByName[LogicVersion]["+"] spec := origSpec defer func() { // restore, opsByOpcode is global @@ -3665,24 +3445,19 @@ int 1 ` ep := defaultEvalParams(nil, nil) - ops, err := AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) + ops := testProg(t, text, AssemblerMaxVersion) pass, err := Eval(ops.Program, ep) require.NoError(t, err) require.True(t, pass) - text = `dup2` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) + text = `int 1; int 2; dup2; pop; pop; pop` + ops = testProg(t, text, AssemblerMaxVersion) pass, err = Eval(ops.Program, ep) - require.Error(t, err) - require.False(t, pass) - - text = `int 1 -dup2 -` - ops, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) + require.True(t, pass) + + text = `int 1; int 2; dup2; pop; pop` + ops = testProg(t, text, AssemblerMaxVersion) pass, err = Eval(ops.Program, ep) require.Error(t, err) require.False(t, pass) @@ -3878,24 +3653,24 @@ func TestAllowedOpcodesV2(t *testing.T) { tests := map[string]string{ "txna": "txna Accounts 0", "gtxna": "gtxna 0 ApplicationArgs 0", - "bz": "bz l\nl:", - "b": "b l\nl:", - "return": "int 1\nreturn", - "addw": "int 0\nint 1\naddw", - "dup2": "dup2", - "concat": "byte 0x41\ndup\nconcat", - "substring": "byte 0x41\nsubstring 0 1", - "substring3": "byte 0x41\ndup\ndup\nsubstring3", - "balance": "int 1\nbalance", - "app_opted_in": "int 0\ndup\napp_opted_in", - "app_local_get": "int 0\nbyte 0x41\napp_local_get", - "app_local_get_ex": "int 0\ndup\nbyte 0x41\napp_local_get_ex", - "app_global_get": "int 0\nbyte 0x41\napp_global_get", - "app_global_get_ex": "int 0\nbyte 0x41\napp_global_get_ex", - "app_local_put": "int 0\ndup\nbyte 0x41\napp_local_put", - "app_global_put": "byte 0x41\ndup\napp_global_put", - "app_local_del": "int 0\nbyte 0x41\napp_local_del", - "app_global_del": "byte 0x41\napp_global_del", + "bz": "int 0; bz l; l:", + "b": "b l; l:", + "return": "int 1; return", + "addw": "int 0; int 1; addw", + "dup2": "int 1; int 2; dup2", + "concat": "byte 0x41; dup; concat", + "substring": "byte 0x41; substring 0 1", + "substring3": "byte 0x41; dup; dup; substring3", + "balance": "int 1; balance", + "app_opted_in": "int 0; dup; app_opted_in", + "app_local_get": "int 0; byte 0x41; app_local_get", + "app_local_get_ex": "int 0; dup; byte 0x41; app_local_get_ex", + "app_global_get": "int 0; byte 0x41; app_global_get", + "app_global_get_ex": "int 0; byte 0x41; app_global_get_ex", + "app_local_put": "int 0; dup; byte 0x41; app_local_put", + "app_global_put": "byte 0x41; dup; app_global_put", + "app_local_del": "int 0; byte 0x41; app_local_del", + "app_global_del": "byte 0x41; app_global_del", "asset_holding_get": "asset_holding_get AssetBalance", "asset_params_get": "asset_params_get AssetTotal", } @@ -3912,17 +3687,17 @@ func TestAllowedOpcodesV2(t *testing.T) { cnt := 0 for _, spec := range OpSpecs { - if spec.Version > 1 && !excluded[spec.Name] { + if spec.Version == 2 && !excluded[spec.Name] { source, ok := tests[spec.Name] - require.True(t, ok, fmt.Sprintf("Missed opcode in the test: %s", spec.Name)) - ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion) - require.NoError(t, err, source) + require.True(t, ok, "Missed opcode in the test: %s", spec.Name) + require.Contains(t, source, spec.Name) + ops := testProg(t, source, AssemblerMaxVersion) // all opcodes allowed in stateful mode so use CheckStateful/EvalStateful - _, err = CheckStateful(ops.Program, ep) + _, err := CheckStateful(ops.Program, ep) require.NoError(t, err, source) _, err = EvalStateful(ops.Program, ep) if spec.Name != "return" { - // "return" opcode is always succeed so ignore it + // "return" opcode always succeeds so ignore it require.Error(t, err, source) require.NotContains(t, err.Error(), "illegal opcode") } @@ -3948,6 +3723,66 @@ func TestAllowedOpcodesV2(t *testing.T) { require.Equal(t, len(tests), cnt) } +// check all v3 opcodes: allowed in v3 and not allowed before +func TestAllowedOpcodesV3(t *testing.T) { + t.Parallel() + + // all tests are expected to fail in evaluation + tests := map[string]string{ + "assert": "int 1; assert", + "min_balance": "int 1; min_balance", + "getbit": "int 15; int 64; getbit", + "setbit": "int 15; int 64; int 0; setbit", + "getbyte": "byte \"john\"; int 5; getbyte", + "setbyte": "byte \"john\"; int 5; int 66; setbyte", + "swap": "int 1; byte \"x\"; swap", + "select": "int 1; byte \"x\"; int 1; select", + "dig": "int 1; int 1; dig 1", + "gtxns": "int 0; gtxns FirstValid", + "gtxnsa": "int 0; gtxnsa Accounts 0", + "pushint": "pushint 7; pushint 4", + "pushbytes": `pushbytes "stringsfail?"`, + } + + excluded := map[string]bool{} + + ep := defaultEvalParams(nil, nil) + + cnt := 0 + for _, spec := range OpSpecs { + if spec.Version == 3 && !excluded[spec.Name] { + source, ok := tests[spec.Name] + require.True(t, ok, "Missed opcode in the test: %s", spec.Name) + require.Contains(t, source, spec.Name) + ops := testProg(t, source, AssemblerMaxVersion) + // all opcodes allowed in stateful mode so use CheckStateful/EvalStateful + _, err := CheckStateful(ops.Program, ep) + require.NoError(t, err, source) + _, err = EvalStateful(ops.Program, ep) + require.Error(t, err, source) + require.NotContains(t, err.Error(), "illegal opcode") + + for v := byte(0); v <= 1; v++ { + ops.Program[0] = v + _, err = Check(ops.Program, ep) + require.Error(t, err, source) + require.Contains(t, err.Error(), "illegal opcode") + _, err = CheckStateful(ops.Program, ep) + require.Error(t, err, source) + require.Contains(t, err.Error(), "illegal opcode") + _, err = Eval(ops.Program, ep) + require.Error(t, err, source) + require.Contains(t, err.Error(), "illegal opcode") + _, err = EvalStateful(ops.Program, ep) + require.Error(t, err, source) + require.Contains(t, err.Error(), "illegal opcode") + } + cnt++ + } + } + require.Equal(t, len(tests), cnt) +} + func TestRekeyFailsOnOldVersion(t *testing.T) { t.Parallel() for v := uint64(0); v < rekeyingEnabledVersion; v++ { @@ -3972,3 +3807,161 @@ func TestRekeyFailsOnOldVersion(t *testing.T) { }) } } + +func obfuscate(program string) string { + // Put a prefix on the program that does nothing interesting, + // but prevents assembly from detecting type errors. Allows + // evaluation testing of a program that would be rejected by + // assembler. + if strings.Contains(program, "obfuscate") { + return program // Already done. Tests sometimes use at multiple levels + } + return "int 0;bnz obfuscate;obfuscate:;" + program +} + +type evalTester func(pass bool, err error) bool + +func testEvaluation(t *testing.T, program string, introduced uint64, tester evalTester) { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + if v < introduced { + testProg(t, obfuscate(program), v, expect{0, "...opcode was introduced..."}) + return + } + ops := testProg(t, program, v) + cost, err := Check(ops.Program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + var txn transactions.SignedTxn + txn.Lsig.Logic = ops.Program + sb := strings.Builder{} + pass, err := Eval(ops.Program, defaultEvalParams(&sb, &txn)) + ok := tester(pass, err) + if !ok { + t.Log(hex.EncodeToString(ops.Program)) + t.Log(sb.String()) + } + require.True(t, ok) + isNotPanic(t, err) // Never want a Go level panic. + }) + } +} + +func testAccepts(t *testing.T, program string, introduced uint64) { + testEvaluation(t, program, introduced, func(pass bool, err error) bool { + return pass && err == nil + }) +} +func testRejects(t *testing.T, program string, introduced uint64) { + testEvaluation(t, program, introduced, func(pass bool, err error) bool { + // Returned False, but didn't panic + return !pass && err == nil + }) +} +func testPanics(t *testing.T, program string, introduced uint64) { + testEvaluation(t, program, introduced, func(pass bool, err error) bool { + // TEAL panic! not just reject at exit + return !pass && err != nil + }) +} + +func TestAssert(t *testing.T) { + t.Parallel() + testAccepts(t, "int 1;assert;int 1", 3) + testRejects(t, "int 1;assert;int 0", 3) + testPanics(t, "int 0;assert;int 1", 3) + testPanics(t, obfuscate("assert;int 1"), 3) + testPanics(t, obfuscate(`byte "john";assert;int 1`), 3) +} + +func TestBits(t *testing.T) { + t.Parallel() + testAccepts(t, "int 1; int 0; getbit; int 1; ==", 3) + testAccepts(t, "int 1; int 1; getbit; int 0; ==", 3) + + testAccepts(t, "int 1; int 63; getbit; int 0; ==", 3) + testPanics(t, "int 1; int 64; getbit; int 0; ==", 3) + + testAccepts(t, "int 0; int 3; int 1; setbit; int 8; ==", 3) + testAccepts(t, "int 8; int 3; getbit; int 1; ==", 3) + + testAccepts(t, "int 15; int 3; int 0; setbit; int 7; ==", 3) + + // bit 10 is the 3rd bit (from the high end) in the second byte + testAccepts(t, "byte 0xfff0; int 10; getbit; int 1; ==", 3) + testAccepts(t, "byte 0xfff0; int 12; getbit; int 0; ==", 3) + testPanics(t, "byte 0xfff0; int 16; getbit; int 0; ==", 3) + + testAccepts(t, "byte 0xfffff0; int 21; int 1; setbit; byte 0xfffff4; ==", 3) + testAccepts(t, "byte 0xfffff4; int 1; int 0; setbit; byte 0xbffff4; ==", 3) + testPanics(t, "byte 0xfffff4; int 24; int 0; setbit; byte 0xbf; ==", 3) + + testAccepts(t, "byte 0x0000; int 3; int 1; setbit; byte 0x1000; ==", 3) + testAccepts(t, "byte 0x0000; int 15; int 1; setbit; byte 0x0001; ==", 3) + testAccepts(t, "int 0x0000; int 3; int 1; setbit; int 0x0008; ==", 3) + testAccepts(t, "int 0x0000; int 12; int 1; setbit; int 0x1000; ==", 3) +} + +func TestBytes(t *testing.T) { + t.Parallel() + testAccepts(t, "byte 0x12345678; int 2; getbyte; int 0x56; ==", 3) + testPanics(t, "byte 0x12345678; int 4; getbyte; int 0x56; ==", 3) + + testAccepts(t, `byte "john"; int 0; getbyte; int 106; ==`, 3) // ascii j + testAccepts(t, `byte "john"; int 1; getbyte; int 111; ==`, 3) // ascii o + testAccepts(t, `byte "john"; int 2; getbyte; int 104; ==`, 3) // ascii h + testAccepts(t, `byte "john"; int 3; getbyte; int 110; ==`, 3) // ascii n + testPanics(t, `byte "john"; int 4; getbyte; int 1; ==`, 3) // past end + + testAccepts(t, `byte "john"; int 2; int 105; setbyte; byte "join"; ==`, 3) + // dup makes copies, modifying one does not change the other + testAccepts(t, `byte "john"; dup; int 2; int 105; setbyte; pop; byte "john"; ==`, 3) +} + +func TestSwap(t *testing.T) { + t.Parallel() + testAccepts(t, "int 1; byte 0x1234; swap; int 1; ==; assert; byte 0x1234; ==", 3) +} + +func TestSelect(t *testing.T) { + t.Parallel() + + testAccepts(t, "int 1; byte 0x1231; int 0; select", 3) // selects the 1 + testRejects(t, "int 0; byte 0x1232; int 0; select", 3) // selects the 0 + + testAccepts(t, "int 0; int 1; int 1; select", 3) // selects the 1 + testPanics(t, "int 1; byte 0x1233; int 1; select", 3) // selects the bytes +} + +func TestDig(t *testing.T) { + t.Parallel() + testAccepts(t, "int 3; int 2; int 1; dig 1; int 2; ==; return", 3) + testPanics(t, "int 3; int 2; int 1; dig 11; int 2; ==; return", 3) +} + +func TestPush(t *testing.T) { + t.Parallel() + testAccepts(t, "int 2; pushint 2; ==", 3) + testAccepts(t, "pushbytes 0x1234; byte 0x1234; ==", 3) + + // There's a savings to be had if the intcblock is entirely avoided + ops1 := testProg(t, "int 1", 3) + ops2 := testProg(t, "pushint 1", 3) + require.Less(t, len(ops2.Program), len(ops1.Program)) + + // There's no savings to be had if the pushint replaces a + // reference to one of the arg{0-3} opcodes, since they only + // use one byte. And the intcblock only grows by the varuint + // encoding size of the pushedint. Which is the same either + // way. + + ops1 = testProg(t, "int 2; int 1", 3) + ops2 = testProg(t, "int 2; pushint 1", 3) + require.Equal(t, len(ops2.Program), len(ops1.Program)) + + // There's a savings to be had when intcblock > 4 elements, + // because references beyong arg 3 require two byte. + ops1 = testProg(t, "int 2; int 3; int 5; int 6; int 1", 3) + ops2 = testProg(t, "int 2; int 3; int 5; int 6; pushint 1", 3) + require.Less(t, len(ops2.Program), len(ops1.Program)) +} diff --git a/data/transactions/logic/fields.go b/data/transactions/logic/fields.go index 78439daba0..eb3b956f19 100644 --- a/data/transactions/logic/fields.go +++ b/data/transactions/logic/fields.go @@ -17,6 +17,8 @@ package logic import ( + "fmt" + "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" ) @@ -79,7 +81,7 @@ const ( ApplicationID // OnCompletion OnCompletion OnCompletion - // ApplicationArgs []basics.TealValue + // ApplicationArgs [][]byte ApplicationArgs // NumAppArgs len(ApplicationArgs) NumAppArgs @@ -123,6 +125,23 @@ const ( FreezeAssetAccount // FreezeAssetFrozen bool FreezeAssetFrozen + // Assets []basics.AssetIndex + Assets + // NumAssets len(ForeignAssets) + NumAssets + // Applications []basics.AppIndex + Applications + // NumApplications len(ForeignApps) + NumApplications + + // GlobalNumUint uint64 + GlobalNumUint + // GlobalNumByteSlice uint64 + GlobalNumByteSlice + // LocalNumUint uint64 + LocalNumUint + // LocalNumByteSlice uint64 + LocalNumByteSlice invalidTxnField // fence for some setup that loops from Sender..invalidTxnField ) @@ -141,7 +160,7 @@ type tfNameSpecMap map[string]txnFieldSpec func (s tfNameSpecMap) getExtraFor(name string) (extra string) { if s[name].version > 1 { - extra = "LogicSigVersion >= 2." + extra = fmt.Sprintf("LogicSigVersion >= %d.", s[name].version) } return } @@ -201,6 +220,14 @@ var txnFieldSpecs = []txnFieldSpec{ {FreezeAsset, StackUint64, 2}, {FreezeAssetAccount, StackBytes, 2}, {FreezeAssetFrozen, StackUint64, 2}, + {Assets, StackUint64, 3}, + {NumAssets, StackUint64, 3}, + {Applications, StackUint64, 3}, + {NumApplications, StackUint64, 3}, + {GlobalNumUint, StackUint64, 3}, + {GlobalNumByteSlice, StackUint64, 3}, + {LocalNumUint, StackUint64, 3}, + {LocalNumByteSlice, StackUint64, 3}, } // TxnaFieldNames are arguments to the 'txna' opcode @@ -211,11 +238,15 @@ var TxnaFieldNames = []string{ApplicationArgs.String(), Accounts.String()} var TxnaFieldTypes = []StackType{ txnaFieldSpecByField[ApplicationArgs].ftype, txnaFieldSpecByField[Accounts].ftype, + txnaFieldSpecByField[Assets].ftype, + txnaFieldSpecByField[Applications].ftype, } var txnaFieldSpecByField = map[TxnField]txnFieldSpec{ ApplicationArgs: {ApplicationArgs, StackBytes, 2}, Accounts: {Accounts, StackBytes, 2}, + Assets: {Assets, StackUint64, 3}, + Applications: {Applications, StackUint64, 3}, } // TxnTypeNames is the values of Txn.Type in enum order @@ -275,6 +306,9 @@ const ( ZeroAddress // GroupSize len(txn group) GroupSize + + // v2 + // LogicSigVersion ConsensusParams.LogicSigVersion LogicSigVersion // Round basics.Round @@ -284,6 +318,11 @@ const ( // CurrentApplicationID uint64 CurrentApplicationID + // v3 + + // CreatorAddress [32]byte + CreatorAddress + invalidGlobalField ) @@ -310,6 +349,7 @@ var globalFieldSpecs = []globalFieldSpec{ {Round, StackUint64, runModeApplication, 2}, {LatestTimestamp, StackUint64, runModeApplication, 2}, {CurrentApplicationID, StackUint64, runModeApplication, 2}, + {CreatorAddress, StackBytes, runModeApplication, 3}, } // GlobalFieldSpecByField maps GlobalField to spec @@ -321,7 +361,7 @@ type gfNameSpecMap map[string]globalFieldSpec func (s gfNameSpecMap) getExtraFor(name string) (extra string) { if s[name].version > 1 { - extra = "LogicSigVersion >= 2." + extra = fmt.Sprintf("LogicSigVersion >= %d.", s[name].version) } return } diff --git a/data/transactions/logic/fields_string.go b/data/transactions/logic/fields_string.go index 046b5a55a9..c06df6944e 100644 --- a/data/transactions/logic/fields_string.go +++ b/data/transactions/logic/fields_string.go @@ -56,12 +56,20 @@ func _() { _ = x[FreezeAsset-45] _ = x[FreezeAssetAccount-46] _ = x[FreezeAssetFrozen-47] - _ = x[invalidTxnField-48] + _ = x[Assets-48] + _ = x[NumAssets-49] + _ = x[Applications-50] + _ = x[NumApplications-51] + _ = x[GlobalNumUint-52] + _ = x[GlobalNumByteSlice-53] + _ = x[LocalNumUint-54] + _ = x[LocalNumByteSlice-55] + _ = x[invalidTxnField-56] } -const _TxnField_name = "SenderFeeFirstValidFirstValidTimeLastValidNoteLeaseReceiverAmountCloseRemainderToVotePKSelectionPKVoteFirstVoteLastVoteKeyDilutionTypeTypeEnumXferAssetAssetAmountAssetSenderAssetReceiverAssetCloseToGroupIndexTxIDApplicationIDOnCompletionApplicationArgsNumAppArgsAccountsNumAccountsApprovalProgramClearStateProgramRekeyToConfigAssetConfigAssetTotalConfigAssetDecimalsConfigAssetDefaultFrozenConfigAssetUnitNameConfigAssetNameConfigAssetURLConfigAssetMetadataHashConfigAssetManagerConfigAssetReserveConfigAssetFreezeConfigAssetClawbackFreezeAssetFreezeAssetAccountFreezeAssetFrozeninvalidTxnField" +const _TxnField_name = "SenderFeeFirstValidFirstValidTimeLastValidNoteLeaseReceiverAmountCloseRemainderToVotePKSelectionPKVoteFirstVoteLastVoteKeyDilutionTypeTypeEnumXferAssetAssetAmountAssetSenderAssetReceiverAssetCloseToGroupIndexTxIDApplicationIDOnCompletionApplicationArgsNumAppArgsAccountsNumAccountsApprovalProgramClearStateProgramRekeyToConfigAssetConfigAssetTotalConfigAssetDecimalsConfigAssetDefaultFrozenConfigAssetUnitNameConfigAssetNameConfigAssetURLConfigAssetMetadataHashConfigAssetManagerConfigAssetReserveConfigAssetFreezeConfigAssetClawbackFreezeAssetFreezeAssetAccountFreezeAssetFrozenAssetsNumAssetsApplicationsNumApplicationsGlobalNumUintGlobalNumByteSliceLocalNumUintLocalNumByteSliceinvalidTxnField" -var _TxnField_index = [...]uint16{0, 6, 9, 19, 33, 42, 46, 51, 59, 65, 81, 87, 98, 107, 115, 130, 134, 142, 151, 162, 173, 186, 198, 208, 212, 225, 237, 252, 262, 270, 281, 296, 313, 320, 331, 347, 366, 390, 409, 424, 438, 461, 479, 497, 514, 533, 544, 562, 579, 594} +var _TxnField_index = [...]uint16{0, 6, 9, 19, 33, 42, 46, 51, 59, 65, 81, 87, 98, 107, 115, 130, 134, 142, 151, 162, 173, 186, 198, 208, 212, 225, 237, 252, 262, 270, 281, 296, 313, 320, 331, 347, 366, 390, 409, 424, 438, 461, 479, 497, 514, 533, 544, 562, 579, 585, 594, 606, 621, 634, 652, 664, 681, 696} func (i TxnField) String() string { if i < 0 || i >= TxnField(len(_TxnField_index)-1) { @@ -82,15 +90,16 @@ func _() { _ = x[Round-6] _ = x[LatestTimestamp-7] _ = x[CurrentApplicationID-8] - _ = x[invalidGlobalField-9] + _ = x[CreatorAddress-9] + _ = x[invalidGlobalField-10] } -const _GlobalField_name = "MinTxnFeeMinBalanceMaxTxnLifeZeroAddressGroupSizeLogicSigVersionRoundLatestTimestampCurrentApplicationIDinvalidGlobalField" +const _GlobalField_name = "MinTxnFeeMinBalanceMaxTxnLifeZeroAddressGroupSizeLogicSigVersionRoundLatestTimestampCurrentApplicationIDCreatorAddressinvalidGlobalField" -var _GlobalField_index = [...]uint8{0, 9, 19, 29, 40, 49, 64, 69, 84, 104, 122} +var _GlobalField_index = [...]uint8{0, 9, 19, 29, 40, 49, 64, 69, 84, 104, 118, 136} func (i GlobalField) String() string { - if i < 0 || i >= GlobalField(len(_GlobalField_index)-1) { + if i >= GlobalField(len(_GlobalField_index)-1) { return "GlobalField(" + strconv.FormatInt(int64(i), 10) + ")" } return _GlobalField_name[_GlobalField_index[i]:_GlobalField_index[i+1]] diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 3f8ae41f8c..8748b42256 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -21,7 +21,7 @@ import ( ) // LogicVersion defines default assembler and max eval versions -const LogicVersion = 2 +const LogicVersion = 3 // rekeyingEnabledVersion is the version of TEAL where RekeyTo functionality // was enabled. This is important to remember so that old TEAL accounts cannot @@ -33,16 +33,54 @@ const rekeyingEnabledVersion = 2 // from being used with applications. Do not edit! const appsEnabledVersion = 2 -// opSize records the length in bytes for an op that is constant-length but not length 1 -type opSize struct { - cost int - size int - checkFunc opCheckFunc +// opDetails records details such as non-standard costs, immediate +// arguments, or dynamic layout controlled by a check function. +type opDetails struct { + Cost int + Size int + checkFunc opCheckFunc + Immediates []immediate } -var opSizeDefault = opSize{1, 1, nil} +var opDefault = opDetails{1, 1, nil, nil} +var opBranch = opDetails{1, 3, checkBranch, []immediate{{"target", immLabel}}} -// OpSpec defines one byte opcode +func costly(cost int) opDetails { + return opDetails{cost, 1, nil, nil} +} + +func immediates(name string, rest ...string) opDetails { + num := 1 + len(rest) + immediates := make([]immediate, num, len(rest)+1) + immediates[0] = immediate{name, immByte} + for i, n := range rest { + immediates[i+1] = immediate{n, immByte} + } + return opDetails{1, 1 + num, nil, immediates} +} + +func varies(checker opCheckFunc, name string, kind immKind) opDetails { + return opDetails{1, 0, checker, []immediate{{name, kind}}} +} + +// immType describes the immediate arguments to an opcode +type immKind byte + +const ( + immByte immKind = iota + immLabel + immInt + immBytes + immInts + immBytess // "ss" not a typo. Multiple "bytes" +) + +type immediate struct { + Name string + kind immKind +} + +// OpSpec defines an opcode type OpSpec struct { Opcode byte Name string @@ -53,17 +91,21 @@ type OpSpec struct { Returns StackTypes // what gets pushed to the stack Version uint64 // TEAL version opcode introduced Modes runMode // if non-zero, then (mode & Modes) != 0 to allow - opSize opSize // opSizes records the size of ops that are constant size but not 1, time 'cost' and custom check functions. + Details opDetails // Special cost or bytecode layout considerations } var oneBytes = StackTypes{StackBytes} var twoBytes = StackTypes{StackBytes, StackBytes} var threeBytes = StackTypes{StackBytes, StackBytes, StackBytes} +var byteInt = StackTypes{StackBytes, StackUint64} var byteIntInt = StackTypes{StackBytes, StackUint64, StackUint64} var oneInt = StackTypes{StackUint64} var twoInts = StackTypes{StackUint64, StackUint64} +var threeInts = StackTypes{StackUint64, StackUint64, StackUint64} var oneAny = StackTypes{StackAny} var twoAny = StackTypes{StackAny, StackAny} +var anyInt = StackTypes{StackAny, StackUint64} +var anyIntInt = StackTypes{StackAny, StackUint64, StackUint64} // OpSpecs is the table of operations that can be assembled and evaluated. // @@ -71,99 +113,118 @@ var twoAny = StackTypes{StackAny, StackAny} // // WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() var OpSpecs = []OpSpec{ - {0x00, "err", opErr, asmDefault, disDefault, nil, nil, 1, modeAny, opSizeDefault}, - {0x01, "sha256", opSHA256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, opSize{7, 1, nil}}, - {0x02, "keccak256", opKeccak256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, opSize{26, 1, nil}}, - {0x03, "sha512_256", opSHA512_256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, opSize{9, 1, nil}}, + {0x00, "err", opErr, asmDefault, disDefault, nil, nil, 1, modeAny, opDefault}, + {0x01, "sha256", opSHA256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, costly(7)}, + {0x02, "keccak256", opKeccak256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, costly(26)}, + {0x03, "sha512_256", opSHA512_256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, costly(9)}, // Cost of these opcodes increases in TEAL version 2 based on measured // performance. Should be able to run max hashes during stateful TEAL // and achieve reasonable TPS. Same opcode for different TEAL versions // is OK. - {0x01, "sha256", opSHA256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, opSize{35, 1, nil}}, - {0x02, "keccak256", opKeccak256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, opSize{130, 1, nil}}, - {0x03, "sha512_256", opSHA512_256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, opSize{45, 1, nil}}, - - {0x04, "ed25519verify", opEd25519verify, asmDefault, disDefault, threeBytes, oneInt, 1, runModeSignature, opSize{1900, 1, nil}}, - {0x08, "+", opPlus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x09, "-", opMinus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x0a, "/", opDiv, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x0b, "*", opMul, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x0c, "<", opLt, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x0d, ">", opGt, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x0e, "<=", opLe, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x0f, ">=", opGe, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x10, "&&", opAnd, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x11, "||", opOr, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x12, "==", opEq, asmDefault, disDefault, twoAny, oneInt, 1, modeAny, opSizeDefault}, - {0x13, "!=", opNeq, asmDefault, disDefault, twoAny, oneInt, 1, modeAny, opSizeDefault}, - {0x14, "!", opNot, asmDefault, disDefault, oneInt, oneInt, 1, modeAny, opSizeDefault}, - {0x15, "len", opLen, asmDefault, disDefault, oneBytes, oneInt, 1, modeAny, opSizeDefault}, - {0x16, "itob", opItob, asmDefault, disDefault, oneInt, oneBytes, 1, modeAny, opSizeDefault}, - {0x17, "btoi", opBtoi, asmDefault, disDefault, oneBytes, oneInt, 1, modeAny, opSizeDefault}, - {0x18, "%", opModulo, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x19, "|", opBitOr, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x1a, "&", opBitAnd, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x1b, "^", opBitXor, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, - {0x1c, "~", opBitNot, asmDefault, disDefault, oneInt, oneInt, 1, modeAny, opSizeDefault}, - {0x1d, "mulw", opMulw, asmDefault, disDefault, twoInts, twoInts, 1, modeAny, opSizeDefault}, - {0x1e, "addw", opAddw, asmDefault, disDefault, twoInts, twoInts, 2, modeAny, opSizeDefault}, - - {0x20, "intcblock", opIntConstBlock, assembleIntCBlock, disIntcblock, nil, nil, 1, modeAny, opSize{1, 0, checkIntConstBlock}}, - {0x21, "intc", opIntConstLoad, assembleIntC, disIntc, nil, oneInt, 1, modeAny, opSize{1, 2, nil}}, - {0x22, "intc_0", opIntConst0, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, - {0x23, "intc_1", opIntConst1, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, - {0x24, "intc_2", opIntConst2, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, - {0x25, "intc_3", opIntConst3, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, - {0x26, "bytecblock", opByteConstBlock, assembleByteCBlock, disBytecblock, nil, nil, 1, modeAny, opSize{1, 0, checkByteConstBlock}}, - {0x27, "bytec", opByteConstLoad, assembleByteC, disBytec, nil, oneBytes, 1, modeAny, opSize{1, 2, nil}}, - {0x28, "bytec_0", opByteConst0, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, - {0x29, "bytec_1", opByteConst1, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, - {0x2a, "bytec_2", opByteConst2, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, - {0x2b, "bytec_3", opByteConst3, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, - {0x2c, "arg", opArg, assembleArg, disArg, nil, oneBytes, 1, runModeSignature, opSize{1, 2, nil}}, - {0x2d, "arg_0", opArg0, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, - {0x2e, "arg_1", opArg1, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, - {0x2f, "arg_2", opArg2, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, - {0x30, "arg_3", opArg3, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, - {0x31, "txn", opTxn, assembleTxn, disTxn, nil, oneAny, 1, modeAny, opSize{1, 2, nil}}, + {0x01, "sha256", opSHA256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, costly(35)}, + {0x02, "keccak256", opKeccak256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, costly(130)}, + {0x03, "sha512_256", opSHA512_256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, costly(45)}, + + {0x04, "ed25519verify", opEd25519verify, asmDefault, disDefault, threeBytes, oneInt, 1, runModeSignature, costly(1900)}, + {0x08, "+", opPlus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x09, "-", opMinus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x0a, "/", opDiv, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x0b, "*", opMul, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x0c, "<", opLt, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x0d, ">", opGt, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x0e, "<=", opLe, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x0f, ">=", opGe, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x10, "&&", opAnd, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x11, "||", opOr, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x12, "==", opEq, asmDefault, disDefault, twoAny, oneInt, 1, modeAny, opDefault}, + {0x13, "!=", opNeq, asmDefault, disDefault, twoAny, oneInt, 1, modeAny, opDefault}, + {0x14, "!", opNot, asmDefault, disDefault, oneInt, oneInt, 1, modeAny, opDefault}, + {0x15, "len", opLen, asmDefault, disDefault, oneBytes, oneInt, 1, modeAny, opDefault}, + {0x16, "itob", opItob, asmDefault, disDefault, oneInt, oneBytes, 1, modeAny, opDefault}, + {0x17, "btoi", opBtoi, asmDefault, disDefault, oneBytes, oneInt, 1, modeAny, opDefault}, + {0x18, "%", opModulo, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x19, "|", opBitOr, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x1a, "&", opBitAnd, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x1b, "^", opBitXor, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opDefault}, + {0x1c, "~", opBitNot, asmDefault, disDefault, oneInt, oneInt, 1, modeAny, opDefault}, + {0x1d, "mulw", opMulw, asmDefault, disDefault, twoInts, twoInts, 1, modeAny, opDefault}, + {0x1e, "addw", opAddw, asmDefault, disDefault, twoInts, twoInts, 2, modeAny, opDefault}, + + {0x20, "intcblock", opIntConstBlock, assembleIntCBlock, disIntcblock, nil, nil, 1, modeAny, varies(checkIntConstBlock, "uint ...", immInts)}, + {0x21, "intc", opIntConstLoad, assembleIntC, disDefault, nil, oneInt, 1, modeAny, immediates("i")}, + {0x22, "intc_0", opIntConst0, asmDefault, disDefault, nil, oneInt, 1, modeAny, opDefault}, + {0x23, "intc_1", opIntConst1, asmDefault, disDefault, nil, oneInt, 1, modeAny, opDefault}, + {0x24, "intc_2", opIntConst2, asmDefault, disDefault, nil, oneInt, 1, modeAny, opDefault}, + {0x25, "intc_3", opIntConst3, asmDefault, disDefault, nil, oneInt, 1, modeAny, opDefault}, + {0x26, "bytecblock", opByteConstBlock, assembleByteCBlock, disBytecblock, nil, nil, 1, modeAny, varies(checkByteConstBlock, "bytes ...", immBytess)}, + {0x27, "bytec", opByteConstLoad, assembleByteC, disDefault, nil, oneBytes, 1, modeAny, immediates("i")}, + {0x28, "bytec_0", opByteConst0, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opDefault}, + {0x29, "bytec_1", opByteConst1, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opDefault}, + {0x2a, "bytec_2", opByteConst2, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opDefault}, + {0x2b, "bytec_3", opByteConst3, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opDefault}, + {0x2c, "arg", opArg, assembleArg, disDefault, nil, oneBytes, 1, runModeSignature, immediates("n")}, + {0x2d, "arg_0", opArg0, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opDefault}, + {0x2e, "arg_1", opArg1, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opDefault}, + {0x2f, "arg_2", opArg2, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opDefault}, + {0x30, "arg_3", opArg3, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opDefault}, + {0x31, "txn", opTxn, assembleTxn, disTxn, nil, oneAny, 1, modeAny, immediates("f")}, // It is ok to have the same opcode for different TEAL versions. // This 'txn' asm command supports additional argument in version 2 and // generates 'txna' opcode in that particular case - {0x31, "txn", opTxn, assembleTxn2, disTxn, nil, oneAny, 2, modeAny, opSize{1, 2, nil}}, - {0x32, "global", opGlobal, assembleGlobal, disGlobal, nil, oneAny, 1, modeAny, opSize{1, 2, nil}}, - {0x33, "gtxn", opGtxn, assembleGtxn, disGtxn, nil, oneAny, 1, modeAny, opSize{1, 3, nil}}, - {0x33, "gtxn", opGtxn, assembleGtxn2, disGtxn, nil, oneAny, 2, modeAny, opSize{1, 3, nil}}, - {0x34, "load", opLoad, assembleLoad, disLoad, nil, oneAny, 1, modeAny, opSize{1, 2, nil}}, - {0x35, "store", opStore, assembleStore, disStore, oneAny, nil, 1, modeAny, opSize{1, 2, nil}}, - {0x36, "txna", opTxna, assembleTxna, disTxna, nil, oneAny, 2, modeAny, opSize{1, 3, nil}}, - {0x37, "gtxna", opGtxna, assembleGtxna, disGtxna, nil, oneAny, 2, modeAny, opSize{1, 4, nil}}, - - {0x40, "bnz", opBnz, assembleBranch, disBranch, oneInt, nil, 1, modeAny, opSize{1, 3, checkBranch}}, - {0x41, "bz", opBz, assembleBranch, disBranch, oneInt, nil, 2, modeAny, opSize{1, 3, checkBranch}}, - {0x42, "b", opB, assembleBranch, disBranch, nil, nil, 2, modeAny, opSize{1, 3, checkBranch}}, - {0x43, "return", opReturn, asmDefault, disDefault, oneInt, nil, 2, modeAny, opSizeDefault}, - {0x48, "pop", opPop, asmDefault, disDefault, oneAny, nil, 1, modeAny, opSizeDefault}, - {0x49, "dup", opDup, asmDefault, disDefault, oneAny, twoAny, 1, modeAny, opSizeDefault}, - {0x4a, "dup2", opDup2, asmDefault, disDefault, twoAny, twoAny.plus(twoAny), 2, modeAny, opSizeDefault}, - - {0x50, "concat", opConcat, asmDefault, disDefault, twoBytes, oneBytes, 2, modeAny, opSizeDefault}, - {0x51, "substring", opSubstring, assembleSubstring, disSubstring, oneBytes, oneBytes, 2, modeAny, opSize{1, 3, nil}}, - {0x52, "substring3", opSubstring3, asmDefault, disDefault, byteIntInt, oneBytes, 2, modeAny, opSizeDefault}, - - {0x60, "balance", opBalance, asmDefault, disDefault, oneInt, oneInt, 2, runModeApplication, opSizeDefault}, - {0x61, "app_opted_in", opAppCheckOptedIn, asmDefault, disDefault, twoInts, oneInt, 2, runModeApplication, opSizeDefault}, - {0x62, "app_local_get", opAppGetLocalState, asmDefault, disDefault, oneInt.plus(oneBytes), oneAny, 2, runModeApplication, opSizeDefault}, - {0x63, "app_local_get_ex", opAppGetLocalStateEx, asmDefault, disDefault, twoInts.plus(oneBytes), oneAny.plus(oneInt), 2, runModeApplication, opSizeDefault}, - {0x64, "app_global_get", opAppGetGlobalState, asmDefault, disDefault, oneBytes, oneAny, 2, runModeApplication, opSizeDefault}, - {0x65, "app_global_get_ex", opAppGetGlobalStateEx, asmDefault, disDefault, oneInt.plus(oneBytes), oneAny.plus(oneInt), 2, runModeApplication, opSizeDefault}, - {0x66, "app_local_put", opAppPutLocalState, asmDefault, disDefault, oneInt.plus(oneBytes).plus(oneAny), nil, 2, runModeApplication, opSizeDefault}, - {0x67, "app_global_put", opAppPutGlobalState, asmDefault, disDefault, oneBytes.plus(oneAny), nil, 2, runModeApplication, opSizeDefault}, - {0x68, "app_local_del", opAppDeleteLocalState, asmDefault, disDefault, oneInt.plus(oneBytes), nil, 2, runModeApplication, opSizeDefault}, - {0x69, "app_global_del", opAppDeleteGlobalState, asmDefault, disDefault, oneBytes, nil, 2, runModeApplication, opSizeDefault}, - - {0x70, "asset_holding_get", opAssetHoldingGet, assembleAssetHolding, disAssetHolding, twoInts, oneAny.plus(oneInt), 2, runModeApplication, opSize{1, 2, nil}}, - {0x71, "asset_params_get", opAssetParamsGet, assembleAssetParams, disAssetParams, oneInt, oneAny.plus(oneInt), 2, runModeApplication, opSize{1, 2, nil}}, + {0x31, "txn", opTxn, assembleTxn2, disTxn, nil, oneAny, 2, modeAny, immediates("f")}, + {0x32, "global", opGlobal, assembleGlobal, disGlobal, nil, oneAny, 1, modeAny, immediates("f")}, + {0x33, "gtxn", opGtxn, assembleGtxn, disGtxn, nil, oneAny, 1, modeAny, immediates("t", "f")}, + {0x33, "gtxn", opGtxn, assembleGtxn2, disGtxn, nil, oneAny, 2, modeAny, immediates("t", "f")}, + {0x34, "load", opLoad, asmDefault, disDefault, nil, oneAny, 1, modeAny, immediates("i")}, + {0x35, "store", opStore, asmDefault, disDefault, oneAny, nil, 1, modeAny, immediates("i")}, + {0x36, "txna", opTxna, assembleTxna, disTxna, nil, oneAny, 2, modeAny, immediates("f", "i")}, + {0x37, "gtxna", opGtxna, assembleGtxna, disGtxna, nil, oneAny, 2, modeAny, immediates("t", "f", "i")}, + // Like gtxn, but gets txn index from stack, rather than immediate arg + {0x38, "gtxns", opGtxns, assembleGtxns, disTxn, oneInt, oneAny, 3, modeAny, immediates("f")}, + {0x39, "gtxnsa", opGtxnsa, assembleGtxns, disTxna, oneInt, oneAny, 3, modeAny, immediates("f", "i")}, + + {0x40, "bnz", opBnz, assembleBranch, disBranch, oneInt, nil, 1, modeAny, opBranch}, + {0x41, "bz", opBz, assembleBranch, disBranch, oneInt, nil, 2, modeAny, opBranch}, + {0x42, "b", opB, assembleBranch, disBranch, nil, nil, 2, modeAny, opBranch}, + {0x43, "return", opReturn, asmDefault, disDefault, oneInt, nil, 2, modeAny, opDefault}, + {0x44, "assert", opAssert, asmDefault, disDefault, oneInt, nil, 3, modeAny, opDefault}, + {0x48, "pop", opPop, asmDefault, disDefault, oneAny, nil, 1, modeAny, opDefault}, + {0x49, "dup", opDup, asmDefault, disDefault, oneAny, twoAny, 1, modeAny, opDefault}, + {0x4a, "dup2", opDup2, asmDefault, disDefault, twoAny, twoAny.plus(twoAny), 2, modeAny, opDefault}, + // There must be at least one thing on the stack for dig, but + // it would be nice if we did better checking than that. + {0x4b, "dig", opDig, asmDefault, disDefault, oneAny, twoAny, 3, modeAny, immediates("n")}, + {0x4c, "swap", opSwap, asmDefault, disDefault, twoAny, twoAny, 3, modeAny, opDefault}, + {0x4d, "select", opSelect, asmDefault, disDefault, twoAny.plus(oneInt), oneAny, 3, modeAny, opDefault}, + + {0x50, "concat", opConcat, asmDefault, disDefault, twoBytes, oneBytes, 2, modeAny, opDefault}, + {0x51, "substring", opSubstring, assembleSubstring, disDefault, oneBytes, oneBytes, 2, modeAny, immediates("s", "e")}, + {0x52, "substring3", opSubstring3, asmDefault, disDefault, byteIntInt, oneBytes, 2, modeAny, opDefault}, + {0x53, "getbit", opGetBit, asmDefault, disDefault, anyInt, oneInt, 3, modeAny, opDefault}, + {0x54, "setbit", opSetBit, asmDefault, disDefault, anyIntInt, oneInt, 3, modeAny, opDefault}, + {0x55, "getbyte", opGetByte, asmDefault, disDefault, byteInt, oneInt, 3, modeAny, opDefault}, + {0x56, "setbyte", opSetByte, asmDefault, disDefault, byteIntInt, oneBytes, 3, modeAny, opDefault}, + + {0x60, "balance", opBalance, asmDefault, disDefault, oneInt, oneInt, 2, runModeApplication, opDefault}, + {0x61, "app_opted_in", opAppCheckOptedIn, asmDefault, disDefault, twoInts, oneInt, 2, runModeApplication, opDefault}, + {0x62, "app_local_get", opAppGetLocalState, asmDefault, disDefault, oneInt.plus(oneBytes), oneAny, 2, runModeApplication, opDefault}, + {0x63, "app_local_get_ex", opAppGetLocalStateEx, asmDefault, disDefault, twoInts.plus(oneBytes), oneAny.plus(oneInt), 2, runModeApplication, opDefault}, + {0x64, "app_global_get", opAppGetGlobalState, asmDefault, disDefault, oneBytes, oneAny, 2, runModeApplication, opDefault}, + {0x65, "app_global_get_ex", opAppGetGlobalStateEx, asmDefault, disDefault, oneInt.plus(oneBytes), oneAny.plus(oneInt), 2, runModeApplication, opDefault}, + {0x66, "app_local_put", opAppPutLocalState, asmDefault, disDefault, oneInt.plus(oneBytes).plus(oneAny), nil, 2, runModeApplication, opDefault}, + {0x67, "app_global_put", opAppPutGlobalState, asmDefault, disDefault, oneBytes.plus(oneAny), nil, 2, runModeApplication, opDefault}, + {0x68, "app_local_del", opAppDeleteLocalState, asmDefault, disDefault, oneInt.plus(oneBytes), nil, 2, runModeApplication, opDefault}, + {0x69, "app_global_del", opAppDeleteGlobalState, asmDefault, disDefault, oneBytes, nil, 2, runModeApplication, opDefault}, + + {0x70, "asset_holding_get", opAssetHoldingGet, assembleAssetHolding, disAssetHolding, twoInts, oneAny.plus(oneInt), 2, runModeApplication, immediates("i")}, + {0x71, "asset_params_get", opAssetParamsGet, assembleAssetParams, disAssetParams, oneInt, oneAny.plus(oneInt), 2, runModeApplication, immediates("i")}, + + {0x78, "min_balance", opMinBalance, asmDefault, disDefault, oneInt, oneInt, 3, runModeApplication, opDefault}, + + // Immediate bytes and ints. Smaller code size for single use of constant. + {0x80, "pushbytes", opPushBytes, asmPushBytes, disPushBytes, nil, oneBytes, 3, modeAny, varies(checkPushBytes, "bytes", immBytes)}, + {0x81, "pushint", opPushInt, asmPushInt, disPushInt, nil, oneInt, 3, modeAny, varies(checkPushInt, "uint", immInt)}, } type sortByOpcode []OpSpec @@ -173,7 +234,7 @@ func (a sortByOpcode) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a sortByOpcode) Less(i, j int) bool { return a[i].Opcode < a[j].Opcode } // OpcodesByVersion returns list of opcodes available in a specific version of TEAL -// by copying v1 opcodes to v2 to create a full list. +// by copying v1 opcodes to v2, and then on to v3 to create a full list func OpcodesByVersion(version uint64) []OpSpec { // for updated opcodes use the lowest version opcode was introduced in maxOpcode := 0 @@ -220,7 +281,9 @@ func OpcodesByVersion(version uint64) []OpSpec { // direct opcode bytes var opsByOpcode [LogicVersion + 1][256]OpSpec -var opsByName [LogicVersion + 1]map[string]OpSpec + +// OpsByName map for each each version, mapping opcode name to OpSpec +var OpsByName [LogicVersion + 1]map[string]OpSpec // Migration from TEAL v1 to TEAL v2. // TEAL v1 allowed execution of program with version 0. @@ -231,27 +294,27 @@ var opsByName [LogicVersion + 1]map[string]OpSpec func init() { // First, initialize baseline v1 opcodes. // Zero (empty) version is an alias for TEAL v1 opcodes and needed for compatibility with v1 code. - opsByName[0] = make(map[string]OpSpec, 256) - opsByName[1] = make(map[string]OpSpec, 256) + OpsByName[0] = make(map[string]OpSpec, 256) + OpsByName[1] = make(map[string]OpSpec, 256) for _, oi := range OpSpecs { if oi.Version == 1 { cp := oi cp.Version = 0 opsByOpcode[0][oi.Opcode] = cp - opsByName[0][oi.Name] = cp + OpsByName[0][oi.Name] = cp opsByOpcode[1][oi.Opcode] = oi - opsByName[1][oi.Name] = oi + OpsByName[1][oi.Name] = oi } } // Start from v2 TEAL and higher, // copy lower version opcodes and overwrite matching version for v := uint64(2); v <= EvalMaxVersion; v++ { - opsByName[v] = make(map[string]OpSpec, 256) + OpsByName[v] = make(map[string]OpSpec, 256) // Copy opcodes from lower version - for opName, oi := range opsByName[v-1] { - opsByName[v][opName] = oi + for opName, oi := range OpsByName[v-1] { + OpsByName[v][opName] = oi } for op, oi := range opsByOpcode[v-1] { opsByOpcode[v][op] = oi @@ -261,7 +324,7 @@ func init() { for _, oi := range OpSpecs { if oi.Version == v { opsByOpcode[v][oi.Opcode] = oi - opsByName[v][oi.Name] = oi + OpsByName[v][oi.Name] = oi } } } diff --git a/data/transactions/logic/opcodes_test.go b/data/transactions/logic/opcodes_test.go index 8bde67a2c9..7c61f90680 100644 --- a/data/transactions/logic/opcodes_test.go +++ b/data/transactions/logic/opcodes_test.go @@ -28,7 +28,7 @@ func TestOpSpecs(t *testing.T) { t.Parallel() for _, spec := range OpSpecs { - require.NotEmpty(t, spec.opSize, spec) + require.NotEmpty(t, spec.Details, spec) } } @@ -84,7 +84,7 @@ func TestOpcodesByVersion(t *testing.T) { OpSpecs2[idx] = cp } - opSpecs := make([][]OpSpec, 2) + opSpecs := make([][]OpSpec, LogicVersion) for v := uint64(1); v <= LogicVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { opSpecs[v-1] = OpcodesByVersion(v) @@ -117,8 +117,8 @@ func TestOpcodesByVersion(t *testing.T) { func TestOpcodesVersioningV2(t *testing.T) { t.Parallel() - require.Equal(t, 3, len(opsByOpcode)) - require.Equal(t, 3, len(opsByName)) + require.Equal(t, 4, len(opsByOpcode)) + require.Equal(t, 4, len(OpsByName)) // ensure v0 has only v0 opcodes cntv0 := 0 @@ -128,12 +128,12 @@ func TestOpcodesVersioningV2(t *testing.T) { cntv0++ } } - for _, spec := range opsByName[0] { + for _, spec := range OpsByName[0] { if spec.op != nil { require.Equal(t, uint64(0), spec.Version) } } - require.Equal(t, cntv0, len(opsByName[0])) + require.Equal(t, cntv0, len(OpsByName[0])) // ensure v1 has only v1 opcodes cntv1 := 0 @@ -143,12 +143,12 @@ func TestOpcodesVersioningV2(t *testing.T) { cntv1++ } } - for _, spec := range opsByName[1] { + for _, spec := range OpsByName[1] { if spec.op != nil { require.Equal(t, uint64(1), spec.Version) } } - require.Equal(t, cntv1, len(opsByName[1])) + require.Equal(t, cntv1, len(OpsByName[1])) require.Equal(t, cntv1, cntv0) require.Equal(t, 52, cntv1) @@ -159,25 +159,24 @@ func TestOpcodesVersioningV2(t *testing.T) { reflect.ValueOf(a.dis).Pointer() == reflect.ValueOf(b.dis).Pointer() && reflect.DeepEqual(a.Args, b.Args) && reflect.DeepEqual(a.Returns, b.Returns) && a.Modes == b.Modes && - a.opSize.cost == b.opSize.cost && a.opSize.size == b.opSize.size && - reflect.ValueOf(a.opSize.checkFunc).Pointer() == reflect.ValueOf(b.opSize.checkFunc).Pointer() + a.Details.Cost == b.Details.Cost && a.Details.Size == b.Details.Size && + reflect.ValueOf(a.Details.checkFunc).Pointer() == reflect.ValueOf(b.Details.checkFunc).Pointer() return } // ensure v0 and v1 are the same require.Equal(t, len(opsByOpcode[1]), len(opsByOpcode[0])) - require.Equal(t, len(opsByName[1]), len(opsByName[0])) + require.Equal(t, len(OpsByName[1]), len(OpsByName[0])) for op, spec1 := range opsByOpcode[1] { spec0 := opsByOpcode[0][op] msg := fmt.Sprintf("%v\n%v\n", spec0, spec1) require.True(t, eqButVersion(&spec1, &spec0), msg) } - for name, spec1 := range opsByName[1] { - spec0 := opsByName[0][name] + for name, spec1 := range OpsByName[1] { + spec0 := OpsByName[0][name] require.True(t, eqButVersion(&spec1, &spec0)) } // ensure v2 has v1 and v2 opcodes - require.Equal(t, len(opsByName[2]), len(opsByName[2])) cntv2 := 0 cntAdded := 0 for _, spec := range opsByOpcode[2] { @@ -189,12 +188,12 @@ func TestOpcodesVersioningV2(t *testing.T) { cntv2++ } } - for _, spec := range opsByName[2] { + for _, spec := range OpsByName[2] { if spec.op != nil { require.True(t, spec.Version == 1 || spec.Version == 2) } } - require.Equal(t, cntv2, len(opsByName[2])) + require.Equal(t, cntv2, len(OpsByName[2])) // hardcode and ensure amount of new v2 opcodes newOpcodes := 22 @@ -202,4 +201,29 @@ func TestOpcodesVersioningV2(t *testing.T) { require.Equal(t, newOpcodes+overwritten, cntAdded) require.Equal(t, cntv2, cntv1+newOpcodes) + + // ensure v3 has v1, v2, v3 opcodes + cntv3 := 0 + cntAdded = 0 + for _, spec := range opsByOpcode[3] { + if spec.op != nil { + require.True(t, spec.Version == 1 || spec.Version == 2 || spec.Version == 3) + if spec.Version == 3 { + cntAdded++ + } + cntv3++ + } + } + for _, spec := range OpsByName[3] { + if spec.op != nil { + require.True(t, spec.Version == 1 || spec.Version == 2 || spec.Version == 3) + } + } + require.Len(t, OpsByName[3], cntv3) + + // assert, min_balance, {get,set}{bit,byte}, swap, select, dig, stxn, stxna, push{int,bytes} + newOpcodes = 13 + overwritten = 0 // ? none yet + require.Equal(t, newOpcodes+overwritten, cntAdded) + } diff --git a/ledger/applications.go b/ledger/applications.go index 68bb135e11..9dee2b4e63 100644 --- a/ledger/applications.go +++ b/ledger/applications.go @@ -19,6 +19,7 @@ package ledger import ( "fmt" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" ) @@ -74,6 +75,16 @@ func (al *logicLedger) Balance(addr basics.Address) (res basics.MicroAlgos, err return record.MicroAlgos, nil } +func (al *logicLedger) MinBalance(addr basics.Address, proto *config.ConsensusParams) (res basics.MicroAlgos, err error) { + // Fetch record with pending rewards applied + record, err := al.cow.Get(addr, true) + if err != nil { + return + } + + return record.MinBalance(proto), nil +} + func (al *logicLedger) AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) { // Fetch the requested balance record record, err := al.cow.Get(addr, false) @@ -131,6 +142,10 @@ func (al *logicLedger) ApplicationID() basics.AppIndex { return al.aidx } +func (al *logicLedger) CreatorAddress() basics.Address { + return al.creator +} + func (al *logicLedger) OptedIn(addr basics.Address, appIdx basics.AppIndex) (bool, error) { if appIdx == basics.AppIndex(0) { appIdx = al.aidx diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index 114839c753..1080d87b1f 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -362,7 +362,7 @@ func TestLedgerBlockHeaders(t *testing.T) { // TODO test rewards cases with changing poolAddr money, with changing round, and with changing total reward units badBlock = bookkeeping.Block{BlockHeader: correctHeader} - badBlock.BlockHeader.TxnRoot = crypto.Digest{} + badBlock.BlockHeader.TxnRoot = crypto.Hash([]byte{0}) a.Error(l.appendUnvalidated(badBlock), "added block header with empty transaction root") badBlock = bookkeeping.Block{BlockHeader: correctHeader} diff --git a/protocol/consensus.go b/protocol/consensus.go index 4428ee6001..7b2a507931 100644 --- a/protocol/consensus.go +++ b/protocol/consensus.go @@ -138,6 +138,11 @@ const ConsensusV25 = ConsensusVersion( "https://github.com/algorandfoundation/specs/tree/bea19289bf41217d2c0af30522fa222ef1366466", ) +// ConsensusV26 adds support for TEAL 3, initial rewards calculation and merkle tree hash commitments +const ConsensusV26 = ConsensusVersion( + "https://github.com/algorandfoundation/specs/tree/ac2255d586c4474d4ebcf3809acccb59b7ef34ff", +) + // ConsensusFuture is a protocol that should not appear in any production // network, but is used to test features before they are released. const ConsensusFuture = ConsensusVersion( @@ -150,7 +155,7 @@ const ConsensusFuture = ConsensusVersion( // ConsensusCurrentVersion is the latest version and should be used // when a specific version is not provided. -const ConsensusCurrentVersion = ConsensusV25 +const ConsensusCurrentVersion = ConsensusV26 // Error is used to indicate that an unsupported protocol has been detected. type Error ConsensusVersion diff --git a/test/README.md b/test/README.md index 096e180710..0061fb0f1d 100644 --- a/test/README.md +++ b/test/README.md @@ -33,16 +33,17 @@ These tests are shell scripts which all run in series against a single private n Each script is provided with a wallet which contains a large supply of algos to use during the test. ``` -usage: e2e_client_runner.py [-h] [--keep-temps] [--timeout TIMEOUT] [--verbose] [scripts [scripts ...]] +usage: e2e_client_runner.py [-h] [--keep-temps] [--timeout TIMEOUT] [--verbose] [--version Future|vXX] [scripts [scripts ...]] positional arguments: scripts scripts to run optional arguments: - -h, --help show this help message and exit - --keep-temps if set, keep all the test files - --timeout TIMEOUT integer seconds to wait for the scripts to run + -h, --help show this help message and exit + --keep-temps if set, keep all the test files + --timeout TIMEOUT integer seconds to wait for the scripts to run --verbose + --version Future|vXX selects the network template file ``` To run a specific test: diff --git a/test/e2e-go/cli/tealdbg/expect/tealdbgTest.exp b/test/e2e-go/cli/tealdbg/expect/tealdbgTest.exp index 57b0fb4052..7be2f90668 100644 --- a/test/e2e-go/cli/tealdbg/expect/tealdbgTest.exp +++ b/test/e2e-go/cli/tealdbg/expect/tealdbgTest.exp @@ -11,15 +11,79 @@ if { [catch { exec mkdir -p $TEST_DIR set TEAL_PROG_FILE "$TEST_DIR/trivial.teal" + + # this is ConsensusV25 + set PROTOCOL_VERSION_2 "https://github.com/algorandfoundation/specs/tree/bea19289bf41217d2c0af30522fa222ef1366466" + + # this is ConsensusV26 + set PROTOCOL_VERSION_3 "https://github.com/algorandfoundation/specs/tree/ac2255d586c4474d4ebcf3809acccb59b7ef34ff" + + # run the test using version 2: + exec printf "#pragma version 2\nint 1\ndup\n+\n" > $TEAL_PROG_FILE set URL "" set PASSED 0 - spawn tealdbg debug -v $TEAL_PROG_FILE + spawn tealdbg debug -v $TEAL_PROG_FILE -p $PROTOCOL_VERSION_2 + expect_background { + timeout { puts "tealdbg debug timed out"; exit 1 } + -re {CDT debugger listening on: (ws://[.a-z0-9:/]+)} { set URL $expect_out(1,string); } + eof { + catch wait result + if { [lindex $result 3] != 0 } { + puts "returned error code is [lindex $result 3]" + exit 1 + } + } + } + set tealdbg_spawn_id $spawn_id + + # wait until URL is set or timeout + set it 0 + while { $it < 10 && $URL == "" } { + set it [expr {$it + 1}] + sleep 1 + } + if { $URL == "" } { + puts "ERROR: URL is not set after timeout" + exit 1 + } + + spawn cdtmock $URL + expect { + timeout { puts "cdt-mock debug timed out"; exit 1 } + -re {Debugger.paused} { set PASSED 1; } + eof { catch wait result; if { [lindex $result 3] == 0 } { puts "Expected non-zero exit code"; exit [lindex $result 3] } } + } + + if { $PASSED == 0 } { + puts "ERROR: have not found 'Debugger.paused' in cdtmock output" + exit 1 + } + + puts "Shutting down tealdbg" + close -i $tealdbg_spawn_id + exec rm $TEAL_PROG_FILE + + # run the test using version 3: + + exec printf "#pragma version 3\nint 1\ndup\n+\n" > $TEAL_PROG_FILE + + set URL "" + set PASSED 0 + spawn tealdbg debug -v $TEAL_PROG_FILE -p $PROTOCOL_VERSION_3 expect_background { timeout { puts "tealdbg debug timed out"; exit 1 } -re {CDT debugger listening on: (ws://[.a-z0-9:/]+)} { set URL $expect_out(1,string); } + eof { + catch wait result + if { [lindex $result 3] != 0 } { + puts "returned error code is [lindex $result 3]" + exit 1 + } + } } + set tealdbg_spawn_id $spawn_id # wait until URL is set or timeout set it 0 @@ -44,6 +108,9 @@ if { [catch { exit 1 } + puts "Shutting down tealdbg" + close -i $tealdbg_spawn_id + } EXCEPTION ] } { puts "ERROR in teadbgTest: $EXCEPTION" exit 1 diff --git a/test/scripts/e2e.sh b/test/scripts/e2e.sh index b94b044947..2ec92c2b8e 100755 --- a/test/scripts/e2e.sh +++ b/test/scripts/e2e.sh @@ -47,7 +47,6 @@ echo "Test output can be found in ${TEMPDIR}" export BINDIR=${TEMPDIR}/bin export DATADIR=${TEMPDIR}/data -RUNNING_COUNT=0 function reset_dirs() { rm -rf ${BINDIR} @@ -61,7 +60,7 @@ function reset_dirs() { reset_dirs echo Killing all instances and installing current build -pkill -u $(whoami) -x algod || true +pkill -u "$(whoami)" -x algod || true if ! ${NO_BUILD} ; then ./scripts/local_install.sh -c ${CHANNEL} -p ${BINDIR} -d ${DATADIR} @@ -82,11 +81,14 @@ cd "${SCRIPT_PATH}" ./timeout 200 ./e2e_basic_start_stop.sh -python3 -m venv ${TEMPDIR}/ve -. ${TEMPDIR}/ve/bin/activate -${TEMPDIR}/ve/bin/pip3 install --upgrade pip -${TEMPDIR}/ve/bin/pip3 install --upgrade py-algorand-sdk cryptography -${TEMPDIR}/ve/bin/python3 e2e_client_runner.py "$SRCROOT"/test/scripts/e2e_subs/*.sh +python3 -m venv "${TEMPDIR}/ve" +. "${TEMPDIR}/ve/bin/activate" +"${TEMPDIR}/ve/bin/pip3" install --upgrade pip +"${TEMPDIR}/ve/bin/pip3" install --upgrade py-algorand-sdk cryptography +"${TEMPDIR}/ve/bin/python3" e2e_client_runner.py "$SRCROOT"/test/scripts/e2e_subs/*.sh +for vdir in "$SRCROOT"/test/scripts/e2e_subs/v??; do + "${TEMPDIR}/ve/bin/python3" e2e_client_runner.py --version "$(basename "$vdir")" "$vdir"/*.sh +done deactivate # Export our root temp folder as 'TESTDIR' for tests to use as their root test folder @@ -97,7 +99,7 @@ export SRCROOT=${SRCROOT} ./e2e_go_tests.sh ${GO_TEST_ARGS} -rm -rf ${TEMPDIR} +rm -rf "${TEMPDIR}" if ! ${NO_BUILD} ; then rm -rf ${PKG_ROOT} diff --git a/test/scripts/e2e_client_runner.py b/test/scripts/e2e_client_runner.py index 8abc2fd56f..7b1d2d0693 100755 --- a/test/scripts/e2e_client_runner.py +++ b/test/scripts/e2e_client_runner.py @@ -366,6 +366,7 @@ def main(): ap.add_argument('--keep-temps', default=False, action='store_true', help='if set, keep all the test files') ap.add_argument('--timeout', default=500, type=int, help='integer seconds to wait for the scripts to run') ap.add_argument('--verbose', default=False, action='store_true') + ap.add_argument('--version', default="Future") args = ap.parse_args() if args.verbose: @@ -392,7 +393,8 @@ def main(): env['NETDIR'] = netdir retcode = 0 - xrun(['goal', 'network', 'create', '-r', netdir, '-n', 'tbd', '-t', os.path.join(repodir, 'test/testdata/nettemplates/TwoNodes50EachFuture.json')], timeout=90) + capv = args.version.capitalize() + xrun(['goal', 'network', 'create', '-r', netdir, '-n', 'tbd', '-t', os.path.join(repodir, f'test/testdata/nettemplates/TwoNodes50Each{capv}.json')], timeout=90) xrun(['goal', 'network', 'start', '-r', netdir], timeout=90) atexit.register(goal_network_stop, netdir, env) diff --git a/test/scripts/e2e_subs/e2e-teal.sh b/test/scripts/e2e_subs/e2e-teal.sh index 631a7b7690..a71c78a10d 100755 --- a/test/scripts/e2e_subs/e2e-teal.sh +++ b/test/scripts/e2e_subs/e2e-teal.sh @@ -126,4 +126,36 @@ echo "#pragma version 100" | ${gcmd} clerk compile - 2>&1 | grep "unsupported ve set -o pipefail +# Compile a v3 version of same program, fund it, use it to lsig. +cat >${TEMPDIR}/true3.teal<&1 | grep "illegal opcode" +set -o pipefail + + date '+e2e_teal OK %Y%m%d_%H%M%S' diff --git a/test/scripts/e2e_subs/v24/teal-v2-only.sh b/test/scripts/e2e_subs/v24/teal-v2-only.sh new file mode 100755 index 0000000000..4b4c87d4cd --- /dev/null +++ b/test/scripts/e2e_subs/v24/teal-v2-only.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +date '+teal-v2-only start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# prints: +# Created new account with address UCTHHNBEAUWHDQWQI5DGQCTB7AR4CSVNU5YNPROAYQIT3Y3LKVDFAA5M6Q +ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') + +cat >${TEMPDIR}/true.teal<&1 | grep "assert opcode was introduced" +set -o pipefail + +# Although we are in an earlier version, v3 can be compiled, it just can't be used. +cat >${TEMPDIR}/true3.teal<&1 | grep "LogicSig.Logic version too new" +set -o pipefail + + +# Now, ensure it still fails, even if using the v2 program, if the +# assert opcode is added. (That is, failure based on opcode choice, +# not just on the version marker.) + +${gcmd} clerk compile ${TEMPDIR}/true.teal -o ${TEMPDIR}/true.lsig +# append "assert" opcode to the true program +(cat ${TEMPDIR}/true.lsig; printf '\x72') > ${TEMPDIR}/assert.lsig +# compute the escrow account for the asserting program +ACCOUNT_TRUE=$(python -c 'import algosdk, sys; print(algosdk.logic.address(sys.stdin.buffer.read()))' < ${TEMPDIR}/assert.lsig) +# fund that escrow account +${gcmd} clerk send --amount 1000000 --from ${ACCOUNT} --to ${ACCOUNT_TRUE} +# try, and fail, to lsig with the assert program +set +o pipefail +${gcmd} clerk send --amount 10 --from-program-bytes ${TEMPDIR}/assert.lsig --to ${ACCOUNTB} 2>&1 | grep "illegal opcode" +set -o pipefail + + + +date '+teal-v2-only OK %Y%m%d_%H%M%S' diff --git a/test/testdata/nettemplates/TwoNodes50EachV24.json b/test/testdata/nettemplates/TwoNodes50EachV24.json new file mode 100644 index 0000000000..b181da21cd --- /dev/null +++ b/test/testdata/nettemplates/TwoNodes50EachV24.json @@ -0,0 +1,29 @@ +{ + "Genesis": { + "NetworkName": "tbd", + "ConsensusProtocol": "https://github.com/algorandfoundation/specs/tree/3a83c4c743f8b17adfd73944b4319c25722a6782", + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 50, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 50, + "Online": true + } + ] + }, + "Nodes": [ + { + "Name": "Primary", + "IsRelay": true, + "Wallets": [{ "Name": "Wallet1", "ParticipationOnly": false }] + }, + { + "Name": "Node", + "Wallets": [{ "Name": "Wallet2", "ParticipationOnly": false }] + } + ] +}