#!/usr/bin/env python # -*- coding: utf-8 -*- """ Blockstack ~~~~~ copyright: (c) 2014-2015 by Halfmoon Labs, Inc. copyright: (c) 2016 by Blockstack.org This file is part of Blockstack Blockstack is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Blockstack is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Blockstack. If not, see . """ import preorder import register import transfer import update import revoke import nameimport import namespacepreorder import namespacereveal import namespaceready import announce import binascii import copy from ..nameset import CONSENSUS_FIELDS_REQUIRED, NAMEREC_MUTATE_FIELDS, NAMEREC_BACKUP_FIELDS from ..config import * from .register import get_registration_recipient_from_outputs from .transfer import get_transfer_recipient_from_outputs from .nameimport import get_import_update_hash_from_outputs from .preorder import tx_extract as extract_preorder, \ restore_delta as restore_preorder, \ check as check_preorder, snv_consensus_extras as preorder_consensus_extras from .register import tx_extract as extract_registration, \ restore_delta as restore_register, \ snv_consensus_extras as register_consensus_extras, check_register as check_registration, check_renewal from .transfer import tx_extract as extract_transfer, \ restore_delta as restore_transfer, \ snv_consensus_extras as transfer_consensus_extras, check as check_transfer from .update import tx_extract as extract_update, \ restore_delta as restore_update, \ snv_consensus_extras as update_consensus_extras, check as check_update from .revoke import tx_extract as extract_revoke, \ restore_delta as restore_revoke, \ check as check_revoke, snv_consensus_extras as revoke_consensus_extras from .namespacepreorder import tx_extract as extract_namespace_preorder, \ restore_delta as restore_namespace_preorder, \ check as check_namespace_preorder, snv_consensus_extras as namespace_preorder_consensus_extras from .nameimport import tx_extract as extract_name_import, \ restore_delta as restore_name_import, \ snv_consensus_extras as name_import_consensus_extras, check as check_name_import from .namespacereveal import tx_extract as extract_namespace_reveal, \ restore_delta as restore_namespace_reveal, \ check as check_namespace_reveal, snv_consensus_extras as namespace_reveal_consensus_extras from .namespaceready import tx_extract as extract_namespace_ready, \ restore_delta as restore_namespace_ready, \ check as check_namespace_ready, snv_consensus_extras as namespace_ready_consensus_extras from .announce import tx_extract as extract_announce, \ restore_delta as restore_announce, \ check as check_announce, snv_consensus_extras as announce_consensus_extras SERIALIZE_FIELDS = { "NAME_PREORDER": preorder.FIELDS, "NAME_REGISTRATION": register.FIELDS, "NAME_RENEWAL": register.FIELDS, "NAME_UPDATE": update.FIELDS, "NAME_TRANSFER": transfer.FIELDS, "NAME_REVOKE": revoke.FIELDS, "NAME_IMPORT": nameimport.FIELDS, "NAMESPACE_PREORDER": namespacepreorder.FIELDS, "NAMESPACE_REVEAL": namespacereveal.FIELDS, "NAMESPACE_READY": namespaceready.FIELDS, "ANNOUNCE": announce.FIELDS } MUTATE_FIELDS = { "NAME_PREORDER": preorder.MUTATE_FIELDS, "NAME_REGISTRATION": register.REGISTER_MUTATE_FIELDS, "NAME_RENEWAL": register.RENEWAL_MUTATE_FIELDS, "NAME_UPDATE": update.MUTATE_FIELDS, "NAME_TRANSFER": transfer.MUTATE_FIELDS, "NAME_REVOKE": revoke.MUTATE_FIELDS, "NAME_IMPORT": nameimport.MUTATE_FIELDS, "NAMESPACE_PREORDER": namespacepreorder.MUTATE_FIELDS, "NAMESPACE_REVEAL": namespacereveal.MUTATE_FIELDS, "NAMESPACE_READY": namespaceready.MUTATE_FIELDS, "ANNOUNCE": announce.MUTATE_FIELDS } BACKUP_FIELDS = { "NAME_PREORDER": preorder.BACKUP_FIELDS, "NAME_REGISTRATION": register.REGISTER_BACKUP_FIELDS, "NAME_RENEWAL": register.RENEWAL_BACKUP_FIELDS, "NAME_UPDATE": update.BACKUP_FIELDS, "NAME_TRANSFER": transfer.BACKUP_FIELDS, "NAME_REVOKE": revoke.BACKUP_FIELDS, "NAME_IMPORT": nameimport.BACKUP_FIELDS, "NAMESPACE_PREORDER": namespacepreorder.BACKUP_FIELDS, "NAMESPACE_REVEAL": namespacereveal.BACKUP_FIELDS, "NAMESPACE_READY": namespaceready.BACKUP_FIELDS, "ANNOUNCE": announce.BACKUP_FIELDS } # NOTE: these all have the same signatures EXTRACT_METHODS = { "NAME_PREORDER": extract_preorder, "NAME_REGISTRATION": extract_registration, "NAME_RENEWAL": extract_registration, "NAME_UPDATE": extract_update, "NAME_TRANSFER": extract_transfer, "NAME_REVOKE": extract_revoke, "NAME_IMPORT": extract_name_import, "NAMESPACE_PREORDER": extract_namespace_preorder, "NAMESPACE_REVEAL": extract_namespace_reveal, "NAMESPACE_READY": extract_namespace_ready, "ANNOUNCE": extract_announce } # NOTE: these all have the same signature CHECK_METHODS = { "NAME_PREORDER": check_preorder, "NAME_REGISTRATION": check_registration, "NAME_RENEWAL": check_renewal, "NAME_UPDATE": check_update, "NAME_TRANSFER": check_transfer, "NAME_REVOKE": check_revoke, "NAME_IMPORT": check_name_import, "NAMESPACE_PREORDER": check_namespace_preorder, "NAMESPACE_REVEAL": check_namespace_reveal, "NAMESPACE_READY": check_namespace_ready, "ANNOUNCE": check_announce } # NOTE: these all have the same signatures RESTORE_METHODS = { "NAME_PREORDER": restore_preorder, "NAME_REGISTRATION": restore_register, "NAME_RENEWAL": restore_register, "NAME_UPDATE": restore_update, "NAME_TRANSFER": restore_transfer, "NAME_REVOKE": restore_revoke, "NAME_IMPORT": restore_name_import, "NAMESPACE_PREORDER": restore_namespace_preorder, "NAMESPACE_REVEAL": restore_namespace_reveal, "NAMESPACE_READY": restore_namespace_ready, "ANNOUNCE": restore_announce } # NOTE: these all have the same signatures SNV_CONSENSUS_EXTRA_METHODS = { "NAME_PREORDER": preorder_consensus_extras, "NAME_REGISTRATION": register_consensus_extras, "NAME_RENEWAL": register_consensus_extras, "NAME_UPDATE": update_consensus_extras, "NAME_TRANSFER": transfer_consensus_extras, "NAME_REVOKE": revoke_consensus_extras, "NAME_IMPORT": name_import_consensus_extras, "NAMESPACE_PREORDER": namespace_preorder_consensus_extras, "NAMESPACE_REVEAL": namespace_reveal_consensus_extras, "NAMESPACE_READY": namespace_ready_consensus_extras, "ANNOUNCE": announce_consensus_extras } # build-in sanity checks.... # required consensus fields are required! for opcode, serialize_set in SERIALIZE_FIELDS.items(): if len(serialize_set) == 0: continue for required_consensus_field in CONSENSUS_FIELDS_REQUIRED: if required_consensus_field not in serialize_set: # do not even allow this package to be imported raise Exception("BUG: missing required consensus field '%s' in '%s' definition" % (required_consensus_field, opcode)) # required mutate fields must be present for opcode, mutate_set in MUTATE_FIELDS.items(): if len(mutate_set) == 0: continue for required_mutate_field in NAMEREC_MUTATE_FIELDS: if required_mutate_field not in mutate_set: # do not even allow this package to be imported raise Exception("BUG: missing required mutate field '%s' of '%s' definition" % (required_mutate_field, opcode)) # required backup fields must be present for opcode, backup_set in BACKUP_FIELDS.items(): if len(backup_set) == 0: continue if '__all__' in backup_set: # everything will be backed up continue for required_backup_field in NAMEREC_BACKUP_FIELDS: if required_backup_field not in backup_set: # do not even allow this package to be imported raise Exception("BUG: missing required backup field '%s' of '%s' definition" % (required_backup_field, opcode)) # mutate fields must be a subset of backup fields for opcode, mutate_set in MUTATE_FIELDS.items(): for mutate_field in mutate_set: if '__all__' in BACKUP_FIELDS[opcode]: # everything will be backed up continue if mutate_field not in BACKUP_FIELDS[opcode]: # do not even allow this package to be imported raise Exception("BUG: mutate field '%s' is not present in the backup fields for '%s'" % (mutate_field, opcode)) del opcode del mutate_set del backup_set del serialize_set del mutate_field del required_backup_field del required_mutate_field del required_consensus_field def op_extract( op_name, data, senders, inputs, outputs, block_id, vtxindex, txid ): """ Extract an operation from transaction data. Return the extracted fields as a dict. """ global EXTRACT_METHODS if op_name not in EXTRACT_METHODS.keys(): raise Exception("No such operation '%s'" % op_name) method = EXTRACT_METHODS[op_name] op_data = method( data, senders, inputs, outputs, block_id, vtxindex, txid ) return op_data def op_check_quirks( state_engine, nameop, block_id, checked_ops ): """ Given the set of arguments for op_check, apply any op-specific quirks that are needed to preserve backwards compatibility """ if nameop['opcode'] == 'NAME_IMPORT': nameop['op_fee'] = float(nameop['op_fee']) def op_snv_consensus_extra_quirks( extras, namerec, block_id, commit, db ): """ Given the set of arguments to snv_consensus_extras, apply any op-specific quirks that are needed to preserve backwards compatibility """ last_creation_opcode = None if namerec.has_key('name'): last_creation_opcode = db.get_name_last_creation_opcode( namerec['name'], block_number=block_id ) if last_creation_opcode is None: if namerec['op'] == NAME_IMPORT: # this is the first-ever import last_creation_opcode = 'NAME_IMPORT' elif namerec['op'] == NAME_PREORDER: # this is the first-ever preorder last_creation_opcode = 'NAME_PREORDER' log.debug("apply SNV QURIKS on %s at %s (created with %s)" % (namerec.get('name', "UNKNOWN"), block_id, last_creation_opcode)) if namerec.has_key('name') and last_creation_opcode == 'NAME_IMPORT': log.debug("apply SNV QUIRK on %s: %s --> %s" % (namerec.get('name', "UNKNOWN"), namerec['op_fee'], float(namerec['op_fee']))) extras['op_fee'] = float(namerec['op_fee']) def op_make_restore_diff_quirks( diff, op_name, cur_rec, prev_block_number, history_index, untrusted_db ): """ Given the set of arguments to restore_diff, apply any op-specific quirks that are needed to preserve backwards compatibility """ last_creation_opcode = None if cur_rec.has_key('name'): last_creation_opcode = untrusted_db.get_name_last_creation_opcode( cur_rec['name'], block_number=prev_block_number, history_index=history_index ) if last_creation_opcode is None: if cur_rec['op'] == NAME_IMPORT: # this is the first-ever import last_creation_opcode = 'NAME_IMPORT' elif cur_rec['op'] == NAME_PREORDER: # this is the first-ever preorder last_creation_opcode = 'NAME_PREORDER' log.debug("apply RESTORE DIFF QUIRKS on %s at %s[%s] (created with %s)" % (cur_rec.get('name', "UNKNOWN"), prev_block_number, history_index, last_creation_opcode)) if cur_rec.has_key('name') and last_creation_opcode == 'NAME_IMPORT': log.debug("apply RESTORE DIFF QUIRK on %s: %s --> %s" % (cur_rec.get('name', "UNKNOWN"), cur_rec['op_fee'], float(cur_rec['op_fee']))) diff['op_fee'] = float(cur_rec['op_fee']) def op_check( state_engine, nameop, block_id, checked_ops ): """ Given the state engine, the current block, the list of pending operations processed so far, and the current operation, determine whether or not it should be accepted. The operation is allowed to change once, as a result of a check """ global CHECK_METHODS, MUTATE_FIELDS count = 0 while count < 3: count += 1 nameop_clone = copy.deepcopy( nameop ) opcode = None if 'opcode' not in nameop_clone.keys(): op = nameop_clone.get('op', None) try: assert op is not None, "BUG: no op defined" opcode = op_get_opcode_name( op ) assert opcode is not None, "BUG: op '%s' undefined" % op except Exception, e: log.exception(e) log.error("FATAL: BUG: no 'op' defined") sys.exit(1) else: opcode = nameop_clone['opcode'] check_method = CHECK_METHODS.get( opcode, None ) try: assert check_method is not None, "BUG: no check-method for '%s'" % opcode except Exception, e: log.exception(e) log.error("FATAL: BUG: no check-method for '%s'" % opcode ) sys.exit(1) rc = check_method( state_engine, nameop_clone, block_id, checked_ops ) if not rc: # rejected break # did the opcode change? # i.e. did the nameop get transformed into a different opcode? new_opcode = nameop_clone.get( 'opcode', None ) if new_opcode is None or new_opcode == opcode: # we're done nameop.clear() nameop.update( nameop_clone ) break else: # try again log.debug("Nameop re-interpreted from '%s' to '%s' (%s)" % (opcode, new_opcode, count)) nameop['opcode'] = new_opcode continue try: assert count < 3, "opcode flipflop loop detected" except Exception, e: log.exception(e) log.error("FATAL: BUG: flipflop loop") sys.exit(1) if rc: op_check_quirks( state_engine, nameop, block_id, checked_ops ) return rc def op_make_restore_diff( op_name, cur_rec, prev_block_number, history_index, working_db, untrusted_db ): """ Given a current name record, an operation name, and a (block number, block history index) coordinate, calculate a diff that, when applied to the given name record, will restore it to the name record as it was when the operation at (block number, block history index) was applied. """ global RESTORE_METHODS, MUTATE_FIELDS if op_name not in RESTORE_METHODS.keys(): raise Exception("No such operation '%s'" % op_name) method = RESTORE_METHODS[op_name] delta = method( cur_rec, prev_block_number, history_index, working_db, untrusted_db ) op_make_restore_diff_quirks( delta, op_name, cur_rec, prev_block_number, history_index, untrusted_db ) return delta def op_get_mutate_fields( op_name ): """ Get the names of the fields that will change when this operation gets applied to a record. """ global MUTATE_FIELDS if op_name not in MUTATE_FIELDS.keys(): raise Exception("No such operation '%s'" % op_name) fields = MUTATE_FIELDS[op_name][:] return fields def op_get_backup_fields( op_name ): """ Get the set of fields to back up to a name's history when applying this operation. These fields should encompass sufficient information to calculate a diff that will restore a future version of a name record to the state it is in now. (NOTE this is different from the mutate fields-- some operations need to back up fields even though they wont be changed, since the consensus hash is derived from them.) """ global BACKUP_FIELDS if op_name not in BACKUP_FIELDS.keys(): raise Exception("No such operation '%s'" % op_name ) fields = BACKUP_FIELDS[op_name][:] return fields def op_get_consensus_fields( op_name ): """ Get the set of consensus-generating fields for an operation. """ global SERIALIZE_FIELDS if op_name not in SERIALIZE_FIELDS.keys(): raise Exception("No such operation '%s'" % op_name ) fields = SERIALIZE_FIELDS[op_name][:] return fields def op_snv_consensus_extra( op_name, prev_name_rec, prev_block_id, db ): """ Derive any missing consensus-generating fields from the fields of a name record (since some of them are dynamically generated when the operation is discovered). This method is used for calculating prior operations from name records for SNV. The given name record is the name record as it was when prev_block_id was processed. The 'vtxindex' field within the name record indicates which the transaction at which it existed. I.e., the given name record is in the state it was in at (prev_block_id, prev_name_rec['vtxindex']). Return the extra conesnsus fields on success. Return None on error. """ global SNV_CONSENSUS_EXTRA_METHODS if op_name not in SNV_CONSENSUS_EXTRA_METHODS.keys(): raise Exception("No such operation '%s'" % op_name) method = SNV_CONSENSUS_EXTRA_METHODS[op_name] extras = method( prev_name_rec, prev_block_id, None, db ) op_snv_consensus_extra_quirks( extras, prev_name_rec, prev_block_id, False, db ) return extras def op_commit_consensus_extra( op_name, committed_name_rec, blockchain_name_data, block_id, db ): """ Like op_snv_consensus_extra, but will be called with the current name record and block number, in order to re-calculate any derived consensus-affecting fields. """ global SNV_CONSENSUS_EXTRA_METHODS, SERIALIZE_FIELDS if op_name not in SNV_CONSENSUS_EXTRA_METHODS.keys(): raise Exception("No such operation '%s'" % op_name) if op_name not in SERIALIZE_FIELDS.keys(): raise Exception("No such operation '%s'" % op_name) method = SNV_CONSENSUS_EXTRA_METHODS[op_name] commit_fields = SERIALIZE_FIELDS[op_name] extras = method( committed_name_rec, block_id, blockchain_name_data, db ) op_snv_consensus_extra_quirks( extras, committed_name_rec, block_id, True, db ) commit_extras = {} for cf in commit_fields + ['__override__']: if cf in extras: commit_extras[cf] = extras[cf] return commit_extras def op_commit_consensus_override( consensus_extras, field ): """ Force a consensus field to change on commit. This is used in the event that an operation encodes one value for this field, but we need to mix a different value for the field into the operation we actually commit. This is used to stay compatible with bugs in previous implementations. """ if not consensus_extras.has_key( '__override__' ): consensus_extras['__override__'] = [field] else: consensus_extras['__override__'].append( field ) def op_commit_consensus_has_override( consensus_extras, field ): """ Is a consensus field overridden? """ if consensus_extras.has_key( '__override__' ): if field in consensus_extras['__override__']: return True return False def op_commit_consensus_sanitize( consensus_extras ): """ Remove any non-commit metadata fields """ for k in ['__override__']: if k in consensus_extras.keys(): del consensus_extras[k] return consensus_extras def op_commit_consensus_get_overrides( consensus_extras ): """ get overridden field names """ return consensus_extras.get("__override__", [])