Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat[tool]: support storage layouts via json and .vyz inputs #4370

Merged
merged 44 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4b84b26
handle storage layout for json
tserg Nov 23, 2024
da1c476
add json test
tserg Nov 23, 2024
8154230
handle json round trip
tserg Nov 24, 2024
cdf62b7
add json round trip test
tserg Nov 24, 2024
df36dd5
fix layout override test
tserg Nov 24, 2024
c893919
fix layout output via json
tserg Nov 24, 2024
2808db6
Merge branch 'master' of https://github.com/vyperlang/vyper into tool…
tserg Nov 24, 2024
08f0396
include transient storage overrides
tserg Nov 24, 2024
1f24a50
handle layout override for zip
tserg Nov 24, 2024
fda0e1e
Revert "include transient storage overrides"
tserg Nov 24, 2024
6e184cc
more reverts
tserg Nov 24, 2024
a3c18d7
fix tests
tserg Nov 24, 2024
73f650e
fix test
tserg Nov 24, 2024
c552876
check for storage layout in zip
tserg Nov 24, 2024
a16d67e
fix more tests
tserg Nov 24, 2024
2f59780
apply bts suggestions
tserg Nov 26, 2024
a93c0b9
pass storage layout
tserg Nov 26, 2024
9def387
Merge branch 'master' of https://github.com/vyperlang/vyper into tool…
tserg Nov 27, 2024
05f0786
apply bts suggestion
tserg Nov 27, 2024
5e8122c
add layout to dict for tests
tserg Nov 27, 2024
7aac6f3
fix solc json round trip test
tserg Nov 27, 2024
49fdd97
remove content key
tserg Nov 27, 2024
8d15e39
fix typo in docs
tserg Nov 27, 2024
c8f42e1
update docs
tserg Nov 27, 2024
4036b67
undo passing down of storage layout
tserg Dec 10, 2024
43f5643
apply cc suggestion on refactoring integrity hash
tserg Dec 10, 2024
49986e5
remove dup test
tserg Dec 10, 2024
42eb3e9
Merge branch 'master' of https://github.com/vyperlang/vyper into tool…
tserg Dec 10, 2024
58c3064
fix integrity sum check and small refactor
charles-cooper Dec 10, 2024
e00be88
add tests
tserg Dec 11, 2024
eea76e2
refactor tests
tserg Dec 12, 2024
848ea34
add round trip layout tests
tserg Dec 12, 2024
5d3c946
change to slot 5
tserg Dec 12, 2024
4183a23
add json layout assertion
tserg Dec 12, 2024
2cdff96
make layout output in json not a string
charles-cooper Dec 13, 2024
bd3af72
fix a test
charles-cooper Dec 13, 2024
9fbe4ba
add unknown path test
tserg Dec 16, 2024
196bbc8
fix test
tserg Dec 16, 2024
5018c5f
Merge branch 'master' of https://github.com/vyperlang/vyper into tool…
tserg Dec 16, 2024
b693b5c
fix storage layout slot
tserg Dec 16, 2024
f640b3e
add multiple layouts for json input
tserg Dec 18, 2024
7265a43
Merge branch 'master' of https://github.com/vyperlang/vyper into tool…
tserg Dec 18, 2024
a6786bc
Merge branch 'master' of https://github.com/vyperlang/vyper into tool…
tserg Dec 31, 2024
d465568
fix merge conflict
tserg Jan 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions docs/compiling-a-contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Online Compilers
Try VyperLang!
-----------------

`Try VyperLang! <https://try.vyperlang.org>`_ is a JupterHub instance hosted by the Vyper team as a sandbox for developing and testing contracts in Vyper. It requires github for login, and supports deployment via the browser.
`Try VyperLang! <https://try.vyperlang.org>`_ is a JupyterHub instance hosted by the Vyper team as a sandbox for developing and testing contracts in Vyper. It requires github for login, and supports deployment via the browser.

Remix IDE
---------
Expand Down Expand Up @@ -203,7 +203,7 @@ The following is a list of supported EVM versions, and changes in the compiler i
Integrity Hash
==============

To help tooling detect whether two builds are the same, Vyper provides the ``-f integrity`` output, which outputs the integrity hash of a contract. The integrity hash is recursively defined as the sha256 of the source code with the integrity hashes of its dependencies (imports).
To help tooling detect whether two builds are the same, Vyper provides the ``-f integrity`` output, which outputs the integrity hash of a contract. The integrity hash is recursively defined as the sha256 of the source code with the integrity hashes of its dependencies (imports) and storage layout overrides (if provided).

.. _vyper-archives:

Expand All @@ -219,15 +219,17 @@ A Vyper archive is a compileable bundle of input sources and settings. Technical
├── compilation_targets
├── compiler_version
├── integrity
├── settings.json
├── searchpaths
└── settings.json
└── storage_layout.json [OPTIONAL]

* ``cli_settings.txt`` is a text representation of the settings that were used on the compilation run that generated this archive.
* ``compilation_targets`` is a newline separated list of compilation targets. Currently only one compilation is supported
* ``compiler_version`` is a text representation of the compiler version used to generate this archive
* ``integrity`` is the :ref:`integrity hash <integrity-hash>` of the input contract
* ``searchpaths`` is a newline-separated list of the search paths used on this compilation run
* ``settings.json`` is a json representation of the settings used on this compilation run. It is 1:1 with ``cli_settings.txt``, but both are provided as they are convenient for different workflows (typically, manually vs automated).
* ``storage_layout.json`` is a json representation of the storage layout overrides to be used on this compilation run. It is optional.

A Vyper archive file can be produced by requesting the ``-f archive`` output format. The compiler can also produce the archive in base64 encoded form using the ``--base64`` flag. The Vyper compiler can accept both ``.vyz`` and base64-encoded Vyper archives directly as input.

Expand Down Expand Up @@ -281,6 +283,14 @@ The following example describes the expected input format of ``vyper-json``. (Co
}
},
// Optional
// Storage layout overrides for the contracts that are compiled
"storage_layout_overrides": {
"contracts/foo.vy": {
"a": {"type": "uint256", "slot": 1, "n_slots": 1},
"b": {"type": "uint256", "slot": 0, "n_slots": 1},
}
},
// Optional
"settings": {
"evmVersion": "cancun", // EVM version to compile for. Can be london, paris, shanghai or cancun (default).
// optional, optimization mode
Expand Down Expand Up @@ -364,6 +374,13 @@ The following example describes the output format of ``vyper-json``. Comments ar
"formattedMessage": "line 5:11 Unsupported type conversion: int128 to bool"
}
],
// Optional: not present if there are no storage layout overrides
"storage_layout_overrides": {
"contracts/foo.vy": {
"a": {"type": "uint256", "slot": 1, "n_slots": 1},
"b": {"type": "uint256", "slot": 0, "n_slots": 1},
}
},
// This contains the file-level outputs. Can be limited/filtered by the outputSelection settings.
"sources": {
"source_file.vy": {
Expand Down
28 changes: 27 additions & 1 deletion tests/unit/cli/storage_layout/test_storage_layout_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

from vyper.cli.vyper_json import compile_json
from vyper.compiler import compile_code
from vyper.evm.opcodes import version_check
from vyper.exceptions import StorageLayoutException
Expand All @@ -13,7 +14,7 @@ def test_storage_layout_overrides():
b: uint256"""

storage_layout_overrides = {
"a": {"type": "uint256", "slot": 1, "n_slots": 1},
"a": {"type": "uint256", "slot": 5, "n_slots": 1},
"b": {"type": "uint256", "slot": 0, "n_slots": 1},
}

Expand All @@ -26,6 +27,31 @@ def test_storage_layout_overrides():
assert out["layout"] == expected_output


def test_storage_layout_overrides_json():
code = """
a: uint256
b: uint256"""

storage_layout_overrides = {
"a": {"type": "uint256", "slot": 5, "n_slots": 1},
"b": {"type": "uint256", "slot": 0, "n_slots": 1},
}

input_json = {
"language": "Vyper",
"sources": {"contracts/foo.vy": {"content": code}},
"storage_layout_overrides": {"contracts/foo.vy": storage_layout_overrides},
"settings": {"outputSelection": {"*": ["*"]}},
}

out = compile_code(
code, output_formats=["layout"], storage_layout_override=storage_layout_overrides
)
assert (
compile_json(input_json)["contracts"]["contracts/foo.vy"]["foo"]["layout"] == out["layout"]
)


def test_storage_layout_for_more_complex():
code = """
foo: HashMap[address, uint256]
Expand Down
120 changes: 93 additions & 27 deletions tests/unit/cli/vyper_compile/test_compile_files.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a test that storage layout is output correctly for archive + solc_json files (and ideally that it round-trips)?

Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import contextlib
import json
import sys
import warnings
import zipfile
from pathlib import Path

import pytest

from vyper.cli.compile_archive import compiler_data_from_zip
from vyper.cli.vyper_compile import compile_files
from vyper.cli.vyper_json import compile_json
from vyper.cli.vyper_json import compile_from_input_dict, compile_json
from vyper.compiler import INTERFACE_OUTPUT_FORMATS, OUTPUT_FORMATS
from vyper.compiler.input_bundle import FilesystemInputBundle
from vyper.compiler.output_bundle import OutputBundle
from vyper.compiler.phases import CompilerData
from vyper.utils import sha256sum

TAMPERED_INTEGRITY_SUM = sha256sum("tampered integrity sum")

INTEGRITY_WARNING = f"Mismatched integrity sum! Expected {TAMPERED_INTEGRITY_SUM}"
INTEGRITY_WARNING += " but got {integrity}." # noqa: FS003
INTEGRITY_WARNING += " (This likely indicates a corrupted archive)"


def test_combined_json_keys(chdir_tmp_path, make_file):
make_file("bar.vy", "")
Expand Down Expand Up @@ -298,6 +307,9 @@ def foo() -> uint256:
import lib
import jsonabi

a: uint256
b: uint256

@external
def foo() -> uint256:
return lib.foo()
Expand All @@ -306,28 +318,52 @@ def foo() -> uint256:
def bar(x: uint256) -> uint256:
return extcall jsonabi(msg.sender).test_json(x)
"""
storage_layout_overrides = {
"a": {"type": "uint256", "n_slots": 1, "slot": 5},
"b": {"type": "uint256", "n_slots": 1, "slot": 0},
}
storage_layout_source = json.dumps(storage_layout_overrides)

tmpdir = tmp_path_factory.mktemp("fake-package")
with open(tmpdir / "lib.vy", "w") as f:
f.write(library_source)
with open(tmpdir / "jsonabi.json", "w") as f:
f.write(json_source)
with open(tmpdir / "layout.json", "w") as f:
f.write(storage_layout_source)

contract_file = make_file("contract.vy", contract_source)

return (tmpdir, tmpdir / "lib.vy", tmpdir / "jsonabi.json", contract_file)
contract_hash = sha256sum(contract_source)
library_hash = sha256sum(library_source)
jsonabi_hash = sha256sum(json_source)
resolved_imports_hash = sha256sum(contract_hash + sha256sum(library_hash) + jsonabi_hash)
storage_layout_hash = sha256sum(storage_layout_source)
expected_integrity = sha256sum(storage_layout_hash + resolved_imports_hash)

return (
tmpdir,
tmpdir / "lib.vy",
tmpdir / "jsonabi.json",
tmpdir / "layout.json",
contract_file,
expected_integrity,
)


def test_import_sys_path(input_files):
tmpdir, _, _, contract_file = input_files
tmpdir, _, _, _, contract_file, _ = input_files
with mock_sys_path(tmpdir):
assert compile_files([contract_file], ["combined_json"]) is not None


def test_archive_output(input_files):
tmpdir, _, _, contract_file = input_files
tmpdir, library_file, jsonabi_file, storage_layout_path, contract_file, integrity = input_files
search_paths = [".", tmpdir]

s = compile_files([contract_file], ["archive"], paths=search_paths)
s = compile_files(
[contract_file], ["archive"], paths=search_paths, storage_layout_paths=[storage_layout_path]
)
archive_bytes = s[contract_file]["archive"]

archive_path = Path("foo.zip")
Expand All @@ -337,13 +373,28 @@ def test_archive_output(input_files):
assert zipfile.is_zipfile(archive_path)

# compare compiling the two input bundles
out = compile_files([contract_file], ["integrity", "bytecode"], paths=search_paths)
out2 = compile_files([archive_path], ["integrity", "bytecode"])
out = compile_files(
[contract_file],
["integrity", "bytecode", "layout"],
paths=search_paths,
storage_layout_paths=[storage_layout_path],
)
out2 = compile_files([archive_path], ["integrity", "bytecode", "layout"])
assert out[contract_file] == out2[archive_path]

# tamper with the integrity sum
archive_compiler_data = compiler_data_from_zip(archive_path, None, False)
archive_compiler_data.expected_integrity_sum = TAMPERED_INTEGRITY_SUM

with warnings.catch_warnings(record=True) as w:
assert archive_compiler_data.integrity_sum is not None

assert len(w) == 1, [s.message for s in w]
assert str(w[0].message).startswith(INTEGRITY_WARNING.format(integrity=integrity))


def test_archive_b64_output(input_files):
tmpdir, _, _, contract_file = input_files
tmpdir, _, _, _, contract_file, _ = input_files
search_paths = [".", tmpdir]

out = compile_files(
Expand All @@ -362,7 +413,7 @@ def test_archive_b64_output(input_files):


def test_archive_compile_options(input_files):
tmpdir, _, _, contract_file = input_files
tmpdir, _, _, _, contract_file, _ = input_files
search_paths = [".", tmpdir]

options = ["abi_python", "json", "ast", "annotated_ast", "ir_json"]
Expand Down Expand Up @@ -417,7 +468,7 @@ def test_archive_compile_options(input_files):


def test_compile_vyz_with_options(input_files):
tmpdir, _, _, contract_file = input_files
tmpdir, _, _, _, contract_file, _ = input_files
search_paths = [".", tmpdir]

for option in format_options:
Expand Down Expand Up @@ -446,7 +497,7 @@ def test_compile_vyz_with_options(input_files):


def test_archive_compile_simultaneous_options(input_files):
tmpdir, _, _, contract_file = input_files
tmpdir, _, _, _, contract_file, _ = input_files
search_paths = [".", tmpdir]

for option in format_options:
Expand All @@ -461,40 +512,55 @@ def test_archive_compile_simultaneous_options(input_files):


def test_solc_json_output(input_files):
tmpdir, _, _, contract_file = input_files
tmpdir, _, _, storage_layout_path, contract_file, integrity = input_files
search_paths = [".", tmpdir]

out = compile_files([contract_file], ["solc_json"], paths=search_paths)

out = compile_files(
[contract_file],
["solc_json"],
paths=search_paths,
storage_layout_paths=[storage_layout_path],
)
json_input = out[contract_file]["solc_json"]

# check that round-tripping solc_json thru standard json produces
# the same as compiling directly
json_out = compile_json(json_input)["contracts"]["contract.vy"]
json_out_bytecode = json_out["contract"]["evm"]["bytecode"]["object"]
json_out_layout = json_out["contract"]["layout"]["storage_layout"]

out2 = compile_files([contract_file], ["integrity", "bytecode"], paths=search_paths)
out2 = compile_files(
[contract_file],
["integrity", "bytecode", "layout"],
paths=search_paths,
storage_layout_paths=[storage_layout_path],
)

assert out2[contract_file]["bytecode"] == json_out_bytecode
assert out2[contract_file]["layout"]["storage_layout"] == json_out_layout

# tamper with the integrity sum
json_input["integrity"] = TAMPERED_INTEGRITY_SUM
_, warn_data = compile_from_input_dict(json_input)

w = warn_data[Path("contract.vy")]
assert len(w) == 1, [s.message for s in w]
assert str(w[0].message).startswith(INTEGRITY_WARNING.format(integrity=integrity))


# maybe this belongs in tests/unit/compiler?
def test_integrity_sum(input_files):
tmpdir, library_file, jsonabi_file, contract_file = input_files
tmpdir, library_file, jsonabi_file, storage_layout_path, contract_file, integrity = input_files
search_paths = [".", tmpdir]

out = compile_files([contract_file], ["integrity"], paths=search_paths)

with library_file.open() as f, contract_file.open() as g, jsonabi_file.open() as h:
library_contents = f.read()
contract_contents = g.read()
jsonabi_contents = h.read()
out = compile_files(
[contract_file],
["integrity"],
paths=search_paths,
storage_layout_paths=[storage_layout_path],
)

contract_hash = sha256sum(contract_contents)
library_hash = sha256sum(library_contents)
jsonabi_hash = sha256sum(jsonabi_contents)
expected = sha256sum(contract_hash + sha256sum(library_hash) + jsonabi_hash)
assert out[contract_file]["integrity"] == expected
assert out[contract_file]["integrity"] == integrity


# does this belong in tests/unit/compiler?
Expand Down
Loading
Loading