def _connect_libraries(self): zk_print_banner(f'Deploying Libraries') sender = self.w3.eth.accounts[0] # Since eth-tester is not persistent -> always automatically deploy libraries with cfg.library_compilation_environment(): with tempfile.TemporaryDirectory() as tmpdir: with log_context('transaction', 'deploy_pki'): pki_sol = save_to_file( tmpdir, f'{cfg.pki_contract_name}.sol', library_contracts.get_pki_contract()) self._pki_contract = self._deploy_contract( sender, self.compile_contract(pki_sol, cfg.pki_contract_name)) zk_print( f'Deployed pki contract at address "{self.pki_contract.address}"' ) with log_context('transaction', 'deploy_verify_libs'): verify_sol = save_to_file( tmpdir, 'verify_libs.sol', library_contracts.get_verify_libs_code()) self._lib_addresses = {} for lib in cfg.external_crypto_lib_names: out = self._deploy_contract( sender, self.compile_contract(verify_sol, lib)) self._lib_addresses[lib] = out.address zk_print( f'Deployed crypto lib {lib} at address "{out.address}"' )
def _verify_library_integrity(self, libraries: List[Tuple[str, str]], contract_with_libs_addr: str, sol_with_libs_filename: str) -> Dict[str, str]: cname = get_contract_names(sol_with_libs_filename)[0] actual_code = self.__normalized_hex(self.w3.eth.getCode(contract_with_libs_addr)) if not actual_code: raise IntegrityError(f'Expected contract {cname} is not deployed at address {contract_with_libs_addr}') code_with_placeholders = self.__normalized_hex(self.compile_contract(sol_with_libs_filename, cname)['deployed_bin']) if len(actual_code) != len(code_with_placeholders): raise IntegrityError(f'Local code of contract {cname} has different length than remote contract') addresses = {} for lib_name, lib_sol in libraries: # Compute placeholder according to # https://solidity.readthedocs.io/en/v0.5.13/using-the-compiler.html#using-the-commandline-compiler hash = self.w3.solidityKeccak(['string'], [f'{lib_sol[lib_sol.rfind("/") + 1:]}:{lib_name}']) placeholder = f'__${self.__normalized_hex(hash)[:34]}$__' # Retrieve concrete address in deployed code at placeholder offset in local code and verify library contract integrity lib_address_offset = code_with_placeholders.find(placeholder) if lib_address_offset != -1: lib_address = self.w3.toChecksumAddress(actual_code[lib_address_offset:lib_address_offset+40]) with cfg.library_compilation_environment(): self._verify_contract_integrity(lib_address, lib_sol, contract_name=lib_name, is_library=True) addresses[lib_name] = lib_address return addresses
def _connect_libraries(self): if not cfg.blockchain_pki_address: raise BlockChainError('Must specify pki address in config.') lib_addresses = [] if cfg.external_crypto_lib_names: lib_addresses = [addr.strip() for addr in cfg.blockchain_crypto_lib_addresses.split(',')] if cfg.blockchain_crypto_lib_addresses else [] if len(lib_addresses) != len(cfg.external_crypto_lib_names): raise BlockChainError('Must specify all crypto library addresses in config\n' f'Expected {len(cfg.external_crypto_lib_names)} was {len(lib_addresses)}') with cfg.library_compilation_environment(): with tempfile.TemporaryDirectory() as tmpdir: for crypto_params in cfg.all_crypto_params(): contract_name = cfg.get_pki_contract_name(crypto_params.crypto_name) pki_sol = save_to_file(tmpdir, f'{contract_name}.sol', library_contracts.get_pki_contract(crypto_params)) self._pki_contract = self._verify_contract_integrity(cfg.blockchain_pki_address, pki_sol, contract_name=contract_name) verify_sol = save_to_file(tmpdir, 'verify_libs.sol', library_contracts.get_verify_libs_code()) self._lib_addresses = {} for lib, addr in zip(cfg.external_crypto_lib_names, lib_addresses): out = self._verify_contract_integrity(addr, verify_sol, contract_name=lib, is_library=True) self._lib_addresses[lib] = out.address
def main(): # parse arguments a = parse_arguments() from zkay.config_version import Versions if a.cmd == 'version': print(Versions.ZKAY_VERSION) return if a.cmd == 'update-solc': try: import solcx solcx.install_solc_pragma( Versions.ZKAY_SOLC_VERSION_COMPATIBILITY.expression) except Exception as e: print(f'ERROR: Error while updating solc\n{e}') return from pathlib import Path import zkay.zkay_frontend as frontend from zkay import my_logging from zkay.config import cfg from zkay.utils.helpers import read_file, save_to_file from zkay.errors.exceptions import ZkayCompilerError from zkay.my_logging.log_context import log_context from zkay.utils.progress_printer import fail_print, success_print from zkay.zkay_ast.process_ast import get_processed_ast, get_parsed_ast_and_fake_code # Load configuration files try: cfg.load_configuration_from_disk(a.config_file) except Exception as e: with fail_print(): print(f"ERROR: Failed to load configuration files\n{e}") exit(42) # Support for overriding any user config setting via command line # The evaluation order for configuration loading is: # Default values in config.py -> Site config.json -> user config.json -> local config.json -> cmdline arguments # Settings defined at a later stage override setting values defined at an earlier stage override_dict = {} for name in vars(UserConfig): if name[0] != '_' and hasattr(a, name): val = getattr(a, name) if val is not None: override_dict[name] = val cfg.override_defaults(override_dict) if a.cmd in ['deploy-pki', 'deploy-crypto-libs']: import tempfile from zkay.compiler.privacy import library_contracts from zkay.transaction.runtime import Runtime with tempfile.TemporaryDirectory() as tmpdir: try: with cfg.library_compilation_environment(): if a.cmd == 'deploy-pki': file = save_to_file( tmpdir, f'{cfg.pki_contract_name}.sol', library_contracts.get_pki_contract()) addr = Runtime.blockchain().deploy_solidity_contract( file, cfg.pki_contract_name, a.account) print(f'Deployed pki contract at: {addr}') else: if not cfg.external_crypto_lib_names: print( 'Current proving scheme does not require library deployment' ) else: file = save_to_file( tmpdir, 'verify_libs.sol', library_contracts.get_verify_libs_code()) for lib in cfg.external_crypto_lib_names: addr = Runtime.blockchain( ).deploy_solidity_contract( file, lib, a.account) print( f'Deployed crypto library {lib} at: {addr}' ) except Exception as e: with fail_print(): print(f"ERROR: Deployment failed\n{e}") else: # Solc version override if hasattr(a, 'solc_version') and a.solc_version is not None: try: cfg.override_solc(a.solc_version) except ValueError as e: with fail_print(): print(f'Error: {e}') exit(10) print(f'Using solc version {Versions.SOLC_VERSION}') input_path = Path(a.input) if not input_path.exists(): with fail_print(): print(f'Error: input file \'{input_path}\' does not exist') exit(1) if a.cmd == 'check': # only type-check print(f'Type checking file {input_path.name}:') code = read_file(str(input_path)) try: get_processed_ast(code) except ZkayCompilerError as e: with fail_print(): print(f'{e}') exit(3) elif a.cmd == 'solify': was_unit_test = cfg.is_unit_test cfg._is_unit_test = True # Suppress other output try: _, fake_code = get_parsed_ast_and_fake_code( read_file(str(input_path))) print(fake_code) except ZkayCompilerError as e: with fail_print(): print(f'{e}') exit(3) finally: cfg._is_unit_test = was_unit_test exit(0) elif a.cmd == 'compile': # create output directory output_dir = Path(a.output).absolute() if not output_dir.exists(): os.makedirs(output_dir) elif not output_dir.is_dir(): with fail_print(): print(f'Error: \'{output_dir}\' is not a directory') exit(2) # Enable logging if a.log: log_file = my_logging.get_log_file(filename='compile', include_timestamp=False, label=None) my_logging.prepare_logger(log_file) # only type-check print(f'Compiling file {input_path.name}:') # compile with log_context('inputfile', os.path.basename(a.input)): try: frontend.compile_zkay_file(str(input_path), str(output_dir)) except ZkayCompilerError as e: with fail_print(): print(f'{e}') exit(3) elif a.cmd == 'import': # create output directory output_dir = Path(a.output).absolute() if output_dir.exists(): with fail_print(): print(f'Error: \'{output_dir}\' already exists') exit(2) try: frontend.extract_zkay_package(str(input_path), str(output_dir)) except ZkayCompilerError as e: with fail_print(): print( f"ERROR while compiling unpacked zkay contract.\n{e}") exit(3) except Exception as e: with fail_print(): print(f"ERROR while unpacking zkay contract\n{e}") exit(5) elif a.cmd == 'export': output_filename = Path(a.output).absolute() os.makedirs(output_filename.parent, exist_ok=True) try: frontend.package_zkay_contract(str(input_path), str(output_filename)) except Exception as e: with fail_print(): print(f"ERROR while exporting zkay contract\n{e}") exit(4) elif a.cmd in ['run', 'deploy', 'connect']: from enum import IntEnum from zkay.transaction.offchain import ContractSimulator def echo_only_simple_expressions(e): if isinstance(e, (bool, int, str, list, tuple, IntEnum)): print(e) # Enable logging if a.log: log_file = my_logging.get_log_file( filename=f'transactions_{input_path.name}', include_timestamp=True, label=None) my_logging.prepare_logger(log_file) contract_dir = str(input_path.absolute()) frontend.use_configuration_from_manifest(contract_dir) if a.account is not None: me = a.account else: me = ContractSimulator.default_address() if me is not None: me = me.val import code import sys if a.cmd == 'run': # Dynamically load module and replace globals with module globals contract_mod = frontend.load_transaction_interface_from_directory( contract_dir) contract_mod.me = me sys.displayhook = echo_only_simple_expressions code.interact(local=contract_mod.__dict__) else: cmod = frontend.load_transaction_interface_from_directory( contract_dir) c = frontend.load_contract_transaction_interface_from_module( cmod) if a.cmd == 'deploy': from ast import literal_eval cargs = a.constructor_args args = [] for arg in cargs: try: val = literal_eval(arg) except Exception: val = arg args.append(val) try: c.deploy(*args, user=me, project_dir=contract_dir) except (ValueError, TypeError) as e: with fail_print(): print( f'ERROR invalid arguments.\n{e}\n\nExpected contructor signature: ', end='') if hasattr(c, 'constructor'): from inspect import signature sig = str(signature(c.constructor)) sig = sig[5:] if not sig[5:].startswith( ",") else sig[7:] # without self print(f'({sig}') else: print('()') exit(11) except Exception as e: with fail_print(): print(f'ERROR: failed to deploy contract\n{e}') exit(12) elif a.cmd == 'connect': try: c_inst = c.connect(address=a.address, user=me, project_dir=contract_dir) except Exception as e: with fail_print(): print(f'ERROR: failed to connect to contract\n{e}') exit(13) # Open interactive shell in context of contract object import inspect contract_scope = {name: getattr(c_inst, name) for name in dir(c_inst) if inspect.isclass(getattr(c_inst, name)) \ or (name != 'constructor' and hasattr(getattr(c_inst, name), '_can_be_external') and getattr(c_inst, name)._can_be_external) or name in ['state', 'api']} contract_scope['me'] = me contract_scope['help'] = lambda o=None: help( o ) if o is not None else ContractSimulator.reduced_help(c) sys.displayhook = echo_only_simple_expressions code.interact(local=contract_scope) else: raise ValueError(f'unexpected command {a.cmd}') exit(0) else: raise NotImplementedError(a.cmd) with success_print(): print("Finished successfully")
def compile_zkay(code: str, output_dir: str, import_keys: bool = False, **kwargs) -> Tuple[CircuitGenerator, str]: """ Parse, type-check and compile the given zkay code. Note: If a SolcException is raised, this indicates a bug in zkay (i.e. zkay produced solidity code which doesn't compile, without raising a ZkayCompilerError) :param code: zkay code to compile :param output_dir: path to a directory where the compilation output should be generated :param import_keys: | if false, zk-snark of all modified circuits will be generated during compilation | if true, zk-snark keys for all circuits are expected to be already present in the output directory, \ and the compilation will use the provided keys to generate the verification contracts | This option is mostly used internally when connecting to a zkay contract provided by a 3rd-party :raise ZkayCompilerError: if any compilation stage fails :raise RuntimeError: if import_keys is True and zkay file, manifest file or any of the key files is missing """ # Copy zkay code to output zkay_filename = 'contract.zkay' if import_keys and not os.path.exists(os.path.join(output_dir, zkay_filename)): raise RuntimeError('Zkay file is expected to already be in the output directory when importing keys') elif not import_keys: _dump_to_output(code, output_dir, zkay_filename) # Type checking zkay_ast = get_processed_ast(code) # Contract transformation with print_step("Transforming zkay -> public contract"): ast, circuits = transform_ast(deepcopy(zkay_ast)) # Dump libraries with print_step("Write library contract files"): with cfg.library_compilation_environment(): for crypto_params in ast.used_crypto_backends: # Write pki contract pki_contract_code = library_contracts.get_pki_contract(crypto_params) pki_contract_file = f'{cfg.get_pki_contract_name(crypto_params)}.sol' _dump_to_output(pki_contract_code, output_dir, pki_contract_file, dryrun_solc=True) # Write library contract _dump_to_output(library_contracts.get_verify_libs_code(), output_dir, ProvingScheme.verify_libs_contract_filename, dryrun_solc=True) # Write public contract file with print_step('Write public solidity code'): output_filename = 'contract.sol' solidity_code_output = _dump_to_output(to_solidity(ast), output_dir, output_filename) # Get all circuit helpers for the transformed contract circuits: List[CircuitHelper] = list(circuits.values()) # Generate offchain simulation code (transforms transactions, interface to deploy and access the zkay contract) offchain_simulation_code = PythonOffchainVisitor(circuits).visit(ast) _dump_to_output(offchain_simulation_code, output_dir, 'contract.py') # Instantiate proving scheme and circuit generator ps = proving_scheme_classes[cfg.proving_scheme]() cg = generator_classes[cfg.snark_backend](circuits, ps, output_dir) if 'verifier_names' in kwargs: assert isinstance(kwargs['verifier_names'], list) verifier_names = get_verification_contract_names(zkay_ast) assert sorted(verifier_names) == sorted([cc.verifier_contract_type.code() for cc in cg.circuits_to_prove]) kwargs['verifier_names'][:] = verifier_names[:] # Generate manifest if not import_keys: with print_step("Writing manifest file"): # Set crypto backends for unused homomorphisms to None for hom in Homomorphism: if hom not in ast.used_homomorphisms: cfg.set_crypto_backend(hom, None) manifest = { Manifest.zkay_version: cfg.zkay_version, Manifest.solc_version: cfg.solc_version, Manifest.zkay_options: cfg.export_compiler_settings(), } _dump_to_output(json.dumps(manifest), output_dir, 'manifest.json') elif not os.path.exists(os.path.join(output_dir, 'manifest.json')): raise RuntimeError('Zkay contract import failed: Manifest file is missing') # Generate circuits and corresponding verification contracts cg.generate_circuits(import_keys=import_keys) # Check that all verification contracts and the main contract compile main_solidity_files = cg.get_verification_contract_filenames() + [os.path.join(output_dir, output_filename)] for f in main_solidity_files: check_compilation(f, show_errors=False) return cg, solidity_code_output
def connect(self, project_dir: str, contract: str, contract_address: AddressValue, user_address: AddressValue) -> Any: """ Create a handle which can be used to interact with an existing contract on the chain after verifying its integrity. Project dir must contain a .zkay file, a manifest.json file as well as a \ subdirectory *verification_contract_name*\\ _out containing 'proving.key' and 'verification.key' for each verification contract. These files are referred to as 'local' files in the following explanation. If this function succeeds, it is guaranteed, that: * the remote main contract at contract_address, matches the solidity contract obtained by running zkay on the local zkay file using the configuration stored in the local manifest * the pki contract referenced in the remote main contract matches the correct zkay pki contract * the verification contracts referenced in the remote solidity contract were generated by running zkay on a zkay file equivalent to local zkay file, with zk-snark keys which match the local keys. * the library contract referenced in the verification contracts matches the correct zkay library contract This reduces the required trust to the zk-snark setup phase (i.e. you must trust that prover/verification keys were generated for the correct circuit), since you can inspect the source code of the local zkay file and check it for malicious behavior yourself (and the zkay implementation, which performs the transformation, is open source as well). Example Scenarios: a) the remote zkay contract is benign (generated by and deployed using zkay): -> you will only be able to connect if the local files are equivalent -> correctness is guaranteed b) the remote zkay contract was tampered with (any of the .sol files was modified was modified before deployment) -> connection will fail, because local zkay compilation will not produce matching evm bytecode c) the prover/verification keys were tampered with (they were generated for a different circuit than the one produced by zkay) * local keys are genuine -> connection will be refused because the keys don't match what is baked into the remote verification contract * same tampered keys locally -> NO GUARANTEES, since the trust assumption is violated :param project_dir: directory where the zkay file, manifest and snark keys reside :param contract: name of the contract to connect to :param contract_address: address of the deployed contract :param user_address: account which connects to the contract :raise IntegrityError: if the integrity check fails (mismatch between local code and remote contract) :return: contract handle for the specified contract """ if not self.is_debug_backend() and cfg.crypto_backend == 'dummy': raise BlockChainError('SECURITY ERROR: Dummy encryption can only be used with debug blockchain backends (w3-eth-tester or w3-ganache).') zk_print_banner(f'Connect to {contract}@{contract_address}') # If not already done, compile zkay file to generate main and verification contracts (but don't generate new prover/verification keys and manifest) zk_file = os.path.join(project_dir, 'contract.zkay') if not os.path.exists(zk_file): raise IntegrityError('No zkay contract found in specified directory') verifier_names = [] if not os.path.exists(os.path.join(project_dir, 'contract.sol')): compile_zkay_file(zk_file, project_dir, import_keys=True, verifier_names=verifier_names) else: with open(zk_file) as f: verifier_names = get_verification_contract_names(f.read()) zk_print(f'Connecting to contract {contract}@{contract_address}') contract_on_chain = self._connect(project_dir, contract, contract_address.val) pki_verifier_addresses = {} # Check integrity of all pki contracts self._pki_contract = {} for crypto_params in cfg.all_crypto_params(): contract_name = cfg.get_pki_contract_name(crypto_params) pki_address = self._req_state_var(contract_on_chain, f'{contract_name}_inst') pki_verifier_addresses[contract_name] = AddressValue(pki_address) with cfg.library_compilation_environment(): contract = self._verify_contract_integrity(pki_address, os.path.join(project_dir, f'{contract_name}.sol')) self._pki_contract[crypto_params.crypto_name] = contract # Check verifier contract and library integrity if verifier_names: some_vname = verifier_names[0] libraries = [(lib_name, os.path.join(project_dir, ProvingScheme.verify_libs_contract_filename)) for lib_name in cfg.external_crypto_lib_names] some_vcontract = self._req_state_var(contract_on_chain, f'{some_vname}_inst') libs = self._verify_library_integrity(libraries, some_vcontract, os.path.join(project_dir, f'{some_vname}.sol')) self._lib_addresses = libs for verifier in verifier_names: v_address = self._req_state_var(contract_on_chain, f'{verifier}_inst') pki_verifier_addresses[verifier] = AddressValue(v_address) vcontract = self._verify_contract_integrity(v_address, os.path.join(project_dir, f'{verifier}.sol'), libraries=libs) # Verify prover key expected_hash = self._req_state_var(vcontract, cfg.prover_key_hash_name) from zkay.transaction.runtime import Runtime actual_hash = Runtime.prover().get_prover_key_hash(os.path.join(project_dir, cfg.get_circuit_output_dir_name(verifier))) if expected_hash != actual_hash: raise IntegrityError(f'Prover key hash in deployed verification contract does not match local prover key file for "{verifier}"') # Check zkay contract integrity self._verify_zkay_contract_integrity(contract_on_chain.address, project_dir, pki_verifier_addresses) with success_print(): zk_print(f'OK: Bytecode on blockchain matches local zkay contract') zk_print(f'Connection from account 0x{user_address} established\n') return contract_on_chain