From ff56c2bc50b49dfe4fd0bf7f3a8d79e11b621d8f Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Thu, 9 Jun 2022 14:47:28 -0500 Subject: [PATCH 01/23] Experimental: test abi methods --- graviton/blackbox.py | 6 + setup.py | 5 +- tests/integration/abi_test.py | 16 +- tests/teal/router/questionable.json | 192 ++++++++++++ tests/teal/router/questionable.teal | 467 ++++++++++++++++++++++++++++ 5 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 tests/teal/router/questionable.json create mode 100644 tests/teal/router/questionable.teal diff --git a/graviton/blackbox.py b/graviton/blackbox.py index 57282593..22c0d846 100644 --- a/graviton/blackbox.py +++ b/graviton/blackbox.py @@ -582,6 +582,12 @@ def transaction_params( return {k: v for k, v in params.items() if v is not None} +class ABIContractExecutor: + def __init__(self, teal: str, contract: str): + self.program = teal + self.contract: abi.Contract = abi.Contract.from_json(contract) + + class DryRunInspector: """Methods to extract information from a single dry run transaction. diff --git a/setup.py b/setup.py index c1a948a7..d145aca5 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,10 @@ author="Algorand", author_email="pypiservice@algorand.com", python_requires=">=3.8", - install_requires=["py-algorand-sdk", "tabulate==0.8.9"], + install_requires=[ + "py-algorand-sdk @ git+https://github.com/algorand/py-algorand-sdk@get-method-by-name", + "tabulate==0.8.9", + ], extras_require={ "development": [ "black==22.3.0", diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index cca020b5..05fb8309 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -19,7 +19,7 @@ from algosdk import abi -from graviton.blackbox import DryRunExecutor, DryRunEncoder +from graviton.blackbox import ABIContractExecutor, DryRunExecutor, DryRunEncoder from graviton.abi_strategy import ABIStrategy from tests.clients import get_algod @@ -222,3 +222,17 @@ def test_roundtrip_abi_strategy(roundtrip_app): assert expected_mut == mut, inspector.report( args, "expected_mut v. mut", last_steps=last_rows ) + + +ROUTER = Path.cwd() / "tests" / "teal" / "router" + + +def test_abi_methods(): + with open(ROUTER / "questionable.json") as f: + contract = f.read() + + with open(ROUTER / "questionable.teal") as f: + teal = f.read() + + ace = ABIContractExecutor(teal, contract) + x = 42 diff --git a/tests/teal/router/questionable.json b/tests/teal/router/questionable.json new file mode 100644 index 00000000..7d5ef3b5 --- /dev/null +++ b/tests/teal/router/questionable.json @@ -0,0 +1,192 @@ +{ + "name": "ASimpleQuestionablyRobustContract", + "methods": [ + { + "name": "add", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "sub", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "mul", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "div", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "mod", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "all_laid_to_args", + "args": [ + { + "type": "uint64", + "name": "_a" + }, + { + "type": "uint64", + "name": "_b" + }, + { + "type": "uint64", + "name": "_c" + }, + { + "type": "uint64", + "name": "_d" + }, + { + "type": "uint64", + "name": "_e" + }, + { + "type": "uint64", + "name": "_f" + }, + { + "type": "uint64", + "name": "_g" + }, + { + "type": "uint64", + "name": "_h" + }, + { + "type": "uint64", + "name": "_i" + }, + { + "type": "uint64", + "name": "_j" + }, + { + "type": "uint64", + "name": "_k" + }, + { + "type": "uint64", + "name": "_l" + }, + { + "type": "uint64", + "name": "_m" + }, + { + "type": "uint64", + "name": "_n" + }, + { + "type": "uint64", + "name": "_o" + }, + { + "type": "uint64", + "name": "_p" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "empty_return_subroutine", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "log_1", + "args": [], + "returns": { + "type": "uint64" + } + }, + { + "name": "log_creation", + "args": [], + "returns": { + "type": "string" + } + }, + { + "name": "approve_if_odd", + "args": [ + { + "type": "uint32", + "name": "condition_encoding" + } + ], + "returns": { + "type": "void" + } + } + ], + "desc": null, + "networks": {} +} diff --git a/tests/teal/router/questionable.teal b/tests/teal/router/questionable.teal new file mode 100644 index 00000000..c4414e9b --- /dev/null +++ b/tests/teal/router/questionable.teal @@ -0,0 +1,467 @@ +#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l20 +txna ApplicationArgs 0 +method "add(uint64,uint64)uint64" +== +bnz main_l19 +txna ApplicationArgs 0 +method "sub(uint64,uint64)uint64" +== +bnz main_l18 +txna ApplicationArgs 0 +method "mul(uint64,uint64)uint64" +== +bnz main_l17 +txna ApplicationArgs 0 +method "div(uint64,uint64)uint64" +== +bnz main_l16 +txna ApplicationArgs 0 +method "mod(uint64,uint64)uint64" +== +bnz main_l15 +txna ApplicationArgs 0 +method "all_laid_to_args(uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64)uint64" +== +bnz main_l14 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l13 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l12 +txna ApplicationArgs 0 +method "log_creation()string" +== +bnz main_l11 +err +main_l11: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +== +&& +assert +callsub logcreation_8 +store 67 +byte 0x151f7c75 +load 67 +concat +log +int 1 +return +main_l12: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 +!= +&& +|| +assert +callsub log1_7 +store 65 +byte 0x151f7c75 +load 65 +itob +concat +log +int 1 +return +main_l13: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +|| +assert +callsub emptyreturnsubroutine_6 +int 1 +return +main_l14: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 30 +txna ApplicationArgs 2 +btoi +store 31 +txna ApplicationArgs 3 +btoi +store 32 +txna ApplicationArgs 4 +btoi +store 33 +txna ApplicationArgs 5 +btoi +store 34 +txna ApplicationArgs 6 +btoi +store 35 +txna ApplicationArgs 7 +btoi +store 36 +txna ApplicationArgs 8 +btoi +store 37 +txna ApplicationArgs 9 +btoi +store 38 +txna ApplicationArgs 10 +btoi +store 39 +txna ApplicationArgs 11 +btoi +store 40 +txna ApplicationArgs 12 +btoi +store 41 +txna ApplicationArgs 13 +btoi +store 42 +txna ApplicationArgs 14 +btoi +store 43 +txna ApplicationArgs 15 +store 46 +load 46 +int 0 +extract_uint64 +store 44 +load 46 +int 8 +extract_uint64 +store 45 +load 30 +load 31 +load 32 +load 33 +load 34 +load 35 +load 36 +load 37 +load 38 +load 39 +load 40 +load 41 +load 42 +load 43 +load 44 +load 45 +callsub alllaidtoargs_5 +store 47 +byte 0x151f7c75 +load 47 +itob +concat +log +int 1 +return +main_l15: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 24 +txna ApplicationArgs 2 +btoi +store 25 +load 24 +load 25 +callsub mod_4 +store 26 +byte 0x151f7c75 +load 26 +itob +concat +log +int 1 +return +main_l16: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 18 +txna ApplicationArgs 2 +btoi +store 19 +load 18 +load 19 +callsub div_3 +store 20 +byte 0x151f7c75 +load 20 +itob +concat +log +int 1 +return +main_l17: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 12 +txna ApplicationArgs 2 +btoi +store 13 +load 12 +load 13 +callsub mul_2 +store 14 +byte 0x151f7c75 +load 14 +itob +concat +log +int 1 +return +main_l18: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 6 +txna ApplicationArgs 2 +btoi +store 7 +load 6 +load 7 +callsub sub_1 +store 8 +byte 0x151f7c75 +load 8 +itob +concat +log +int 1 +return +main_l19: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 0 +txna ApplicationArgs 2 +btoi +store 1 +load 0 +load 1 +callsub add_0 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return +main_l20: +txn OnCompletion +int OptIn +== +bnz main_l22 +err +main_l22: +txn ApplicationID +int 0 +!= +assert +byte "optin call" +log +int 1 +return + +// add +add_0: +store 4 +store 3 +load 3 +load 4 ++ +store 5 +load 5 +retsub + +// sub +sub_1: +store 10 +store 9 +load 9 +load 10 +- +store 11 +load 11 +retsub + +// mul +mul_2: +store 16 +store 15 +load 15 +load 16 +* +store 17 +load 17 +retsub + +// div +div_3: +store 22 +store 21 +load 21 +load 22 +/ +store 23 +load 23 +retsub + +// mod +mod_4: +store 28 +store 27 +load 27 +load 28 +% +store 29 +load 29 +retsub + +// all_laid_to_args +alllaidtoargs_5: +store 63 +store 62 +store 61 +store 60 +store 59 +store 58 +store 57 +store 56 +store 55 +store 54 +store 53 +store 52 +store 51 +store 50 +store 49 +store 48 +load 48 +load 49 ++ +load 50 ++ +load 51 ++ +load 52 ++ +load 53 ++ +load 54 ++ +load 55 ++ +load 56 ++ +load 57 ++ +load 58 ++ +load 59 ++ +load 60 ++ +load 61 ++ +load 62 ++ +load 63 ++ +store 64 +load 64 +retsub + +// empty_return_subroutine +emptyreturnsubroutine_6: +byte "appear in both approval and clear state" +log +retsub + +// log_1 +log1_7: +int 1 +store 66 +load 66 +retsub + +// log_creation +logcreation_8: +byte "logging creation" +len +itob +extract 6 0 +byte "logging creation" +concat +store 68 +load 68 +retsub + + \ No newline at end of file From 439e447cf563fd91dba1e6890d540e28b7b2c28c Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Fri, 10 Jun 2022 11:40:45 -0500 Subject: [PATCH 02/23] ABIStrategy -> RandomABIStrategy + abstract class --- graviton/abi_strategy.py | 47 ++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/graviton/abi_strategy.py b/graviton/abi_strategy.py index a520db47..3f7d5bc4 100644 --- a/graviton/abi_strategy.py +++ b/graviton/abi_strategy.py @@ -3,6 +3,7 @@ TODO: Leverage Hypothesis! """ +from abc import ABC, abstractmethod from collections import OrderedDict import random import string @@ -13,7 +14,20 @@ PY_TYPES = Union[bool, int, list, str, bytes] -class ABIStrategy: +class ABIStrategy(ABC): + @abstractmethod + def __init__(self, abi_instance: abi.ABIType, dynamic_length: Optional[int] = None): + pass + + @abstractmethod + def get(self) -> PY_TYPES: + pass + + def get_many(self, n: int) -> List[PY_TYPES]: + return [self.get() for _ in range(n)] + + +class RandomABIStrategy(ABIStrategy): DEFAULT_DYNAMIC_ARRAY_LENGTH = 3 STRING_CHARS = string.digits + string.ascii_letters + string.punctuation @@ -46,7 +60,7 @@ def __init__(self, abi_instance: abi.ABIType, dynamic_length: Optional[int] = No self.abi_type: abi.ABIType = abi_instance self.dynamic_length: Optional[int] = dynamic_length - def get_random(self) -> Union[bool, int, list, str, bytes]: + def get(self) -> PY_TYPES: if isinstance(self.abi_type, abi.UfixedType): raise NotImplementedError( f"Currently cannot get a random sample for {self.abi_type}" @@ -59,17 +73,17 @@ def get_random(self) -> Union[bool, int, list, str, bytes]: return random.randint(0, (1 << self.abi_type.bit_size) - 1) if isinstance(self.abi_type, abi.ByteType): - return ABIStrategy(abi.UintType(8)).get_random() + return RandomABIStrategy(abi.UintType(8)).get() if isinstance(self.abi_type, abi.TupleType): return [ - ABIStrategy(child_type).get_random() + RandomABIStrategy(child_type).get() for child_type in self.abi_type.child_types ] if isinstance(self.abi_type, abi.ArrayStaticType): return [ - ABIStrategy(self.abi_type.child_type).get_random() + RandomABIStrategy(self.abi_type.child_type).get() for _ in range(self.abi_type.static_length) ] @@ -78,11 +92,11 @@ def get_random(self) -> Union[bool, int, list, str, bytes]: bytearray( cast( List[int], - ABIStrategy( + RandomABIStrategy( abi.ArrayStaticType( abi.ByteType(), self.abi_type.byte_len() ) - ).get_random(), + ).get(), ) ) ) @@ -94,8 +108,7 @@ def get_random(self) -> Union[bool, int, list, str, bytes]: ) if isinstance(self.abi_type, abi.ArrayDynamicType): return [ - ABIStrategy(self.abi_type.child_type).get_random() - for _ in dynamic_range + RandomABIStrategy(self.abi_type.child_type).get() for _ in dynamic_range ] if isinstance(self.abi_type, abi.StringType): @@ -125,7 +138,7 @@ def address_logic(x): y = encoding.decode_address(x) return encoding.encode_address( bytearray( - ABIStrategy( + RandomABIStrategy( abi.ArrayStaticType(abi.ByteType(), len(y)) ).mutate_for_roundtrip(y) ) @@ -138,19 +151,23 @@ def address_logic(x): (abi.UintType, lambda x: (1 << self.abi_type.bit_size) - 1 - x), ( abi.ByteType, - lambda x: ABIStrategy(abi.UintType(8)).mutate_for_roundtrip(x), + lambda x: RandomABIStrategy(abi.UintType(8)).mutate_for_roundtrip( + x + ), ), ( abi.TupleType, lambda x: [ - ABIStrategy(child_type).mutate_for_roundtrip(x[i]) + RandomABIStrategy(child_type).mutate_for_roundtrip(x[i]) for i, child_type in enumerate(self.abi_type.child_types) ], ), ( abi.ArrayStaticType, lambda x: [ - ABIStrategy(self.abi_type.child_type).mutate_for_roundtrip(y) + RandomABIStrategy( + self.abi_type.child_type + ).mutate_for_roundtrip(y) for y in x ], ), @@ -158,7 +175,9 @@ def address_logic(x): ( abi.ArrayDynamicType, lambda x: [ - ABIStrategy(self.abi_type.child_type).mutate_for_roundtrip(y) + RandomABIStrategy( + self.abi_type.child_type + ).mutate_for_roundtrip(y) for y in x ], ), From a6fdd4f496d0c72685886e6141284b75966cb5a4 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Fri, 10 Jun 2022 11:42:28 -0500 Subject: [PATCH 03/23] better mypy via PY_TYPES alias --- graviton/blackbox.py | 177 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 143 insertions(+), 34 deletions(-) diff --git a/graviton/blackbox.py b/graviton/blackbox.py index 22c0d846..7173d484 100644 --- a/graviton/blackbox.py +++ b/graviton/blackbox.py @@ -5,7 +5,18 @@ import io from tabulate import tabulate -from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast +from typing import ( + Any, + Callable, + Dict, + Final, + List, + Optional, + Sequence, + Type, + Union, + cast, +) from algosdk import abi from algosdk.v2client.algod import AlgodClient @@ -15,6 +26,7 @@ StateSchema, SuggestedParams, ) +from graviton.abi_strategy import PY_TYPES, ABIStrategy, RandomABIStrategy from graviton.dryrun import ( assert_error, @@ -248,7 +260,7 @@ class DryRunEncoder: @classmethod def encode_args( cls, - args: Sequence[Union[bytes, str, int]], + args: Sequence[PY_TYPES], abi_types: List[Optional[abi.ABIType]] = None, ) -> List[Union[bytes, str]]: """ @@ -309,7 +321,7 @@ def _to_bytes( @classmethod def _encode_arg( - cls, arg: Union[bytes, int, str], idx: int, abi_type: Optional[abi.ABIType] + cls, arg: PY_TYPES, idx: int, abi_type: Optional[abi.ABIType] ) -> Union[str, bytes]: partial = cls._partial_encode_assert( arg, abi_type, f"problem encoding arg ({arg!r}) at index ({idx})" @@ -322,12 +334,15 @@ def _encode_arg( # int -> bytes # str -> str return cast( - Union[str, bytes], cls._to_bytes(arg, only_attempt_int_conversion=True) + Union[str, bytes], + cls._to_bytes( + cast(Union[int, str, bytes], arg), only_attempt_int_conversion=True + ), ) @classmethod def _partial_encode_assert( - cls, arg: Union[bytes, int, str], abi_type: Optional[abi.ABIType], msg: str = "" + cls, arg: PY_TYPES, abi_type: Optional[abi.ABIType], msg: str = "" ) -> Optional[bytes]: """ When have an `abi_type` is present, attempt to encode `arg` accordingly (returning `bytes`) @@ -356,6 +371,15 @@ class DryRunExecutor: * `abi_return_type` is given the `DryRunInspector`'s resulting from execution for ABI-decoding into Python """ + # `CREATION_APP_CALL` and `EXISTING_APP_CALL` are enum-like constants used to denote whether a dry run + # execution will simulate calling during on-creation vs pre-existance. + # In the default case that a dry run is executed without a provided application id (aka `index`), the `index` + # supplied will be: + # * `EXISTING_APP_CALL` in the case of `is_app_create == False` + # * `CREATION_APP_CALL` in the case of `is_app_create == True` + CREATION_APP_CALL: Final[int] = 0 + EXISTING_APP_CALL: Final[int] = 42 + SUGGESTED_PARAMS = SuggestedParams(int(1000), int(1), int(100), "", flat_fee=True) @classmethod @@ -363,14 +387,15 @@ def dryrun_app( cls, algod: AlgodClient, teal: str, - args: Sequence[Union[bytes, str, int]], + args: Sequence[PY_TYPES], abi_argument_types: List[Optional[abi.ABIType]] = None, abi_return_type: abi.ABIType = None, + is_app_create: bool = False, + on_complete: OnComplete = OnComplete.NoOpOC, *, sender: str = None, sp: SuggestedParams = None, index: int = None, - on_complete: OnComplete = None, local_schema: StateSchema = None, global_schema: StateSchema = None, approval_program: str = None, @@ -397,8 +422,12 @@ def dryrun_app( note=note, lease=lease, rekey_to=rekey_to, - index=0 if index is None else index, - on_complete=OnComplete.NoOpOC if on_complete is None else on_complete, + index=( + (cls.CREATION_APP_CALL if is_app_create else cls.EXISTING_APP_CALL) + if index is None + else index + ), + on_complete=on_complete, local_schema=local_schema, global_schema=global_schema, approval_program=approval_program, @@ -453,18 +482,26 @@ def dryrun_app_on_sequence( cls, algod: AlgodClient, teal: str, - inputs: List[Sequence[Union[str, int]]], + inputs: List[Sequence[PY_TYPES]], abi_argument_types: List[Optional[abi.ABIType]] = None, abi_return_type: abi.ABIType = None, + is_app_create: bool = True, + on_complete: OnComplete = OnComplete.NoOpOC, ) -> List["DryRunInspector"]: # TODO: handle txn_params - return cls._map( - cls.dryrun_app, - algod, - teal, - abi_argument_types, - inputs, - abi_return_type, + return list( + map( + lambda args: cls.dryrun_app( + algod=algod, + teal=teal, + args=args, + abi_argument_types=abi_argument_types, + abi_return_type=abi_return_type, + is_app_create=is_app_create, + on_complete=on_complete, + ), + inputs, + ) ) @classmethod @@ -477,27 +514,16 @@ def dryrun_logicsig_on_sequence( abi_return_type: abi.ABIType = None, ) -> List["DryRunInspector"]: # TODO: handle txn_params - return cls._map( - cls.dryrun_logicsig, - algod, - teal, - abi_argument_types, - inputs, - abi_return_type, - ) - - @classmethod - def _map(cls, f, algod, teal, abi_types, inps, abi_ret_type): return list( map( - lambda args: f( + lambda args: cls.dryrun_logicsig( algod=algod, teal=teal, args=args, - abi_argument_types=abi_types, - abi_return_type=abi_ret_type, + abi_argument_types=abi_argument_types, + abi_return_type=abi_return_type, ), - inps, + inputs, ) ) @@ -506,7 +532,7 @@ def execute_one_dryrun( cls, algod: AlgodClient, teal: str, - args: Sequence[Union[bytes, int, str]], + args: Sequence[PY_TYPES], mode: ExecutionMode, abi_argument_types: List[Optional[abi.ABIType]] = None, abi_return_type: abi.ABIType = None, @@ -558,6 +584,9 @@ def transaction_params( foreign_assets: List[str] = None, extra_pages: int = None, ) -> Dict[str, Any]: + """ + Returns a `dict` with keys the same as method params, after removing all `None` values + """ params = dict( sender=sender, sp=sp, @@ -583,9 +612,89 @@ def transaction_params( class ABIContractExecutor: - def __init__(self, teal: str, contract: str): + """Execute an ABI Contract via Dry Run""" + + def __init__( + self, + teal: str, + contract: str, + argument_strategy: Optional[Type[ABIStrategy]] = RandomABIStrategy, + dry_runs: int = 1, + ): self.program = teal self.contract: abi.Contract = abi.Contract.from_json(contract) + self.argument_strategy: Optional[Type[ABIStrategy]] = argument_strategy + self.dry_runs = dry_runs + + def argument_types(self, method: Optional[str] = None) -> List[abi.ABIType]: + if not method: + return [] + + # the method selector is not abi-encoded, hence its abi-type is set to None + return [None] + [ + arg.type for arg in self.contract.get_method_by_name(method).args + ] + + def return_type(self, method: Optional[str] = None) -> Optional[abi.ABIType]: + if not method: + return None + + return_type = self.contract.get_method_by_name(method).returns().type + if return_type == abi.Returns.VOID: + return None + + return return_type + + def generate_inputs(self, method: Optional[str]) -> List[Sequence[PY_TYPES]]: + assert ( + self.argument_strategy + ), "cannot generate inputs without an argument_strategy" + + if not method: + # bare calls receive no arguments + return [tuple() for _ in range(self.dry_runs)] + + selector = self.contract.get_method_by_name(method).get_selector() + arg_types = self.argument_types(method) + + def gen_args(): + return tuple( + [selector] + + [self.argument_strategy(arg_type).get() for arg_type in arg_types] + ) + + return [gen_args() for _ in range(self.dry_runs)] + + def dry_run( + self, + algod: AlgodClient, + method: Optional[str] = None, + on_complete: OnComplete = OnComplete.NoOpOC, + inputs: Optional[List[Sequence[PY_TYPES]]] = None, + ) -> List["DryRunInspector"]: + """ARC-4 Compliant Dry Run""" + arg_types = self.argument_types(method) + return_type = self.return_type(method) + + if inputs is None: + inputs = self.generate_inputs(method) + else: + processed: List[Sequence[PY_TYPES]] = [] + selector = self.contract.get_method_by_name(method).get_selector() + for i, args in enumerate(inputs): + assert ( + len(args) == len(arg_types) - 1 + ), f"length mismatch for args=inputs[{i}]: Should be {len(arg_types) - 1} to accomodate selector + method params, but was {len(args)}" + processed.append(tuple([selector] + list(args))) + inputs = processed + + return DryRunExecutor.dryrun_app_on_sequence( + algod, + self.program, + inputs, + abi_argument_types=arg_types, + abi_return_type=return_type, + ) class DryRunInspector: From e0a0b556d8f593a9f4b1ce7fe70b9faf988e3bd7 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Fri, 10 Jun 2022 11:43:16 -0500 Subject: [PATCH 04/23] wip - router test positive cases half way there --- tests/integration/abi_test.py | 99 +++++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 05fb8309..873060bc 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -18,9 +18,15 @@ import pytest from algosdk import abi +from algosdk.future.transaction import OnComplete -from graviton.blackbox import ABIContractExecutor, DryRunExecutor, DryRunEncoder -from graviton.abi_strategy import ABIStrategy +from graviton.blackbox import ( + ABIContractExecutor, + DryRunExecutor as DRE, + DryRunEncoder, + DryRunProperty as DRProp, +) +from graviton.abi_strategy import RandomABIStrategy from tests.clients import get_algod @@ -84,7 +90,7 @@ def test_dynamic_array_sum(): args = ("ignored", [1, 2, 3, 4, 5]) abi_arg_types = (None, abi.ArrayDynamicType(abi.UintType(64))) abi_out_type = abi.UintType(64) - inspector = DryRunExecutor.dryrun_app( + inspector = DRE.dryrun_app( algod, DYNAMIC_ARRAY_SUM_TEAL, args, abi_arg_types, abi_out_type ) # with default config: @@ -126,7 +132,7 @@ def process_filename(filename): abi_str = abi_info[0] abi_instance = abi.ABIType.from_string(abi_str) - abi_strat = ABIStrategy(abi_instance, length) + abi_strat = RandomABIStrategy(abi_instance, length) return abi_str, length, abi_instance, abi_strat @@ -139,7 +145,7 @@ def test_unit_abi_strategy_get_random(roundtrip_app): filename = str(roundtrip_app) abi_str, length, abi_instance, abi_strat = process_filename(filename) - rand = abi_strat.get_random() + rand = abi_strat.get() encoded = DryRunEncoder.encode_args([rand], abi_types=[abi_instance]) decoded = abi_instance.decode(encoded[0]) assert decoded == rand @@ -175,7 +181,7 @@ def test_roundtrip_abi_strategy(roundtrip_app): ) return - rand = abi_strat.get_random() + rand = abi_strat.get() algod = get_algod() args = (rand,) @@ -185,9 +191,7 @@ def test_roundtrip_abi_strategy(roundtrip_app): with open(filename) as f: roundtrip_teal = f.read() - inspector = DryRunExecutor.dryrun_app( - algod, roundtrip_teal, args, abi_arg_types, abi_out_type - ) + inspector = DRE.dryrun_app(algod, roundtrip_teal, args, abi_arg_types, abi_out_type) cost = inspector.cost() passed = inspector.passed() @@ -226,13 +230,74 @@ def test_roundtrip_abi_strategy(roundtrip_app): ROUTER = Path.cwd() / "tests" / "teal" / "router" +QUESTIONABLE_CONTRACT = None +with open(ROUTER / "questionable.json") as f: + QUESTIONABLE_CONTRACT = f.read() + +QUESTIONABLE_TEAL = None +with open(ROUTER / "questionable.teal") as f: + QUESTIONABLE_TEAL = f.read() + +QUESTIONABLE_ACE = ABIContractExecutor( + QUESTIONABLE_TEAL, QUESTIONABLE_CONTRACT, argument_strategy=RandomABIStrategy +) + +QUESTIONABLE_CASES = [ + # LEGEND: + # * 0: method name when `str` or bare app call when `None` + # * 1: `OnComplete` values to test (`None` is the same as `[(OnComplete.NoOpOc)]`) + # * 2: invariants being asserted `dict[DRProp, Callable]` + ("add", None, {DRProp.lastLog: lambda args: args[0] + args[1]}), + ( + "sub", + None, + { + DRProp.lastLog: lambda args, actual: True + if args[0] < args[1] + else actual == args[0] - args[1] + }, + ), + ("mul", None, {DRProp.lastLog: lambda args: args[0] * args[1]}), + ("div", None, {DRProp.lastLog: lambda args: args[0] // args[1]}), + ("mod", None, {DRProp.lastLog: lambda args: args[0] % args[1]}), + ("all_laid_to_args", None, {DRProp.lastLog: lambda args: sum(args)}), + ( + "empty_return_subroutine", + {DRProp.lastLog: "appear in both approval and clear state"}, + ), + ("log_1", {DRProp.lastLog: lambda args: 1}), + ("log_creation", {DRProp.lastLog: lambda args: "logging creation"}), + ("approve_if_odd", {DRProp.lastLog: lambda args: args[0] + args[1]}), + ( + OnComplete.CloseOutOC, + None, + ), # TODO - does this make sense here? or in the "opposites" case? + (OnComplete.ClearStateOC, {DRProp.passed: True}), + (OnComplete.DeleteApplicationOC, None), + (OnComplete.NoOpOC, None), + ( + OnComplete.OptInOC, + {DRProp.passed: True, DRProp.lastLog: "optin call"}, + ), + (OnComplete.UpdateApplicationOC, None), +] +# TODO: for some cases will need to somehow nudge gravition-dry-run to set ApplicationID == 0 +# TODO: need to test the clear program as well! + +DRY_RUNS_PER_TEST = 7 + + +@pytest.mark.parametrize("method", QUESTIONABLE_CASES) +def test_method_or_barecall_positive(method): + """ + Test the _positive_ version of a case. In other words, ensure that for the given: + * method or bare call + * OnComplete value + * number of arguments + that the app call succeeds with the given + """ + ace = QUESTIONABLE_ACE + # method = ace.contract.get_method_by_name(method) + inputs = ace.generate_inputs(method, DRY_RUNS_PER_TEST) -def test_abi_methods(): - with open(ROUTER / "questionable.json") as f: - contract = f.read() - - with open(ROUTER / "questionable.teal") as f: - teal = f.read() - - ace = ABIContractExecutor(teal, contract) x = 42 From c737556fa9323c10d45e881040c9345f17a2aba9 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Fri, 10 Jun 2022 22:39:55 -0500 Subject: [PATCH 05/23] passing the POSITIVE test cases. Next, need to explore the NEGATIVE space --- Makefile | 2 +- graviton/abi_strategy.py | 21 +++- graviton/blackbox.py | 131 +++++++++++++++++++------ graviton/dryrun.py | 8 +- graviton/invariant.py | 48 ++++----- graviton/models.py | 8 ++ tests/integration/abi_test.py | 121 ++++++++++++++++------- tests/integration/blackbox_test.py | 4 +- tests/integration/doc_examples_test.py | 4 +- tests/teal/router/questionable.teal | 2 +- tests/unit/inspector_test.py | 10 +- 11 files changed, 259 insertions(+), 100 deletions(-) diff --git a/Makefile b/Makefile index 8b915585..58dd78bd 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ flake8: flake8 graviton tests mypy: - mypy . + mypy --show-error-codes . lint: black flake8 mypy diff --git a/graviton/abi_strategy.py b/graviton/abi_strategy.py index 3f7d5bc4..0d3be7e8 100644 --- a/graviton/abi_strategy.py +++ b/graviton/abi_strategy.py @@ -7,11 +7,11 @@ from collections import OrderedDict import random import string -from typing import Callable, Dict, List, Optional, Union, cast +from typing import Callable, Dict, List, Optional, Sequence, Union, cast from algosdk import abi, encoding -PY_TYPES = Union[bool, int, list, str, bytes] +PY_TYPES = Union[bool, int, Sequence, str, bytes] class ABIStrategy(ABC): @@ -187,3 +187,20 @@ def address_logic(x): ) return self.map(waterfall, py_abi_instance) + + +class RandomABIStrategyHalfSized(RandomABIStrategy): + def __init__( + self, + abi_instance: abi.ABIType, + dynamic_length: Optional[int] = None, + ): + super().__init__(abi_instance, dynamic_length=dynamic_length) + + def get(self) -> PY_TYPES: + full_random = super().get() + + if not isinstance(self.abi_type, abi.UintType): + return full_random + + return full_random % (1 << (self.abi_type.bit_size // 2)) diff --git a/graviton/blackbox.py b/graviton/blackbox.py index 7173d484..f996b56f 100644 --- a/graviton/blackbox.py +++ b/graviton/blackbox.py @@ -26,6 +26,9 @@ StateSchema, SuggestedParams, ) + +from algosdk import atomic_transaction_composer as atc + from graviton.abi_strategy import PY_TYPES, ABIStrategy, RandomABIStrategy from graviton.dryrun import ( @@ -37,6 +40,9 @@ from graviton.models import ZERO_ADDRESS +MAX_APP_ARG_LIMIT = atc.AtomicTransactionComposer.MAX_APP_ARG_LIMIT + + class ExecutionMode(Enum): Signature = auto() Application = auto() @@ -275,15 +281,35 @@ def encode_args( abi_types (optional) - When present this list needs to be the same length as `args`. When `None` is supplied as the abi_type, the corresponding element of `args` is not encoded. """ + a_len = len(args) if abi_types: - a_len, t_len = len(args), len(abi_types) + t_len = len(abi_types) assert ( a_len == t_len ), f"mismatch between args (length={a_len}) and abi_types (length={t_len})" - return [ - cls._encode_arg(a, i, abi_types[i] if abi_types else None) + + if a_len <= MAX_APP_ARG_LIMIT: + return [ + cls._encode_arg(a, i, abi_types[i] if abi_types else None) + for i, a in enumerate(args) + ] + + assert ( + abi_types + ), f"for non-ABI app calls, there is no specification for encoding more than {MAX_APP_ARG_LIMIT} arguments. But encountered an app call attempt with {a_len} arguments" + + final_index = MAX_APP_ARG_LIMIT - 1 + simple_15 = [ + cls._encode_arg(a, i, abi_types[i]) for i, a in enumerate(args) + if i < final_index ] + jammed_in = cls._encode_arg( + args[final_index:], + final_index, + abi_type=abi.TupleType(abi_types[final_index:]), + ) + return simple_15 + [jammed_in] @classmethod def hex0x(cls, x) -> str: @@ -409,6 +435,19 @@ def dryrun_app( rekey_to: str = None, extra_pages: int = None, ) -> "DryRunInspector": + """ + Execute a dry run to simulate an app call using provided: + + * algod + * teal program for the approval (or clear in the case `on_complete=OnComplete.ClearStateOC`) + * args - the application arguments as Python types + * abi_argument_types - ABI types of the arguments, in the case of an ABI method call + * abi_return_type - the ABI type returned, in the case of an ABI method call + * is_app_create to indicate whether or not to simulate an app create call + * on_complete - the OnComplete that should be provided in the app call transaction + + Additional application call transaction parameters can be provided as well + """ return cls.execute_one_dryrun( algod, teal, @@ -485,7 +524,7 @@ def dryrun_app_on_sequence( inputs: List[Sequence[PY_TYPES]], abi_argument_types: List[Optional[abi.ABIType]] = None, abi_return_type: abi.ABIType = None, - is_app_create: bool = True, + is_app_create: bool = False, on_complete: OnComplete = OnComplete.NoOpOC, ) -> List["DryRunInspector"]: # TODO: handle txn_params @@ -543,7 +582,7 @@ def execute_one_dryrun( ), f"assuming only 2 ExecutionMode's but have {len(ExecutionMode)}" assert mode in ExecutionMode, f"unknown mode {mode} of type {type(mode)}" is_app = mode == ExecutionMode.Application - args = DryRunEncoder.encode_args(args, abi_types=abi_argument_types) + encoded_args = DryRunEncoder.encode_args(args, abi_types=abi_argument_types) builder: Callable[[str, List[Union[bytes, str]], Dict[str, Any]], DryrunRequest] builder = ( @@ -551,10 +590,10 @@ def execute_one_dryrun( if is_app else DryRunHelper.singleton_logicsig_request ) - dryrun_req = builder(teal, args, txn_params) + dryrun_req = builder(teal, encoded_args, txn_params) dryrun_resp = algod.dryrun(dryrun_req) return DryRunInspector.from_single_response( - dryrun_resp, abi_type=abi_return_type + dryrun_resp, args, encoded_args, abi_type=abi_return_type ) @classmethod @@ -639,7 +678,7 @@ def return_type(self, method: Optional[str] = None) -> Optional[abi.ABIType]: if not method: return None - return_type = self.contract.get_method_by_name(method).returns().type + return_type = self.contract.get_method_by_name(method).returns.type if return_type == abi.Returns.VOID: return None @@ -660,7 +699,11 @@ def generate_inputs(self, method: Optional[str]) -> List[Sequence[PY_TYPES]]: def gen_args(): return tuple( [selector] - + [self.argument_strategy(arg_type).get() for arg_type in arg_types] + + [ + self.argument_strategy(arg_type).get() + for arg_type in arg_types + if arg_type + ] ) return [gen_args() for _ in range(self.dry_runs)] @@ -669,10 +712,13 @@ def dry_run( self, algod: AlgodClient, method: Optional[str] = None, + is_app_create: bool = False, on_complete: OnComplete = OnComplete.NoOpOC, inputs: Optional[List[Sequence[PY_TYPES]]] = None, ) -> List["DryRunInspector"]: """ARC-4 Compliant Dry Run""" + # TODO: handle txn_params + arg_types = self.argument_types(method) return_type = self.return_type(method) @@ -682,9 +728,9 @@ def dry_run( processed: List[Sequence[PY_TYPES]] = [] selector = self.contract.get_method_by_name(method).get_selector() for i, args in enumerate(inputs): - assert ( - len(args) == len(arg_types) - 1 - ), f"length mismatch for args=inputs[{i}]: Should be {len(arg_types) - 1} to accomodate selector + method params, but was {len(args)}" + assert len(args) == len( + arg_types + ), f"length mismatch for args=inputs[{i}]: Should be {len(arg_types)} but was {len(args)}" processed.append(tuple([selector] + list(args))) inputs = processed @@ -694,6 +740,8 @@ def dry_run( inputs, abi_argument_types=arg_types, abi_return_type=return_type, + is_app_create=is_app_create, + on_complete=on_complete, ) @@ -753,9 +801,16 @@ class DryRunInspector: To suppress decoding the last log entry altogether, and show the raw hex, set `config(suppress_abi=True)`. """ - CONFIG_OPTIONS = {"suppress_abi", "has_abi_prefix"} + CONFIG_OPTIONS = {"suppress_abi", "has_abi_prefix", "show_internal_errors_on_log"} - def __init__(self, dryrun_resp: dict, txn_index: int, abi_type: abi.ABIType = None): + def __init__( + self, + dryrun_resp: dict, + txn_index: int, + args: Sequence[PY_TYPES], + encoded_args: List[Union[bytes, str]], + abi_type: abi.ABIType = None, + ): txns = dryrun_resp.get("txns", []) assert txns, "Dry Run response is missing transactions" @@ -764,6 +819,8 @@ def __init__(self, dryrun_resp: dict, txn_index: int, abi_type: abi.ABIType = No ), f"Out of bounds txn_index {txn_index} when there are only {len(txns)} transactions in the Dry Run response" txn = txns[txn_index] + self.args = args + self.encoded_args = encoded_args self.mode: ExecutionMode = self.get_txn_mode(txn) self.parent_dryrun_response: dict = dryrun_resp @@ -775,7 +832,12 @@ def __init__(self, dryrun_resp: dict, txn_index: int, abi_type: abi.ABIType = No # config options: self.suppress_abi: bool self.has_abi_prefix: bool - self.config(suppress_abi=False, has_abi_prefix=bool(self.abi_type)) + self.show_internal_errors_on_log: bool + self.config( + suppress_abi=False, + has_abi_prefix=bool(self.abi_type), + show_internal_errors_on_log=True, + ) def config(self, **kwargs: bool): bad_keys = set(kwargs.keys()) - self.CONFIG_OPTIONS @@ -810,7 +872,11 @@ def get_txn_mode(cls, txn: dict) -> ExecutionMode: @classmethod def from_single_response( - cls, dryrun_resp: dict, abi_type: abi.ABIType = None + cls, + dryrun_resp: dict, + args: Sequence[PY_TYPES], + encoded_args: List[Union[bytes, str]], + abi_type: abi.ABIType = None, ) -> "DryRunInspector": error = dryrun_resp.get("error") assert not error, f"dryrun response included the following error: [{error}]" @@ -820,7 +886,7 @@ def from_single_response( len(txns) == 1 ), f"require exactly 1 dry run transaction to create a singleton but had {len(txns)} instead" - return cls(dryrun_resp, 0, abi_type=abi_type) + return cls(dryrun_resp, 0, args, encoded_args, abi_type=abi_type) def dig(self, dr_property: DryRunProperty, **kwargs: Dict[str, Any]) -> Any: """Main router for assertable properties""" @@ -838,7 +904,20 @@ def dig(self, dr_property: DryRunProperty, **kwargs: Dict[str, Any]) -> Any: last_log = txn.get("logs", [None])[-1] if last_log is None: return last_log - return b64decode(last_log).hex() + + last_log = b64decode(last_log).hex() + if not self.abi_type or self.suppress_abi: + return last_log + + try: + if self.has_abi_prefix: + # skip the first 8 hex char's == first 4 bytes: + last_log = last_log[8:] + return self.abi_type.decode(bytes.fromhex(last_log)) + except Exception as e: + if self.show_internal_errors_on_log: + return str(e) + raise e if dr_property == DryRunProperty.finalScratch: return {k: v.as_python_type() for k, v in bbr.final_scratch_state.items()} @@ -896,13 +975,7 @@ def last_log(self) -> Optional[str]: if not self.is_app(): return None - res = self.dig(DRProp.lastLog) - if not self.abi_type or self.suppress_abi: - return res - - if self.has_abi_prefix: - res = res[8:] # skip the first 8 hex char's == first 4 bytes - return self.abi_type.decode(bytes.fromhex(res)) + return self.dig(DRProp.lastLog) def stack_top(self) -> Union[int, str]: """Assertable property for the contents of the top of the stack and the end of a dry run execution @@ -1064,12 +1137,14 @@ def empty_hack(se): def report( self, - args: Sequence[Union[str, int]], + args: Optional[Sequence[PY_TYPES]] = None, msg: str = "Dry Run Inspector Report", row: int = 0, last_steps: int = 100, ) -> str: bbr = self.black_box_results + if args is None: + args = self.args return f"""=============== <<<<<<<<<<<{msg}>>>>>>>>>>> =============== @@ -1106,8 +1181,8 @@ def report( """ def csv_row( - self, row_num: int, args: Sequence[Union[int, str]] - ) -> Dict[str, Union[str, int, None]]: + self, row_num: int, args: Sequence[PY_TYPES] + ) -> Dict[str, Optional[PY_TYPES]]: return { " Run": row_num, " cost": self.cost(), diff --git a/graviton/dryrun.py b/graviton/dryrun.py index 6819bd3c..bb563a9f 100644 --- a/graviton/dryrun.py +++ b/graviton/dryrun.py @@ -214,7 +214,13 @@ def singleton_logicsig_request( def singleton_app_request( cls, program: str, args: List[Union[bytes, str]], txn_params: Dict[str, Any] ): - return cls.dryrun_request(program, models.App(args=args), txn_params) + creator = txn_params.get("sender") + app_idx = txn_params.get("index") + on_complete = txn_params.get("on_complete") + app = models.App.factory( + creator=creator, app_idx=app_idx, on_complete=on_complete, args=args + ) + return cls.dryrun_request(program, app, txn_params) @classmethod def _txn_params_with_defaults(cls, txn_params: dict, for_app: bool) -> dict: diff --git a/graviton/invariant.py b/graviton/invariant.py index a9268969..9a09dca1 100644 --- a/graviton/invariant.py +++ b/graviton/invariant.py @@ -1,6 +1,7 @@ from inspect import signature from typing import cast, Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from graviton.abi_strategy import PY_TYPES from graviton.blackbox import ( DryRunInspector, DryRunProperty, @@ -26,41 +27,32 @@ def __init__( def __repr__(self): return f"Invariant({self.definition})"[:100] - def __call__( - self, args: Sequence[Union[str, int]], actual: Union[str, int] - ) -> Tuple[bool, str]: + def __call__(self, args: Sequence[PY_TYPES], actual: PY_TYPES) -> Tuple[bool, str]: invariant = self.predicate(args, actual) msg = "" if not invariant: - msg = f"Invariant for '{self.name}' failed for for args {args}: actual is [{actual}] BUT expected [{self.expected(args)}]" + msg = f"Invariant for '{self.name}' failed for for args {args!r}: actual is [{actual!r}] BUT expected [{self.expected(args)!r}]" if self.enforce: assert invariant, msg return invariant, msg - def expected(self, args: Sequence[Union[str, int]]) -> Union[str, int]: + def expected(self, args: Sequence[PY_TYPES]) -> PY_TYPES: return self._expected(args) def validates( self, dr_property: DryRunProperty, - inputs: List[Sequence[Union[str, int]]], inspectors: List[DryRunInspector], ): - N = len(inputs) - assert N == len( - inspectors - ), f"inputs (len={N}) and dryrun responses (len={len(inspectors)}) must have the same length" - assert isinstance( dr_property, DryRunProperty ), f"invariants types must be DryRunProperty's but got [{dr_property}] which is a {type(dr_property)}" - for i, args in enumerate(inputs): - res = inspectors[i] - actual = res.dig(dr_property) - ok, msg = self(args, actual) - assert ok, res.report(args, msg, row=i + 1) + for i, inspector in enumerate(inspectors): + actual = inspector.dig(dr_property) + ok, msg = self(inspector.args, actual) + assert ok, inspector.report(msg=msg, row=i + 1) @classmethod def prepare_predicate(cls, predicate): @@ -140,15 +132,25 @@ def inputs_and_invariants( and all(isinstance(args, tuple) for args in inputs) ), "need a list of inputs with at least one args and all args must be tuples" - invariants: Dict[DryRunProperty, Any] = {} predicates = cast(Dict[DryRunProperty, Any], scenario.get("invariants", {})) if predicates: assert isinstance(predicates, dict), "invariants must be a dict" - for key, predicate in predicates.items(): - assert isinstance(key, DryRunProperty) and mode_has_property( - mode, key - ), f"each key must be a DryRunProperty's appropriate to {mode}. This is not the case for key {key}" - invariants[key] = Invariant(predicate, name=str(key)) + return inputs, predicates if raw_predicates else cls.as_invariants( + predicates, mode + ) + + @classmethod + def as_invariants( + cls, + predicates: Dict[DryRunProperty, Any], + mode: ExecutionMode = ExecutionMode.Application, + ) -> Dict[DryRunProperty, "Invariant"]: + invariants: Dict[DryRunProperty, Any] = {} - return inputs, predicates if raw_predicates else invariants # type: ignore + for key, predicate in predicates.items(): + assert isinstance(key, DryRunProperty) and mode_has_property( + mode, key + ), f"each key must be a DryRunProperty's appropriate to {mode}. This is not the case for key {key}" + invariants[key] = Invariant(predicate, name=str(key)) + return invariants diff --git a/graviton/models.py b/graviton/models.py index e82f6848..d53a8e91 100644 --- a/graviton/models.py +++ b/graviton/models.py @@ -38,3 +38,11 @@ class App: args: Optional[List[Union[bytes, str]]] = None accounts: Optional[List[Union[str, Account]]] = None global_state: Optional[List[TealKeyValue]] = None + + @classmethod + def factory(cls, **kwargs) -> "App": + app = cls() + for key, val in kwargs.items(): + if hasattr(app, key) and val is not None: + setattr(app, key, val) + return app diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 873060bc..358f4190 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -16,6 +16,7 @@ from pathlib import Path import pytest +from typing import Any, Dict, List, Optional, Tuple from algosdk import abi from algosdk.future.transaction import OnComplete @@ -26,7 +27,8 @@ DryRunEncoder, DryRunProperty as DRProp, ) -from graviton.abi_strategy import RandomABIStrategy +from graviton.abi_strategy import RandomABIStrategy, RandomABIStrategyHalfSized +from graviton.invariant import Invariant from tests.clients import get_algod @@ -228,7 +230,11 @@ def test_roundtrip_abi_strategy(roundtrip_app): ) +# --- ABI Router Dry Run Testing --- # + ROUTER = Path.cwd() / "tests" / "teal" / "router" +NUM_ROUTER_DRYRUNS = 7 + QUESTIONABLE_CONTRACT = None with open(ROUTER / "questionable.json") as f: @@ -239,65 +245,110 @@ def test_roundtrip_abi_strategy(roundtrip_app): QUESTIONABLE_TEAL = f.read() QUESTIONABLE_ACE = ABIContractExecutor( - QUESTIONABLE_TEAL, QUESTIONABLE_CONTRACT, argument_strategy=RandomABIStrategy + QUESTIONABLE_TEAL, + QUESTIONABLE_CONTRACT, + argument_strategy=RandomABIStrategyHalfSized, + dry_runs=NUM_ROUTER_DRYRUNS, ) -QUESTIONABLE_CASES = [ +QUESTIONABLE_CASES: List[ + Tuple[Optional[str], Optional[List[Tuple[bool, OnComplete]]], Dict[DRProp, Any]] +] = [ # LEGEND: - # * 0: method name when `str` or bare app call when `None` - # * 1: `OnComplete` values to test (`None` is the same as `[(OnComplete.NoOpOc)]`) - # * 2: invariants being asserted `dict[DRProp, Callable]` - ("add", None, {DRProp.lastLog: lambda args: args[0] + args[1]}), + # + # * @0 - method: str | None + # method name when `str` or bare app call when `None` + # + # * @1 - call_types: ...tuple[bool, OncComplete] | None + # [(is_app_create, `OnComplete`), ...] contexts to test (`None` is short-hand for `[(False, OnComplete.NoOpOC)]`) + # + # * @2 - invariants: dict[DRProp, Any] + # these are being asserted after being processed into actual Invariant's + # + ("add", None, {DRProp.lastLog: lambda args: args[1] + args[2]}), ( "sub", None, { DRProp.lastLog: lambda args, actual: True - if args[0] < args[1] - else actual == args[0] - args[1] + if args[1] < args[2] + else actual == args[1] - args[2] }, ), - ("mul", None, {DRProp.lastLog: lambda args: args[0] * args[1]}), - ("div", None, {DRProp.lastLog: lambda args: args[0] // args[1]}), - ("mod", None, {DRProp.lastLog: lambda args: args[0] % args[1]}), - ("all_laid_to_args", None, {DRProp.lastLog: lambda args: sum(args)}), + ("mul", None, {DRProp.lastLog: lambda args: args[1] * args[2]}), + ("div", None, {DRProp.lastLog: lambda args: args[1] // args[2]}), + ("mod", None, {DRProp.lastLog: lambda args: args[1] % args[2]}), + ("all_laid_to_args", None, {DRProp.lastLog: lambda args: sum(args[1:])}), ( "empty_return_subroutine", - {DRProp.lastLog: "appear in both approval and clear state"}, + [(False, OnComplete.NoOpOC), (False, OnComplete.OptInOC)], + {DRProp.lastLog: DryRunEncoder.hex("appear in both approval and clear state")}, ), - ("log_1", {DRProp.lastLog: lambda args: 1}), - ("log_creation", {DRProp.lastLog: lambda args: "logging creation"}), - ("approve_if_odd", {DRProp.lastLog: lambda args: args[0] + args[1]}), ( - OnComplete.CloseOutOC, - None, - ), # TODO - does this make sense here? or in the "opposites" case? - (OnComplete.ClearStateOC, {DRProp.passed: True}), - (OnComplete.DeleteApplicationOC, None), - (OnComplete.NoOpOC, None), + "log_1", + [(False, OnComplete.NoOpOC), (False, OnComplete.OptInOC)], + {DRProp.lastLog: 1}, + ), ( - OnComplete.OptInOC, - {DRProp.passed: True, DRProp.lastLog: "optin call"}, + "log_creation", + [(True, OnComplete.NoOpOC)], + {DRProp.lastLog: "logging creation"}, + ), + ( + "approve_if_odd", + [], # this should only appear in the clear-state program + {DRProp.lastLog: "THIS MAKES ABSOLUTELY NO SENSE ... SHOULD NEVER GET HERE!!!"}, + ), + ( + None, + [(False, OnComplete.OptInOC)], + {DRProp.passed: True, DRProp.lastLog: DryRunEncoder.hex("optin call")}, ), - (OnComplete.UpdateApplicationOC, None), ] -# TODO: for some cases will need to somehow nudge gravition-dry-run to set ApplicationID == 0 -# TODO: need to test the clear program as well! -DRY_RUNS_PER_TEST = 7 +# TODO: need to test the clear program as well! -@pytest.mark.parametrize("method", QUESTIONABLE_CASES) -def test_method_or_barecall_positive(method): +@pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CASES) +def test_method_or_barecall_positive(method, call_types, invariants): """ Test the _positive_ version of a case. In other words, ensure that for the given: * method or bare call * OnComplete value * number of arguments - that the app call succeeds with the given + that the app call succeeds for the given invariants """ ace = QUESTIONABLE_ACE - # method = ace.contract.get_method_by_name(method) - inputs = ace.generate_inputs(method, DRY_RUNS_PER_TEST) + algod = get_algod() + + if call_types is None: + call_types = [(False, OnComplete.NoOpOC)] - x = 42 + if not call_types: + return + + invariants = Invariant.as_invariants(invariants) + for is_app_create, on_complete in call_types: + inspectors = ace.dry_run( + algod, + method=method, + is_app_create=is_app_create, + on_complete=on_complete, + ) + for dr_property, invariant in invariants.items(): + invariant.validates(dr_property, inspectors) + + +@pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CASES) +def test_method_or_barecall_negative(method, call_types, invariants): + """ + Test the _negative_ version of a case. In other words, ensure that for the given: + * method or bare call + * OnComplete value + * number of arguments + explore the space _OUTSIDE_ of each constraint and assert that the app call FAILS!!! + """ + ace = QUESTIONABLE_ACE + algod = get_algod() + _ = ace + _ = algod diff --git a/tests/integration/blackbox_test.py b/tests/integration/blackbox_test.py index 471e877a..327995ee 100644 --- a/tests/integration/blackbox_test.py +++ b/tests/integration/blackbox_test.py @@ -383,7 +383,7 @@ def test_app_with_report(filebase: str): print( f"{i+1}. Semantic invariant for {case_name}-{mode}: {dr_property} <<{invariant}>>" ) - invariant.validates(dr_property, inputs, dryrun_results) + invariant.validates(dr_property, dryrun_results) # NOTE: logic sig dry runs are missing some information when compared with app dry runs. @@ -575,4 +575,4 @@ def test_logicsig_with_report(filebase: str): print( f"{i+1}. Semantic invariant for {case_name}-{mode}: {dr_property} <<{invariant}>>" ) - invariant.validates(dr_property, inputs, dryrun_results) + invariant.validates(dr_property, dryrun_results) diff --git a/tests/integration/doc_examples_test.py b/tests/integration/doc_examples_test.py index 3ccda6f9..7922d511 100644 --- a/tests/integration/doc_examples_test.py +++ b/tests/integration/doc_examples_test.py @@ -179,7 +179,7 @@ def test_step9(): # Invariant assertions on sequence: for dr_property, invariant in invariants.items(): - invariant.validates(dr_property, inputs, inspectors) + invariant.validates(dr_property, inspectors) @pytest.mark.parametrize("exercise", ["A", "B"]) @@ -218,4 +218,4 @@ def test_exercises(exercise): # Invariant assertions on sequence: for dr_property, invariant in invariants.items(): - invariant.validates(dr_property, inputs, inspectors) + invariant.validates(dr_property, inspectors) diff --git a/tests/teal/router/questionable.teal b/tests/teal/router/questionable.teal index c4414e9b..5a6539c1 100644 --- a/tests/teal/router/questionable.teal +++ b/tests/teal/router/questionable.teal @@ -292,7 +292,7 @@ return main_l19: txn OnCompletion int NoOp -== +== // I believe there ahould be an assertion here to ensure that this is actually a NoOp txn ApplicationID int 0 != diff --git a/tests/unit/inspector_test.py b/tests/unit/inspector_test.py index e3222d73..1e53267a 100644 --- a/tests/unit/inspector_test.py +++ b/tests/unit/inspector_test.py @@ -11,7 +11,7 @@ def test_from_single_response_errors(): } with pytest.raises(AssertionError) as ae: - DryRunInspector.from_single_response(error_resp) + DryRunInspector.from_single_response(error_resp, None, None) assert ( ae.value.args[0] @@ -25,7 +25,7 @@ def test_from_single_response_errors(): } with pytest.raises(AssertionError) as ae: - DryRunInspector.from_single_response(no_txns_resp1) + DryRunInspector.from_single_response(no_txns_resp1, None, None) assert ( ae.value.args[0] @@ -38,7 +38,7 @@ def test_from_single_response_errors(): } with pytest.raises(AssertionError) as ae: - DryRunInspector.from_single_response(no_txns_resp2) + DryRunInspector.from_single_response(no_txns_resp2, None, None) assert ( ae.value.args[0] @@ -48,7 +48,7 @@ def test_from_single_response_errors(): too_many_txns_resp = {"protocol-version": "future", "txns": [1, 2, 3]} with pytest.raises(AssertionError) as ae: - DryRunInspector.from_single_response(too_many_txns_resp) + DryRunInspector.from_single_response(too_many_txns_resp, None, None) assert ( ae.value.args[0] @@ -62,7 +62,7 @@ def test_from_single_response_errors(): } with pytest.raises(AssertionError) as ae: - DryRunInspector.from_single_response(too_many_txns_resp_w_err) + DryRunInspector.from_single_response(too_many_txns_resp_w_err, None, None) assert ( ae.value.args[0] From 9b3190b2f2130568bfe749934df6b747bccdd4b6 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Fri, 10 Jun 2022 23:08:23 -0500 Subject: [PATCH 06/23] prep CHANGELOG --- CHANGELOG.md | 12 +++++++++++- graviton/blackbox.py | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de0dd8d..e9195268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog +## `v0.4.0` (_aka_ 🐕) + +### Added + +* ABI Contract / Router / Execution functionality + +### Fixed + +* A bug that made all app calls run as if during creation + ## `v0.3.0` (_aka_ 🐈) ### Added @@ -13,7 +23,7 @@ ### Added -* ABI Functionality +* ABI Functionality (types only) ## `v0.1.2` diff --git a/graviton/blackbox.py b/graviton/blackbox.py index f996b56f..aa95bb93 100644 --- a/graviton/blackbox.py +++ b/graviton/blackbox.py @@ -398,11 +398,11 @@ class DryRunExecutor: """ # `CREATION_APP_CALL` and `EXISTING_APP_CALL` are enum-like constants used to denote whether a dry run - # execution will simulate calling during on-creation vs pre-existance. + # execution will simulate calling during on-creation vs post-creation. # In the default case that a dry run is executed without a provided application id (aka `index`), the `index` # supplied will be: - # * `EXISTING_APP_CALL` in the case of `is_app_create == False` # * `CREATION_APP_CALL` in the case of `is_app_create == True` + # * `EXISTING_APP_CALL` in the case of `is_app_create == False` CREATION_APP_CALL: Final[int] = 0 EXISTING_APP_CALL: Final[int] = 42 From b5f1b155d6ce299decc1217baf55b7bc419f7ac4 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Sat, 11 Jun 2022 00:06:32 -0500 Subject: [PATCH 07/23] start exploring the negative space --- tests/integration/abi_test.py | 64 ++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 358f4190..fcb411a8 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -13,7 +13,7 @@ * then copy the contents of the generated directory "./tests/integration/generated/roundtrip/" into the ROUNDTRIP directory """ - +from itertools import product from pathlib import Path import pytest from typing import Any, Dict, List, Optional, Tuple @@ -265,39 +265,68 @@ def test_roundtrip_abi_strategy(roundtrip_app): # * @2 - invariants: dict[DRProp, Any] # these are being asserted after being processed into actual Invariant's # - ("add", None, {DRProp.lastLog: lambda args: args[1] + args[2]}), + ( + "add", + None, + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] + args[2]}, + ), ( "sub", None, { + DRProp.passed: lambda args: args[1] >= args[2], DRProp.lastLog: lambda args, actual: True if args[1] < args[2] - else actual == args[1] - args[2] + else actual == args[1] - args[2], }, ), - ("mul", None, {DRProp.lastLog: lambda args: args[1] * args[2]}), - ("div", None, {DRProp.lastLog: lambda args: args[1] // args[2]}), - ("mod", None, {DRProp.lastLog: lambda args: args[1] % args[2]}), - ("all_laid_to_args", None, {DRProp.lastLog: lambda args: sum(args[1:])}), + ( + "mul", + None, + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] * args[2]}, + ), + ( + "div", + None, + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] // args[2]}, + ), + ( + "mod", + None, + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] % args[2]}, + ), + ( + "all_laid_to_args", + None, + {DRProp.passed: True, DRProp.lastLog: lambda args: sum(args[1:])}, + ), ( "empty_return_subroutine", [(False, OnComplete.NoOpOC), (False, OnComplete.OptInOC)], - {DRProp.lastLog: DryRunEncoder.hex("appear in both approval and clear state")}, + { + DRProp.passed: True, + DRProp.lastLog: DryRunEncoder.hex( + "appear in both approval and clear state" + ), + }, ), ( "log_1", [(False, OnComplete.NoOpOC), (False, OnComplete.OptInOC)], - {DRProp.lastLog: 1}, + {DRProp.passed: True, DRProp.lastLog: 1}, ), ( "log_creation", [(True, OnComplete.NoOpOC)], - {DRProp.lastLog: "logging creation"}, + {DRProp.passed: True, DRProp.lastLog: "logging creation"}, ), ( "approve_if_odd", [], # this should only appear in the clear-state program - {DRProp.lastLog: "THIS MAKES ABSOLUTELY NO SENSE ... SHOULD NEVER GET HERE!!!"}, + { + DRProp.passed: True, + DRProp.lastLog: "THIS MAKES ABSOLUTELY NO SENSE ... SHOULD NEVER GET HERE!!!", + }, ), ( None, @@ -350,5 +379,14 @@ def test_method_or_barecall_negative(method, call_types, invariants): """ ace = QUESTIONABLE_ACE algod = get_algod() - _ = ace - _ = algod + + if call_types is None: + call_types = [(False, OnComplete.NoOpOC)] + + # iac_n_oc --> (is_app_create, on_complete) + call_types_negation = [ + iac_n_oc + for iac_n_oc in product((True, False), OnComplete) + if iac_n_oc not in call_types + ] + x = 42 From 573206a0b1e169d68d5ddd097b2f0866253c38b7 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Sun, 12 Jun 2022 00:11:50 -0500 Subject: [PATCH 08/23] Better assertion message for invariant predicates of 2 variables + better type hints --- CHANGELOG.md | 1 + graviton/invariant.py | 60 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9195268..146c6039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixed * A bug that made all app calls run as if during creation +* Addressed [Issue #5](https://github.com/algorand/graviton/issues/5): Better assertion message for invariant predicates of 2 variables ## `v0.3.0` (_aka_ 🐈) diff --git a/graviton/invariant.py b/graviton/invariant.py index 9a09dca1..3fb1b688 100644 --- a/graviton/invariant.py +++ b/graviton/invariant.py @@ -1,4 +1,4 @@ -from inspect import signature +from inspect import getsource, signature from typing import cast, Any, Callable, Dict, List, Optional, Sequence, Tuple, Union from graviton.abi_strategy import PY_TYPES @@ -10,12 +10,20 @@ ) +INVARIANT_TYPE = Union[ + PY_TYPES, + Dict[Sequence[PY_TYPES], PY_TYPES], + Callable[[PY_TYPES], PY_TYPES], + Callable[[PY_TYPES], bool], +] + + class Invariant: """Enable asserting invariants on a sequence of dry run executions""" def __init__( self, - predicate: Union[Dict[Tuple, Union[str, int]], Callable], + predicate: INVARIANT_TYPE, enforce: bool = False, name: Optional[str] = None, ): @@ -31,7 +39,10 @@ def __call__(self, args: Sequence[PY_TYPES], actual: PY_TYPES) -> Tuple[bool, st invariant = self.predicate(args, actual) msg = "" if not invariant: - msg = f"Invariant for '{self.name}' failed for for args {args!r}: actual is [{actual!r}] BUT expected [{self.expected(args)!r}]" + expected = self.expected(args) + if callable(expected): + expected = getsource(expected) + msg = f"Invariant for '{self.name}' failed for for args {args!r}: actual is [{actual!r}] BUT expected [{expected!r}]" if self.enforce: assert invariant, msg @@ -44,6 +55,8 @@ def validates( self, dr_property: DryRunProperty, inspectors: List[DryRunInspector], + *, + msg: str = "", ): assert isinstance( dr_property, DryRunProperty @@ -51,18 +64,31 @@ def validates( for i, inspector in enumerate(inspectors): actual = inspector.dig(dr_property) - ok, msg = self(inspector.args, actual) - assert ok, inspector.report(msg=msg, row=i + 1) + ok, fail_msg = self(inspector.args, actual) + if msg: + fail_msg += f". invariant provided message:{msg}" + assert ok, inspector.report(msg=fail_msg, row=i + 1) @classmethod - def prepare_predicate(cls, predicate): + def prepare_predicate( + cls, + predicate: INVARIANT_TYPE, + ) -> Tuple[Callable[[Sequence[PY_TYPES], PY_TYPES], bool], Callable]: + # returns + # * Callable[[Sequence[PY_TYPES], PY_TYPES], bool] + # * Callable[[Sequence[PY_TYPES]], PY_TYPES] if isinstance(predicate, dict): + d_predicate = cast(Dict[PY_TYPES, PY_TYPES], predicate) return ( - lambda args, actual: predicate[args] == actual, - lambda args: predicate[args], + lambda args, actual: d_predicate[args] == actual, + lambda args: d_predicate[args], ) - if not isinstance(predicate, Callable): + # predicate = cast(Callable, predicate) + # returns + # * Callable[[Any], PY_TYPES], bool] + # * Callable[[Any], PY_TYPES] + if not callable(predicate): # constant function in this case: return lambda _, actual: predicate == actual, lambda _: predicate @@ -77,12 +103,22 @@ def prepare_predicate(cls, predicate): assert N in (1, 2), f"predicate has the wrong number of paramters {N}" if N == 2: - return predicate, lambda _: predicate + c2_predicate = cast( + Callable[[Sequence[PY_TYPES], PY_TYPES], bool], predicate + ) + # returns + # * Callable[[Sequence[PY_TYPES], PY_TYPES], bool] + # * Callable[Any, Callable[[Sequence[PY_TYPES], PY_TYPES], bool]] + return c2_predicate, lambda _: c2_predicate # N == 1: - return lambda args, actual: predicate(args) == actual, lambda args: predicate( + c1_predicate = cast(Callable[[Sequence[PY_TYPES]], bool], predicate) + # returns + # * Callable[[Sequence[PY_TYPES]], bool] + # * Callable[[Sequence[PY_TYPES]], PY_TYPES] + return lambda args, actual: c1_predicate( args - ) + ) == actual, lambda args: c1_predicate(args) @classmethod def inputs_and_invariants( From 8fe7d691ad96d6cbf3ed50e3639b3ff432c8f069 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Sun, 12 Jun 2022 00:14:29 -0500 Subject: [PATCH 09/23] inputs validation --- graviton/blackbox.py | 57 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/graviton/blackbox.py b/graviton/blackbox.py index aa95bb93..586cdd95 100644 --- a/graviton/blackbox.py +++ b/graviton/blackbox.py @@ -708,13 +708,48 @@ def gen_args(): return [gen_args() for _ in range(self.dry_runs)] - def dry_run( + def validate_inputs(self, method: str, inputs: List[Sequence[PY_TYPES]]): + arg_types = self.argument_types(method) + selector = self.contract.get_method_by_name(method).get_selector() + for i, args in enumerate(inputs): + error = self._validate_args(selector, args, arg_types) + assert not error, f"inputs is invalid at index {i}: {error}" + + @classmethod + def _validate_args( + cls, + method_selector: Optional[str], + args: Sequence[PY_TYPES], + arg_types: List[abi.ABIType], + ) -> Optional[str]: + if not isinstance(args, tuple): + return f"expected tuple for args but got {type(args)}" + + if not method_selector: # bare app call case + if args: + return ( + f"an ARC-4 bare app call should receive no arguments but got {args}" + ) + return None + + a_len, t_len = len(args), len(arg_types) + if not a_len == t_len: + return f"length mismatch between args ({a_len}) and arg_types ({t_len})" + + if args[0] != method_selector: + return f"first argument should be method selector {method_selector} but was {args[0]}" + + return None + + def dry_run_on_sequence( self, algod: AlgodClient, method: Optional[str] = None, is_app_create: bool = False, on_complete: OnComplete = OnComplete.NoOpOC, inputs: Optional[List[Sequence[PY_TYPES]]] = None, + *, + provided_input_has_selector: bool = True, ) -> List["DryRunInspector"]: """ARC-4 Compliant Dry Run""" # TODO: handle txn_params @@ -724,15 +759,6 @@ def dry_run( if inputs is None: inputs = self.generate_inputs(method) - else: - processed: List[Sequence[PY_TYPES]] = [] - selector = self.contract.get_method_by_name(method).get_selector() - for i, args in enumerate(inputs): - assert len(args) == len( - arg_types - ), f"length mismatch for args=inputs[{i}]: Should be {len(arg_types)} but was {len(args)}" - processed.append(tuple([selector] + list(args))) - inputs = processed return DryRunExecutor.dryrun_app_on_sequence( algod, @@ -943,16 +969,23 @@ def dig(self, dr_property: DryRunProperty, **kwargs: Dict[str, Any]) -> Any: return self.extracts["status"] == "REJECT" if dr_property == DryRunProperty.error: + """ + * when `contains` kwarg is NOT provided + - asserts that there was an error + * when `contains` kwarg IS provided + - asserts that there was an error AND that it's message includes `contains`'s value + """ contains = kwargs.get("contains") ok, msg = assert_error( self.parent_dryrun_response, contains=contains, enforce=False ) - # when there WAS an error, we return its msg, else False return ok if dr_property == DryRunProperty.errorMessage: + """ + * when there was no error, we return None, else return its msg + """ _, msg = assert_no_error(self.parent_dryrun_response, enforce=False) - # when there was no error, we return None, else return its msg return msg if msg else None if dr_property == DryRunProperty.lastMessage: From 2ad23a1a2ffa9a5ec4f953f4cf126be66fcb9733 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Sun, 12 Jun 2022 00:15:32 -0500 Subject: [PATCH 10/23] negative space eploration: 1. disallowed is_create_app + on_complete combos --- tests/integration/abi_test.py | 48 ++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index fcb411a8..813f4809 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -302,7 +302,11 @@ def test_roundtrip_abi_strategy(roundtrip_app): ), ( "empty_return_subroutine", - [(False, OnComplete.NoOpOC), (False, OnComplete.OptInOC)], + [ + (False, OnComplete.NoOpOC), + (False, OnComplete.OptInOC), + (True, OnComplete.OptInOC), + ], { DRProp.passed: True, DRProp.lastLog: DryRunEncoder.hex( @@ -321,8 +325,8 @@ def test_roundtrip_abi_strategy(roundtrip_app): {DRProp.passed: True, DRProp.lastLog: "logging creation"}, ), ( - "approve_if_odd", - [], # this should only appear in the clear-state program + "approve_if_odd", # this should only appear in the clear-state program + [], { DRProp.passed: True, DRProp.lastLog: "THIS MAKES ABSOLUTELY NO SENSE ... SHOULD NEVER GET HERE!!!", @@ -358,7 +362,7 @@ def test_method_or_barecall_positive(method, call_types, invariants): invariants = Invariant.as_invariants(invariants) for is_app_create, on_complete in call_types: - inspectors = ace.dry_run( + inspectors = ace.dry_run_on_sequence( algod, method=method, is_app_create=is_app_create, @@ -368,8 +372,19 @@ def test_method_or_barecall_positive(method, call_types, invariants): invariant.validates(dr_property, inspectors) -@pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CASES) -def test_method_or_barecall_negative(method, call_types, invariants): +NEGATIVE_INVARIANTS = Invariant.as_invariants( + { + DRProp.rejected: True, + DRProp.error: True, + DRProp.errorMessage: lambda _, actual: ( + ("assert failed" in actual) or ("err opcode" in actual) + ), + } +) + + +@pytest.mark.parametrize("method, call_types, _", QUESTIONABLE_CASES) +def test_method_or_barecall_negative(method, call_types, _): """ Test the _negative_ version of a case. In other words, ensure that for the given: * method or bare call @@ -389,4 +404,23 @@ def test_method_or_barecall_negative(method, call_types, invariants): for iac_n_oc in product((True, False), OnComplete) if iac_n_oc not in call_types ] - x = 42 + good_inputs = ace.generate_inputs(method) + + # I. explore all UNEXPECTED (is_app_create, on_complete) combos + for is_app_create, on_complete in call_types_negation: + inspectors = ace.dry_run_on_sequence( + algod, + method=method, + is_app_create=is_app_create, + on_complete=on_complete, + inputs=good_inputs, + ) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + msg = f""" +TEST CASE: +method={method} +is_app_create={is_app_create} +on_complete={on_complete!r} +dr_prop={dr_prop} +invariant={invariant}""" + invariant.validates(dr_prop, inspectors, msg=msg) From 5ed03a38db14ae2c50f5e8d5d582096c56992dc5 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Sun, 12 Jun 2022 01:10:15 -0500 Subject: [PATCH 11/23] make note of this failing test - shows that router doesn't guard against too many args --- graviton/blackbox.py | 22 ++++++++++--- tests/integration/abi_test.py | 58 ++++++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/graviton/blackbox.py b/graviton/blackbox.py index 586cdd95..7837e199 100644 --- a/graviton/blackbox.py +++ b/graviton/blackbox.py @@ -708,7 +708,13 @@ def gen_args(): return [gen_args() for _ in range(self.dry_runs)] - def validate_inputs(self, method: str, inputs: List[Sequence[PY_TYPES]]): + def validate_inputs(self, method: Optional[str], inputs: List[Sequence[PY_TYPES]]): + if not method: + assert not any( + inputs + ), f"bare app calls require args to be empty but inputs={inputs}" + return + arg_types = self.argument_types(method) selector = self.contract.get_method_by_name(method).get_selector() for i, args in enumerate(inputs): @@ -749,17 +755,25 @@ def dry_run_on_sequence( on_complete: OnComplete = OnComplete.NoOpOC, inputs: Optional[List[Sequence[PY_TYPES]]] = None, *, - provided_input_has_selector: bool = True, + arg_types: Optional[List[abi.ABIType]] = None, + return_type: Optional[abi.ABIType] = None, + validate_inputs: bool = True, ) -> List["DryRunInspector"]: """ARC-4 Compliant Dry Run""" # TODO: handle txn_params - arg_types = self.argument_types(method) - return_type = self.return_type(method) + if not arg_types: + arg_types = self.argument_types(method) + + if not return_type: + return_type = self.return_type(method) if inputs is None: inputs = self.generate_inputs(method) + if validate_inputs: + self.validate_inputs(method, inputs) + return DryRunExecutor.dryrun_app_on_sequence( algod, self.program, diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 813f4809..3cc7d1ab 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -405,6 +405,16 @@ def test_method_or_barecall_negative(method, call_types, _): if iac_n_oc not in call_types ] good_inputs = ace.generate_inputs(method) + good_arg_types = ace.argument_types(method) + + def msg(): + return f""" +TEST CASE: +method={method} +is_app_create={is_app_create} +on_complete={on_complete!r} +dr_prop={dr_prop} +invariant={invariant}""" # I. explore all UNEXPECTED (is_app_create, on_complete) combos for is_app_create, on_complete in call_types_negation: @@ -416,11 +426,43 @@ def test_method_or_barecall_negative(method, call_types, _): inputs=good_inputs, ) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): - msg = f""" -TEST CASE: -method={method} -is_app_create={is_app_create} -on_complete={on_complete!r} -dr_prop={dr_prop} -invariant={invariant}""" - invariant.validates(dr_prop, inspectors, msg=msg) + invariant.validates(dr_prop, inspectors, msg=msg()) + + # II. explore changing the number of args over the "good" call_types + if good_inputs and good_inputs[0]: + extra_arg = [args + (args[-1],) for args in good_inputs] + extra_arg_types = good_arg_types + [good_arg_types[-1]] + + missing_arg = [args[:-1] for args in good_inputs] + missing_arg_types = good_arg_types[:-1] + else: + extra_arg = ["testing" for _ in good_inputs] + extra_arg_types = [abi.StringType()] + + missing_arg = None + + for is_app_create, on_complete in call_types: + inspectors = ace.dry_run_on_sequence( + algod, + method=method, + is_app_create=is_app_create, + on_complete=on_complete, + inputs=extra_arg, + validate_inputs=False, + arg_types=extra_arg_types, + ) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) + + if missing_arg: + inspectors = ace.dry_run_on_sequence( + algod, + method=method, + is_app_create=is_app_create, + on_complete=on_complete, + inputs=missing_arg, + validate_inputs=False, + arg_types=missing_arg_types, + ) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) From b54296b9c87e0b5ec0983b3471ed1cc2a3591e6a Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Sun, 12 Jun 2022 23:32:00 -0500 Subject: [PATCH 12/23] only failing because of bugs --- graviton/blackbox.py | 4 +- graviton/invariant.py | 6 ++- tests/integration/abi_test.py | 72 +++++++++++++++++++++-------------- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/graviton/blackbox.py b/graviton/blackbox.py index 7837e199..ad010b91 100644 --- a/graviton/blackbox.py +++ b/graviton/blackbox.py @@ -762,10 +762,10 @@ def dry_run_on_sequence( """ARC-4 Compliant Dry Run""" # TODO: handle txn_params - if not arg_types: + if arg_types is None: arg_types = self.argument_types(method) - if not return_type: + if return_type is None: return_type = self.return_type(method) if inputs is None: diff --git a/graviton/invariant.py b/graviton/invariant.py index 3fb1b688..4c923993 100644 --- a/graviton/invariant.py +++ b/graviton/invariant.py @@ -33,7 +33,11 @@ def __init__( self.name = name def __repr__(self): - return f"Invariant({self.definition})"[:100] + defn = self.definition + if callable(defn): + defn = getsource(defn) + + return f"Invariant({defn})" def __call__(self, args: Sequence[PY_TYPES], actual: PY_TYPES) -> Tuple[bool, str]: invariant = self.predicate(args, actual) diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 3cc7d1ab..9c6e8e78 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -13,9 +13,11 @@ * then copy the contents of the generated directory "./tests/integration/generated/roundtrip/" into the ROUNDTRIP directory """ +import inspect from itertools import product from pathlib import Path import pytest +import re from typing import Any, Dict, List, Optional, Tuple from algosdk import abi @@ -349,7 +351,7 @@ def test_method_or_barecall_positive(method, call_types, invariants): * method or bare call * OnComplete value * number of arguments - that the app call succeeds for the given invariants + that the app call succeeds according to the provided _invariants success definition_ """ ace = QUESTIONABLE_ACE algod = get_algod() @@ -372,12 +374,20 @@ def test_method_or_barecall_positive(method, call_types, invariants): invariant.validates(dr_property, inspectors) +# cf. https://death.andgravity.com/f-re for an explanation of verbose regex'es +EXPECTED_ERR_PATTERN = r""" + assert\ failed # pyteal generated assert's ok +| err\ opcode # pyteal generated err's ok +| invalid\ ApplicationArgs\ index # failing because an app arg wasn't provided +| extract\ range\ beyond\ length\ of\ string # failing because couldn't extract from jammed in tuple +""" + NEGATIVE_INVARIANTS = Invariant.as_invariants( { DRProp.rejected: True, DRProp.error: True, DRProp.errorMessage: lambda _, actual: ( - ("assert failed" in actual) or ("err opcode" in actual) + bool(re.search(EXPECTED_ERR_PATTERN, actual, re.VERBOSE)) ), } ) @@ -407,62 +417,68 @@ def test_method_or_barecall_negative(method, call_types, _): good_inputs = ace.generate_inputs(method) good_arg_types = ace.argument_types(method) + def dry_runner(**kwargs): + inputs = kwargs.get("inputs") + assert inputs + + validate_inputs = kwargs.get("validate_inputs", True) + arg_types = kwargs.get("arg_types") + + return ace.dry_run_on_sequence( + algod, + method=method, + is_app_create=is_app_create, + on_complete=on_complete, + inputs=inputs, + validate_inputs=validate_inputs, + arg_types=arg_types, + ) + def msg(): return f""" TEST CASE: +test_function={inspect.stack()[1][3]} +scenario={scenario} method={method} is_app_create={is_app_create} on_complete={on_complete!r} dr_prop={dr_prop} invariant={invariant}""" - # I. explore all UNEXPECTED (is_app_create, on_complete) combos + scenario = "I. explore all UNEXPECTED (is_app_create, on_complete) combos" for is_app_create, on_complete in call_types_negation: - inspectors = ace.dry_run_on_sequence( - algod, - method=method, - is_app_create=is_app_create, - on_complete=on_complete, - inputs=good_inputs, - ) + inspectors = dry_runner(inputs=good_inputs) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) - # II. explore changing the number of args over the "good" call_types + # II. explore changing the number of args over the 'good' call_types if good_inputs and good_inputs[0]: extra_arg = [args + (args[-1],) for args in good_inputs] extra_arg_types = good_arg_types + [good_arg_types[-1]] missing_arg = [args[:-1] for args in good_inputs] missing_arg_types = good_arg_types[:-1] + if not missing_arg[0]: + # skip this, as this becomes a bare app call case which is tested elsewhere + missing_arg = None else: - extra_arg = ["testing" for _ in good_inputs] + extra_arg = [("testing",) for _ in good_inputs] extra_arg_types = [abi.StringType()] missing_arg = None for is_app_create, on_complete in call_types: - inspectors = ace.dry_run_on_sequence( - algod, - method=method, - is_app_create=is_app_create, - on_complete=on_complete, - inputs=extra_arg, - validate_inputs=False, - arg_types=extra_arg_types, + scenario = "II(a). adding an extra duplicate argument" + inspectors = dry_runner( + inputs=extra_arg, validate_inputs=False, arg_types=extra_arg_types ) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) if missing_arg: - inspectors = ace.dry_run_on_sequence( - algod, - method=method, - is_app_create=is_app_create, - on_complete=on_complete, - inputs=missing_arg, - validate_inputs=False, - arg_types=missing_arg_types, + scenario = "II(b). removing the final argument" + inspectors = dry_runner( + inputs=missing_arg, validate_inputs=False, arg_types=missing_arg_types ) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) From 22ae13b5c80a2d07c139b69a3d0efbd0a3661831 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Mon, 13 Jun 2022 00:43:03 -0500 Subject: [PATCH 13/23] finish negative case but temporarily_skip_iia --- .flake8 | 2 +- tests/integration/abi_test.py | 66 ++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/.flake8 b/.flake8 index a6f727b8..fe326c8d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,2 @@ [flake8] -ignore = E501, W503 +ignore = E203, E501, W503 diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 9c6e8e78..33531c11 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -17,6 +17,7 @@ from itertools import product from pathlib import Path import pytest +import random import re from typing import Any, Dict, List, Optional, Tuple @@ -393,6 +394,9 @@ def test_method_or_barecall_positive(method, call_types, invariants): ) +temporarily_skip_iia = True + + @pytest.mark.parametrize("method, call_types, _", QUESTIONABLE_CASES) def test_method_or_barecall_negative(method, call_types, _): """ @@ -469,11 +473,12 @@ def msg(): for is_app_create, on_complete in call_types: scenario = "II(a). adding an extra duplicate argument" - inspectors = dry_runner( - inputs=extra_arg, validate_inputs=False, arg_types=extra_arg_types - ) - for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): - invariant.validates(dr_prop, inspectors, msg=msg()) + if not temporarily_skip_iia: + inspectors = dry_runner( + inputs=extra_arg, validate_inputs=False, arg_types=extra_arg_types + ) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) if missing_arg: scenario = "II(b). removing the final argument" @@ -482,3 +487,54 @@ def msg(): ) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) + + # III. explore changing method selector arg[0] by edit distance 1 + if good_inputs and good_inputs[0]: + + def factory(action): + def selector_mod(args): + args = args[:] + selector = args[0] + idx = random.randint(0, 4) + prefix, suffix = selector[:idx], selector[idx:] + if action == "insert": + selector = prefix + random.randbytes(1) + suffix + elif action == "delete": + selector = (prefix[:-1] + suffix) if prefix else (suffix[:-1]) + else: # "replace" + assert ( + action == "replace" + ), f"expected action=replace but got [{action}]" + idx = random.randint(0, 3) + selector = ( + selector[:idx] + + bytes([(selector[idx] + 1) % 256]) + + selector[idx + 1 :] + ) + return (selector,) + args[1:] + + return selector_mod + + selectors_inserted = map(factory("insert"), good_inputs) + selectors_deleted = map(factory("delete"), good_inputs) + selectors_modded = map(factory("replace"), good_inputs) + else: + selectors_inserted = selectors_deleted = selectors_modded = None + + scenario = "III(a). inserting an extra random byte into method selector" + if selectors_inserted: + inspectors = dry_runner(inputs=selectors_inserted, validate_inputs=False) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) + + scenario = "III(b). removing a random byte from method selector" + if selectors_deleted: + inspectors = dry_runner(inputs=selectors_deleted, validate_inputs=False) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) + + scenario = "III(c). replacing a random byte in method selector" + if selectors_modded: + inspectors = dry_runner(inputs=selectors_modded, validate_inputs=False) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) From 5a676db70fbfdf8124ecaa8b3e73c6bbd34b9d97 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Mon, 13 Jun 2022 01:06:06 -0500 Subject: [PATCH 14/23] bump min python from 3.8 to 3.8 because need random.randbytes() --- .github/workflows/build.yml | 4 ++-- CHANGELOG.md | 4 ++++ setup.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f91c0398..71801ec3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: container: python:${{ matrix.python }} strategy: matrix: - python: [ "3.8", "3.9", "3.10" ] + python: [ "3.9", "3.10" ] steps: - run: python3 --version - name: Check out code @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python: [ "3.8", "3.9", "3.10" ] + python: [ "3.9", "3.10" ] steps: - name: Check out code uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 146c6039..03f13635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ * A bug that made all app calls run as if during creation * Addressed [Issue #5](https://github.com/algorand/graviton/issues/5): Better assertion message for invariant predicates of 2 variables +### Upgraded + +* Minimum python is bumped up to 3.9 (previously 3.8) + ## `v0.3.0` (_aka_ 🐈) ### Added diff --git a/setup.py b/setup.py index d145aca5..022636c5 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ long_description=long_description, author="Algorand", author_email="pypiservice@algorand.com", - python_requires=">=3.8", + python_requires=">=3.9", install_requires=[ "py-algorand-sdk @ git+https://github.com/algorand/py-algorand-sdk@get-method-by-name", "tabulate==0.8.9", From e7609a4a1afa0840249db22713beae26cc42e42f Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Mon, 13 Jun 2022 09:19:05 -0500 Subject: [PATCH 15/23] remove temporarily_skip_iia --- tests/integration/abi_test.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 33531c11..b0170542 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -394,9 +394,6 @@ def test_method_or_barecall_positive(method, call_types, invariants): ) -temporarily_skip_iia = True - - @pytest.mark.parametrize("method, call_types, _", QUESTIONABLE_CASES) def test_method_or_barecall_negative(method, call_types, _): """ @@ -473,12 +470,11 @@ def msg(): for is_app_create, on_complete in call_types: scenario = "II(a). adding an extra duplicate argument" - if not temporarily_skip_iia: - inspectors = dry_runner( - inputs=extra_arg, validate_inputs=False, arg_types=extra_arg_types - ) - for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): - invariant.validates(dr_prop, inspectors, msg=msg()) + inspectors = dry_runner( + inputs=extra_arg, validate_inputs=False, arg_types=extra_arg_types + ) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) if missing_arg: scenario = "II(b). removing the final argument" From 9680deceea0c317e7f641197babe7fb4678fe5f8 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Tue, 14 Jun 2022 18:11:49 -0500 Subject: [PATCH 16/23] Update tests/teal/router/questionable.teal Co-authored-by: Michael Diamant --- tests/teal/router/questionable.teal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/teal/router/questionable.teal b/tests/teal/router/questionable.teal index 5a6539c1..c4414e9b 100644 --- a/tests/teal/router/questionable.teal +++ b/tests/teal/router/questionable.teal @@ -292,7 +292,7 @@ return main_l19: txn OnCompletion int NoOp -== // I believe there ahould be an assertion here to ensure that this is actually a NoOp +== txn ApplicationID int 0 != From b4dbc832214ff017469468f94a866b80bb7f037c Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Tue, 14 Jun 2022 18:33:28 -0500 Subject: [PATCH 17/23] refactoring and respond to CR suggestions --- graviton/abi_strategy.py | 4 - tests/integration/abi_test.py | 213 ++++++++++++++++------ tests/teal/router/questionable_clear.teal | 67 +++++++ 3 files changed, 224 insertions(+), 60 deletions(-) create mode 100644 tests/teal/router/questionable_clear.teal diff --git a/graviton/abi_strategy.py b/graviton/abi_strategy.py index 0d3be7e8..a17e19ce 100644 --- a/graviton/abi_strategy.py +++ b/graviton/abi_strategy.py @@ -15,10 +15,6 @@ class ABIStrategy(ABC): - @abstractmethod - def __init__(self, abi_instance: abi.ABIType, dynamic_length: Optional[int] = None): - pass - @abstractmethod def get(self) -> PY_TYPES: pass diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index b0170542..1e9e13a8 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -233,7 +233,7 @@ def test_roundtrip_abi_strategy(roundtrip_app): ) -# --- ABI Router Dry Run Testing --- # +# ---- ABI Router Dry Run Testing - SETUP ---- # ROUTER = Path.cwd() / "tests" / "teal" / "router" NUM_ROUTER_DRYRUNS = 7 @@ -247,6 +247,10 @@ def test_roundtrip_abi_strategy(roundtrip_app): with open(ROUTER / "questionable.teal") as f: QUESTIONABLE_TEAL = f.read() +QUESTIONABLE_CLEAR_TEAL = None +with open(ROUTER / "questionable_clear.teal") as f: + QUESTIONABLE_CLEAR_TEAL = f.read() + QUESTIONABLE_ACE = ABIContractExecutor( QUESTIONABLE_TEAL, QUESTIONABLE_CONTRACT, @@ -254,20 +258,28 @@ def test_roundtrip_abi_strategy(roundtrip_app): dry_runs=NUM_ROUTER_DRYRUNS, ) +QUESTIONABLE_CLEAR_ACE = ABIContractExecutor( + QUESTIONABLE_CLEAR_TEAL, + QUESTIONABLE_CONTRACT, # weird, but the methods in the clear program do belong to the contract + argument_strategy=RandomABIStrategyHalfSized, + dry_runs=NUM_ROUTER_DRYRUNS, +) + + +# LEGEND FOR TEST CASES (*_CASES and *_CLEAR_CASES): +# +# * @0 - method: str | None +# method name when `str` or bare app call when `None` +# +# * @1 - call_types: ...tuple[bool, OncComplete] | None +# [(is_app_create, `OnComplete`), ...] contexts to test (`None` is short-hand for `[(False, OnComplete.NoOpOC)]`) +# +# * @2 - invariants: dict[DRProp, Any] +# these are being asserted after being processed into actual Invariant's +# QUESTIONABLE_CASES: List[ Tuple[Optional[str], Optional[List[Tuple[bool, OnComplete]]], Dict[DRProp, Any]] ] = [ - # LEGEND: - # - # * @0 - method: str | None - # method name when `str` or bare app call when `None` - # - # * @1 - call_types: ...tuple[bool, OncComplete] | None - # [(is_app_create, `OnComplete`), ...] contexts to test (`None` is short-hand for `[(False, OnComplete.NoOpOC)]`) - # - # * @2 - invariants: dict[DRProp, Any] - # these are being asserted after being processed into actual Invariant's - # ( "add", None, @@ -342,11 +354,82 @@ def test_roundtrip_abi_strategy(roundtrip_app): ), ] -# TODO: need to test the clear program as well! +QUESTIONABLE_CLEAR_CASES: List[ + Tuple[Optional[str], Optional[List[Tuple[bool, OnComplete]]], Dict[DRProp, Any]] +] = [ + ( + "add", + [], # shouldn't appear in PyTEAL generated clear state program + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] + args[2]}, + ), + ( + "sub", + [], # shouldn't appear in PyTEAL generated clear state program + { + DRProp.passed: lambda args: args[1] >= args[2], + DRProp.lastLog: lambda args, actual: True + if args[1] < args[2] + else actual == args[1] - args[2], + }, + ), + ( + "mul", + [], # shouldn't appear in PyTEAL generated clear state program + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] * args[2]}, + ), + ( + "div", + [], # shouldn't appear in PyTEAL generated clear state program + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] // args[2]}, + ), + ( + "mod", + [], # shouldn't appear in PyTEAL generated clear state program + {DRProp.passed: True, DRProp.lastLog: lambda args: args[1] % args[2]}, + ), + ( + "all_laid_to_args", + [], # shouldn't appear in PyTEAL generated clear state program + {DRProp.passed: True, DRProp.lastLog: lambda args: sum(args[1:])}, + ), + ( + "empty_return_subroutine", + [ + (False, OnComplete.ClearStateOC), + ], + { + DRProp.passed: True, + DRProp.lastLog: DryRunEncoder.hex( + "appear in both approval and clear state" + ), + }, + ), + ( + "log_1", + [(False, OnComplete.ClearStateOC)], + {DRProp.passed: True, DRProp.lastLog: 1}, + ), + ( + "log_creation", + [], # shouldn't appear in PyTEAL generated clear state program + {DRProp.passed: True, DRProp.lastLog: "logging creation"}, + ), + ( + "approve_if_odd", + [(False, OnComplete.ClearStateOC)], + { + DRProp.passed: lambda args: bool(args[1] % 2), + }, + ), + ( + None, + [(False, OnComplete.ClearStateOC)], + {DRProp.passed: True, DRProp.lastLog: None}, + ), +] -@pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CASES) -def test_method_or_barecall_positive(method, call_types, invariants): +def method_or_barecall_positive_test_runner(ace, method, call_types, invariants): """ Test the _positive_ version of a case. In other words, ensure that for the given: * method or bare call @@ -354,7 +437,6 @@ def test_method_or_barecall_positive(method, call_types, invariants): * number of arguments that the app call succeeds according to the provided _invariants success definition_ """ - ace = QUESTIONABLE_ACE algod = get_algod() if call_types is None: @@ -363,6 +445,16 @@ def test_method_or_barecall_positive(method, call_types, invariants): if not call_types: return + def msg(): + return f""" +TEST CASE: +test_function={inspect.stack()[2][3]} +method={method} +is_app_create={is_app_create} +on_complete={on_complete!r} +dr_prop={dr_property} +invariant={invariant}""" + invariants = Invariant.as_invariants(invariants) for is_app_create, on_complete in call_types: inspectors = ace.dry_run_on_sequence( @@ -372,7 +464,7 @@ def test_method_or_barecall_positive(method, call_types, invariants): on_complete=on_complete, ) for dr_property, invariant in invariants.items(): - invariant.validates(dr_property, inspectors) + invariant.validates(dr_property, inspectors, msg=msg()) # cf. https://death.andgravity.com/f-re for an explanation of verbose regex'es @@ -394,8 +486,7 @@ def test_method_or_barecall_positive(method, call_types, invariants): ) -@pytest.mark.parametrize("method, call_types, _", QUESTIONABLE_CASES) -def test_method_or_barecall_negative(method, call_types, _): +def method_or_barecall_negative_test_runner(ace, method, call_types): """ Test the _negative_ version of a case. In other words, ensure that for the given: * method or bare call @@ -403,7 +494,6 @@ def test_method_or_barecall_negative(method, call_types, _): * number of arguments explore the space _OUTSIDE_ of each constraint and assert that the app call FAILS!!! """ - ace = QUESTIONABLE_ACE algod = get_algod() if call_types is None: @@ -452,39 +542,7 @@ def msg(): for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) - # II. explore changing the number of args over the 'good' call_types - if good_inputs and good_inputs[0]: - extra_arg = [args + (args[-1],) for args in good_inputs] - extra_arg_types = good_arg_types + [good_arg_types[-1]] - - missing_arg = [args[:-1] for args in good_inputs] - missing_arg_types = good_arg_types[:-1] - if not missing_arg[0]: - # skip this, as this becomes a bare app call case which is tested elsewhere - missing_arg = None - else: - extra_arg = [("testing",) for _ in good_inputs] - extra_arg_types = [abi.StringType()] - - missing_arg = None - - for is_app_create, on_complete in call_types: - scenario = "II(a). adding an extra duplicate argument" - inspectors = dry_runner( - inputs=extra_arg, validate_inputs=False, arg_types=extra_arg_types - ) - for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): - invariant.validates(dr_prop, inspectors, msg=msg()) - - if missing_arg: - scenario = "II(b). removing the final argument" - inspectors = dry_runner( - inputs=missing_arg, validate_inputs=False, arg_types=missing_arg_types - ) - for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): - invariant.validates(dr_prop, inspectors, msg=msg()) - - # III. explore changing method selector arg[0] by edit distance 1 + # II. explore changing method selector arg[0] by edit distance 1 if good_inputs and good_inputs[0]: def factory(action): @@ -517,20 +575,63 @@ def selector_mod(args): else: selectors_inserted = selectors_deleted = selectors_modded = None - scenario = "III(a). inserting an extra random byte into method selector" + scenario = "II(a). inserting an extra random byte into method selector" if selectors_inserted: inspectors = dry_runner(inputs=selectors_inserted, validate_inputs=False) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) - scenario = "III(b). removing a random byte from method selector" + scenario = "II(b). removing a random byte from method selector" if selectors_deleted: inspectors = dry_runner(inputs=selectors_deleted, validate_inputs=False) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) - scenario = "III(c). replacing a random byte in method selector" + scenario = "II(c). replacing a random byte in method selector" if selectors_modded: inspectors = dry_runner(inputs=selectors_modded, validate_inputs=False) for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): invariant.validates(dr_prop, inspectors, msg=msg()) + + # III. explore changing the number of args over the 'good' call_types + # (extra args testing is omitted as this is prevented by SDK's cf. https://github.com/algorand/algorand-sdk-testing/issues/190) + if good_inputs and good_inputs[0]: + missing_arg = [args[:-1] for args in good_inputs] + missing_arg_types = good_arg_types[:-1] + if not missing_arg[0]: + # skip this, as this becomes a bare app call case which is tested elsewhere + return + + for is_app_create, on_complete in call_types: + scenario = "III. removing the final argument" + inspectors = dry_runner( + inputs=missing_arg, validate_inputs=False, arg_types=missing_arg_types + ) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) + + +# ---- ABI Router Dry Run Testing - TESTS ---- # + + +@pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CASES) +def test_questionable_approval_method_or_barecall_positive( + method, call_types, invariants +): + method_or_barecall_positive_test_runner( + QUESTIONABLE_ACE, method, call_types, invariants + ) + + +@pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CLEAR_CASES) +def test_questionable_clear_method_or_barecall_positive(method, call_types, invariants): + method_or_barecall_positive_test_runner( + QUESTIONABLE_CLEAR_ACE, method, call_types, invariants + ) + + +@pytest.mark.parametrize("method, call_types, _", QUESTIONABLE_CASES) +def test_questionable_approval_program_method_or_barecall_negative( + method, call_types, _ +): + method_or_barecall_negative_test_runner(QUESTIONABLE_ACE, method, call_types) diff --git a/tests/teal/router/questionable_clear.teal b/tests/teal/router/questionable_clear.teal new file mode 100644 index 00000000..624f5d64 --- /dev/null +++ b/tests/teal/router/questionable_clear.teal @@ -0,0 +1,67 @@ +#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l8 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l7 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l6 +txna ApplicationArgs 0 +method "approve_if_odd(uint32)void" +== +bnz main_l5 +err +main_l5: +txna ApplicationArgs 1 +int 0 +extract_uint32 +store 2 +load 2 +callsub approveifodd_2 +int 1 +return +main_l6: +callsub log1_1 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return +main_l7: +callsub emptyreturnsubroutine_0 +int 1 +return +main_l8: +int 1 +return +// empty_return_subroutine +emptyreturnsubroutine_0: +byte "appear in both approval and clear state" +log +retsub +// log_1 +log1_1: +int 1 +store 0 +load 0 +retsub +// approve_if_odd +approveifodd_2: +store 3 +load 3 +int 2 +% +bnz approveifodd_2_l2 +int 0 +return +approveifodd_2_l2: +int 1 +return \ No newline at end of file From 11800d211790f54e7273b1152b6c720c08eb8d61 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Tue, 14 Jun 2022 18:49:07 -0500 Subject: [PATCH 18/23] all tests in place for "QUESTIONABLE" approval + clear X positive + negative --- tests/integration/abi_test.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 1e9e13a8..8f2bb292 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -486,7 +486,9 @@ def msg(): ) -def method_or_barecall_negative_test_runner(ace, method, call_types): +def method_or_barecall_negative_test_runner( + ace, method, call_types, is_clear_state_program +): """ Test the _negative_ version of a case. In other words, ensure that for the given: * method or bare call @@ -508,6 +510,8 @@ def method_or_barecall_negative_test_runner(ace, method, call_types): good_inputs = ace.generate_inputs(method) good_arg_types = ace.argument_types(method) + is_app_create = on_complete = None + def dry_runner(**kwargs): inputs = kwargs.get("inputs") assert inputs @@ -529,6 +533,7 @@ def msg(): return f""" TEST CASE: test_function={inspect.stack()[1][3]} +is_clear_state_program={is_clear_state_program} scenario={scenario} method={method} is_app_create={is_app_create} @@ -536,11 +541,12 @@ def msg(): dr_prop={dr_prop} invariant={invariant}""" - scenario = "I. explore all UNEXPECTED (is_app_create, on_complete) combos" - for is_app_create, on_complete in call_types_negation: - inspectors = dry_runner(inputs=good_inputs) - for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): - invariant.validates(dr_prop, inspectors, msg=msg()) + if not is_clear_state_program: + scenario = "I. explore all UNEXPECTED (is_app_create, on_complete) combos" + for is_app_create, on_complete in call_types_negation: + inspectors = dry_runner(inputs=good_inputs) + for dr_prop, invariant in NEGATIVE_INVARIANTS.items(): + invariant.validates(dr_prop, inspectors, msg=msg()) # II. explore changing method selector arg[0] by edit distance 1 if good_inputs and good_inputs[0]: @@ -634,4 +640,13 @@ def test_questionable_clear_method_or_barecall_positive(method, call_types, inva def test_questionable_approval_program_method_or_barecall_negative( method, call_types, _ ): - method_or_barecall_negative_test_runner(QUESTIONABLE_ACE, method, call_types) + method_or_barecall_negative_test_runner( + QUESTIONABLE_ACE, method, call_types, is_clear_state_program=False + ) + + +@pytest.mark.parametrize("method, call_types, _", QUESTIONABLE_CLEAR_CASES) +def test_questionable_clear_program_method_or_barecall_negative(method, call_types, _): + method_or_barecall_negative_test_runner( + QUESTIONABLE_ACE, method, call_types, is_clear_state_program=True + ) From 3fc9a9f53b43955949a226d03c60b9774733a27d Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Tue, 14 Jun 2022 19:16:15 -0500 Subject: [PATCH 19/23] looks like recent updates of dry run give log when rejecting... adjust accordingly --- tests/integration/blackbox_test.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/integration/blackbox_test.py b/tests/integration/blackbox_test.py index 327995ee..ca597c1a 100644 --- a/tests/integration/blackbox_test.py +++ b/tests/integration/blackbox_test.py @@ -206,11 +206,7 @@ def prop_assert(dr_resp, actual, expected): "inputs": [(i,) for i in range(100)], "invariants": { DRProp.cost: 14, - DRProp.lastLog: { - # since execution REJECTS for 0, expect last log for this case to be None - (i,): Encoder.hex(i * i) if i else None - for i in range(100) - }, + DRProp.lastLog: {(i,): Encoder.hex(i * i) for i in range(100)}, DRProp.finalScratch: lambda args: ( {0: args[0], 1: args[0] ** 2} if args[0] else {} ), @@ -247,9 +243,7 @@ def prop_assert(dr_resp, actual, expected): "inputs": [("xyzw", i) for i in range(100)], "invariants": { DRProp.cost: lambda args: 30 + 15 * args[1], - DRProp.lastLog: ( - lambda args: Encoder.hex(args[0] * args[1]) if args[1] else None - ), + DRProp.lastLog: (lambda args: Encoder.hex(args[0] * args[1])), # due to dryrun 0-scratchvar artifact, special case for i == 0: DRProp.finalScratch: lambda args: ( { @@ -308,7 +302,7 @@ def prop_assert(dr_resp, actual, expected): "invariants": { DRProp.cost: lambda args: (fib_cost(args) if args[0] < 17 else 70_000), DRProp.lastLog: lambda args: ( - Encoder.hex(fib(args[0])) if 0 < args[0] < 17 else None + Encoder.hex(fib(args[0])) if args[0] < 17 else None ), DRProp.finalScratch: lambda args, actual: ( actual == {0: args[0], 1: fib(args[0])} From 0727a96380d6785bef310efcf16d69d20f563bad Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Tue, 14 Jun 2022 19:43:26 -0500 Subject: [PATCH 20/23] yacc copy pasta --- tests/integration/abi_test.py | 62 +++++ tests/teal/router/yacc.teal | 437 ++++++++++++++++++++++++++++++ tests/teal/router/yacc_clear.teal | 60 ++++ 3 files changed, 559 insertions(+) create mode 100644 tests/teal/router/yacc.teal create mode 100644 tests/teal/router/yacc_clear.teal diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index 8f2bb292..ddcef82e 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -251,6 +251,15 @@ def test_roundtrip_abi_strategy(roundtrip_app): with open(ROUTER / "questionable_clear.teal") as f: QUESTIONABLE_CLEAR_TEAL = f.read() +YACC_TEAL = None +with open(ROUTER / "yacc.teal") as f: + YACC_TEAL = f.read() + +YACC_CLEAR_TEAL = None +with open(ROUTER / "yacc_clear.teal") as f: + YACC_CLEAR_TEAL = f.read() + + QUESTIONABLE_ACE = ABIContractExecutor( QUESTIONABLE_TEAL, QUESTIONABLE_CONTRACT, @@ -266,6 +275,21 @@ def test_roundtrip_abi_strategy(roundtrip_app): ) +YACC_ACE = ABIContractExecutor( + YACC_TEAL, + QUESTIONABLE_CONTRACT, # same JSON contract as QUESTIONABLE + argument_strategy=RandomABIStrategyHalfSized, + dry_runs=NUM_ROUTER_DRYRUNS, +) + +YACC_CLEAR_ACE = ABIContractExecutor( + YACC_CLEAR_TEAL, + QUESTIONABLE_CONTRACT, + argument_strategy=RandomABIStrategyHalfSized, + dry_runs=NUM_ROUTER_DRYRUNS, +) + + # LEGEND FOR TEST CASES (*_CASES and *_CLEAR_CASES): # # * @0 - method: str | None @@ -354,6 +378,9 @@ def test_roundtrip_abi_strategy(roundtrip_app): ), ] +# strip out the bare calls for YACC ~ "yetAnotherContractConstructedFromRouter": +YACC_CASES = [c for c in QUESTIONABLE_CASES if c[0]] + QUESTIONABLE_CLEAR_CASES: List[ Tuple[Optional[str], Optional[List[Tuple[bool, OnComplete]]], Dict[DRProp, Any]] ] = [ @@ -428,6 +455,10 @@ def test_roundtrip_abi_strategy(roundtrip_app): ), ] +# the YACC clear program accepts no bare calls! +# strip out the bare calls for YACC ~ "yetAnotherContractConstructedFromRouter": +YACC_CLEAR_CASES = [c for c in QUESTIONABLE_CLEAR_CASES if c[0]] + def method_or_barecall_positive_test_runner(ace, method, call_types, invariants): """ @@ -619,6 +650,8 @@ def selector_mod(args): # ---- ABI Router Dry Run Testing - TESTS ---- # +### QUESTIONABLE ### + @pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CASES) def test_questionable_approval_method_or_barecall_positive( @@ -650,3 +683,32 @@ def test_questionable_clear_program_method_or_barecall_negative(method, call_typ method_or_barecall_negative_test_runner( QUESTIONABLE_ACE, method, call_types, is_clear_state_program=True ) + + +### YACC (QUESTIONABLE Copy Pasta 🍝)### + + +@pytest.mark.parametrize("method, call_types, invariants", YACC_CASES) +def test_yacc_approval_method_or_barecall_positive(method, call_types, invariants): + method_or_barecall_positive_test_runner(YACC_ACE, method, call_types, invariants) + + +@pytest.mark.parametrize("method, call_types, invariants", YACC_CLEAR_CASES) +def test_yacc_clear_method_or_barecall_positive(method, call_types, invariants): + method_or_barecall_positive_test_runner( + YACC_CLEAR_ACE, method, call_types, invariants + ) + + +@pytest.mark.parametrize("method, call_types, _", YACC_CASES) +def test_yacc_approval_program_method_or_barecall_negative(method, call_types, _): + method_or_barecall_negative_test_runner( + YACC_ACE, method, call_types, is_clear_state_program=False + ) + + +@pytest.mark.parametrize("method, call_types, _", YACC_CLEAR_CASES) +def test_yacc_clear_program_method_or_barecall_negative(method, call_types, _): + method_or_barecall_negative_test_runner( + YACC_ACE, method, call_types, is_clear_state_program=True + ) diff --git a/tests/teal/router/yacc.teal b/tests/teal/router/yacc.teal new file mode 100644 index 00000000..0247dca0 --- /dev/null +++ b/tests/teal/router/yacc.teal @@ -0,0 +1,437 @@ +#pragma version 6 +txna ApplicationArgs 0 +method "add(uint64,uint64)uint64" +== +bnz main_l18 +txna ApplicationArgs 0 +method "sub(uint64,uint64)uint64" +== +bnz main_l17 +txna ApplicationArgs 0 +method "mul(uint64,uint64)uint64" +== +bnz main_l16 +txna ApplicationArgs 0 +method "div(uint64,uint64)uint64" +== +bnz main_l15 +txna ApplicationArgs 0 +method "mod(uint64,uint64)uint64" +== +bnz main_l14 +txna ApplicationArgs 0 +method "all_laid_to_args(uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64)uint64" +== +bnz main_l13 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l12 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l11 +txna ApplicationArgs 0 +method "log_creation()string" +== +bnz main_l10 +err +main_l10: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +== +&& +assert +callsub logcreation_8 +store 67 +byte 0x151f7c75 +load 67 +concat +log +int 1 +return +main_l11: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 +!= +&& +|| +assert +callsub log1_7 +store 65 +byte 0x151f7c75 +load 65 +itob +concat +log +int 1 +return +main_l12: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +|| +assert +callsub emptyreturnsubroutine_6 +int 1 +return +main_l13: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 30 +txna ApplicationArgs 2 +btoi +store 31 +txna ApplicationArgs 3 +btoi +store 32 +txna ApplicationArgs 4 +btoi +store 33 +txna ApplicationArgs 5 +btoi +store 34 +txna ApplicationArgs 6 +btoi +store 35 +txna ApplicationArgs 7 +btoi +store 36 +txna ApplicationArgs 8 +btoi +store 37 +txna ApplicationArgs 9 +btoi +store 38 +txna ApplicationArgs 10 +btoi +store 39 +txna ApplicationArgs 11 +btoi +store 40 +txna ApplicationArgs 12 +btoi +store 41 +txna ApplicationArgs 13 +btoi +store 42 +txna ApplicationArgs 14 +btoi +store 43 +txna ApplicationArgs 15 +store 46 +load 46 +int 0 +extract_uint64 +store 44 +load 46 +int 8 +extract_uint64 +store 45 +load 30 +load 31 +load 32 +load 33 +load 34 +load 35 +load 36 +load 37 +load 38 +load 39 +load 40 +load 41 +load 42 +load 43 +load 44 +load 45 +callsub alllaidtoargs_5 +store 47 +byte 0x151f7c75 +load 47 +itob +concat +log +int 1 +return +main_l14: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 24 +txna ApplicationArgs 2 +btoi +store 25 +load 24 +load 25 +callsub mod_4 +store 26 +byte 0x151f7c75 +load 26 +itob +concat +log +int 1 +return +main_l15: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 18 +txna ApplicationArgs 2 +btoi +store 19 +load 18 +load 19 +callsub div_3 +store 20 +byte 0x151f7c75 +load 20 +itob +concat +log +int 1 +return +main_l16: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 12 +txna ApplicationArgs 2 +btoi +store 13 +load 12 +load 13 +callsub mul_2 +store 14 +byte 0x151f7c75 +load 14 +itob +concat +log +int 1 +return +main_l17: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 6 +txna ApplicationArgs 2 +btoi +store 7 +load 6 +load 7 +callsub sub_1 +store 8 +byte 0x151f7c75 +load 8 +itob +concat +log +int 1 +return +main_l18: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 0 +txna ApplicationArgs 2 +btoi +store 1 +load 0 +load 1 +callsub add_0 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return +// add +add_0: +store 4 +store 3 +load 3 +load 4 ++ +store 5 +load 5 +retsub +// sub +sub_1: +store 10 +store 9 +load 9 +load 10 +- +store 11 +load 11 +retsub +// mul +mul_2: +store 16 +store 15 +load 15 +load 16 +* +store 17 +load 17 +retsub +// div +div_3: +store 22 +store 21 +load 21 +load 22 +/ +store 23 +load 23 +retsub +// mod +mod_4: +store 28 +store 27 +load 27 +load 28 +% +store 29 +load 29 +retsub +// all_laid_to_args +alllaidtoargs_5: +store 63 +store 62 +store 61 +store 60 +store 59 +store 58 +store 57 +store 56 +store 55 +store 54 +store 53 +store 52 +store 51 +store 50 +store 49 +store 48 +load 48 +load 49 ++ +load 50 ++ +load 51 ++ +load 52 ++ +load 53 ++ +load 54 ++ +load 55 ++ +load 56 ++ +load 57 ++ +load 58 ++ +load 59 ++ +load 60 ++ +load 61 ++ +load 62 ++ +load 63 ++ +store 64 +load 64 +retsub +// empty_return_subroutine +emptyreturnsubroutine_6: +byte "appear in both approval and clear state" +log +retsub +// log_1 +log1_7: +int 1 +store 66 +load 66 +retsub +// log_creation +logcreation_8: +byte "logging creation" +len +itob +extract 6 0 +byte "logging creation" +concat +store 68 +load 68 +retsub \ No newline at end of file diff --git a/tests/teal/router/yacc_clear.teal b/tests/teal/router/yacc_clear.teal new file mode 100644 index 00000000..5fd6fa0e --- /dev/null +++ b/tests/teal/router/yacc_clear.teal @@ -0,0 +1,60 @@ +#pragma version 6 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l6 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l5 +txna ApplicationArgs 0 +method "approve_if_odd(uint32)void" +== +bnz main_l4 +err +main_l4: +txna ApplicationArgs 1 +int 0 +extract_uint32 +store 2 +load 2 +callsub approveifodd_2 +int 1 +return +main_l5: +callsub log1_1 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return +main_l6: +callsub emptyreturnsubroutine_0 +int 1 +return +// empty_return_subroutine +emptyreturnsubroutine_0: +byte "appear in both approval and clear state" +log +retsub +// log_1 +log1_1: +int 1 +store 0 +load 0 +retsub +// approve_if_odd +approveifodd_2: +store 3 +load 3 +int 2 +% +bnz approveifodd_2_l2 +int 0 +return +approveifodd_2_l2: +int 1 +return \ No newline at end of file From 75f9bff1ddae8f2f8c053a5ad79590be288ef929 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Tue, 14 Jun 2022 19:55:55 -0500 Subject: [PATCH 21/23] flake8 --- tests/integration/abi_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/abi_test.py b/tests/integration/abi_test.py index ddcef82e..40208515 100644 --- a/tests/integration/abi_test.py +++ b/tests/integration/abi_test.py @@ -650,7 +650,7 @@ def selector_mod(args): # ---- ABI Router Dry Run Testing - TESTS ---- # -### QUESTIONABLE ### +# ## QUESTIONABLE ## # @pytest.mark.parametrize("method, call_types, invariants", QUESTIONABLE_CASES) @@ -685,7 +685,7 @@ def test_questionable_clear_program_method_or_barecall_negative(method, call_typ ) -### YACC (QUESTIONABLE Copy Pasta 🍝)### +# ## YACC (QUESTIONABLE Copy Pasta 🍝) ## # @pytest.mark.parametrize("method, call_types, invariants", YACC_CASES) From 0ced9d716c3f612fd4517899c2277d555a2fa252 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Wed, 15 Jun 2022 10:48:25 -0500 Subject: [PATCH 22/23] pin py-algorand-sdk@develop after get-method-by-name branch has been merged --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 022636c5..e4ce6534 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ author_email="pypiservice@algorand.com", python_requires=">=3.9", install_requires=[ - "py-algorand-sdk @ git+https://github.com/algorand/py-algorand-sdk@get-method-by-name", + "py-algorand-sdk @ git+https://github.com/algorand/py-algorand-sdk@develop", "tabulate==0.8.9", ], extras_require={ From 2a50f1bf584dbeb5a3f8445380beb68d7f5d9cf9 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Thu, 16 Jun 2022 10:16:18 -0500 Subject: [PATCH 23/23] point py-algorand-sdk==1.15.0 before merging --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index e4ce6534..2bffbf26 100644 --- a/setup.py +++ b/setup.py @@ -12,10 +12,7 @@ author="Algorand", author_email="pypiservice@algorand.com", python_requires=">=3.9", - install_requires=[ - "py-algorand-sdk @ git+https://github.com/algorand/py-algorand-sdk@develop", - "tabulate==0.8.9", - ], + install_requires=["py-algorand-sdk==1.15.0", "tabulate==0.8.9"], extras_require={ "development": [ "black==22.3.0",