mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-14 22:20:17 +08:00
447 lines
16 KiB
Python
447 lines
16 KiB
Python
#!/usr/bin/env python2
|
|
# -*- 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 <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
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
|
|
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, check as check_preorder
|
|
from .register import tx_extract as extract_registration, check_register as check_registration, check_renewal
|
|
from .transfer import tx_extract as extract_transfer, check as check_transfer, canonicalize as canonicalize_transfer
|
|
from .update import tx_extract as extract_update, check as check_update, canonicalize as canonicalize_update
|
|
from .revoke import tx_extract as extract_revoke, check as check_revoke
|
|
from .nameimport import tx_extract as extract_name_import, check as check_name_import, canonicalize as canonicalize_name_import
|
|
from .namespacepreorder import tx_extract as extract_namespace_preorder, check as check_namespace_preorder, canonicalize as canonicalize_namespace_preorder, decanonicalize as decanonicalize_namespace_preorder
|
|
from .namespacereveal import tx_extract as extract_namespace_reveal, check as check_namespace_reveal, canonicalize as canonicalize_namespace_reveal, decanonicalize as decanonicalize_namespace_reveal
|
|
from .namespaceready import tx_extract as extract_namespace_ready, check as check_namespace_ready, canonicalize as canonicalize_namespace_ready, decanonicalize as decanonicalize_namespace_ready
|
|
from .announce import tx_extract as extract_announce, check as check_announce
|
|
|
|
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
|
|
}
|
|
|
|
# fields that do not have columns in the db schema, but are part of this operation's consensus ops hash
|
|
UNSTORED_CANONICAL_FIELDS = {
|
|
'NAME_PREORDER': [],
|
|
'NAME_REGISTRATION': [],
|
|
'NAME_RENEWAL': [],
|
|
'NAME_UPDATE': update.UNSTORED_CANONICAL_FIELDS,
|
|
'NAME_TRANSFER': transfer.UNSTORED_CANONICAL_FIELDS,
|
|
'NAME_REVOKE': [],
|
|
'NAME_IMPORT': [],
|
|
'NAMESPACE_PREORDER': [],
|
|
'NAMESPACE_REVEAL': [],
|
|
'NAMESPACE_READY': [],
|
|
'ANNOUNCE': []
|
|
}
|
|
|
|
# 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 signature
|
|
CANONICALIZE_METHODS = {
|
|
"NAME_UPDATE": canonicalize_update,
|
|
"NAME_TRANSFER": canonicalize_transfer,
|
|
"NAME_IMPORT": canonicalize_name_import,
|
|
"NAMESPACE_PREORDER": canonicalize_namespace_preorder,
|
|
"NAMESPACE_REVEAL": canonicalize_namespace_reveal,
|
|
"NAMESPACE_READY": canonicalize_namespace_ready,
|
|
}
|
|
|
|
|
|
# NOTE: these all have the same signature
|
|
DECANONICALIZE_METHODS = {
|
|
"NAMESPACE_PREORDER": decanonicalize_namespace_preorder,
|
|
"NAMESPACE_REVEAL": decanonicalize_namespace_reveal,
|
|
"NAMESPACE_READY": decanonicalize_namespace_ready
|
|
}
|
|
|
|
|
|
# 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))
|
|
|
|
del opcode
|
|
del mutate_set
|
|
del serialize_set
|
|
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_canonicalize(op_name, parsed_op):
|
|
"""
|
|
Get the canonical representation of a parsed operation's data.
|
|
Meant for backwards-compatibility
|
|
"""
|
|
global CANONICALIZE_METHODS
|
|
|
|
if op_name not in CANONICALIZE_METHODS:
|
|
# no canonicalization needed
|
|
return parsed_op
|
|
else:
|
|
return CANONICALIZE_METHODS[op_name](parsed_op)
|
|
|
|
|
|
def op_decanonicalize(op_name, canonical_op):
|
|
"""
|
|
Get the current representation of a parsed operation's data, given the canonical representation
|
|
Meant for backwards-compatibility
|
|
"""
|
|
global DECANONICALIZE_METHODS
|
|
|
|
if op_name not in DECANONICALIZE_METHODS:
|
|
# no decanonicalization needed
|
|
return canonical_op
|
|
else:
|
|
return DECANONICALIZE_METHODS[op_name](canonical_op)
|
|
|
|
|
|
def op_canonicalize_quirks(op_name, new_record, current_record):
|
|
"""
|
|
This method preserves backwards-compatibility quirks on canonicalized op data,
|
|
for snapshotting purposes. It gets called after the nameop is committed and
|
|
its history is saved, but before it gets snapshotted to form the new consensus hash.
|
|
|
|
Backwards-compatibility quirks:
|
|
* if the operation was created by a NAME_IMPORT, then 'op_fee' must be a float (i.e. must end in .0)
|
|
* always preserve the 'last_creation_op' field if it is not set in new_record
|
|
|
|
Returns the new canonicalized op data
|
|
"""
|
|
ret = {}
|
|
ret.update(new_record)
|
|
|
|
if op_name in OPCODE_NAME_NAMEOPS and op_name in OPCODE_NAME_STATE_TRANSITIONS:
|
|
# nameop state transition. Need to ensure the op_fee is in the right format.
|
|
# depends on what created it
|
|
if op_name != 'NAME_IMPORT':
|
|
# this is some other state transition besides an import. A prior record must exist.
|
|
assert current_record
|
|
|
|
if current_record:
|
|
for f in ['op_fee', 'last_creation_op']:
|
|
assert f in current_record, 'BUG: missing {} for existing {}'.format(f, op_name)
|
|
|
|
# extract quirky values
|
|
quirk_values = {}
|
|
for f in ['op_fee', 'last_creation_op']:
|
|
# get the quirky field, favoring the new record's value over the current record
|
|
val = new_record.get(f)
|
|
if val is None and current_record is not None:
|
|
val = current_record.get(f)
|
|
|
|
assert val, 'Neither new record nor current record has a value for "{}"'.format(f)
|
|
quirk_values[f] = val
|
|
|
|
# QUIRK: NAME_IMPORT-created ops need a float(op_fee). Everyone else just takes it as it is.
|
|
assert quirk_values['op_fee'] is not None, 'BUG: no op_fee carried over'
|
|
if quirk_values['last_creation_op'] == NAME_IMPORT:
|
|
quirk_values['op_fee'] = float(quirk_values['op_fee'])
|
|
else:
|
|
quirk_values['op_fee'] = int(quirk_values['op_fee'])
|
|
|
|
ret['op_fee'] = quirk_values['op_fee']
|
|
|
|
# QUIRK: preserve last_creation_op across records
|
|
ret['last_creation_op'] = quirk_values['last_creation_op']
|
|
|
|
return ret
|
|
|
|
|
|
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 be "type-cast" to a new operation, but only once.
|
|
If this happens, the operation will be checked again.
|
|
Subsequent casts are considered bugs, and will cause a program abort.
|
|
|
|
TODO: remove type-cast
|
|
"""
|
|
global CHECK_METHODS, MUTATE_FIELDS
|
|
|
|
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
|
|
return False
|
|
|
|
# accepted! clean up and canonicalize
|
|
nameop.clear()
|
|
nameop.update(nameop_clone)
|
|
|
|
# nameop = op_canonicalize(nameop['opcode'], nameop)
|
|
op_canonicalize(nameop['opcode'], nameop)
|
|
|
|
# make sure we don't send unstored fields to the db that are otherwise canonical
|
|
unstored_canonical_fields = UNSTORED_CANONICAL_FIELDS.get(nameop['opcode'])
|
|
assert unstored_canonical_fields is not None, "BUG: no UNSTORED_CANONICAL_FIELDS entry for {}".format(nameop['opcode'])
|
|
|
|
for f in unstored_canonical_fields:
|
|
if f in nameop:
|
|
del nameop[f]
|
|
|
|
return rc
|
|
|
|
'''
|
|
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
|
|
|
|
# was this type-cast to a new operation?
|
|
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, "BUG: multiple opcode type-casts detected"
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: BUG: multiple opcode type-casts detected")
|
|
sys.exit(1)
|
|
|
|
if rc:
|
|
nameop = op_canonicalize(nameop['opcode'], nameop)
|
|
|
|
# make sure we don't send unstored fields to the db that are otherwise canonical
|
|
unstored_canonical_fields = UNSTORED_CANONICAL_FIELDS.get(nameop['opcode'])
|
|
assert unstored_canonical_fields is not None, "BUG: no UNSTORED_CANONICAL_FIELDS entry for {}".format(nameop['opcode'])
|
|
|
|
for f in unstored_canonical_fields:
|
|
if f in nameop:
|
|
del nameop[f]
|
|
|
|
return rc
|
|
'''
|
|
|
|
|
|
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_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_get_quirk_fields( op_name ):
|
|
"""
|
|
Get the set of fields in a database record that are required for compatibility quirks
|
|
"""
|
|
quirk_field_table = {
|
|
'NAME_REGISTRATION': ['last_creation_op'],
|
|
'NAME_UPDATE': ['last_creation_op'],
|
|
'NAME_TRANSFER': ['last_creation_op'],
|
|
'NAME_RENEWAL': ['last_creation_op'],
|
|
'NAME_REVOKE': ['last_creation_op'],
|
|
'NAME_IMPORT': ['last_creation_op'],
|
|
}
|
|
|
|
return quirk_field_table.get(op_name, [])
|