diff --git a/blockstore/lib/nameset/namedb.py b/blockstore/lib/nameset/namedb.py index 026027a28..8ba1bf16e 100644 --- a/blockstore/lib/nameset/namedb.py +++ b/blockstore/lib/nameset/namedb.py @@ -3,18 +3,34 @@ """ Blockstore ~~~~~ - :copyright: (c) 2015 by Openname.org - :license: MIT, see LICENSE for more details. + copyright: (c) 2014 by Halfmoon Labs, Inc. + copyright: (c) 2015 by Blockstack.org + + This file is part of Blockstore + + Blockstore 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. + + Blockstore 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 Blockstore. If not, see . """ import json import traceback import binascii import hashlib +import math from collections import defaultdict -from ..config import NAMESPACE_DEFAULT, MIN_OP_LENGTHS, OPCODES, MAGIC_BYTES, TESTSET +from ..config import NAMESPACE_DEFAULT, MIN_OP_LENGTHS, OPCODES, MAGIC_BYTES, TESTSET, MAX_NAMES_PER_SENDER, EXPIRATION_PERIOD, NAME_PREORDER, NAMESPACE_PREORDER from ..operations import build_namespace_define +from ..hashing import * import virtualchain log = virtualchain.session.log @@ -66,14 +82,20 @@ class BlockstoreDBIterator(object): Serialize a name record: make it sortable on the name (names are unique, so this imposes a total order on the set of serialized name records), and include the - owner's script_pubkey and profile hash. + owner's script_pubkey, profile hash, and revocation status. """ profile_hash = name_record.get('value_hash', "") + revoked_string = "" + if profile_hash is None: profile_hash = "" + + if name_record['revoked']: + revoked_string = "-revoked" - name_string = (name + name_record['sender'] + value_hash).encode('utf8') + name_string = (name + name_record['sender'] + profile_hash + revoked_string).encode('utf8') + return name_string @@ -97,8 +119,6 @@ class BlockstoreDBIterator(object): try: namespace_string = (namespace_id + rules_string + sender).encode('utf8') except Exception, e: - - print "\n%s\n" % namespace_record raise e return namespace_string @@ -158,6 +178,8 @@ class BlockstoreDBIterator(object): self.next_name += 1 serialized_name_record = self.serialize_name_record( name, self.db.get_name( name ) ) + + log.debug(" Serialized name record: '%s' (%s)" % (serialized_name_record, name) ) return serialized_name_record # out of names @@ -182,6 +204,8 @@ class BlockstoreDBIterator(object): self.next_namespace += 1 serialized_namespace_record = self.serialize_namespace_record( namespace_id, self.db.get_namespace( namespace_id ) ) + + log.debug(" Serialized namespace record: '%s' (%s)" % (serialized_namespace_record, namespace_id) ) return serialized_namespace_record # out of namespaces @@ -206,6 +230,8 @@ class BlockstoreDBIterator(object): self.next_import_namespace += 1 serialized_importing_namespace = self.serialize_importing_namespace( namespace_id_hash, self.db.get_importing_namespace_raw( namespace_id_hash ) ) + + log.debug(" Serialized import record: '%s' (%s)" % (serialized_importing_namespace, namespace_id_hash) ) return serialized_importing_namespace # out of importing namespaces @@ -264,17 +290,20 @@ class BlockstoreDB( virtualchain.StateEngine ): # "first_registered": block when registered, # "last_renewed": block when last renewed, # "address": bitcoin public key of the sender - # "value_hash": hex string of hash of profile JSON } + # "value_hash": hex string of hash of profile JSON + # "revoked": True if this name was revoked; False if not} self.owner_names = defaultdict(list) # map sender_script_pubkey hex string to list of names owned by the principal it represents + self.hash_names = {} # map hex_hash160(name) to name self.preorders = {} # map preorder name.ns_id+script_pubkey hash (as a hex string) to its first "preorder" nameop self.namespaces = {} # map namespace ID to first instance of NAMESPACE_DEFINE op (a dict) combined with the namespace name and sender script_pubkey - self.namespace_preorders = {} # map hash(ns_id+script_pubkey) hex string to NAMESPACE_PREORDER op self.pending_imports = {} # map an in-progress namespace import (as the hex string of ns_id+script_pubkey hash) to a list of nameops. # The first element is always the NAMESPACE_PREORDER nameop. # The second element is always the NAMESPACE_DEFINE nameop. - self.block_name_registers = defaultdict(list) # map a block ID to the list of names that were registered at that block. Used to find expired names. + self.block_name_renewals = defaultdict(list) # map a block ID to the list of names that were renewed at that block. Used to find expired names. + + self.name_consensus_hash_name = {} # temporary table for mapping the hash(name + consensus_hash) in an update to its name # default namespace (empty string) self.namespaces[""] = NAMESPACE_DEFAULT @@ -292,8 +321,10 @@ class BlockstoreDB( virtualchain.StateEngine ): if 'namespaces' in db_dict: self.namespaces = db_dict['namespaces'] - if 'namespace_preorders' in db_dict: - self.namespace_preorders = db_dict['namespace_preorders'] + # translate empty namesapce ID from json-ized format + if "null" in self.namespaces.keys(): + self.namespaces[None] = self.namespaces["null"] + del self.namespaces["null"] if 'pending_imports' in db_dict: self.pending_imports = db_dict['pending_imports'] @@ -304,6 +335,15 @@ class BlockstoreDB( virtualchain.StateEngine ): except Exception as e: pass + # build up our reverse indexes + for name, name_record in self.name_records.items(): + self.block_name_renewals[ name_record['last_renewed'] ] = name + self.owner_names[ name_record['sender'] ].append( name ) + self.hash_names[ hash256_trunc128( name ) ] = name + + # load up consensus hash for this block + self.snapshot( self.lastblock ) + def save_db(self, filename): """ @@ -319,8 +359,7 @@ class BlockstoreDB( virtualchain.StateEngine ): 'registrations': self.name_records, 'preorders': self.preorders, 'namespaces': self.namespaces, - 'namespace_preorders': self.namespace_preorders, - 'pending_imports': self.pending_imports + 'pending_imports': self.pending_imports, } f.write(json.dumps(db_dict)) @@ -397,6 +436,7 @@ class BlockstoreDB( virtualchain.StateEngine ): names = self.owner_names.get( sender, None ) if names is None: + # invalid name owner return None @@ -404,7 +444,8 @@ class BlockstoreDB( virtualchain.StateEngine ): for i in xrange( block_id - virtualchain.config.BLOCKS_CONSENSUS_HASH_IS_VALID, block_id ): consensus_hash = self.get_consensus_at( i ) - possible_consensus_hashes.append( consensus_hash ) + if consensus_hash is not None: + possible_consensus_hashes.append( consensus_hash ) for name in names: for consensus_hash in possible_consensus_hashes: @@ -492,14 +533,13 @@ class BlockstoreDB( virtualchain.StateEngine ): return self.pending_imports[ namespace_id_hash ] - def find_expires_at( self, block_id ): + def find_renewed_at( self, block_id ): """ - Given a block ID, find all names that will have expired exactly - EXPIRATION_PERIOD blocks ago. + Find all names registered at a particular block. Returns a list of names on success (which can be empty) """ - return self.block_name_registers.get( block_id, [] ) + return self.block_name_renewals.get( block_id, [] ) def is_name_registered(self, name): @@ -528,10 +568,10 @@ class BlockstoreDB( virtualchain.StateEngine ): Return False if not. """ - if namespace_id_hash not in self.namespace_preorders.keys(): + namespace_preorder = self.get_importing_namespace_preorder( namespace_id_hash ) + if namespace_preorder is None: return False - namespace_preorder = self.get_importing_namespace_preorder( namespace_id_hash ) if namespace_preorder['sender'] != sender_script_pubkey: return False @@ -658,13 +698,14 @@ class BlockstoreDB( virtualchain.StateEngine ): Return True if so. Return False if not. """ - return (namespace_id_hash not in self.namespace_preorders.keys()) + return (self.get_importing_namespace_preorder(namespace_id_hash) is None) def is_name_imported( self, name, sender_script_pubkey ): """ Given a name and a sender script_pubkey hex string, determine if the given name is part of a namespace import. The name must have been sent by the same sender who sent the NAMESPACE_DEFINE + Return True if so Return False if not. """ @@ -678,6 +719,22 @@ class BlockstoreDB( virtualchain.StateEngine ): return self.is_namespace_importing_hash( namespace_id_hash ) + + def is_name_revoked( self, name ): + """ + Given a name, determine whether or not it was revoked. + If the name was not registered in the first place, then it was not revoked. + + Return True if so + Return False if not + """ + + if name not in self.name_records.keys(): + # doesn't exist, so not revoked + return False + + return self.name_records[name]['revoked'] + def is_name_expired_at( self, name, block_id ): """ @@ -689,7 +746,7 @@ class BlockstoreDB( virtualchain.StateEngine ): """ expiring_block_number = block_id - EXPIRATION_PERIOD - names_expiring = self.find_expires_at( expiring_block_number ) + names_expiring = self.find_renewed_at( expiring_block_number ) return (name in names_expiring) @@ -698,7 +755,8 @@ class BlockstoreDB( virtualchain.StateEngine ): Remove a name that has expired. """ - name_hash128 = hash256_trunc128( name ) + name_hash = hash256_trunc128( name ) + owner = None if not self.name_records.has_key( name ): @@ -711,6 +769,9 @@ class BlockstoreDB( virtualchain.StateEngine ): if self.owner_names.has_key( owner ): del self.owner_names[ owner ] + if self.hash_names.has_key( name_hash ): + del self.hash_names[ name_hash ] + def commit_name_expire_all( self, block_id ): """ @@ -718,23 +779,14 @@ class BlockstoreDB( virtualchain.StateEngine ): exactly EXPIRATION_PERIOD blocks ago. """ - expired_names = self.find_expires_at( block_id ) + expiring_block_number = block_id - EXPIRATION_PERIOD + expired_names = self.find_renewed_at( expiring_block_number ) for name in expired_names: self.commit_name_expire( name ) - - def commit_remove_namespace_preorder( self, namespace_id, script_pubkey ): - """ - Given the namespace ID and a script_pubkey hex string, - remove the namespace preorder. - """ - try: - namespace_id_hash = hash_name(namespace_id, script_pubkey) - except ValueError: - return None - else: - del self.namespace_preorders[namespace_id_hash] + if expiring_block_number in self.block_name_renewals.keys(): + del self.block_name_renewals[ expiring_block_number ] def commit_remove_preorder( self, name, script_pubkey ): @@ -794,20 +846,30 @@ class BlockstoreDB( virtualchain.StateEngine ): self.commit_namespace_import( nameop, current_block_number ) else: - # registered! - self.commit_remove_preorder( name, sender ) - name_record = { - 'value_hash': None, # i.e. the hex hash of profile data in immutable storage. - 'sender': str(sender), - 'first_registered': current_block_number, - 'last_renewed': current_block_number, - 'address': address - } - - self.name_records[ name ] = name_record - self.block_expirations[ current_block_number ].append( name ) - self.owner_names[ sender ].append( str(sender) ) + # is this a renewal? + if self.is_name_registered( name ): + + self.commit_renewal( nameop, current_block_number ) + + else: + + # registered! + self.commit_remove_preorder( name, sender ) + + name_record = { + 'value_hash': None, # i.e. the hex hash of profile data in immutable storage. + 'sender': str(sender), + 'first_registered': current_block_number, + 'last_renewed': current_block_number, + 'address': address, + 'revoked': False + } + + self.name_records[ name ] = name_record + self.owner_names[ sender ].append( str(name) ) + self.hash_names[ hash256_trunc128( name ) ] = name + self.block_name_renewals[ current_block_number ].append( name ) def commit_renewal( self, nameop, current_block_number ): @@ -819,8 +881,8 @@ class BlockstoreDB( virtualchain.StateEngine ): block_last_renewed = self.name_records[name]['last_renewed'] # name no longer expires at last renewal time... - self.block_expirations[ block_last_renewed ].remove( name ) - self.block_expirations[ current_block_number ].append( name ) + self.block_name_renewals[ block_last_renewed ].remove( name ) + self.block_name_renewals[ current_block_number ].append( name ) self.name_records[name]['last_renewed'] = current_block_number @@ -832,6 +894,14 @@ class BlockstoreDB( virtualchain.StateEngine ): """ sender = nameop['sender'] + name_consensus_hash = nameop['name_hash'] + + try: + name = nameop['name'] + except: + print "\n\nnameop: %s\n\n" % nameop + name = self.name_consensus_hash_name[ name_consensus_hash ] + del self.name_consensus_hash_name[ name_consensus_hash ] if self.is_name_imported( name, sender ): # this is part of a namespace import @@ -839,7 +909,6 @@ class BlockstoreDB( virtualchain.StateEngine ): else: - name = nameop['name'] self.name_records[name]['value_hash'] = nameop['update_hash'] @@ -847,19 +916,30 @@ class BlockstoreDB( virtualchain.StateEngine ): """ Commit a transfer--update the name record to indicate the recipient of the transaction as the new owner. - - TODO: blow away previous owner's data, if requested """ + name = nameop['name'] owner = nameop['sender'] recipient = nameop['recipient'] + keep_data = nameop['keep_data'] self.name_records[name]['sender'] = recipient self.owner_names[owner].remove( name ) self.owner_names[recipient].append( name ) + if not keep_data: + self.name_records[name]['value_hash'] = None + + def commit_revoke( self, nameop, current_block_number ): + """ + Commit a revocation--update the name record. + """ + + name = nameop['name'] + self.name_records[name]['revoked'] = True + def commit_namespace_preorder( self, nameop, block_number ): """ @@ -873,8 +953,6 @@ class BlockstoreDB( virtualchain.StateEngine ): namespace_id_hash = nameop['namespace_id_hash'] sender = nameop['sender'] - self.commit_remove_namespace_preorder( namespace_id_hash, sender ) - # this namespace is preordered, but not yet defined self.pending_imports[ namespace_id_hash ] = [nameop] @@ -970,6 +1048,10 @@ class BlockstoreDB( virtualchain.StateEngine ): preorder_name_hash = nameop['preorder_name_hash'] consensus_hash = nameop['consensus_hash'] + for pending_preorders in pending_nameops[ NAME_PREORDER ]: + if pending_preorders['preorder_name_hash'] == preorder_name_hash: + return False + if self.is_new_preorder(preorder_name_hash) and self.is_consensus_hash_valid( block_id, consensus_hash ): # new hash and right consensus return True @@ -994,7 +1076,7 @@ class BlockstoreDB( virtualchain.StateEngine ): sender = nameop['sender'] namespace_id = get_namespace_from_name( name ) - if self.log_namespace_import( nameop, block_id ): + if self.log_namespace_import( pending_nameops, nameop, block_id ): # this registration is part of a namespace import # will add to the namespace's list of imports. return True @@ -1007,11 +1089,11 @@ class BlockstoreDB( virtualchain.StateEngine ): return False # sender exceeded maximum number of names? - if len( self.owner_names.get( name, [] ) ) >= config.MAX_NAMES_PER_SENDER: + if len( self.owner_names.get( name, [] ) ) >= MAX_NAMES_PER_SENDER: return False - namespace_base_price = nameop['cost'] - namespace_price_decay = nameop['price_decay'] + namespace_base_price = namespace['cost'] + namespace_price_decay = namespace['price_decay'] # is this registration valid? if not self.is_name_registered( name ) and self.has_preordered_name( name, sender ) and is_mining_fee_sufficient( name, nameop['fee'], namespace_base_price, namespace_price_decay ): @@ -1049,16 +1131,25 @@ class BlockstoreDB( virtualchain.StateEngine ): # nothing to do--write is stale or on a fork return False - # remember the name, so we don't have to re-calculate it + # remember the name, so we don't have to re-calculate it (either in log_namespace_import, or in commit_update) + self.name_consensus_hash_name[ name_consensus_hash ] = name nameop['name'] = name - if self.log_namespace_import( nameop, block_id ): + if self.log_namespace_import( pending_nameops, nameop, block_id ): # this update is part of a namespace import # will add to the namespace's list of imports. return True else: + if not self.is_name_registered( name ): + # doesn't exist + return False + + if self.is_name_revoked( name ): + # revoked + return False + if self.is_name_owner( name, sender ): # update is sent by the owner of the name, so accept @@ -1071,7 +1162,7 @@ class BlockstoreDB( virtualchain.StateEngine ): def log_transfer( self, pending_nameops, nameop, block_id ): """ Log a name's transferrance to another private key. - The name must exist and be owned by the sender. + 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. @@ -1079,10 +1170,21 @@ class BlockstoreDB( virtualchain.StateEngine ): Return False if not """ - name = nameop['name'] + name_hash = nameop['name_hash'] + name = self.hash_names.get( name_hash ) + + if name is None: + # invalid + return False + + consensus_hash = nameop['consensus_hash'] sender = nameop['sender'] recipient = nameop['recipient'] + if not self.is_consensus_hash_valid( consensus_hash, block_id ): + # nope + return False + if sender == recipient: # nonsensical return False @@ -1093,6 +1195,10 @@ class BlockstoreDB( virtualchain.StateEngine ): if not self.is_name_owner( name, sender ): return False + if self.is_name_revoked( name ): + # name exists and was revoked + return False + if recipient in self.owner_names.keys(): # recipient already has names... @@ -1108,6 +1214,38 @@ class BlockstoreDB( virtualchain.StateEngine ): return True + def log_revoke( self, pending_nameops, nameop, block_id ): + """ + Revoke a name. It will still exist, but no future + updates or transfers on it will be accepted. + The sender must own the name, and the name must be registered. + + Return True if accepted + Return False if not + """ + + name = nameop['name'] + sender = nameop['sender'] + + if self.log_namespace_import( pending_nameops, nameop, block_id ): + # this update is part of a namespace import + # will add to the namespace's list of imports. + return True + + if not self.is_name_registered( name ): + return False + + if not self.is_name_owner( name, sender ): + return False + + if self.is_name_revoked( name ): + # name was already revoked + return False + + return True + + + def log_namespace_preorder( self, pending_nameops, nameop, block_id ): """ Given a NAMESPACE_PREORDER nameop, see if we can preorder it. @@ -1120,7 +1258,12 @@ class BlockstoreDB( virtualchain.StateEngine ): namespace_id_hash = nameop['namespace_id_hash'] consensus_hash = nameop['consensus_hash'] - if self.is_new_namespace_preorder(preorder_name_hash) and self.is_consensus_hash_valid( block_id, consensus_hash ): + # block duplicate preorders + for pending_namespace_preorder in pending_nameops[ NAMESPACE_PREORDER ]: + if pending_namespace_preorder['namespace_id_hash'] == namespace_id_hash: + return False + + if self.is_new_namespace_preorder(namespace_id_hash) and self.is_consensus_hash_valid( block_id, consensus_hash ): # new hash and right consensus return True @@ -1240,13 +1383,13 @@ def price_name( name, namespace_base_price, namespace_decay ): price = float(namespace_base_price) # adjust the price by a factor X for every character beyond the first - price = ceil( price / (namespace_decay**(len(name)-1)) ) + price = math.ceil( price / (namespace_decay**(len(name)-1)) ) # price cannot be lower than 1 satoshi if price < 1: price = 1 - return price + return int(price) def is_mining_fee_sufficient( name, mining_fee, namespace_base_price, namespace_decay ): @@ -1258,5 +1401,5 @@ def is_mining_fee_sufficient( name, mining_fee, namespace_base_price, namespace_ Return False if not. """ - name_price = calculate_name_price(name, namespace_base_price, namespace_decay) + name_price = price_name(name, namespace_base_price, namespace_decay) return (mining_fee >= name_price)