Skip to content

Commit

Permalink
fix: deleting proxies [APE-1346] (#1653)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Sep 15, 2023
1 parent 21405f5 commit 0092088
Show file tree
Hide file tree
Showing 8 changed files with 647 additions and 573 deletions.
28 changes: 23 additions & 5 deletions src/ape/managers/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,12 +823,30 @@ def __delitem__(self, address: AddressType):
if address in self._local_contract_types:
del self._local_contract_types[address]

if self._is_live_network:
if not self._contract_types_cache.is_dir():
return
# Delete proxy.
if address in self._local_proxies:
info = self._local_proxies[address]
target = info.target
del self._local_proxies[address]

address_file = self._contract_types_cache / f"{address}.json"
address_file.unlink(missing_ok=True)
# Also delete target.
if target in self._local_contract_types:
del self._local_contract_types[target]

if self._is_live_network:
if self._contract_types_cache.is_dir():
address_file = self._contract_types_cache / f"{address}.json"
address_file.unlink(missing_ok=True)

if self._proxy_info_cache.is_dir():
disk_info = self._get_proxy_info_from_disk(address)
if disk_info:
target = disk_info.target
address_file = self._proxy_info_cache / f"{address}.json"
address_file.unlink()

# Also delete the target.
self.__delitem__(target)

def __contains__(self, address: AddressType) -> bool:
return self.get(address) is not None
Expand Down
9 changes: 6 additions & 3 deletions src/ape_ethereum/proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from ape.api.networks import ProxyInfoAPI
from ape.contracts import ContractContainer

MINIMAL_PROXY_TARGET_PLACEHOLDER = "bebebebebebebebebebebebebebebebebebebebe"
MINIMAL_PROXY_BYTES = (
"0x3d602d80600a3d3981f3363d3d373d3d3d363d73"
"bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3"
f"{MINIMAL_PROXY_TARGET_PLACEHOLDER}5af43d82803e903d91602b57fd5bf3"
)


Expand Down Expand Up @@ -87,8 +88,10 @@ class ProxyInfo(ProxyInfoAPI):
)


def _make_minimal_proxy() -> ContractContainer:
bytecode = {"bytecode": MINIMAL_PROXY_BYTES}
def _make_minimal_proxy(address: str = MINIMAL_PROXY_TARGET_PLACEHOLDER) -> ContractContainer:
address = address.replace("0x", "")
code = MINIMAL_PROXY_BYTES.replace(MINIMAL_PROXY_TARGET_PLACEHOLDER, address)
bytecode = {"bytecode": code}
contract_type = ContractType(abi=[], deploymentBytecode=bytecode)
return ContractContainer(contract_type=contract_type)

Expand Down
11 changes: 11 additions & 0 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ape.logging import LogLevel
from ape.logging import logger as _logger
from ape.types import AddressType, ContractLog
from ape_ethereum.proxies import minimal_proxy as _minimal_proxy_container

PROJECT_PATH = Path(__file__).parent
CONTRACTS_FOLDER = PROJECT_PATH / "data" / "contracts" / "ethereum" / "local"
Expand Down Expand Up @@ -503,3 +504,13 @@ def vyper_factory(owner, get_contract_type):
def vyper_blueprint(owner, vyper_contract_container):
receipt = owner.declare(vyper_contract_container)
return receipt.contract_address


@pytest.fixture
def minimal_proxy_container():
return _minimal_proxy_container


@pytest.fixture
def minimal_proxy(owner, minimal_proxy_container):
return owner.deploy(minimal_proxy_container)
149 changes: 149 additions & 0 deletions tests/functional/test_block_container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import time
from queue import Queue
from typing import List

import pytest
from ethpm_types import HexBytes

from ape.exceptions import ChainError


def test_iterate_blocks(chain_that_mined_5):
blocks = [b for b in chain_that_mined_5.blocks]
assert len(blocks) >= 5 # Because mined 5 blocks so is at least 5

iterator = blocks[0].number
for block in blocks:
assert block.number == iterator
iterator += 1


def test_blocks_range(chain_that_mined_5):
# The number of the block before mining the 5
start_block = len(chain_that_mined_5.blocks) - 5
num_to_get = 3 # Expecting blocks [s, s+1, s+2]
blocks = [b for b in chain_that_mined_5.blocks.range(start_block, start_block + num_to_get)]
assert len(blocks) == num_to_get

expected_number = start_block
prev_block_hash = (
HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000")
if start_block == 0
else chain_that_mined_5.blocks[start_block - 1].hash
)
for block in blocks:
assert block.number == expected_number
expected_number += 1
assert block.parent_hash == prev_block_hash
prev_block_hash = block.hash


def test_blocks_range_too_high_stop(chain_that_mined_5):
num_blocks = len(chain_that_mined_5.blocks)
num_blocks_add_1 = num_blocks + 1
expected = (
rf"'stop={num_blocks_add_1}' cannot be greater than the chain length \({num_blocks}\)\. "
rf"Use 'poll_blocks\(\)' to wait for future blocks\."
)
with pytest.raises(ChainError, match=expected):
# Have to run through generator to trigger code in definition.
list(chain_that_mined_5.blocks.range(num_blocks_add_1))


def test_block_range_with_step(chain_that_mined_5):
blocks = [b for b in chain_that_mined_5.blocks.range(3, step=2)]
assert len(blocks) == 2
assert blocks[0].number == 0
assert blocks[1].number == 2


def test_block_range_negative_start(chain_that_mined_5):
with pytest.raises(ValueError) as err:
_ = [b for b in chain_that_mined_5.blocks.range(-1, 3, step=2)]

assert "ensure this value is greater than or equal to 0" in str(err.value)


def test_block_range_out_of_order(chain_that_mined_5):
with pytest.raises(ValueError) as err:
_ = [b for b in chain_that_mined_5.blocks.range(3, 1, step=2)]

assert "stop_block: '0' cannot be less than start_block: '3'." in str(err.value)


def test_block_timestamp(chain):
chain.mine()
assert chain.blocks.head.timestamp == chain.blocks.head.datetime.timestamp()


def test_poll_blocks_stop_block_not_in_future(chain_that_mined_5):
bad_stop_block = chain_that_mined_5.blocks.height

with pytest.raises(ValueError, match="'stop' argument must be in the future."):
_ = [x for x in chain_that_mined_5.blocks.poll_blocks(stop_block=bad_stop_block)]


def test_poll_blocks(chain_that_mined_5, eth_tester_provider, owner, PollDaemon):
blocks: Queue = Queue(maxsize=3)
poller = chain_that_mined_5.blocks.poll_blocks()

with PollDaemon("blocks", poller, blocks.put, blocks.full):
# Sleep first to ensure listening before mining.
time.sleep(1)
eth_tester_provider.mine(3)

assert blocks.full()
first = blocks.get().number
second = blocks.get().number
third = blocks.get().number
assert first == second - 1
assert second == third - 1


def test_poll_blocks_reorg(chain_that_mined_5, eth_tester_provider, owner, PollDaemon, caplog):
blocks: Queue = Queue(maxsize=6)
poller = chain_that_mined_5.blocks.poll_blocks()

with PollDaemon("blocks", poller, blocks.put, blocks.full):
# Sleep first to ensure listening before mining.
time.sleep(1)

snapshot = chain_that_mined_5.snapshot()
chain_that_mined_5.mine(2)

# Wait to allow blocks before re-org to get yielded
time.sleep(5)

# Simulate re-org by reverting to the snapshot
chain_that_mined_5.restore(snapshot)

# Allow it time to trigger realizing there was a re-org
time.sleep(1)
chain_that_mined_5.mine(2)
time.sleep(1)

chain_that_mined_5.mine(3)

assert blocks.full()

# Show that re-org was detected
expected_error = (
"Chain has reorganized since returning the last block. "
"Try adjusting the required network confirmations."
)
assert caplog.records, "Didn't detect re-org"
assert expected_error in caplog.records[-1].message

# Show that there are duplicate blocks
block_numbers: List[int] = [blocks.get().number for _ in range(6)]
assert len(set(block_numbers)) < len(block_numbers)


def test_poll_blocks_timeout(
vyper_contract_instance, chain_that_mined_5, eth_tester_provider, owner, PollDaemon
):
poller = chain_that_mined_5.blocks.poll_blocks(new_block_timeout=1)

with pytest.raises(ChainError, match=r"Timed out waiting for new block \(time_waited=1.\d+\)."):
with PollDaemon("blocks", poller, lambda x: None, lambda: False):
time.sleep(1.5)
Loading

0 comments on commit 0092088

Please sign in to comment.