Repository: 0xSpaceShard/starknet-devnet (language: python)

Issue:
Declaring Cairo 1 Contract fail in forked mode
**Describe the bug (observed vs expected behavior)**
Error will tring to do :
`const declareResponse = await account.declare({
        contract: compiledContract,
        casm: compiledSierraCasm,
    });`
  With devnet running in forking mode.
  
A clear and concise description of what the bug is. If applicable, add screenshots to help explain your problem. However, prefer providing plain text.
```
[2023-05-05 15:28:33 +0200] [76540] [ERROR] {'prime': ['Unknown field.'], 'hints': ['Unknown field.'], 'pythonic_hints': ['Unknown field.'], 'compiler_version': ['Unknown field.'], 'bytecode': ['Unknown field.']}
Traceback (most recent call last):
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starkware_utils/error_handling.py", line 262, in wrap_with_stark_exception
    yield
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starknet/business_logic/transaction/state_objects.py", line 120, in apply_concurrent_changes
    return self._apply_specific_concurrent_changes(
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starknet/business_logic/transaction/objects.py", line 669, in _apply_specific_concurrent_changes
    state.get_compiled_class_hash(class_hash=self.class_hash),
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starknet/business_logic/state/state.py", line 530, in get_compiled_class_hash
    return self.state.get_compiled_class_hash(class_hash=class_hash)
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starknet/business_logic/state/state.py", line 62, in get_compiled_class_hash
    return execute_coroutine_threadsafe(
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/python/utils.py", line 570, in execute_coroutine_threadsafe
    return future.result()
  File "/opt/homebrew/Cellar/python@3.9/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 446, in result
    return self.__get_result()
  File "/opt/homebrew/Cellar/python@3.9/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 391, in __get_result
    raise self._exception
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starknet/business_logic/state/state.py", line 236, in get_compiled_class_hash
    compiled_class_hash = await self.state_reader.get_compiled_class_hash(
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starknet/business_logic/state/state.py", line 236, in get_compiled_class_hash
    compiled_class_hash = await self.state_reader.get_compiled_class_hash(
  File "/users/cairo_venv/lib/python3.9/site-packages/starknet_devnet/forked_state.py", line 109, in get_compiled_class_hash
    compiled_class = CompiledClassBase.load(compiled_class_dict)
  File "/users/cairo_venv/lib/python3.9/site-packages/starkware/starkware_utils/serializable_dataclass.py", line 23, in load
    return cls.Schema().load(data=data)
  File "/users/cairo_venv/lib/python3.9/site-packages/marshmallow_dataclass/__init__.py", line 638, in load
    all_loaded = super().load(data, many=many, **kwargs)
  File "/users/cairo_venv/lib/python3.9/site-packages/marshmallow/schema.py", line 722, in load
    return self._do_load(
  File "/users/cairo_venv/lib/python3.9/site-packages/marshmallow/schema.py", line 909, in _do_load
    raise exc
marshmallow.exceptions.ValidationError: {'prime': ['Unknown field.'], 'hints': ['Unknown field.'], 'pythonic_hints': ['Unknown field.'], 'compiler_version': ['Unknown field.'], 'bytecode': ['Unknown field.']}
```

**Not reproducible on alpha-goerli**

- [ ] This issue is only present on Devnet and cannot be reproduced on alpha-goerli (check the box if true).

**To Reproduce**
Steps to reproduce the behavior:

1. ...  `starknet-devnet  --fork-network alpha-goerli`.
2. ...StarknetJs 
`const declareResponse = await account.declare({
        contract: compiledContract,
        casm: compiledSierraCasm,
    });`
   ...

**Devnet version**

This code works if I am not running devnet in fork mode.

- I am using Devnet version: 0.5.1
- [ ] This happens with a dockerized Devnet (check the box if true).
- This does not appear on the following Devnet version:

**System specifications**

- OS:
- CPU:
- Memory:
- Python version:



Discussion / hints:
This happens when trying to declare an already declared Cairo1 class in forked mode. The loading here:
https://github.com/0xSpaceShard/starknet-devnet/blob/b7388321471e504a04c831dbc175d5a569b76f0c/starknet_devnet/forked_state.py#L109

should be done using the utility function already defined in https://github.com/0xSpaceShard/starknet-devnet/blob/b7388321471e504a04c831dbc175d5a569b76f0c/starknet_devnet/forked_state.py#L51

Will create a fix today.

Fix patch:
diff --git a/page/docs/guide/fork.md b/page/docs/guide/fork.md
index b8c204792..e1c2fa16b 100644
--- a/page/docs/guide/fork.md
+++ b/page/docs/guide/fork.md
@@ -18,7 +18,7 @@ You can use the `--fork-retries` parameter to specify the number of retries of f
 
 All calls will first try Devnet's state and then fall back to the forking block.
 
-If you are forking another Devnet instance, keep in mind that it doesn't support polling specific blocks, but will always fall back to the currently latest block.
+If you are forking another Devnet instance, retrieving Cairo 1 classes might not work as expected if the class is only declared on the origin Devnet. Redeclaring it in the fork should fail (as expected).
 
 ## Chain ID
 
diff --git a/starknet_devnet/forked_state.py b/starknet_devnet/forked_state.py
index 8ace0cd4f..219e433b2 100644
--- a/starknet_devnet/forked_state.py
+++ b/starknet_devnet/forked_state.py
@@ -106,7 +106,7 @@ async def get_compiled_class_hash(self, class_hash: int) -> int:
                         block_number=self.__block_number,
                     )
                 )
-            compiled_class = CompiledClassBase.load(compiled_class_dict)
+            compiled_class = _load_compiled_class(compiled_class_dict)
         except BadRequest as bad_request:
             if is_originally_starknet_exception(bad_request):
                 original_error = _extract_original_stark_exception(bad_request)
diff --git a/starknet_devnet/starknet_wrapper.py b/starknet_devnet/starknet_wrapper.py
index 61f63bf5c..d9c0e4aea 100644
--- a/starknet_devnet/starknet_wrapper.py
+++ b/starknet_devnet/starknet_wrapper.py
@@ -652,7 +652,6 @@ async def get_compiled_class_by_class_hash(self, class_hash: int) -> CompiledCla
         Return compiled class given the class hash (sierra hash).
         Should report an undeclared class if given the hash of a deprecated class
         """
-
         state = self.get_state().state
 
         try:


Test patch:
diff --git a/test/account.py b/test/account.py
index 8ae9b1342..4c79b8c6d 100644
--- a/test/account.py
+++ b/test/account.py
@@ -464,11 +464,12 @@ def send_declare_v2(
     compiled_class_hash: int,
     sender_address: str,
     sender_key: int,
+    gateway_url=APP_URL,
 ):
     """Send a declare v2 transaction"""
     max_fee = int(1e18)  # should be enough
     version = 2
-    nonce = get_nonce(sender_address)
+    nonce = get_nonce(sender_address, feeder_gateway_url=gateway_url)
     chain_id = StarknetChainId.TESTNET.value
     hash_value = calculate_declare_transaction_hash(
         contract_class=contract_class,
@@ -491,4 +492,6 @@ def send_declare_v2(
     ).dump()
     declaration_body["type"] = "DECLARE"
 
-    return requests.post(f"{APP_URL}/gateway/add_transaction", json=declaration_body)
+    return requests.post(
+        f"{gateway_url}/gateway/add_transaction", json=declaration_body
+    )
diff --git a/test/test_declare_v2.py b/test/test_declare_v2.py
index d2e582ec3..1b641d617 100644
--- a/test/test_declare_v2.py
+++ b/test/test_declare_v2.py
@@ -38,20 +38,27 @@
 )
 
 
-def assert_declare_v2_accepted(resp: requests.Response):
+def assert_declare_v2_accepted(resp: requests.Response, feeder_gateway_url=APP_URL):
     """Assert status of declaration is accepted"""
     assert resp.status_code == 200
 
     declare_tx_hash = resp.json()["transaction_hash"]
-    assert_tx_status(tx_hash=declare_tx_hash, expected_tx_status="ACCEPTED_ON_L2")
+    assert_tx_status(
+        tx_hash=declare_tx_hash,
+        expected_tx_status="ACCEPTED_ON_L2",
+        feeder_gateway_url=feeder_gateway_url,
+    )
 
 
-def _assert_already_declared(declaration_resp: requests.Response):
+def assert_already_declared(
+    declaration_resp: requests.Response, feeder_gateway_url=APP_URL
+):
+    """Assert that response informs of an already declared class"""
     assert declaration_resp.status_code == 200, declaration_resp.json()
     declare_tx_hash = declaration_resp.json()["transaction_hash"]
 
     tx_resp = requests.get(
-        f"{APP_URL}/feeder_gateway/get_transaction",
+        f"{feeder_gateway_url}/feeder_gateway/get_transaction",
         params={"transactionHash": declare_tx_hash},
     )
     assert tx_resp.status_code == 200, tx_resp.json()
@@ -120,7 +127,7 @@ def test_redeclaring_v2():
         sender_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
     )
 
-    _assert_already_declared(
+    assert_already_declared(
         send_declare_v2(
             contract_class=contract_class,
             compiled_class_hash=compiled_class_hash,
diff --git a/test/test_declare_v2_forked.py b/test/test_declare_v2_forked.py
index 59544bada..b3fee6c73 100644
--- a/test/test_declare_v2_forked.py
+++ b/test/test_declare_v2_forked.py
@@ -3,27 +3,34 @@
 The benefit of having this in a separate file is the ability to parallelize.
 """
 
+import pytest
 from starkware.starknet.services.api.contract_class.contract_class import ContractClass
 from starkware.starknet.services.api.contract_class.contract_class_utils import (
     load_sierra,
 )
 
 from .account import send_declare_v2
-from .settings import APP_URL
+from .settings import APP_URL, HOST, bind_free_port
 from .shared import (
     CONTRACT_1_CASM_PATH,
     CONTRACT_1_PATH,
     EXPECTED_CLASS_1_HASH,
+    PREDEPLOY_ACCOUNT_CLI_ARGS,
     PREDEPLOYED_ACCOUNT_ADDRESS,
     PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
 )
-from .test_declare_v2 import assert_declare_v2_accepted, load_cairo1_contract
+from .test_declare_v2 import (
+    assert_already_declared,
+    assert_declare_v2_accepted,
+    load_cairo1_contract,
+)
 from .testnet_deployment import (
     TESTNET_DEPLOYMENT_BLOCK,
     TESTNET_FORK_PARAMS,
     TESTNET_URL,
 )
 from .util import (
+    DevnetBackgroundProc,
     assert_class_by_hash_not_present,
     assert_compiled_class_by_hash,
     assert_compiled_class_by_hash_not_present,
@@ -32,6 +39,21 @@
     get_class_by_hash,
 )
 
+FORKING_DEVNET = DevnetBackgroundProc()
+
+ORIGIN_PORT, ORIGIN_URL = bind_free_port(HOST)
+FORK_PORT, FORK_URL = bind_free_port(HOST)
+
+
+@pytest.fixture(autouse=True)
+def run_before_and_after_test():
+    """Cleanup after tests finish."""
+    # before test
+    FORKING_DEVNET.stop()
+    yield
+    # after test
+    FORKING_DEVNET.stop()
+
 
 @devnet_in_background(
     *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK - 1)
@@ -86,3 +108,39 @@ def test_declare_v2_and_get_class_by_hash():
         expected_path=CONTRACT_1_CASM_PATH,
         feeder_gateway_url=APP_URL,
     )
+
+
+@devnet_in_background("--port", ORIGIN_PORT, *PREDEPLOY_ACCOUNT_CLI_ARGS)
+def test_declare_v2_if_already_declared_on_origin():
+    """Declare on origin; declare on fork; assert rejected"""
+
+    # declare on origin
+    contract_class, _, compiled_class_hash = load_cairo1_contract()
+
+    declaration_resp = send_declare_v2(
+        contract_class=contract_class,
+        compiled_class_hash=compiled_class_hash,
+        sender_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        sender_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+        gateway_url=ORIGIN_URL,
+    )
+    assert_declare_v2_accepted(declaration_resp, feeder_gateway_url=ORIGIN_URL)
+
+    # fork
+    FORKING_DEVNET.start(
+        "--port", FORK_PORT, "--fork-network", ORIGIN_URL, "--accounts", "0"
+    )
+
+    # if forking devnet with devnet, as it currently is in this test,
+    # asserting at this point that compiled class is present by requesting it
+    # would fail because of how get_compiled_class_by_class_hash is implemented
+
+    # attempt declaring on fork
+    redeclaration_resp = send_declare_v2(
+        contract_class=contract_class,
+        compiled_class_hash=compiled_class_hash,
+        sender_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        sender_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+        gateway_url=FORK_URL,
+    )
+    assert_already_declared(redeclaration_resp, feeder_gateway_url=FORK_URL)