mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-24 03:45:38 +08:00
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:
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user