Merge sqlite3 into develop. Mostly refactoring: move operation

parsing, validation, and consensus logic into the individual operation's
.py file.
This commit is contained in:
Jude Nelson
2016-09-02 00:20:24 -04:00
parent 45194de4bf
commit 692319b263
10 changed files with 2808 additions and 22 deletions

View File

@@ -21,6 +21,169 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..config import *
from ..scripts import *
from ..hashing import *
from ..nameset import *
from utilitybelt import is_hex
from binascii import hexlify, unhexlify
# consensus hash fields (none for announcements)
FIELDS = []
# fields that this operation changes (none)
MUTATE_FIELDS = []
# fields that should be backed up when applying this operation (none)
BACKUP_FIELDS = []
def process_announcement( op ):
"""
If the announcement is valid, then immediately record it.
"""
# valid announcement
announce_hash = op['message_hash']
announcer_id = op['announcer_id']
# go get the text...
announcement_text = get_announcement( announce_hash )
if announcement_text is None:
log.critical( "\n\n(INTERNAL ERROR): Failed to fetch announcement with hash %s from '%s'\n\n" % (announce_hash, announcer_id))
else:
log.critical("ANNOUNCEMENT (from %s): %s\n------BEGIN MESSAGE------\n%s\n------END MESSAGE------\n" % (announcer_id, announce_hash, announcement_text))
store_announcement( announce_hash, announcement_text )
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse):
sender: the script_pubkey (as a hex string) of the principal that sent the name preorder transaction
address: the address from the sender script
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
try:
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload )
assert parsed_payload is not None
ret = {
"sender": sender_script,
"address": sender_address,
"block_number": block_id,
"vtxindex": vtxindex,
"txid": txid,
"op": ANNOUNCE
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
return ret
def parse(bin_payload):
"""
Interpret a block's nulldata back into a SHA256. The first three bytes (2 magic + 1 opcode)
will not be present in bin_payload.
"""
message_hash = hexlify(bin_payload)
if not is_hex( message_hash ):
log.error("Not a message hash")
return None
if len(message_hash) != 40:
log.error("Not a 160-bit hash")
return None
return {
'opcode': 'ANNOUNCE',
'message_hash': message_hash
}
def check( state_engine, nameop, block_id, checked_ops ):
"""
Log an announcement from the blockstack developers,
but first verify that it is correct.
Return True if the announcement came from the announce IDs whitelist
Return False otherwise
"""
sender = nameop['sender']
sending_blockchain_id = None
found = False
for blockchain_id in state_engine.get_announce_ids():
blockchain_namerec = state_engine.get_name( blockchain_id )
if blockchain_namerec is None:
# this name doesn't exist yet, or is expired or revoked
continue
if str(sender) == str(blockchain_namerec['sender']):
# yup!
found = True
sending_blockchain_id = blockchain_id
break
if not found:
log.debug("Announcement not sent from our whitelist of blockchain IDs")
return False
nameop['announcer_id'] = sending_blockchain_id
process_announcement( nameop )
return True
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
return {}
def snv_consensus_extras( new_name_rec, block_id, blockchain_name_data, db ):
"""
Calculate any derived missing data that goes into the check() operation,
given the block number, the name record at the block number, and the db.
"""
return {}

View File

@@ -26,15 +26,69 @@ from binascii import hexlify, unhexlify
from ..config import *
from ..scripts import *
from ..hashing import *
from ..nameset import *
from ..nameset import NAMEREC_FIELDS
from blockstack_client.operations import *
# consensus hash fields (ORDER MATTERS!)
FIELDS = NAMEREC_FIELDS + [
'recipient', # scriptPubKey hex that identifies the name recipient
'recipient_address' # address of the recipient
FIELDS = NAMEREC_FIELDS[:] + [
'sender', # scriptPubKey hex that identifies the name recipient
'address' # address of the recipient
]
# fields that change when applying this operation
MUTATE_FIELDS = NAMEREC_MUTATE_FIELDS[:] + [
'value_hash',
'sender',
'sender_pubkey',
'address',
'importer',
'importer_address',
'preorder_hash',
'preorder_block_number',
'first_registered',
'last_renewed',
'revoked',
'block_number',
'namespace_block_number',
'transfer_send_block_id'
]
# fields to preserve when applying this operation
BACKUP_FIELDS = MUTATE_FIELDS[:] + [
'consensus_hash'
]
def get_import_recipient_from_outputs( outputs ):
"""
Given the outputs from a name import operation,
find the recipient's script hex.
By construction, it will be the first non-OP_RETURN
output (i.e. the second output).
"""
ret = None
for output in outputs:
output_script = output['scriptPubKey']
output_asm = output_script.get('asm')
output_hex = output_script.get('hex')
output_addresses = output_script.get('addresses')
if output_asm[0:9] != 'OP_RETURN' and output_hex:
ret = output_hex
break
if ret is None:
raise Exception("No recipients found")
return ret
def get_import_update_hash_from_outputs( outputs, recipient ):
"""
This is meant for NAME_IMPORT operations, which
@@ -68,3 +122,295 @@ def get_import_update_hash_from_outputs( outputs, recipient ):
return ret
def get_prev_imported( state_engine, checked_ops, name ):
"""
See if a name has been imported previously.
Check the DB *and* current ops.
Make sure the returned record has the name history
"""
imported = find_by_opcode( checked_ops, "NAME_IMPORT" )
for opdata in reversed(imported):
if opdata['name'] == name:
hist = state_engine.get_name_history_diffs(name)
ret = copy.deepcopy(opdata)
ret['history'] = hist
return ret
name_rec = state_engine.get_name( name )
return name_rec
@state_create( "name", "name_records", "check_noop_collision", always_set=["consensus_hash"] )
def check( state_engine, nameop, block_id, checked_ops ):
"""
Given a NAME_IMPORT nameop, see if we can import it.
* the name must be well-formed
* the namespace must be revealed, but not ready
* the name cannot have been imported yet
* the sender must be the same as the namespace's sender
Set the __preorder__ and __prior_history__ fields, since this
is a state-creating operation.
Return True if accepted
Return False if not
"""
from ..nameset import BlockstackDB
name = str(nameop['name'])
sender = str(nameop['sender'])
sender_pubkey = None
recipient = str(nameop['recipient'])
recipient_address = str(nameop['recipient_address'])
preorder_hash = hash_name( nameop['name'], sender, recipient_address )
preorder_block_number = block_id
name_block_number = block_id
name_first_registered = block_id
name_last_renewed = block_id
transfer_send_block_id = None
if not nameop.has_key('sender_pubkey'):
log.debug("Name import requires a sender_pubkey (i.e. use of a p2pkh transaction)")
return False
# name must be well-formed
if not is_name_valid( name ):
log.debug("Malformed name '%s'" % name)
return False
name_without_namespace = get_name_from_fq_name( name )
namespace_id = get_namespace_from_name( name )
# namespace must be revealed, but not ready
if not state_engine.is_namespace_revealed( namespace_id ):
log.debug("Namespace '%s' is not revealed" % namespace_id )
return False
namespace = state_engine.get_namespace_reveal( namespace_id )
# sender p2pkh script must use a public key derived from the namespace revealer's public key
sender_pubkey_hex = str(nameop['sender_pubkey'])
sender_pubkey = virtualchain.BitcoinPublicKey( str(sender_pubkey_hex) )
sender_address = sender_pubkey.address()
import_addresses = BlockstackDB.load_import_keychain( namespace['namespace_id'] )
if import_addresses is None:
# the first name imported must be the revealer's address
if sender_address != namespace['recipient_address']:
log.debug("First NAME_IMPORT must come from the namespace revealer's address")
return False
# need to generate a keyring from the revealer's public key
log.debug("Generating %s-key keychain for '%s'" % (NAME_IMPORT_KEYRING_SIZE, namespace_id))
import_addresses = BlockstackDB.build_import_keychain( namespace['namespace_id'], sender_pubkey_hex )
# sender must be the same as the the person who revealed the namespace
# (i.e. sender's address must be from one of the valid import addresses)
if sender_address not in import_addresses:
log.debug("Sender address '%s' is not in the import keychain" % (sender_address))
return False
# we can overwrite, but emit a warning
# search *current* block as well as last block
prev_name_rec = get_prev_imported( state_engine, checked_ops, name )
if prev_name_rec is not None and (prev_name_rec['block_number'] < block_id or (prev_name_rec['block_number'] == block_id and prev_name_rec['vtxindex'] < nameop['vtxindex'])):
log.warning("Overwriting already-imported name '%s'" % name)
# propagate preorder block number and hash...
preorder_block_number = prev_name_rec['preorder_block_number']
name_block_number = prev_name_rec['block_number']
name_first_registered = prev_name_rec['first_registered']
name_last_renewed = prev_name_rec['last_renewed']
preorder_hash = prev_name_rec['preorder_hash']
transfer_send_block_id = prev_name_rec.get('transfer_send_block_id',None)
# if this name had existed prior to being imported here,
# (i.e. the namespace was revealed and then expired), then
# preserve its prior history (since this is a state-creating operation)
prior_hist = None
if prev_name_rec is not None:
# set preorder and prior history...
prior_hist = prior_history_create( nameop, prev_name_rec, block_id, state_engine, extra_backup_fields=['consensus_hash','namespace_block_number','transfer_send_block_id'] )
# can never have been preordered
state_create_put_preorder( nameop, None )
# might have existed as a previous import in the past
state_create_put_prior_history( nameop, prior_hist )
# carry out the transition
del nameop['recipient']
del nameop['recipient_address']
nameop['sender'] = recipient
nameop['address'] = recipient_address
nameop['importer'] = sender
nameop['importer_address'] = sender_address
nameop['op_fee'] = price_name( name_without_namespace, namespace )
nameop['namespace_block_number'] = namespace['block_number']
nameop['consensus_hash'] = None
nameop['preorder_hash'] = preorder_hash
nameop['block_number'] = name_block_number
nameop['first_registered'] = name_first_registered
nameop['last_renewed'] = name_last_renewed
nameop['preorder_block_number'] = preorder_block_number
nameop['opcode'] = "NAME_IMPORT"
nameop['transfer_send_block_id'] = transfer_send_block_id
# good!
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse)
sender: the script_pubkey (as a hex string) of the principal that sent the name import transaction
address: the address from the sender script
recipient: the script_pubkey (as a hex string) of the principal that is meant to receive the name
recipient_address: the address from the recipient script
import_update_hash: the hash of the data belonging to the recipient
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender = None
sender_address = None
sender_pubkey_hex = None
recipient = None
recipient_address = None
import_update_hash = None
try:
recipient = get_import_recipient_from_outputs( outputs )
recipient_address = virtualchain.script_hex_to_address( recipient )
assert recipient is not None
assert recipient_address is not None
import_update_hash = get_import_update_hash_from_outputs( outputs, recipient )
assert import_update_hash is not None
assert is_hex( import_update_hash )
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload, recipient, import_update_hash )
assert parsed_payload is not None
ret = {
"sender": sender,
"address": sender_address,
"recipient": recipient,
"recipient_address": recipient_address,
"value_hash": import_update_hash,
"revoked": False,
"vtxindex": vtxindex,
"txid": txid,
"first_registered": block_id, # NOTE: will get deleted if this is a re-import
"last_renewed": block_id, # NOTE: will get deleted if this is a re-import
"op": NAME_IMPORT,
"opcode": "NAME_IMPORT"
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
return ret
def parse(bin_payload, recipient, update_hash ):
"""
# NOTE: first three bytes were stripped
"""
fqn = bin_payload
if not is_name_valid( fqn ):
log.error("Name '%s' is invalid" % fqn)
return None
return {
'opcode': 'NAME_IMPORT',
'name': fqn,
'recipient': recipient,
'value_hash': update_hash
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
# sanity check...
if 'importer' not in name_rec.keys() or 'importer_address' not in name_rec.keys():
raise Exception("Name was not imported")
recipient = str(name_rec['sender'])
recipient_address = str(name_rec['address'])
sender = str(name_rec['importer'])
address = str(name_rec['importer_address'])
name_rec_script = build_name_import( str(name_rec['name']) )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse( name_rec_payload, recipient, str(name_rec['value_hash']) )
# reconstruct recipient and sender...
ret_op['recipient'] = recipient
ret_op['recipient_address'] = recipient_address
ret_op['sender'] = sender
ret_op['address'] = address
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Given a name record most recently affected by an instance of this operation,
find the dict of consensus-affecting fields from the operation that are not
already present in the name record.
"""
ret_op = {}
# reconstruct the recipient information
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
# reconstruct preorder hash
ret_op['preorder_hash'] = hash_name( str(name_rec['name']), name_rec['sender'], ret_op['recipient_address'] )
return ret_op

View File

@@ -21,9 +21,17 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..config import *
from ..scripts import *
from ..hashing import *
from ..nameset import *
from blockstack_client.operations import *
from binascii import hexlify, unhexlify
# consensus hash fields (ORDER MATTERS!)
FIELDS = [
'namespace_id_hash', # hash(namespace_id,sender,reveal_addr)
'preorder_hash', # hash(namespace_id,sender,reveal_addr)
'consensus_hash', # consensus hash at the time issued
'op', # bytecode describing the operation (not necessarily 1 byte)
'op_fee', # fee paid for the namespace to the burn address
@@ -31,7 +39,161 @@ FIELDS = [
'vtxindex', # the index in the block where the tx occurs
'block_number', # block number at which this transaction occurred
'sender', # scriptPubKey hex from the principal that issued this preorder (identifies the preorderer)
'sender_pubkey', # if sender is a p2pkh script, this is the public key.
'sender_pubkey', # if sender is a p2pkh script, this is the public key
'address' # address from the scriptPubKey
]
# save everything
MUTATE_FIELDS = FIELDS[:]
# fields to back up when this operation is applied
BACKUP_FIELDS = [
"__all__"
]
@state_preorder("check_preorder_collision")
def check( state_engine, nameop, block_id, checked_ops ):
"""
Given a NAMESPACE_PREORDER nameop, see if we can preorder it.
It must be unqiue.
Return True if accepted.
Return False if not.
"""
namespace_id_hash = nameop['preorder_hash']
consensus_hash = nameop['consensus_hash']
# namespace must not exist
# NOTE: now checked externally
"""
for pending_namespace_preorder in pending_nameops[ NAMESPACE_PREORDER ]:
if pending_namespace_preorder['namespace_id_hash'] == namespace_id_hash:
log.debug("Namespace hash '%s' is already preordered" % namespace_id_hash)
return False
"""
# cannot be preordered already
if not state_engine.is_new_namespace_preorder( namespace_id_hash ):
log.debug("Namespace preorder '%s' already in use" % namespace_id_hash)
return False
# has to have a reasonable consensus hash
if not state_engine.is_consensus_hash_valid( block_id, consensus_hash ):
valid_consensus_hashes = state_engine.get_valid_consensus_hashes( block_id )
log.debug("Invalid consensus hash '%s': expected any of %s" % (consensus_hash, ",".join( valid_consensus_hashes )) )
return False
# has to have paid a fee
if not 'op_fee' in nameop:
log.debug("Missing namespace preorder fee")
return False
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse):
sender: the script_pubkey (as a hex string) of the principal that sent the name preorder transaction
address: the address from the sender script
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
try:
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload )
assert parsed_payload is not None
ret = {
"sender": sender_script,
"address": sender_address,
"block_number": block_id,
"vtxindex": vtxindex,
"txid": txid,
"op": NAMESPACE_PREORDER
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
return ret
def parse( bin_payload ):
"""
NOTE: the first three bytes will be missing
"""
if len(bin_payload) != LENGTHS['preorder_name_hash'] + LENGTHS['consensus_hash']:
log.error("Invalid namespace preorder payload length %s" % len(bin_payload))
return None
namespace_id_hash = bin_payload[ :LENGTHS['preorder_name_hash'] ]
consensus_hash = bin_payload[ LENGTHS['preorder_name_hash']: LENGTHS['preorder_name_hash'] + LENGTHS['consensus_hash'] ]
namespace_id_hash = hexlify( namespace_id_hash )
consensus_hash = hexlify( consensus_hash )
return {
'opcode': 'NAMESPACE_PREORDER',
'preorder_hash': namespace_id_hash,
'consensus_hash': consensus_hash
}
def restore_delta( rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
name_rec_script = build_namespace_preorder( None, None, None, str(rec['consensus_hash']), namespace_id_hash=str(rec['preorder_hash']) )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse(name_rec_payload)
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Calculate any derived missing data that goes into the check() operation,
given the block number, the name record at the block number, and the db.
"""
return {}

View File

@@ -21,13 +21,181 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..b40 import *
from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *
from binascii import hexlify, unhexlify
from blockstack_client.operations import *
import virtualchain
log = virtualchain.get_logger("blockstack-server")
from namespacereveal import FIELDS as NAMESPACE_REVEAL_FIELDS
# consensus hash fields (ORDER MATTERS!)
FIELDS = NAMESPACE_REVEAL_FIELDS + [
FIELDS = NAMESPACE_REVEAL_FIELDS[:] + [
'ready_block', # block number at which the namespace was readied
]
# fields this operation changes
MUTATE_FIELDS = NAMEREC_MUTATE_FIELDS[:] + [
'ready_block',
'sender'
]
# fields to back up when applying this operation
BACKUP_FIELDS = NAMESPACE_REVEAL_FIELDS + MUTATE_FIELDS
@state_transition("namespace_id", "namespaces")
def check( state_engine, nameop, block_id, checked_ops ):
"""
Verify the validity of a NAMESPACE_READY operation.
It is only valid if it has been imported by the same sender as
the corresponding NAMESPACE_REVEAL, and the namespace is still
in the process of being imported.
"""
namespace_id = nameop['namespace_id']
sender = nameop['sender']
# must have been revealed
if not state_engine.is_namespace_revealed( namespace_id ):
log.debug("Namespace '%s' is not revealed" % namespace_id )
return False
# must have been sent by the same person who revealed it
revealed_namespace = state_engine.get_namespace_reveal( namespace_id )
if revealed_namespace['recipient'] != sender:
log.debug("Namespace '%s' is not owned by '%s' (but by %s)" % (namespace_id, sender, revealed_namespace['recipient']))
return False
# can't be ready yet
if state_engine.is_namespace_ready( namespace_id ):
# namespace already exists
log.debug("Namespace '%s' is already registered" % namespace_id )
return False
# preserve from revealed
nameop['sender_pubkey'] = revealed_namespace['sender_pubkey']
nameop['address'] = revealed_namespace['address']
# can commit imported nameops
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse):
sender: the script_pubkey (as a hex string) of the principal that sent the name preorder transaction
address: the address from the sender script
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
try:
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload )
assert parsed_payload is not None
ret = {
"sender": sender_script,
"address": sender_address,
"ready_block": block_id,
"vtxindex": vtxindex,
"txid": txid,
"op": NAMESPACE_READY
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
return ret
def parse( bin_payload ):
"""
NOTE: the first three bytes will be missing
NOTE: the first byte in bin_payload is a '.'
"""
if len(bin_payload) == 0:
log.error("empty namespace")
return None
if bin_payload[0] != '.':
log.error("Missing namespace delimiter '.'")
return None
namespace_id = bin_payload[ 1: ]
# sanity check
if not is_b40( namespace_id ) or "+" in namespace_id or namespace_id.count(".") > 0:
log.error("Invalid namespace ID '%s'" % namespace_id)
return None
if len(namespace_id) <= 0 or len(namespace_id) > LENGTHS['blockchain_id_namespace_id']:
log.error("Invalid namespace of length %s" % len(namespace_id))
return None
return {
'opcode': 'NAMESPACE_READY',
'namespace_id': namespace_id
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
name_rec_script = build_namespace_ready( str(name_rec['namespace_id']) )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse( name_rec_payload )
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Calculate any derived missing data that goes into the check() operation,
given the block number, the name record at the block number, and the db.
"""
return {}

View File

@@ -21,13 +21,22 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *
from binascii import hexlify, unhexlify
from blockstack_client.operations import *
import virtualchain
log = virtualchain.get_logger("blockstack-log")
log = virtualchain.get_logger("blockstack-server")
# consensus hash fields (ORDER MATTERS!)
FIELDS = [
'namespace_id', # human-readable namespace ID
'namespace_id_hash', # hash(namespace_id,sender,reveal_addr) from the preorder (binds this namespace to its preorder)
'preorder_hash', # hash(namespace_id,sender,reveal_addr) from the preorder (binds this namespace to its preorder)
'version', # namespace rules version
'sender', # the scriptPubKey hex script that identifies the preorderer
@@ -50,3 +59,367 @@ FIELDS = [
'no_vowel_discount', # multiplicative coefficient that drops a name's price if it has no vowels
]
# fields this operation changes
# everything but the block number
MUTATE_FIELDS = filter( lambda f: f not in ["block_number"], FIELDS )
# fields that must be backed up when applying this operation (all of them)
BACKUP_FIELDS = ["__all__"]
def namespacereveal_sanity_check( namespace_id, version, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount ):
"""
Verify the validity of a namespace reveal.
Return True if valid
Raise an Exception if not valid.
"""
# sanity check
if not is_b40( namespace_id ) or "+" in namespace_id or namespace_id.count(".") > 0:
raise Exception("Namespace ID '%s' has non-base-38 characters" % namespace_id)
if len(namespace_id) > LENGTHS['blockchain_id_namespace_id']:
raise Exception("Invalid namespace ID length for '%s' (expected length between 1 and %s)" % (namespace_id, LENGTHS['blockchain_id_namespace_id']))
if lifetime < 0 or lifetime > (2**32 - 1):
lifetime = NAMESPACE_LIFE_INFINITE
if coeff < 0 or coeff > 255:
raise Exception("Invalid cost multiplier %s: must be in range [0, 256)" % coeff)
if base < 0 or base > 255:
raise Exception("Invalid base price %s: must be in range [0, 256)" % base)
if type(bucket_exponents) != list:
raise Exception("Bucket exponents must be a list")
if len(bucket_exponents) != 16:
raise Exception("Exactly 16 buckets required")
for i in xrange(0, len(bucket_exponents)):
if bucket_exponents[i] < 0 or bucket_exponents[i] > 15:
raise Exception("Invalid bucket exponent %s (must be in range [0, 16)" % bucket_exponents[i])
if nonalpha_discount <= 0 or nonalpha_discount > 15:
raise Exception("Invalid non-alpha discount %s: must be in range [0, 16)" % nonalpha_discount)
if no_vowel_discount <= 0 or no_vowel_discount > 15:
raise Exception("Invalid no-vowel discount %s: must be in range [0, 16)" % no_vowel_discount)
return True
@state_create( "namespace_id", "namespaces", "check_namespace_collision" )
def check( state_engine, nameop, block_id, checked_ops ):
"""
Check a NAMESPACE_REVEAL operation to the name database.
It is only valid if it is the first such operation
for this namespace, and if it was sent by the same
sender who sent the NAMESPACE_PREORDER.
Return True if accepted
Return False if not
"""
namespace_id = nameop['namespace_id']
namespace_id_hash = nameop['preorder_hash']
sender = nameop['sender']
namespace_preorder = None
if not nameop.has_key('sender_pubkey'):
log.debug("Namespace reveal requires a sender_pubkey (i.e. a p2pkh transaction)")
return False
if not nameop.has_key('recipient'):
log.debug("No recipient script for namespace '%s'" % namespace_id)
return False
if not nameop.has_key('recipient_address'):
log.debug("No recipient address for namespace '%s'" % namespace_id)
return False
# well-formed?
if not is_b40( namespace_id ) or "+" in namespace_id or namespace_id.count(".") > 0:
log.debug("Malformed namespace ID '%s': non-base-38 characters")
return False
# can't be revealed already
if state_engine.is_namespace_revealed( namespace_id ):
# this namespace was already revealed
log.debug("Namespace '%s' is already revealed" % namespace_id )
return False
# can't be ready already
if state_engine.is_namespace_ready( namespace_id ):
# this namespace already exists (i.e. was already begun)
log.debug("Namespace '%s' is already registered" % namespace_id )
return False
# must currently be preordered
namespace_preorder = state_engine.get_namespace_preorder( namespace_id_hash )
if namespace_preorder is None:
# not preordered
log.debug("Namespace '%s' is not preordered (no preorder %s)" % (namespace_id, namespace_id_hash) )
return False
# must be sent by the same principal who preordered it
if namespace_preorder['sender'] != sender:
# not sent by the preorderer
log.debug("Namespace '%s' is not preordered by '%s'" % (namespace_id, sender))
# must be a version we support
if int(nameop['version']) != BLOCKSTACK_VERSION:
log.debug("Namespace '%s' requires version %s, but this blockstack is version %s" % (namespace_id, nameop['version'], BLOCKSTACK_VERSION))
return False
# check fee...
if not 'op_fee' in namespace_preorder:
log.debug("Namespace '%s' preorder did not pay the fee" % (namespace_id))
return False
namespace_fee = namespace_preorder['op_fee']
# must have paid enough
if namespace_fee < price_namespace( namespace_id ):
# not enough money
log.debug("Namespace '%s' costs %s, but sender paid %s" % (namespace_id, price_namespace(namespace_id), namespace_fee ))
return False
# record preorder
nameop['block_number'] = namespace_preorder['block_number']
nameop['reveal_block'] = block_id
state_create_put_preorder( nameop, namespace_preorder )
state_create_put_prior_history( nameop, None )
# NOTE: not fed into the consensus hash, but necessary for database constraints:
nameop['ready_block'] = 0
nameop['op_fee'] = namespace_preorder['op_fee']
# can begin import
return True
def get_reveal_recipient_from_outputs( outputs ):
"""
There are between three outputs:
* the OP_RETURN
* the pay-to-address with the "reveal_addr", not the sender's address
* the change address (i.e. from the namespace preorderer)
Given the outputs from a namespace_reveal operation,
find the revealer's address's script hex.
By construction, it will be the first non-OP_RETURN
output (i.e. the second output).
"""
ret = None
if len(outputs) != 3:
# invalid
raise Exception("Outputs are not from a namespace reveal")
reveal_output = outputs[1]
output_script = reveal_output['scriptPubKey']
output_asm = output_script.get('asm')
output_hex = output_script.get('hex')
output_addresses = output_script.get('addresses')
if output_asm[0:9] != 'OP_RETURN' and output_hex is not None:
# recipient's script hex
ret = output_hex
else:
raise Exception("No namespace reveal script found")
return ret
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
recipient_script = None
recipient_address = None
try:
recipient_script = get_reveal_recipient_from_outputs( outputs )
recipient_address = virtualchain.script_hex_to_address( recipient_script )
assert recipient_script is not None
assert recipient_address is not None
# by construction, the first input comes from the principal
# who sent the reveal transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("No reveal address")
parsed_payload = parse( payload, sender_script, recipient_address )
assert parsed_payload is not None
ret = {
"sender": sender_script,
"address": sender_address,
"recipient": recipient_script,
"recipient_address": recipient_address,
"reveal_block": block_id,
"vtxindex": vtxindex,
"txid": txid,
"op": NAMESPACE_REVEAL
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
return ret
def parse( bin_payload, sender_script, recipient_address ):
"""
NOTE: the first three bytes will be missing
"""
if len(bin_payload) < MIN_OP_LENGTHS['namespace_reveal']:
raise AssertionError("Payload is too short to be a namespace reveal")
off = 0
life = None
coeff = None
base = None
bucket_hex = None
buckets = []
discount_hex = None
nonalpha_discount = None
no_vowel_discount = None
version = None
namespace_id = None
namespace_id_hash = None
life = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_life']]), 16 )
off += LENGTHS['blockchain_id_namespace_life']
coeff = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_coeff']]), 16 )
off += LENGTHS['blockchain_id_namespace_coeff']
base = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_base']]), 16 )
off += LENGTHS['blockchain_id_namespace_base']
bucket_hex = hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_buckets']])
off += LENGTHS['blockchain_id_namespace_buckets']
discount_hex = hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_discounts']])
off += LENGTHS['blockchain_id_namespace_discounts']
version = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_version']]), 16)
off += LENGTHS['blockchain_id_namespace_version']
namespace_id = bin_payload[off:]
namespace_id_hash = None
try:
namespace_id_hash = hash_name( namespace_id, sender_script, register_addr=recipient_address )
except:
log.error("Invalid namespace ID and/or sender script")
return None
# extract buckets
buckets = [int(x, 16) for x in list(bucket_hex)]
# extract discounts
nonalpha_discount = int( list(discount_hex)[0], 16 )
no_vowel_discount = int( list(discount_hex)[1], 16 )
try:
rc = namespacereveal_sanity_check( namespace_id, version, life, coeff, base, buckets, nonalpha_discount, no_vowel_discount )
if not rc:
raise Exception("Invalid namespace parameters")
except Exception, e:
log.error("Invalid namespace parameters")
return None
return {
'opcode': 'NAMESPACE_REVEAL',
'lifetime': life,
'coeff': coeff,
'base': base,
'buckets': buckets,
'version': version,
'nonalpha_discount': nonalpha_discount,
'no_vowel_discount': no_vowel_discount,
'namespace_id': namespace_id,
'preorder_hash': namespace_id_hash
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
buckets = name_rec['buckets']
if type(buckets) in [str, unicode]:
# serialized bucket list.
# unserialize
reg = "[" + "[ ]*[0-9]+[ ]*," * 15 + "[ ]*[0-9]+[ ]*]"
match = re.match( reg, buckets )
if match is None:
log.error("FATAL: bucket list '%s' is not parsable" % (buckets))
os.abort()
try:
buckets = [int(b) for b in buckets.strip("[]").split(", ")]
except Exception, e:
log.exception(e)
log.error("FATAL: failed to parse '%s' into a 16-elemenet list" % (buckets))
os.abort()
name_rec_script = build_namespace_reveal( str(name_rec['namespace_id']), name_rec['version'], str(name_rec['recipient_address']), \
name_rec['lifetime'], name_rec['coeff'], name_rec['base'], buckets,
name_rec['nonalpha_discount'], name_rec['no_vowel_discount'] )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse( name_rec_payload, str(name_rec['sender']), str(name_rec['recipient_address']) )
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Calculate any derived missing data that goes into the check() operation,
given the block number, the name record at the block number, and the db.
"""
return {}

View File

@@ -21,9 +21,19 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *
from binascii import hexlify, unhexlify
from blockstack_client.operations import *
from register import FIELDS as register_FIELDS
# consensus hash fields (ORDER MATTERS!)
FIELDS = [
'preorder_name_hash', # hash(name,sender,register_addr)
'preorder_hash', # hash(name,sender,register_addr)
'consensus_hash', # consensus hash at time of send
'sender', # scriptPubKey hex that identifies the principal that issued the preorder
'sender_pubkey', # if sender is a pubkeyhash script, then this is the public key
@@ -36,3 +46,170 @@ FIELDS = [
'op_fee', # blockstack fee (sent to burn address)
]
# fields this operation changes
MUTATE_FIELDS = FIELDS[:]
# fields to back up when processing this operation
BACKUP_FIELDS = [
"__all__"
]
@state_preorder("check_preorder_collision")
def check( state_engine, nameop, block_id, checked_ops ):
"""
Verify that a preorder of a name at a particular block number is well-formed
NOTE: these *can't* be incorporated into namespace-imports,
since we have no way of knowning which namespace the
nameop belongs to (it is blinded until registration).
But that's okay--we don't need to preorder names during
a namespace import, because we will only accept names
sent from the importer until the NAMESPACE_REVEAL operation
is sent.
Return True if accepted
Return False if not.
"""
from .register import get_num_names_owned
preorder_name_hash = nameop['preorder_hash']
consensus_hash = nameop['consensus_hash']
sender = nameop['sender']
# must be unique in this block
# NOTE: now checked externally in the @state_preorder decorator
"""
for pending_preorders in checked_nameops[ NAME_PREORDER ]:
if pending_preorders['preorder_name_hash'] == preorder_name_hash:
log.debug("Name hash '%s' is already preordered" % preorder_name_hash)
return False
"""
# must be unique across all pending preorders
if not state_engine.is_new_preorder( preorder_name_hash ):
log.debug("Name hash '%s' is already preordered" % preorder_name_hash )
return False
# must have a valid consensus hash
if not state_engine.is_consensus_hash_valid( block_id, consensus_hash ):
log.debug("Invalid consensus hash '%s'" % consensus_hash )
return False
# sender must be beneath quota
num_names = get_num_names_owned( state_engine, checked_ops, sender )
if num_names >= MAX_NAMES_PER_SENDER:
log.debug("Sender '%s' exceeded name quota of %s" % (sender, MAX_NAMES_PER_SENDER ))
return False
# burn fee must be present
if not 'op_fee' in nameop:
log.debug("Missing preorder fee")
return False
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse):
sender: the script_pubkey (as a hex string) of the principal that sent the name preorder transaction
address: the address from the sender script
sender_pubkey_hex: the public key of the sender
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
try:
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload )
assert parsed_payload is not None
ret = {
"sender": sender_script,
"address": sender_address,
"block_number": block_id,
"vtxindex": vtxindex,
"txid": txid,
"op": NAME_PREORDER
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
else:
ret['sender_pubkey'] = None
return ret
def parse(bin_payload):
"""
Parse a name preorder.
NOTE: bin_payload *excludes* the leading 3 bytes (magic + op) returned by build.
"""
if len(bin_payload) != LENGTHS['preorder_name_hash'] + LENGTHS['consensus_hash']:
return None
name_hash = hexlify( bin_payload[0:LENGTHS['preorder_name_hash']] )
consensus_hash = hexlify( bin_payload[LENGTHS['preorder_name_hash']:] )
return {
'opcode': 'NAME_PREORDER',
'preorder_hash': name_hash,
'consensus_hash': consensus_hash
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
# reconstruct the previous fields of the preorder op...
name_rec_script = build_preorder( None, None, None, str(name_rec['consensus_hash']), \
name_hash=str(name_rec['preorder_hash']) )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_delta = parse( name_rec_payload )
return ret_delta
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Calculate any derived missing data that goes into the check() operation,
given the block number, the name record at the block number, and the db.
"""
return {}

View File

@@ -21,17 +21,57 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *
from binascii import hexlify, unhexlify
from blockstack_client.operations import *
import virtualchain
log = virtualchain.get_logger("blockstack-server")
from ..nameset import NAMEREC_FIELDS
# consensus hash fields (ORDER MATTERS!)
FIELDS = NAMEREC_FIELDS + [
'recipient', # scriptPubKey hex script that identifies the principal to own this name
'recipient_address' # principal's address from the scriptPubKey in the transaction
'sender', # scriptPubKey hex script that identifies the principal to own this name
'address' # principal's address from the scriptPubKey in the transaction
]
# fields this operation changes
REGISTER_MUTATE_FIELDS = NAMEREC_MUTATE_FIELDS + [
'last_renewed',
'first_registered',
'revoked',
'sender',
'address',
'sender_pubkey',
'name',
'value_hash',
'importer',
'importer_address',
'preorder_hash',
'preorder_block_number',
'consensus_hash'
]
# fields renewal changes
RENEWAL_MUTATE_FIELDS = NAMEREC_MUTATE_FIELDS + [
'last_renewed',
'sender_pubkey',
'sender',
'address'
]
# fields to back up when applying this operation
REGISTER_BACKUP_FIELDS = NAMEREC_NAME_BACKUP_FIELDS[:] + REGISTER_MUTATE_FIELDS[:]
RENEWAL_BACKUP_FIELDS = NAMEREC_NAME_BACKUP_FIELDS[:] + RENEWAL_MUTATE_FIELDS[:] + [
'consensus_hash',
]
def get_registration_recipient_from_outputs( outputs ):
"""
There are three or four outputs: the OP_RETURN, the registration
@@ -65,3 +105,506 @@ def get_registration_recipient_from_outputs( outputs ):
return ret
def get_num_names_owned( state_engine, checked_ops, sender ):
"""
Find out how many preorders a given sender (i.e. a script)
actually owns, as of this transaction.
"""
count = 0
registers = find_by_opcode( checked_ops, "NAME_REGISTRATION" )
for reg in registers:
if reg['sender'] == sender:
count += 1
count += len( state_engine.get_names_owned_by_sender( sender ) )
log.debug("Sender '%s' owns %s names" % (sender, count))
return count
@state_create( "name", "name_records", "check_name_collision" )
def check_register( state_engine, nameop, block_id, checked_ops ):
"""
Verify the validity of a registration nameop.
* the name must be well-formed
* the namespace must be ready
* the name does not collide
* either the name was preordered by the same sender, or the name exists and is owned by this sender (the name cannot be registered and owned by someone else)
* the mining fee must be high enough.
* if the name was expired, then merge the preorder information from the expired preorder (since this is a state-creating operation,
we set the __preorder__ and __prior_history__ fields to preserve this).
NAME_REGISTRATION is not allowed during a namespace import, so the namespace must be ready.
Return True if accepted.
Return False if not.
"""
from ..nameset import BlockstackDB
name = nameop['name']
sender = nameop['sender']
# address mixed into the preorder
register_addr = nameop.get('recipient_address', None)
if register_addr is None:
log.debug("No registration address given")
return False
recipient = nameop.get('recipient', None)
if recipient is None:
log.debug("No recipient script given")
return False
name_fee = None
namespace = None
preorder_hash = None
preorder_block_number = None
name_block_number = None
consensus_hash = None
transfer_send_block_id = None
opcode = nameop['opcode']
first_registered = nameop['first_registered']
# name must be well-formed
if not is_b40( name ) or "+" in name or name.count(".") > 1:
log.debug("Malformed name '%s': non-base-38 characters" % name)
return False
# name must not be revoked
if state_engine.is_name_revoked( name ):
log.debug("Name '%s' is revoked" % name)
return False
namespace_id = get_namespace_from_name( name )
# namespace must exist and be ready
if not state_engine.is_namespace_ready( namespace_id ):
log.debug("Namespace '%s' is not ready" % namespace_id)
return False
# get namespace...
namespace = state_engine.get_namespace( namespace_id )
# cannot exceed quota
# recipient_names = state_engine.get_names_owned_by_sender( recipient )
# log.debug("Recipient '%s' owns %s names" % (recipient,len(recipient_names)))
# if len(recipient_names) >= MAX_NAMES_PER_SENDER:
num_names = get_num_names_owned( state_engine, checked_ops, recipient )
if num_names >= MAX_NAMES_PER_SENDER:
log.debug("Recipient '%s' has exceeded quota" % recipient)
return False
# get preorder...
preorder = state_engine.get_name_preorder( name, sender, register_addr )
old_name_rec = state_engine.get_name( name, include_expired=True )
if preorder is not None:
# Case 1(a-b): registering or re-registering
# can't be registered already
if state_engine.is_name_registered( name ):
log.debug("Name '%s' is already registered" % name)
return False
# name can't be registered if it was reordered before its namespace was ready
if not namespace.has_key('ready_block') or preorder['block_number'] < namespace['ready_block']:
log.debug("Name '%s' preordered before namespace '%s' was ready" % (name, namespace_id))
return False
# name must be preordered by the same sender
if preorder['sender'] != sender:
log.debug("Name '%s' was not preordered by %s" % (name, sender))
return False
# fee was included in the preorder
if not 'op_fee' in preorder:
log.debug("Name '%s' preorder did not pay the fee" % (name))
return False
name_fee = preorder['op_fee']
preorder_hash = preorder['preorder_hash']
preorder_block_number = preorder['block_number']
# pass along the preorder
state_create_put_preorder( nameop, preorder )
if old_name_rec is None:
# Case 1(a): registered for the first time ever
name_block_number = preorder['block_number']
state_create_put_prior_history( nameop, None )
else:
# Case 1(b): name expired, and is now re-registered
log.debug("Re-registering name '%s'" % name )
# push back preorder block number to the original preorder
name_block_number = old_name_rec['block_number']
# first_registered = old_name_rec['first_registered']
transfer_send_block_id = old_name_rec['transfer_send_block_id']
# re-registering
prior_hist = prior_history_create( nameop, old_name_rec, preorder_block_number, state_engine, extra_backup_fields=['consensus_hash','preorder_hash','transfer_send_block_id'])
state_create_put_prior_history( nameop, prior_hist )
elif state_engine.is_name_registered( name ):
# Case 2: we're renewing
# name must be owned by the recipient already
if not state_engine.is_name_owner( name, recipient ):
log.debug("Renew: Name '%s' not owned by recipient %s" % (name, recipient))
return False
# name must be owned by the sender
if not state_engine.is_name_owner( name, sender ):
log.debug("Renew: Name '%s' not owned by sender %s" % (name, sender))
return False
# fee borne by the renewal
if not 'op_fee' in nameop:
log.debug("Name '%s' renewal did not pay the fee" % (name))
return False
log.debug("Renewing name '%s'" % name )
prev_name_rec = state_engine.get_name( name )
first_registered = prev_name_rec['first_registered']
preorder_block_number = prev_name_rec['preorder_block_number']
name_block_number = prev_name_rec['block_number']
name_fee = nameop['op_fee']
preorder_hash = prev_name_rec['preorder_hash']
transfer_send_block_id = prev_name_rec['transfer_send_block_id']
opcode = "NAME_RENEWAL" # will cause this operation to be re-checked under check_renewal()
# pass along prior history
prior_hist = prior_history_create( nameop, old_name_rec, block_id, state_engine, extra_backup_fields=['consensus_hash','preorder_hash','transfer_send_block_id'])
state_create_put_prior_history( nameop, prior_hist )
state_create_put_preorder( nameop, None )
else:
# Case 3: has never existed, and not preordered
log.debug("Name '%s' does not exist, or is not preordered by %s" % (name, sender))
return False
# check name fee
name_without_namespace = get_name_from_fq_name( name )
# fee must be high enough
if name_fee < price_name( name_without_namespace, namespace ):
log.debug("Name '%s' costs %s, but paid %s" % (name, price_name( name_without_namespace, namespace ), name_fee ))
return False
nameop['opcode'] = opcode
nameop['op_fee'] = name_fee
nameop['preorder_hash'] = preorder_hash
nameop['importer'] = None
nameop['importer_address'] = None
nameop['consensus_hash'] = consensus_hash
nameop['revoked'] = False
nameop['namespace_block_number'] = namespace['block_number']
nameop['first_registered'] = first_registered
nameop['last_renewed'] = block_id
nameop['preorder_block_number'] = preorder_block_number
nameop['block_number'] = name_block_number
nameop['transfer_send_block_id'] = transfer_send_block_id
# propagate new sender information
nameop['sender'] = nameop['recipient']
nameop['address'] = nameop['recipient_address']
del nameop['recipient']
del nameop['recipient_address']
# regster/renewal
return True
@state_transition( "name", "name_records")
def check_renewal( state_engine, nameop, block_id, checked_ops ):
"""
Verify the validity of a renewal nameop.
* the name must be well-formed
* the namespace must be ready
* the request must be sent by the owner.
* the mining fee must be high enough.
* the name must not be expired
Return True if accepted.
Return False if not.
"""
name = nameop['name']
sender = nameop['sender']
address = nameop['address']
# address mixed into the preorder
recipient_addr = nameop.get('recipient_address', None)
if recipient_addr is None:
log.debug("No registration address given")
return False
recipient = nameop.get('recipient', None)
if recipient is None:
log.debug("No recipient p2pkh given")
return False
# on renewal, the sender and recipient must be the same
if sender != recipient:
log.debug("Sender '%s' is not the recipient '%s'" % (sender, recipient))
return False
if recipient_addr != address:
log.debug("Sender address '%s' is not the recipient address '%s'" % (address, recipient_addr))
return False
name_fee = None
namespace = None
preorder_hash = None
preorder_block_number = None
name_block_number = None
opcode = nameop['opcode']
# first_registered = nameop['first_registered']
# name must be well-formed
if not is_b40( name ) or "+" in name or name.count(".") > 1:
log.debug("Malformed name '%s': non-base-38 characters" % name)
return False
# name must not be revoked
if state_engine.is_name_revoked( name ):
log.debug("Name '%s' is revoked" % name)
return False
namespace_id = get_namespace_from_name( name )
# namespace must exist and be ready
if not state_engine.is_namespace_ready( namespace_id ):
log.debug("Namespace '%s' is not ready" % namespace_id)
return False
# get namespace...
namespace = state_engine.get_namespace( namespace_id )
# cannot exceed quota
# if len( state_engine.owner_names.get( recipient, [] ) ) >= MAX_NAMES_PER_SENDER:
# recipient_names = state_engine.get_names_owned_by_sender( recipient )
# if len(recipient_names) >= MAX_NAMES_PER_SENDER:
num_names = get_num_names_owned( state_engine, checked_ops, recipient )
if num_names >= MAX_NAMES_PER_SENDER:
log.debug("Recipient '%s' has exceeded quota" % recipient)
return False
# name must be registered already
if not state_engine.is_name_registered( name ):
log.debug("Name '%s' is not registered" % name)
return False
# name must be owned by the recipient already
if not state_engine.is_name_owner( name, recipient ):
log.debug("Renew: Name '%s' not owned by recipient %s" % (name, recipient))
return False
# name must be owned by the sender
if not state_engine.is_name_owner( name, sender ):
log.debug("Renew: Name '%s' not owned by sender %s" % (name, sender))
return False
# fee borne by the renewal
if not 'op_fee' in nameop:
log.debug("Name '%s' renewal did not pay the fee" % (name))
return False
prev_name_rec = state_engine.get_name( name )
first_registered = prev_name_rec['first_registered']
preorder_block_number = prev_name_rec['preorder_block_number']
name_block_number = prev_name_rec['block_number']
name_fee = nameop['op_fee']
preorder_hash = prev_name_rec['preorder_hash']
value_hash = prev_name_rec['value_hash']
# check name fee
name_without_namespace = get_name_from_fq_name( name )
# fee must be high enough
if name_fee < price_name( name_without_namespace, namespace ):
log.debug("Name '%s' costs %s, but paid %s" % (name, price_name( name_without_namespace, namespace ), name_fee ))
return False
nameop['op'] = "%s:" % (NAME_REGISTRATION,)
nameop['opcode'] = "NAME_RENEWAL"
nameop['op_fee'] = name_fee
nameop['preorder_hash'] = preorder_hash
nameop['namespace_block_number'] = namespace['block_number']
nameop['first_registered'] = first_registered
nameop['preorder_block_number'] = preorder_block_number
nameop['block_number'] = name_block_number
nameop['value_hash'] = value_hash
# renewal
nameop['last_renewed'] = block_id
# propagate new sender information
del nameop['recipient']
del nameop['recipient_address']
# renewal!
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse):
sender: the script_pubkey (as a hex string) of the principal that sent the name preorder transaction
address: the address from the sender script
recipient: the script_pubkey (as a hex string) of the principal that is meant to receive the name
recipient_address: the address from the recipient script
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
recipient = None
recipient_address = None
try:
recipient = get_registration_recipient_from_outputs( outputs )
recipient_address = virtualchain.script_hex_to_address( recipient )
assert recipient is not None
assert recipient_address is not None
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload )
assert parsed_payload is not None
ret = {
"value_hash": None,
"sender": sender_script,
"address": sender_address,
"recipient": recipient,
"recipient_address": recipient_address,
"revoked": False,
"last_renewed": block_id,
"vtxindex": vtxindex,
"txid": txid,
"first_registered": block_id, # NOTE: will get deleted if this is a renew
"last_renewed": block_id, # NOTE: will get deleted if this is a renew
"op": NAME_REGISTRATION
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
else:
ret['sender_pubkey'] = None
return ret
def parse(bin_payload):
"""
Interpret a block's nulldata back into a name. The first three bytes (2 magic + 1 opcode)
will not be present in bin_payload.
The name will be directly represented by the bytes given.
"""
fqn = bin_payload
if not is_name_valid( fqn ):
return None
return {
'opcode': 'NAME_REGISTRATION',
'name': fqn
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
from ..nameset import BlockstackDB
name_rec_script = build_registration( str(name_rec['name']) )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse( name_rec_payload )
# reconstruct the registration/renewal op's recipient info
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
# restore history to find prevoius sender, address, and public key
name_rec_prev = BlockstackDB.get_previous_name_version( name_rec, block_number, history_index, untrusted_db )
sender = name_rec_prev['sender']
address = name_rec_prev['address']
sender_pubkey = None
if op_get_opcode_name(name_rec['op']) == "NAME_RENEWAL":
log.debug("NAME_RENEWAL: sender_pubkey = '%s'" % name_rec['sender_pubkey'])
sender_pubkey = name_rec['sender_pubkey']
else:
log.debug("NAME_REGISTRATION: sender_pubkey = '%s'" % name_rec_prev['sender_pubkey'])
sender_pubkey = name_rec_prev['sender_pubkey']
ret_op['sender'] = sender
ret_op['address'] = address
ret_op['revoked'] = False
ret_op['sender_pubkey'] = sender_pubkey
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Given a name record most recently affected by an instance of this operation,
find the dict of consensus-affecting fields from the operation that are not
already present in the name record.
"""
ret_op = {}
# reconstruct the recipient information
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
return ret_op

View File

@@ -21,8 +21,193 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..nameset import NAMEREC_FIELDS
from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *
from binascii import hexlify, unhexlify
from blockstack_client.operations import *
# consensus hash fields (ORDER MATTERS!)
FIELDS = NAMEREC_FIELDS
FIELDS = NAMEREC_FIELDS[:]
# fields that this operation changes
MUTATE_FIELDS = NAMEREC_MUTATE_FIELDS[:] + [
'revoked',
'value_hash',
'sender_pubkey'
]
# fields to back up when applying this operation
BACKUP_FIELDS = NAMEREC_NAME_BACKUP_FIELDS[:] + MUTATE_FIELDS[:] + [
'consensus_hash'
]
@state_transition("name", "name_records")
def check( state_engine, nameop, block_id, checked_ops ):
"""
Revoke a name--make it available for registration.
* it must be well-formed
* its namespace must be ready.
* the name must be registered
* it must be sent by the name owner
NAME_REVOKE isn't allowed during an import, so the name's namespace must be ready.
Return True if accepted
Return False if not
"""
name = nameop['name']
sender = nameop['sender']
namespace_id = get_namespace_from_name( name )
# name must be well-formed
if not is_b40( name ) or "+" in name or name.count(".") > 1:
log.debug("Malformed name '%s': non-base-38 characters" % name)
return False
# name must exist
name_rec = state_engine.get_name( name )
if name_rec is None:
log.debug("Name '%s' does not exist" % name)
return False
# namespace must be ready
if not state_engine.is_namespace_ready( namespace_id ):
log.debug("Namespace '%s' is not ready" % namespace_id )
return False
# name must not be revoked
if state_engine.is_name_revoked( name ):
log.debug("Name '%s' is revoked" % name)
return False
# name must not be expired
if state_engine.is_name_expired( name, block_id ):
log.debug("Name '%s' is expired" % name)
return False
# the name must be registered
if not state_engine.is_name_registered( name ):
log.debug("Name '%s' is not registered" % name )
return False
# the sender must own this name
if not state_engine.is_name_owner( name, sender ):
log.debug("Name '%s' is not owned by %s" % (name, sender))
return False
# apply state transition
nameop['revoked'] = True
nameop['value_hash'] = None
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse):
sender: the script_pubkey (as a hex string) of the principal that sent the name preorder transaction
address: the address from the sender script
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
try:
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload )
assert parsed_payload is not None
ret = {
"sender": sender_script,
"address": sender_address,
"txid": txid,
"vtxindex": vtxindex,
"op": NAME_REVOKE
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
else:
ret['sender_pubkey'] = None
return ret
def parse(bin_payload):
"""
Interpret a block's nulldata back into a name. The first three bytes (2 magic + 1 opcode)
will not be present in bin_payload.
The name will be directly represented by the bytes given.
"""
fqn = bin_payload
if not is_name_valid( fqn ):
return None
return {
'opcode': 'NAME_REVOKE',
'name': fqn
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
from ..nameset import BlockstackDB
name_rec_script = build_revoke( str(name_rec['name']) )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse( name_rec_payload )
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Calculate any derived missing data that goes into the check() operation,
given the block number, the name record at the block number, and the db.
"""
ret_op = {}
return ret_op

View File

@@ -21,15 +21,38 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..nameset import NAMEREC_FIELDS
from ..b40 import *
from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *
from binascii import hexlify, unhexlify
from blockstack_client.operations import *
# consensus hash fields (ORDER MATTERS!)
FIELDS = NAMEREC_FIELDS + [
'name_hash', # hash(name)
FIELDS = NAMEREC_FIELDS[:] + [
'name_hash128', # hash(name)
'consensus_hash', # consensus hash when this operation was sent
'keep_data' # whether or not to keep the profile data associated with the name when transferred
]
# fields this operation mutates
# NOTE: due to an earlier quirk in the design of this system,
# we do NOT write the consensus hash (but we should have)
MUTATE_FIELDS = NAMEREC_MUTATE_FIELDS[:] + [
'sender',
'address',
'sender_pubkey',
'value_hash',
]
# fields to back up when applying this operation
BACKUP_FIELDS = NAMEREC_NAME_BACKUP_FIELDS[:] + MUTATE_FIELDS[:] + [
'consensus_hash'
]
def get_transfer_recipient_from_outputs( outputs ):
"""
Given the outputs from a name transfer operation,
@@ -59,3 +82,414 @@ def get_transfer_recipient_from_outputs( outputs ):
return ret
def transfer_sanity_check( name, consensus_hash ):
"""
Verify that data for a transfer is valid.
Return True on success
Raise Exception on error
"""
if name is not None and (not is_b40( name ) or "+" in name or name.count(".") > 1):
raise Exception("Name '%s' has non-base-38 characters" % name)
# without the scheme, name must be 37 bytes
if name is not None and (len(name) > LENGTHS['blockchain_id_name']):
raise Exception("Name '%s' is too long; expected %s bytes" % (name, LENGTHS['blockchain_id_name']))
return True
def find_last_transfer_consensus_hash( name_rec, block_id, vtxindex ):
"""
Given a name record, find the last non-NAME_TRANSFER consensus hash.
Return None if not found.
"""
from ..nameset import BlockstackDB
history_keys = name_rec['history'].keys()
history_keys.sort()
history_keys.reverse()
for hk in history_keys:
history_states = BlockstackDB.restore_from_history( name_rec, hk )
for history_state in reversed(history_states):
if history_state['block_number'] > block_id or (history_state['block_number'] == block_id and history_state['vtxindex'] > vtxindex):
# from the future
continue
if history_state['op'][0] == NAME_TRANSFER:
# skip NAME_TRANSFERS
continue
if history_state['op'][0] == NAME_PREORDER:
# out of history
return None
if name_rec['consensus_hash'] is not None:
return name_rec['consensus_hash']
return None
@state_transition( "name", "name_records", always_set=['transfer_send_block_id', 'consensus_hash'] )
def check( state_engine, nameop, block_id, checked_ops ):
"""
Verify the validity of a name's transferrance to another private key.
The name must exist, not be revoked, and be owned by the sender.
The recipient must not exceed the maximum allowed number of names per keypair,
and the recipient cannot own an equivalent name.
NAME_TRANSFER isn't allowed during an import, so the name's namespace must be ready.
Return True if accepted
Return False if not
"""
name_hash = nameop['name_hash128']
name = state_engine.get_name_from_name_hash128( name_hash )
consensus_hash = nameop['consensus_hash']
sender = nameop['sender']
recipient_address = nameop['recipient_address']
recipient = nameop['recipient']
transfer_send_block_id = None
if name is None:
# invalid
log.debug("No name found for '%s'" % name_hash )
return False
namespace_id = get_namespace_from_name( name )
name_rec = state_engine.get_name( name )
if name_rec is None:
log.debug("Name '%s' does not exist" % name)
return False
# namespace must be ready
if not state_engine.is_namespace_ready( namespace_id ):
# non-existent namespace
log.debug("Namespace '%s' is not ready" % (namespace_id))
return False
# name must not be revoked
if state_engine.is_name_revoked( name ):
log.debug("Name '%s' is revoked" % name)
return False
# name must not be expired
if state_engine.is_name_expired( name, state_engine.lastblock ):
log.debug("Name '%s' is expired" % name)
return False
if not state_engine.is_consensus_hash_valid( block_id, consensus_hash ):
# invalid concensus hash
log.debug("Invalid consensus hash '%s'" % consensus_hash )
return False
if sender == recipient:
# nonsensical transfer
log.debug("Sender is the same as the Recipient (%s)" % sender )
return False
if not state_engine.is_name_registered( name ):
# name is not registered
log.debug("Name '%s' is not registered" % name)
return False
if not state_engine.is_name_owner( name, sender ):
# sender doesn't own the name
log.debug("Name '%s' is not owned by %s (but %s)" % (name, sender, state_engine.get_name_owner(name)))
return False
names_owned = state_engine.get_names_owned_by_sender( recipient )
if name in names_owned:
# recipient already owns it
log.debug("Recipient %s already owns '%s'" % (recipient, name))
return False
if len(names_owned) >= MAX_NAMES_PER_SENDER:
# exceeds quota
log.debug("Recipient %s has exceeded name quota" % recipient)
return False
# QUIRK: we use either the consensus hash from the last non-NAME_TRANSFER
# operation, or if none exists, we use the one from the NAME_TRANSFER itself.
transfer_consensus_hash = find_last_transfer_consensus_hash( name_rec, block_id, nameop['vtxindex'] )
transfer_send_block_id = state_engine.get_block_from_consensus( nameop['consensus_hash'] )
if transfer_send_block_id is None:
# wrong consensus hash
log.debug("Unrecognized consensus hash '%s'" % nameop['consensus_hash'] )
return False
# remember the name, so we don't have to look it up later
nameop['name'] = name
# carry out transition, putting the operation into the state to be committed
nameop['sender'] = recipient
nameop['address'] = recipient_address
nameop['sender_pubkey'] = None
nameop['transfer_send_block_id'] = transfer_send_block_id
nameop['consensus_hash'] = transfer_consensus_hash
if not nameop['keep_data']:
nameop['value_hash'] = None
nameop['op'] = "%s%s" % (NAME_TRANSFER, TRANSFER_REMOVE_DATA)
else:
# preserve
nameop['value_hash'] = name_rec['value_hash']
nameop['op'] = "%s%s" % (NAME_TRANSFER, TRANSFER_KEEP_DATA)
del nameop['recipient']
del nameop['recipient_address']
del nameop['keep_data']
del nameop['name_hash128']
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required:
sender: the script_pubkey (as a hex string) of the principal that sent the transfer transaction
address: the address from the sender script
recipient: the script_pubkey (as a hex string) of the principal that is meant to receive the name
recipient_address: the address from the recipient script
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender = None
sender_address = None
sender_pubkey_hex = None
recipient = None
recipient_address = None
try:
recipient = get_transfer_recipient_from_outputs( outputs )
recipient_address = virtualchain.script_hex_to_address( recipient )
assert recipient is not None
assert recipient_address is not None
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload, recipient )
assert parsed_payload is not None
ret = {
"sender": sender,
"address": sender_address,
"recipient": recipient,
"recipient_address": recipient_address,
"vtxindex": vtxindex,
"txid": txid,
"op": NAME_TRANSFER
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
else:
ret['sender_pubkey'] = None
return ret
def parse(bin_payload, recipient):
"""
# NOTE: first three bytes were stripped
"""
if len(bin_payload) != 1 + LENGTHS['name_hash'] + LENGTHS['consensus_hash']:
log.error("Invalid transfer payload length %s" % len(bin_payload))
return None
disposition_char = bin_payload[0:1]
name_hash128 = bin_payload[1:1+LENGTHS['name_hash']]
consensus_hash = bin_payload[1+LENGTHS['name_hash']:]
if disposition_char not in [TRANSFER_REMOVE_DATA, TRANSFER_KEEP_DATA]:
log.error("Invalid disposition character")
return None
# keep data by default
disposition = True
if disposition_char == TRANSFER_REMOVE_DATA:
disposition = False
try:
rc = transfer_sanity_check( None, consensus_hash )
if not rc:
raise Exception("Invalid transfer data")
except Exception, e:
log.error("Invalid transfer data")
return None
return {
'opcode': 'NAME_TRANSFER',
'name_hash128': hexlify( name_hash128 ),
'consensus_hash': hexlify( consensus_hash ),
'recipient': recipient,
'keep_data': disposition
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
from ..nameset import BlockstackDB
# reconstruct the transfer op...
KEEPDATA_OP = "%s%s" % (NAME_TRANSFER, TRANSFER_KEEP_DATA)
REMOVEDATA_OP = "%s%s" % (NAME_TRANSFER, TRANSFER_REMOVE_DATA)
keep_data = None
try:
if name_rec['op'] == KEEPDATA_OP:
keep_data = True
elif name_rec['op'] == REMOVEDATA_OP:
keep_data = False
else:
raise Exception("Invalid transfer op sequence '%s'" % name_rec['op'])
except Exception, e:
log.exception(e)
log.error("FATAL: invalid op transfer sequence")
os.abort()
# what was the previous owner?
recipient = str(name_rec['sender'])
recipient_address = str(name_rec['address'])
# when was the NAME_TRANSFER sent?
if not name_rec.has_key('transfer_send_block_id'):
log.error("FATAL: Obsolete database: no 'transfer_send_block_id' defined")
os.abort()
transfer_send_block_id = name_rec['transfer_send_block_id']
if transfer_send_block_id is None:
log.error("FATAL: no transfer-send block ID set")
os.abort()
# restore history temporarily...
name_rec_prev = BlockstackDB.get_previous_name_version( name_rec, block_number, history_index, untrusted_db )
sender = name_rec_prev['sender']
address = name_rec_prev['address']
consensus_hash = untrusted_db.get_consensus_at( transfer_send_block_id )
name_rec_script = build_transfer( str(name_rec['name']), keep_data, consensus_hash )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse( name_rec_payload, recipient )
# reconstruct recipient and sender
ret_op['recipient'] = recipient
ret_op['recipient_address'] = recipient_address
ret_op['sender'] = sender
ret_op['address'] = address
ret_op['keep_data'] = keep_data
if consensus_hash is not None:
# only set if we have it; otherwise use the one that's in the name record
# that this delta will be applied over
ret_op['consensus_hash'] = consensus_hash
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Given a name record most recently affected by an instance of this operation,
find the dict of consensus-affecting fields from the operation that are not
already present in the name record.
Specific to NAME_TRANSFER:
The consensus hash is a field that we snapshot when we discover the transfer,
but it is not a field that we preserve. It will instead be present in the
snapshots database, indexed by the block number in `transfer_send_block_id`.
(This is an artifact of a design quirk of a previous version of the system).
"""
from __init__ import op_commit_consensus_override, op_commit_consensus_get_overrides
from ..nameset import BlockstackDB
ret_op = {}
# reconstruct the recipient information
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
# reconstruct name_hash, consensus_hash, keep_data
keep_data = None
try:
assert len(name_rec['op']) == 2, "Invalid op sequence '%s'" % (name_rec['op'])
if name_rec['op'][-1] == TRANSFER_KEEP_DATA:
keep_data = True
elif name_rec['op'][-1] == TRANSFER_REMOVE_DATA:
keep_data = False
else:
raise Exception("Invalid op sequence '%s'" % (name_rec['op']))
except Exception, e:
log.exception(e)
log.error("FATAL: invalid transfer op sequence")
os.abort()
ret_op['keep_data'] = keep_data
ret_op['name_hash128'] = hash256_trunc128( str(name_rec['name']) )
ret_op['sender_pubkey'] = None
if blockchain_name_data is None:
consensus_hash = find_last_transfer_consensus_hash( name_rec, block_id, name_rec['vtxindex'] )
ret_op['consensus_hash'] = consensus_hash
else:
ret_op['consensus_hash'] = blockchain_name_data['consensus_hash']
if ret_op['consensus_hash'] is None:
# no prior consensus hash; must be the one in the name operation itself
ret_op['consensus_hash'] = db.get_consensus_at( name_rec['transfer_send_block_id'] )
# 'consensus_hash' will be different than what we recorded in the db
op_commit_consensus_override( ret_op, 'consensus_hash' )
return ret_op

View File

@@ -21,14 +21,249 @@
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""
from ..b40 import *
from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *
from binascii import hexlify, unhexlify
import virtualchain
log = virtualchain.get_logger("blockstack-server")
from ..nameset import NAMEREC_FIELDS
from blockstack_client.operations import *
# consensus hash fields (ORDER MATTERS!)
FIELDS = NAMEREC_FIELDS + [
'name_hash', # hash(name,consensus_hash)
FIELDS = NAMEREC_FIELDS[:] + [
'name_consensus_hash', # hash(name,consensus_hash)
'consensus_hash' # consensus hash when this update was sent
]
# fields this operation mutates
MUTATE_FIELDS = NAMEREC_MUTATE_FIELDS[:] + [
'value_hash',
'consensus_hash'
]
# fields to back up when applying this operation
BACKUP_FIELDS = NAMEREC_NAME_BACKUP_FIELDS[:] + MUTATE_FIELDS[:]
def update_sanity_test( name, consensus_hash, data_hash ):
"""
Verify the validity of an update's data
Return True if valid
Raise exception if not
"""
if name is not None and (not is_b40( name ) or "+" in name or name.count(".") > 1):
raise Exception("Name '%s' has non-base-38 characters" % name)
if data_hash is not None and not is_hex( data_hash ):
raise Exception("Invalid hex string '%s': not hex" % (data_hash))
if len(data_hash) != 2 * LENGTHS['value_hash']:
raise Exception("Invalid hex string '%s': bad length" % (data_hash))
return True
@state_transition("name", "name_records")
def check(state_engine, nameop, block_id, checked_ops ):
"""
Verify the validity of an update to a name's associated data.
Use the nameop's 128-bit name hash to find the name itself.
NAME_UPDATE isn't allowed during an import, so the name's namespace must be ready.
Return True if accepted
Return False if not.
"""
name_consensus_hash = nameop['name_consensus_hash']
sender = nameop['sender']
# deny updates if we exceed quota--the only legal operations are to revoke or transfer.
sender_names = state_engine.get_names_owned_by_sender( sender )
if len(sender_names) > MAX_NAMES_PER_SENDER:
log.debug("Sender '%s' has exceeded quota: only transfers or revokes are allowed" % (sender))
return False
name, consensus_hash = state_engine.get_name_from_name_consensus_hash( name_consensus_hash, sender, block_id )
# name must exist
if name is None or consensus_hash is None:
log.debug("Unable to resolve name consensus hash '%s' to a name owned by '%s'" % (name_consensus_hash, sender))
# nothing to do--write is stale or on a fork
return False
namespace_id = get_namespace_from_name( name )
name_rec = state_engine.get_name( name )
if name_rec is None:
log.debug("Name '%s' does not exist" % name)
return False
# namespace must be ready
if not state_engine.is_namespace_ready( namespace_id ):
# non-existent namespace
log.debug("Namespace '%s' is not ready" % (namespace_id))
return False
# name must not be revoked
if state_engine.is_name_revoked( name ):
log.debug("Name '%s' is revoked" % name)
return False
# name must not be expired
if state_engine.is_name_expired( name, state_engine.lastblock ):
log.debug("Name '%s' is expired" % name)
return False
# the name must be registered
if not state_engine.is_name_registered( name ):
# doesn't exist
log.debug("Name '%s' is not registered" % name )
return False
# the name must be owned by the same person who sent this nameop
if not state_engine.is_name_owner( name, sender ):
# wrong owner
log.debug("Name '%s' is not owned by '%s'" % (name, sender))
return False
# remember the name and consensus hash, so we don't have to re-calculate it...
nameop['name'] = name
nameop['consensus_hash'] = consensus_hash
nameop['sender_pubkey'] = name_rec['sender_pubkey']
# not stored, but re-calculateable
del nameop['name_consensus_hash']
return True
def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
"""
Extract and return a dict of fields from the underlying blockchain transaction data
that are useful to this operation.
Required (+ parse):
sender: the script_pubkey (as a hex string) of the principal that sent the name preorder transaction
address: the address from the sender script
Optional:
sender_pubkey_hex: the public key of the sender
"""
sender_script = None
sender_address = None
sender_pubkey_hex = None
try:
# by construction, the first input comes from the principal
# who sent the registration transaction...
assert len(senders) > 0
assert 'script_pubkey' in senders[0].keys()
assert 'addresses' in senders[0].keys()
sender_script = str(senders[0]['script_pubkey'])
sender_address = str(senders[0]['addresses'][0])
assert sender_script is not None
assert sender_address is not None
if str(senders[0]['script_type']) == 'pubkeyhash':
sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )
except Exception, e:
log.exception(e)
raise Exception("Failed to extract")
parsed_payload = parse( payload )
assert parsed_payload is not None
ret = {
"sender": sender_script,
"address": sender_address,
"vtxindex": vtxindex,
"txid": txid,
"op": NAME_UPDATE
}
ret.update( parsed_payload )
if sender_pubkey_hex is not None:
ret['sender_pubkey'] = sender_pubkey_hex
return ret
def parse(bin_payload):
"""
Parse a payload to get back the name and update hash.
NOTE: bin_payload excludes the leading three bytes.
"""
if len(bin_payload) != LENGTHS['name_consensus_hash'] + LENGTHS['value_hash']:
log.error("Invalid update length %s" % len(bin_payload))
return None
name_consensus_hash_bin = bin_payload[:LENGTHS['name_consensus_hash']]
value_hash_bin = bin_payload[LENGTHS['name_consensus_hash']:]
name_consensus_hash = hexlify( name_consensus_hash_bin )
value_hash = hexlify( value_hash_bin )
try:
rc = update_sanity_test( None, name_consensus_hash, value_hash )
if not rc:
raise Exception("Invalid update data")
except Exception, e:
log.error("Invalid update data")
return None
return {
'opcode': 'NAME_UPDATE',
'name_consensus_hash': name_consensus_hash,
'value_hash': value_hash
}
def restore_delta( name_rec, block_number, history_index, untrusted_db ):
"""
Find the fields in a name record that were changed by an instance of this operation, at the
given (block_number, history_index) point in time in the past. The history_index is the
index into the list of changes for this name record in the given block.
Return the fields that were modified on success.
Return None on error.
"""
data_hash = None
if name_rec['value_hash'] is not None:
data_hash = str(name_rec['value_hash'])
name_rec_script = build_update( str(name_rec['name']), str(name_rec['consensus_hash']), data_hash=data_hash )
name_rec_payload = unhexlify( name_rec_script )[3:]
ret_op = parse(name_rec_payload)
return ret_op
def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
"""
Given a name record most recently affected by an instance of this operation,
find the dict of consensus-affecting fields from the operation that are not
already present in the name record.
"""
ret_op = {}
# reconstruct name_hash
ret_op['name_consensus_hash'] = hash256_trunc128( str(name_rec['name']) + str(name_rec['consensus_hash']) )
return ret_op