diff --git a/database.sql b/database.sql index 792a94d..720a219 100644 --- a/database.sql +++ b/database.sql @@ -1,7 +1,7 @@ -\restrict AsCPTOt3U9KhJ9fUdZGWUtrBsH1prEN0qQZE48Juv4M8WNoa7mPKZojrPqmzFhv +\restrict 7mZpScgT6ftGUlRA0ATHWmg8xgbioRvZDlJHy9RkZPwcyJEcQw9LTLQE0Lu8V3h --- Dumped from database version 16.11 (Ubuntu 16.11-1.pgdg24.04+1) --- Dumped by pg_dump version 16.11 (Ubuntu 16.11-1.pgdg24.04+1) +-- Dumped from database version 16.12 (Ubuntu 16.12-1.pgdg24.04+1) +-- Dumped by pg_dump version 16.12 (Ubuntu 16.12-1.pgdg24.04+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -195,6 +195,26 @@ END; $$; +-- +-- Name: validate_additional_input(jsonb); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.validate_additional_input(obj jsonb) RETURNS boolean + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN obj IS NULL OR ( + is_jsonb_object(obj) AND + validate_json_object_keys( + obj, + array []::text[], + array ['storage_layout_overrides'] + ) + ); +END; +$$; + + -- -- Name: validate_code_artifacts_cbor_auxdata(jsonb); Type: FUNCTION; Schema: public; Owner: - -- @@ -381,8 +401,8 @@ BEGIN is_jsonb_object(obj) AND validate_json_object_keys( obj, - array ['abi', 'userdoc', 'devdoc', 'sources', 'storageLayout'], - array []::text[] + array ['abi', 'sources'], + array ['userdoc', 'devdoc', 'storageLayout', 'transientStorageLayout'] ) AND validate_compilation_artifacts_abi(obj -> 'abi') AND validate_compilation_artifacts_sources(obj -> 'sources'); @@ -933,6 +953,8 @@ CREATE TABLE public.compiled_contracts ( creation_code_artifacts jsonb NOT NULL, runtime_code_hash bytea NOT NULL, runtime_code_artifacts jsonb NOT NULL, + additional_input jsonb, + CONSTRAINT additional_input_json_schema CHECK (public.validate_additional_input(additional_input)), CONSTRAINT compilation_artifacts_json_schema CHECK (public.validate_compilation_artifacts(compilation_artifacts)), CONSTRAINT creation_code_artifacts_json_schema CHECK (public.validate_creation_code_artifacts(creation_code_artifacts)), CONSTRAINT runtime_code_artifacts_json_schema CHECK (public.validate_runtime_code_artifacts(runtime_code_artifacts)) @@ -1731,7 +1753,7 @@ ALTER TABLE ONLY public.verified_contracts -- PostgreSQL database dump complete -- -\unrestrict AsCPTOt3U9KhJ9fUdZGWUtrBsH1prEN0qQZE48Juv4M8WNoa7mPKZojrPqmzFhv +\unrestrict 7mZpScgT6ftGUlRA0ATHWmg8xgbioRvZDlJHy9RkZPwcyJEcQw9LTLQE0Lu8V3h -- @@ -1743,4 +1765,7 @@ INSERT INTO public.schema_migrations (version) VALUES ('20250723145429'), ('20251023134207'), ('20251106144315'), - ('20260126113330'); + ('20260126113330'), + ('20260224135405'), + ('20260224171441'), + ('20260225083159'); diff --git a/migrations/20260224135405_support_old_solc_output.sql b/migrations/20260224135405_support_old_solc_output.sql new file mode 100644 index 0000000..259aac0 --- /dev/null +++ b/migrations/20260224135405_support_old_solc_output.sql @@ -0,0 +1,41 @@ +-- migrate:up + +/* + Make 'userdoc', 'devdoc' and 'storageLayout' optional in the compilation artifacts, + because older solc versions do not output them. + See https://github.com/argotorg/sourcify/issues/2296 +*/ + +CREATE OR REPLACE FUNCTION validate_compilation_artifacts(obj jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN + is_jsonb_object(obj) AND + validate_json_object_keys( + obj, + array ['abi', 'sources'], + array ['userdoc', 'devdoc', 'storageLayout'] + ) AND + validate_compilation_artifacts_abi(obj -> 'abi') AND + validate_compilation_artifacts_sources(obj -> 'sources'); +END; +$$ LANGUAGE plpgsql; + +-- migrate:down + +CREATE OR REPLACE FUNCTION validate_compilation_artifacts(obj jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN + is_jsonb_object(obj) AND + validate_json_object_keys( + obj, + array ['abi', 'userdoc', 'devdoc', 'sources', 'storageLayout'], + array []::text[] + ) AND + validate_compilation_artifacts_abi(obj -> 'abi') AND + validate_compilation_artifacts_sources(obj -> 'sources'); +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/20260224171441_add_transientStorageLayout.sql b/migrations/20260224171441_add_transientStorageLayout.sql new file mode 100644 index 0000000..ab80f16 --- /dev/null +++ b/migrations/20260224171441_add_transientStorageLayout.sql @@ -0,0 +1,39 @@ +-- migrate:up + +/* + Allow storing the new compiler output field `transientStorageLayout`. +*/ + +CREATE OR REPLACE FUNCTION validate_compilation_artifacts(obj jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN + is_jsonb_object(obj) AND + validate_json_object_keys( + obj, + array ['abi', 'sources'], + array ['userdoc', 'devdoc', 'storageLayout', 'transientStorageLayout'] + ) AND + validate_compilation_artifacts_abi(obj -> 'abi') AND + validate_compilation_artifacts_sources(obj -> 'sources'); +END; +$$ LANGUAGE plpgsql; + +-- migrate:down + +CREATE OR REPLACE FUNCTION validate_compilation_artifacts(obj jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN + is_jsonb_object(obj) AND + validate_json_object_keys( + obj, + array ['abi', 'sources'], + array ['userdoc', 'devdoc', 'storageLayout'] + ) AND + validate_compilation_artifacts_abi(obj -> 'abi') AND + validate_compilation_artifacts_sources(obj -> 'sources'); +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/20260225083159_add_additional_input_to_compiled_contracts.sql b/migrations/20260225083159_add_additional_input_to_compiled_contracts.sql new file mode 100644 index 0000000..3080942 --- /dev/null +++ b/migrations/20260225083159_add_additional_input_to_compiled_contracts.sql @@ -0,0 +1,36 @@ +-- migrate:up + +/* + Add `additional_input` column to `compiled_contracts` to store compiler options + that appear at the top level of standard JSON input but are not part of the `settings` field. + Currently restricted to `storage_layout_overrides`, used by Vyper. + See https://github.com/vyperlang/vyper/pull/4370 +*/ + +CREATE OR REPLACE FUNCTION validate_additional_input(obj jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN obj IS NULL OR ( + is_jsonb_object(obj) AND + validate_json_object_keys( + obj, + array []::text[], + array ['storage_layout_overrides'] + ) + ); +END; +$$ LANGUAGE plpgsql; + +ALTER TABLE compiled_contracts + ADD COLUMN additional_input jsonb; + +ALTER TABLE compiled_contracts + ADD CONSTRAINT additional_input_json_schema + CHECK (validate_additional_input(additional_input)); + +-- migrate:down + +ALTER TABLE compiled_contracts DROP CONSTRAINT IF EXISTS additional_input_json_schema; +ALTER TABLE compiled_contracts DROP COLUMN IF EXISTS additional_input; +DROP FUNCTION IF EXISTS validate_additional_input(jsonb); diff --git a/tests/helpers.py b/tests/helpers.py index 2132839..ed88d3f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -101,6 +101,7 @@ class CompiledContract: compilation_artifacts = dict() creation_code_artifacts = dict() runtime_code_artifacts = dict() + additional_input = Null @staticmethod def dummy(): @@ -128,17 +129,24 @@ def dummy(): return instance def insert(self, connection, creation_code_hash, runtime_code_hash): + query_columns = ("id, compiler, version, language, name, fully_qualified_name, " + "compiler_settings, compilation_artifacts, creation_code_hash, creation_code_artifacts, " + "runtime_code_hash, runtime_code_artifacts") + values = [self.id, self.compiler, self.version, self.language, self.name, self.fully_qualified_name, + json.dumps(self.compiler_settings), json.dumps(self.compilation_artifacts), + creation_code_hash, json.dumps(self.creation_code_artifacts), + runtime_code_hash, json.dumps(self.runtime_code_artifacts)] + + if self.additional_input != Null: + query_columns += ", additional_input" + values.append(json.dumps(self.additional_input)) + + query_values = ", ".join(["%s"] * len(values)) with connection.cursor() as cursor: - cursor.execute(""" - INSERT INTO compiled_contracts ( - id, compiler, version, language, name, fully_qualified_name, - compiler_settings, compilation_artifacts, creation_code_hash, creation_code_artifacts, - runtime_code_hash, runtime_code_artifacts) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, (self.id, self.compiler, self.version, self.language, self.name, self.fully_qualified_name, - json.dumps(self.compiler_settings), json.dumps( - self.compilation_artifacts), creation_code_hash, json.dumps(self.creation_code_artifacts), - runtime_code_hash, json.dumps(self.runtime_code_artifacts))) + cursor.execute(f""" + INSERT INTO compiled_contracts ({query_columns}) + VALUES ({query_values}) + """, values) class VerifiedContract: diff --git a/tests/test_constraint_additional_input_json_schema.py b/tests/test_constraint_additional_input_json_schema.py new file mode 100644 index 0000000..e208e3a --- /dev/null +++ b/tests/test_constraint_additional_input_json_schema.py @@ -0,0 +1,49 @@ +from helpers import * + + +@pytest.fixture(scope='function', autouse=True) +def setup(connection, dummy_code): + dummy_code.insert(connection) + + +class TestAdditionalInput: + def test_null_succeeds(self, connection, dummy_code, dummy_compiled_contract): + # additional_input defaults to SQL NULL when not set + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) + + def test_storage_layout_overrides_succeeds(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.additional_input = { + "storage_layout_overrides": { + "x": {"slot": "0x0", "type": "uint256", "n_slots": 1} + } + } + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) + + def test_empty_object_succeeds(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.additional_input = {} + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) + + def test_unknown_key_fails(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.additional_input = {"unknown_key": {}} + check_constraint_fails( + lambda: dummy_compiled_contract.insert( + connection, dummy_code.code_hash, dummy_code.code_hash), + 'additional_input_json_schema') + + def test_unknown_key_alongside_valid_key_fails(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.additional_input = { + "storage_layout_overrides": {}, + "unknown_key": {} + } + check_constraint_fails( + lambda: dummy_compiled_contract.insert( + connection, dummy_code.code_hash, dummy_code.code_hash), + 'additional_input_json_schema') + + @pytest.mark.parametrize("value", ["a string", [1, 2], 42], ids=["string", "array", "number"]) + def test_non_object_type_fails(self, value, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.additional_input = value + check_constraint_fails( + lambda: dummy_compiled_contract.insert( + connection, dummy_code.code_hash, dummy_code.code_hash), + 'additional_input_json_schema') diff --git a/tests/test_constraint_compilation_artifacts_json_schema.py b/tests/test_constraint_compilation_artifacts_json_schema.py index e94ed10..0a03a7d 100644 --- a/tests/test_constraint_compilation_artifacts_json_schema.py +++ b/tests/test_constraint_compilation_artifacts_json_schema.py @@ -59,32 +59,45 @@ def test_missing_abi_fails(self, connection, dummy_code, dummy_compiled_contract check_constraint_fails( lambda: dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash), 'compilation_artifacts_json_schema') - def test_missing_userdoc_fails(self, connection, dummy_code, dummy_compiled_contract): + def test_missing_userdoc_succeeds(self, connection, dummy_code, dummy_compiled_contract): dummy_compiled_contract.compilation_artifacts.pop("userdoc") - check_constraint_fails( - lambda: dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash), 'compilation_artifacts_json_schema') + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) - def test_missing_devdoc_fails(self, connection, dummy_code, dummy_compiled_contract): + def test_missing_devdoc_succeeds(self, connection, dummy_code, dummy_compiled_contract): dummy_compiled_contract.compilation_artifacts.pop("devdoc") - check_constraint_fails( - lambda: dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash), 'compilation_artifacts_json_schema') + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) + + def test_missing_storage_layout_succeeds(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.compilation_artifacts.pop("storageLayout") + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) + + def test_only_mandatory_fields_succeeds(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.compilation_artifacts = {"abi": None, "sources": None} + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) def test_missing_sources_fails(self, connection, dummy_code, dummy_compiled_contract): dummy_compiled_contract.compilation_artifacts.pop("sources") check_constraint_fails( lambda: dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash), 'compilation_artifacts_json_schema') - def test_missing_storage_layout_fails(self, connection, dummy_code, dummy_compiled_contract): - dummy_compiled_contract.compilation_artifacts.pop("storageLayout") - check_constraint_fails( - lambda: dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash), 'compilation_artifacts_json_schema') - def test_unknown_field_fails(self, connection, dummy_code, dummy_compiled_contract): dummy_compiled_contract.compilation_artifacts['unknown_key'] = None check_constraint_fails( lambda: dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash), 'compilation_artifacts_json_schema') +class TestTransientStorageLayout: + # transientStorageLayout was added as an optional field by the add_transientStorageLayout migration + + def test_with_transient_storage_layout_as_none_succeeds(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.compilation_artifacts["transientStorageLayout"] = None + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) + + def test_with_transient_storage_layout_as_object_succeeds(self, connection, dummy_code, dummy_compiled_contract): + dummy_compiled_contract.compilation_artifacts["transientStorageLayout"] = {"MyVar": {"slot": "0x0", "type": "uint256"}} + dummy_compiled_contract.insert(connection, dummy_code.code_hash, dummy_code.code_hash) + + class TestAbi: @pytest.mark.parametrize("value", [0, "", {}], ids=["number", "string", "object"]) def test_invalid_type_fails(self, value, connection, dummy_code, dummy_compiled_contract):