From 09f95c5d3921bb193f35f7fff8f653a1f0bb79b6 Mon Sep 17 00:00:00 2001 From: sudo rm -rf --no-preserve-root / Date: Wed, 6 Sep 2023 20:14:48 +0100 Subject: [PATCH 01/10] chore: add `ir_runtime` option to cli help (#3592) --- vyper/cli/vyper_compile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index 9c97f8c667..bdd01eebbe 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -41,6 +41,7 @@ opcodes_runtime - List of runtime opcodes as a string ir - Intermediate representation in list format ir_json - Intermediate representation in JSON format +ir_runtime - Intermediate representation of runtime bytecode in list format asm - Output the EVM assembly of the deployable bytecode hex-ir - Output IR and assembly constants in hex instead of decimal """ From 1ed445765d546437febb7a2d3347d29bea6d943d Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 6 Sep 2023 15:18:46 -0400 Subject: [PATCH 02/10] chore(ci): fix macos universal2 build (#3590) this was a build regression introduced by the inclusion of the `cbor2` package in 96d20425fa2fb. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4be1043c1..684955bea1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: - name: Generate Binary run: >- - pip install --no-binary pycryptodome . && + pip install --no-binary pycryptodome --no-binary cbor2 . && pip install pyinstaller && make freeze From 294d97c2b853fb67ec7ca5398dfd60808384d4fb Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 6 Sep 2023 16:02:51 -0400 Subject: [PATCH 03/10] fix: version parsing for release candidates (#3593) the npm spec library is buggy and does not handle release candidates correctly. switch to the pypa packaging library which does pep440. note that we do a hack in order to support commonly used npm prefixes: no prefix, and `^` as prefix. going forward in v0.4.x, we will switch to pep440 entirely. --- docs/structure-of-a-contract.rst | 2 +- tests/ast/test_pre_parser.py | 32 ++++++------------------ vyper/ast/pre_parser.py | 43 ++++++++++---------------------- 3 files changed, 21 insertions(+), 56 deletions(-) diff --git a/docs/structure-of-a-contract.rst b/docs/structure-of-a-contract.rst index f58ab3b067..d2c5d48d96 100644 --- a/docs/structure-of-a-contract.rst +++ b/docs/structure-of-a-contract.rst @@ -17,7 +17,7 @@ Vyper supports several source code directives to control compiler modes and help Version Pragma -------------- -The version pragma ensures that a contract is only compiled by the intended compiler version, or range of versions. Version strings use `NPM `_ style syntax. +The version pragma ensures that a contract is only compiled by the intended compiler version, or range of versions. Version strings use `NPM `_ style syntax. Starting from v0.4.0 and up, version strings will use `PEP440 version specifiers _`. As of 0.3.10, the recommended way to specify the version pragma is as follows: diff --git a/tests/ast/test_pre_parser.py b/tests/ast/test_pre_parser.py index 150ee55edf..5427532c16 100644 --- a/tests/ast/test_pre_parser.py +++ b/tests/ast/test_pre_parser.py @@ -21,16 +21,9 @@ def set_version(version): "0.1.1", ">0.0.1", "^0.1.0", - "<=1.0.0 >=0.1.0", - "0.1.0 - 1.0.0", - "~0.1.0", - "0.1", - "0", - "*", - "x", - "0.x", - "0.1.x", - "0.2.0 || 0.1.1", + "<=1.0.0,>=0.1.0", + # "0.1.0 - 1.0.0", + "~=0.1.0", ] invalid_versions = [ "0.1.0", @@ -44,7 +37,6 @@ def set_version(version): "1.x", "0.2.x", "0.2.0 || 0.1.3", - "==0.1.1", "abc", ] @@ -70,9 +62,10 @@ def test_invalid_version_pragma(file_version, mock_version): "<0.1.1-rc.1", ">0.1.1a1", ">0.1.1-alpha.1", - "0.1.1a9 - 0.1.1-rc.10", + ">=0.1.1a9,<=0.1.1-rc.10", "<0.1.1b8", "<0.1.1rc1", + "<0.2.0", ] prerelease_invalid_versions = [ ">0.1.1-beta.9", @@ -80,19 +73,8 @@ def test_invalid_version_pragma(file_version, mock_version): "0.1.1b8", "0.1.1rc2", "0.1.1-rc.9 - 0.1.1-rc.10", - "<0.2.0", - pytest.param( - "<0.1.1b1", - marks=pytest.mark.xfail( - reason="https://github.com/rbarrois/python-semanticversion/issues/100" - ), - ), - pytest.param( - "<0.1.1a9", - marks=pytest.mark.xfail( - reason="https://github.com/rbarrois/python-semanticversion/issues/100" - ), - ), + "<0.1.1b1", + "<0.1.1a9", ] diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py index 788c44ef19..0ead889787 100644 --- a/vyper/ast/pre_parser.py +++ b/vyper/ast/pre_parser.py @@ -2,7 +2,7 @@ import re from tokenize import COMMENT, NAME, OP, TokenError, TokenInfo, tokenize, untokenize -from semantic_version import NpmSpec, Version +from packaging.specifiers import InvalidSpecifier, SpecifierSet from vyper.compiler.settings import OptimizationLevel, Settings @@ -12,21 +12,6 @@ from vyper.exceptions import StructureException, SyntaxException, VersionException from vyper.typing import ModificationOffsets, ParserPosition -VERSION_ALPHA_RE = re.compile(r"(?<=\d)a(?=\d)") # 0.1.0a17 -VERSION_BETA_RE = re.compile(r"(?<=\d)b(?=\d)") # 0.1.0b17 -VERSION_RC_RE = re.compile(r"(?<=\d)rc(?=\d)") # 0.1.0rc17 - - -def _convert_version_str(version_str: str) -> str: - """ - Convert loose version (0.1.0b17) to strict version (0.1.0-beta.17) - """ - version_str = re.sub(VERSION_ALPHA_RE, "-alpha.", version_str) # 0.1.0-alpha.17 - version_str = re.sub(VERSION_BETA_RE, "-beta.", version_str) # 0.1.0-beta.17 - version_str = re.sub(VERSION_RC_RE, "-rc.", version_str) # 0.1.0-rc.17 - - return version_str - def validate_version_pragma(version_str: str, start: ParserPosition) -> None: """ @@ -34,28 +19,26 @@ def validate_version_pragma(version_str: str, start: ParserPosition) -> None: """ from vyper import __version__ - # NOTE: should be `x.y.z.*` - installed_version = ".".join(__version__.split(".")[:3]) - - strict_file_version = _convert_version_str(version_str) - strict_compiler_version = Version(_convert_version_str(installed_version)) - - if len(strict_file_version) == 0: + if len(version_str) == 0: raise VersionException("Version specification cannot be empty", start) + # X.Y.Z or vX.Y.Z => ==X.Y.Z, ==vX.Y.Z + if re.match("[v0-9]", version_str): + version_str = "==" + version_str + # convert npm to pep440 + version_str = re.sub("^\\^", "~=", version_str) + try: - npm_spec = NpmSpec(strict_file_version) - except ValueError: + spec = SpecifierSet(version_str) + except InvalidSpecifier: raise VersionException( - f'Version specification "{version_str}" is not a valid NPM semantic ' - f"version specification", - start, + f'Version specification "{version_str}" is not a valid PEP440 specifier', start ) - if not npm_spec.match(strict_compiler_version): + if not spec.contains(__version__, prereleases=True): raise VersionException( f'Version specification "{version_str}" is not compatible ' - f'with compiler version "{installed_version}"', + f'with compiler version "{__version__}"', start, ) From aca2b4c5e54791943547342fae3c06552db1a3a7 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 6 Sep 2023 16:04:14 -0400 Subject: [PATCH 04/10] chore: CI for pre-release (release candidate) actions (#3589) --- .github/workflows/build.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 684955bea1..e81aa236d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: branches: - master release: - types: [released] + types: [published] # releases and pre-releases (release candidates) defaults: run: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 44c6978295..f268942e7d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,7 +5,7 @@ name: Publish to PyPI on: release: - types: [released] + types: [published] # releases and pre-releases (release candidates) jobs: From bb6e69acc3158f0acf16f23637f053d63d226e5b Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 6 Sep 2023 16:35:03 -0400 Subject: [PATCH 05/10] chore(ci): build binaries on pull requests (#3591) build binaries on all pull requests, to have better oversight over binary build success --- .github/workflows/build.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e81aa236d1..7243a05408 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build and release artifacts +name: Build artifacts on: workflow_dispatch: @@ -8,6 +8,7 @@ on: push: branches: - master + pull_request: release: types: [published] # releases and pre-releases (release candidates) @@ -42,6 +43,7 @@ jobs: pip install pyinstaller && make freeze + - name: Upload Artifact uses: actions/upload-artifact@v3 with: @@ -101,3 +103,13 @@ jobs: "https://uploads.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets?name=${BIN_NAME}" \ --data-binary "@${BIN_NAME}" done + + # check build success for pull requests + build-success: + if: always() + runs-on: ubuntu-latest + needs: [windows-build, unix-build] + steps: + - name: check that all builds succeeded + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 From 0cb37e3ef96ce374dafec5b1fcb40849fe074c62 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 6 Sep 2023 17:51:33 -0400 Subject: [PATCH 06/10] fix: dependency specification for `packaging` (#3594) setup.py regression introduced in 294d97c2b853fb --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c251071229..c81b9bed4a 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def _global_version(version): "cbor2>=5.4.6,<6", "asttokens>=2.0.5,<3", "pycryptodome>=3.5.1,<4", - "semantic-version>=2.10,<3", + "packaging>=23.1,<24", "importlib-metadata", "wheel", ], From 3b310d5292c4d1448e673d7b3adb223f9353260e Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 7 Sep 2023 17:45:35 -0400 Subject: [PATCH 07/10] chore(ci): fix binary names in release asset upload (#3597) rename binary during asset upload to properly escape the filename for the github API call. (Github API states: > GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames. For more information and help, contact GitHub Support. ) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7243a05408..c8d7f7d6c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,7 +100,7 @@ jobs: -X POST \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\ -H "Content-Type: application/octet-stream" \ - "https://uploads.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets?name=${BIN_NAME}" \ + "https://uploads.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets?name=${BIN_NAME/+/%2B}" \ --data-binary "@${BIN_NAME}" done From 344fd8f36c7f0cf1e34fd06ec30f34f6c487f340 Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Sun, 10 Sep 2023 17:30:01 +0200 Subject: [PATCH 08/10] docs: add README banner about Vyper audit competition (#3599) Add a temporary banner at the top of the README to advertise the audit competition --------- Co-authored-by: Charles Cooper --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index af987ffd4f..bad929956d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +**Vyper compiler security audit competition starts 14th September with $150k worth of bounties.** [See the competition on CodeHawks](https://www.codehawks.com/contests/cll5rujmw0001js08menkj7hc) and find [more details in this blog post](https://mirror.xyz/0xBA41A04A14aeaEec79e2D694B21ba5Ab610982f1/WTZ3l3MLhTz9P4avq6JqipN5d4HJNiUY-d8zT0pfmXg). From 0b740280c1e3c5528a20d47b29831948ddcc6d83 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 15 Sep 2023 18:01:03 -0400 Subject: [PATCH 09/10] fix: only allow valid identifiers to be nonreentrant keys (#3605) disallow invalid identifiers like `" "`, `"123abc"` from being keys for non-reentrant locks. this commit also refactors the `validate_identifiers` helper function to be in the `ast/` subdirectory, and slightly improves the VyperException constructor by allowing None (optional) annotations. --- .../exceptions/test_structure_exception.py | 23 +++- .../features/decorators/test_nonreentrant.py | 4 +- tests/parser/test_call_graph_stability.py | 2 +- tests/parser/types/test_identifier_naming.py | 2 +- vyper/ast/identifiers.py | 111 ++++++++++++++++ vyper/exceptions.py | 4 +- vyper/semantics/namespace.py | 119 +----------------- vyper/semantics/types/base.py | 2 +- vyper/semantics/types/function.py | 6 +- 9 files changed, 147 insertions(+), 126 deletions(-) create mode 100644 vyper/ast/identifiers.py diff --git a/tests/parser/exceptions/test_structure_exception.py b/tests/parser/exceptions/test_structure_exception.py index 08794b75f2..97ac2b139d 100644 --- a/tests/parser/exceptions/test_structure_exception.py +++ b/tests/parser/exceptions/test_structure_exception.py @@ -56,9 +56,26 @@ def double_nonreentrant(): """, """ @external -@nonreentrant("B") -@nonreentrant("C") -def double_nonreentrant(): +@nonreentrant(" ") +def invalid_nonreentrant_key(): + pass + """, + """ +@external +@nonreentrant("") +def invalid_nonreentrant_key(): + pass + """, + """ +@external +@nonreentrant("123") +def invalid_nonreentrant_key(): + pass + """, + """ +@external +@nonreentrant("!123abcd") +def invalid_nonreentrant_key(): pass """, """ diff --git a/tests/parser/features/decorators/test_nonreentrant.py b/tests/parser/features/decorators/test_nonreentrant.py index ac73b35bec..9e74019250 100644 --- a/tests/parser/features/decorators/test_nonreentrant.py +++ b/tests/parser/features/decorators/test_nonreentrant.py @@ -142,7 +142,7 @@ def set_callback(c: address): @external @payable -@nonreentrant('default') +@nonreentrant("lock") def protected_function(val: String[100], do_callback: bool) -> uint256: self.special_value = val _amount: uint256 = msg.value @@ -166,7 +166,7 @@ def unprotected_function(val: String[100], do_callback: bool): @external @payable -@nonreentrant('default') +@nonreentrant("lock") def __default__(): pass """ diff --git a/tests/parser/test_call_graph_stability.py b/tests/parser/test_call_graph_stability.py index b651092d16..a6193610e2 100644 --- a/tests/parser/test_call_graph_stability.py +++ b/tests/parser/test_call_graph_stability.py @@ -6,8 +6,8 @@ from hypothesis import given, settings import vyper.ast as vy_ast +from vyper.ast.identifiers import RESERVED_KEYWORDS from vyper.compiler.phases import CompilerData -from vyper.semantics.namespace import RESERVED_KEYWORDS def _valid_identifier(attr): diff --git a/tests/parser/types/test_identifier_naming.py b/tests/parser/types/test_identifier_naming.py index f4f602f471..5cfc7e8ed7 100755 --- a/tests/parser/types/test_identifier_naming.py +++ b/tests/parser/types/test_identifier_naming.py @@ -1,10 +1,10 @@ import pytest from vyper.ast.folding import BUILTIN_CONSTANTS +from vyper.ast.identifiers import RESERVED_KEYWORDS from vyper.builtins.functions import BUILTIN_FUNCTIONS from vyper.codegen.expr import ENVIRONMENT_VARIABLES from vyper.exceptions import NamespaceCollision, StructureException, SyntaxException -from vyper.semantics.namespace import RESERVED_KEYWORDS from vyper.semantics.types.primitives import AddressT BUILTIN_CONSTANTS = set(BUILTIN_CONSTANTS.keys()) diff --git a/vyper/ast/identifiers.py b/vyper/ast/identifiers.py new file mode 100644 index 0000000000..985b04e5cd --- /dev/null +++ b/vyper/ast/identifiers.py @@ -0,0 +1,111 @@ +import re + +from vyper.exceptions import StructureException + + +def validate_identifier(attr, ast_node=None): + if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr): + raise StructureException(f"'{attr}' contains invalid character(s)", ast_node) + if attr.lower() in RESERVED_KEYWORDS: + raise StructureException(f"'{attr}' is a reserved keyword", ast_node) + + +# https://docs.python.org/3/reference/lexical_analysis.html#keywords +# note we don't technically need to block all python reserved keywords, +# but do it for hygiene +_PYTHON_RESERVED_KEYWORDS = { + "False", + "None", + "True", + "and", + "as", + "assert", + "async", + "await", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "try", + "while", + "with", + "yield", +} +_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS} + +# Cannot be used for variable or member naming +RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | { + # decorators + "public", + "external", + "nonpayable", + "constant", + "immutable", + "transient", + "internal", + "payable", + "nonreentrant", + # "class" keywords + "interface", + "struct", + "event", + "enum", + # EVM operations + "unreachable", + # special functions (no name mangling) + "init", + "_init_", + "___init___", + "____init____", + "default", + "_default_", + "___default___", + "____default____", + # more control flow and special operations + "range", + # more special operations + "indexed", + # denominations + "ether", + "wei", + "finney", + "szabo", + "shannon", + "lovelace", + "ada", + "babbage", + "gwei", + "kwei", + "mwei", + "twei", + "pwei", + # sentinal constant values + # TODO remove when these are removed from the language + "zero_address", + "empty_bytes32", + "max_int128", + "min_int128", + "max_decimal", + "min_decimal", + "max_uint256", + "zero_wei", +} diff --git a/vyper/exceptions.py b/vyper/exceptions.py index aa23614e85..defca7cc53 100644 --- a/vyper/exceptions.py +++ b/vyper/exceptions.py @@ -54,7 +54,9 @@ def __init__(self, message="Error Message not found.", *items): # support older exceptions that don't annotate - remove this in the future! self.lineno, self.col_offset = items[0][:2] else: - self.annotations = items + # strip out None sources so that None can be passed as a valid + # annotation (in case it is only available optionally) + self.annotations = [k for k in items if k is not None] def with_annotation(self, *annotations): """ diff --git a/vyper/semantics/namespace.py b/vyper/semantics/namespace.py index b88bc3d817..613ac0c03b 100644 --- a/vyper/semantics/namespace.py +++ b/vyper/semantics/namespace.py @@ -1,12 +1,7 @@ import contextlib -import re - -from vyper.exceptions import ( - CompilerPanic, - NamespaceCollision, - StructureException, - UndeclaredDefinition, -) + +from vyper.ast.identifiers import validate_identifier +from vyper.exceptions import CompilerPanic, NamespaceCollision, UndeclaredDefinition from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions @@ -121,111 +116,3 @@ def override_global_namespace(ns): finally: # unclobber _namespace = tmp - - -def validate_identifier(attr): - if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr): - raise StructureException(f"'{attr}' contains invalid character(s)") - if attr.lower() in RESERVED_KEYWORDS: - raise StructureException(f"'{attr}' is a reserved keyword") - - -# https://docs.python.org/3/reference/lexical_analysis.html#keywords -# note we don't technically need to block all python reserved keywords, -# but do it for hygiene -_PYTHON_RESERVED_KEYWORDS = { - "False", - "None", - "True", - "and", - "as", - "assert", - "async", - "await", - "break", - "class", - "continue", - "def", - "del", - "elif", - "else", - "except", - "finally", - "for", - "from", - "global", - "if", - "import", - "in", - "is", - "lambda", - "nonlocal", - "not", - "or", - "pass", - "raise", - "return", - "try", - "while", - "with", - "yield", -} -_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS} - -# Cannot be used for variable or member naming -RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | { - # decorators - "public", - "external", - "nonpayable", - "constant", - "immutable", - "transient", - "internal", - "payable", - "nonreentrant", - # "class" keywords - "interface", - "struct", - "event", - "enum", - # EVM operations - "unreachable", - # special functions (no name mangling) - "init", - "_init_", - "___init___", - "____init____", - "default", - "_default_", - "___default___", - "____default____", - # more control flow and special operations - "range", - # more special operations - "indexed", - # denominations - "ether", - "wei", - "finney", - "szabo", - "shannon", - "lovelace", - "ada", - "babbage", - "gwei", - "kwei", - "mwei", - "twei", - "pwei", - # sentinal constant values - # TODO remove when these are removed from the language - "zero_address", - "empty_bytes32", - "max_int128", - "min_int128", - "max_decimal", - "min_decimal", - "max_uint256", - "zero_wei", -} diff --git a/vyper/semantics/types/base.py b/vyper/semantics/types/base.py index af955f6071..c5af5c2a39 100644 --- a/vyper/semantics/types/base.py +++ b/vyper/semantics/types/base.py @@ -3,6 +3,7 @@ from vyper import ast as vy_ast from vyper.abi_types import ABIType +from vyper.ast.identifiers import validate_identifier from vyper.exceptions import ( CompilerPanic, InvalidLiteral, @@ -12,7 +13,6 @@ UnknownAttribute, ) from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions -from vyper.semantics.namespace import validate_identifier # Some fake type with an overridden `compare_type` which accepts any RHS diff --git a/vyper/semantics/types/function.py b/vyper/semantics/types/function.py index 506dae135c..77b9efb13d 100644 --- a/vyper/semantics/types/function.py +++ b/vyper/semantics/types/function.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple from vyper import ast as vy_ast +from vyper.ast.identifiers import validate_identifier from vyper.ast.validation import validate_call_args from vyper.exceptions import ( ArgumentException, @@ -220,7 +221,10 @@ def from_FunctionDef( msg = "Nonreentrant decorator disallowed on `__init__`" raise FunctionDeclarationException(msg, decorator) - kwargs["nonreentrant"] = decorator.args[0].value + nonreentrant_key = decorator.args[0].value + validate_identifier(nonreentrant_key, decorator.args[0]) + + kwargs["nonreentrant"] = nonreentrant_key elif isinstance(decorator, vy_ast.Name): if FunctionVisibility.is_valid_value(decorator.id): From 823675a8dc49e8148b7a8c79e86f01dea7115cd9 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 18 Sep 2023 08:16:51 -0700 Subject: [PATCH 10/10] fix: dense selector table when there are empty buckets (#3606) certain combinations of selectors can result in some buckets being empty. in this case, the header section is incomplete. this commit fixes the issue by bailing out of the mkbucket routine when there are empty buckets (thus treating the configurations with empty buckets as invalid) --------- Co-authored-by: Tanguy Rocher --- tests/parser/test_selector_table.py | 431 ++++++++++++++++++++++++++++ vyper/codegen/jumptable_utils.py | 25 +- vyper/codegen/module.py | 8 +- 3 files changed, 458 insertions(+), 6 deletions(-) diff --git a/tests/parser/test_selector_table.py b/tests/parser/test_selector_table.py index 01a83698b7..3ac50707c2 100644 --- a/tests/parser/test_selector_table.py +++ b/tests/parser/test_selector_table.py @@ -10,6 +10,437 @@ from vyper.compiler.settings import OptimizationLevel +def test_dense_selector_table_empty_buckets(get_contract): + # some special combination of selectors which can result in + # some empty bucket being returned from _mk_buckets (that is, + # len(_mk_buckets(..., n_buckets)) != n_buckets + code = """ +@external +def aX61QLPWF()->uint256: + return 1 +@external +def aQHG0P2L1()->uint256: + return 2 +@external +def a2G8ME94W()->uint256: + return 3 +@external +def a0GNA21AY()->uint256: + return 4 +@external +def a4U1XA4T5()->uint256: + return 5 +@external +def aAYLMGOBZ()->uint256: + return 6 +@external +def a0KXRLHKE()->uint256: + return 7 +@external +def aDQS32HTR()->uint256: + return 8 +@external +def aP4K6SA3S()->uint256: + return 9 +@external +def aEB94ZP5S()->uint256: + return 10 +@external +def aTOIMN0IM()->uint256: + return 11 +@external +def aXV2N81OW()->uint256: + return 12 +@external +def a66PP6Y5X()->uint256: + return 13 +@external +def a5MWMTEWN()->uint256: + return 14 +@external +def a5ZFST4Z8()->uint256: + return 15 +@external +def aR13VXULX()->uint256: + return 16 +@external +def aWITH917Y()->uint256: + return 17 +@external +def a59NP6C5O()->uint256: + return 18 +@external +def aJ02590EX()->uint256: + return 19 +@external +def aUAXAAUQ8()->uint256: + return 20 +@external +def aWR1XNC6J()->uint256: + return 21 +@external +def aJABKZOKH()->uint256: + return 22 +@external +def aO1TT0RJT()->uint256: + return 23 +@external +def a41442IOK()->uint256: + return 24 +@external +def aMVXV9FHQ()->uint256: + return 25 +@external +def aNN0KJDZM()->uint256: + return 26 +@external +def aOX965047()->uint256: + return 27 +@external +def a575NX2J3()->uint256: + return 28 +@external +def a16EN8O7W()->uint256: + return 29 +@external +def aSZXLFF7O()->uint256: + return 30 +@external +def aQKQCIPH9()->uint256: + return 31 +@external +def aIP8021DL()->uint256: + return 32 +@external +def aQAV0HSHX()->uint256: + return 33 +@external +def aZVPAD745()->uint256: + return 34 +@external +def aJYBSNST4()->uint256: + return 35 +@external +def aQGWC4NYQ()->uint256: + return 36 +@external +def aFMBB9CXJ()->uint256: + return 37 +@external +def aYWM7ZUH1()->uint256: + return 38 +@external +def aJAZONIX1()->uint256: + return 39 +@external +def aQZ1HJK0H()->uint256: + return 40 +@external +def aKIH9LOUB()->uint256: + return 41 +@external +def aF4ZT80XL()->uint256: + return 42 +@external +def aYQD8UKR5()->uint256: + return 43 +@external +def aP6NCCAI4()->uint256: + return 44 +@external +def aY92U2EAZ()->uint256: + return 45 +@external +def aHMQ49D7P()->uint256: + return 46 +@external +def aMC6YX8VF()->uint256: + return 47 +@external +def a734X6YSI()->uint256: + return 48 +@external +def aRXXPNSMU()->uint256: + return 49 +@external +def aL5XKDTGT()->uint256: + return 50 +@external +def a86V1Y18A()->uint256: + return 51 +@external +def aAUM8PL5J()->uint256: + return 52 +@external +def aBAEC1ERZ()->uint256: + return 53 +@external +def a1U1VA3UE()->uint256: + return 54 +@external +def aC9FGVAHC()->uint256: + return 55 +@external +def aWN81WYJ3()->uint256: + return 56 +@external +def a3KK1Y07J()->uint256: + return 57 +@external +def aAZ6P6OSG()->uint256: + return 58 +@external +def aWP5HCIB3()->uint256: + return 59 +@external +def aVEK161C5()->uint256: + return 60 +@external +def aY0Q3O519()->uint256: + return 61 +@external +def aDHHHFIAE()->uint256: + return 62 +@external +def aGSJBCZKQ()->uint256: + return 63 +@external +def aZQQIUDHY()->uint256: + return 64 +@external +def a12O9QDH5()->uint256: + return 65 +@external +def aRQ1178XR()->uint256: + return 66 +@external +def aDT25C832()->uint256: + return 67 +@external +def aCSB01C4E()->uint256: + return 68 +@external +def aYGBPKZSD()->uint256: + return 69 +@external +def aP24N3EJ8()->uint256: + return 70 +@external +def a531Y9X3C()->uint256: + return 71 +@external +def a4727IKVS()->uint256: + return 72 +@external +def a2EX1L2BS()->uint256: + return 73 +@external +def a6145RN68()->uint256: + return 74 +@external +def aDO1ZNX97()->uint256: + return 75 +@external +def a3R28EU6M()->uint256: + return 76 +@external +def a9BFC867L()->uint256: + return 77 +@external +def aPL1MBGYC()->uint256: + return 78 +@external +def aI6H11O48()->uint256: + return 79 +@external +def aX0248DZY()->uint256: + return 80 +@external +def aE4JBUJN4()->uint256: + return 81 +@external +def aXBDB2ZBO()->uint256: + return 82 +@external +def a7O7MYYHL()->uint256: + return 83 +@external +def aERFF4PB6()->uint256: + return 84 +@external +def aJCUBG6TJ()->uint256: + return 85 +@external +def aQ5ELXM0F()->uint256: + return 86 +@external +def aWDT9UQVV()->uint256: + return 87 +@external +def a7UU40DJK()->uint256: + return 88 +@external +def aH01IT5VS()->uint256: + return 89 +@external +def aSKYTZ0FC()->uint256: + return 90 +@external +def aNX5LYRAW()->uint256: + return 91 +@external +def aUDKAOSGG()->uint256: + return 92 +@external +def aZ86YGAAO()->uint256: + return 93 +@external +def aIHWQGKLO()->uint256: + return 94 +@external +def aKIKFLAR9()->uint256: + return 95 +@external +def aCTPE0KRS()->uint256: + return 96 +@external +def aAD75X00P()->uint256: + return 97 +@external +def aDROUEF2F()->uint256: + return 98 +@external +def a8CDIF6YN()->uint256: + return 99 +@external +def aD2X7TM83()->uint256: + return 100 +@external +def a3W5UUB4L()->uint256: + return 101 +@external +def aG4MOBN4B()->uint256: + return 102 +@external +def aPRS0MSG7()->uint256: + return 103 +@external +def aKN3GHBUR()->uint256: + return 104 +@external +def aGE435RHQ()->uint256: + return 105 +@external +def a4E86BNFE()->uint256: + return 106 +@external +def aYDG928YW()->uint256: + return 107 +@external +def a2HFP5GQE()->uint256: + return 108 +@external +def a5DPMVXKA()->uint256: + return 109 +@external +def a3OFVC3DR()->uint256: + return 110 +@external +def aK8F62DAN()->uint256: + return 111 +@external +def aJS9EY3U6()->uint256: + return 112 +@external +def aWW789JQH()->uint256: + return 113 +@external +def a8AJJN3YR()->uint256: + return 114 +@external +def a4D0MUIDU()->uint256: + return 115 +@external +def a35W41JQR()->uint256: + return 116 +@external +def a07DQOI1E()->uint256: + return 117 +@external +def aFT43YNCT()->uint256: + return 118 +@external +def a0E75I8X3()->uint256: + return 119 +@external +def aT6NXIRO4()->uint256: + return 120 +@external +def aXB2UBAKQ()->uint256: + return 121 +@external +def aHWH55NW6()->uint256: + return 122 +@external +def a7TCFE6C2()->uint256: + return 123 +@external +def a8XYAM81I()->uint256: + return 124 +@external +def aHQTQ4YBY()->uint256: + return 125 +@external +def aGCZEHG6Y()->uint256: + return 126 +@external +def a6LJTKIW0()->uint256: + return 127 +@external +def aBDIXTD9S()->uint256: + return 128 +@external +def aCB83G21P()->uint256: + return 129 +@external +def aZC525N4K()->uint256: + return 130 +@external +def a40LC94U6()->uint256: + return 131 +@external +def a8X9TI93D()->uint256: + return 132 +@external +def aGUG9CD8Y()->uint256: + return 133 +@external +def a0LAERVAY()->uint256: + return 134 +@external +def aXQ0UEX19()->uint256: + return 135 +@external +def aKK9C7NE7()->uint256: + return 136 +@external +def aS2APW8UE()->uint256: + return 137 +@external +def a65NT07MM()->uint256: + return 138 +@external +def aGRMT6ZW5()->uint256: + return 139 +@external +def aILR4U1Z()->uint256: + return 140 + """ + c = get_contract(code) + + assert c.aX61QLPWF() == 1 # will revert if the header section is misaligned + + @given( n_methods=st.integers(min_value=1, max_value=100), seed=st.integers(min_value=0, max_value=2**64 - 1), diff --git a/vyper/codegen/jumptable_utils.py b/vyper/codegen/jumptable_utils.py index 6987ce90bd..6404b75532 100644 --- a/vyper/codegen/jumptable_utils.py +++ b/vyper/codegen/jumptable_utils.py @@ -43,7 +43,11 @@ def _image_of(xs, magic): return [((x * magic) >> bits_shift) % len(xs) for x in xs] -class _Failure(Exception): +class _FindMagicFailure(Exception): + pass + + +class _HasEmptyBuckets(Exception): pass @@ -53,7 +57,7 @@ def find_magic_for(xs): if len(test) == len(set(test)): return m - raise _Failure(f"Could not find hash for {xs}") + raise _FindMagicFailure(f"Could not find hash for {xs}") def _mk_buckets(method_ids, n_buckets): @@ -72,6 +76,11 @@ def _mk_buckets(method_ids, n_buckets): def _dense_jumptable_info(method_ids, n_buckets): buckets = _mk_buckets(method_ids, n_buckets) + # if there are somehow empty buckets, bail out as that can mess up + # the bucket header layout + if len(buckets) != n_buckets: + raise _HasEmptyBuckets() + ret = {} for bucket_id, method_ids in buckets.items(): magic = find_magic_for(method_ids) @@ -98,8 +107,16 @@ def generate_dense_jumptable_info(signatures): while n_buckets > 0: try: # print(f"trying {n_buckets} (bucket size {n // n_buckets})") - ret = _dense_jumptable_info(method_ids, n_buckets) - except _Failure: + solution = _dense_jumptable_info(method_ids, n_buckets) + assert len(solution) == n_buckets + ret = n_buckets, solution + + except _HasEmptyBuckets: + # found a solution which has empty buckets; skip it since + # it will break the bucket layout. + pass + + except _FindMagicFailure: if ret is not None: break diff --git a/vyper/codegen/module.py b/vyper/codegen/module.py index 8caea9ee9b..6445a5e1e0 100644 --- a/vyper/codegen/module.py +++ b/vyper/codegen/module.py @@ -124,8 +124,12 @@ def _selector_section_dense(external_functions, global_ctx): ir_node = ["label", label, ["var_list"], entry_point.ir_node] function_irs.append(IRnode.from_list(ir_node)) - jumptable_info = jumptable_utils.generate_dense_jumptable_info(entry_points.keys()) - n_buckets = len(jumptable_info) + n_buckets, jumptable_info = jumptable_utils.generate_dense_jumptable_info(entry_points.keys()) + # note: we are guaranteed by jumptable_utils that there are no buckets + # which are empty. sanity check that the bucket ids are well-behaved: + assert n_buckets == len(jumptable_info) + for i, (bucket_id, _) in enumerate(sorted(jumptable_info.items())): + assert i == bucket_id # bucket magic <2 bytes> | bucket location <2 bytes> | bucket size <1 byte> # TODO: can make it smaller if the largest bucket magic <= 255