class PdoClientConnectHelper(PdoRegistryHelper): def __init__(self, url, keyfile=None, key_str=None, auto_generate=False, cli=False): super(PdoClientConnectHelper, self).__init__(url) if not cli: self._dbg_dump = PdoDbgDump(LOGGER) else: self._dbg_dump = PdoDbgDump() self._make_signer(keyfile, key_str, auto_generate) def generate_new_signer_key(self): self._make_signer(auto_generate=True) def get_signer_public_key_as_hex(self): return self._signer.get_public_key_as_hex() def get_signer_private_key_as_hex(self): return self._signer.get_private_key_as_hex() def _make_signer(self, keyfile=None, key_str=None, auto_generate=False): if key_str or auto_generate: self._signer = CreatePdoSawtoothSigner(key_str) elif keyfile: try: with open(keyfile) as fd: key_str = fd.read().strip() fd.close() except OSError as err: raise ClientConnectException( 'Failed to read private key: {}'.format(str(err))) self._signer = CreatePdoSawtoothSigner(key_str) else: raise ClientConnectException('No option to create a signing key') def get_status(self, batch_id, wait): try: result = self.send_request( 'batch_statuses?id={}&wait={}'.format(batch_id, wait), ) return yaml.safe_load(result)['data'][0]['status'] except BaseException as err: raise ClientConnectException(err) def send_transaction(self, payload, family, wait=None, transaction_output_list=None, transaction_input_list=None, verbose=False, exception_type=TimeoutError, transaction_dependency_list=None): if not transaction_output_list: if family == self.get_ccl_family_name(): transaction_output_list = [ self.get_ccl_info_prefix(), self.get_ccl_state_prefix() ] elif family == self.get_contract_registry_family_name(): transaction_output_list = [self.get_contract_prefix()] else: transaction_output_list = [self.get_enclave_prefix()] if not transaction_input_list: transaction_input_list = [ self.get_enclave_prefix(), self.get_contract_prefix(), '000000', self.get_ccl_info_prefix(), self.get_ccl_state_prefix() ] if verbose: self._dbg_dump.dump_str( "output_list: {}".format(transaction_output_list)) self._dbg_dump.dump_str( "intput_list: {}".format(transaction_input_list)) self._dbg_dump.dump_str("family: {}".format(family)) self._dbg_dump.dump_str( "dependency_list: {}".format(transaction_dependency_list)) header = TransactionHeader( signer_public_key=self.get_signer_public_key_as_hex(), family_name=family, family_version="1.0", inputs=transaction_input_list, outputs=transaction_output_list, dependencies=transaction_dependency_list, payload_sha512=self._sha512(payload), batcher_public_key=self.get_signer_public_key_as_hex(), nonce=time.time().hex().encode()).SerializeToString() signature = self._signer.sign(header) transaction = Transaction(header=header, payload=payload, header_signature=signature) batch_list = self._create_batch_list([transaction]) batch_id = batch_list.batches[0].header_signature if wait and wait > 0: wait_time = 0 start_time = time.time() response = self.send_request( "batches", batch_list.SerializeToString(), 'application/octet-stream', ) while wait_time < wait: status = self.get_status( batch_id, wait - int(wait_time), ) wait_time = time.time() - start_time if status != 'PENDING': if verbose: self._dbg_dump.dump_str( "Transaction status: '{}'".format(status)) if status != "COMMITTED" and exception_type: # this is a temporary fix for the fact that Sawtooth may return INVALID status for a short while after submitting batch for commit # FIX: if total wait time < 10 (ad hoc), and we get INVALID, we wait for 0.1s before checking the status again if wait_time < 10 and wait_time < wait: LOGGER.info( "Unexpected status {}. Waiting for 0.1s before rechecking status" .format(status)) time.sleep(0.1) continue else: raise exception_type( "Transaction submission failed with status '{}'" .format(status)) return response, signature if not exception_type: return response, signature else: if verbose: self._dbg_dump.dump_str("Transaction submission timeout") raise exception_type("Transaction submission timeout") response = self.send_request( "batches", batch_list.SerializeToString(), 'application/octet-stream', ) return response, signature def _create_batch_list(self, transactions): transaction_signatures = [t.header_signature for t in transactions] header = BatchHeader( signer_public_key=self.get_signer_public_key_as_hex(), transaction_ids=transaction_signatures).SerializeToString() signature = self._signer.sign(header) batch = Batch(header=header, transactions=transactions, header_signature=signature) return BatchList(batches=[batch]) def execute_json_transaction(self, json_input, address_family, wait, exception_type=None, verbose=False, timeout_exception_type=TimeoutError, transaction_output_list=None, transaction_input_list=None, transaction_dependency_list=None): json_dict = json.loads(json_input) verb = json_dict['verb'] if not verb: if not exception_type: return False raise exception_type("no 'verb' in the json input") if address_family == self.get_enclave_registry_family_name(): txn = PdoContractEnclaveTransaction() txn.verb = verb txn.verifying_key = json_dict.get("verifying_key") if verb == 'register': details = PdoContractEnclaveRegister() proof_data = json_dict.get("proof_data") if proof_data is None or isinstance(proof_data, str): json_format.Parse(json_input, details, ignore_unknown_fields=True) else: if not exception_type: return False raise exception_type("missing or invalid 'proof_data'") elif verb == 'update': details = PdoContractEnclaveUpdate() json_format.Parse(json_input, details, ignore_unknown_fields=True) elif verb == 'delete': details = None else: if not exception_type: return False raise exception_type( "unknown verb in the json input '{}'".format(verb)) if details: txn.transaction_details = txn.transaction_details = details.SerializeToString( ) if verbose: self._dbg_dump.dump_contract_enclave_transaction(txn) self._dbg_dump.dump_enclave_transaction_protobuf_message_to_json( txn) elif address_family == self.get_contract_registry_family_name(): txn = PdoContractTransaction() txn.verb = verb if 'contract_id' in json_dict: txn.contract_id = json_dict.get("contract_id") if verb == 'register': details = PdoContractRegister() elif verb == 'add-enclaves': details = PdoContractAddEnclaves() elif verb == 'remove-enclaves': details = PdoContractRemoveEnclaves() elif verb == 'delete': details = None else: if not exception_type: return False raise exception_type( "unknown verb in the json input '{}'".format(verb)) if details: json_format.Parse(json_input, details, ignore_unknown_fields=True) txn.transaction_details = details.SerializeToString() if verbose: self._dbg_dump.dump_contract_transaction(txn) self._dbg_dump.dump_contract_transaction_protobuf_message_to_json( txn) elif address_family == self.get_ccl_family_name(): if verb not in ['initialize', 'update', 'terminate', 'delete']: if not exception_type: return False raise exception_type( "unknown verb in the json input '{}'".format(verb)) txn = CCL_TransactionPayload() json_format.Parse(json_input, txn, ignore_unknown_fields=True) if verbose: self._dbg_dump.dump_ccl_transaction(txn) self._dbg_dump.dump_ccl_transaction_protobuf_message_to_json( txn) else: if not exception_type: return False raise exception_type( "unknown 'af' (a.k.a. address family) in the json input '{}'". format(address_family)) result, signature = self.send_transaction( txn.SerializeToString(), address_family, wait=wait, exception_type=timeout_exception_type, transaction_output_list=transaction_output_list, transaction_input_list=transaction_input_list, transaction_dependency_list=transaction_dependency_list) if verbose: self._dbg_dump.dump_str("") self._dbg_dump.dump_str(result) self._dbg_dump.dump_str("") self._dbg_dump.dump_str( "Transaction signature: {}".format(signature)) return signature
class ContractCclTransactionHandler(TransactionHandler): def __init__(self, debug_on, dbg_dump_to_logger=True): self.connect = PdoTpConnectHelper() self._debug_on = debug_on if dbg_dump_to_logger: self.dbg_dump = PdoDbgDump(LOGGER) else: self.dbg_dump = PdoDbgDump() LOGGER.debug("CCL state namespace prefix: %s", self.connect.get_ccl_state_prefix()) LOGGER.debug("CCL information namespace prefix: %s", self.connect.get_ccl_info_prefix()) LOGGER.debug("Contract namespace prefix: %s", self.connect.get_contract_prefix()) LOGGER.debug("Enclave namespace prefix: %s", self.connect.get_enclave_prefix()) @property def family_name(self): family = self.connect.get_ccl_family_name() LOGGER.debug("CCL family name: %s", family) return family @property def family_versions(self): return ['1.0'] @property def namespaces(self): return [ self.connect.get_ccl_state_prefix(), self.connect.get_ccl_info_prefix() ] def _set_ccl_state(self, context, ccl_state): self.connect.set_state( context, self.connect.get_ccl_state_address( ccl_state.state_update.contract_id, ccl_state.state_update.current_state_hash), ccl_state.SerializeToString()) def _get_ccl_state(self, context, contract_id, state_hash): address = self.connect.get_ccl_state_address(contract_id, state_hash) return self.connect.get_state(context, address, CCL_ContractState) def _delete_ccl_state(self, context, contract_id, state_hash): address = self.connect.get_ccl_state_address(contract_id, state_hash) self.connect.delete_state(context, address) def _set_ccl_info(self, context, ccl_info): address = self.connect.get_ccl_info_address(ccl_info.contract_id) self.connect.set_state(context, address, ccl_info.SerializeToString()) def _get_ccl_info(self, context, contract_id): address = self.connect.get_ccl_info_address(contract_id) result = self.connect.get_state(context, address, CCL_ContractInformation) return result def _delete_ccl_info(self, context, contract_id): address = self.connect.get_ccl_info_address(contract_id) self.connect.delete_state(context, address) def _get_contract_info(self, context, contract_id): address = self.connect.get_contract_address(contract_id) return self.connect.get_state(context, address, PdoContractInfo) def _get_enclave_info(self, context, enclave_id): address = self.connect.get_enclave_address(enclave_id) return self.connect.get_state(context, address, PdoContractEnclaveInfo) def _verify_common(self, context, payload, signer, initialize=False): # check that signer matches channel_id if payload.channel_id != signer: raise InvalidTransaction( "Payload channel id '{0}' doesn't match signer '{1}'".format( payload.channel_id, signer)) # check that this contract exists contract = self._get_contract_info(context, payload.state_update.contract_id) if payload.state_update.contract_id != contract.contract_id: raise InvalidTransaction('No contract in registry {}'.format( payload.state_update.contract_id)) # check that this enclave exists enclave = self._get_enclave_info(context, payload.contract_enclave_id) if payload.contract_enclave_id != enclave.verifying_key: raise InvalidTransaction('Enclave does not exist for {}'.format( payload.contract_enclave_id)) # check that this enclave is added to the contract enclave_found = False for e in contract.enclaves_info: if e.contract_enclave_id == payload.contract_enclave_id: enclave_found = True break if not enclave_found: raise InvalidTransaction( 'Enclave {0} has not been added to contract {1}'.format( payload.contract_enclave_id, payload.state_update.contract_id)) # check dependencies for d in payload.state_update.dependency_list: state = self._get_ccl_state(context, d.contract_id, d.state_hash) if not state.state_update.contract_id: raise InvalidTransaction( "Dependency doesn't exist for '{0}' '{1}'".format( d.contract_id, d.state_hash)) # check enclave signature if not verify_ccl_transaction_signature(payload, contract): raise InvalidTransaction( 'Contract CCL enclave signature is invalid') # verify PDO signature if initialize: if not verify_ccl_transaction_pdo_signature(payload, contract): raise InvalidTransaction( 'Contract CCL Initialize PDO signature is invalid') def _check_current_ccl_state_and_info(self, context, payload): info = self._get_ccl_info(context, payload.state_update.contract_id) self.dbg_dump.dump_ccl_info(info) if payload.verb == 'initialize': if info.contract_id: raise InvalidTransaction( 'CCL Contract already exists for {}'.format( payload.state_update.contract_id)) else: if not info.contract_id: raise InvalidTransaction( 'CCL Contract does not exist: {0}'.format( payload.state_update.contract_id)) else: if not info.is_active: raise InvalidTransaction( 'CCL Contract has been terminated: {0}'.format( payload.state_update.contract_id)) state = self._get_ccl_state(context, info.current_state.contract_id, info.current_state.state_hash) self.dbg_dump.dump_ccl_state(state) if state.state_update.contract_id !=\ info.current_state.contract_id\ or\ state.state_update.current_state_hash != \ info.current_state.state_hash: raise InvalidTransaction( "CCL Contract state doesn't exist or invalid") return state # return new state in case of "initialize" action return CCL_ContractState() def _verify_initialize(self, context, payload, signer): if payload.state_update.previous_state_hash: raise InvalidTransaction( 'Previous state hash must be empty on initialize') if len(payload.state_update.dependency_list) != 0: raise InvalidTransaction( 'Dependency list must be empty on initialize') self._check_current_ccl_state_and_info(context, payload) self._verify_common(context, payload, signer, True) def _verify_update(self, context, payload, signer): state = self._check_current_ccl_state_and_info(context, payload) if payload.state_update.previous_state_hash !=\ state.state_update.current_state_hash: raise InvalidTransaction( 'Previous state hash in transcation {0}'\ 'mismatches current {1}'.format( payload.state_update.previous_state_hash, state.state_update.current_state_hash)) self._verify_common(context, payload, signer) return state def _verify_terminate(self, context, payload, signer): state = self._check_current_ccl_state_and_info(context, payload) if payload.state_update.previous_state_hash !=\ state.state_update.current_state_hash: raise InvalidTransaction( 'Previous state hash in transcation {0}'\ 'mismatches current {1}'.format( payload.state_update.previous_state_hash, state.state_update.current_state_hash)) if payload.state_update.current_state_hash: raise InvalidTransaction( 'Current state hash must be empty on terminate') self._verify_common(context, payload, signer) return state def _complete_action(self, context, transaction, payload, contract_id): ccl_state = CCL_ContractState() ccl_info = CCL_ContractInformation() ccl_state.transaction_id = transaction.signature ccl_state.state_update.CopyFrom(payload.state_update) ccl_info.contract_id = \ payload.state_update.contract_id ccl_info.is_active = \ True if payload.verb != 'terminate' else False ccl_info.current_state.contract_id = \ ccl_state.state_update.contract_id ccl_info.current_state.state_hash = \ ccl_state.state_update.current_state_hash self.dbg_dump.dump_ccl_info(ccl_info) self.dbg_dump.dump_ccl_state(ccl_state) if payload.verb != 'terminate': self._set_ccl_state(context, ccl_state) else: ccl_info.current_state.state_hash = \ payload.state_update.previous_state_hash self._set_ccl_info(context, ccl_info) def apply(self, transaction, context): txn_header = transaction.header txn_signer_public_key = txn_header.signer_public_key payload = CCL_TransactionPayload() payload.ParseFromString(transaction.payload) self.dbg_dump.dump_ccl_transaction(payload) if payload.verb == 'initialize': self._verify_initialize(context, payload, txn_signer_public_key) self._complete_action(context, transaction, payload, payload.state_update.contract_id) LOGGER.info("Contract CCL initialized for contract %s", payload.state_update.contract_id) elif payload.verb == 'update': contract_id = self._verify_update(context, payload, txn_signer_public_key) self._complete_action(context, transaction, payload, contract_id) LOGGER.info("Contract CCL updated for contract %s", payload.state_update.contract_id) elif payload.verb == 'terminate': contract_id = self._verify_terminate(context, payload, txn_signer_public_key) self._complete_action(context, transaction, payload, contract_id) LOGGER.info("Contract CCL updated for contract %s", payload.state_update.contract_id) elif payload.verb == 'delete': # 'delete' is useful for development/testing # it should be removed from the production # it is for debug only so no verification # 1) delete states listed in state_update.dependencies_list if not self._debug_on: raise InvalidTransaction( 'Delete is not allowed, debug support is OFF') for d in payload.state_update.dependency_list: state = self._get_ccl_state(context, d.contract_id, d.state_hash) if state.state_update.contract_id != d.contract_id: LOGGER.info("CCL state doesn't exist for '%s':'%s'", d.contract_id, d.state_hash) else: self._delete_ccl_state(context, d.contract_id, d.state_hash) LOGGER.info("CCL state deleted for '%s':'%s'", d.contract_id, d.state_hash) # 2) if payload.state_update.contract_id != "" remove info also if payload.state_update.contract_id: info = self._get_ccl_info(context, payload.state_update.contract_id) if info.current_state.contract_id != \ payload.state_update.contract_id: LOGGER.info("CCL info doesn't exist for '%s'", payload.state_update.contract_id) else: self._delete_ccl_info(context, payload.state_update.contract_id) LOGGER.info("CCL info deleted for %s", payload.state_update.contract_id) else: raise InvalidTransaction('Invalid transaction verb {}'.format( payload.verb))