mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-09 22:37:47 +08:00
445 lines
15 KiB
Python
445 lines
15 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Blockstack
|
|
~~~~~
|
|
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
|
|
copyright: (c) 2016 by Blockstack.org
|
|
|
|
This file is part of Blockstack
|
|
|
|
Blockstack is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Blockstack is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
You should have received a copy of the GNU General Public License
|
|
along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import keylib
|
|
from binascii import hexlify, unhexlify
|
|
|
|
from ..config import *
|
|
from ..scripts import *
|
|
from ..hashing import *
|
|
from ..nameset import *
|
|
|
|
import blockstack_client
|
|
from blockstack_client.operations import *
|
|
|
|
# consensus hash fields (ORDER MATTERS!)
|
|
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',
|
|
'op_fee',
|
|
'last_creation_op'
|
|
]
|
|
|
|
# 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
|
|
have five outputs: the OP_RETURN, the sender (i.e.
|
|
the namespace owner), the name's recipient, the
|
|
name's update hash, and the burn output.
|
|
This method extracts the name update hash from
|
|
the list of outputs.
|
|
|
|
By construction, the update hash address in
|
|
the NAME_IMPORT operation is the first
|
|
non-OP_RETURN output that is *not* the recipient.
|
|
"""
|
|
|
|
ret = None
|
|
count = 0
|
|
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 is not None and output_hex != recipient:
|
|
|
|
ret = hexlify( keylib.b58check.b58check_decode( str(output_addresses[0]) ) )
|
|
break
|
|
|
|
if ret is None:
|
|
raise Exception("No update hash found")
|
|
|
|
return ret
|
|
|
|
|
|
def get_prev_imported( state_engine, checked_ops, name ):
|
|
"""
|
|
See if a name has been imported previously--either in
|
|
this block, or in the last operation on this name.
|
|
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
|
|
|
|
|
|
def is_earlier_than( nameop1, block_id, vtxindex ):
|
|
"""
|
|
Does nameop1 come before bock_id and vtxindex?
|
|
"""
|
|
return nameop1['block_number'] < block_id or (nameop1['block_number'] == block_id and nameop1['vtxindex'] < vtxindex)
|
|
|
|
|
|
@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 )
|
|
log.debug("preorder_hash = %s (%s, %s, %s)" % (preorder_hash, 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 is_earlier_than( prev_name_rec, block_id, 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']
|
|
|
|
log.debug("use previous preorder_hash = %s" % prev_name_rec['preorder_hash'])
|
|
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','op_fee','last_creation_op'] )
|
|
|
|
# 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, block_id )
|
|
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"
|
|
|
|
# not required for consensus, but for SNV
|
|
nameop['transfer_send_block_id'] = transfer_send_block_id
|
|
nameop['last_creation_op'] = NAME_IMPORT
|
|
|
|
# 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, working_db, 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.
|
|
"""
|
|
return blockstack_client.operations.nameimport.snv_consensus_extras( name_rec, block_id, blockchain_name_data )
|
|
|
|
'''
|
|
ret_op = {}
|
|
|
|
# reconstruct the recipient information
|
|
ret_op['recipient'] = str(name_rec['sender'])
|
|
ret_op['recipient_address'] = str(name_rec['address'])
|
|
|
|
# the preorder hash used is the *first* preorder hash calculated in a series of NAME_IMPORTs
|
|
if name_rec.has_key('preorder_hash'):
|
|
ret_op['preorder_hash'] = name_rec['preorder_hash']
|
|
|
|
else:
|
|
ret_op['preorder_hash'] = hash_name( str(name_rec['name']), name_rec['importer'], ret_op['recipient_address'] )
|
|
|
|
log.debug("restore preorder hash: %s --> %s (%s, %s, %s)" % (name_rec.get('preorder_hash', "None"), ret_op['preorder_hash'], name_rec['name'], name_rec['importer'], ret_op['recipient_address']))
|
|
return ret_op
|
|
'''
|
|
|
|
|