Skip to content

Commit

Permalink
Hangzhou support (#265)
Browse files Browse the repository at this point in the history
  • Loading branch information
m-kus authored Nov 26, 2021
1 parent 6f7284d commit c51282a
Show file tree
Hide file tree
Showing 111 changed files with 2,721 additions and 179 deletions.
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog

## 3.3.0 - 2021-11-26

### Added

* Changelog 😅
* Support for on-chain views:
- Multiple `view` sections are correctly parsed/unparsed
- in REPL `VIEW` instruction works both with self-recursive calls and on-chain contracts (if shell is attached)
- `ContractInterface` provides a seamless interface to views (works pretty much the same as with off-chain views)
* Partial support for global constants:
- added new operation kind `register_global_constant`
- `ExecutionContext` allows to register constants as well
- `ContractInterface` resolves all the constants using the context upon creation
- Since there is no RPC for retrieving on-chain global constants proper resolving cannot be implemented
- It is not possible to use constants in transaction parameters and origination script with high-level entities
* Minimal support for timelock feature:
- `chest`, `chest_key`, and `CHEST` primitives are supported in parser, but not in the REPL
- There are currenty no way to construct a timelock

### Changed

* Hangzhou (PtHangz2) RPC endpoint (`hangzhou` is the default shell now), sandbox image (`v11.0-1`)

### Fixed

* `pytezos sandbox` CLI command now works properly and provides almost flextesa-like experience at lesser cost
* Operation branch was calculated incorrectly based on the TTL (before `head~{60-ttl}`, after `head~{120-ttl}`)
43 changes: 33 additions & 10 deletions src/pytezos/cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import atexit
import io
import sys
import tarfile
Expand All @@ -9,6 +10,8 @@

import click
import docker # type: ignore
import requests
from testcontainers.core.generic import DockerContainer # type: ignore

from pytezos import ContractInterface, __version__, pytezos
from pytezos.cli.github import create_deployment, create_deployment_status
Expand All @@ -17,8 +20,8 @@
from pytezos.michelson.types.base import generate_pydoc
from pytezos.operation.result import OperationResult
from pytezos.rpc.errors import RpcError
from pytezos.sandbox.node import SandboxedNodeTestCase
from pytezos.sandbox.parameters import EDO, FLORENCE
from pytezos.sandbox.node import DOCKER_IMAGE
from pytezos.sandbox.parameters import EDO, FLORENCE, HANGZHOU

kernel_js_path = join(dirname(dirname(__file__)), 'assets', 'kernel.js')
kernel_json = {
Expand All @@ -45,6 +48,7 @@ def get_local_contract_path(path, extension='tz'):
return path
return False


def get_contract(path):
path = get_local_contract_path(path)
if path:
Expand Down Expand Up @@ -314,9 +318,10 @@ def smartpy_compile(
else:
logger.error('No local script found. Please ensure a valid script is present or specify path.')


@cli.command(help='Run containerized sandbox node')
@click.option('--image', type=str, help='Docker image to use', default=SandboxedNodeTestCase.IMAGE)
@click.option('--protocol', type=click.Choice(['florence', 'edo']), help='Protocol to use', default='florence')
@click.option('--image', type=str, help='Docker image to use', default=DOCKER_IMAGE)
@click.option('--protocol', type=click.Choice(['edo', 'florence', 'hangzhou']), help='Protocol to use', default='hangzhou')
@click.option('--port', '-p', type=int, help='Port to expose', default=8732)
@click.option('--interval', '-i', type=float, help='Interval between baked blocks (in seconds)', default=1.0)
@click.option('--blocks', '-b', type=int, help='Number of blocks to bake before exit')
Expand All @@ -329,21 +334,38 @@ def sandbox(
interval: float,
blocks: int,
):
protocol = {
protocol_hash = {
'edo': EDO,
'florence': FLORENCE,
'hangzhou': HANGZHOU,
}[protocol]

SandboxedNodeTestCase.PROTOCOL = protocol
SandboxedNodeTestCase.IMAGE = image
SandboxedNodeTestCase.PORT = port
SandboxedNodeTestCase.setUpClass()
container = DockerContainer(image)
if port:
container.ports[8732] = port

container.start()
atexit.register(container.stop)

container_id = container.get_wrapped_container().id
host = container.get_docker_client().bridge_ip(container_id)
client = pytezos.using(key='bootstrap1', shell=f'http://{host}:8732')

while True:
try:
client.shell.node.get("/version/")
break
except requests.exceptions.ConnectionError:
time.sleep(0.1)

logger.info('Activating protocol %s...', protocol_hash)
client.using(key='dictator').activate_protocol(protocol_hash).fill().sign().inject()

blocks_baked = 0
while True:
try:
logger.info('Baking block %s...', blocks_baked)
block_hash = SandboxedNodeTestCase.get_client().using(key='bootstrap1').bake_block().fill().work().sign().inject()
block_hash = client.bake_block().fill().work().sign().inject()
logger.info('Baked block: %s', block_hash)
blocks_baked += 1

Expand All @@ -354,6 +376,7 @@ def sandbox(
except KeyboardInterrupt:
break


@cli.command(help='Update Ligo compiler (docker pull ligolang/ligo)')
@click.option('--tag', '-t', type=str, help='Version or tag to pull', default='0.13.0')
@click.pass_context
Expand Down
16 changes: 14 additions & 2 deletions src/pytezos/context/abstract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Tuple
from typing import List, Optional, Tuple

from pyblake2 import blake2b # type: ignore

Expand Down Expand Up @@ -66,12 +66,21 @@ def spend_balance(self, amount: int):
def get_parameter_expr(self, address=None) -> Optional: # type: ignore
raise NotImplementedError

def get_storage_expr(self):
def get_storage_expr(self, address=None) -> Optional: # type: ignore
raise NotImplementedError

def get_storage_value(self, address=None) -> Optional: # type: ignore
raise NotImplementedError

def get_code_expr(self):
raise NotImplementedError

def get_view_expr(self, name, address=None) -> Optional: # type: ignore
raise NotImplementedError

def get_views_expr(self) -> List: # type: ignore
raise NotImplementedError

def get_input_expr(self):
raise NotImplementedError

Expand Down Expand Up @@ -111,6 +120,9 @@ def set_parameter_expr(self, type_expr):
def set_code_expr(self, code_expr):
raise NotImplementedError

def set_view_expr(self, name, view_expr):
raise NotImplementedError

def set_input_expr(self, code_expr):
raise NotImplementedError

Expand Down
100 changes: 90 additions & 10 deletions src/pytezos/context/impl.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from datetime import datetime
from itertools import chain
from typing import Optional, Tuple
from typing import List, Optional, Tuple

from pytezos.context.abstract import AbstractContext, get_originated_address # type: ignore
from pytezos.crypto.encoding import base58_encode
from pytezos.crypto.key import Key
from pytezos.logging import logger
from pytezos.michelson.micheline import get_script_section
from pytezos.michelson.forge import forge_micheline, forge_script_expr
from pytezos.michelson.micheline import get_script_section, get_script_sections, MichelineT
from pytezos.operation import DEFAULT_OPERATIONS_TTL, MAX_OPERATIONS_TTL
from pytezos.rpc.errors import RpcError
from pytezos.rpc.shell import ShellQuery

DEFAULT_IPFS_GATEWAY = 'https://ipfs.io/ipfs'


class ExecutionContext(AbstractContext):

def __init__(self, amount=None, chain_id=None, protocol=None, source=None, sender=None, balance=None,
block_id=None, now=None, level=None, voting_power=None, total_voting_power=None,
key=None, shell=None, address=None, counter=None, script=None, tzt=False, mode=None, ipfs_gateway=None):
key=None, shell=None, address=None, counter=None, script=None, tzt=False, mode=None, ipfs_gateway=None,
global_constants=None):
self.key: Optional[Key] = key
self.shell: Optional[ShellQuery] = shell
self.counter = counter
Expand All @@ -38,6 +41,7 @@ def __init__(self, amount=None, chain_id=None, protocol=None, source=None, sende
self.parameter_expr = get_script_section(script, name='parameter') if script and not tzt else None
self.storage_expr = get_script_section(script, name='storage') if script and not tzt else None
self.code_expr = get_script_section(script, name='code') if script else None
self.views_expr = get_script_sections(script, name='view') if script else []
self.input_expr = get_script_section(script, name='input') if script and tzt else None
self.output_expr = get_script_section(script, name='output') if script and tzt else None
self.sender_expr = get_script_section(script, name='sender') if script and tzt else None
Expand All @@ -56,9 +60,11 @@ def __init__(self, amount=None, chain_id=None, protocol=None, source=None, sende
self.balance_update = 0
self.big_maps = {}
self.tzt_big_maps = {}
self.global_constants = global_constants or {}
self.debug = False
self._sandboxed: Optional[bool] = None
self.ipfs_gateway = (ipfs_gateway or DEFAULT_IPFS_GATEWAY).rstrip('/')
self.storage_value = script.get('storage') if script else None

def __copy__(self):
raise ValueError("It's not allowed to copy context")
Expand All @@ -73,7 +79,8 @@ def constants(self):
@property
def script(self) -> Optional[dict]:
if self.parameter_expr and self.storage_expr and self.code_expr:
return dict(code=[self.parameter_expr, self.storage_expr, self.code_expr])
return dict(code=[self.parameter_expr, self.storage_expr, self.code_expr, *self.views_expr],
storage=self.storage_expr)
else:
return None

Expand All @@ -96,6 +103,7 @@ def reset(self):
self.balance_update = 0
self.big_maps.clear()
self.tzt_big_maps.clear()
self.global_constants.clear()

def set_counter(self, counter: int):
self.counter = counter
Expand Down Expand Up @@ -173,20 +181,55 @@ def spend_balance(self, amount: int):
assert amount <= balance, f'cannot spend {amount} tez, {balance} tez left'
self.balance_update -= amount

def get_parameter_expr(self, address=None) -> Optional[str]:
def get_parameter_expr(self, address=None) -> Optional[dict]:
if self.shell and address:
if address == get_originated_address(0):
return None # dummy callback
else:
script = self.shell.contracts[address].script()
return get_script_section(script, name='parameter', cls=None, required=True)
return None if address else self.parameter_expr
expr = get_script_section(script, name='parameter', cls=None, required=True) # type: ignore
elif address:
return None
else:
expr = self.parameter_expr
return self.resolve_global_constants(expr)

def get_storage_expr(self):
return self.storage_expr
def get_storage_expr(self, address=None) -> Optional[dict]:
if self.shell and address:
script = self.shell.contracts[address].script()
expr = get_script_section(script, name='storage', cls=None, required=True) # type: ignore
elif address:
return None
else:
expr = self.storage_expr
return self.resolve_global_constants(expr)

def get_storage_value(self, address=None) -> Optional[dict]:
if self.shell:
return self.shell.head.context.contracts[address].storage()
return None if address else self.resolve_global_constants(self.storage_value)

def get_code_expr(self):
return self.code_expr
return self.resolve_global_constants(self.code_expr)

def get_views_expr(self) -> List[dict]:
return self.resolve_global_constants(self.views_expr)

def get_view_expr(self, name, address=None) -> Optional[dict]:
if address:
if self.shell:
script = self.shell.contracts[address].script()
views = get_script_sections(script, name='view', cls=None)
else:
return None
else:
views = self.views_expr

try:
expr = next(view for view in views if view['args'][0]['string'] == name)
return self.resolve_global_constants(expr)
except (StopIteration, KeyError, IndexError):
return None

def get_input_expr(self):
return self.input_expr
Expand Down Expand Up @@ -378,3 +421,40 @@ def get_operations_ttl(self) -> int:
if self.sandboxed:
return MAX_OPERATIONS_TTL
return DEFAULT_OPERATIONS_TTL

def register_global_constant(self, expression):
""" Register global constant
:param expression: Micheline expression
"""
constant_hash = forge_script_expr(forge_micheline(expression))
self.global_constants[constant_hash] = expression

def resolve_global_constants(self, expression):
""" Replace global constants with their respectful values or throw an error if the constant is not defined
:param expression: Micheline expression
"""
def _resolve_constant(node):
try:
constant_hash = node['args'][0]['string']
except (KeyError, IndexError) as e:
raise ValueError('Unexpected constant expression') from e
if constant_hash not in self.global_constants:
raise KeyError(f'Constant {constant_hash} is not defined')
return _resolve(self.global_constants[constant_hash])
# TODO: check if global constants are really recursive

def _resolve(node):
if isinstance(node, dict):
if node.get('prim') == 'constant':
return _resolve_constant(node)
elif node.get('args'):
args = list(map(_resolve, node['args']))
return {k: v if k != 'args' else args for k, v in node.items()}
else:
return node
elif isinstance(node, list):
return list(map(_resolve, node))
else:
return node

return _resolve(expression)
7 changes: 5 additions & 2 deletions src/pytezos/context/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pytezos.rpc import RpcMultiNode, RpcNode, ShellQuery
from pytezos.rpc.errors import RpcError

default_network = 'granadanet'
default_network = 'hangzhounet'
default_key = 'edsk33N474hxzA4sKeWVM6iuGNGDpX2mGwHNxEA4UbWS8sW3Ta3NKH' # please, use responsibly
default_key_hash = 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm'

Expand All @@ -25,7 +25,8 @@
'sandboxnet': ['http://127.0.0.1:8732/'],
'localhost': ['http://127.0.0.1:8732/'],
'florencenet': ['https://testnet-tezos.giganode.io'],
'granadanet': ['https://rpc.tzkt.io/granadanet']
'granadanet': ['https://rpc.tzkt.io/granadanet'],
'hangzhounet': ['https://rpc.tzkt.io/hangzhou2net'],
}
keys = {
'alice': alice_key,
Expand Down Expand Up @@ -123,6 +124,7 @@ def _spawn_context(
mode: Optional[str] = None,
script: Optional[dict] = None,
ipfs_gateway: Optional[str] = None,
balance: Optional[int] = None,
) -> ExecutionContext:
if isinstance(shell, str):
if shell.endswith('.pool'):
Expand Down Expand Up @@ -166,4 +168,5 @@ def _spawn_context(
script=script or self.context.script,
mode=mode or self.context.mode,
ipfs_gateway=ipfs_gateway,
balance=balance or self.context.balance
)
Loading

0 comments on commit c51282a

Please sign in to comment.