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 ContractRegistryTransactionHandler(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("Contract namespace prefix: %s", self.connect.get_contract_prefix()) @property def family_name(self): family = self.connect.get_contract_registry_family_name() LOGGER.debug("Contract family name: %s", family) return family @property def family_versions(self): return ['1.0'] @property def namespaces(self): return self.connect.get_contract_prefix() def _get_enclave_state(self, context, enclave_id): address = self.connect.get_enclave_address(enclave_id) return self.connect.get_state(context, address, PdoContractEnclaveInfo) def _set_contract_state(self, context, contract_id, data): address = self.connect.get_contract_address(contract_id) self.connect.set_state(context, address, data) def _delete_contract_state(self, context, contract_id): address = self.connect.get_contract_address(contract_id) self.connect.delete_state(context, address) def _get_contract_state(self, context, contract_id): address = self.connect.get_contract_address(contract_id) return self.connect.get_state(context, address, PdoContractInfo) def check_address(self, context, contract_id, register_new): LOGGER.debug("check_address.address of: %s", contract_id) try: state = self._get_contract_state(context, contract_id) if register_new: if state.contract_id: raise InvalidTransaction( 'Contract already exists with signing_key {}'\ .format(contract_id)) else: return state else: if not state.contract_id: raise InvalidTransaction( 'Contract does not exist: {0}'.format(contract_id)) else: return state except InternalError as error: if not register_new: raise InvalidTransaction( 'Contract does not exist: {0}'.format(contract_id)) else: return PdoContractInfo() @staticmethod def _verify_register(reg_info, txn_signer_public_key): if not reg_info.contract_code_hash: raise InvalidTransaction('Contract code hash is empty') if len(reg_info.provisioning_service_ids) == 0: raise InvalidTransaction('No provisioning service specified') for provisioning_service_id in reg_info.provisioning_service_ids: if not provisioning_service_id: raise InvalidTransaction('Empty provisioning service id') if not verify_contract_register_signature(reg_info, txn_signer_public_key): raise InvalidTransaction('Contract register signature is invalid') def _verify_enclave_info(self, context, contract_info, enclave_info): # enclave_info.contract_enclave_id is not empty if not enclave_info.contract_enclave_id: raise InvalidTransaction('Empty contract_enclave_id') contract_enclave_info = \ self._get_enclave_state(context, enclave_info.contract_enclave_id) # enclave_info.contract_enclave_id is already in the state list for c in contract_info.enclaves_info: if enclave_info.contract_enclave_id == c.contract_enclave_id: raise InvalidTransaction( 'Contract enclave id is already in the state list') # enclave_info.encrypted_contract_state_encryption_key not empty if not enclave_info.encrypted_contract_state_encryption_key: raise InvalidTransaction( 'Empty encrypted_contract_state_encryption_key') # enclave_info.enclave_signature is not empty if not enclave_info.enclave_signature: raise InvalidTransaction('Empty enclave_signature') # enclave_info.enclaves_map is not empty if len(enclave_info.enclaves_map) == 0: raise InvalidTransaction('Empty enclaves_map') for m in enclave_info.enclaves_map: # m.provisioning_service_public_key is not empty if not m.provisioning_service_public_key: raise InvalidTransaction( 'Empty provisioning_service_public_key') # m.provisioning_service_public_key exists in contract_info if not m.provisioning_service_public_key\ in contract_info.provisioning_service_ids: raise InvalidTransaction( 'Provisioning service id {} not in contract'.format( m.provisioning_service_public_key)) # m.provisioning_contract_state_secret if not m.provisioning_contract_state_secret: raise InvalidTransaction( 'Empty provisioning_contract_state_secret') if not contract_enclave_info.verifying_key: raise InvalidTransaction('Unregisteted enclave with id {}'.format( enclave_info.contract_enclave_id)) # verify signature if not verify_add_enclave_to_contract_signature( enclave_info, contract_info): raise InvalidTransaction('Enclave signature is invalid') def apply(self, transaction, context): txn_header = transaction.header txn_signer_public_key = txn_header.signer_public_key payload = PdoContractTransaction() payload.ParseFromString(transaction.payload) if payload.verb == 'register': sig_unxelified = binascii.unhexlify(transaction.signature) digest = hashlib.sha256(sig_unxelified).digest() sig_base64 = base64.b64encode(digest) contract_id = sig_base64.decode("utf-8", "ignore") else: contract_id = payload.contract_id self.dbg_dump.dump_contract_transaction(payload) info = self.check_address(context, contract_id, payload.verb == 'register') if payload.verb != 'register': self.dbg_dump.dump_contract_state(info, "Received PdoContractInfo") if payload.verb == 'register': details = PdoContractRegister() details.ParseFromString(payload.transaction_details) self._verify_register(details, txn_signer_public_key) info.contract_id = contract_id info.contract_code_hash = details.contract_code_hash info.pdo_contract_creator_pem_key = details.pdo_contract_creator_pem_key for id in details.provisioning_service_ids: info.provisioning_service_ids.append(id) self.dbg_dump.dump_contract_state(info, "Setting new PdoContractInfo") self._set_contract_state(context, contract_id, info.SerializeToString()) LOGGER.info("Contract %s was added to the registry.", payload.contract_id) elif payload.verb == 'delete': if not self._debug_on: raise InvalidTransaction( 'Delete is not allowed, debug support is OFF') LOGGER.info("Contract %s was deleted", payload.contract_id) self._delete_contract_state(context, contract_id) elif payload.verb == 'add-enclaves': details = PdoContractAddEnclaves() details.ParseFromString(payload.transaction_details) if not verify_add_enclave_to_contract_pdo_signature( details, info, txn_signer_public_key): raise InvalidTransaction( 'Overall PDO signature for add enclaves transaction is invalid' ) for enclave_info in details.enclaves_info: self._verify_enclave_info(context, info, enclave_info) for ei in details.enclaves_info: enclave_info = info.enclaves_info.add() enclave_info.CopyFrom(ei) self.dbg_dump.dump_contract_state( info, "PdoContractInfo After adding enclave(s)") self._set_contract_state(context, contract_id, info.SerializeToString()) LOGGER.info("Enclaves were added to contract %s.", payload.contract_id) elif payload.verb == 'remove-enclaves': details = PdoContractRemoveEnclaves() details.ParseFromString(payload.transaction_details) for id in details.contract_enclave_ids: for ei in info.enclaves_info: if ei.contract_enclave_id == id: info.enclaves_info.remove(ei) break self.dbg_dump.dump_contract_state( info, "PdoContractInfo after removing enclave(s)") self._set_contract_state(context, contract_id, info.SerializeToString()) LOGGER.info("Enclaves were removed from contract %s.", payload.contract_id) else: raise InvalidTransaction('Invalid transaction action {}'.format( payload.verb))