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))
示例#2
0
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